Javagenerics何时需要<? 扩展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) 

然后编译工作。

所以有三个问题:

  1. 为什么现在的版本不能编译? 虽然我依稀的理解这里的协变问题,但是如果必须的话,我当然不能解释。
  2. 在改变assertThat方法Matcher<? extends T>是否有任何缺点Matcher<? extends T> Matcher<? extends T> ? 如果你这样做的话,还有其他的情况会破坏吗?
  3. 是否有任何指向JUnit的assertThat方法的泛化? Matcher类似乎并不需要它,因为JUnit调用了不是任何genericstypes的matches方法,而只是看起来像一个强制types安全的尝试,因为Matcher不会做任何事情实际上是匹配的,而testing将会失败。 没有涉及不安全的操作(或者看起来如此)。

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

首先 – 我必须引导你到http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html – 她做了一个了不起的工作。

基本的想法是你使用

 <T extends SomeClass> 

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

在你的例子中,

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

你在说可以包含代表任何实现Serializable类的Class对象。 你的结果地图说它只能保存Date类对象。

当你传入结果时,你将T设置为正确的String MapDate类对象,它不匹配任何可SerializableString Map

有一件事要检查 – 你确定你想Class<Date>而不是DateStringClass<Date>映射通常听起来不是非常有用(它可以容纳Date.class作为值而不是Date实例)

至于泛化assertThat ,这个想法是,该方法可以确保符合结果types的Matcher被传入。

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

我将以List的forms重申这个问题,因为它只有一个generics参数,这样会更容易理解。

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

考虑List的情况。 我的问题的本质是为什么一个接受typesT和List的方法不会接受比T更接近inheritance链的东西的列表。考虑这个人为的例子:

 List<java.util.Date> dateList = new ArrayList<java.util.Date>(); Serilizable s = new String(); addGeneric(s, dateList); .... private <T> void addGeneric(T element, List<T> list) { list.add(element); } 

这将不会编译,因为list参数是date列表,而不是string列表。 generics不会非常有用,如果这样做编译。

同样的事情适用于Map <String, Class<? extends Serializable>> <String, Class<? extends Serializable>>它与Map <String, Class<java.util.Date>>不是一回事。 它们不是协变的,所以如果我想从包含date类的映射中获取一个值,并把它放到包含可序列化元素的映射中,那很好,但是一个方法签名说:

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

希望能够同时做到这一点:

 T x = list.get(0); 

 list.add(value); 

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

关于第二个问题,

 Matcher<? extends T> 

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

第三个问题的答案是没有任何东西会被丢失,就未经检查的function而言(如果这个方法没有被泛化,那么在JUnit API中就不会有不安全的types转换),但是他们试图完成其他的工作 – 静态地确保两个参数可能匹配。

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

assertThat方法签名的一个大问题是试图将variablesT与T的generics参数等同起来。这是行不通的,因为它们不是协变的。 所以例如,你可能有一个T是一个List<String>但是然后传递一个匹配到编译器Matcher<ArrayList<T>> 。 现在,如果它不是一个types参数,事情会好起来的,因为List和ArrayList是协变的,但是因为generics,就编译器而言,需要ArrayList,因为我希望清楚的原因它不能容忍List从上面。

归结为:

 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,因为不能保证“unknown”类是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的types变成了? 扩展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> Matcher<T> to Matcher<? extends T> ,你基本上引入类似于分配l1 = l2的场景;

嵌套的通配符仍然非常混乱,但是希望通过查看如何分配generics引用来理解generics是有帮助的。 这也是令人困惑的,因为编译器在进行函数调用时推断出T的types(你没有明确告诉它是T)。

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

例如,给定的代码如下所示,将new TreeMap<String, Long.class>()>赋予expected是完全有效的。 如果编译器允许编译代码,则assertThat()可能会中断,因为它会期望Date对象,而不是它在映射中find的Long对象。

我理解通配符的一个方法是认为通配符没有指定给定generics引用可以“拥有”的可能对象的types,但是它与其他通用引用的types是兼容的(这可能听起来很混乱…)因此,第一个答案是非常误导的措辞。

换句话说, List<? extends Serializable> List<? extends Serializable>意味着您可以将该引用分配给其他types为某种未知types的子类或Serializable的子类。 不要把它看作是能够容纳Serializable的子类的单个列表(因为这是不正确的语义,并导致对generics的误解)。

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

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

如果我们有一个T的List,List当然可以包含扩展T的types的实例。 如果列表包含动物,列表可以同时包含狗和猫(都是动物)。 狗有一个属性“woofvolume”和猫有一个属性“meowVolume”。 虽然我们可能想根据T子类所特有的这些属性进行sorting,但我们怎么能期望这种方法呢? 比较器的限制是它只能比较两种只有一种types( T )的东西。 所以只需要一个Comparator<T>就可以使用这个方法。 但是,这种方法的创造者认识到,如果事物是​​一个T ,那么它也是T的超类的一个实例。 因此,他允许我们使用TT任何超类的比较器,即? super T ? super T

如果你使用什么

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