This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 4f4453d32 fix(java): support TreeSet/TreeMap subclasses without 
Comparator constructor in SortedSet/SortedMapSerializer (#3344)
4f4453d32 is described below

commit 4f4453d321ca95a809b744dac9a45260d33c8149
Author: Sebastian Mandrean <[email protected]>
AuthorDate: Tue Feb 17 13:32:35 2026 +0100

    fix(java): support TreeSet/TreeMap subclasses without Comparator 
constructor in SortedSet/SortedMapSerializer (#3344)
    
    ## Why?
    
    `SortedSetSerializer` and `SortedMapSerializer` unconditionally require
    a `(Comparator)` constructor for all `TreeSet`/`TreeMap` subclasses.
    Java constructors are not inherited, so a `class ChildTreeSet extends
    TreeSet<String>` (or `class ChildTreeMap extends TreeMap<String,
    String>`) with only a no-arg constructor throws
    `UnsupportedOperationException` when registered with the corresponding
    serializer.
    
    ## What does this PR do?
    
    Changes both `SortedSetSerializer` and `SortedMapSerializer` to try the
    `(Comparator)` constructor first, and fall back to the no-arg
    constructor if not found. At deserialization time, uses whichever
    constructor is available. The no-arg fallback uses natural ordering,
    which is the common case for `TreeSet`/`TreeMap` subclasses.
    
    Only throws `UnsupportedOperationException` if neither constructor is
    found.
    
    ## Related issues
    
    - Fixes #3343
    
    ## Does this PR introduce any user-facing change?
    
    - [ ] Does this PR introduce any public API change?
    No. Existing behavior is preserved for classes that have a
    `(Comparator)` constructor. Classes that previously threw
    `UnsupportedOperationException` now work.
    
    - [ ] Does this PR introduce any binary protocol compatibility change?
    No. The write path is unchanged (size + comparator ref), and the read
    path still reads the same bytes in the same order. Only the post-read
    instantiation strategy differs (which constructor is used). Wire format
    is identical in both directions.
    
    ## Benchmark
    
    Not applicable; the constructor lookup happens once at serializer
    construction time, not per serialization. The runtime paths
    (`newCollection`/`newMap`) add a single null check (`if
    (comparatorConstructor != null)`) which is negligible.
---
 .../collection/CollectionSerializers.java          | 26 ++++--
 .../fory/serializer/collection/MapSerializers.java | 28 ++++++-
 .../collection/CollectionSerializersTest.java      | 96 ++++++++++++++++++++++
 .../serializer/collection/MapSerializersTest.java  | 93 +++++++++++++++++++++
 4 files changed, 235 insertions(+), 8 deletions(-)

diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
index 25eb5a615..01fff3e02 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
@@ -183,15 +183,23 @@ public class CollectionSerializers {
   }
 
   public static class SortedSetSerializer<T extends SortedSet> extends 
CollectionSerializer<T> {
-    private MethodHandle constructor;
+    private MethodHandle comparatorConstructor;
+    private MethodHandle noArgConstructor;
 
     public SortedSetSerializer(Fory fory, Class<T> cls) {
       super(fory, cls, true);
       if (cls != TreeSet.class) {
         try {
-          constructor = ReflectionUtils.getCtrHandle(cls, Comparator.class);
+          comparatorConstructor = ReflectionUtils.getCtrHandle(cls, 
Comparator.class);
         } catch (Exception e) {
-          throw new UnsupportedOperationException(e);
+          // Subclass doesn't have a (Comparator) constructor, fall back to 
no-arg constructor.
+          try {
+            noArgConstructor = ReflectionUtils.getCtrHandle(cls);
+          } catch (Exception e2) {
+            throw new UnsupportedOperationException(
+                "Class " + cls.getName() + " requires either a (Comparator) or 
no-arg constructor",
+                e2);
+          }
         }
       }
     }
@@ -217,7 +225,11 @@ public class CollectionSerializers {
         collection = (T) new TreeSet(comparator);
       } else {
         try {
-          collection = (T) constructor.invoke(comparator);
+          if (comparatorConstructor != null) {
+            collection = (T) comparatorConstructor.invoke(comparator);
+          } else {
+            collection = (T) noArgConstructor.invoke();
+          }
         } catch (Throwable e) {
           throw new RuntimeException(e);
         }
@@ -234,7 +246,11 @@ public class CollectionSerializers {
         collection = new TreeSet(comparator);
       } else {
         try {
-          collection = (T) constructor.invoke(comparator);
+          if (comparatorConstructor != null) {
+            collection = (T) comparatorConstructor.invoke(comparator);
+          } else {
+            collection = (T) noArgConstructor.invoke();
+          }
         } catch (Throwable e) {
           throw new RuntimeException(e);
         }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
index 90d8469b3..42bd21f6f 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
@@ -19,6 +19,7 @@
 
 package org.apache.fory.serializer.collection;
 
+import java.lang.invoke.MethodHandle;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -113,11 +114,24 @@ public class MapSerializers {
   }
 
   public static class SortedMapSerializer<T extends SortedMap> extends 
MapSerializer<T> {
+    private MethodHandle comparatorConstructor;
+    private MethodHandle noArgConstructor;
 
     public SortedMapSerializer(Fory fory, Class<T> cls) {
       super(fory, cls, true);
       if (cls != TreeMap.class) {
-        this.constructor = ReflectionUtils.getCtrHandle(cls, Comparator.class);
+        try {
+          comparatorConstructor = ReflectionUtils.getCtrHandle(cls, 
Comparator.class);
+        } catch (Exception e) {
+          // Subclass doesn't have a (Comparator) constructor, fall back to 
no-arg constructor.
+          try {
+            noArgConstructor = ReflectionUtils.getCtrHandle(cls);
+          } catch (Exception e2) {
+            throw new UnsupportedOperationException(
+                "Class " + cls.getName() + " requires either a (Comparator) or 
no-arg constructor",
+                e2);
+          }
+        }
       }
     }
 
@@ -141,7 +155,11 @@ public class MapSerializers {
         map = (T) new TreeMap(comparator);
       } else {
         try {
-          map = (T) constructor.invoke(comparator);
+          if (comparatorConstructor != null) {
+            map = (T) comparatorConstructor.invoke(comparator);
+          } else {
+            map = (T) noArgConstructor.invoke();
+          }
         } catch (Throwable e) {
           throw new RuntimeException(e);
         }
@@ -158,7 +176,11 @@ public class MapSerializers {
         map = new TreeMap(comparator);
       } else {
         try {
-          map = (Map) constructor.invoke(comparator);
+          if (comparatorConstructor != null) {
+            map = (Map) comparatorConstructor.invoke(comparator);
+          } else {
+            map = (Map) noArgConstructor.invoke();
+          }
         } catch (Throwable e) {
           throw new RuntimeException(e);
         }
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionSerializersTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionSerializersTest.java
index 12b8d9a4a..bd373ae12 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionSerializersTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionSerializersTest.java
@@ -243,6 +243,102 @@ public class CollectionSerializersTest extends 
ForyTestBase {
     }
   }
 
+  // TreeSet subclass without a Comparator constructor (natural ordering only)
+  public static class ChildTreeSet extends TreeSet<String> {
+    public ChildTreeSet() {
+      super();
+    }
+  }
+
+  // TreeSet subclass with a Comparator constructor
+  public static class ChildTreeSetWithComparator extends TreeSet<String> {
+    public ChildTreeSetWithComparator() {
+      super();
+    }
+
+    public ChildTreeSetWithComparator(Comparator<? super String> comparator) {
+      super(comparator);
+    }
+  }
+
+  @Test(dataProvider = "referenceTrackingConfig")
+  public void testSortedSetSubclassWithoutComparatorCtor(boolean 
referenceTrackingConfig) {
+    Fory fory =
+        Fory.builder()
+            .withLanguage(Language.JAVA)
+            .withRefTracking(referenceTrackingConfig)
+            .requireClassRegistration(false)
+            .build();
+    ChildTreeSet set = new ChildTreeSet();
+    set.add("b");
+    set.add("a");
+    set.add("c");
+    ChildTreeSet deserialized = serDe(fory, set);
+    assertEquals(deserialized, set);
+    assertEquals(deserialized.getClass(), ChildTreeSet.class);
+  }
+
+  @Test(dataProvider = "referenceTrackingConfig")
+  public void testSortedSetSubclassWithComparatorCtor(boolean 
referenceTrackingConfig) {
+    Fory fory =
+        Fory.builder()
+            .withLanguage(Language.JAVA)
+            .withRefTracking(referenceTrackingConfig)
+            .requireClassRegistration(false)
+            .build();
+    ChildTreeSetWithComparator set = new ChildTreeSetWithComparator();
+    set.add("b");
+    set.add("a");
+    set.add("c");
+    ChildTreeSetWithComparator deserialized = serDe(fory, set);
+    assertEquals(deserialized, set);
+    assertEquals(deserialized.getClass(), ChildTreeSetWithComparator.class);
+  }
+
+  @Test(dataProvider = "referenceTrackingConfig")
+  public void testSortedSetSubclassRegisteredWithSortedSetSerializer(
+      boolean referenceTrackingConfig) {
+    Fory fory =
+        Fory.builder()
+            .withLanguage(Language.JAVA)
+            .withRefTracking(referenceTrackingConfig)
+            .requireClassRegistration(false)
+            .build();
+    // Subclass without Comparator constructor should not throw when 
registered with
+    // SortedSetSerializer
+    fory.registerSerializer(
+        ChildTreeSet.class,
+        new CollectionSerializers.SortedSetSerializer<>(fory, 
ChildTreeSet.class));
+    ChildTreeSet set = new ChildTreeSet();
+    set.add("b");
+    set.add("a");
+    set.add("c");
+    ChildTreeSet deserialized = serDe(fory, set);
+    assertEquals(deserialized, set);
+    assertEquals(deserialized.getClass(), ChildTreeSet.class);
+  }
+
+  @Test(dataProvider = "referenceTrackingConfig")
+  public void 
testSortedSetSubclassWithComparatorRegisteredWithSortedSetSerializer(
+      boolean referenceTrackingConfig) {
+    Fory fory =
+        Fory.builder()
+            .withLanguage(Language.JAVA)
+            .withRefTracking(referenceTrackingConfig)
+            .requireClassRegistration(false)
+            .build();
+    fory.registerSerializer(
+        ChildTreeSetWithComparator.class,
+        new CollectionSerializers.SortedSetSerializer<>(fory, 
ChildTreeSetWithComparator.class));
+    ChildTreeSetWithComparator set = new ChildTreeSetWithComparator();
+    set.add("b");
+    set.add("a");
+    set.add("c");
+    ChildTreeSetWithComparator deserialized = serDe(fory, set);
+    assertEquals(deserialized, set);
+    assertEquals(deserialized.getClass(), ChildTreeSetWithComparator.class);
+  }
+
   @Test
   public void testEmptyCollection() {
     serDeCheckSerializer(getJavaFory(), Collections.EMPTY_LIST, 
"EmptyListSerializer");
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/MapSerializersTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/MapSerializersTest.java
index e27b53b73..0719a9e87 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/MapSerializersTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/MapSerializersTest.java
@@ -341,6 +341,99 @@ public class MapSerializersTest extends ForyTestBase {
     copyCheck(fory, beanForMap);
   }
 
+  // TreeMap subclass without a Comparator constructor (natural ordering only)
+  public static class ChildTreeMap extends TreeMap<String, String> {
+    public ChildTreeMap() {
+      super();
+    }
+  }
+
+  // TreeMap subclass with a Comparator constructor
+  public static class ChildTreeMapWithComparator extends TreeMap<String, 
String> {
+    public ChildTreeMapWithComparator() {
+      super();
+    }
+
+    public ChildTreeMapWithComparator(Comparator<? super String> comparator) {
+      super(comparator);
+    }
+  }
+
+  @Test(dataProvider = "referenceTrackingConfig")
+  public void testSortedMapSubclassWithoutComparatorCtor(boolean 
referenceTrackingConfig) {
+    Fory fory =
+        builder()
+            .withLanguage(Language.JAVA)
+            .withRefTracking(referenceTrackingConfig)
+            .requireClassRegistration(false)
+            .build();
+    ChildTreeMap map = new ChildTreeMap();
+    map.put("b", "B");
+    map.put("a", "A");
+    map.put("c", "C");
+    ChildTreeMap deserialized = serDe(fory, map);
+    assertEquals(deserialized, map);
+    assertEquals(deserialized.getClass(), ChildTreeMap.class);
+  }
+
+  @Test(dataProvider = "referenceTrackingConfig")
+  public void testSortedMapSubclassWithComparatorCtor(boolean 
referenceTrackingConfig) {
+    Fory fory =
+        builder()
+            .withLanguage(Language.JAVA)
+            .withRefTracking(referenceTrackingConfig)
+            .requireClassRegistration(false)
+            .build();
+    ChildTreeMapWithComparator map = new ChildTreeMapWithComparator();
+    map.put("b", "B");
+    map.put("a", "A");
+    map.put("c", "C");
+    ChildTreeMapWithComparator deserialized = serDe(fory, map);
+    assertEquals(deserialized, map);
+    assertEquals(deserialized.getClass(), ChildTreeMapWithComparator.class);
+  }
+
+  @Test(dataProvider = "referenceTrackingConfig")
+  public void testSortedMapSubclassRegisteredWithSortedMapSerializer(
+      boolean referenceTrackingConfig) {
+    Fory fory =
+        builder()
+            .withLanguage(Language.JAVA)
+            .withRefTracking(referenceTrackingConfig)
+            .requireClassRegistration(false)
+            .build();
+    fory.registerSerializer(
+        ChildTreeMap.class, new MapSerializers.SortedMapSerializer<>(fory, 
ChildTreeMap.class));
+    ChildTreeMap map = new ChildTreeMap();
+    map.put("b", "B");
+    map.put("a", "A");
+    map.put("c", "C");
+    ChildTreeMap deserialized = serDe(fory, map);
+    assertEquals(deserialized, map);
+    assertEquals(deserialized.getClass(), ChildTreeMap.class);
+  }
+
+  @Test(dataProvider = "referenceTrackingConfig")
+  public void 
testSortedMapSubclassWithComparatorRegisteredWithSortedMapSerializer(
+      boolean referenceTrackingConfig) {
+    Fory fory =
+        builder()
+            .withLanguage(Language.JAVA)
+            .withRefTracking(referenceTrackingConfig)
+            .requireClassRegistration(false)
+            .build();
+    fory.registerSerializer(
+        ChildTreeMapWithComparator.class,
+        new MapSerializers.SortedMapSerializer<>(fory, 
ChildTreeMapWithComparator.class));
+    ChildTreeMapWithComparator map = new ChildTreeMapWithComparator();
+    map.put("b", "B");
+    map.put("a", "A");
+    map.put("c", "C");
+    ChildTreeMapWithComparator deserialized = serDe(fory, map);
+    assertEquals(deserialized, map);
+    assertEquals(deserialized.getClass(), ChildTreeMapWithComparator.class);
+  }
+
   @Test
   public void testEmptyMap() {
     serDeCheckSerializer(getJavaFory(), Collections.EMPTY_MAP, 
"EmptyMapSerializer");


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to