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)
然后编译工作。
所以有三个问题:
- 为什么现在的版本不能编译? 虽然我依稀的理解这里的协变问题,但是如果必须的话,我当然不能解释。
- 在改变
assertThat
方法Matcher<? extends T>
是否有任何缺点Matcher<? extends T>
Matcher<? extends T>
? 如果你这样做的话,还有其他的情况会破坏吗? - 是否有任何指向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
Map
到Date
类对象,它不匹配任何可Serializable
的String
Map
。
有一件事要检查 – 你确定你想Class<Date>
而不是Date
? String
到Class<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
的超类的一个实例。 因此,他允许我们使用T
或T
任何超类的比较器,即? super T
? super T
如果你使用什么
Map<String, ? extends Class<? extends Serializable>> expected = null;