在范型的使用上,问号”?”表示通配符,代表运行时未知类型。通配符可用作type parameter:

  • 方法形参
  • 成员变量
  • 局部变量
  • 方法返回值(不建议这么做,方法的返回值应该是明确的)

       通配符不能用作泛型方法的实参,不能用于范型类的初始化,即不能用作type argument。


1. 有界通配符

       有界通配符与有界泛型一样,代表着泛型的某个边界。不同的地方在于,有界通配符除了上界之外,还有下界,通过super关键字实现。


1.1 上界通配符

       利用“? extends UpperBoundClass”可以实现上界通配符,比如你想实现一个工具方法,对一个数字列表进行求和,可以将方法参数声明为: <? extends Number>:

public static double sumOfList1(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list) {
        s += n.doubleValue();
    }
    return s;
}

代码清单1


       调用代码清单1的sumOfList1时,可以传入元素类型为Number子类的列表,如:

//compile success
System.out.println(sumOfList1(Arrays.asList(1, 2, 2.3, new BigDecimal("2"))));
System.out.println(sumOfList1(Arrays.asList(1, 2, 3)));
System.out.println(sumOfList1(Arrays.asList(1.1, 2.2, 2.3)));
//output: 7.3, 6.0, 5.6

代码清单2


       与上界泛型类似,在处理通配符列表时,我们可以通过对上界类型的公共方法的访问,通过多态忽略具体类型不同对代码的影响。


1.2 下界通配符

       利用“? super LowerBoundClass”可以实现下界通配符,将允许接收的最低层次类型限制在LowerBoundClass。 与上界通配符完全不同的是,在代码中不能访问下界通配符的元素。在上界通配符中,能够保证元素一定是UpperBoundClass类型,而在下界通配符中,没有办法保证元素一定是什么类型。 所以我们只能往下界通配符列表中插入一个LowerBoundClass元素,但是不能读取或者遍历该列表。

       代码示例如下所示:

//compile success
public static void sumOfListLower(List<? super Integer> list) {
    list.add(1);
}

//compile fail: incompatible type
public static void sumOfListLower(List<? super Integer> list) {
    Integer value = list.get(0);
}

代码清单3


2. 无界通配符

       利用“?”可以实现无界通配符,表示完全未知的类型。有2种场景非常适合无界通配符:

  • 当某个方法需要满足不同的参数类型,又仅需要Object类提供的功能时。
  • 处于泛型类中,又不依赖于泛型T的方法,比如List.size()。

       我们可以看看JDK中的例子:

/**
 * JDK1.8 java.util.Collections
 *
 * Reverses the order of the elements in the specified list.<p>
 *
 * This method runs in linear time.
 *
 * @param  list the list whose elements are to be reversed.
 * @throws UnsupportedOperationException if the specified list or
 *         its list-iterator does not support the <tt>set</tt> operation.
 */
public static void reverse(List<?> list) {
    int size = list.size();
    if (size < REVERSE_THRESHOLD || list instanceof RandomAccess) {
        for (int i=0, mid=size>>1, j=size-1; i<mid; i++, j--)
            swap(list, i, j);
    } else {
        ListIterator fwd = list.listIterator();
        ListIterator rev = list.listIterator(size);
        for (int i=0, mid=list.size()>>1; i<mid; i++) {
            Object tmp = fwd.next();
            fwd.set(rev.previous());
            rev.set(tmp);
        }
    }
}

代码清单4


       在代码清单4的reverse方法中,反转逻辑和具体类型是无关的,所以方法参数采用了List<?>。


3. 通配符类型捕获

       在某些情况下,编译器会代码上下文进行类型推到,这个过程称为通配符捕获。 通常情况下开发者不需要关注本小节内容,因为通配符捕获的错误仅在下述十分罕见的代码中才会发生:

//compile fail: incompatible type
public static void setFalse(List<?> target) {
    target.set(0, target.get(0));
}

代码清单5


       如果要对某些容器做写入操作,最好声明成明确的类型。对于代码清单5的错误,Oracle官方教程给出了具体的解释,具体内容请参考Wildcard Capture and Helper Methods


延伸

       有关通配符上下界的使用,还可以阅读泛型中<? super T>和<? extends T>的区别Guidelines for Wildcard Use


附件

       本博文所展示的源代码由此获得


参考文献

[1] Josh Juneau.泛型:工作原理及其重要性[EB/OL].https://www.oracle.com/technetwork/cn/articles/java/juneau-generics-2255374-zhs.html,2019-07-02.
[2] The Java™ Tutorials - Generics[EB/OL].https://docs.oracle.com/javase/tutorial/java/generics/index.html,2019-07-02.
[3] The Java® Virtual Machine Specification[EB/OL].https://docs.oracle.com/javase/specs/jvms/se10/html/jvms-4.html#jvms-4.3,2019-07-04.