From da51fefb023e7d60850c8eb5ce8b9cf87f7806c4 Mon Sep 17 00:00:00 2001 From: James Roper Date: Mon, 2 Nov 2015 13:55:13 +1100 Subject: [PATCH] Support for just resolving type variables Added support for just resolving type variables into classes/parameterized types etc. --- .../net/jodah/typetools/TypeDescriptor.java | 30 +++++ .../net/jodah/typetools/TypeResolver.java | 113 +++++++++++++++--- .../typetools/functional/LambdaTest.java | 61 ++++++++++ 3 files changed, 186 insertions(+), 18 deletions(-) diff --git a/src/main/java/net/jodah/typetools/TypeDescriptor.java b/src/main/java/net/jodah/typetools/TypeDescriptor.java index 8c3b6b6..58e1c41 100644 --- a/src/main/java/net/jodah/typetools/TypeDescriptor.java +++ b/src/main/java/net/jodah/typetools/TypeDescriptor.java @@ -29,6 +29,10 @@ */ package net.jodah.typetools; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + /** * A Java field or method type. This class can be used to make it easier to manipulate type and * method descriptors. @@ -150,6 +154,32 @@ final class TypeDescriptor { public static final TypeDescriptor VOID_TYPE = new TypeDescriptor(VOID, null, ('V' << 24) | (5 << 16) | (0 << 8) | 0, 1); + private static final Map, Class> PRIMITIVES_TO_WRAPPERS; + + static { + Map, Class> map = new HashMap<>(); + map.put(boolean.class, Boolean.class); + map.put(byte.class, Byte.class); + map.put(char.class, Character.class); + map.put(double.class, Double.class); + map.put(float.class, Float.class); + map.put(int.class, Integer.class); + map.put(long.class, Long.class); + map.put(short.class, Short.class); + map.put(void.class, Void.class); + PRIMITIVES_TO_WRAPPERS = Collections.unmodifiableMap(map); + } + + public static Class primitiveToWrapper(Class clazz) { + if (clazz.isPrimitive()) { + Class wrapper = PRIMITIVES_TO_WRAPPERS.get(clazz); + if (wrapper != null) { + return wrapper; + } + } + return clazz; + } + /** * A buffer containing the internal name of this Java type. This field is only used for reference * types. diff --git a/src/main/java/net/jodah/typetools/TypeResolver.java b/src/main/java/net/jodah/typetools/TypeResolver.java index 48e5f70..b539ee1 100644 --- a/src/main/java/net/jodah/typetools/TypeResolver.java +++ b/src/main/java/net/jodah/typetools/TypeResolver.java @@ -17,13 +17,7 @@ import java.lang.ref.Reference; import java.lang.ref.WeakReference; -import java.lang.reflect.Array; -import java.lang.reflect.GenericArrayType; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; +import java.lang.reflect.*; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -181,6 +175,49 @@ public static Class[] resolveRawArguments(Type genericType, Class subType) return result; } + /** + * Returns an array of types representing arguments for the {@code genericType} using type variable information + * from the {@code subType}. Arguments for {@code genericType} that cannot be resolved are returned as + * {@code Unknown.class}. If no arguments can be resolved then {@code null} is returned. + * + * This method only resolves type variables, it does not resolve anything further than that. It is primarily useful + * for getting the types of lambda parameters. + * + * @param genericType to resolve arguments for + * @param subType to extract type variable information from + * @return array of types representing arguments for the {@code genericType} else {@code null} if no type + * arguments are declared + */ + public static Type[] resolveTypeArgumentVariables(Type genericType, Class subType) { + Type[] result = null; + Class functionalInterface = null; + + // Handle lambdas + if (SUPPORTS_LAMBDAS && subType.isSynthetic()) { + Class fi = genericType instanceof ParameterizedType + && ((ParameterizedType) genericType).getRawType() instanceof Class + ? (Class) ((ParameterizedType) genericType).getRawType() + : genericType instanceof Class ? (Class) genericType : null; + if (fi != null && fi.isInterface()) + functionalInterface = fi; + } + + if (genericType instanceof ParameterizedType) { + ParameterizedType paramType = (ParameterizedType) genericType; + result = paramType.getActualTypeArguments(); + } else if (genericType instanceof TypeVariable) { + result = new Type[1]; + result[0] = resolveTypeVariable((TypeVariable) genericType, subType, functionalInterface); + } else if (genericType instanceof Class) { + TypeVariable[] typeParams = ((Class) genericType).getTypeParameters(); + result = new Type[typeParams.length]; + for (int i = 0; i < typeParams.length; i++) + result[i] = resolveTypeVariable(typeParams[i], subType, functionalInterface); + } + + return result; + } + /** * Returns the generic {@code type} using type variable information from the {@code subType} else {@code null} if the * generic type cannot be resolved. @@ -229,7 +266,7 @@ public static Class resolveRawClass(Type genericType, Class subType) { private static Class resolveRawClass(Type genericType, Class subType, Class functionalInterface) { if (genericType instanceof Class) { - return (Class) genericType; + return TypeDescriptor.primitiveToWrapper((Class) genericType); } else if (genericType instanceof ParameterizedType) { return resolveRawClass(((ParameterizedType) genericType).getRawType(), subType, functionalInterface); } else if (genericType instanceof GenericArrayType) { @@ -246,6 +283,12 @@ private static Class resolveRawClass(Type genericType, Class subType, Clas return genericType instanceof Class ? (Class) genericType : Unknown.class; } + private static Type resolveTypeVariable(TypeVariable typeVariable, Class subType, Class functionalInterface) { + Type genericType = getTypeVariableMap(subType, functionalInterface).get(typeVariable); + genericType = genericType == null ? resolveBound(typeVariable) : genericType; + return genericType != null ? genericType : Unknown.class; + } + private static Map, Type> getTypeVariableMap(final Class targetType, Class functionalInterface) { Reference, Type>> ref = typeVariableCache.get(targetType); @@ -323,26 +366,60 @@ private static void populateLambdaArgs(Class functionalInterface, final Class } } + // Try to load the class that the method ref is defined on + Class ownerClass = TypeDescriptor.getObjectType(methodRefInfo[0]).getType(lambdaType.getClassLoader()); + + // Now load the argument classes + TypeDescriptor[] arguments = TypeDescriptor.getArgumentTypes(methodRefInfo[2]); + Class[] argumentClasses = new Class[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + argumentClasses[i] = arguments[i].getType(lambdaType.getClassLoader()); + } + + // Now try to locate the referenced method + Executable method = null; + + if (ownerClass != null) { + if (methodRefInfo[1].equals("")) { + method = ownerClass.getDeclaredConstructor(argumentClasses); + } else { + method = ownerClass.getDeclaredMethod(methodRefInfo[1], argumentClasses); + } + } + if (returnTypeVar instanceof TypeVariable) { - Class returnType = TypeDescriptor.getReturnType(methodRefInfo[2]).getType(lambdaType.getClassLoader()); - if (!returnType.equals(Void.class)) + Type returnType; + if (method != null) { + if (method instanceof Method) { + returnType = ((Method) method).getGenericReturnType(); + } else { + returnType = ownerClass; + } + } else { + returnType = TypeDescriptor.getReturnType(methodRefInfo[2]).getType(lambdaType.getClassLoader()); + } + if (!Void.class.equals(returnType)) map.put((TypeVariable) returnTypeVar, returnType); } - TypeDescriptor[] arguments = TypeDescriptor.getArgumentTypes(methodRefInfo[2]); - // Handle arbitrary object instance method references int offset = 0; if (paramTypeVars[0] instanceof TypeVariable && paramTypeVars.length == arguments.length + 1) { - Class instanceType = TypeDescriptor.getObjectType(methodRefInfo[0]) - .getType(lambdaType.getClassLoader()); - map.put((TypeVariable) paramTypeVars[0], instanceType); + map.put((TypeVariable) paramTypeVars[0], ownerClass); offset = 1; } - for (int i = 0; i < arguments.length; i++) - if (paramTypeVars[i + offset] instanceof TypeVariable) - map.put((TypeVariable) paramTypeVars[i + offset], arguments[i].getType(lambdaType.getClassLoader())); + for (int i = 0; i < arguments.length; i++) { + if (paramTypeVars[i + offset] instanceof TypeVariable) { + Type paramType; + if (method != null) { + paramType = method.getGenericParameterTypes()[i]; + } else { + paramType = arguments[i].getType(lambdaType.getClassLoader()); + } + map.put((TypeVariable) paramTypeVars[i + offset], paramType); + } + } break; } } diff --git a/src/test/java/net/jodah/typetools/functional/LambdaTest.java b/src/test/java/net/jodah/typetools/functional/LambdaTest.java index 02cc967..21573b9 100644 --- a/src/test/java/net/jodah/typetools/functional/LambdaTest.java +++ b/src/test/java/net/jodah/typetools/functional/LambdaTest.java @@ -1,8 +1,12 @@ package net.jodah.typetools.functional; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.Comparator; +import java.util.Optional; import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -243,4 +247,61 @@ private void handlePassedFunction(Function fn) { assertEquals(typeArgs[0], UUID.class); assertEquals(typeArgs[1], String.class); } + + /** + * Asserts that when resolving type argument variables for a method ref, the correct ParameterizedType is returned. + */ + public void shouldResolveMethodRefGenericArguments() { + Consumer> consumer = this::foo; + + Type[] arguments = TypeResolver.resolveTypeArgumentVariables(Consumer.class, consumer.getClass()); + assertEquals(arguments.length, 1); + Type argument = arguments[0]; + assertTrue(argument instanceof ParameterizedType); + ParameterizedType type = (ParameterizedType) argument; + assertEquals(type.getRawType(), Optional.class); + assertEquals(type.getActualTypeArguments()[0], String.class); + } + + private void foo(Optional foo) { + } + + /** + * Asserts that when resolving type argument variables for a lambda, the correct ParameterizedType is returned. + * + * Note: disabled since the Java compiler doesn't include generic information in synthetic lambda method signatures. + */ + @Test(enabled = false) + public void shouldResolveLambdaGenericArguments() { + Consumer> consumer = (Optional optString) -> System.out.println(optString); + + Type[] arguments = TypeResolver.resolveTypeArgumentVariables(Consumer.class, consumer.getClass()); + assertEquals(arguments.length, 1); + Type argument = arguments[0]; + assertTrue(argument instanceof ParameterizedType); + ParameterizedType type = (ParameterizedType) argument; + assertEquals(type.getRawType(), Optional.class); + assertEquals(type.getActualTypeArguments()[0], String.class); + } + + public static class Foo2 { + public Foo2(Optional arg) {} + } + + /** + * Asserts that when resolving type argument variables for a constructor ref, the correct ParameterizedType is + * returned. + */ + public void shouldResolveConstructorRefArguments() { + Consumer> consumer = Foo2::new; + + Type[] arguments = TypeResolver.resolveTypeArgumentVariables(Consumer.class, consumer.getClass()); + assertEquals(arguments.length, 1); + Type argument = arguments[0]; + assertTrue(argument instanceof ParameterizedType); + ParameterizedType type = (ParameterizedType) argument; + assertEquals(type.getRawType(), Optional.class); + assertEquals(type.getActualTypeArguments()[0], String.class); + } + }