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]