Java泛型何时需要<?扩展了T>而不是<T>,切换是否有不利之处?


205

给定以下示例(将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. assertThat在JUnit中通用化方法有什么意义吗?该Matcher级似乎并不需要它,因为JUnit的调用matches方法,它不与任何通用的,只是看起来像键入企图迫使一个类型安全这并不做任何事情,因为Matcher只会事实上并不匹配,则无论如何测试都将失败。不涉及任何不安全的操作(或看起来如此)。

供参考,以下是JUnit的实现assertThat

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());
    }
}

这对链接非常有用(泛型,继承和子类型):docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Dariush Jafari

Answers:


145

首先-我必须将您定向到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类对象。

当您在结果传递,你设置T准确MapStringDate类对象,这不匹配MapString,以任何的Serializable

要检查的一件事-确定要Class<Date>不要Date吗?一般而言,Stringto 的映射Class<Date>听起来并不是很有用(它可以容纳的只是Date.class值而不是的实例Date

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


在这种情况下,是的,我确实想要一个类映射。我给出的示例旨在使用标准JDK类而不是我的自定义类,但是在这种情况下,该类实际上是通过反射实例化的,并且是基于键使用的。(一个分布式应用程序,其中客户端没有可用的服务器类,仅是用于执行服务器端工作的类的键)。
伊沙岛

6
我想我的大脑陷入的问题是为什么包含Date类型类的Map不能很好地适合包含Serializable类型类的Maps。当然,Serializable类型的类也可以是其他类,但是它当然包括Date类型。
伊沙岛

关于确保为您执行转换的断言,matcher.matches()方法无关紧要,所以既然从不使用T,为什么还要使用它呢?(方法返回类型为空)
Yishai

啊-这就是我没有足够仔细地阅读assertThat的定义而得到的。看起来只是要确保将匹配的Matcher通过...
Scott Stanchfield,2009年

28

感谢所有回答此问题的人,它确实为我澄清了一切。最终,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是一个Object时,实际上没有接受任何东西的缺点,这不是API的意图。目的是静态地确保匹配器与实际对象匹配,并且无法从该计算中排除对象。

第三个问题的答案是,就未经检查的功能而言,什么都不会丢失(如果未通用此方法,则在JUnit API中不会存在不安全的类型转换),但是他们正在尝试实现其他目标-静态地确保两个参数可能会匹配。

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

assertThat方法签名的主要问题之一是试图将变量T与通用参数T等效。这是行不通的,因为它们不是协变的。因此,例如,您可能有一个T,它是a,List<String>但随后将匹配传递给编译器算出Matcher<ArrayList<T>>。现在,如果它不是类型参数,那就好了,因为List和ArrayList是协变的,但是由于泛型,就编译器而言,它需要ArrayList,因此我无法理解List,因为我希望这很清楚从上面。


我仍然不明白为什么我不能up。为什么我不能将日期列表变成可序列化的列表?
托马斯·阿勒

@ThomasAhle,因为当它们找到字符串或任何其他可序列化的内容时,认为它是日期列表的引用将发生转换错误。
Yishai 2014年

我知道了,但是如果我以某种方式摆脱了旧的引用,就好像我List<Date>从类型为type的方法中返回了该List<Object>怎么办?即使Java不允许,这也应该是安全的。
Thomas Ahle

14

归结为:

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。它不是typsesafe,因此编译器不允许使用。

但是,如果我们引入其他对象(例如List)(在您的示例中,该对象为Matcher),则以下内容将成立:

List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

...但是,如果列表的类型变为?扩展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 is)。


9

你原来的代码不能编译的原因是,<? extends Serializable>确实不是意味着,“扩展序列化任何类”,而是“延伸序列化的一些未知的,但具体的类。”

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


1
我不太在意-当您说“不是指...而是...”时,有什么区别?(例如,什么是适合前一种定义但不适合后者的“已知但非特定”类的示例?)
poundifdef,2009年

是的,这有点尴尬;不知道如何更好地表达它……说“'?”更有意义吗?是未知的类型,而不是与任何内容匹配的类型?”
埃里克森(Erickson),2009年

1
可能有助于解释的是,对于“任何可扩展Serializable的类”,您可以简单地使用<Serializable>
c0der

8

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

换句话说,List<? extends Serializable>意味着您可以将该引用分配给其他List,其中类型是某种未知类型,它是Serializable的子类。不要以单列表能够容纳Serializable的子类的方式来考虑它(因为语义不正确并导致对泛型的误解)。


这当然有帮助,但是“听起来可能令人困惑”已被“声音令人困惑”所代替。作为后续,为什么根据此解释,使用Matcher的方法可以<? extends T>编译?
逸斋

如果我们将其定义为List <Serializable>,它会做同样的事情吗?我说的是你的第二段。即多态性会处理吗?
Supun Wijerathne

3

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

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

如果我们有一个List of T,则List当然可以包含extension类型的实例T。如果列表包含动物,则列表可以同时包含狗和猫(均为动物)。狗的属性为“ woofVolume”,猫的属性为“ meowVolume”。尽管我们可能希望根据特定于的子类的这些属性进行排序T,但我们如何期望这种方法做到这一点呢?比较器的局限性在于它只能比较只有一种类型(T)的两件事。因此,仅需使用a即可Comparator<T>使此方法可用。但是,此方法的创建者认识到,如果某物是T,则它也是的超类的一个实例T。因此,他允许我们使用的比较器T或的任何超类T,即? super T


1

如果使用

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

是的,这就是我上面的答案所要解决的问题。
GreenieMeanie,2009年

不,至少在我尝试过的方式上,这没有帮助。
逸斋
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.