Java 泛型啥时候需要 <?扩展 T> 而不是 <T> 并且切换有啥缺点吗?

Posted

技术标签:

【中文标题】Java 泛型啥时候需要 <?扩展 T> 而不是 <T> 并且切换有啥缺点吗?【英文标题】:When do Java generics require <? extends T> instead of <T> and is there any downside of switching?Java 泛型什么时候需要 <?扩展 T> 而不是 <T> 并且切换有什么缺点吗? 【发布时间】:2009-05-22 13:48:39 【问题描述】:

给出以下示例(将 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&lt;? extends T&gt; 有什么缺点吗?如果您这样做,是否还有其他情况会中断? 在 JUnit 中泛化 assertThat 方法有什么意义吗? Matcher 类似乎不需要它,因为 JUnit 调用了 match 方法,该方法没有使用任何泛型进行类型化,并且看起来像是在尝试强制类型安全,它什么都不做,就像 @987654332 @ 实际上不匹配,无论如何测试都会失败。不涉及不安全的操作(或者看起来如此)。

供参考,这里是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());
    

【问题讨论】:

这个链接非常有用(泛型、继承和子类型):docs.oracle.com/javase/tutorial/java/generics/inheritance.html 这很奇怪,我使用的是 Java 8,public static &lt;T&gt; void assertThat(T actual, Matcher&lt;T&gt; matcher) public static &lt;T&gt; void assertThat(T result, Matcher&lt;? extends T&gt; matcher) 编译都没有问题 【参考方案1】:

首先 - 我必须引导你到 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 的类的类对象。您的结果映射表明它只能包含 Date 类对象。

当您传入结果时,您将 MapString 设置为 Date 类对象,这与 StringMap 的任何 Serializable 不匹配.

要检查的一件事——您确定要Class&lt;Date&gt; 而不是DateStringClass&lt;Date&gt; 的映射通常听起来不是很有用(它只能将 Date.class 作为值而不是 Date 的实例)

至于泛化assertThat,思路是方法可以保证传入一个符合结果类型的Matcher

【讨论】:

在这种情况下,是的,我确实想要一张班级地图。我给出的示例被设计为使用标准 JDK 类而不是我的自定义类,但在这种情况下,该类实际上是通过反射实例化并基于密钥使用的。 (客户端没有可用的服务器类的分布式应用程序,只是使用哪个类来完成服务器端工作的关键)。 我想我的大脑被卡住的地方是为什么包含 Date 类型的类的 Map 不能很好地适合包含 Serializable 类型的类的 Maps 类型。当然,Serializable 类型的类也可以是其他类,但它肯定包括 Date 类型。 在 assertThat 确保为您执行转换时,matcher.matches() 方法不在乎,所以既然 T 从未使用过,为什么要涉及它? (方法返回类型为void) Ahhh - 这就是我没有阅读 assertThat 的 def 足够接近的结果。看起来只是为了确保传入一个合适的 Matcher...【参考方案2】:

感谢所有回答这个问题的人,这真的帮助我澄清了一些事情。最后,Scott Stanchfield 的回答与我最终理解的方式最接近,但由于他第一次写它时我不理解他,所以我试图重申这个问题,希望其他人能从中受益。

我将用 List 来重申这个问题,因为它只有一个通用参数,这样更容易理解。

参数化类(如示例中的 List&lt;Date&gt; 或 Map&lt;K, V&gt;)的目的是强制向下转换并让编译器保证这是安全的(否运行时异常)。

考虑 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&lt;String, Class&lt;? extends Serializable&gt;&gt; 它与 Map&lt;String, Class&lt;java.util.Date&gt;&gt; 不同。它们不是协变的,所以如果我想从包含日期类的映射中获取一个值并将其放入包含可序列化元素的映射中,那很好,但是方法签名说:

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&lt;String&gt;,但随后将编译器解决的匹配项传递给 Matcher&lt;ArrayList&lt;T&gt;&gt;。现在,如果它不是类型参数,一切都会好起来的,因为 List 和 ArrayList 是协变的,但是由于泛型,就编译器而言需要 ArrayList,它不能容忍 List,原因我希望很清楚综上所述。

【讨论】:

我仍然不明白为什么我不能向上转型。为什么我不能将日期列表转换为可序列化列表? @ThomasAhle,因为认为它是日期列表的引用在找到字符串或任何其他可序列化时会遇到转换错误。 我明白了,但是如果我以某种方式摆脱了旧的引用,就好像我从 List&lt;Object&gt; 类型的方法中返回了 List&lt;Date&gt; 一样?这应该是安全的,即使 java 不允许。【参考方案3】:

归结为:

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&lt;Long&gt;),但显然不能转换为 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&lt;T&gt; to Matcher&lt;? extends T&gt;,您基本上是在引入类似于分配l1 = l2;的场景

嵌套通配符仍然非常令人困惑,但希望通过查看如何将泛型引用相互分配有助于理解泛型,这是有道理的。这也更加令人困惑,因为编译器在您进行函数调用时推断 T 的类型(您没有明确告诉它是 T 是)。

【讨论】:

【参考方案4】:

您的原始代码无法编译的原因是&lt;? extends Serializable&gt; 确实不是意味着“任何扩展 Serializable 的类”,而是“扩展 Serializable 的一些未知但特定的类。” p>

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

【讨论】:

我不太理解 - 当你说“并不意味着......但是......”时:有什么区别? (例如,符合前者定义但不符合后者定义的“已知但非特定”类的示例是什么?) 是的,这有点尴尬;不知道如何更好地表达它......说“''更有意义吗?是未知的类型,不是匹配任何东西的类型?” 可能有助于解释的是,对于“任何扩展 Serializable 的类”,您可以简单地使用 &lt;Serializable&gt;【参考方案5】:

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

换句话说,List&lt;? extends Serializable&gt; 意味着您可以将该引用分配给其他列表,其中类型是某种未知类型或 Serializable 的子类。不要认为 A SINGLE LIST 能够保存 Serializable 的子类(因为这是不正确的语义并导致对泛型的误解)。

【讨论】:

这当然有帮助,但“可能听起来令人困惑”有点被“听起来令人困惑”所取代。作为后续,那么为什么根据这个解释,带有Matcher&lt;? extends T&gt;的方法可以编译? 如果我们定义为 List,它做同样的事情对吗?我说的是你的第二段。即多态性会处理它吗?【参考方案6】:

我知道这是一个老问题,但我想分享一个我认为可以很好地解释有界通配符的示例。 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&lt;T&gt; 即可使此方法可用。但是,此方法的创建者认识到,如果某物是T,那么它也是T 的超类的一个实例。因此,他允许我们使用T 的比较器或T 的任何超类,即? super T

【讨论】:

【参考方案7】:

如果你使用会怎样

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

【讨论】:

是的,这就是我上述回答的目的。 不,这对情况没有帮助,至少我尝试过的方式是这样。

以上是关于Java 泛型啥时候需要 <?扩展 T> 而不是 <T> 并且切换有啥缺点吗?的主要内容,如果未能解决你的问题,请参考以下文章

JAVA泛型<T>的使用,<? extends E>和<? super E>的含义

Java泛型 - 为什么允许“扩展T”而不是“实现T”?

java泛型

java泛型

如何在 Scala 中扩展包含泛型方法的 Java 接口?

简单的java泛型总结