Java泛型何时需要<?扩展 T> 而不是 <T>切换是否有任何缺点?

2022-08-31 05:41:29

给定以下示例(将 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. 将方法更改为 有什么缺点吗?如果您这样做,还有其他情况会破裂吗?assertThatMatcher<? extends T>
  3. 在 JUnit 中泛化该方法有什么意义吗?该类似乎不需要它,因为JUnit调用了matchs方法,该方法未使用任何泛型键入,并且看起来像是试图强制类型安全,而不执行任何操作,因为will实际上并不匹配,并且测试无论如何都会失败。不涉及不安全的操作(至少看起来是这样)。assertThatMatcherMatcher

作为参考,以下是 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());
    }
}

答案 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));

您是说可以包含表示实现 的任何类的类对象。结果映射显示它只能保存类对象。expectedSerializableDate

当您传入结果时,您将设置为正好 of to class 对象,这与 任何 .TMapStringDateMapStringSerializable

有一件事要检查 - 你确定你想要而不是?一般来说,to的映射听起来并不是非常有用(它所能容纳的只是作为值而不是实例Class<Date>DateStringClass<Date>Date.classDate)

至于泛化,这个想法是该方法可以确保传入符合结果类型的 a。assertThatMatcher


答案 2

感谢所有回答这个问题的人,这真的帮助我澄清了事情。最后,斯科特·斯坦奇菲尔德(Scott Stanchfield)的答案最接近我最终理解它的方式,但由于他在第一次写它时我不理解他,我试图重申这个问题,希望其他人会受益。

我将从List的角度重述这个问题,因为它只有一个通用参数,这将使它更容易理解。

参数化类(如示例中的 List 或 Map)的目的是强制下放,并让编译器保证这是安全的(没有运行时异常)。<Date><K, V>

以列表为例。我的问题的本质是,为什么一个采用类型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 的意图。其目的是静态地确保匹配器与实际对象匹配,并且无法从该计算中排除 Object。

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

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

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


推荐