Home > front end >  Return class of generic type in default interface method
Return class of generic type in default interface method

Time:09-17

I have the following interface:

public interface Message<T> {

    //some other methods

    Class<T> getType();
}

Each implementation is always returning the class of T. For example:

public class StringMessage implements Message<String> {
     //other overrides

     @Override
     Class<String> getType() {
         return String.class;
     }
}

I would like to make this getType a default method of the interface, but that's not possible since I can't call T.class because of type erasure:

public interface Message<T> {
    //some other methods

    default Class<T> getType() {
        return T.class; //<-- not allowed
    }
}

Do you know any trick to avoid having to repeat the return XXX.class on each implementation?

Note that I can't bound T to anything, it must remain unbounded.

Note 1: if the answer is no and well explained, I'll accept it.

Note 2: I found many questions that "sound like this" but are not exactly the same (they usually refer to instances and not to the static interface itself). If you find the right duplicate, don't hesitate to mark it as such and I'll delete it.

CodePudding user response:

Yes. And No. It's complicated.

Before we go down this path, are you sure?

Generally, caring about the <T> in Class<T> is a code smell and means your API design is bad. For example, the T in generics can not represent primitives, but it can represent parameterized types; for example, Stream<List<? extends Foo & Bar>> is fine. An instance of java.lang.Class on the other hand can represent primitives (return int.class;), but cannot represent parameterized types. List<String>.class is not a thing, and there is no instance of j.l.Class that represents "a list of strings". List is as far as it goes.

Generally if you think you want a Class instance what you really wanted is a factory. A factory is the way to abstract away a constructor. Instead of returning a Class<T> this code should probably be wanting a Supplier<T> perhaps or some other interface-of-T that does the job of whatever you're currently using Class<?> for. If you're using that class instance for invoking .getConstructors(), then move that logic into an interface instead. Etcetera.

You've considered all that and still insist.

Generics are erased at runtime, yes, but they still exist in those places where they are part of signatures. In other words, in the extends and implements clauses of class definitions, in the types of fields, and in the parameters and return type of any method definition. The JVM considers these comments (the JVM does not know or care about generics in the slightest, it is purely something javac and editors worry about), but they are available in the class file and therefore you can theoretically at least query it.

But, and this is very important, only the literal thing that is there, at compile (write) time, is available.

So, yes, you CAN retrieve the String bit in public class StringMsg implements Message<String>. But what if you write public class GeneralMsg<T> implements Message<T>? Then all you get is T. What if you write public class ListOfStringsMessage implements Message<List<String>>? You can obtain List<String> here, but that notion cannot be conveyed in terms of a value of type java.lang.Class.

The way to do this, is to use the method .getGenericInterfaces(). But, this is a very low level method that just gets you literally what you asked for: The list of interfaces (with any type params preserved) that the class you invoked this on directly implements. Therefore, you need to write a ton of code. After all, maybe you have this:

class StringMessage implements Message<String> {}

class UnicornStringMessage extends StringMessage {}

or even

interface StringMessage implements Message<String> {}
class MyStringMessage implements StringMessage {}

You need to write lots of code to trawl through the entire class hierarchy. Hence, writing it all out in this answer is a bridge too far. This merely handles the very simplest case, and will fail on all others. So, you need to take this code and expand on it, and add:

  • Detect a misuse, such as writing class GenericMsg<T> implements Message<T>.
  • Detect hierarchy usage (or alternatively, disallow it with a proper message, if you prefer that) by way of having an interface that extends Message<Concrete>, and then having a class that implements the subinterface.
  • Same, but for interface SubIntf<T> implements Message<T> and then having a class Foo implements SubIntf<String>.
  • Same, but with the class hierarchy: class MyMsg implements Message<String> class MySubMsg extends MyMsg.

Taking that into consideration:

default Class<T> getType() {
    for (Type i : getClass().getGenericInterfaces()) {
        if (!(i instanceof ParameterizedType pt)) continue;
        if (!pt.getRawType().equals(Message.class)) continue;
        Type param = pt.getActualTypeArguments[0];
        if (param instanceof Class<?> paramC) return paramC;
    }
    throw new IllegalArgumentException("Programmer bug: "   getClass()   " must implements Message<ConcreteType>");
}
  • Related