泛型的泛型如何工作? [英] How do generics of generics work?
问题描述
虽然我的确了解了一些泛型的角落案例,但我错过了以下示例。
我有以下班级
1 public class Test< T> {
2 public static void main(String [] args){
3 Test<扩展Number> t = new Test< BigDecimal>();
4清单<测试<扩展Number>> l = Collections.singletonList(t);
5}
6}
第4行给出错误
类型不匹配:无法从List< Test< capture#1-of?扩展Number>>
到List< Test<扩展Number>>`。
显然,编译器认为不同的?
并不完全相同。尽管我的直觉告诉我,但这是正确的。
任何人都可以提供一个例子,说明如果第4行是合法的,我会得到运行时错误?
<为了避免混淆,我将第3行中的 = null
替换为具体的任务
正如Kenny在他的评论中指出的那样,您可以通过以下方式解决这个问题:
列表<测试<?扩展Number>> l =
收藏。<测试< extends Number>> singletonList(t);
这立即告诉我们该操作不是不安全的,它只是有限推论的受害者。如果它不安全,上面的代码就不会编译。
由于在上面的泛型方法中使用显式类型参数只能用作提示,所以我们可以推测它是必需的这是推理机的技术限制。事实上,Java 8编译器目前预计会随许多类型推理改进一起发布。我不确定你的具体情况是否会得到解决。
那么,究竟发生了什么?
好,我们得到的编译错误显示 Collections.singletonList
的类型参数 T
被推断为捕获<试验< ;?扩展Number>>
。换句话说,通配符具有一些与其关联的元数据,将其链接到特定的上下文。
- 想想捕获通配符的最佳方法(
capture< ;? extends Foo>
)是相同边界的 unnamed 类型参数(即< T extends Foo>
),但不能引用T
)。 - 释放捕获能力的最佳方法是将其绑定到通用方法的指定类型参数。我将在下面的示例中演示这一点。请参阅Java教程通配符捕获和帮助器方法(感谢您参考@WChargin )进一步阅读。
假设我们希望有一种方法可以将列表转移到后面。然后让我们假设我们的列表有一个未知(通配符)类型。
public static void main(String ... args){
列表< ;? extends String> list = new ArrayList<>(Arrays.asList(a,b,c));
列表< ;? extends String> cycledTwice =循环(循环(列表));
}
public static< T>列表与LT; T>周期(列表< T>列表){
list.add(list.remove(0));
返回列表;
$ / code>
这很好,因为 T
解析为捕获<?扩展字符串>
,而不是?扩展字符串
。如果我们改用循环的非通用实现:
public static List <? extends String>循环(List< ;? extends String> list){
list.add(list.remove(0));
返回列表;
}
它将无法编译,因为我们还没有通过因此,这开始解释了为什么 singletonList
的用户可以从类型中受益-inferer解析 T
为测试<捕获<扩展Number>
,并因此返回一个 List< Test< capture<扩展Number>>>
而不是 List< Test< ;?扩展Number>>
。
但为什么不能分配给另一个?
为什么我们不能只分配一个列表<试验<捕获< ;?将Number>>>< / code>扩展到
List< Test< ;?如果我们考虑到
capture <?>的事实, extends Number>
等价于一个具有 Number
的上限的匿名类型参数,那么我们可以把这个问题变成为什么不下面编译? (它不!):
public static< T extends Number>列表<试验< ;?扩展Number>> assign(List< Test< t> t){
return t;
}
这是不编译的好理由。如果是这样,那么这是可能的:
//所有这些都是有效的
列表< Test< Double> ;> doubleTests = null;
列表<测试<扩展Number>> numberTests = assign(doubleTests);
测试<整数> integerTest = null;
numberTests.add(integerTest); //输入错误,现在doubleTests包含一个Test< Integer>
那为什么要做显式的工作?
让我们循环回到开头。如果上述内容不安全,那么这是怎么来的?
List< Test<扩展Number>> l =
收藏。<测试< extends Number>> singletonList(t);
为此,它意味着允许以下内容:
测试<捕捉<扩展Number>> capturedT;
测试<?扩展Number> t = capturedT;
好吧,这不是有效的语法,因为我们不能明确地引用捕获,所以让我们使用与上面相同的技术对其进行评估!让我们将捕获绑定到不同的assign变体:
public static< T extends Number>测试与LT ;?扩展Number>分配(Test< T> t){
return t;
}
编译成功。不难看出为什么它应该是安全的。这是类似于
List 的用例。扩展Number> l =新列表< Double>();
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
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 referenceT
). - 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>();
这篇关于泛型的泛型如何工作?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!