0

I am trying to retrieve the Class<?> representation of a type parameter at runtime like this:

public <E extends Exception> Try<T> onCatch (Consumer<E> onCatch) { 
    // retrieve `Class<E>`
}

While normally it is not possible to retrieve the types at runtime because of type erasure, due to reflection utilities and meta-data stored about generics in bytecode it should be possible like seen in this answer to the same question I have.

The accepted answer looks the following

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

but does not work for me, due to .getActualTypeArguments()[0] not being an instance of ParameterizedType but Class and thus failing the cast.

Is there any way to reliably do this? I know I can manually pass a Class<E> reference to the method but due to this being a library being able to just use the generic type would be much more convenient.

0xff
  • 181
  • 2
  • 11
  • What do you actually want to retrieve? The `E` in `Consumer` is not representable as a `Class`. You should be able to get it as a `TypeVariable` though. – Sweeper Mar 25 '22 at 17:02
  • The `E` is what I want to retrieve. If thought it was possible to be represented as a `Class`, [this answer](https://stackoverflow.com/questions/1901164/get-type-of-a-generic-parameter-in-java-with-reflection/1901275#1901275) makes it seem like that. How would I obtain this `TypeVariable`? – 0xff Mar 25 '22 at 17:04
  • You should be able to also get the bound of `E` - `Exception` - as an instance of `Class`. Is that what you want to do? – Sweeper Mar 25 '22 at 17:04
  • I need the specific subclass of `Exception` this `E` represents – 0xff Mar 25 '22 at 17:05
  • @Sweeper I tried this as well and the cast failed there as well, because `Type` was an instance of `Class` instead of `ParameterizedType`, the same as with the other "solution" – 0xff Mar 25 '22 at 17:15
  • Oh I see. It appears that as an implementation detail, javac doesn't emit the type parameter metadata when creating a lambda instance with invokedynamic... If the caller does `onCatch(new Consumer() { ... })`, then getting the generic interface would work correctly. – Sweeper Mar 25 '22 at 17:19
  • So when using a lambda literal without an explicit type definition this will not work in any way? This already helps me out a lot, thanks! – 0xff Mar 25 '22 at 17:21

2 Answers2

2

(not a good enough reputation to answer, but:)

I think it must be said that it's not only a "first-order" inheritance issue. As the following example shows:

abstract class Lifted<T> implements Consumer<T> { public abstract void accept(T x); }

<T> Consumer<T> lift(Consumer<T> f) { return new Lifted<T>() { @Override public void accept(T x) { f.accept(x);}};}

Consumer<String> g = System.out::println;
Consumer<String> c = lift(g);

((java.lang.reflect.ParameterizedType) c.getClass().getGenericSuperclass()).getActualTypeArguments();

The above example will print Type[1] { T } and doesn't contain any reference to the runtime-type String.

On JShell (haven't investigated a "standard" runtime), the type of g is a lambda whose direct super type is Object. I haven't found type parameters on it.

judekeyser
  • 21
  • 3
1

The reason why

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

works was because the superclass of this happens to be a class with a parameterised type as its superclass. Therefore you can get the actual type arguments of that type. The type parameters of superclasses are stored as metadata in the class file if you write them in the source file.

However, in your case, whatever is passed to the onCatch parameter is not going to have a superclass of Consumer<T>. After all, Consumer<T> is not a class! You need to use getGenericInterfaces and find the one that has the name that starts with java.util.function.Consumer.

System.out.println(
    // I've assumed the Consumer interface is the first one, to keep it brief
    ((ParameterizedType)onCatch.getClass().getGenericInterfaces()[0])
        .getActualTypeArguments()[0]
 );

This would work if the caller calls onCatch like this:

onCatch(new Consumer<RuntimeException>() {
    @Override
    public void accept(RuntimeException e) {

    }
});

The anonymous class will implement Consumer<RuntimeException>, and this information will be written to the class file representing the anonymous class.

However, if you use a lambda:

onCatch((RuntimeException e) -> {});

Then only a method like this is generated in the same class as the caller:

private static void lambda$caller$0(RuntimeException e) {

}

and at runtime, invokedynamic is used to create an instance that implements Consumer, and this is the bad news: the type parameter RuntimeException is not part of the generated class for this instance, for whatever reason.

The only way you can find RuntimeException now then, is if you somehow know who the caller is, and find the lambda$caller$0 method, and look at its parameter.

That said, everything I've wrote so far is pretty much all implementation detail, and I wouldn't use any of that in production code. I would say you should just add a Class<E> parameter:

onCatch(RuntimeException.class, e -> {});

It doesn't look that different on the caller's side anyway.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • "The reason why ... worked in the case of this question: ... was because the superclass of ArrayList happens to be AbstractList, which is a parameterised type. Therefore you can get the actual type arguments of that type." No. That code *does not*, and *cannot*, work in the case of that question. What is stored in the `ArrayList` class file is that its supertype is `AbstractList`. There is no concrete type argument stored in the `ArrayList` class file, and you cannot get any type argument at runtime from a `new ArrayList()`. – newacct Apr 13 '22 at 18:21
  • It is different in the case of your anonymous class example, because that anonymous class does have `Consumer` (with a fixed type argument hard-coded at the anonymous class's compile-time) stored as its superclass in its class file. – newacct Apr 13 '22 at 18:25
  • @newacct Huh, I'm now looking at this and wondering what I was thinking when I wrote that that day lol. I must have brainfarted... I have now edited. – Sweeper Apr 13 '22 at 18:29
  • @newacct now I wonder why the OP of that question accepted the answer in the first place… – Sweeper Apr 13 '22 at 18:51