Skip to content

Context API

There are 2 APIs: context API (primary) used to support introspection (reflection analysis) and direct utilities. Context API is safe because it always performs compatibility checks and throws descriptive exceptions. With utilities, usage errors are possible (quite possible to use wrong generics map), but in simple cases it's not a problem. Use API that fits best your use case.

Class hierarchy needs to be parsed to properly resolve all generics:

GenericsContext context = GenericsResolver.resolve(Root.class)

Now we can perform introspection of any class in the hierarchy (look methods or fields) and know exact generics.

If root class also contains generics, they are resolved by upper generic bound (e.g. <T extends Model> will be resolved as T=Model.class, and resolved as Object.class when no bounds set)

Resolved class hierarchy is cached internally, so it's cheap to resolve single class many times (call GenericsResolver.resolve(Class)).

Limit hierarchy resolution

You can limit hierarchy resolution depth (or exclude some interfaces) by providing ignored classes:

GenericsResolver.resolve(Root.class, Base.class, SomeInterface.class)

Here hierarchy resolution will stop on Base class (will not be included) and all appeared SomeInterface will be skipped (interfaces are also hierarchical so it could exclude sub hierarchy too).

Option exists for very rare cases when some types breaks analysis (as possible bug workaround).

Warning

When ignored classes specified, resolved generics information is not cached(!) even if complete type resolution was done before (descriptor always computed, but it's not expensive).

Context

GenericsResolver.resolve(Class) returns immutable context (GenericsContext) set to root class (by default).

Actually, context is a map (Map<Class, Map<String, Type>>) containing generics of all types in hierarchy:

class Root                           // [:]
  extends Base<Integer, Long>        // [T:Integer, K:Long]

Note that map could also contain visible outer type generics. E.g. for Outer<A>.Inner<B,C> A,B and C will be contained inside type generics map.

It is very important concept: you will always need to resolve types in context of particular class from hierarchy and context ties all methods to that class (selects correct generics collection).

To navigate on different class use

context.type(Base.class)

Which returns new instance of context. This method is used to navigate through all types in resoled class hierarchy. Note that new context will use the same root map of generics (it's just a navigation mechanism) and so there is no need to remember root context reference: you can navigate from any type to any type inside the resolved hierarchy.

For methods and constructors, which may contain extra generics, generics are resolved in time of method or constructor context navigation.

Context operates on types (Type), not classes, because only types holds all generics information, including composite generics info (e.g. List<List<String>>). Any type, obtained using reflection may be resolved through api to real class.

Important

See context API methods javadoc: it almost always contains example. Moreover, methods are grouped by name to simplify search (you can note generic.., resolve.., toString.. groups).

All classes in root class hierarchy may be obtained like this:

context.getGenericsInfo().getComposingTypes()

This will be all classes and interfaces in hierarchy (including root class), even if they not contain generics.

Tip

toString() called on context instance returns complete context with current position marker:

context.type(Base.class).toString():

class Root                           
  extends Base<Integer, Long>    <-- current

For example, in intellij idea, current context (with resolved generics) and position could be seen by using "view value" link inside debugger

Class generics

First group of context api methods targets context type own generics. All these methods starts from generic...

For example (in context of Base class),

context.genericsMap() == [T: Integer, K: Long] 

Returns complete mapping of generic names to resolved types, which may be used to some name based substitution.

context.genericsAsString() == ["Integer", "Long"]

Returns string representation of generic types, which may be used for logging or reporting. If generic value is parameterizable type then string will include it too: "List<String>".

See api for all supported methods.

Methods

When working with methods it's required to use method context:

MethodGenericsContext methodContext = context.method(methodInstance)

This will also check if method belongs to current hierarchy and switch context to methods's declaring class (in order to correctly solve referenced generics).

Special method context is important because of method generics, for example:

class A {

  public <T> T method(T arg) {
  ...
  }
}

Initially generics are resolved as type, so if you try to analyze generified method parameter it will fail, because of unknown generic (T).

Method context resolve method generics as upper bound. In the example above it would be: T == Object. But in more complex example:

class A<Q> {

  public <T extends Q, K extends Cloneable> T method(T arg, List<K> arg2) {
  ...
  }
}

class B extends A<Serializable>

Method generics upper bounds could be resolved as:

Method method = A.getMethod("method", Object.class, Cloneable.class)
GenericsResolver.resolve(B.class).method(method)
    .methodGenericTypes() == ["T": Serializable.class, "K": Cloneable.class]

Method context additional methods to work with parameters and return type:

methodContext.resolveParameters() == [Serializable.class, List.class]
methodContext.resolveReturnClass() == Serializable.class

Types resolution api (the same as in types context) will count method generics too:

methodContext.resolveGenericOf(method.getGenericParameterTypes()[1]) == Cloneable.class

Constructors

Constructor could declare generics like:

class Some {
    <T> Some(T arg);
}

To work with constructor generics, constructor context must be created:

ConstructorGenericsContext ctorContext = context.constructor(Some.class.getConstructor(Object.class))

By analogy with method context, constructor context contains extra methods for working with constructor generics and parameters.

Fields

Context contains special shortcut methods for working with fields. For example,

context.resolveFieldClass(field)

In essence, it's the same as: context.resolveClass(field.getGenericType())

It is just shorter and, if field does not belongs to current hierarchy, more concrete error will be thrown. But it would be IllegalArgumentException instead of WrongGenericsContextException because field case assumed to be simpler to track and more obvious to predict.

Types resolution

Both MethodGenericContext and ConstructorGenericContext extends from GenericsContext and so share common api. The only difference is that in method and context contexts amount of known generics could be bigger (due to method/constructor generics).

All type resolution api methods starts with 'resolve..'.

This api most likely will be used together with reflection introspection of classes in hierarchy (e.g. when searching for method or need to know exact method return type).

Any Type could be resolved to actual class (simpler to use in logic) and manual navigation to actual context type is not required.

Suppose we have more complex case:

class Base<T, K extends Collection<T> {
  K foo;
}

class Root extends Base<Integer, List<Integer>> {...}

And we need to know type and actual type of collection in field foo:

Field field = Base.class.getField("foo")
GenericsContext context = GenericsResolver.resolve(Root.class)
        // this is optional step (result will be the same even without it)
        .type(Base.class)
context.resolveClass(field.getGenericType()) == List.class
context.resolveGenericOf(field.getGenericType()) == Integer.class

Here you can see how both main class and generic class resolved from single type instance.

See api for all supported methods.

Note that type navigation (.type()) is important when you need to access exact type generics. For example, in order to use type's generics map in direct utility calls.

To string

class Base<T, K> {
  T doSomething(K arg);
}

class Root extends Base<Integer, Long> {...}

Any type could be resolved as string:

context.toStringType(doSomethingMethod.getGenericReturnType()) == "List<Integer>"

Or context class:

context.type(Base.class).toStringCurrentClass() == "Base<Integer, Long>"; 
context.type(Base.class).toStringCurrentClassDeclaration() == "Base<T, K>"

To string context method:

context.method(doSomethingMethod).toStringMethod() == "Integer doSomething(Long)"

By analogy, constructor context also contains toStringConstructor() method.