什么时候Java泛型需要<! - ?扩展T - >而不是<t>并且是否存在切换的缺点?</t>

时间:2009-05-22 13:48:39

标签: java generics junit

给出以下示例(使用带有Hamcrest匹配器的JUnit):

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. JUnit中assertThat方法的泛化是否有任何意义? Matcher类似乎不需要它,因为JUnit调用匹配方法,该方法没有使用任何泛型类型,并且看起来像是试图强制不执行任何操作的类型安全,如Matcher实际上不匹配,测试将无论如何都会失败。不涉及不安全的操作(或者看起来如此)。
  4. 供参考,以下是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());
        }
    }
    

7 个答案:

答案 0 :(得分:126)

首先 - 我必须指导你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设置为MapString类对象的Date,这与Map不匹配String的任何内容Serializable

要检查一件事 - 您确定要Class<Date>而不是Date吗? StringClass<Date>的地图一般听起来并不十分有用(它只能保留Date.class作为值而不是Date的实例)

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

答案 1 :(得分:24)

感谢所有回答问题的人,这对我来说真的很有帮助。最后斯科特·斯坦奇菲尔德的答案与我最终理解它的方式最接近,但由于我在第一次写作时并不理解他,我试图重述这个问题,以便其他人有望受益。

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

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

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

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是一个Object而不是API意图时,会有真正接受任何东西的缺点。目的是静态地确保匹配器匹配实际对象,并且无法从该计算中排除Object。

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

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

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

答案 2 :(得分:14)

归结为:

Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

你可以看到类引用c1可以包含一个Long实例(因为在给定时间的底层对象可能是List<Long>),但显然不能转换为Date,因为不能保证“未知的“类是日期。它不是典型的,所以编译器不允许它。

但是,如果我们引入一些其他对象,比如说List(在你的例子中这个对象是Matcher),则以下内容成为现实:

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是),这也让人感到更加困惑。

答案 3 :(得分:8)

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

例如,考虑到编写的代码,将new TreeMap<String, Long.class>()>分配给expected是完全有效的。如果编译器允许编译代码,那么assertThat()可能会中断,因为它会期望Date个对象而不是它在地图中找到的Long个对象。

答案 4 :(得分:7)

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

换句话说,List<? extends Serializable>表示您可以将该引用分配给其中类型为某种未知类型的其他列表,或者是Serializable的子类。不要以单个列表的形式来考虑它能够保存Serializable的子类(因为这是错误的语义并导致对泛型的误解)。

答案 5 :(得分:3)

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

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

如果我们有一个T列表,列表当然可以包含扩展T的类型实例。如果列表包含动物,则列表可以包含狗和猫(两种动物)。狗有“woofVolume”属性,猫有“meowVolume”属性。虽然我们可能希望根据T的子类特定的这些属性进行排序,但我们怎么能期望这种方法呢? Comparator的一个限制是它只能比较一种类型的两个东西(T)。因此,仅需要Comparator<T>即可使此方法可用。但是,此方法的创建者认识到,如果某个内容是T,那么它也是T的超类的实例。因此,他允许我们使用T的比较符或T的任何超类,即? super T

答案 6 :(得分:1)

如果你使用

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