什么时候Java泛型需要<?

When do Java generics require <? extends T> instead of <T> and is there any downside of switching?

给出以下示例(使用JUnit和Hamcrest Matchers):

1
2
3
Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));

这不会使用JUnit assertThat方法签名编译:

1
public static <T> void assertThat(T actual, Matcher<T> matcher)

编译器错误消息为:

1
2
3
4
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方法签名更改为:

1
public static <T> void assertThat(T result, Matcher<? extends T> matcher)

然后编辑工作就开始了。

所以有三个问题:

  • 为什么当前版本不能编译?虽然我对协方差的问题有模糊的理解,但如果必须这样做的话,我当然无法解释。
  • assertThat方法改为Matcher有什么不利之处吗?如果你这样做,还有其他的情况会被打破吗?
  • JUnit中的assertThat方法是否有点通用化?Matcher类似乎不需要它,因为JUnit调用了Matches方法,该方法不是用任何泛型类型化的,并且看起来像是试图强制类型安全,但它什么也不做,因为Matcher实际上不匹配,测试无论如何都会失败。没有涉及不安全的操作(或者看起来是这样)。
  • 以下是assertThat的JUnit实现,供参考:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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("
    Expected:"
    );
            matcher.describeTo(description);
            description
                .appendText("
         got:"
    )
                .appendValue(actual)
                .appendText("
    "
    );

            throw new java.lang.AssertionError(description.toString());
        }
    }


    首先,我得告诉你http://www.angelikalanger.com/genericsfaq/javagenericsfaq.html——她做得很出色。

    基本的想法是你使用

    1
    <T extends SomeClass>

    当实际参数可以是SomeClass或它的任何子类型时。

    在你的例子中,

    1
    2
    3
    Map<String, Class<? extends Serializable>> expected = null;
    Map<String, Class<java.util.Date>> result = null;
    assertThat(result, is(expected));

    您的意思是,expected可以包含表示实现Serializable的任何类的类对象。结果图显示它只能保存Date类对象。

    当您传递结果时,您将T精确设置为StringMapDate类对象,这与StringMapSerializable的任何对象都不匹配。

    有一件事要检查——你确定你想要的是Class,而不是Date?从StringClass的地图一般来说并不十分有用(它所能容纳的只是Date.class作为值,而不是Date的实例)。

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


    感谢所有回答这个问题的人,它真的帮助我澄清了事情。最后,斯科特·斯坦奇菲尔德的回答最接近于我是如何理解这个问题的,但由于他第一次写这篇文章时我不理解他,所以我试图重述这个问题,希望有人能从中受益。

    我将以列表的形式重述这个问题,因为它只有一个通用参数,这将使它更容易理解。

    参数化类(如示例中的list或map)的目的是强制进行向下强制转换,并让编译器保证这是安全的(没有运行时异常)。

    考虑一下列表的情况。我的问题的本质是,为什么一个采用类型t和列表的方法不接受继承链下的某个东西的列表,而不是t。考虑这个人为的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    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>,它与map>不同。它们不是协变的,所以如果我想从包含日期类的映射中获取一个值,并将其放入包含可序列化元素的映射中,这很好,但是方法签名会说:

    1
    private <T> void genericAdd(T value, List<T> list)

    希望能够同时执行以下两项:

    1
    T x = list.get(0);

    1
    list.add(value);

    在这种情况下,尽管junit方法实际上并不关心这些事情,但是方法签名需要协方差,而协方差不是它得到的,因此它不编译。

    关于第二个问题,

    1
    Matcher<? extends T>

    当t是一个对象,而不是API的意图时,它会有真正接受任何东西的缺点。其目的是静态地确保匹配器与实际对象匹配,并且无法从该计算中排除对象。

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

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

    断言方法签名的一个大问题是试图将变量t与t的泛型参数相等。这不起作用,因为它们不是协变的。例如,您可能有一个t,它是一个List,但随后传递一个匹配,编译器将计算结果传递给Matcher>。如果它不是一个类型参数,事情会很好,因为list和arraylist是协变的,但是由于泛型,对于编译器来说需要arraylist,它不能容忍一个列表,因为我希望从上面可以清楚地看到。