51

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.

Radiodef
  • 37,180
  • 14
  • 90
  • 125
user1677663
  • 1,431
  • 15
  • 21

6 Answers6

37

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>.

Community
  • 1
  • 1
Radiodef
  • 37,180
  • 14
  • 90
  • 125
  • My [answer](http://stackoverflow.com/a/30385058/1129332) breaks your statement, with a trick of helping method to prevent the catch. – Ilya Gazman May 21 '15 at 22:43
  • @Ilya_Gazman Your answer uses an unchecked cast, and it doesn't work for a `TbinList extends Base>`. – Radiodef May 21 '15 at 22:47
  • I edit it now. The idea is the same. Why unchecked casting is bad in this case? – Ilya Gazman May 21 '15 at 22:55
  • @Ilya_Gazman Look at this example which uses it to cause heap pollution: http://ideone.com/bpKdMy. It's not type-safe. http://stackoverflow.com/a/2745301/2891664 So, yeah, of course we can do anything we want with unchecked casting. It's beside the point. It certainly doesn't invalidate my answer. – Radiodef May 21 '15 at 23:04
  • You always need to be carful when using unchecked casting. But in this case its the solution for the capture conversion. – Ilya Gazman May 21 '15 at 23:14
  • 1
    It's also interesting to think that, in `TbinList extends Base> test2 = new TbinList<>();`, what is T inferred to be? Well, T=Base :) So test2 is really a `TbinList`, and adding `Tbin` is definitely not good. – ZhongYu May 22 '15 at 00:03
10

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.

tynn
  • 38,113
  • 8
  • 108
  • 143
  • This is reasonable, assuming they don't ever need only e.g. a `TbinList` which is equivalent to an `ArrayList>`. – Radiodef May 21 '15 at 22:25
3

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.

  • strange, just copy pasted it. gimme a sec, i'll fix it –  May 21 '15 at 22:03
  • @Radiodef well, guess i should pay a bit more attention when copy pasting. again wrong line. thanks for pointing it out –  May 21 '15 at 22:05
  • That page describes what appears to be the same problem I'm having (at the bottom of the page), but it doesn't give a solution. – user1677663 May 21 '15 at 22:10
  • @user1677663 well, it clearly says that the bounded wildcard will prevent writing to the list. So the solution is pretty obvious: remove the bounded wildcard or replace it with `Base`. actually the wildcard isn't even necassary at all in this case –  May 21 '15 at 22:13
1

you cannot add any objects to TbinList<? extends Base> ,it is not guaranteed what objects you are inserting into the list. It is supposed to read data from test2 when you use wildcard extends

If you declared as TbinList<? extends Base> which means you it is any subclass of the class Base or class Base itself, and when you initialize it you use diamond other than concrete class name, it makes your test2 not obvious which makes it harder to tell what objects can be inserted. My suggestion is that avoid such declaration it is dangerous, it may not have compile errors but it is horrible code, you might add something, but you also might add the WRONG thing which will break your code.

Haifeng Zhang
  • 30,077
  • 19
  • 81
  • 125
  • 1
    You can, actually. `test2.add(new Tbin<>());` compiles fine, and clearly adds an empty list when I print `test2` before and after. – azurefrog May 21 '15 at 22:07
  • when I say `you cannot` I mean it is dangerous to add unclear type of objects :) I think I should use 'you'd better not' – Haifeng Zhang May 21 '15 at 22:12
1

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>());
M A
  • 71,713
  • 13
  • 134
  • 174
1

OK, here's the answer:

import java.util.*;

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

class Base {}
class Derived extends Base {}

public class Test {
  public static void main(String[] args) {

    TbinList<Base> test3 = new TbinList<>();
    test3.add(new Tbin<Derived>());

  }
}

As I expected, kind of obvious once I saw it. But a lot of thrashing around to get here. Java generics seem simple if you only look at working code.

Thanks, everyone, for being a sounding board.

user1677663
  • 1,431
  • 15
  • 21