How to replace a parameterized type with a more specific one

1.9k views Asked by At

Consider the following setup:

We have an interface SuperType which is parameterized like this:

public interface SuperType<V> {
}

SuperType supports method chaining. Hence it defines another type parameter which captures the concrete implementing subtype returned by each method like this:

public interface SuperType<V, S extends SuperType<V, S>> {

    public S doSomething();
}

Let's consider an implementation of SuperType<V, S extends SuperType<V, S>>:

public class SubType<V> implements SuperType<V, SubType<V>> {

    private final V value;

    public SubType(V value) { this.value = value; }

    public SubType<V> doSomething() { return this; }
}

Someone instantiates SubType<V> using for example strings but provides Object for the type parameter V:

Object s = "Java Generics";
SubType<Object> x = new SubType<>(s);

Now we want to define another method for SuperType<V, S extends SuperType<V, S>> which takes a more specific type parameter of V and returns the same implementation type S but now parameterized with W extends V:

public interface SuperType<V, S extends SuperType<V, S>> {

    public S doSomething();

    public <W extends V, T extends SuperType<W, T>> T doMoreSpecific(Class<W> typeToken);
}

This new interface definition is intended to support:

Object s = "Java Generics";
SubType<Object> x = new SubType<>(s);
SubType<String> y = x.doMoreSpecific(String.class);

Here I struggle to implement SubType<V>. What I want to provide as an implementation is:

public class SubType<V> implements SuperType<V, SubType<V>> {

    private final V value;

    public SubType(V value) { this.value = value; }

    public SubType<V> doSomething() { return this; };

    public <W extends V> SubType<W> doMoreSpecific(Class<W> typeToken) {
        return new SubType<>((W) value);
    }
}

My Question is:

How should I define the signature for the method doMoreSpecific() in the type SuperType<V, S extends SuperType<V, S>> so that the implementation provided by SubType<V> implements SuperType<V, SubType<V>> is acceptable?

Or otherwise, which implementation and interface method definition would do the trick?

Or else, why can't we do this in Java?

3

There are 3 answers

2
sp00m On BEST ANSWER

Using the following signature:

<W extends V> SuperType<W, ?> doMoreSpecific(Class<W> typeToken);

There might be some unsafe cases though that I have not been able to find yet, any criticism is welcome!

1
Bohemian On

You can't do what your code suggests you want, you have a typed method: W is inferred from the parameter type, but the parameter type is known only at the call site. ie there isn't one version of the method doMoreSpecific() that can be specified in the interface SuperType (that must be implemented).

The closest you could get is to make W a generic type parameter of SuperType, but then your implementation would work for exactly one class W, which would be the typeToken redundant, which is clearly not what you want.

0
ultimate On

I don't know what your exact intention is behind doMoreSpecific, but if it is just casting from SubType<Object> to SubType<String> you could do the following (although this is not very good practice...):

Object s = "Java Generics";
SubType<Object> x = new SubType<>(s);
SubType<String> y = (SubType<String>) (SubType<?>) x;

Note 1: This will still give a Warning.

Note 2: This cast will even work if s is not of Type String! But you will then get a ClassCastException when calling y.doSomething(). (Which you would get in your example for that case, too).

For example: The following piece of (ugly) code would work, too (except for the lines highlighted):

    ArrayList<?> lo = new ArrayList<Object>();
    ArrayList<Integer> li = (ArrayList<Integer>) lo;
    ArrayList<String> ls = (ArrayList<String>) lo;

    li.add(5);
    ls.add("five");

    System.out.println(lo);        // prints "[5, five]"

    System.out.println(li.get(0)); // prints "5"
    System.out.println(li.get(1)); // ClassCastException

    System.out.println(ls.get(0)); // ClassCastException
    System.out.println(ls.get(1)); // prints "five"

Note 3: This perfectly shows how generics in fact work: All they do is automatically insert casts at the required positions for you.