文档

Java™ 教程
隐藏目录
通配符捕获和辅助方法
路径:学习Java语言
课程:泛型(已更新)
部分:通配符

通配符捕获和辅助方法

在某些情况下,编译器会推断通配符的类型。例如,列表可以定义为List<?>,但在计算表达式时,编译器会从代码中推断出特定的类型。这种情况被称为通配符捕获

大部分情况下,你不需要担心通配符捕获,除非你看到包含"capture of"短语的错误信息。

当编译WildcardError示例时,会产生一个捕获错误:

import java.util.List;

public class WildcardError {

    void foo(List<?> i) {
        i.set(0, i.get(0));
    }
}

在这个例子中,编译器将i输入参数处理为Object类型。当foo方法调用List.set(int, E)时,编译器无法确认插入列表的对象类型,因此会产生错误。当出现这种类型的错误时,通常意味着编译器认为你将错误的类型赋给了变量。泛型是为了在编译时强制执行类型安全而添加到Java语言中的。

在Oracle的JDK 7 javac实现中,编译WildcardError示例会产生以下错误:

WildcardError.java:6: error: method set in interface List<E> cannot be applied to given types;
    i.set(0, i.get(0));
     ^
  required: int,CAP#1
  found: int,Object
  reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 error

在这个例子中,代码试图执行一个安全操作,那么你如何解决编译器错误呢?你可以通过编写一个私有辅助方法来捕获通配符。在这种情况下,你可以通过创建私有辅助方法fooHelper来解决问题,如WildcardFixed所示:

public class WildcardFixed {

    void foo(List<?> i) {
        fooHelper(i);
    }


    // Helper method created so that the wildcard can be captured
    // through type inference.
    private <T> void fooHelper(List<T> l) {
        l.set(0, l.get(0));
    }

}

由于辅助方法的存在,编译器使用推断确定TCAP#1,即捕获变量,在调用中。现在示例可以成功编译。

按照惯例,辅助方法通常命名为originalMethodNameHelper

现在考虑一个更复杂的例子,WildcardErrorBad

import java.util.List;

public class WildcardErrorBad {

    void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
      Number temp = l1.get(0);
      l1.set(0, l2.get(0)); // 预期是CAP#1 extends Number,
                            // 得到的是CAP#2 extends Number;
                            // 边界相同,但类型不同
      l2.set(0, temp);	    // 预期是CAP#1 extends Number,
                            // 得到的是Number
    }
}

在这个例子中,代码尝试进行一次不安全的操作。例如,考虑以下对swapFirst方法的调用:

List<Integer> li = Arrays.asList(1, 2, 3);
List<Double>  ld = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(li, ld);

虽然List<Integer>List<Double>都满足List<? extends Number>的条件,但从Integer值的列表中取出一个项目并尝试放入Double值的列表中是明显不正确的。

使用Oracle的JDK javac编译器编译代码会产生以下错误:

WildcardErrorBad.java:7: error: method set in interface List<E> cannot be applied to given types;
      l1.set(0, l2.get(0)); // 预期是CAP#1 extends Number,
        ^
  需要: int,CAP#1
  找到: int,Number
  原因: 无法将实际参数Number转换为方法调用转换的CAP#1
  其中E是类型变量:
    E extends Object 在接口List中声明
  其中CAP#1是一个新的类型变量:
    CAP#1 extends Number 从? extends Number的捕获中得到
WildcardErrorBad.java:10: error: method set in interface List<E> cannot be applied to given types;
      l2.set(0, temp);      // 预期是CAP#1 extends Number,
        ^
  需要: int,CAP#1
  找到: int,Number
  原因: 无法将实际参数Number转换为方法调用转换的CAP#1
  其中E是类型变量:
    E extends Object 在接口List中声明
  其中CAP#1是一个新的类型变量:
    CAP#1 extends Number 从? extends Number的捕获中得到
WildcardErrorBad.java:15: error: method set in interface List<E> cannot be applied to given types;
        i.set(0, i.get(0));
         ^
  需要: int,CAP#1
  找到: int,Object
  原因: 无法将实际参数Object转换为方法调用转换的CAP#1
  其中E是类型变量:
    E extends Object 在接口List中声明
  其中CAP#1是一个新的类型变量:
    CAP#1 extends Object 从?的捕获中得到
3 errors

没有辅助方法可以解决这个问题,因为代码本质上是错误的:从一个Integer值的列表中取出一个项目,并尝试将其放入Double值的列表中是明显错误的。


上一页: 通配符和子类型化
下一页: 通配符使用指南