6

Consider the following generic converter class (simplified/incomplete for brevity)...

public abstract class BaseConverter <TModel>
{
    public void convert(String data, Class<TModel> classOfModelType)
    {
    }

    public void convert(String data)
    {
        convert(data, TModel.class); // This doesn't work!
    }
}

Externally in the subclass I can call the first convert without issue. Consider this subclass with Car as the generic type argument. I can pass in Car.class, like so...

public class CarConverter extends BaseConverter<Car>
{
    public void someFunction()
    {
        String someString;
        convert(someString, Car.class);
    }
}

What I'm trying to do is move as much as possible to the base class. Since 'Car' is known here and therefore convert expects Car.class, and TModel represents Car in the base class (for this concrete BaseConverter anyway), I was trying to use TModel.class in the base class, but as stated, it doesn't work.

So how do you achieve this in Java?

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • I don't think there is an equivalent. The type is unknown at compile time, so what would the compiler substitute? You pass in the class type in `convert`, why not use that same technique? Add a class type to the ctor, it'll give you a base class to use for the converter. – markspace Aug 03 '17 at 18:51
  • But that's just it... it *is* known at compile time. For instance, if I add a function in BaseConverter that returned TModel, if you look at the signature on the subclass, it would be returning Car because the compiler knows that type. The point is the compiler does know what's passed into it or else it couldn't resolve any of the usages. Other languages allow this. Guess it's just a limitation of Java. – Mark A. Donohoe Aug 03 '17 at 18:58
  • Actually I think what you want to achieve in the second `convert` method is what the first `convert` *exactly* does. The `Class` will be the `Class` object of the generic type argument of the subclass. – M A Aug 03 '17 at 19:01
  • Maybe what you wanted to achieve in the first `convert` should have been something like: `public void convert(String data, Class extends TModel> classOfModelType) { }` – M A Aug 03 '17 at 19:02
  • But isn't TModel only a placeholder for the type? How can you extend something that doesn't actually exist? I admit I'm new to Java so I'm not sure what this is as I've never seen '? extends TModel' before. – Mark A. Donohoe Aug 03 '17 at 19:09

3 Answers3

4

This is a limitation of generics in Java. The generics only exist at compile time, i.e. at runtime the object is just a BaseConverter, and so you can't query it about its generic type. The easiest solution is usually to pass in a Class<TModel> object (as you are doing) when you call the method. You can also pass it to the constructor for BaseConverter if you don't want to have to pass it in multiple times.

csander
  • 1,385
  • 10
  • 11
  • It's just frustrating because as you said, at runtime it's compiled to BaseConverter essentially replacing TModel with Car. In other words, if you have four subclasses that pass in four different TModel types, you end up with four concrete classes (i.e. all types *are* known at compile time) and the generic doesn't actually exist anywhere. Why it can't get the class at compile time in that case is beyond me and very frustrating. – Mark A. Donohoe Aug 03 '17 at 19:01
  • 2
    You don't end up with 4 classes, but with just one: the one in which `TModel` is replaced with `Object`. So the initial type information is lost (the actual term is _erased_) and it cannot be restored. – Roman Puchkovskiy Aug 03 '17 at 19:04
  • Yeah, I just read that. That's how Java differs from other languages where Generics are first-class citizens. There you do end up with four new concrete types. Such a disappointment how they implemented it here in Java when being spoiled with how things could be. – Mark A. Donohoe Aug 03 '17 at 19:11
3

Store a reference to the concrete Class by accepting it in the BaseConverter constructor. Something like this:

public abstract class BaseConverter <TModel>
{
    private Class<TModel> clazz;

    public BaseConverter(Class<TModel> clazz) {
        this.clazz = clazz;
    }

    public void convert(String data, Class<TModel> classOfModelType)
    {
    }

    public void convert(String data)
    {
        convert(data, clazz); 
    }
}

public class CarConverter extends BaseConverter<Car>
{
    public CarConverter() {
        super(Car.class);
    }

    public void someFunction()
    {
        String someString;
        convert(someString, Car.class);
    }
}
GriffeyDog
  • 8,186
  • 3
  • 22
  • 34
  • While I know I can do that, and I do appreciate that I can then omit it in other cals, I was trying to infer as much as possible from the generic's type so I wouldn't have to do that. After all, at compile time, internally it will know they are Car types, so how can it not know the Car class? Other languages allow this kind of thing so it looks to be a limitation of the Java compiler/language, not the design of generics themselves. – Mark A. Donohoe Aug 03 '17 at 19:04
  • At compile time, in `BaseConverter`, all it knows is the generic type `TModel`. Why would the compiler think `TModel` = `Car`? After all, it's not like `CarConverter` is the _only_ class that can extend `BaseConverter`. – GriffeyDog Aug 03 '17 at 19:16
  • But that's just it... the compiler (in other languages) *is not* compiling TModel. They are compiling Car and all the other instances. If you create ten subclasses of a single generic, specifying ten different type parameters, you end up with ten concrete types, again, in other languages. I only found out today about Java's type erasure which in the case I just described you only have *one* type, not ten, and TModel is essentially changed to Object. That was the missing piece of information. I incorrectly assumed that like in other languages, Java did the same thing, but it doesn't. – Mark A. Donohoe Aug 03 '17 at 23:36
2

Generics are erased during compilation and do not exist at run time, so T.class is not possible to do. If you have BaseConverter<T> in your code, it will become BaseConverter<Object> at run time.

But you can save the class yourself:

public abstract class BaseConverter <TModel> {
    private final Class<TModel> clazz;

    protected BaseConverter(Class<TModel> clazz) {
        this.clazz = clazz;
    }

    public void someFunction()
    {
        String someString;
        convert(someString, clazz); // use class here
    }
}

public class CarConverter extends BaseConverter<Car> {
    public CarConverter() {
        super(Car.class);
    }
}
Roman Puchkovskiy
  • 11,415
  • 5
  • 36
  • 72