Java泛型什么时候需要<?T&gt延伸;而不是<T>转换有什么缺点吗?

给出下面的例子(使用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)

然后进行编译工作。

所以有三个问题

  1. 为什么当前版本不能编译?虽然我模糊地理解了这里的协方差问题,但如果必须的话,我当然无法解释它。
  2. assertThat方法更改为Matcher<? extends T>是否有任何缺点?如果你这么做,还有其他案子会破案吗?
  3. 在JUnit中泛化assertThat方法有什么意义吗?Matcher类似乎不需要它,因为JUnit调用matches方法,它没有使用任何泛型类型,看起来就像试图强制一个不做任何事情的类型安全,因为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());
}
}
389260 次浏览

首先,我必须把你引向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可以包含Class对象,表示任何实现Serializable的类。你的结果映射说它只能保存Date类对象。

当你传递result时,你将T设置为StringDate类对象的Map,这不会将StringMap匹配到任何Serializable

有一件事要检查——你确定你想要Class<Date>而不是Date吗?StringClass<Date>的映射通常听起来不是特别有用(它只能保存Date.class作为值,而不是Date的实例)

至于泛化assertThat,其思想是该方法可以确保传入符合结果类型的Matcher

你的原始代码无法编译的原因是<? extends Serializable>中的表示“任何扩展Serializable的类”。而是“某个未知但特定的类,它扩展了Serializable”;

例如,给定已编写的代码,将new TreeMap<String, Long.class>()赋值给expected是完全有效的。如果编译器允许代码编译,assertThat()可能会中断,因为它期望的是Date对象,而不是它在映射中发现的Long对象。

我理解通配符的一种方法是认为通配符并没有指定给定泛型引用可以“拥有”的可能对象的类型,而是它与其他泛型引用兼容的类型(这听起来可能令人困惑…)因此,第一个答案的措辞非常具有误导性。

换句话说,List<? extends Serializable>意味着你可以将该引用赋值给其他列表,其中类型是某个未知类型或Serializable的子类。不要认为它是一个单一的列表,可以容纳Serializable的子类(因为这是不正确的语义,会导致对泛型的误解)。

这可以归结为:

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> to Matcher<? extends T>,你基本上引入了类似于赋值l1 = l2的场景;

嵌套通配符仍然很令人困惑,但希望这有助于理解为什么通过查看如何将泛型引用分配给彼此来理解泛型。当你调用函数时,编译器会推断T的类型(你没有显式地告诉它是T是),这也让人更加困惑。

如果你使用

Map<String, ? extends Class<? extends Serializable>> expected = null;

感谢每个回答这个问题的人,这真的帮助我理清了一些事情。最后,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<? extends Serializable>>,它与Map<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的意图。目的是静态地确保匹配器与实际对象匹配,并且没有办法将object排除在计算之外。

第三个问题的答案是,就未检查的功能而言(如果该方法没有泛化,JUnit API中就不会有不安全的类型转换),不会有任何损失,但是他们试图完成其他事情——静态地确保两个参数可能匹配。

编辑(经过进一步的思考和经验):

assertThat方法签名的一个大问题是试图将变量T等同于泛型参数T,这是行不通的,因为它们不是协变的。例如,你可能有一个T,它是List<String>,但随后将编译器计算出的匹配传递给Matcher<ArrayList<T>>。现在如果它不是一个类型参数,事情就会好起来,因为List和ArrayList是协变的,但由于泛型,就编译器而言,它需要ArrayList,它不能容忍List,我希望上面的原因已经很清楚了。

我知道这是一个老问题,但我想分享一个例子,我认为它很好地解释了有界通配符。java.util.Collections提供了这个方法:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}

如果我们有一个T的List,这个List当然可以包含扩展T类型的实例。如果列表包含动物,列表可以同时包含狗和猫(都是动物)。狗有一个属性“woofVolume”,猫有一个属性“meowVolume”。虽然我们可能希望基于T的子类特有的这些属性进行排序,但我们如何期望这个方法做到这一点呢?Comparator的一个限制是它只能比较一种类型(T)的两个东西。因此,只需要Comparator<T>就可以使这个方法可用。但是,该方法的创建者认识到,如果某个对象是T,那么它也是T的超类的实例。因此,他允许我们使用T的Comparator或T的任何超类,即? super T