ChatGPT解决这个技术问题 Extra ChatGPT

Java 泛型什么时候需要 <?扩展 T> 而不是 <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)

然后编译工作。

所以三个问题:

为什么当前版本不能编译?虽然我对这里的协方差问题有模糊的了解,但如果必须要解释的话,我当然无法解释。将 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());
    }
}
这是一个非常有用的链接(泛型、继承和子类型):docs.oracle.com/javase/tutorial/java/generics/inheritance.html
这很奇怪,我使用的是 Java 8,public static <T> void assertThat(T actual, Matcher<T> matcher) public static <T> void assertThat(T result, Matcher<? extends T> matcher) 编译都没有任何问题

C
Cache Staheli

首先 - 我必须引导你去 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 设置为 StringDate 个类对象中的 Map,这与 String 中的 MapSerializable 的任何内容都不匹配。

要检查的一件事 - 您确定要 Class<Date> 而不是 DateStringClass<Date> 的映射通常听起来不是很有用(它所能容纳的只是 Date.class 作为值而不是 Date 的实例)

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


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

感谢所有回答这个问题的人,这真的帮助我澄清了一些事情。最后,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,原因我希望很清楚从上面。


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

它归结为:

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 是)。


e
erickson

您的原始代码无法编译的原因是,<? extends Serializable> 不是的意思是“任何扩展 Serializable 的类”,而是“一些未知但特定的扩展 Serializable 的类”。

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


我不太关注 - 当你说“并不意味着......但是......”时:有什么区别? (例如,符合前一个定义但不符合后者的“已知但非特定”类的示例是什么?)
是的,这有点尴尬。不知道如何更好地表达它......说“''更有意义吗?是未知的类型,不是匹配任何东西的类型?”
可能有助于解释的是,对于“任何扩展 Serializable 的类”,您可以简单地使用 <Serializable>
G
GreenieMeanie

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

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


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

我知道这是一个老问题,但我想分享一个我认为可以很好地解释有界通配符的示例。 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


n
newacct

如果你使用怎么办

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

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

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅