Java泛型:通配符<?> vs类型参数<E>?

Gho*_*ica 41 java generics

我正在刷新我对Java泛型的知识.所以我转向Oracle的优秀教程......并开始为我的同事准备一份演示文稿.我在教程中遇到了关于通配符的部分,其中说:

考虑以下方法,printList:

public static void printList(List<Object> list) {
...
Run Code Online (Sandbox Code Playgroud)

printList的目标是打印任何类型的列表,但它无法实现该目标 - 它只打印一个Object实例列表; 它不能打印List<Integer>,List<String>,List<Double>,等等,因为它们不是的亚型List<Object>.要编写通用的printList方法,请使用List<?>:

public static void printList(List<?> list) {
Run Code Online (Sandbox Code Playgroud)

我明白这是List<Object>行不通的; 但我把代码更改为

static <E> void printObjects(List<E> list) {
    for (E e : list) {
        System.out.println(e.toString());
    }
}
...
    List<Object> objects = Arrays.<Object>asList("1", "two");
    printObjects(objects);
    List<Integer> integers = Arrays.asList(3, 4);
    printObjects(integers);
Run Code Online (Sandbox Code Playgroud)

你猜怎么着; 使用List<E>我可以打印不同类型的列表没有任何问题.

长话短说:至少教程表明需要通配符来解决这个问题; 但如图所示,它也可以通过这种方式解决.那么,我错过了什么?!

(旁注:用Java7测试;所以这可能是Java5,Java6的一个问题;但另一方面,Oracle似乎在他们的教程更新方面做得很好)

gex*_*ide 40

使用通用方法的方法比使用通配符的方法更强大,所以是的,您的方法也是可行的.但是,本教程并未声明使用通配符是唯一可行的解​​决方案,因此本教程也是正确的.

与通用方法相比,使用通配符获得的结果是:由于非泛型方法更容易掌握,因此必须编写更少且界面更"清晰".

为什么泛型方法比通配符方法更强大:为参数指定一个可以引用的名称.例如,考虑一种方法,该方法删除列表的第一个元素并将其添加到列表的后面.使用通用参数,我们可以执行以下操作:

static <T> boolean rotateOneElement(List<T> l){
    return l.add(l.remove(0));
}
Run Code Online (Sandbox Code Playgroud)

使用通配符,这是不可能的,因为l.remove(0)会返回capture-1-of-?,但l.add需要capture-2-of-?.即,编译器无法推断出结果与预期的remove类型相同add.这与编译器可以推断两者都是相同类型的第一个示例相反T.此代码无法编译:

static boolean rotateOneElement(List<?> l){
    return l.add(l.remove(0)); //ERROR!
}
Run Code Online (Sandbox Code Playgroud)

那么,如果你想使用带通配符的rotateOneElement方法,你能做什么,因为它比通用解决方案更容易使用?答案很简单:让通配符方法调用泛型方法,然后它可以工作:

// Private implementation
private static <T> boolean rotateOneElementImpl(List<T> l){
    return l.add(l.remove(0));
}

//Public interface
static void rotateOneElement(List<?> l){
     rotateOneElementImpl(l);
}
Run Code Online (Sandbox Code Playgroud)

标准库在许多地方使用这个技巧.其中之一是IIRC,Collections.java


Dji*_*eus 8

两种解决方案实际上是相同的,只是在第二种解决方案中,您正在命名通配符.当您想在签名中多次使用通配符时,这可以派上用场,但是要确保两者都引用相同的类型:

static <E> void printObjects(List<E> list, PrintFormat<E> format) {
Run Code Online (Sandbox Code Playgroud)


Tan*_*til 8

从技术上讲,两者之间没有区别

<E> void printObjects(List<E> list) {
Run Code Online (Sandbox Code Playgroud)

void printList(List<?> list) {
Run Code Online (Sandbox Code Playgroud)
  • 当您声明一个类型参数并仅使用它一次时,它实际上变成了一个通配符参数.
  • 另一方面,如果您不止一次使用它,差异就会变得很大.例如

    <E> void printObjectsExceptOne(List<E> list, E object) {
    
    Run Code Online (Sandbox Code Playgroud)

    完全不同于

    void printObjects(List<?> list, Object object) {
    
    Run Code Online (Sandbox Code Playgroud)

    您可能会看到第一种情况强制两种类型相同.虽然第二种情况没有限制.


其结果是,如果你要使用的类型参数只有一次,它甚至没有意义的命名.这就是为什么java架构师发明了所谓的通配符参数(最有可能).

通配符参数可避免不必要的代码膨胀并使代码更具可读性.如果您需要两个,则必须回退到类型参数的常规语法.

希望这可以帮助.