Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic class type parameter detail at runtime

Libraries like Jackson can create objects from JSON if we provide enough information about generics.

In Jackson, we can do

    Class<?>[] parameterTypes; 
    JavaType type = objectMapper.getTypeFactory().constructParametricType(ObjectClass, parameterTypes);
    objectMapper.readValue(json, type);

In java, a Generic class can be defined in many ways, like one generic class having another generic class and that can have another generic class, for simple illustration consider these three classes.

  public class MultiLevelGenericTestData<T, V> {
    private GenericTestData<T> tGenericTestData;
    private GenericTestData<V> vGenericTestData;
  }

  public class MultiGenericTestData<K, V> {
    private K key;
    private V value;
  }
 
  public class MultiGenericTestDataSameType<T> {
    private GenericTestData<T> genericTestData;
    private MultiGenericTestData<T, T> multiGenericTestData;
  }

I know about type erasure and other things but is there a way to identify the type T, V from the object of MultiLevelGenericTestData?

One way I thought of check generic types and look at their name and inspect all the fields until you have found all the types. This quickly becomes tricky as soon as we hit the case where there's more than one field with the same generic type, for example in MultiGenericTestDataSameType, we should get only one generic type.

 // This method should find all type's class names in the list
 // that can be used to construct the object without any issue.
void genericFieldClassNames(List<String> types, List<String> classes, Object payload)
      throws IllegalAccessException {
    for (Field field : payload.getClass().getDeclaredFields()) {
      // ignorefield without annotation
      if (!field.isAnnotationPresent(GenericField.class)) {
        continue;
      }
      Type genericType = field.getGenericType();
      // not a generic field
      if (genericType.equals(field.getType())) {
        continue;
      }
      // null value nothing can be done
      Object fieldVal = FieldUtils.readField(field, payload, true);
      if (fieldVal == null) {
        continue;
      }
      String genericFieldType = genericType.getTypeName();
      Class<?> fieldClass = fieldVal.getClass();
      // problematic cases when we start traversing up 
      if (genericFieldType.endsWith(">")) {
        genericFieldClassNames(types, classes, fieldVal);
      } else {
        // here a check can be added to avoid duplicate type name but as soon as  
        // we add type genericFieldType check it will fail when we have used similar  
        // types in construction like MultiGenericTestData<String, String>
        types.add(genericFieldType);
        classes.add(fieldClass.getName());
      }
    }
  }

The number of type parameters can be found via the method getTypeParameters, how can we combine this to get exact type information.

Example

MultiLevelGenericTestData<String, String> data;

In this case, we should get [String, String]

MultiLevelGenericTestData<String, Integer> data;

In this case, we should get [String, Integer]

MultiGenericTestData<String, String> data;

In this case, we should get [String, String]

MultiGenericTestDataSameType<String> data;

In this case, we should get [String]

This becomes even more interesting when type T itself is generic for example

MultiGenericTestDataSameType< MultiGenericTestData< String, Integer> > data;

For this data, we should get MultiGenericTestData and it's generic parameters String and Integer.

Edit:

For further clarification, I would like to get type information without creating any additional class and that should be passed to the serializer i.e I don't want to change my serializer method signature that looks something similar to this []byte serialize(Object payload). We can create as many helper classes we need, also it can be made mandatory to extend payload class from some superclass, (superclass can have logic to extract generic information).

like image 856
sonus21 Avatar asked Nov 17 '20 10:11

sonus21


People also ask

Is generic type information present at runtime?

Is generic type information present at runtime? Generic type information is not present at runtime. C. You cannot create an instance using a generic class type parameter.

Which of these type parameter is used for a generic class?

Which of these type parameters is used for a generic class to return and accept any type of object? Explanation: T is used for type, A type variable can be any non-primitive type you specify: any class type, any interface type, any array type, or even another type variable. 3.

How many type parameters can be used in a generic class?

Multiple parameters You can also use more than one type parameter in generics in Java, you just need to pass specify another type parameter in the angle brackets separated by comma.

Can a generic class have multiple parameters?

A Generic class can have muliple type parameters.


1 Answers

This is a rather long answer, but should get you into a good starting position to do what you want.

The "trick" to obtain the generic types at runtime is rather old and the most famous (I guess) modern library that uses that is gson and guava. I guess jackson uses the same trick, because there is simply no other way to do it.

To put it simply, you need a class like this to begin with:

static abstract class MappingRegistrar<IN> {

    private final Type type;

    protected MappingRegistrar() {
        // more will be here shortly
    }
 
    // ... more will come here shortly

} 

If you want to create an instance of it, you are forced to provide a class that would extend it. So you are forced to write something like:

MappingRegistrar<String> one = new MappingRegistrar<>() {};

If you are forced to provide such a superclass, the trick (in the constructor) can take place:

static abstract class MappingRegistrar<IN> {

    private final Type type;

    protected MappingRegistrar() {
        Class<?> cls = getClass();
        Type[] type = ((ParameterizedType) cls.getGenericSuperclass()).getActualTypeArguments();
        this.type = type[0];
    }

}

And now you can find out the generic types. But that's not it. You need to correctly parse them, because a Type can be actually multiple things...

static abstract class MappingRegistrar<IN> {

    private final Type type;

    protected MappingRegistrar() {
        Class<?> cls = getClass();
        Type[] type = ((ParameterizedType) cls.getGenericSuperclass()).getActualTypeArguments();
        this.type = type[0];
    }

    public void seeIt() {
        innerSeeIt(type);
    }

    private void innerSeeIt(Type type) {
        if (type instanceof Class) {
            Class<?> cls = (Class<?>) type;
            boolean isArray = cls.isArray();
            if (isArray) {
                System.out.print(cls.getComponentType().getSimpleName() + "[]");
                return;
            }
            System.out.print(cls.getSimpleName());

        }

        if (type instanceof TypeVariable) {
            Type[] bounds = ((TypeVariable<?>) type).getBounds();
            String s = Arrays.stream(bounds).map(Type::getTypeName).collect(Collectors.joining(", ", "[", "]"));
            System.out.print(s);
        }

        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            String rawType = parameterizedType.getRawType().getTypeName();
            System.out.print(rawType + "<");
            Type[] arguments = parameterizedType.getActualTypeArguments();

            for (int i = 0; i < arguments.length; ++i) {
                innerSeeIt(arguments[i]);
                if (i != arguments.length - 1) {
                    System.out.print(", ");
                }

            }

            System.out.print(">");
            //System.out.println(Arrays.toString(arguments));
        }

        if (type instanceof GenericArrayType) {
            // you need to handle this one too
        }

        if (type instanceof WildcardType) {
            // you need to handle this one too, but it isn't trivial
        }
    }

}

It's not a complete implementation, but for some examples here is what it would print:

 public class Playground2<R extends Number & Serializable> {

    public static void main(String[] args) {
        new Playground2<Integer>().samples();
    }


    public void samples() {

        MappingRegistrar<String> one = new MappingRegistrar<>() {};
        one.seeIt();
        System.out.println("\n-------------");

        MappingRegistrar<String[]> two = new MappingRegistrar<>() {};
        two.seeIt();
        System.out.println("\n-------------");

        MappingRegistrar<R> three = new MappingRegistrar<>() {};
        three.seeIt();
        System.out.println("\n-------------");

        MappingRegistrar<MultiLevelGenericTestData<String, String>> four = new MappingRegistrar<>() {};
        four.seeIt();
        System.out.println("\n-------------");

        MappingRegistrar<MultiGenericTestDataSameType<MultiGenericTestData<String, Integer>>> five = new MappingRegistrar<>() {};
        five.seeIt();
        System.out.println("\n-------------");

    }
}

The results are:

String
-------------
String[]
-------------
[java.lang.Number, java.io.Serializable]
-------------
Playground2$MultiLevelGenericTestData<String, String>
-------------
Playground2$MultiGenericTestDataSameType<Playground2$MultiGenericTestData<String, Integer>>
-------------
like image 68
Eugene Avatar answered Oct 12 '22 23:10

Eugene