Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
280 views
in Technique[技术] by (71.8m points)

java - How do generics of generics work?

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

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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>();

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...