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 9699707e8 fix(java): fix collection get element type bug (#3803)
9699707e8 is described below

commit 9699707e8145d0e51e18e86fe82cbcda5e5e76b7
Author: Pigsy-Monk <[email protected]>
AuthorDate: Wed Jul 1 00:52:00 2026 +0800

    fix(java): fix collection get element type bug (#3803)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    
    Closes #3798
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence or equivalent persisted links of the
    final clean AI review results from both fresh reviewers described in
    `AI_POLICY.md`, the Fory-guided reviewer and the independent general
    reviewer, on the current PR diff or current HEAD after the latest code
    changes.
    
    
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    
    ---------
    
    Co-authored-by: 慕白 <[email protected]>
---
 .../main/java/org/apache/fory/meta/FieldTypes.java |  51 ++---
 .../main/java/org/apache/fory/reflect/TypeRef.java | 224 ++++++++++++++++++---
 .../org/apache/fory/serializer/FieldGroups.java    |   8 -
 .../serializer/collection/MapLikeSerializer.java   |  68 +------
 .../main/java/org/apache/fory/type/ScalaTypes.java |   7 +-
 .../main/java/org/apache/fory/type/TypeUtils.java  |  61 +++---
 .../java/org/apache/fory/reflect/TypeRefTest.java  | 143 +++++++++++++
 .../collection/CollectionSerializersTest.java      | 203 +++++++++++++++++++
 .../serializer/collection/MapSerializersTest.java  | 153 ++++++++++++++
 9 files changed, 747 insertions(+), 171 deletions(-)

diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java 
b/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java
index 6dc1266a2..2f9da98bb 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java
@@ -278,12 +278,9 @@ public class FieldTypes {
           buildFieldType(
               resolver,
               null, // nested fields don't have Field reference
-              genericType.getTypeParameter0() == null
-                  ? GenericType.build(Object.class)
-                  : genericType.getTypeParameter0()));
+              getTypeParameter(genericType, 0)));
     } else if (MAP_TYPE.isSupertypeOf(genericType.getTypeRef())
         || (isXlang && resolver.isMap(rawType))) {
-      Tuple2<TypeRef<?>, TypeRef<?>> mapKeyValueType = 
getMapKeyValueType(genericType);
       return new MapFieldType(
           typeId,
           nullable,
@@ -291,15 +288,11 @@ public class FieldTypes {
           buildFieldType(
               resolver,
               null, // nested fields don't have Field reference
-              mapKeyValueType.f0 == null
-                  ? GenericType.build(Object.class)
-                  : resolver.buildGenericType(mapKeyValueType.f0)),
+              getTypeParameter(genericType, 0)),
           buildFieldType(
               resolver,
               null, // nested fields don't have Field reference
-              mapKeyValueType.f1 == null
-                  ? GenericType.build(Object.class)
-                  : resolver.buildGenericType(mapKeyValueType.f1)));
+              getTypeParameter(genericType, 1)));
     } else if (isUnionType || Union.class.isAssignableFrom(rawType)) {
       return new UnionFieldType(nullable, trackingRef);
     } else if (Types.isEnumType(typeId)) {
@@ -346,16 +339,11 @@ public class FieldTypes {
     }
   }
 
-  private static Tuple2<TypeRef<?>, TypeRef<?>> getMapKeyValueType(GenericType 
genericType) {
-    if (genericType.getTypeParametersCount() >= 2) {
-      return Tuple2.of(
-          genericType.getTypeParameter0().getTypeRef(),
-          genericType.getTypeParameter1().getTypeRef());
+  private static GenericType getTypeParameter(GenericType genericType, int 
index) {
+    if (genericType.getTypeParametersCount() <= index) {
+      return GenericType.build(Object.class);
     }
-    if (!MAP_TYPE.isSupertypeOf(genericType.getTypeRef())) {
-      return Tuple2.of(TypeRef.of(Object.class), TypeRef.of(Object.class));
-    }
-    return TypeUtils.getMapKeyValueType(genericType.getTypeRef());
+    return genericType.getTypeParameters()[index];
   }
 
   private static TypeExtMeta primitiveListInlineMeta(TypeRef<?> typeRef) {
@@ -918,16 +906,13 @@ public class FieldTypes {
         return collectionOf(elementType, TypeExtMeta.of(typeId, nullable, 
trackingRef));
       }
       if (!declaredClass.isArray()) {
-        if (declElementType.equals(elementType)) {
-          return declared;
-        }
         TypeExtMeta extMeta = typeExtMeta(typeId, nullable, trackingRef, 
declared);
-        if (!java.util.Collection.class.isAssignableFrom(declaredClass)
-            && resolver.isCollection(declaredClass)) {
-          return TypeRef.of(
-              declaredClass, extMeta, 
java.util.Collections.singletonList(elementType), null);
+        if (declElementType.equals(elementType)
+            && Objects.equals(declared.getTypeExtMeta(), extMeta)) {
+          return declared;
         }
-        return collectionOf(declaredClass, elementType, extMeta);
+        return TypeRef.of(
+            declared.getType(), extMeta, 
java.util.Collections.singletonList(elementType), null);
       }
       // Build array type from element type
       // elementType could be base type (int) or intermediate array (int[])
@@ -1028,13 +1013,13 @@ public class FieldTypes {
         TypeExtMeta extMeta = typeExtMeta(typeId, nullable, trackingRef, 
declared);
         TypeRef<?> keyTypeRef = keyType.toTypeToken(classResolver, keyDecl);
         TypeRef<?> valueTypeRef = valueType.toTypeToken(classResolver, 
valueDecl);
-        Class<?> declaredClass = declared.getRawType();
-        if (!java.util.Map.class.isAssignableFrom(declaredClass)
-            && classResolver.isMap(declaredClass)) {
-          return TypeRef.of(
-              declaredClass, extMeta, java.util.Arrays.asList(keyTypeRef, 
valueTypeRef), null);
+        if (keyDecl.equals(keyTypeRef)
+            && valueDecl.equals(valueTypeRef)
+            && Objects.equals(declared.getTypeExtMeta(), extMeta)) {
+          return declared;
         }
-        return mapOf(declaredClass, keyTypeRef, valueTypeRef, extMeta);
+        return TypeRef.of(
+            declared.getType(), extMeta, java.util.Arrays.asList(keyTypeRef, 
valueTypeRef), null);
       }
       return mapOf(
           keyType.toTypeToken(classResolver, keyDecl),
diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/TypeRef.java 
b/java/fory-core/src/main/java/org/apache/fory/reflect/TypeRef.java
index cc28b2d5a..407a6ddfa 100644
--- a/java/fory-core/src/main/java/org/apache/fory/reflect/TypeRef.java
+++ b/java/fory-core/src/main/java/org/apache/fory/reflect/TypeRef.java
@@ -14,7 +14,6 @@
 
 package org.apache.fory.reflect;
 
-import java.lang.annotation.Annotation;
 import java.lang.reflect.Array;
 import java.lang.reflect.GenericArrayType;
 import java.lang.reflect.Modifier;
@@ -26,14 +25,18 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import javax.annotation.CheckForNull;
 import org.apache.fory.annotation.Internal;
+import org.apache.fory.collection.Tuple2;
 import org.apache.fory.meta.TypeExtMeta;
+import org.apache.fory.type.ScalaTypes;
 import org.apache.fory.type.TypeUtils;
 
 // Mostly derived from Guava 32.1.2 com.google.common.reflect.TypeToken
@@ -94,8 +97,16 @@ public class TypeRef<T> {
       TypeRef<?> componentType) {
     this.type = type;
     this.typeExtMeta = typeExtMeta;
-    this.typeArguments = typeArguments;
+    this.typeArguments =
+        typeArguments == null
+            ? null
+            : immutableTypeArguments(normalizeContainerTypeArguments(type, 
typeArguments));
     this.componentType = componentType;
+    this.hasTypeExtMeta = hasNestedTypeExtMeta(typeExtMeta, 
this.typeArguments, componentType);
+  }
+
+  private static boolean hasNestedTypeExtMeta(
+      TypeExtMeta typeExtMeta, List<TypeRef<?>> typeArguments, TypeRef<?> 
componentType) {
     boolean hasMeta = typeExtMeta != null;
     if (!hasMeta && typeArguments != null) {
       for (TypeRef<?> typeArg : typeArguments) {
@@ -108,7 +119,7 @@ public class TypeRef<T> {
     if (!hasMeta && componentType != null) {
       hasMeta = componentType.hasTypeExtMeta();
     }
-    this.hasTypeExtMeta = hasMeta;
+    return hasMeta;
   }
 
   /** Returns an instance of type token that wraps {@code type}. */
@@ -130,9 +141,7 @@ public class TypeRef<T> {
       TypeExtMeta typeExtMeta,
       List<TypeRef<?>> typeArguments,
       TypeRef<?> componentType) {
-    List<TypeRef<?>> explicitTypeArguments =
-        typeArguments == null ? null : Collections.unmodifiableList(new 
ArrayList<>(typeArguments));
-    return new TypeRef<>(type, typeExtMeta, explicitTypeArguments, 
componentType);
+    return new TypeRef<>(type, typeExtMeta, typeArguments, componentType);
   }
 
   @Internal
@@ -140,6 +149,151 @@ public class TypeRef<T> {
     return TypeUseMetadata.typeRef(typeUse);
   }
 
+  private static List<TypeRef<?>> immutableTypeArguments(List<TypeRef<?>> 
typeArguments) {
+    return typeArguments == null
+        ? null
+        : Collections.unmodifiableList(new ArrayList<>(typeArguments));
+  }
+
+  private static List<TypeRef<?>> normalizeContainerTypeArguments(
+      Type type, List<TypeRef<?>> typeArguments) {
+    if (typeArguments.isEmpty()) {
+      return typeArguments;
+    }
+    Class<?> rawType = TypeUtils.getRawType(type);
+    if (isMapLike(rawType)) {
+      return normalizeMapTypeArguments(type, rawType, typeArguments);
+    }
+    if (isIterableLike(rawType)) {
+      return normalizeIterableTypeArguments(type, rawType, typeArguments);
+    }
+    return typeArguments;
+  }
+
+  private static List<TypeRef<?>> normalizeIterableTypeArguments(
+      Type type, Class<?> rawType, List<TypeRef<?>> typeArguments) {
+    if (!hasFullExplicitRawArgs(type, rawType, typeArguments)) {
+      return typeArguments;
+    }
+    return Collections.singletonList(
+        resolveTypeVariables(
+            rawIterableElementType(rawType).getType(),
+            explicitTypeVarRefs(rawType, typeArguments)));
+  }
+
+  private static List<TypeRef<?>> normalizeMapTypeArguments(
+      Type type, Class<?> rawType, List<TypeRef<?>> typeArguments) {
+    if (!hasFullExplicitRawArgs(type, rawType, typeArguments)) {
+      return typeArguments;
+    }
+    Tuple2<TypeRef<?>, TypeRef<?>> keyValueType = rawMapKeyValueTypes(rawType);
+    Map<TypeVariableKey, TypeRef<?>> typeVarRefs = 
explicitTypeVarRefs(rawType, typeArguments);
+    return Arrays.asList(
+        resolveTypeVariables(keyValueType.f0.getType(), typeVarRefs),
+        resolveTypeVariables(keyValueType.f1.getType(), typeVarRefs));
+  }
+
+  private static boolean hasFullExplicitRawArgs(
+      Type type, Class<?> rawType, List<TypeRef<?>> typeArguments) {
+    return type instanceof ParameterizedType
+        && ((ParameterizedType) type).getActualTypeArguments().length == 
typeArguments.size()
+        && rawType.getTypeParameters().length == typeArguments.size();
+  }
+
+  private static Map<TypeVariableKey, TypeRef<?>> explicitTypeVarRefs(
+      Class<?> rawType, List<TypeRef<?>> typeArguments) {
+    TypeVariable<?>[] variables = rawType.getTypeParameters();
+    Map<TypeVariableKey, TypeRef<?>> typeVarRefs = new HashMap<>();
+    for (int i = 0; i < variables.length; i++) {
+      typeVarRefs.put(new TypeVariableKey(variables[i]), typeArguments.get(i));
+    }
+    return typeVarRefs;
+  }
+
+  private static TypeRef<?> rawIterableElementType(Class<?> rawType) {
+    if (isScalaIterable(rawType)) {
+      @SuppressWarnings({"rawtypes", "unchecked"})
+      TypeRef<?> iterableType =
+          ((TypeRef) TypeRef.of(rawType)).getSupertype((Class) 
ScalaTypes.getScalaIterableType());
+      return iterableType
+          .resolveType(ScalaTypes.getScalaIteratorReturnType())
+          .resolveType(ScalaTypes.getScalaNextReturnType());
+    }
+    @SuppressWarnings("unchecked")
+    TypeRef<?> iterableType =
+        ((TypeRef<? extends Iterable<?>>) 
TypeRef.of(rawType)).getSupertype(Iterable.class);
+    return iterableType.resolveType(Iterable.class.getTypeParameters()[0]);
+  }
+
+  private static Tuple2<TypeRef<?>, TypeRef<?>> rawMapKeyValueTypes(Class<?> 
rawType) {
+    if (isScalaMap(rawType)) {
+      TypeRef<?> kvTupleType = rawIterableElementType(rawType);
+      ParameterizedType type = (ParameterizedType) kvTupleType.getType();
+      Type[] types = type.getActualTypeArguments();
+      return Tuple2.of(TypeRef.of(types[0]), TypeRef.of(types[1]));
+    }
+    @SuppressWarnings("unchecked")
+    TypeRef<?> mapType =
+        ((TypeRef<? extends Map<?, ?>>) 
TypeRef.of(rawType)).getSupertype(Map.class);
+    TypeVariable<?>[] typeParameters = Map.class.getTypeParameters();
+    return Tuple2.of(
+        mapType.resolveType(typeParameters[0]), 
mapType.resolveType(typeParameters[1]));
+  }
+
+  private static TypeRef<?> resolveTypeVariables(
+      Type type, Map<TypeVariableKey, TypeRef<?>> typeVarRefs) {
+    if (type instanceof TypeVariable) {
+      TypeRef<?> typeRef = typeVarRefs.get(new 
TypeVariableKey((TypeVariable<?>) type));
+      return typeRef == null ? TypeRef.of(type) : typeRef;
+    }
+    if (type instanceof ParameterizedType) {
+      ParameterizedType parameterizedType = (ParameterizedType) type;
+      Type ownerType = parameterizedType.getOwnerType();
+      Type resolvedOwnerType =
+          ownerType == null ? null : resolveTypeVariables(ownerType, 
typeVarRefs).getType();
+      Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
+      List<TypeRef<?>> resolvedArguments = new 
ArrayList<>(actualTypeArguments.length);
+      Type[] resolvedTypes = new Type[actualTypeArguments.length];
+      for (int i = 0; i < actualTypeArguments.length; i++) {
+        TypeRef<?> resolvedType = resolveTypeVariables(actualTypeArguments[i], 
typeVarRefs);
+        resolvedArguments.add(resolvedType);
+        resolvedTypes[i] = resolvedType.getType();
+      }
+      return TypeRef.of(
+          new ParameterizedTypeImpl(
+              resolvedOwnerType, parameterizedType.getRawType(), 
resolvedTypes),
+          null,
+          resolvedArguments,
+          null);
+    }
+    if (type instanceof GenericArrayType) {
+      TypeRef<?> componentType =
+          resolveTypeVariables(((GenericArrayType) 
type).getGenericComponentType(), typeVarRefs);
+      return TypeRef.of(newArrayType(componentType.getType()), null, null, 
componentType);
+    }
+    return TypeRef.of(type);
+  }
+
+  private static boolean isMapLike(Class<?> rawType) {
+    return Map.class.isAssignableFrom(rawType) || isScalaMap(rawType);
+  }
+
+  private static boolean isIterableLike(Class<?> rawType) {
+    return Iterable.class.isAssignableFrom(rawType) || 
isScalaIterable(rawType);
+  }
+
+  private static boolean isScalaMap(Class<?> rawType) {
+    return ScalaTypes.SCALA_AVAILABLE
+        && rawType.getName().startsWith("scala.collection")
+        && ScalaTypes.getScalaMapType().isAssignableFrom(rawType);
+  }
+
+  private static boolean isScalaIterable(Class<?> rawType) {
+    return ScalaTypes.SCALA_AVAILABLE
+        && rawType.getName().startsWith("scala.collection")
+        && ScalaTypes.getScalaIterableType().isAssignableFrom(rawType);
+  }
+
   /** Returns the captured type. */
   private Type capture() {
     final Type superclass = getClass().getGenericSuperclass();
@@ -212,7 +366,7 @@ public class TypeRef<T> {
   }
 
   public boolean hasExplicitTypeArguments() {
-    return typeArguments != null;
+    return typeArguments != null || type instanceof ParameterizedType;
   }
 
   public List<TypeRef<?>> getTypeArguments() {
@@ -220,10 +374,12 @@ public class TypeRef<T> {
       return typeArguments;
     }
     if (type instanceof ParameterizedType) {
-      ParameterizedType parameterizedType = (ParameterizedType) type;
-      return Arrays.stream(parameterizedType.getActualTypeArguments())
-          .map(TypeRef::of)
-          .collect(Collectors.toList());
+      Type[] actualTypeArguments = ((ParameterizedType) 
type).getActualTypeArguments();
+      List<TypeRef<?>> args = new ArrayList<>(actualTypeArguments.length);
+      for (Type actualTypeArgument : actualTypeArguments) {
+        args.add(TypeRef.of(actualTypeArgument));
+      }
+      return immutableTypeArguments(normalizeContainerTypeArguments(type, 
args));
     }
     return new ArrayList<>();
   }
@@ -483,7 +639,6 @@ public class TypeRef<T> {
       Type resolvedRawType = resolveType0(parameterizedType.getRawType(), 
mappings).type;
 
       Type[] args = parameterizedType.getActualTypeArguments();
-
       Type[] resolvedArgs = new Type[args.length];
       for (int i = 0; i < args.length; i++) {
         resolvedArgs[i] = resolveType0(args[i], mappings).type;
@@ -515,15 +670,20 @@ public class TypeRef<T> {
   }
 
   private static void populateTypeMapping(Map<TypeVariableKey, Type> storage, 
Type... types) {
+    populateTypeMapping(storage, new HashSet<>(), types);
+  }
+
+  private static void populateTypeMapping(
+      Map<TypeVariableKey, Type> storage, Set<Class<?>> visitedClasses, 
Type... types) {
     for (Type type : types) {
       if (type == null) {
         continue;
       }
 
       if (type instanceof TypeVariable) {
-        populateTypeMapping(storage, ((TypeVariable<?>) type).getBounds());
+        populateTypeMapping(storage, visitedClasses, ((TypeVariable<?>) 
type).getBounds());
       } else if (type instanceof WildcardType) {
-        populateTypeMapping(storage, ((WildcardType) type).getUpperBounds());
+        populateTypeMapping(storage, visitedClasses, ((WildcardType) 
type).getUpperBounds());
       } else if (type instanceof ParameterizedType) {
         ParameterizedType parameterizedType = (ParameterizedType) type;
 
@@ -554,12 +714,21 @@ public class TypeRef<T> {
           }
           storage.put(key, arg);
         }
-        populateTypeMapping(storage, rawClass);
-        populateTypeMapping(storage, parameterizedType.getOwnerType());
+        // Scala collection traits can form recursive generic supertype 
graphs. The parameterized
+        // occurrence above still contributes its mappings; the raw class 
hierarchy only needs one
+        // walk per resolution.
+        if (visitedClasses.add(rawClass)) {
+          populateTypeMapping(storage, visitedClasses, 
rawClass.getGenericSuperclass());
+          populateTypeMapping(storage, visitedClasses, 
rawClass.getGenericInterfaces());
+        }
+        populateTypeMapping(storage, visitedClasses, 
parameterizedType.getOwnerType());
       } else if (type instanceof Class) {
         Class<?> clazz = (Class<?>) type;
-        populateTypeMapping(storage, clazz.getGenericSuperclass());
-        populateTypeMapping(storage, clazz.getGenericInterfaces());
+        if (!visitedClasses.add(clazz)) {
+          continue;
+        }
+        populateTypeMapping(storage, visitedClasses, 
clazz.getGenericSuperclass());
+        populateTypeMapping(storage, visitedClasses, 
clazz.getGenericInterfaces());
       } else {
         throw new AssertionError("Unknown type: " + type);
       }
@@ -1215,7 +1384,13 @@ public class TypeRef<T> {
 
       LocalClass<String> localClassInstance = new LocalClass<String>() {};
       Class<?> subclass = localClassInstance.getClass();
-      ParameterizedType parameterizedType = (ParameterizedType) 
subclass.getGenericSuperclass();
+      Type genericSuperclass = subclass.getGenericSuperclass();
+      if (!(genericSuperclass instanceof ParameterizedType)) {
+        // Android release minification can strip this probe's Signature 
attribute while leaving
+        // user generic metadata intact. The probe must not make TypeRef 
unusable in that case.
+        return LOCAL_CLASS_HAS_NO_OWNER;
+      }
+      ParameterizedType parameterizedType = (ParameterizedType) 
genericSuperclass;
       for (ClassOwnership behavior : ClassOwnership.values()) {
         if (behavior.getOwnerType(LocalClass.class) == 
parameterizedType.getOwnerType()) {
           return behavior;
@@ -1445,13 +1620,10 @@ public class TypeRef<T> {
 
     @Override
     public int hashCode() {
-      Annotation[] declaredAnnotations = typeVariable.getDeclaredAnnotations();
-      String name = typeVariable.getName();
-
-      int result = 1;
-      result =
-          31 * result + (declaredAnnotations != null ? 
Arrays.hashCode(declaredAnnotations) : 0);
-      result = 31 * result + (name != null ? name.hashCode() : 0);
+      // Match typeVariablesEquals and avoid TypeVariable annotation APIs, 
which are absent on
+      // Android runtimes used by the instrumented tests.
+      int result = typeVariable.getGenericDeclaration().hashCode();
+      result = 31 * result + typeVariable.getName().hashCode();
       return result;
     }
   }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java 
b/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java
index 1337285e9..06e763707 100644
--- a/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java
+++ b/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java
@@ -22,7 +22,6 @@ package org.apache.fory.serializer;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.IdentityHashMap;
 import java.util.List;
@@ -263,13 +262,6 @@ public class FieldGroups {
         t = resolver.buildGenericType(typeRef);
       }
       Class<?> cls = t.getCls();
-      if (t.getTypeParametersCount() > 0) {
-        boolean skip =
-            Arrays.stream(t.getTypeParameters()).allMatch(p -> p.getCls() == 
Object.class);
-        if (skip) {
-          t = new GenericType(t.getTypeRef(), t.isMonomorphic());
-        }
-      }
       genericType = t;
       Field field = descriptor.getField();
       if (field != null) {
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapLikeSerializer.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapLikeSerializer.java
index 334bd8a35..4f6f828d1 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapLikeSerializer.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapLikeSerializer.java
@@ -30,7 +30,6 @@ import static 
org.apache.fory.serializer.collection.MapFlags.TRACKING_KEY_REF;
 import static 
org.apache.fory.serializer.collection.MapFlags.TRACKING_VALUE_REF;
 import static org.apache.fory.serializer.collection.MapFlags.VALUE_DECL_TYPE;
 import static org.apache.fory.serializer.collection.MapFlags.VALUE_HAS_NULL;
-import static org.apache.fory.type.TypeUtils.MAP_TYPE;
 
 import java.lang.invoke.MethodHandle;
 import java.lang.reflect.Constructor;
@@ -38,7 +37,6 @@ import java.util.Iterator;
 import java.util.Map;
 import java.util.Map.Entry;
 import org.apache.fory.annotation.CodegenInvoke;
-import org.apache.fory.collection.Tuple2;
 import org.apache.fory.config.Config;
 import org.apache.fory.context.CopyContext;
 import org.apache.fory.context.ReadContext;
@@ -47,7 +45,6 @@ import org.apache.fory.exception.DeserializationException;
 import org.apache.fory.memory.MemoryBuffer;
 import org.apache.fory.platform.AndroidSupport;
 import org.apache.fory.reflect.ReflectionUtils;
-import org.apache.fory.reflect.TypeRef;
 import org.apache.fory.resolver.RefMode;
 import org.apache.fory.resolver.TypeInfo;
 import org.apache.fory.resolver.TypeInfoHolder;
@@ -55,7 +52,6 @@ import org.apache.fory.resolver.TypeResolver;
 import org.apache.fory.serializer.Serializer;
 import org.apache.fory.type.GenericType;
 import org.apache.fory.type.Generics;
-import org.apache.fory.type.TypeUtils;
 import org.apache.fory.util.Preconditions;
 
 /** Serializer for all map-like objects. */
@@ -68,10 +64,6 @@ public abstract class MapLikeSerializer<T> extends 
Serializer<T> {
     final TypeInfoHolder keyTypeInfoReadCache;
     final TypeInfoHolder valueTypeInfoWriteCache;
     final TypeInfoHolder valueTypeInfoReadCache;
-    GenericType partialGenericKVTypeKey0;
-    GenericType partialGenericKVTypeValue0;
-    GenericType partialGenericKVTypeKey1;
-    GenericType partialGenericKVTypeValue1;
 
     private MapTypeCache(TypeResolver typeResolver) {
       keyTypeInfoWriteCache = typeResolver.nilTypeInfoHolder();
@@ -138,15 +130,12 @@ public abstract class MapLikeSerializer<T> extends 
Serializer<T> {
     Generics generics = writeContext.getGenerics();
     while (entry != null) {
       GenericType genericType = 
generics.nextGenericType(writeContext.getDepth());
-      if (genericType == null) {
+      if (genericType == null || genericType.getTypeParametersCount() < 2) {
         entry = writeJavaNullChunk(writeContext, entry, iterator, null, null);
         if (entry != null) {
           entry = writeJavaChunk(writeContext, classResolver, entry, iterator, 
null, null);
         }
       } else {
-        if (genericType.getTypeParametersCount() < 2) {
-          genericType = getKVGenericType(genericType);
-        }
         GenericType keyGenericType = genericType.getTypeParameter0();
         GenericType valueGenericType = genericType.getTypeParameter1();
         entry =
@@ -445,12 +434,6 @@ public abstract class MapLikeSerializer<T> extends 
Serializer<T> {
       Entry<Object, Object> entry,
       Iterator<Entry<Object, Object>> iterator) {
     MemoryBuffer buffer = writeContext.getBuffer();
-    // type parameters count for `Map field` will be 0;
-    // type parameters count for `SubMap<V> field` which SubMap is
-    // `SubMap<V> implements Map<String, V>` will be 1;
-    if (genericType.getTypeParametersCount() < 2) {
-      genericType = getKVGenericType(genericType);
-    }
     GenericType keyGenericType = genericType.getTypeParameter0();
     GenericType valueGenericType = genericType.getTypeParameter1();
     if (keyGenericType == objType && valueGenericType == objType) {
@@ -538,43 +521,6 @@ public abstract class MapLikeSerializer<T> extends 
Serializer<T> {
     return entry;
   }
 
-  private GenericType getKVGenericType(GenericType genericType) {
-    GenericType mapGenericType = getCachedMapGenericType(genericType);
-    if (mapGenericType == null) {
-      TypeRef<?> typeRef = genericType.getTypeRef();
-      if (!MAP_TYPE.isSupertypeOf(typeRef)) {
-        mapGenericType = GenericType.build(TypeUtils.mapOf(Object.class, 
Object.class));
-      } else {
-        Tuple2<TypeRef<?>, TypeRef<?>> mapKeyValueType = 
TypeUtils.getMapKeyValueType(typeRef);
-        mapGenericType = GenericType.build(TypeUtils.mapOf(mapKeyValueType.f0, 
mapKeyValueType.f1));
-      }
-      cacheMapGenericType(genericType, mapGenericType);
-    }
-    return mapGenericType;
-  }
-
-  private GenericType getCachedMapGenericType(GenericType genericType) {
-    MapTypeCache state = mapTypeCache;
-    if (state == null) {
-      return null;
-    }
-    if (genericType == state.partialGenericKVTypeKey0) {
-      return state.partialGenericKVTypeValue0;
-    }
-    if (genericType == state.partialGenericKVTypeKey1) {
-      return state.partialGenericKVTypeValue1;
-    }
-    return null;
-  }
-
-  private void cacheMapGenericType(GenericType genericType, GenericType 
mapGenericType) {
-    MapTypeCache state = mapTypeCache();
-    state.partialGenericKVTypeKey1 = state.partialGenericKVTypeKey0;
-    state.partialGenericKVTypeValue1 = state.partialGenericKVTypeValue0;
-    state.partialGenericKVTypeKey0 = genericType;
-    state.partialGenericKVTypeValue0 = mapGenericType;
-  }
-
   protected <K, V> void copyEntry(CopyContext copyContext, Map<K, V> 
originMap, Map<K, V> newMap) {
     TypeResolver classResolver = typeResolver;
     MapTypeCache state = mapTypeCache();
@@ -650,7 +596,7 @@ public abstract class MapLikeSerializer<T> extends 
Serializer<T> {
         break;
       }
       GenericType genericType = 
generics.nextGenericType(readContext.getDepth());
-      if (genericType == null) {
+      if (genericType == null || genericType.getTypeParametersCount() < 2) {
         sizeAndHeader = readJavaChunk(readContext, map, size, chunkHeader, 
null, null);
       } else {
         sizeAndHeader =
@@ -755,9 +701,7 @@ public abstract class MapLikeSerializer<T> extends 
Serializer<T> {
       ReadContext readContext, boolean trackRef, boolean isKey) {
     Generics generics = readContext.getGenerics();
     GenericType genericType = generics.nextGenericType(readContext.getDepth());
-    if (genericType.getTypeParametersCount() < 2) {
-      genericType = getKVGenericType(genericType);
-    }
+    Preconditions.checkState(genericType != null && 
genericType.getTypeParametersCount() >= 2);
     GenericType type = isKey ? genericType.getTypeParameter0() : 
genericType.getTypeParameter1();
     generics.pushGenericType(type, readContext.getDepth());
     Serializer<?> serializer = type.getSerializer(typeResolver);
@@ -858,12 +802,6 @@ public abstract class MapLikeSerializer<T> extends 
Serializer<T> {
       int chunkHeader) {
     MemoryBuffer buffer = readContext.getBuffer();
     MapTypeCache state = mapTypeCache();
-    // type parameters count for `Map field` will be 0;
-    // type parameters count for `SubMap<V> field` which SubMap is
-    // `SubMap<V> implements Map<String, V>` will be 1;
-    if (genericType.getTypeParametersCount() < 2) {
-      genericType = getKVGenericType(genericType);
-    }
     GenericType keyGenericType = genericType.getTypeParameter0();
     GenericType valueGenericType = genericType.getTypeParameter1();
     // noinspection Duplicates
diff --git a/java/fory-core/src/main/java/org/apache/fory/type/ScalaTypes.java 
b/java/fory-core/src/main/java/org/apache/fory/type/ScalaTypes.java
index 7c814d6ec..ea6efd366 100644
--- a/java/fory-core/src/main/java/org/apache/fory/type/ScalaTypes.java
+++ b/java/fory-core/src/main/java/org/apache/fory/type/ScalaTypes.java
@@ -21,6 +21,7 @@ package org.apache.fory.type;
 
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
+import org.apache.fory.annotation.Internal;
 import org.apache.fory.collection.Tuple2;
 import org.apache.fory.reflect.ReflectionUtils;
 import org.apache.fory.reflect.TypeRef;
@@ -92,7 +93,8 @@ public class ScalaTypes {
         .resolveType(getScalaNextReturnType());
   }
 
-  private static Type getScalaIteratorReturnType() {
+  @Internal
+  public static Type getScalaIteratorReturnType() {
     if (SCALA_ITERATOR_RETURN_TYPE == null) {
       try {
         SCALA_ITERATOR_RETURN_TYPE =
@@ -104,7 +106,8 @@ public class ScalaTypes {
     return SCALA_ITERATOR_RETURN_TYPE;
   }
 
-  private static Type getScalaNextReturnType() {
+  @Internal
+  public static Type getScalaNextReturnType() {
     if (SCALA_NEXT_RETURN_TYPE == null) {
       Class<?> scalaIteratorType = 
ReflectionUtils.loadClass("scala.collection.Iterator");
       try {
diff --git a/java/fory-core/src/main/java/org/apache/fory/type/TypeUtils.java 
b/java/fory-core/src/main/java/org/apache/fory/type/TypeUtils.java
index ab6adc373..91405ce19 100644
--- a/java/fory-core/src/main/java/org/apache/fory/type/TypeUtils.java
+++ b/java/fory-core/src/main/java/org/apache/fory/type/TypeUtils.java
@@ -65,7 +65,6 @@ import java.util.OptionalLong;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.WeakHashMap;
-import java.util.stream.Collectors;
 import org.apache.fory.annotation.Ref;
 import org.apache.fory.collection.BFloat16List;
 import org.apache.fory.collection.BoolList;
@@ -574,11 +573,10 @@ public class TypeUtils {
   public static TypeRef<?> getElementType(TypeRef<?> typeRef) {
     if (typeRef.hasExplicitTypeArguments()) {
       List<TypeRef<?>> typeArguments = typeRef.getTypeArguments();
-      if (typeArguments.size() == 1) {
-        Class<?> rawType = getRawType(typeRef);
-        if (Iterable.class.isAssignableFrom(rawType) || 
isScalaCollectionClass(rawType)) {
-          return typeArguments.get(0);
-        }
+      Class<?> rawType = getRawType(typeRef);
+      if (typeArguments.size() == 1
+          && (Iterable.class.isAssignableFrom(rawType) || 
isScalaIterableClass(rawType))) {
+        return typeArguments.get(0);
       }
     }
     Type type = typeRef.getType();
@@ -593,8 +591,7 @@ public class TypeUtils {
         }
       }
     }
-    if (ScalaTypes.SCALA_AVAILABLE
-        && typeRef.getType().getTypeName().startsWith("scala.collection")) {
+    if (isScalaIterableClass(typeRef.getRawType())) {
       return ScalaTypes.getElementType(typeRef);
     }
     TypeRef<?> supertype = ((TypeRef<? extends Iterable<?>>) 
typeRef).getSupertype(Iterable.class);
@@ -611,12 +608,10 @@ public class TypeUtils {
   public static Tuple2<TypeRef<?>, TypeRef<?>> getMapKeyValueType(TypeRef<?> 
typeRef) {
     if (typeRef.hasExplicitTypeArguments()) {
       List<TypeRef<?>> typeArguments = typeRef.getTypeArguments();
-      if (typeArguments.size() == 2) {
-        Class<?> rawType = getRawType(typeRef);
-        if ((Map.class.isAssignableFrom(rawType) || 
isScalaCollectionClass(rawType))
-            && rawType.getTypeParameters().length == 2) {
-          return Tuple2.of(typeArguments.get(0), typeArguments.get(1));
-        }
+      Class<?> rawType = getRawType(typeRef);
+      if (typeArguments.size() == 2
+          && (Map.class.isAssignableFrom(rawType) || 
isScalaMapClass(rawType))) {
+        return Tuple2.of(typeArguments.get(0), typeArguments.get(1));
       }
     }
     Type type = typeRef.getType();
@@ -632,8 +627,7 @@ public class TypeUtils {
         }
       }
     }
-    if (ScalaTypes.SCALA_AVAILABLE
-        && typeRef.getType().getTypeName().startsWith("scala.collection")) {
+    if (isScalaMapClass(typeRef.getRawType())) {
       return ScalaTypes.getMapKeyValueType(typeRef);
     }
     @SuppressWarnings("unchecked")
@@ -643,8 +637,16 @@ public class TypeUtils {
     return Tuple2.of(keyType, valueType);
   }
 
-  private static boolean isScalaCollectionClass(Class<?> rawType) {
-    return ScalaTypes.SCALA_AVAILABLE && 
rawType.getName().startsWith("scala.collection");
+  private static boolean isScalaMapClass(Class<?> rawType) {
+    return ScalaTypes.SCALA_AVAILABLE
+        && rawType.getName().startsWith("scala.collection")
+        && ScalaTypes.getScalaMapType().isAssignableFrom(rawType);
+  }
+
+  private static boolean isScalaIterableClass(Class<?> rawType) {
+    return ScalaTypes.SCALA_AVAILABLE
+        && rawType.getName().startsWith("scala.collection")
+        && ScalaTypes.getScalaIterableType().isAssignableFrom(rawType);
   }
 
   public static void applyRefTrackingOverride(
@@ -656,14 +658,9 @@ public class TypeUtils {
     if (ref != null) {
       genericType.setTrackingRefOverride(ref.enable() && globalTrackingRef);
     }
-    Object[] typeUseArgs = TypeUseMetadata.typeUseArguments(typeUse);
-    if (typeUseArgs != null) {
-      GenericType[] typeParameters = genericType.getTypeParameters();
-      int len = Math.min(typeUseArgs.length, typeParameters.length);
-      for (int i = 0; i < len; i++) {
-        applyRefTrackingOverride(typeParameters[i], typeUseArgs[i], 
globalTrackingRef);
-      }
-    }
+    // Child type-use metadata is already folded into TypeRef explicit 
arguments. Replaying JVM
+    // type-use children here is unsafe because collection/map GenericType 
parameters are normalized
+    // to element or key/value, not necessarily the raw declared type-argument 
order.
   }
 
   public static TypeRef<?> getFieldTypeRef(Field field) {
@@ -1050,17 +1047,7 @@ public class TypeUtils {
 
   /** Returns generic type arguments of <code>typeToken</code>. */
   public static List<TypeRef<?>> getTypeArguments(TypeRef<?> typeRef) {
-    if (typeRef.hasExplicitTypeArguments()) {
-      return typeRef.getTypeArguments();
-    }
-    if (typeRef.getType() instanceof ParameterizedType) {
-      ParameterizedType parameterizedType = (ParameterizedType) 
typeRef.getType();
-      return Arrays.stream(parameterizedType.getActualTypeArguments())
-          .map(TypeRef::of)
-          .collect(Collectors.toList());
-    } else {
-      return new ArrayList<>();
-    }
+    return typeRef.getTypeArguments();
   }
 
   /**
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/reflect/TypeRefTest.java 
b/java/fory-core/src/test/java/org/apache/fory/reflect/TypeRefTest.java
index a9e17babc..ef85f4eef 100644
--- a/java/fory-core/src/test/java/org/apache/fory/reflect/TypeRefTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/reflect/TypeRefTest.java
@@ -25,6 +25,9 @@ import java.lang.reflect.Field;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.lang.reflect.WildcardType;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -39,9 +42,12 @@ import org.apache.fory.annotation.Ref;
 import org.apache.fory.collection.Tuple2;
 import org.apache.fory.config.Int32Encoding;
 import org.apache.fory.meta.TypeExtMeta;
+import org.apache.fory.type.GenericType;
+import org.apache.fory.type.ScalaTypes;
 import org.apache.fory.type.TypeUtils;
 import org.apache.fory.type.Types;
 import org.testng.Assert;
+import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class TypeRefTest extends ForyTestBase {
@@ -96,6 +102,143 @@ public class TypeRefTest extends ForyTestBase {
     serDeCheck(fory, new MyClass());
   }
 
+  static class MultiParamList<A, E> extends ArrayList<E> {}
+
+  static class MultiParamMap<A, K, V> extends HashMap<K, V> {}
+
+  static class StringKeyMap<V> extends HashMap<String, V> {}
+
+  static class ArrayElementList<A, E> extends ArrayList<E[]> {}
+
+  static class ArrayValueMap<A, K, V> extends HashMap<K, V[]> {}
+
+  static class TypeUseItem {}
+
+  static class FixedElementList<A> extends ArrayList<TypeUseItem> {}
+
+  static class ContainerTypeUseStruct {
+    MultiParamList<String, List<TypeUseItem>> nestedItems;
+    FixedElementList<@Ref(enable = false) TypeUseItem> fixedItems;
+  }
+
+  @Test
+  public void testCustomContainerTypeRefNormalization() {
+    TypeRef<?> listType = new TypeRef<MultiParamList<String, Integer>>() {};
+    Assert.assertEquals(listType.getTypeArguments().size(), 1);
+    Assert.assertEquals(listType.getTypeArguments().get(0), 
TypeRef.of(Integer.class));
+    Assert.assertEquals(TypeUtils.getElementType(listType), 
TypeRef.of(Integer.class));
+
+    GenericType listGenericType = GenericType.build(listType);
+    Assert.assertEquals(listGenericType.getTypeParametersCount(), 1);
+    Assert.assertEquals(listGenericType.getTypeParameter0().getCls(), 
Integer.class);
+
+    TypeRef<?> mapType = new TypeRef<MultiParamMap<String, Long, Integer>>() 
{};
+    Assert.assertEquals(mapType.getTypeArguments().size(), 2);
+    Assert.assertEquals(mapType.getTypeArguments().get(0), 
TypeRef.of(Long.class));
+    Assert.assertEquals(mapType.getTypeArguments().get(1), 
TypeRef.of(Integer.class));
+
+    Tuple2<TypeRef<?>, TypeRef<?>> keyValueType = 
TypeUtils.getMapKeyValueType(mapType);
+    Assert.assertEquals(keyValueType.f0, TypeRef.of(Long.class));
+    Assert.assertEquals(keyValueType.f1, TypeRef.of(Integer.class));
+
+    GenericType mapGenericType = GenericType.build(mapType);
+    Assert.assertEquals(mapGenericType.getTypeParametersCount(), 2);
+    Assert.assertEquals(mapGenericType.getTypeParameter0().getCls(), 
Long.class);
+    Assert.assertEquals(mapGenericType.getTypeParameter1().getCls(), 
Integer.class);
+
+    TypeRef<?> fixedKeyMapType = new TypeRef<StringKeyMap<List<Integer>>>() {};
+    Assert.assertEquals(fixedKeyMapType.getTypeArguments().size(), 2);
+    Assert.assertEquals(fixedKeyMapType.getTypeArguments().get(0), 
TypeRef.of(String.class));
+    Assert.assertEquals(fixedKeyMapType.getTypeArguments().get(1), new 
TypeRef<List<Integer>>() {});
+
+    GenericType fixedKeyMapGenericType = GenericType.build(fixedKeyMapType);
+    Assert.assertEquals(fixedKeyMapGenericType.getTypeParametersCount(), 2);
+    Assert.assertEquals(fixedKeyMapGenericType.getTypeParameter0().getCls(), 
String.class);
+    Assert.assertEquals(fixedKeyMapGenericType.getTypeParameter1().getCls(), 
List.class);
+  }
+
+  @Test
+  public void testCustomContainerArrayNormalization() {
+    TypeRef<?> elementType =
+        TypeUtils.getElementType(new TypeRef<ArrayElementList<String, 
Integer>>() {});
+    Assert.assertEquals(elementType.getRawType(), Integer[].class);
+    Assert.assertEquals(elementType.getComponentType(), 
TypeRef.of(Integer.class));
+
+    Tuple2<TypeRef<?>, TypeRef<?>> keyValueType =
+        TypeUtils.getMapKeyValueType(new TypeRef<ArrayValueMap<String, Long, 
Integer>>() {});
+    Assert.assertEquals(keyValueType.f0, TypeRef.of(Long.class));
+    Assert.assertEquals(keyValueType.f1.getRawType(), Integer[].class);
+    Assert.assertEquals(keyValueType.f1.getComponentType(), 
TypeRef.of(Integer.class));
+  }
+
+  @Test
+  public void testCustomContainerTypeUseMetadata() throws Exception {
+    Field nestedItemsField = 
ContainerTypeUseStruct.class.getDeclaredField("nestedItems");
+    TypeRef<?> nestedItemsType = 
TypeRef.ofTypeUse(nestedItemsField.getAnnotatedType());
+    Assert.assertEquals(nestedItemsType.getTypeArguments().size(), 1);
+    TypeRef<?> nestedListType = nestedItemsType.getTypeArguments().get(0);
+    Assert.assertEquals(nestedListType.getRawType(), List.class);
+    Assert.assertEquals(nestedListType.getTypeArguments().get(0), 
TypeRef.of(TypeUseItem.class));
+
+    TypeExtMeta elementMeta = TypeExtMeta.of(Types.UNKNOWN, true, false);
+    TypeRef<?> refItemsType =
+        TypeRef.of(
+            new TypeRef.ParameterizedTypeImpl(
+                null, MultiParamList.class, new Type[] {String.class, 
TypeUseItem.class}),
+            null,
+            Arrays.asList(TypeRef.of(String.class), 
TypeRef.of(TypeUseItem.class, elementMeta)),
+            null);
+    Assert.assertEquals(refItemsType.getTypeArguments().size(), 1);
+    
Assert.assertEquals(refItemsType.getTypeArguments().get(0).getTypeExtMeta(), 
elementMeta);
+
+    TypeExtMeta keyMeta = TypeExtMeta.of(Types.UNKNOWN, true, false);
+    TypeExtMeta valueMeta = TypeExtMeta.of(Types.UNKNOWN, true, true);
+    TypeRef<?> refMapType =
+        TypeRef.of(
+            new TypeRef.ParameterizedTypeImpl(
+                null,
+                MultiParamMap.class,
+                new Type[] {String.class, TypeUseItem.class, 
TypeUseItem.class}),
+            null,
+            Arrays.asList(
+                TypeRef.of(String.class),
+                TypeRef.of(TypeUseItem.class, keyMeta),
+                TypeRef.of(TypeUseItem.class, valueMeta)),
+            null);
+    Assert.assertEquals(refMapType.getTypeArguments().size(), 2);
+    Assert.assertEquals(refMapType.getTypeArguments().get(0).getTypeExtMeta(), 
keyMeta);
+    Assert.assertEquals(refMapType.getTypeArguments().get(1).getTypeExtMeta(), 
valueMeta);
+
+    Field fixedItemsField = 
ContainerTypeUseStruct.class.getDeclaredField("fixedItems");
+    TypeRef<?> fixedItemsType = 
TypeRef.ofTypeUse(fixedItemsField.getAnnotatedType());
+    TypeRef<?> fixedElementType = TypeUtils.getElementType(fixedItemsType);
+    Assert.assertEquals(fixedElementType, TypeRef.of(TypeUseItem.class));
+    Assert.assertFalse(fixedElementType.hasTypeExtMeta());
+  }
+
+  @Test
+  public void testScalaContainerTypeRefNormalization() throws Exception {
+    if (!ScalaTypes.SCALA_AVAILABLE) {
+      throw new SkipException("Scala is not available on the Java test 
classpath");
+    }
+    Class<?> listClass = Class.forName("scala.collection.immutable.List");
+    TypeRef<?> listType =
+        TypeRef.of(new TypeRef.ParameterizedTypeImpl(null, listClass, new 
Type[] {String.class}));
+    Assert.assertEquals(listType.getTypeArguments().size(), 1);
+    Assert.assertEquals(listType.getTypeArguments().get(0), 
TypeRef.of(String.class));
+    Assert.assertEquals(TypeUtils.getElementType(listType), 
TypeRef.of(String.class));
+
+    Class<?> mapClass = Class.forName("scala.collection.immutable.Map");
+    TypeRef<?> mapType =
+        TypeRef.of(
+            new TypeRef.ParameterizedTypeImpl(
+                null, mapClass, new Type[] {String.class, Integer.class}));
+    Tuple2<TypeRef<?>, TypeRef<?>> keyValueType = 
TypeUtils.getMapKeyValueType(mapType);
+    Assert.assertEquals(mapType.getTypeArguments().size(), 2);
+    Assert.assertEquals(keyValueType.f0, TypeRef.of(String.class));
+    Assert.assertEquals(keyValueType.f1, TypeRef.of(Integer.class));
+  }
+
   static class TypeUseMetadataStruct {
     @Nullable String nickname;
 
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 ebff4537e..ff4261652 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
@@ -34,6 +34,7 @@ import java.io.ObjectInput;
 import java.io.ObjectOutput;
 import java.io.Serializable;
 import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
 import java.util.AbstractCollection;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -70,18 +71,26 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import org.apache.fory.Fory;
 import org.apache.fory.ForyTestBase;
+import org.apache.fory.annotation.Ref;
+import org.apache.fory.config.CompatibleMode;
+import org.apache.fory.config.Int64Encoding;
+import org.apache.fory.config.Language;
 import org.apache.fory.context.ReadContext;
 import org.apache.fory.exception.DeserializationException;
 import org.apache.fory.exception.SerializationException;
 import org.apache.fory.memory.MemoryBuffer;
 import org.apache.fory.memory.MemoryUtils;
+import org.apache.fory.meta.FieldTypes;
+import org.apache.fory.platform.JdkVersion;
 import org.apache.fory.reflect.FieldAccessor;
 import org.apache.fory.reflect.TypeRef;
 import org.apache.fory.resolver.TypeResolver;
 import 
org.apache.fory.serializer.collection.CollectionSerializers.JDKCompatibleCollectionSerializer;
 import org.apache.fory.test.bean.Cyclic;
 import org.apache.fory.type.GenericType;
+import org.apache.fory.type.Types;
 import org.testng.Assert;
+import org.testng.SkipException;
 import org.testng.annotations.Test;
 import org.testng.collections.Maps;
 
@@ -254,6 +263,200 @@ public class CollectionSerializersTest extends 
ForyTestBase {
     assertThrowsCause(RuntimeException.class, () -> fory.deserialize(bytes2));
   }
 
+  public static class MultiParamList<A, E> extends ArrayList<E> {
+    private A metadata;
+
+    public MultiParamList() {}
+
+    public MultiParamList(A metadata) {
+      this.metadata = metadata;
+    }
+
+    public A getMetadata() {
+      return metadata;
+    }
+
+    public void setMetadata(A metadata) {
+      this.metadata = metadata;
+    }
+  }
+
+  public static class MultiParamListHolder {
+    private String name;
+    private MultiParamList<String, Integer> numbers;
+
+    public MultiParamListHolder() {}
+
+    public MultiParamListHolder(String name, MultiParamList<String, Integer> 
numbers) {
+      this.name = name;
+      this.numbers = numbers;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public MultiParamList<String, Integer> getNumbers() {
+      return numbers;
+    }
+  }
+
+  public static class CollectionRefItem {
+    public int id;
+  }
+
+  public static class MultiParamListRefHolder {
+    private MultiParamList<String, @Ref(enable = false) CollectionRefItem> 
noRefItems;
+    private MultiParamList<String, @Ref(enable = true) CollectionRefItem> 
refItems;
+
+    public MultiParamListRefHolder() {}
+  }
+
+  public static class NestedMultiParamListHolder {
+    private MultiParamList<String, List<@Ref(enable = false) 
CollectionRefItem>> nestedItems;
+
+    public NestedMultiParamListHolder() {}
+  }
+
+  public static class FixedElementList<A> extends ArrayList<CollectionRefItem> 
{
+    private A metadata;
+
+    public FixedElementList() {}
+
+    public FixedElementList(A metadata) {
+      this.metadata = metadata;
+    }
+  }
+
+  public static class FixedElementListHolder {
+    private FixedElementList<@Ref(enable = false) CollectionRefItem> items;
+
+    public FixedElementListHolder() {}
+  }
+
+  @Test(dataProvider = "enableCodegen")
+  public void testMultiParamCollectionRoundTrip(boolean enableCodegen) {
+    MultiParamList<String, Integer> list = new MultiParamList<>("my-metadata");
+    list.add(1);
+    list.add(2);
+    list.add(3);
+
+    MultiParamListHolder holder = new MultiParamListHolder("test-container", 
list);
+    Fory fory = collectionGenericFory(enableCodegen);
+    MultiParamListHolder cloned = (MultiParamListHolder) 
fory.deserialize(fory.serialize(holder));
+
+    Assert.assertEquals(cloned.getName(), "test-container");
+    Assert.assertNotNull(cloned.getNumbers());
+    Assert.assertEquals(cloned.getNumbers().getMetadata(), "my-metadata");
+    Assert.assertEquals(cloned.getNumbers().size(), 3);
+    Assert.assertEquals(cloned.getNumbers().get(0), Integer.valueOf(1));
+    Assert.assertEquals(cloned.getNumbers().get(1), Integer.valueOf(2));
+    Assert.assertEquals(cloned.getNumbers().get(2), Integer.valueOf(3));
+  }
+
+  @Test(dataProvider = "enableCodegen")
+  public void testMultiParamCollectionRefOverride(boolean enableCodegen) {
+    skipMemberGenericTypeUseOnJdk11();
+    CollectionRefItem noRefItem = new CollectionRefItem();
+    noRefItem.id = 1;
+    CollectionRefItem refItem = new CollectionRefItem();
+    refItem.id = 2;
+
+    MultiParamListRefHolder holder = new MultiParamListRefHolder();
+    holder.noRefItems = new MultiParamList<>("no-ref");
+    holder.noRefItems.add(noRefItem);
+    holder.noRefItems.add(noRefItem);
+    holder.refItems = new MultiParamList<>("ref");
+    holder.refItems.add(refItem);
+    holder.refItems.add(refItem);
+
+    Fory fory = collectionGenericFory(enableCodegen);
+    MultiParamListRefHolder cloned =
+        (MultiParamListRefHolder) fory.deserialize(fory.serialize(holder));
+    Assert.assertNotSame(cloned.noRefItems.get(0), cloned.noRefItems.get(1));
+    Assert.assertSame(cloned.refItems.get(0), cloned.refItems.get(1));
+  }
+
+  @Test(dataProvider = "enableCodegen")
+  public void testNestedMultiParamCollectionRef(boolean enableCodegen) {
+    skipMemberGenericTypeUseOnJdk11();
+    CollectionRefItem item = new CollectionRefItem();
+    item.id = 1;
+
+    NestedMultiParamListHolder holder = new NestedMultiParamListHolder();
+    holder.nestedItems = new MultiParamList<>("nested");
+    List<CollectionRefItem> inner = new ArrayList<>();
+    inner.add(item);
+    inner.add(item);
+    holder.nestedItems.add(inner);
+
+    Fory fory = collectionGenericFory(enableCodegen);
+    NestedMultiParamListHolder cloned =
+        (NestedMultiParamListHolder) fory.deserialize(fory.serialize(holder));
+    Assert.assertNotSame(cloned.nestedItems.get(0).get(0), 
cloned.nestedItems.get(0).get(1));
+  }
+
+  private static void skipMemberGenericTypeUseOnJdk11() {
+    if (JdkVersion.MAJOR_VERSION <= 11) {
+      throw new SkipException(
+          "JDK 11 and earlier do not expose member-class generic field 
type-use metadata");
+    }
+  }
+
+  @Test(dataProvider = "enableCodegen")
+  public void testFixedElementCollectionRefMeta(boolean enableCodegen) {
+    CollectionRefItem element = new CollectionRefItem();
+    element.id = 1;
+    CollectionRefItem metadata = new CollectionRefItem();
+    metadata.id = 2;
+
+    FixedElementListHolder holder = new FixedElementListHolder();
+    holder.items = new FixedElementList<>(metadata);
+    holder.items.add(element);
+    holder.items.add(element);
+
+    Fory fory = collectionGenericFory(enableCodegen);
+    FixedElementListHolder cloned =
+        (FixedElementListHolder) fory.deserialize(fory.serialize(holder));
+    Assert.assertSame(cloned.items.get(0), cloned.items.get(1));
+  }
+
+  @Test
+  public void testCollectionFieldTypeKeepsDeclaredType() {
+    Fory fory = 
builder().withXlang(false).requireClassRegistration(false).build();
+    TypeRef<?> declared = new TypeRef<List<Integer>>() {};
+    FieldTypes.CollectionFieldType fieldType =
+        new FieldTypes.CollectionFieldType(
+            Types.LIST,
+            true,
+            true,
+            new FieldTypes.RegisteredFieldType(true, false, Types.INT32, -1));
+
+    TypeRef<?> rebuilt = fieldType.toTypeToken(fory.getTypeResolver(), 
declared);
+
+    Assert.assertTrue(rebuilt.getType() instanceof ParameterizedType);
+    Assert.assertEquals(rebuilt.getRawType(), List.class);
+    Assert.assertEquals(rebuilt.getTypeArguments().size(), 1);
+    Assert.assertEquals(rebuilt.getTypeExtMeta().typeId(), Types.LIST);
+    Assert.assertTrue(rebuilt.getTypeExtMeta().nullable());
+    Assert.assertTrue(rebuilt.getTypeExtMeta().trackingRef());
+  }
+
+  private static Fory collectionGenericFory(boolean enableCodegen) {
+    return Fory.builder()
+        .withLanguage(Language.JAVA)
+        .requireClassRegistration(false)
+        .withRefTracking(true)
+        .withCompatibleMode(CompatibleMode.COMPATIBLE)
+        .withAsyncCompilation(false)
+        .withIntCompressed(true)
+        .withCodegen(enableCodegen)
+        .withLongCompressed(Int64Encoding.VARINT)
+        .withIntArrayCompressed(true)
+        .withLongArrayCompressed(true)
+        .build();
+  }
+
   @Test(dataProvider = "referenceTrackingConfig")
   public void testSortedSet(boolean referenceTrackingConfig) {
     Fory fory =
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 bb5f721c5..ba2303b27 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
@@ -31,6 +31,7 @@ import java.io.IOException;
 import java.io.ObjectInput;
 import java.io.ObjectOutput;
 import java.io.Serializable;
+import java.lang.reflect.ParameterizedType;
 import java.util.AbstractMap;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -58,9 +59,12 @@ import org.apache.fory.ForyTestBase;
 import org.apache.fory.annotation.Ref;
 import org.apache.fory.collection.LazyMap;
 import org.apache.fory.collection.MapEntry;
+import org.apache.fory.config.CompatibleMode;
 import org.apache.fory.exception.SerializationException;
 import org.apache.fory.memory.MemoryBuffer;
 import org.apache.fory.memory.MemoryUtils;
+import org.apache.fory.meta.FieldTypes;
+import org.apache.fory.platform.JdkVersion;
 import org.apache.fory.reflect.TypeRef;
 import org.apache.fory.serializer.Serializer;
 import 
org.apache.fory.serializer.collection.CollectionSerializersTest.TestEnum;
@@ -68,7 +72,9 @@ import org.apache.fory.test.bean.BeanB;
 import org.apache.fory.test.bean.Cyclic;
 import org.apache.fory.test.bean.MapFields;
 import org.apache.fory.type.GenericType;
+import org.apache.fory.type.Types;
 import org.testng.Assert;
+import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class MapSerializersTest extends ForyTestBase {
@@ -1160,6 +1166,153 @@ public class MapSerializersTest extends ForyTestBase {
             new HashMap<>(mapOf(new HashMap<>(mapOf(1, list)), list))));
   }
 
+  public static class MultiParamMap<A, K, V> extends HashMap<K, V> {}
+
+  @Data
+  public static class MultiParamMapHolder {
+    public MultiParamMap<String, Integer, Long> map;
+  }
+
+  @Data
+  public static class MultiParamMapRefHolder {
+    public MultiParamMap<String, Integer, @Ref(enable = false) MapRefItem> 
noRefMap;
+    public MultiParamMap<String, Integer, @Ref(enable = true) MapRefItem> 
refMap;
+  }
+
+  @Data
+  public static class NestedMultiParamMapRefHolder {
+    public MultiParamMap<String, Integer, List<@Ref(enable = false) 
MapRefItem>> nestedMap;
+  }
+
+  public static class FixedValueMap<K, A> extends HashMap<K, MapRefItem> {}
+
+  @Data
+  public static class FixedValueMapRefHolder {
+    public FixedValueMap<Integer, @Ref(enable = false) MapRefItem> map;
+  }
+
+  @Test(dataProvider = "enableCodegen")
+  public void testMultiParamMapGeneric(boolean enableCodegen) {
+    Fory fory =
+        builder()
+            .withXlang(false)
+            .withCodegen(enableCodegen)
+            .requireClassRegistration(false)
+            .build();
+    MultiParamMapHolder holder = new MultiParamMapHolder();
+    holder.map = new MultiParamMap<>();
+    holder.map.put(1, 2L);
+    holder.map.put(3, 4L);
+    serDeCheck(fory, holder);
+  }
+
+  @Test
+  public void testMapFieldTypeKeepsDeclaredType() {
+    Fory fory = 
builder().withXlang(false).requireClassRegistration(false).build();
+    TypeRef<?> declared = new TypeRef<Map<String, Integer>>() {};
+    FieldTypes.MapFieldType fieldType =
+        new FieldTypes.MapFieldType(
+            Types.MAP,
+            true,
+            true,
+            new FieldTypes.RegisteredFieldType(true, false, Types.STRING, -1),
+            new FieldTypes.RegisteredFieldType(true, false, Types.INT32, -1));
+
+    TypeRef<?> rebuilt = fieldType.toTypeToken(fory.getTypeResolver(), 
declared);
+
+    Assert.assertTrue(rebuilt.getType() instanceof ParameterizedType);
+    Assert.assertEquals(rebuilt.getRawType(), Map.class);
+    Assert.assertEquals(rebuilt.getTypeArguments().size(), 2);
+    Assert.assertEquals(rebuilt.getTypeExtMeta().typeId(), Types.MAP);
+    Assert.assertTrue(rebuilt.getTypeExtMeta().nullable());
+    Assert.assertTrue(rebuilt.getTypeExtMeta().trackingRef());
+  }
+
+  @Test(dataProvider = "enableCodegen")
+  public void testMultiParamMapRefOverride(boolean enableCodegen) {
+    skipMemberGenericTypeUseOnJdk11();
+    Fory fory =
+        builder()
+            .withXlang(false)
+            .withRefTracking(true)
+            .withCodegen(enableCodegen)
+            .withCompatibleMode(CompatibleMode.COMPATIBLE)
+            .requireClassRegistration(false)
+            .build();
+    MapRefItem noRefItem = new MapRefItem();
+    noRefItem.id = 1;
+    noRefItem.name = "no-ref";
+    MapRefItem refItem = new MapRefItem();
+    refItem.id = 2;
+    refItem.name = "ref";
+    MultiParamMapRefHolder holder = new MultiParamMapRefHolder();
+    holder.noRefMap = new MultiParamMap<>();
+    holder.noRefMap.put(1, noRefItem);
+    holder.noRefMap.put(2, noRefItem);
+    holder.refMap = new MultiParamMap<>();
+    holder.refMap.put(1, refItem);
+    holder.refMap.put(2, refItem);
+
+    MultiParamMapRefHolder cloned = serDe(fory, holder);
+    Assert.assertNotSame(cloned.noRefMap.get(1), cloned.noRefMap.get(2));
+    Assert.assertSame(cloned.refMap.get(1), cloned.refMap.get(2));
+  }
+
+  @Test(dataProvider = "enableCodegen")
+  public void testNestedMultiParamMapRefOverride(boolean enableCodegen) {
+    skipMemberGenericTypeUseOnJdk11();
+    Fory fory =
+        builder()
+            .withXlang(false)
+            .withRefTracking(true)
+            .withCodegen(enableCodegen)
+            .withCompatibleMode(CompatibleMode.COMPATIBLE)
+            .requireClassRegistration(false)
+            .build();
+    MapRefItem item = new MapRefItem();
+    item.id = 1;
+    item.name = "nested";
+    List<MapRefItem> values = new ArrayList<>();
+    values.add(item);
+    values.add(item);
+
+    NestedMultiParamMapRefHolder holder = new NestedMultiParamMapRefHolder();
+    holder.nestedMap = new MultiParamMap<>();
+    holder.nestedMap.put(1, values);
+
+    NestedMultiParamMapRefHolder cloned = serDe(fory, holder);
+    Assert.assertNotSame(cloned.nestedMap.get(1).get(0), 
cloned.nestedMap.get(1).get(1));
+  }
+
+  private static void skipMemberGenericTypeUseOnJdk11() {
+    if (JdkVersion.MAJOR_VERSION <= 11) {
+      throw new SkipException(
+          "JDK 11 and earlier do not expose member-class generic field 
type-use metadata");
+    }
+  }
+
+  @Test(dataProvider = "enableCodegen")
+  public void testFixedValueMapIgnoresMetadataRef(boolean enableCodegen) {
+    Fory fory =
+        builder()
+            .withXlang(false)
+            .withRefTracking(true)
+            .withCodegen(enableCodegen)
+            .withCompatibleMode(CompatibleMode.COMPATIBLE)
+            .requireClassRegistration(false)
+            .build();
+    MapRefItem value = new MapRefItem();
+    value.id = 1;
+    value.name = "value";
+    FixedValueMapRefHolder holder = new FixedValueMapRefHolder();
+    holder.map = new FixedValueMap<>();
+    holder.map.put(1, value);
+    holder.map.put(2, value);
+
+    FixedValueMapRefHolder cloned = serDe(fory, holder);
+    Assert.assertSame(cloned.map.get(1), cloned.map.get(2));
+  }
+
   public static class StringKeyMap<T> extends HashMap<String, T> {}
 
   @Test


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

Reply via email to