Java 通配符

参数类型限定

前面我们提到一个例子,为了将Integer对象添加到Number容器中,我们的类型参数使用了其他类型参数作为上界,我们提到,这种写法有点烦琐,它可以替换为更为简洁的通配符形式:

public void addAll(DynamicArray<? extends E> c) {
    for(int i=0; i<c.size; i++){
        add(c.get(i));
    }
}

这个方法没有定义类型参数,c的类型是DynamicArray<? extends E>, ?表示通配符,<? extends E>表示有限定通配符,匹配E或E的某个子类型,具体什么子类型是未知的。使用这个方法的代码不需要做任何改动,还可以是:

DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);

这里,E是Number类型,DynamicArray<? extends E>可以匹配DynamicArray<Integer>

那么问题来了,同样是extends关键字,同样应用于泛型,<T extends E><? extends E>到底有什么关系?它们用的地方不一样,我们解释一下:

  • 用于定义类型参数,它声明了一个类型参数T,可放在泛型类定义中类名后面、泛型方法返回值前面。
  • <? extends E>用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。

虽然它们不一样,但两种写法经常可以达成相同目标,比如,前面例子中,下面两种写法都可以:

public void addAll(DynamicArray<? extends E> c)
public <T extends E> void addAll(DynamicArray<T> c)

那么,到底应该用哪种形式呢?我们先进一步理解通配符,然后再解释。

理解通配符

除了有限定通配符,还有一种通配符,形如DynamicArray<? >,称为无限定通配符。我们来看个例子,在DynamicArray中查找指定元素,代码如下:

public static int indexOf(DynamicArray<? > arr, Object elm){
    for(int i=0; i<arr.size(); i++){
        if(arr.get(i).equals(elm)){
            return i;
        }
    }
    return -1;
}

其实,这种无限定通配符形式也可以改为使用类型参数。也就是说,下面的写法:

public static int indexOf(DynamicArray<? > arr, Object elm)

可以改为:

public static <T> int indexOf(DynamicArray<T> arr, Object elm)

不过,通配符形式更为简洁。虽然通配符形式更为简洁,但上面两种通配符都有一个重要的限制:只能读,不能写。怎么理解呢?看下面的例子:

DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Integer a = 200;
numbers.add(a); //错误!
numbers.add((Number)a); //错误!
numbers.add((Object)a); //错误!

三种add方法都是非法的,无论是Integer,还是Number或Object,编译器都会报错。为什么呢?问号就是表示类型安全无知,? extends Number表示是Number的某个子类型,但不知道具体子类型,如果允许写入,Java就无法确保类型安全性,所以干脆禁止。我们来看个例子,看看如果允许写入会发生什么:

DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Number n = new Double(23.0);
Object o = new String("hello world");
numbers.add(n);
numbers.add(o);

如果允许写入Object或Number类型,则最后两行编译就是正确的,也就是说,Java将允许把Double或String对象放入Integer容器,这显然违背了Java关于类型安全的承诺。

大部分情况下,这种限制是好的,但这使得一些理应正确的基本操作无法完成,比如交换两个元素的位置,看如下代码:

public static void swap(DynamicArray<? > arr, int i, int j){
    Object tmp = arr.get(i);
    arr.set(i, arr.get(j));
    arr.set(j, tmp);
}

这个代码看上去应该是正确的,但Java会提示编译错误,两行set语句都是非法的。不过,借助带类型参数的泛型方法,这个问题可以如下解决:

private static <T> void swapInternal(DynamicArray<T> arr, int i, int j){
    T tmp = arr.get(i);
    arr.set(i, arr.get(j));
    arr.set(j, tmp);
}
public static void swap(DynamicArray<? > arr, int i, int j){
    swapInternal(arr, i, j);
}

swap可以调用swapInternal,而带类型参数的swapInternal可以写入。Java容器类中就有类似这样的用法,公共的API是通配符形式,形式更简单,但内部调用带类型参数的方法。

除了这种需要写的场合,如果参数类型之间有依赖关系,也只能用类型参数,比如,将src容器中的内容复制到dest中:

public static <D, S extends D> void copy(DynamicArray<D> dest,
        DynamicArray<S> src){
    for(int i=0; i<src.size(); i++){
        dest.add(src.get(i));
    }
}

S和D有依赖关系,要么相同,要么S是D的子类,否则类型不兼容,有编译错误。不过,上面的声明可以使用通配符简化,两个参数可以简化为一个,如下所示:

public static <D> void copy(DynamicArray<D> dest,
        DynamicArray<? extends D> src){
    for(int i=0; i<src.size(); i++){
        dest.add(src.get(i));
    }
}

如果返回值依赖于类型参数,也不能用通配符,比如,计算动态数组中的最大值,如下所示:

public static <T extends Comparable<T>> T max(DynamicArray<T> arr){
    T max = arr.get(0);
    for(int i=1; i<arr.size(); i++){
        if(arr.get(i).compareTo(max)>0){
            max = arr.get(i);
        }
    }
    return max;
}

上面的代码就难以用通配符代替。

现在我们再来看泛型方法到底应该用通配符的形式还是加类型参数。两者到底有什么关系?我们总结如下。

  • 通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做。
  • 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以,能用通配符的就用通配符。
  • 如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。
  • 通配符形式和类型参数往往配合使用,比如,上面的copy方法,定义必要的类型参数,使用通配符表达依赖,并接受更广泛的数据类型。
赞(0)

评论 抢沙发

评论前必须登录!