简体   繁体   中英

Using a generic type of any nested subclass within its abstract superclass

Suppose you have the following abstract java class:

public abstract class AbstractRequestHandler<I,O> {
    I input;
    O output;
}

and the following child classes hierarchy:

public abstract class AbstractUserRequestHandler<I extends User,O> extends AbstractRequestHandler<I,O>{...}
public abstract class AbstractUniversityRequestHandler<I extends UniversityUser> extends AbstractUserRequestHandler<I,String>{...}
public class StudentRequestHandler extends AbstractUniversityRequestHandler<Student>{...}
public class TeacherRequestHandler extends AbstractUniversityRequestHandler<Teacher>{...}

Suppose you need to use at a given point on the super class the generic type, for example in order to deserialize on the constructor the request json to the specific request object using gson library as follow:

public AbstractRequestHandler(final String inputJson) {
        input = new Gson().fromJson(inputJson,typeOfI);
}

You need the type of generic I within variable "typeOfI"

Is there a global solution that allows to get the generic type specified by a concrete child class that respects the following constraints?

  1. The type is gotten at runtime regardless the child classes hierarchy ( that can be also more complex the one given as example on this question )
  2. The developer just needs to define the generic extending the super class without manually specify the generic type somewhere on concrete child class ( for example on overrided method or constructor )

So that if you want to define a new concrete child that assign a new value to a generic you can just write the following concrete class for example:

public class StudentRequestHandler extends AbstractUniversityRequestHandler<Student>{

    public StudentRequestHandler(String inputJson) {
        super(inputJson);
    }

}

I found the following solutions but they don't respect both the asked solution constraints.

Solution that breaks constraint n°2

A solution could be to define an abstract method on the superclass as follow

protected abstract Type getRequestType();

and then implement it on every concrete child class that defines the generic:

public class StudentRequestHandler extends AbstractUniversityRequestHandler<Student>{

    public StudentRequestHandler(String inputJson) {
        super(inputJson);
    }

    @Override
    protected Type getRequestType() {
        return Student.class;
    }
}

Then the getRequestType() method can be used on constructor on the target superclass:

public AbstractRequestHandler(final String inputJson) {
        request = new Gson().fromJson(inputJson,getRequestType());
}

But even if it works regardless the child classes hierarchy ( respect constraint n°1 ) the developer should manually implement an abstract method on each concrete child class.

Solution that breaks constraint n°1

If the hierarchy is simple having only a direct child that extend from the target superclass , as for example:

public class TeacherRequestHandler extends AbstractRequestHandler<Teacher,String>{...}

a working solution has been proposed by @naikus ( https://stackoverflow.com/users/306602/naikus ) on the following stackoverflow thread: Using a generic type of a subclass within it's abstract superclass?

However this doesn't work if the concrete class is not a direct child of the superclass that defines the generics ( as the one proposed as example on this question ).

Edit: after reading your answer and testing many other possible cases I decided to edit your code and re-write it to support all other possible edge cases to include tracking of generics nested deeply inside other generic types.

Sadly to support all cases we need a lot more code than you provided, generics are very tricky, like consider class like this:

private class SomeClass<A, B, C, D, E, F> {}

private class SomeConfusingClass<A> extends SomeClass<List<Void>[], List<? extends A>[], List<? extends A[][][]>[][][], List<? extends String[]>[], Map<List<? extends A[]>, A[][]>[], A> {}

private class TestClass extends SomeConfusingClass<Void> {}

To even start doing this we need to have own implementation of java generic types to later be able to construct types like List<String>[] as there is no way to create such type dynamically with raw java API.
This is pretty popular way of handling generic in libraries like that, you can see similar thing in jackson library and many more.
So we need implementation of GenericArrayType , ParameterizedType and WildcardType :

private static class ResolvedGenericArrayType implements GenericArrayType {
    private final Type genericComponentType;

    ResolvedGenericArrayType(Type genericComponentType) {
        this.genericComponentType = genericComponentType;
    }

    @Override
    public Type getGenericComponentType() {
        return genericComponentType;
    }

    public String toString() {
        return getGenericComponentType().toString() + "[]";
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof GenericArrayType) {
            GenericArrayType that = (GenericArrayType) o;
            return Objects.equals(genericComponentType, that.getGenericComponentType());
        } else
            return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(genericComponentType);
    }
}

private static class ResolvedParameterizedType implements ParameterizedType {
    private final Type[] actualTypeArguments;
    private final Class<?> rawType;
    private final Type ownerType;

    private ResolvedParameterizedType(Type rawType, Type[] actualTypeArguments, Type ownerType) {
        this.actualTypeArguments = actualTypeArguments;
        this.rawType = (Class<?>) rawType;
        this.ownerType = (ownerType != null) ? ownerType : this.rawType.getDeclaringClass();
    }

    public Type[] getActualTypeArguments() {
        return actualTypeArguments.clone();
    }

    public Class<?> getRawType() {
        return rawType;
    }

    public Type getOwnerType() {
        return ownerType;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ParameterizedType)) {
            return false;
        }
        ParameterizedType that = (ParameterizedType) o;
        if (this == that)
            return true;
        Type thatOwner = that.getOwnerType();
        Type thatRawType = that.getRawType();
        return Objects.equals(ownerType, thatOwner) && Objects.equals(rawType, thatRawType) &&
                Arrays.equals(actualTypeArguments, that.getActualTypeArguments());
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(actualTypeArguments) ^
                Objects.hashCode(ownerType) ^
                Objects.hashCode(rawType);
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        if (ownerType != null) {
            sb.append(ownerType.getTypeName());
            sb.append("$");
            if (ownerType instanceof ResolvedParameterizedType) {
                sb.append(rawType.getName().replace(((ResolvedParameterizedType) ownerType).rawType.getName() + "$", ""));
            } else
                sb.append(rawType.getSimpleName());
        } else
            sb.append(rawType.getName());
        if (actualTypeArguments != null) {
            StringJoiner sj = new StringJoiner(", ", "<", ">");
            sj.setEmptyValue("");
            for (Type t : actualTypeArguments) {
                sj.add(t.getTypeName());
            }
            sb.append(sj.toString());
        }
        return sb.toString();
    }
}

private static class ResolvedWildcardType implements WildcardType {
    private final Type[] upperBounds;
    private final Type[] lowerBounds;

    public ResolvedWildcardType(Type[] upperBounds, Type[] lowerBounds) {
        this.upperBounds = upperBounds;
        this.lowerBounds = lowerBounds;
    }

    public Type[] getUpperBounds() {
        return upperBounds.clone();
    }

    public Type[] getLowerBounds() {
        return lowerBounds.clone();
    }

    public String toString() {
        Type[] lowerBounds = getLowerBounds();
        Type[] bounds = lowerBounds;
        StringBuilder sb = new StringBuilder();
        if (lowerBounds.length > 0)
            sb.append("? super ");
        else {
            Type[] upperBounds = getUpperBounds();
            if (upperBounds.length > 0 && !upperBounds[0].equals(Object.class)) {
                bounds = upperBounds;
                sb.append("? extends ");
            } else
                return "?";
        }
        StringJoiner sj = new StringJoiner(" & ");
        for (Type bound : bounds) {
            sj.add(bound.getTypeName());
        }
        sb.append(sj.toString());
        return sb.toString();
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof WildcardType) {
            WildcardType that = (WildcardType) o;
            return Arrays.equals(this.getLowerBounds(), that.getLowerBounds()) && Arrays.equals(this.getUpperBounds(), that.getUpperBounds());
        } else
            return false;
    }

    @Override
    public int hashCode() {
        Type[] lowerBounds = getLowerBounds();
        Type[] upperBounds = getUpperBounds();
        return Arrays.hashCode(lowerBounds) ^ Arrays.hashCode(upperBounds);
    }
} 

You can basically copy them from JDK and just do some cleanup.

Next utility we need is a function to validate at the end if we did everything right, like we don't want to return Map<List<? extends X>[]> Map<List<? extends X>[]> where X is still not resolved TypeVariable :

private static boolean isDefined(Type type) {
    if (type instanceof Class) {
        return true;
    }
    if (type instanceof GenericArrayType) {
        return isDefined(((GenericArrayType) type).getGenericComponentType());
    }
    if (type instanceof WildcardType) {
        for (Type lowerBound : ((WildcardType) type).getLowerBounds()) {
            if (!isDefined(lowerBound)) {
                return false;
            }
        }
        for (Type upperBound : ((WildcardType) type).getUpperBounds()) {
            if (!isDefined(upperBound)) {
                return false;
            }
        }
        return true;
    }
    if (!(type instanceof ParameterizedType)) {
        return false;
    }
    for (Type typeArgument : ((ParameterizedType) type).getActualTypeArguments()) {
        if (!isDefined(typeArgument)) {
            return false;
        }
    }
    return true;
}

Simple recursive function will do this for us. We just check for every possible generic type and check if every member of it is also defined, and unless we will find some hidden TypeVariable we are fine.
Main function can stay the same as in your code, we only will edit that one check at the end to use our new function:

public static Type getParameterizedType(Class<?> klass, Class<?> rootClass, int paramTypeNumber) throws GenericsException {

    int targetClassParametersNumber = rootClass.getTypeParameters().length;
    if (targetClassParametersNumber == 0) {
        throw new GenericsException(String.format("Target class [%s] has no parameters type", rootClass.getName()));
    } else if (targetClassParametersNumber - 1 < paramTypeNumber)
        throw new GenericsException(String.format("Target class [%s] has parameters type which index start from [0] to [%s]. You requested instead parameter with index [%s]", rootClass, paramTypeNumber - 1, targetClassParametersNumber));

    Type type = analyzeParameterizedTypes(klass, klass, rootClass, paramTypeNumber, null);
    if (!isDefined(type))
        throw new GenericsException(String.format("Parameter [%s] with index [%d] defined on class [%s] has not been valued yet on child class [%s]", type, paramTypeNumber, rootClass.getName(), klass.getName()));
    return type;
}

Now lets work on our main

public static Type analyzeParameterizedTypes(final Class<?> klass, final Class<?> targetClass, final Class<?> rootClass, final int paramTypeNumber, Map<Integer, Type> childClassTypes) throws GenericsException { 

function, the begging stays the same, we collect all TypeVariable to simple map, keeping already collected information from previous loop on previous class.

    Type superclassType = klass.getGenericSuperclass();
    Map<TypeVariable<?>, Type> currentClassTypes = new HashMap<>();
    int z = 0;
    if (childClassTypes != null) {
        for (TypeVariable<?> variable : klass.getTypeParameters()) {
            currentClassTypes.put(variable, childClassTypes.get(z));
            z++;
        }
    }

Then we have our loop collecting and refining our type arguments:

    Map<Integer, Type> superClassesTypes = new HashMap<>();
    if (superclassType instanceof ParameterizedType) {
        int i = 0;
        for (final Type argType : ((ParameterizedType) superclassType).getActualTypeArguments()) {
            if (argType instanceof TypeVariable) {
                superClassesTypes.put(i, currentClassTypes.containsKey(argType) ? currentClassTypes.get(argType) : argType);
            } else {
                superClassesTypes.put(i, refineType(klass, argType, currentClassTypesByName));
            }
            i++;
        }
    }

There 2 paths for each type argument, if its TypeVariable we just keep tracking it, and if its anything else we try to "refine" it from any possible references to TypeVariable . This is the most complicated process of this code, and this is why we needed all these classes above.
We start from this simple recursive dispatch method that handles all possible types:

private static Type refineType(Type type, Map<TypeVariable<?>, Type> typeVariablesMap) throws GenericsException {
    if (type instanceof Class) {
        return type;
    }
    if (type instanceof GenericArrayType) {
        return refineArrayType((GenericArrayType) type, typeVariablesMap);
    }
    if (type instanceof ParameterizedType) {
        return refineParameterizedType((ParameterizedType) type, typeVariablesMap);
    }
    if (type instanceof WildcardType) {
        return refineWildcardType((WildcardType) type, typeVariablesMap);
    }
    if (type instanceof TypeVariable) {
        return typeVariablesMap.get(type);
    }
    throw new GenericsException("Unsolvable generic type: " + type);
}

And small utility method to run it on array of types:

private static Type[] refineTypes(Type[] types, Map<TypeVariable<?>, Type> typeVariablesMap) throws GenericsException {
    Type[] refinedTypes = new Type[types.length];
    for (int i = 0; i < types.length; i++) {
        refinedTypes[i] = refineType(types[i], typeVariablesMap);
    }
    return refinedTypes;
}

Each type goes to own function, or if its TypeVariable we just fetch resolved one from map. Note that this can return null, and I did not handle it here. This could be improved later. For classes we don't need to do anything so we can just return class itself.

For GenericArrayType we need to first find out how many dimension such array might have (this could be handled by recursion in our refine method too, but then its a bit harder to debug in my opinion):

private static int getArrayDimensions(GenericArrayType genericArrayType) {
    int levels = 1;
    GenericArrayType currentArrayLevel = genericArrayType;
    while (currentArrayLevel.getGenericComponentType() instanceof GenericArrayType) {
        currentArrayLevel = (GenericArrayType) currentArrayLevel.getGenericComponentType();
        levels += 1;
    }
    return levels;
}

Then we want to extract that nested component type of array, so for List<A>[][][] we want just List<A> :

private static Type getArrayNestedComponentType(GenericArrayType genericArrayType) {
    GenericArrayType currentArrayLevel = genericArrayType;
    while (currentArrayLevel.getGenericComponentType() instanceof GenericArrayType) {
        currentArrayLevel = (GenericArrayType) currentArrayLevel.getGenericComponentType();
    }
    return currentArrayLevel.getGenericComponentType();
}

And then we need to refine this type, so our List<A> will change to eg List<String> :

    Type arrayComponentType = refineType(getArrayNestedComponentType(genericArrayType), typeVariablesMap);

And rebuild our generic structure using refined type, so our created List<String> will change back to List<String>[][][] :

private static Type buildArrayType(Type componentType, int levels) throws GenericsException {
    if (componentType instanceof Class) {
        return Array.newInstance(((Class<?>) componentType), new int[levels]).getClass();
    } else if (componentType instanceof ParameterizedType) {
        GenericArrayType genericArrayType = new ResolvedGenericArrayType(componentType);
        for (int i = 1; i < levels; i++) {
            genericArrayType = new ResolvedGenericArrayType(genericArrayType);
        }
        return genericArrayType;
    } else {
        throw new GenericsException("Array can't be of generic type");
    }
}

And whole function looks like this:

private static Type refineArrayType( GenericArrayType genericArrayType, Map<TypeVariable<?>, Type> typeVariablesMap) throws GenericsException {
    int levels = getArrayDimensions(genericArrayType);
    Type arrayComponentType = refineType(getArrayNestedComponentType(genericArrayType), typeVariablesMap);
    return buildArrayType(arrayComponentType, levels);
}

For ParameterizedType its much simpler, we just refine type arguments, and create new ParameterizedType instance with these refined arguments:

private static Type refineParameterizedType(ParameterizedType parameterizedType, Map<TypeVariable<?>, Type> typeVariablesMap) throws GenericsException {
    Type[] refinedTypeArguments = refineTypes(parameterizedType.getActualTypeArguments(), typeVariablesMap);
    return new ResolvedParameterizedType(parameterizedType.getRawType(), refinedTypeArguments, parameterizedType.getOwnerType());
}

Same for WildcardType :

private static Type refineWildcardType(WildcardType wildcardType, Map<TypeVariable<?>, Type> typeVariablesMap) throws GenericsException {
    Type[] refinedUpperBounds = refineTypes(wildcardType.getUpperBounds(), typeVariablesMap);
    Type[] refinedLowerBounds = refineTypes(wildcardType.getLowerBounds(), typeVariablesMap);
    return new ResolvedWildcardType(refinedUpperBounds, refinedLowerBounds);
}

And this leaves us with whole analyze function looking like this:

public static Type analyzeParameterizedTypes(final Class<?> klass, final Class<?> targetClass, final Class<?> rootClass, final int paramTypeNumber, Map<Integer, Type> childClassTypes) throws GenericsException {
    Type superclassType = klass.getGenericSuperclass();
    Map<TypeVariable<?>, Type> currentClassTypes = new HashMap<>();
    int z = 0;
    if (childClassTypes != null) {
        for (TypeVariable<?> variable : klass.getTypeParameters()) {
            currentClassTypes.put(variable, childClassTypes.get(z));
            z++;
        }
    }

    Map<Integer, Type> superClassesTypes = new HashMap<>();
    if (superclassType instanceof ParameterizedType) {
        int i = 0;
        for (final Type argType : ((ParameterizedType) superclassType).getActualTypeArguments()) {
            if (argType instanceof TypeVariable) {
                superClassesTypes.put(i, currentClassTypes.getOrDefault(argType, argType));
            } else {
                superClassesTypes.put(i, refineType(argType, currentClassTypes));
            }
            i++;
        }
    }

    if (klass != rootClass) {
        final Class<?> superClass = klass.getSuperclass();
        if (superClass == null)
            throw new GenericsException(String.format("Class [%s] not found on class parent hierarchy [%s]", rootClass, targetClass));
        return analyzeParameterizedTypes(superClass, targetClass, rootClass, paramTypeNumber, superClassesTypes);
    }
    return childClassTypes.get(paramTypeNumber);

}

Example usage:

private class SomeClass<A, B, C, D, E, F> {}
private class SomeConfusingClass<A> extends SomeClass<List<Void>[], List<? extends A>[], List<? extends A[][][]>[][][], List<? extends String[]>[], Map<List<? extends A[]>, A[][]>[], A> {}
private class TestClass extends SomeConfusingClass<Void> {}

public static void main(String[] args) throws Exception {
    System.out.println(GenericsUtils.getParameterizedType(TestClass.class, SomeClass.class, 0));
    System.out.println(GenericsUtils.getParameterizedType(TestClass.class, SomeClass.class, 1));
    System.out.println(GenericsUtils.getParameterizedType(TestClass.class, SomeClass.class, 2));
    System.out.println(GenericsUtils.getParameterizedType(TestClass.class, SomeClass.class, 3));
    System.out.println(GenericsUtils.getParameterizedType(TestClass.class, SomeClass.class, 4));
    System.out.println(GenericsUtils.getParameterizedType(TestClass.class, SomeClass.class, 5));
}

And results:

java.util.List<java.lang.Void>[]
java.util.List<? extends java.lang.Void>[]
java.util.List<? extends java.lang.Void[][][]>[][][]
java.util.List<? extends java.lang.String[]>[]
java.util.Map<java.util.List<? extends java.lang.Void[]>, java.lang.Void[][]>[]
class java.lang.Void

Whole code with tests can be found here: https://gist.github.com/GotoFinal/33b9e282f270dbfe61907aa830c27587 or here: https://github.com/GotoFinal/generics-utils/tree/edge-cases-1

Based on OP original answer code, but with most of edge cases covered.

You can't get generic parameter without inheritence because of generics in java has type erasure : https://docs.oracle.com/javase/tutorial/java/generics/erasure.html

You need to do approach which you've been shared in your question. Using inheritence and ParameterizedType :

(Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];

The answer is: Java does not support reified generics, see this feature request from 2004 with lots of duplicates. See also:

So unless you want to switch to Kotlin, there is simply nothing you can do because generic type information in Java is available to the compiler only, not during runtime (reified generics).

I am sorry if you do not like the answer, but still it is correct as of Java 13 in early 2020.

I think a working solution would be to extend the one proposed by @naikus. It only needs to go up in the hierarchy on the constructor.

import java.lang.reflect.ParameterizedType;

public abstract class AbstractRequestHandler<I,O> {

    protected I input;
    protected O output;

    protected Class<I> inputClass;
    protected Class<O> outputClass;

    protected AbstractRequestHandler() {
        Class<?> clazz = getClass();
        while (!clazz.getSuperclass().equals(AbstractRequestHandler.class)) {
            clazz = clazz.getSuperclass();
        }
        ParameterizedType genericSuperclass = (ParameterizedType) clazz.getGenericSuperclass();
        this.inputClass = (Class<I>) genericSuperclass.getActualTypeArguments()[0];
        this.outputClass = (Class<O>) genericSuperclass.getActualTypeArguments()[1];
    }
}

I've worked to an utility library that offers a method that generally solve the question analyzing recursively all parent classes hierarchy to get a specific generic type.

It is available on my GitHub project: https://github.com/gregorycallea/generics-utils

UPDATE : Thanks to @GoToFinal user that with his great effort improved the project covering also several differents complex generics case ( such as GenericArrayType , ParameterizedType and WildcardType ). For all details about these improvements see his answer on this question.


This is the summarized scenario the method works on:

  1. Suppose you have a parameterized root class with an undefined number of generics defined.

Example: Let's consider as root class the following " Base " class that defines 3 generics:

 private class Base<I, E, F> { I var1; E var2; F var3; }

NOTE: To each generic is assigned an index starting from 0. So index mapping for this class is:

 I = 0 E = 1 F = 2
  1. Suppose this root class have a complex and multi-leveled hierarchy of child classes.

Example:

 // Base<I,E,F> // BaseA<G,H> extends Base<H,Boolean,G> // BaseB<T> extends BaseA<T,String> // BaseC<H> extends BaseB<H> // BaseD extends BaseC<Integer> // BaseE extends BaseD // BaseF extends BaseE // BaseG extends BaseF // BaseH<H> extends BaseG<H,Double> // BaseI<T> extends BaseF<T> // BaseL<J> extends BaseI<J> // BaseM extends BaseL<Float> // BaseN extends BaseM

NOTE: Notice that walking the child hierarchy new parameterized classes are defined and also some classes are not parameterized at all

  1. Then suppose you want to choose whatever class on root class child hierarchy and then get the exactly type of a specific generic defined on root class starting from this.

Example: You want to know the type of E generic ( with index = 1 ) defined on Base class starting from child class BaseN .

To do this you can simply execute the GenericsUtils.getParameterizedType method as follow:

Type targetType = GenericsUtils.getParameterizedType(GenericChildClass.class, RootClass.class, genericRootClassIndex);

Example:

 Type EType = GenericsUtils.getParameterizedType(BaseN.class, Base.class, 1);

I evaluated several cases for this example scenario with unit tests. Take a look at: https://github.com/gregorycallea/generics-utils/blob/master/src/test/java/com/github/gregorycallea/generics/GenericsUtilsTest.java


About the initial scenario exposed on my question instead we can use this method on AbstractRequestHandler constructor as follow:

public abstract class AbstractRequestHandler<I,O> {
        I input;
        O output;

        public AbstractRequestHandler(String inputJson) throws GenericsException {
            this.input = new Gson().fromJson(inputJson,GenericsUtils.getParameterizedType(getClass(), AbstractRequestHandler.class, 0));
        }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM