diff --git a/src/main/java/net/jodah/typetools/ReifiedParameterizedType.java b/src/main/java/net/jodah/typetools/ReifiedParameterizedType.java index 781248f..f21fbbf 100644 --- a/src/main/java/net/jodah/typetools/ReifiedParameterizedType.java +++ b/src/main/java/net/jodah/typetools/ReifiedParameterizedType.java @@ -2,15 +2,35 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.util.Arrays; class ReifiedParameterizedType implements ParameterizedType { private final ParameterizedType original; private final Type[] reifiedTypeArguments; + private final boolean[] loop; + private int reified = 0; ReifiedParameterizedType(ParameterizedType original) { this.original = original; this.reifiedTypeArguments = new Type[original.getActualTypeArguments().length]; + this.loop = new boolean[original.getActualTypeArguments().length]; + } + + /** + * This method is used to set reified types as they are processed. For example, + * When reifying some {@code T}, in order to reify {@code T} we need + * to reify first {@code E1} and then {@code E2} in order. The reified counterpart + * of {@code T} is allocated before, and then the results from reifying {@code E1} + * and {@code E2} are added through this method. + * @param type the reification result to be added + */ + /* package-private */ void addReifiedTypeArgument(Type type) { + if (reified >= reifiedTypeArguments.length) { + return; + } + if (type == this) { + loop[reified] = true; + } + reifiedTypeArguments[reified++] = type; } @Override @@ -29,20 +49,15 @@ public Type getOwnerType() { } /** - * NOTE: This method should only be called once per instance. + * Keep this consistent with {@link sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl#toString} */ - void setReifiedTypeArguments(Type[] reifiedTypeArguments) { - System.arraycopy(reifiedTypeArguments, 0, this.reifiedTypeArguments, 0, this.reifiedTypeArguments.length); - } - - /** Keep this consistent with {@link sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl#toString} */ @Override public String toString() { final Type ownerType = getOwnerType(); final Type rawType = getRawType(); final Type[] actualTypeArguments = getActualTypeArguments(); - StringBuilder sb = new StringBuilder(); + final StringBuilder sb = new StringBuilder(); if (ownerType != null) { if (ownerType instanceof Class) { @@ -51,13 +66,15 @@ public String toString() { sb.append(ownerType.toString()); } - sb.append("."); + sb.append("$"); if (ownerType instanceof ParameterizedType) { // Find simple name of nested type by removing the // shared prefix with owner. sb.append(rawType.getTypeName() .replace(((ParameterizedType) ownerType).getRawType().getTypeName() + "$", "")); + } else if (rawType instanceof Class){ + sb.append(((Class) rawType).getSimpleName()); } else { sb.append(rawType.getTypeName()); } @@ -68,13 +85,27 @@ public String toString() { if (actualTypeArguments != null && actualTypeArguments.length > 0) { sb.append("<"); - boolean first = true; - for (Type t: actualTypeArguments) { - if (!first) { + for (int i = 0; i < actualTypeArguments.length; i++) { + if (i != 0) { sb.append(", "); } - sb.append(t == null ? "null" : t.getTypeName()); - first = false; + + final Type t = actualTypeArguments[i]; + + if (i >= reified) { + sb.append("?"); + } else if (t == null) { + sb.append("null"); + } else if (loop[i]) { + // Instead of recursing into this argument, which would overflow the stack, + // print three dots to indicate "self-loop structure is here". Note + // that if the full string examined is some other type that contains this + // instance, then the notation is ambiguous: We don't know which type + // is self-loop where. + sb.append("..."); + } else { + sb.append(t.getTypeName()); + } } sb.append(">"); } @@ -92,14 +123,39 @@ public boolean equals(Object o) { } ReifiedParameterizedType that = (ReifiedParameterizedType) o; - return original.equals(that.original) && - Arrays.equals(reifiedTypeArguments, that.reifiedTypeArguments); + if (!original.equals(that.original)) { + return false; + } + + if (reifiedTypeArguments.length != that.reifiedTypeArguments.length) { + return false; + } + + for (int i = 0; i < reifiedTypeArguments.length; i++) { + if (loop[i] != that.loop[i]) { + return false; + } + if (loop[i]) { + continue; + } + if (reifiedTypeArguments[i] != that.reifiedTypeArguments[i]) { + return false; + } + } + return true; } @Override public int hashCode() { int result = original.hashCode(); - result = 31 * result + Arrays.hashCode(reifiedTypeArguments); + for (int i = 0; i < reifiedTypeArguments.length; i++) { + if (loop[i]) { + continue; + } + if (reifiedTypeArguments[i] instanceof ReifiedParameterizedType) { + result = 31 * result + reifiedTypeArguments[i].hashCode(); + } + } return result; } } \ No newline at end of file diff --git a/src/main/java/net/jodah/typetools/TypeResolver.java b/src/main/java/net/jodah/typetools/TypeResolver.java index afb9b1a..b9badd2 100644 --- a/src/main/java/net/jodah/typetools/TypeResolver.java +++ b/src/main/java/net/jodah/typetools/TypeResolver.java @@ -399,55 +399,46 @@ private static Type reify(final Type genericType, final Map, Typ else if (genericType instanceof Class) return genericType; else - return reify(genericType, typeVariableTypeMap, new HashMap()); + return reify(genericType, typeVariableTypeMap, new HashMap()); } /** * Works like {@link #resolveRawClass(Type, Class, Class)} but does not stop at raw classes. Instead, traverses * referenced types. * - * @param cache contains a mapping of generic types to reified types. A value of {@code null} inside a + * @param partial contains a mapping of generic types to reified types. A value of {@code null} inside a * {@link ReifiedParameterizedType} instance means that this type is currently being reified. */ - private static Type reify(Type genericType, final Map, Type> typeVariableMap, Map cache) { + private static Type reify(Type genericType, final Map, Type> typeVariableMap, Map partial) { // Terminal case. if (genericType instanceof Class) return genericType; - // For cycles of length larger than one, find its last element by chasing through cache. - while (cache.containsKey(genericType)) { - genericType = cache.get(genericType); - } - // Recursive cases. if (genericType instanceof ParameterizedType) { final ParameterizedType parameterizedType = (ParameterizedType) genericType; + // Self-referential type needs special attention. Otherwise we might accidentally overflow the stack. + if (partial.containsKey(parameterizedType)) { + ReifiedParameterizedType res = partial.get(genericType); + res.addReifiedTypeArgument(res); + return res; + } final Type[] genericTypeArguments = parameterizedType.getActualTypeArguments(); - final Type[] reifiedTypeArguments = new Type[genericTypeArguments.length]; - - ReifiedParameterizedType result = new ReifiedParameterizedType(parameterizedType); - cache.put(genericType, result); - - boolean changed = false; - for (int i = 0; i < genericTypeArguments.length; i++) { - // Cycle detection. In case a genericTypeArgument is null, it is currently being resolved, - // thus there's a cycle in the type's structure. - if (genericTypeArguments[i] == null) { - return parameterizedType; + final ReifiedParameterizedType result = new ReifiedParameterizedType(parameterizedType); + partial.put(parameterizedType, result); + for (Type genericTypeArgument : genericTypeArguments) { + Type reified = reify(genericTypeArgument, typeVariableMap, partial); + // Self-references are added as soon as they are detected, see above. + // In this case, skip adding. + if (reified != result) { + result.addReifiedTypeArgument(reified); } - reifiedTypeArguments[i] = reify(genericTypeArguments[i], typeVariableMap, cache); - changed = changed || (reifiedTypeArguments[i] != genericTypeArguments[i]); } - - if (!changed) - return parameterizedType; - - result.setReifiedTypeArguments(reifiedTypeArguments); return result; } else if (genericType instanceof GenericArrayType) { final GenericArrayType genericArrayType = (GenericArrayType) genericType; final Type genericComponentType = genericArrayType.getGenericComponentType(); - final Type reifiedComponentType = reify(genericArrayType.getGenericComponentType(), typeVariableMap, cache); + final Type reifiedComponentType = reify(genericArrayType.getGenericComponentType(), typeVariableMap, partial); if (genericComponentType == reifiedComponentType) return genericComponentType; @@ -461,36 +452,26 @@ private static Type reify(Type genericType, final Map, Type> typ } else if (genericType instanceof TypeVariable) { final TypeVariable typeVariable = (TypeVariable) genericType; final Type mapping = typeVariableMap.get(typeVariable); - if (mapping != null) { - cache.put(typeVariable, mapping); - return reify(mapping, typeVariableMap, cache); - } - - final Type[] upperBounds = typeVariable.getBounds(); - - // Copy cache in case the bound is mutually recursive on the variable. This is to avoid sharing of - // cache in different branches of the call-graph of reify. - cache = new HashMap(cache); - + if (mapping != null) + return reify(mapping, typeVariableMap, partial); // NOTE: According to https://docs.oracle.com/javase/tutorial/java/generics/bounded.html // if there are multiple upper bounds where one bound is a class, then this must be the - // leftmost/first bound. Therefore we blindly take this one, hoping is the most relevant. + // leftmost/first bound. Therefore we blindly take this one, hoping it is the most relevant. // Hibernate does the same when erasing types, see also // https://github.com/hibernate/hibernate-validator/blob/6.0/engine/src/main/java/org/hibernate/validator/internal/util/TypeHelper.java#L181-L186 - cache.put(typeVariable, upperBounds[0]); - return reify(upperBounds[0], typeVariableMap, cache); + return reify(typeVariable.getBounds()[0], typeVariableMap, partial); } else if (genericType instanceof WildcardType) { final WildcardType wildcardType = (WildcardType) genericType; final Type[] upperBounds = wildcardType.getUpperBounds(); final Type[] lowerBounds = wildcardType.getLowerBounds(); if (upperBounds.length == 1 && lowerBounds.length == 0) - return reify(upperBounds[0], typeVariableMap, cache); + return reify(upperBounds[0], typeVariableMap, partial); throw new UnsupportedOperationException( - "Attempted to reify wildcard type with name '" + wildcardType + "' which has " + - upperBounds.length + " upper bounds and " + lowerBounds.length + " lower bounds. " + - "Reification of wildcard types is only supported for " + - "the trivial case of exactly one upper bound and no lower bounds."); + "Attempted to reify wildcard type with name '" + wildcardType.getTypeName() + + "' which has " + upperBounds.length + " upper bounds and " + lowerBounds.length + + " lower bounds. Reification of wildcard types is only supported for" + + " the trivial case of exactly 1 upper bound and 0 lower bounds."); } throw new UnsupportedOperationException( "Reification of type with name '" + genericType.getTypeName() + "' and " + diff --git a/src/test/java/net/jodah/typetools/TypeResolverTest.java b/src/test/java/net/jodah/typetools/TypeResolverTest.java index b5f818c..24fe9fa 100644 --- a/src/test/java/net/jodah/typetools/TypeResolverTest.java +++ b/src/test/java/net/jodah/typetools/TypeResolverTest.java @@ -9,6 +9,7 @@ import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -119,6 +120,13 @@ public void shouldResolveParameterizedTypeForBazFromFoo() { assert secondTypeArgument.getActualTypeArguments()[0] == Object.class; } + public void shouldResolveTypeVariable() { + TypeVariable> typeVariable = Baz.class.getTypeParameters()[0]; + Type type = TypeResolver.reify(typeVariable, Bar.class); + assert type instanceof ParameterizedType; + assert ((ParameterizedType) type).getRawType().equals(HashSet.class); + } + public void shouldResolvePartialParameterizedTypeForBazFromBar() { Type type = TypeResolver.reify(Baz.class, Bar.class); @@ -319,6 +327,7 @@ public void shouldReifyWildcardWithComplexUpperBound() throws Exception { } static abstract class EnumBound> { + public S enumField; } static abstract class SubEnumBound> extends EnumBound { @@ -334,7 +343,7 @@ static abstract class RecursiveOnSecond> ex static abstract class RecursiveLongBase {} - static abstract class RecursiveLong>> {} + static abstract class RecursiveLong>> extends RecursiveLongBase {} public void shouldReifyRecursiveBound() { Type result = TypeResolver.reify(EnumBound.class, SubEnumBound.class); @@ -343,23 +352,58 @@ public void shouldReifyRecursiveBound() { // Navigate into enum parameter. assert parameterizedType.getActualTypeArguments()[0] instanceof ParameterizedType; - parameterizedType = (ParameterizedType)parameterizedType.getActualTypeArguments()[0]; + ParameterizedType parameterizedType2 = (ParameterizedType)parameterizedType.getActualTypeArguments()[0]; + assert parameterizedType.getActualTypeArguments()[0].equals(parameterizedType2); // Assert existence of loop - assert parameterizedType.getActualTypeArguments()[0] == parameterizedType; + assert parameterizedType2.getActualTypeArguments()[0] == parameterizedType2; + + assert !parameterizedType.equals(parameterizedType2); + } + + public void shouldReifyEnumBound() throws NoSuchFieldException { + Type result = TypeResolver.reify(SubEnumBound.class.getField("enumField").getGenericType(), SubEnumBound.class); + assert result instanceof ParameterizedType; + ParameterizedType parameterizedType = (ParameterizedType) result; + assert parameterizedType.toString().equals("java.lang.Enum<...>"); + + // Navigate into enum parameter. + assert parameterizedType.getActualTypeArguments()[0] instanceof ParameterizedType; + ParameterizedType parameterizedType2 = (ParameterizedType)parameterizedType.getActualTypeArguments()[0]; + assert parameterizedType.getActualTypeArguments()[0].equals(parameterizedType2); + + // Assert existence of loop + assert parameterizedType2.getActualTypeArguments()[0] == parameterizedType2; + + // Reify the same type again, which will create new instances of + // ReifiedParameterizedType that are not referentially equal, but + // equal to the previous result. + Type same = TypeResolver.reify(SubEnumBound.class.getField("enumField").getGenericType(), SubEnumBound.class); + assert same.equals(result); + + // Just to call hashCode() and equals() a bit more. + Set types = new HashSet<>(); + types.add(parameterizedType); + types.add(parameterizedType2); + types.add(same); + assert types.size() == 1; + assert types.contains(same); } public void shouldReifyMutuallyRecursiveBound() { Type result = TypeResolver.reify(MutuallyRecursiveBase.class, MutuallyRecursive.class); assert result instanceof ParameterizedType; ParameterizedType parent = (ParameterizedType) result; - + assert result.toString().equals("net.jodah.typetools.TypeResolverTest$MutuallyRecursiveBase, java.util.List<...>>"); assert parent.getActualTypeArguments()[0] instanceof ParameterizedType; ParameterizedType parameterizedType = (ParameterizedType)parent.getActualTypeArguments()[0]; assert parameterizedType.getActualTypeArguments()[0] == parameterizedType; + assert parameterizedType.getActualTypeArguments()[0].equals(parameterizedType); assert parent.getActualTypeArguments()[1] instanceof ParameterizedType; - parameterizedType = (ParameterizedType)parent.getActualTypeArguments()[1]; - assert parameterizedType.getActualTypeArguments()[0] == parameterizedType; + ParameterizedType parameterizedType2 = (ParameterizedType)parent.getActualTypeArguments()[1]; + assert parameterizedType2.getActualTypeArguments()[0] == parameterizedType2; + + assert parameterizedType.equals(parameterizedType2); } public void shouldReifyRecursiveOnSecondBound() {