22

While I do understand some of the corner-cases of generics, I'm missing something with the following example.

I have the following class

1 public class Test<T> {
2   public static void main(String[] args) {
3     Test<? extends Number> t = new Test<BigDecimal>();
4     List<Test<? extends Number>> l =Collections.singletonList(t);
5   }
6 }

Line 4 gives me the error

Type mismatch: cannot convert from List<Test<capture#1-of ? extends Number>> 
to List<Test<? extends Number>>`. 

Obviously, the compiler thinks that the different ? are not really equal. While my gut-feeling tells me, this is correct.

Can anyone provide an example where I would get a runtime-error if line 4 was legal?

EDIT:

To avoid confusion, I replaced the =null in Line 3 by a concrete assignment

Bhesh Gurung
  • 50,430
  • 22
  • 93
  • 142
Jonathan
  • 2,698
  • 24
  • 37
  • 4
    You could make this compile by writing `Collections.> singletonList(t)`. – kennytm May 08 '13 at 20:44
  • 2
    By using `?` you are telling the compiler that they are not necessarily equal, if you wanted them to be treated as equal you would use `T` or some other placeholder type – Jason Sperske May 08 '13 at 20:45
  • KennyTM: Now I'm completely confused. This works but why? – Jonathan May 08 '13 at 20:51
  • I'm confused by this part of your question: ` can anyone provide an example where I would get a runtime-error if line 4 was legal?` Are you asking if a runtime error could be thrown assuming there wasn't a compile time error? – Jason Sperske May 08 '13 at 20:52
  • 1
    Take a look at [Wildcard Capture and Helper Methods](http://docs.oracle.com/javase/tutorial/java/generics/capture.html). – wchargin May 08 '13 at 20:53
  • @Jason Sperske: That's exactly the question. We all know, the compiler is over-cautious and disallows some constellations with generics which would actually work. My question is: Is this one of those situations or is there a concrete example which justifies the compiler to disallow code? – Jonathan May 08 '13 at 20:55
  • 1
    @Jonathan, my guess is the circumstances where you would want to pass different types that each extended Number (and not have things blow up) are better served by using `T` and having `T extends Number`. It looks like you are looking for flexibility to "do the right thing" that Generics (collections are just collections of Objects) were introduced to sort out (enforce some semblance of type safety) – Jason Sperske May 08 '13 at 21:01
  • I'll say this question is a whole lot more interesting (to me) than when I had originally read it :) – Jason Sperske May 08 '13 at 21:02
  • @Jason Sperske: I'm not actually trying to use this in actual code. I just stumbled accross this issue by accident and asked myself is there a reason to disallow this? – Jonathan May 08 '13 at 21:06
  • @KennyTM: Can you post your comment as an answer? I think, it shows, that the compiler is paranoid here. – Jonathan May 08 '13 at 21:42

5 Answers5

22

As Kenny has noted in his comment, you can get around this with:

List<Test<? extends Number>> l =
    Collections.<Test<? extends Number>>singletonList(t);

This immediately tells us that the operation isn't unsafe, it's just a victim of limited inference. If it were unsafe, the above wouldn't compile.

Since using explicit type parameters in a generic method as above is only ever necessary to act as a hint, we can surmise that it being required here is a technical limitation of the inference engine. Indeed, the Java 8 compiler is currently slated to ship with many improvements to type-inference. I'm not sure whether your specific case will be resolved.

So, what's actually happening?

Well, the compile error we're getting shows that the type parameter T of Collections.singletonList is being inferred to be capture<Test<? extends Number>>. In other words, the wildcard has some metadata associated with it that links it to a specific context.

  • The best way to think of a capture of a wildcard (capture<? extends Foo>) is as an unnamed type parameter of the same bounds (i.e. <T extends Foo>, but without being able to reference T).
  • The best way to "unleash" the power of the capture is by binding it to a named type parameter of a generic method. I'll demonstrate this in an example below. See the Java tutorial "Wildcard Capture and Helper Methods" (thanks for the reference @WChargin) for further reading.

Say we want to have a method that shifts a list, wrapping to the back. Then let's assume that our list has an unknown (wildcard) type.

public static void main(String... args) {
    List<? extends String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
    List<? extends String> cycledTwice = cycle(cycle(list));
}

public static <T> List<T> cycle(List<T> list) {
    list.add(list.remove(0));
    return list;
}

This works fine, because T is resolved to capture<? extends String>, not ? extends String. If we instead used this non-generic implementation of cycle:

public static List<? extends String> cycle(List<? extends String> list) {
    list.add(list.remove(0));
    return list;
}

It would fail to compile, because we haven't made the capture accessible by assigning it to a type parameter.

So this begins to explain why the consumer of singletonList would benefit from the type-inferer resolving T to Test<capture<? extends Number>, and thus returning a List<Test<capture<? extends Number>>> instead of a List<Test<? extends Number>>.

But why isn't one assignable to the other?

Why can't we just assign a List<Test<capture<? extends Number>>> to a List<Test<? extends Number>>?

Well if we think about the fact that capture<? extends Number> is the equivalent of an anonymous type parameter with an upper bound of Number, then we can turn this question into "Why doesn't the following compile?" (it doesn't!):

public static <T extends Number> List<Test<? extends Number>> assign(List<Test<T>> t) {
    return t;
} 

This has a good reason for not compiling. If it did, then this would be possible:

//all this would be valid
List<Test<Double>> doubleTests = null;
List<Test<? extends Number>> numberTests = assign(doubleTests);

Test<Integer> integerTest = null;
numberTests.add(integerTest); //type error, now doubleTests contains a Test<Integer>

So why does being explicit work?

Let's loop back to the beginning. If the above is unsafe, then how come this is allowed:

List<Test<? extends Number>> l =
    Collections.<Test<? extends Number>>singletonList(t);

For this to work, it implies that the following is allowed:

Test<capture<? extends Number>> capturedT;
Test<? extends Number> t = capturedT;

Well, this isn't valid syntax, as we can't reference the capture explicitly, so let's evaluate it using the same technique as above! Let's bind the capture to a different variant of "assign":

public static <T extends Number> Test<? extends Number> assign(Test<T> t) {
    return t;
} 

This compiles successfully. And it's not hard to see why it should be safe. It's the very use case of something like

List<? extends Number> l = new List<Double>();
Mark Peters
  • 80,126
  • 17
  • 159
  • 190
8

There is no potential runtime error, it's just outside the compiler's ability to statically determine that. Whenever you cause a type inference it automatically generates a new capture of <? extends Number>, and two captures are not considered equivalent.

Hence if you remove the inference from the invocation of singletonList by specifying <T> for it:

List<Test<? extends Number>> l = Collections.<Test<? extends Number>>singletonList(t);

It works fine. The generated code is no different than if your call had been legal, it's just a limitation of the compiler that it can't figure that out on its own.

The rule that an inference creates a capture and captures aren't compatible is what stops this tutorial example from compiling and then blowing up at runtime:

public static void swap(List<? extends Number> l1, List<? extends Number> l2) {
    Number num = l1.get(0);
    l1.add(0, l2.get(0));
    l2.add(0, num);
}

Yes the language specification and compiler probably could be made more sophisticated to tell your example apart from that, but it's not and it's simple enough to work around.

Affe
  • 47,174
  • 11
  • 83
  • 83
0

The reason is that the compiler doesn't know that your wildcard types are the same type.

It also doesn't know that your instance is null. Although null is a member of all types, the compiler considers only declared types, not what the value of the variable might contain, when type checking.

If the code executed, it wouldn't cause an exception, but that's only because the value is null. There is still a potential type mismatch, and that's what the compiler's job is - to disallow type mismatches.

Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • As I already wrote, I do understand, this is potentially dangerous. The question is, is there an example where there would be a runtime-error, if the line was legal – Jonathan May 08 '13 at 20:49
0

Take a look at type erasure. The problem is "compile time" is the only opportunity that Java has to enforce these generics, so if it let this through it wouldn't be able to tell if you tried to insert something invalid. This is actually a good thing, because it means that once the program compiles then Generics don't incur any performance penalty at run time.

Let's try and look at your example another way (let's use two types that extend Number but behave very differently). Consider the following program:

import java.math.BigDecimal;
import java.util.*;

public class q16449799<T extends Number> {
  public T val;

  public static void main(String ... args) {
    q16449799<BigDecimal> t = new q16449799<>();
    t.val = new BigDecimal(Math.PI);

    List<q16449799<BigDecimal>> l = Collections.singletonList(t);
    for(q16449799<BigDecimal> i : l) {
      System.out.println(i.val);
    }
  }
}

This outputs (as one would expect):

3.141592653589793115997963468544185161590576171875

Now assuming the code you presented didn't cause a compiler error:

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicLong;

public class q16449799<T extends Number> {
  public T val;

  public static void main(String ... args) {
    q16449799<BigDecimal> t = new q16449799<>();
    t.val = new BigDecimal(Math.PI);

    List<q16449799<AtomicLong>> l = Collections.singletonList(t);
    for(q16449799<AtomicLong> i : l) {
      System.out.println(i.val);
    }
  }
}

What would you expect the output to be? You can't reasonably cast a BigDecimal to an AtomicLong (you could construct an AtomicLong from the value of a BigDecimal, but casting and constructing are different things and Generics are implemented as compile time sugar to make sure casts are successful). As for @KennyTM's comment, a concrete type is seeking in when you initial example but try and compile this:

import java.math.BigDecimal;
import java.util.*;

public class q16449799<T> {
  public T val;

  public static void main(String ... args) {
    q16449799<? extends Number> t = new q16449799<BigDecimal>();

    t.val = new BigDecimal(Math.PI);

    List<q16449799<? extends Number>> l = Collections.<q16449799<? extends Number>>singletonList(t);
    for(q16449799<? extends Number> i : l) {
      System.out.println(i.val);
    }
  }
}

This will error out the moment you try and set a value to t.val.

Jason Sperske
  • 29,816
  • 8
  • 73
  • 124
  • How about the boxing/unboxing cost? Also, what is the performance penalty of "real" generics used in other languages? – Theodoros Chatzigiannakis May 08 '13 at 21:12
  • boxing/unboxing just relates to the conversion from primitive to Object and back (which applies to numbers and has a run time cost). Generics in Java were implemented on top of Java's type system and are amazingly backwards compatible ([retroweaver](http://retroweaver.sourceforge.net/) has added them to Java 1.4 by adding a jar to the class path (you still have to use java 1.5 to compile them). I can't speak for other languages though. – Jason Sperske May 08 '13 at 21:16
  • As I already wrote: I do have a bad gut feeling about the code but I can't think of concrete examples where it would go wrong. So, is there any? – Jonathan May 08 '13 at 21:21
  • @JasonSperske Right, sorry, bad choice of words. I meant to say upcasting/downcasting. I believe that generics with type erasure work by casting the input to `Object` and then casting any output from `Object` (exactly to preserve the compatibility you speak of). Is that right? And in this case, doesn't this casting have an overhead, compared to languages such as C# that actually JIT compile the different concrete implementations of the generic class and don't put any casts in between? – Theodoros Chatzigiannakis May 09 '13 at 06:15
0

Maybe this can explain the problem of the compiler:

List<? extends Number> myNums = new ArrayList<Integer>();

This genric wildcard list can hold any elements extending from Number. So its OK to assign an Integer list to it. However now I could add a Double to myNums because Double is also extending from Number which would lead to a runtime problem. So the compiler forbids every write access to myNums and I can only use read methods on it, because I only know what ever I get can be cast to Number.

And so the compiler is complaining about a lot of things you can do with such a wildcard generic. Sometimes he is mad about things which you can ensure they are safe and OK.

But luckily there is a trick to get around this error so you can test on your own what can maybe break this:

public static void main(String[] args) {

    List<? extends Number> list1 = new ArrayList<BigDecimal>();
    List<List<? extends Number>> list2 = copyHelper(list1);


}

private static <T> List<List<T>> copyHelper(List<T> list) {
    return Collections.singletonList(list);

}
mszalbach
  • 10,612
  • 1
  • 41
  • 53
  • I know that code, its somewhere in the Oracle-tutorial, unfortunately, it doesn't work with generics of generics. I tried both my example and your code and in both cases I get the `cannot convert` error – Jonathan May 08 '13 at 21:26
  • Yes you are right. My IDE did not complain about it any more but when I make the whole project I get the error too (with Java 1.7). Maybe they fixed this with some Java update. However the problem relies in the "?", which can not be used to create objects (this what the singeltonList is trying to do). – mszalbach May 08 '13 at 22:06