Java Generics通用方法中的通配符捕获

时间:2015-04-24 18:07:23

标签: java generics wildcard

我正在学习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代码中显然是非法的,所以必须发生其他事情,请问你能告诉我它是什么?

感谢

卢卡

2 个答案:

答案 0 :(得分:3)

T 中的reverse2()参数未被推断为? extends Object,并且未使用通配符?执行实例化。

只能在调用reverse2()的方法中进行推理。例如,如果您调用Collections.emptyList(),那么类型参数是什么?在那个例子中,它是未知的,但通常可以在调用站点推断:

List<String> empty = Collections.emptyList();

被推断为对Collections.<String>emptyList()(显式形式)的调用。

在您的情况下,T没有限制,因此任何类型都是兼容的。但是,如果将类型变量声明为T extends String,则通配符?将过于笼统,无法满足该限制,并且该调用将是非法的。

  好吧,我明白了,那么它是什么呢?我的意思是,推断出T是什么?

Treverse2()中的一个类型变量,正如我上面所解释的那样,类型推断在调用者中发生,而不是被调用者,所以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

  

如果reverse2Ti形式的通配符类型参数,则?新鲜类型变量 [... ]