When do Java generics require <? extends T> instead of <T> and is there any downside of switching?
给出以下示例(使用JUnit和Hamcrest Matchers):
1 2 3 |
这不会使用JUnit
1 | public static <T> void assertThat(T actual, Matcher<T> matcher) |
编译器错误消息为:
1 2 3 4 |
但是,如果我将
1 | public static <T> void assertThat(T result, Matcher<? extends T> matcher) |
然后编辑工作就开始了。
所以有三个问题:
以下是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public static <T> void assertThat(T actual, Matcher<T> matcher) { assertThat("", actual, matcher); } public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) { if (!matcher.matches(actual)) { Description description = new StringDescription(); description.appendText(reason); description.appendText(" Expected:"); matcher.describeTo(description); description .appendText(" got:") .appendValue(actual) .appendText(" "); throw new java.lang.AssertionError(description.toString()); } } |
首先,我得告诉你http://www.angelikalanger.com/genericsfaq/javagenericsfaq.html——她做得很出色。
基本的想法是你使用
1 | <T extends SomeClass> |
当实际参数可以是
在你的例子中,
1 2 3 |
您的意思是,
当您传递结果时,您将
有一件事要检查——你确定你想要的是
至于将
感谢所有回答这个问题的人,它真的帮助我澄清了事情。最后,斯科特·斯坦奇菲尔德的回答最接近于我是如何理解这个问题的,但由于他第一次写这篇文章时我不理解他,所以我试图重述这个问题,希望有人能从中受益。
我将以列表的形式重述这个问题,因为它只有一个通用参数,这将使它更容易理解。
参数化类(如示例中的list
考虑一下列表的情况。我的问题的本质是,为什么一个采用类型t和列表的方法不接受继承链下的某个东西的列表,而不是t。考虑这个人为的例子:
1 2 3 4 5 6 7 8 | List<java.util.Date> dateList = new ArrayList<java.util.Date>(); Serializable s = new String(); addGeneric(s, dateList); .... private <T> void addGeneric(T element, List<T> list) { list.add(element); } |
这不会编译,因为list参数是日期列表,而不是字符串列表。如果真的编译了,泛型将不会非常有用。
同样的事情也适用于map
1 | private <T> void genericAdd(T value, List<T> list) |
希望能够同时执行以下两项:
1 | T x = list.get(0); |
和
1 | list.add(value); |
在这种情况下,尽管junit方法实际上并不关心这些事情,但是方法签名需要协方差,而协方差不是它得到的,因此它不编译。
关于第二个问题,
1 | Matcher<? extends T> |
当t是一个对象,而不是API的意图时,它会有真正接受任何东西的缺点。其目的是静态地确保匹配器与实际对象匹配,并且无法从该计算中排除对象。
第三个问题的答案是,在未检查的功能方面,不会丢失任何内容(如果不将此方法泛型化,则JUnit API中不会有不安全的类型转换),但他们正在尝试完成其他任务-静态地确保两个参数可能匹配。
编辑(经过进一步的思考和经验后):
断言方法签名的一个大问题是试图将变量t与t的泛型参数相等。这不起作用,因为它们不是协变的。例如,您可能有一个t,它是一个
归根结底是:
1 2 3 4 | Class<? extends Serializable> c1 = null; Class<java.util.Date> d1 = null; c1 = d1; // compiles d1 = c1; // wont compile - would require cast to Date |
您可以看到类引用c1可以包含一个长实例(因为在给定时间的基础对象可能是
但是,如果我们引入其他对象,比如list(在您的示例中,这个对象是matcher),那么下面的内容就变成了真的:
1 2 3 4 | List<Class<? extends Serializable>> l1 = null; List<Class<java.util.Date>> l2 = null; l1 = l2; // wont compile l2 = l1; // wont compile |
…但是,如果列表的类型变为?扩展t而不是t…。
1 2 3 4 | List<? extends Class<? extends Serializable>> l1 = null; List<? extends Class<java.util.Date>> l2 = null; l1 = l2; // compiles l2 = l1; // won't compile |
我认为,通过改变
使用嵌套通配符仍然非常令人困惑,但希望这能够解释为什么通过查看如何为彼此分配泛型引用来帮助理解泛型。这也更加令人困惑,因为编译器在进行函数调用时推断t的类型(您没有明确地告诉它是t是)。
原始代码不编译的原因是
例如,考虑到编写的代码,将
我理解通配符的一种方法是认为通配符没有指定给定泛型引用可以"拥有"的可能对象的类型,但它与其他泛型引用的类型兼容(这听起来可能令人困惑…)因此,第一个答案在其措辞中非常误导。
换句话说,
我知道这是一个老问题,但我想分享一个我认为可以很好地解释有界通配符的例子。
1 2 3 | public static <T> void sort(List<T> list, Comparator<? super T> c) { list.sort(c); } |
如果我们有一个
如果你使用
1 |