我正在学习Java Generics,而且我正在阅读Naftalin和Wadler的(非常好的)书籍,我得到了他在谈论通用方法中捕获通配符的地方,比如实现在Collections.reverse中:
public static <T> void reverse(List<T> list){
List<T> temp=new ArrayList<>(list);
for(int i=0;i<list.size();i++)
list.set(i,temp.get(list.size()-1-i));
}
他说Collections类中的方法是为了简单起见使用通配符实现的:
public static void reverse(List<?> list){
//code..
}
但使用第一个方法体不起作用:
public static void reverse(List<?> list){
List<Object> temp=new ArrayList<Object>(list); //legal
for(int i=0;i<list.size();i++)
list.set(temp.get(list.size()-1-i)); //illegal
}
它不起作用,因为它试图将Object类型元素放在列表中 其类型是未知的(?),它可能是所有扩展对象(它是..well,所有)
所以从第二个方法调用第一个方法应该可以解决问题:
public static void reverse1(List<?> list){
reverse2(list);
}
public static <T> void reverse2(List<T> list){
List<T> temp=new ArrayList<T>(list);
for(int i=0;i<list.size();i++)
list.set(i,temp.get(list.size()-1-i));
}
现在,跟随方法调用发生的事情,例如传递
List<String> myList
1)List<String> myList
被上传到局部变量String<?> list
(字符串扩展Object,它是通配符的上限,使List<String>
子类型为List<?>
)
2)list
现在传递给reverse2()
,参数T被推断为? extends Object
,现在我在实例化新{{1}时如何将其用作参数} ???这在Java代码中显然是非法的,所以必须发生其他事情,请问你能告诉我它是什么?
感谢
卢卡
答案 0 :(得分:3)
T
中的reverse2()
参数未被推断为? extends Object
,并且未使用通配符?
执行实例化。
只能在调用reverse2()
的方法中进行推理。例如,如果您调用Collections.emptyList()
,那么类型参数是什么?在那个例子中,它是未知的,但通常可以在调用站点推断:
List<String> empty = Collections.emptyList();
被推断为对Collections.<String>emptyList()
(显式形式)的调用。
在您的情况下,T
没有限制,因此任何类型都是兼容的。但是,如果将类型变量声明为T extends String
,则通配符?
将过于笼统,无法满足该限制,并且该调用将是非法的。
好吧,我明白了,那么它是什么呢?我的意思是,推断出T是什么?
T
是reverse2()
中的一个类型变量,正如我上面所解释的那样,类型推断在调用者中发生,而不是被调用者,所以T
不是“推断”的任何东西。
也许你的意思是在编译的字节代码中显示什么类型?在这种情况下,不会声明T
类型的变量;永远不会使用T
,也不会进行类型检查。因此,请考虑以下人为的例子:
final class Reverse {
static <T extends String> void reverse(List<T> list) {
List<T> tmp = new ArrayList<>(list);
for (int i = 0; i < list.size(); ++i)
list.set(i, tmp.get(list.size() - 1 - i));
}
}
现在调用该方法的客户端:
final class Test {
public static void main(String... argv) {
List<String> list = Arrays.asList("A", "B", "C");
Reverse.reverse(list);
System.out.println(list);
}
}
将这些类一起编译并运行Test
,然后按预期获得[C, B, A]
。现在,无需重新编译Test
,更改reverse()
方法的签名并仅重新编译Reverse
类:
static <T extends Integer> void reverse(List<T> list)
重新运行Test
会产生相同的结果,而不是失败!
现在更改reverse()
方法的实现,再次重新编译Reverse
类:
static <T extends Integer> void reverse(List<T> list) {
List<T> tmp = new ArrayList<>(list);
for (int i = 0; i < list.size(); ++i) {
T el = tmp.get(list.size() - 1 - i);
list.set(i, el);
}
}
这一次,运行Test
会产生您上次可能遇到的失败:
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
那是因为现在实际引用了T
:
T el = tmp.get(list.size() - 1 - i);
在这个赋值中,编译器会将一个强制转换插入到type参数的上限,在本例中为Integer
:
T el = (Integer) tmp.get(list.size() - 1 - i);
如果类型T
不受限制(其上限为Object
),则不执行强制转换,因为它永远不会失败。
答案 1 :(得分:0)
list
现已传递至reverse2()
,参数T
推断为? extends Object
,[...]。
当调用reverse2
时,通配符被捕获到一个新的类型变量,不再是通配符。* {{1}的类型推断为捕获的类型。
类型系统确保我们只能以安全的方式使用此功能。
例如,假设我们有一个接受两个列表的方法:
T
如果我们尝试使用通配符捕获来调用它:
static <T> void swap2(List<T> list1, List<T> list2) {
List<T> temp = new ArrayList<>(list1);
list1.clear();
list1.addAll(list2);
list2.clear();
list2.addAll(temp);
}
这将无法编译,因为编译器知道它不能推断static void swap(List<?> list1, List<?> list2) {
swap2(list1, list2); // <- doesn't compile
}
和list1
在传递给list2
之前具有相同的类型。
swap
是类型安全的,因为它会向列表中添加最初位于列表中的元素。
*技术证明是5.1.10 Capture Conversion:
如果
reverse2
是Ti
形式的通配符类型参数,则?
是新鲜类型变量 [... ]