给定以下示例(将 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)
然后编译工作。
所以三个问题:
为什么当前版本不能编译?虽然我对这里的协方差问题有模糊的了解,但如果必须要解释的话,我当然无法解释。将 assertThat 方法更改为 Matcher< 有什么缺点吗?扩展 T>?如果您这样做,是否还有其他情况会中断?在 JUnit 中泛化 assertThat 方法有什么意义吗? Matcher 类似乎不需要它,因为 JUnit 调用了 match 方法,该方法没有使用任何泛型进行类型化,并且看起来像是试图强制不做任何事情的类型安全,因为 Matcher 不会实际上匹配,无论如何测试都会失败。不涉及不安全的操作(或者看起来如此)。
作为参考,这里是 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());
}
}
public static <T> void assertThat(T actual, Matcher<T> matcher)
和 public static <T> void assertThat(T result, Matcher<? extends T> matcher)
编译都没有任何问题
首先 - 我必须引导你去 http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - 她做得很棒。
基本思想是你使用
<T extends SomeClass>
当实际参数可以是 SomeClass
或其任何子类型时。
在你的例子中,
Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));
您是说 expected
可以包含代表任何实现 Serializable
的类的 Class 对象。您的结果映射表明它只能包含 Date
个类对象。
当您传入结果时,您将 T
设置为 String
到 Date
个类对象中的 Map
,这与 String
中的 Map
与 Serializable
的任何内容都不匹配。
要检查的一件事 - 您确定要 Class<Date>
而不是 Date
? String
到 Class<Date>
的映射通常听起来不是很有用(它所能容纳的只是 Date.class
作为值而不是 Date
的实例)
至于泛化assertThat
,思路是方法可以保证传入一个符合结果类型的Matcher
。
感谢所有回答这个问题的人,这真的帮助我澄清了一些事情。最后,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 参数是日期列表,而不是字符串列表。如果确实编译,泛型将不是很有用。
同样适用于地图<String, Class<? extends Serializable>>
它与地图不同<String, Class<java.util.Date>>
。它们不是协变的,所以如果我想从包含日期类的映射中获取一个值并将其放入包含可序列化元素的映射中,那很好,但是方法签名说:
private <T> void genericAdd(T value, List<T> list)
希望能够做到这两点:
T x = list.get(0);
和
list.add(value);
在这种情况下,即使 junit 方法实际上并不关心这些事情,但方法签名需要协方差,它没有得到,因此它不会编译。
关于第二个问题,
Matcher<? extends T>
当 T 是一个对象时,会真正接受任何东西的缺点,这不是 API 的意图。目的是静态地确保匹配器与实际对象匹配,并且无法从计算中排除对象。
第三个问题的答案是,就未经检查的功能而言,不会丢失任何东西(如果此方法未泛化,则 JUnit API 内不会有不安全的类型转换),但他们正在尝试完成其他事情 - 静态地确保两个参数可能匹配。
编辑(经过进一步的思考和经验):
assertThat 方法签名的一大问题是试图将变量 T 等同于 T 的泛型参数。这不起作用,因为它们不是协变的。因此,例如,您可能有一个 T,它是一个 List<String>
,但随后将编译器计算出来的匹配项传递给 Matcher<ArrayList<T>>
。现在,如果它不是类型参数,一切都会好起来的,因为 List 和 ArrayList 是协变的,但是由于泛型,就编译器而言需要 ArrayList,它不能容忍 List,原因我希望很清楚从上面。
List<Object>
的方法中返回了 List<Date>
一样?即使java不允许,这应该是安全的。
它归结为:
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),则以下情况变为 true:
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> to Matcher<? extends T>
,您基本上是在引入类似于分配 l1 = l2; 的场景。
嵌套通配符仍然非常令人困惑,但希望通过查看如何将泛型引用相互分配来帮助理解泛型是有道理的。这也更加令人困惑,因为编译器在您进行函数调用时推断 T 的类型(您没有明确告诉它是 T 是)。
您的原始代码无法编译的原因是,<? extends Serializable>
不是的意思是“任何扩展 Serializable 的类”,而是“一些未知但特定的扩展 Serializable 的类”。
例如,给定编写的代码,将 new TreeMap<String, Long.class>()
分配给 expected
是完全有效的。如果编译器允许代码编译,则 assertThat()
可能会中断,因为它需要 Date
对象而不是它在映射中找到的 Long
对象。
<Serializable>
我理解通配符的一种方法是认为通配符没有指定给定泛型引用可以“拥有”的可能对象的类型,而是它兼容的其他泛型引用的类型(这听起来可能令人困惑...)因此,第一个答案的措辞非常具有误导性。
换句话说,List<? extends Serializable>
意味着您可以将该引用分配给其他列表,其中类型是某种未知类型,或者是 Serializable 的子类。不要将其视为 A SINGLE LIST 能够保存 Serializable 的子类(因为这是不正确的语义并导致对泛型的误解)。
<? extends T>
的方法可以编译?
我知道这是一个老问题,但我想分享一个我认为可以很好地解释有界通配符的示例。 java.util.Collections
提供此方法:
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
如果我们有一个 T
的列表,那么该列表当然可以包含扩展 T
的类型的实例。如果列表包含动物,则列表可以同时包含 Dogs 和 Cats(均为 Animals)。狗有一个属性“woofVolume”,猫有一个属性“meowVolume”。虽然我们可能希望根据 T
的子类特有的这些属性进行排序,但我们怎么能期望这个方法做到这一点呢? Comparator 的一个限制是它只能比较一种类型 (T
) 的两个事物。因此,只需一个 Comparator<T>
即可使该方法可用。但是,此方法的创建者认识到,如果某物是 T
,那么它也是 T
的超类的一个实例。因此,他允许我们使用 T
的比较器或 T
的任何超类,即 ? super T
。
如果你使用怎么办
Map<String, ? extends Class<? extends Serializable>> expected = null;