JDC
JDC

Reputation: 4385

Nested Generics Inheritance

I have the following classes:

class Field<T> {

  private final Class<T> type;

  public Field(Class<T> type) {
    this.type = type;
  }
}

class Pick<V> {
  private final V value;
  private final Class<V> type;

  public Pick(V value, Class<V> type) {
    this.value = value;
    this.type = type;
  }
}

and the class the question is related to:

class PickField<T> extends Field<Pick<T>> {

  public PickField(Class<Pick<T>> type) {
    super(type);
  }
}

Now this seems to be accepted by the compiler. Unfortunately I do not know/understand how I could create a new instance of PickField, e.g. for String picks.

This does - of course - not work:
new PickField<String>(Pick.class)

This is not allowed (I think I understand why):
new PickField<String>(Pick<String>.class)

So how to do it? Or does the whole approach somehow "smell"?

Upvotes: 18

Views: 1489

Answers (4)

fps
fps

Reputation: 34460

In order to pass generics information as an argument, Class<T> is not enough. You need some extra power to achive that. Please see this article, where it is explained what a super type token is.

In short, if you have the following class:

public abstract class TypeToken<T> {

    protected final Type type;

    protected TypeToken() {
        Type superClass = getClass().getGenericSuperclass();
        this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
    }

    public Type getType() {
        return this.type;
    }
}

You could use it to store generics type information, such as Pick<String>.class (which is illegal). The trick is to use the superclass' generic type information, which is accessible via the Class.getGenericSuperclass() and ParameterizedType.getActualTypeArguments() methods.

I have slightly modified your Pick, Field and PickField classes, so that they use a super type token instead of a Class<t>. Please see the modified code:

class Field<T> {

    private final TypeToken<T> type;

    public Field(TypeToken<T> type) {
        this.type = type;
    }
}

class Pick<V> {

    private final V value;

    private final TypeToken<V> type;

    public Pick(V value, TypeToken<V> type) {
        this.value = value;
        this.type = type;
    }
}

class PickField<T> extends Field<Pick<T>> {

    public PickField(TypeToken<Pick<T>> type) {
        super(type);
    }
}

And here is a sample usage:

TypeToken<Pick<String>> type = new TypeToken<Pick<String>>() {};
PickField<String> pickField = new PickField<>(type);

As TypeToken class is abstract, you need to subclass it (this explains the {} at the end of its declaration.

Upvotes: 0

Konstantin Yovkov
Konstantin Yovkov

Reputation: 62864

I think PickField should be parameterized with Pick instances only.

So doing this should be fine:

class PickField<T extends Pick<T>> extends Field<T> {

    public PickField(Class<T> c) {
        super(c);
    }
}

Then, you could just instantiate it with:

PickField<SomeSpecificPick> instance = new PickField<>(SomeSpecificPick.class);

where SomeSpecificPick is defined as:

public class SomeSpecificPick extends Pick<SomeSpecificPick> {

    public SomeSpecificPick(SomeSpecificPick value, Class<SomeSpecificPick> type) {
        super(value, type);
    }
}

More info (related with the topic):

Upvotes: 16

ZetQ
ZetQ

Reputation: 51

There is a way, but is not exactly a good one. You need to create a method like this:

public static <I, O extends I> O toGeneric(I input) {
    return (O) input;
}

Then, you create the object:

new PickField<String>(toGeneric(Pick.class));

Like I said, not really a good way, since you basically just lie to the compiler, but it works.

Upvotes: 1

Mena
Mena

Reputation: 48404

There are various issues here.

Firstly as you point out, you cannot obtain the class of a parametrized type at compile time, since only one class is compiled for generic types, not one per given type parameter (e.g. the Pick<String>.class idiom does not compile, and doesn't actually make sense).

Again as you mention, parametrizing the PickField<String> constructor with Pick.class only will not compile again, as the signatures aren't matched.

You could use a runtime idiom to infer the right Pick<T> parameter, but that creates another problem: due to to type erasure, your type argument for T will be unknown at runtime.

As such, you can parametrize your constructor invocation by explicitly casting, as follows:

new PickField<String>(
    (Class<Pick<String>>)new Pick<String>("", String.class).getClass()
);

... which will compile with an "unchecked cast" warning (Type safety: Unchecked cast from Class<capture#1-of ? extends Pick> to Class<Pick<String>>).

The real question is likely why do you need to know the value of type in your Pick class.

Upvotes: 7

Related Questions