Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Generics Puzzler, extending a class and using wildcards

I've been beating my head against this one for awhile and thought that maybe some fresh eyes will see the issue; thanks for your time.

import java.util.*;

class Tbin<T> extends ArrayList<T> {}
class TbinList<T> extends ArrayList<Tbin<T>> {}

class Base {}
class Derived extends Base {}

public class Test {
  public static void main(String[] args) {
    ArrayList<Tbin<? extends Base>> test = new ArrayList<>();
    test.add(new Tbin<Derived>());

    TbinList<? extends Base> test2 = new TbinList<>();
    test2.add(new Tbin<Derived>());
  }
}

Using Java 8. It looks to me like the direct creation of the container in test is equivalent to the container in test2, but the compiler says:

Test.java:15: error: no suitable method found for add(Tbin<Derived>)
    test2.add(new Tbin<Derived>());
         ^

How do I write Tbin and TbinList so the last line is acceptable?

Note that I will actually be adding typed Tbins which is why I specified Tbin<Derived> in the last line.

like image 373
user1677663 Avatar asked May 21 '15 21:05

user1677663


People also ask

How do you use generic wildcards?

Guidelines for Wildcards. Upper bound wildcard − If a variable is of in category, use extends keyword with wildcard. Lower bound wildcard − If a variable is of out category, use super keyword with wildcard. Unbounded wildcard − If a variable can be accessed using Object class method then use an unbound wildcard.

Why do we use wildcards in Java generics?

Wildcards in Java are basically the question marks which we use in generic programming, it basically represents the unknown type. We use Java Wildcard widely in situations such as in a type of parameter, local variable, or field and also as a return type.

Can a generic class be extended?

We can add generic type parameters to class methods, static methods, and interfaces. Generic classes can be extended to create subclasses of them, which are also generic.


4 Answers

This happens because of the way capture conversion works:

There exists a capture conversion from a parameterized type G<T1,...,Tn> to a parameterized type G<S1,...,Sn>, where, for 1 ≤ i ≤ n :

  • If Ti is a wildcard type argument of the form ? extends Bi, then Si is a fresh type variable [...].

Capture conversion is not applied recursively.

Note the end bit. So, what this means is that, given a type like this:

    Map<?, List<?>>
//      │  │    └ no capture (not applied recursively)
//      │  └ T2 is not a wildcard
//      └ T1 is a wildcard

Only "outside" wildcards are captured. The Map key wildcard is captured, but the List element wildcard is not. This is why, for example, we can add to a List<List<?>>, but not a List<?>. The placement of the wildcard is what matters.

Carrying this over to TbinList, if we have an ArrayList<Tbin<?>>, the wildcard is in a place where it does not get captured, but if we have a TbinList<?>, the wildcard is in a place where it gets captured.

As I alluded to in the comments, one very interesting test is this:

ArrayList<Tbin<? extends Base>> test3 = new TbinList<>();

We get this error:

error: incompatible types: cannot infer type arguments for TbinList<>
    ArrayList<Tbin<? extends Base>> test3 = new TbinList<>();
                                                        ^
    reason: no instance(s) of type variable(s) T exist so that
            TbinList<T> conforms to ArrayList<Tbin<? extends Base>>

So there's no way to make it work as-is. One of the class declarations needs to be changed.


Additionally, think about it this way.

Suppose we had:

class Derived1 extends Base {}
class Derived2 extends Base {}

And since a wildcard allows subtyping, we can do this:

TbinList<? extends Base> test4 = new TbinList<Derived1>();

Should we be able to add a Tbin<Derived2> to test4? No, this would be heap pollution. We might end up with Derived2s floating around in a TbinList<Derived1>.

like image 167
Radiodef Avatar answered Oct 19 '22 13:10

Radiodef


Replacing the definition of TbinList with

class TbinList<T> extends ArrayList<Tbin<? extends T>> {}

and defining test2 with

TbinList<Base> test2 = new TbinList<>();

instead would solve the issue.

With your definition you're ending up with an ArrayList<Tbin<T>> where T is any fixed class extending Base.

like image 23
tynn Avatar answered Oct 19 '22 14:10

tynn


You're using a bounded wildcard (TbinList<? extends Base>> ...). This wildcard will prevent you from adding any elements to the list. If you want more info, heres the section about Wildcards in the docs.

like image 3
Paul Avatar answered Oct 19 '22 12:10

Paul


You can define the generic types as follows:

class Tbin<T> extends ArrayList<T> {}
class TbinList<K, T extends Tbin<K>> extends ArrayList<T> {}

Then you would create instance like:

TbinList<? extends Base, Tbin<? extends Base>> test2 = new TbinList<>();
test2.add(new Tbin<Derived>());
like image 1
M A Avatar answered Oct 19 '22 13:10

M A