通配符有着令人费解和混淆的语法,但通配符大量应用于 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。
那么问题来了,同样是 extends 关键字,同样应用于泛型,和 <? extends E> 到底有什么关系?它们用的地方不一样:
- 用于定义类型参数,它声明了一个类型参数 T,可放在泛型类定义中类名后面、泛型方法返回值前面。
- 用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。
虽然他们不一样,但两种写法经常可以达成相同目标。
public void addAll(DynamicArray<? extends E> c) public <T extends E> void addAll(DynamicArray<T> c)
那么到底应该用哪种形式?
-
理解通配符
除了有限定通配符,还有一种通配符,形如 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 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 方法,定义必要的类型参数,使用通配符表达依赖,并解说更广泛的数据类型。
-
超类型通配符
还有一种通配符,与形式 <? extends E> 正好相反,它的形式为 <? super E>,称为超类型通配符,表示 E 的某个父类型。有了它,我们就可以更灵活地写入了。
如果没有这种语法,写入会有一些限制。
public void copyTo(DynamicArray<E> dest) { for(int i = 0; i< size; i++) { dest.add(get(i)); } }
这个方法也很简单,将当前容器中的元素添加到传入的目标容器中。我们可能希望这个使用:
DynamicArray<Integer> ints = new DynamicArray<Integer>(); ints.add(100); ints.add(34); DynamicArray<Number> numbers = new DynamicArray<>(); ints.copyTo(numbers);
Integer 是 Number 的子类,将 Integer 对象拷贝进 Number 容器,这种用法应该是合情合理的,但 Java 会提示编译错误,理由我们之前也说过,期望的参数类型是 DynamicArray,DynamicArray并不适用。
如之前所说,一般而言,不能将 DynamicArray看作 DynamicArray,但我们这里的用法没有问题,Java 解决这个问题的方法就是超类型通配符,可以将 copyTo 代码该为:
public void copyTo(DynamicArray<? super E> dest) { for(int i = 0; i< size; i++) { dest.add(get(i)); } }
这样就没问题。
超类型通配符另一个常用的场合是 Comparable/Comparator 接口。同样,如果不适用会有什么限制。
public static <T extends Comparable<T>> T max(DynamicArray<T> arr)
这个声明有什么限制呢?举个简单的例子,有两个类 Base 和 Child
class Base implements Comparable<Base> { private int sortOrder; public Base(int sortOrder) { this.sortOrder = sortOrder; } @Override public int compareTo(Base o) { if(sortOrder < o.sortOrder) { return -1; } else if(sortOrder > o.sortOrder) { return 1; } else { return 0; } } } class Child extends Base { public Child(int sortOrder) { super(sortOrder); } }
这里,Child 非常简单,只是继承了 Base。注意:Child 没有重新实现 Comparable 接口,因为 Child 的比较规则和 Base 是一样的。我们可能希望使用前面的 max 方法操作 Child 容器。
DynamicArray<Child> childs = new DynamicArray<Child>(); childs.add(new Child(20)); childs.add(new Child(80)); Child maxChild = mac(childs);
遗憾的是,Java 会提示错误,类型不匹配。我们可能会认为,Java 会将 max 方法的类型参数 T 推断为 Child 类型,但类型 T 的要求是 extends Comparable,而 Child 并没有实现 Comparable,它实现的是 Comparable。
但我们的需求是合理的,Base 类的代码已经有了关于比较所需要的全部数据,它应该可以用于比较 Child 对象。解决这个问题的方法就是修改 max 方法声明,使用超类型通配符。
public static <T extends Comparable<? super T>> T max(DynamicArray<T> arr)
这么修改一下就可以了,这种写法比较抽象,将 T 替换为 Child,就是:
Child extends Comparable<? super Child>
类型参数限定只有 extends 形式,没有 super 形式,比如前面的 copyTo 方法的通配符形式的声明:
public void copyTo(DynamicArray<? super E> dest)
如果类型参数限定支持 super 形式,则应该是:
public <T super E> void copyTo(DynamicArray<T> dest)
事实是,Java 并不支持这种语法。
对于有限定的通配符形式 <? extends E>,可以用类型参数限定替代,但是对于类似上面的超类型通配符,则无法用类型参数替代。
泛型的三种通配符形式 <?>、<? super E> 和 <? extends E>,并分析了与类型参数形式的区别和联系,他们都容易混淆:
- 他们的目的都是为了使方法接口更为灵活,可以接受更为广泛的类型。
- 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数形式替代。
- 和用于灵活读取,使得方法可以读取E或E的任意子类型的容器对象,他们可以用类型参数的形式替代,但通配符形式更为简洁。
在使用泛型类、方法和接口时,有一些值得注意的地方:
- 基本类型不能用于实例化类型参数
- 运行时类型信息不适用于泛型
- 类型擦除可能会引发一些冲突
在定义泛型类、方法和接口时,也有一些需要注意的地方:
- 不能通过类型参数创建对象
- 泛型类类型参数不能用于静态变量和方法
- 了解多个类型限定的语法
泛型与数组的关系:
- Java 不支持创建泛型数组
- 如果要存放泛型对象,可以使用原始类型的数组,或者使用泛型容器
- 泛型容器内部使用 Object 数组,如果要转换泛型容器为对应类型的数组,需要使用反射。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于