给出下面的例子(使用JUnit和Hamcrest匹配器):
Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));
它不使用JUnit assertThat方法签名编译:
public static <T> void assertThat(T actual, Matcher<T> matcher)
编译器错误消息是:
Error:Error:line (102)cannot find symbol method
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>,
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class
<? extends java.io.Serializable>>>)
但是,如果我将assertThat方法签名更改为:
public static <T> void assertThat(T result, Matcher<? extends T> matcher)
然后进行编译工作。
所以有三个问题
Why exactly doesn't the current version compile? Although I vaguely understand the covariance issues here, I certainly couldn't explain it if I had to.
Is there any downside in changing the assertThat method to Matcher<? extends T>? Are there other cases that would break if you did that?
Is there any point to the genericizing of the assertThat method in JUnit? The Matcher class doesn't seem to require it, since JUnit calls the matches method, which is not typed with any generic, and just looks like an attempt to force a type safety which doesn't do anything, as the Matcher will just not in fact match, and the test will fail regardless. No unsafe operations involved (or so it seems).
作为参考,下面是assertThat的JUnit实现:
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("\nExpected: ");
matcher.describeTo(description);
description
.appendText("\n got: ")
.appendValue(actual)
.appendText("\n");
throw new java.lang.AssertionError(description.toString());
}
}
这可以归结为:
Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date
您可以看到Class引用c1可以包含一个Long实例(因为给定时间的底层对象可能是List<Long>),但显然不能转换为Date,因为不能保证“未知”类是Date。它不是类型安全的,所以编译器不允许它。
然而,如果我们引入一些其他对象,比如List(在你的例子中,这个对象是Matcher),那么下面的情况就会成立:
List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile
...但是,如果List的类型变成?扩展T而不是T....
List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile
我认为通过将Matcher<T>更改为Matcher<?扩展T>,你基本上引入了类似于赋值l1 = l2的场景;
嵌套通配符仍然很令人困惑,但希望这有助于理解为什么通过查看如何将泛型引用分配给彼此来理解泛型。当你调用函数时,编译器会推断T的类型(你没有显式地告诉它是T是),这也让人更加困惑。
感谢每个回答这个问题的人,这真的帮助我理清了一些事情。最后,Scott Stanchfield的回答与我最终理解它的方式最接近,但由于他第一次写的时候我并不理解他,所以我试图重申这个问题,希望其他人能从中受益。
我将从List的角度重申这个问题,因为它只有一个通用参数,这将使它更容易理解。
参数化类(如示例中的List<Date>或Map<K, V>)的目的是强制向下转换,并让编译器保证这是安全的(没有运行时异常)。
以List为例。我的问题的本质是,为什么一个接受类型T和List的方法不接受继承链上比T更低的List:
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<String, Class<?它与Map<String, Class<java.util.Date>>不同。它们不是协变的,所以如果我想从包含日期类的映射中获取一个值,并将其放入包含可序列化元素的映射中,这很好,但是一个方法签名说:
private <T> void genericAdd(T value, List<T> list)
希望能够做到这两点:
T x = list.get(0);
and
list.add(value);
在这种情况下,尽管junit方法实际上并不关心这些事情,但方法签名需要协方差,而它没有得到协方差,因此它不会编译。
关于第二个问题,
Matcher<? extends T>
如果T是一个对象,就不能接受任何东西,这不是api的意图。目的是静态地确保匹配器与实际对象匹配,并且没有办法将object排除在计算之外。
第三个问题的答案是,就未检查的功能而言(如果该方法没有泛化,JUnit API中就不会有不安全的类型转换),不会有任何损失,但是他们试图完成其他事情——静态地确保两个参数可能匹配。
编辑(经过进一步的思考和经验):
One of the big issues with the assertThat method signature is attempts to equate a variable T with a generic parameter of T. That doesn't work, because they are not covariant. So for example you may have a T which is a List<String> but then pass a match that the compiler works out to Matcher<ArrayList<T>>. Now if it wasn't a type parameter, things would be fine, because List and ArrayList are covariant, but since Generics, as far as the compiler is concerned require ArrayList, it can't tolerate a List for reasons that I hope are clear from the above.