类型参数的限定
无论是泛型类、泛型方法还是泛型接口,关于类型参数,我们都知之甚少,只能把它当作Object,但Java支持限定这个参数的一个上界,也就是说:参数必须为给定的上界类型或其子类型,这个限定是通过extends
关键字来表示的。
上界为某个具体类
public class NumberPair<U extends Number> {
}
指定边界之后,类型擦除时就不会转换为Object了,而是会转换为它的边界类型。
上界为某个接口
在泛型方法中,我们想要参数必须实现了某个接口。
public static <T extends Comparable<T>> T max(T[] arr) {
T max = arr[0];
for(int i = 1; i < arr.length; i++) {
if(arr[i].compareTo(max) > 0) {
max = arr[i];
}
}
return max;
}
<T extends Comparable<T>>
可能会令人比较费解,这称为递归类型限制。 T表示一种数据类型,必须实现Comparable接口,且必须可以和同类型的元素进行比较。
上界为其他类型参数
假设我们有这么一个类,该类主要仿写集合中的一些功能。
public class DynamicArray<E> {
private Object[] elementData;
private static final int DEFAULT_CAPACITY = 10;
private int size;
public DynamicArray() {
this.elementData = new Object[DEFAULT_CAPACITY];
}
public void add(E e) {
// 扩容方法,在此省略
ensureCapacity(size + 1);
elementData[size++] = e;
}
public E get(int index) {
return (E) elementData[index];
}
public int size() {
return size;
}
public E set(int index, E element) {
E oldValue = get(index);
elementData[index] = element;
return oldValue;
}
}
现在我们想添加一个功能,就是一次插入一个DynamicArray。我们可能会想当然的这么想
public void addAll(DynamicArray<E> c) {
for(int i = 0; i < c.size; i++) {
add(c.get(i));
}
}
测试:
public static void main(String[] args) {
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
numbers.add(99);
ints.add(100);
numbers.addAll(ints);
}
这个时候,其实最后一行的addAll方法是会报错的。
numbers是一个Number类型的容器,ints是一个Integer类型的容器,Integer是Number的子类,我们希望把ints添加到numbers中,应该说是比较合理的。
但是我们这样的addAll方法是有问题的,我们可以这样来假设,假设刚才的addAll方法是可以编译成功的
DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<Number> numbers = ints; // 假设合法
numbers.add(new Double(3.14));
这样是不是就发现问题了?破坏了Java泛型关于类型安全的保证
这里的原因是:Integer是Number的子类,但是DynamicArray<Integer>
不是DynamicArray<Number>
的子类。
但是这里的需求是没有问题的,将Integer添加到Number容器中。我们可以通过类型限定来解决:
public <T extends E> void addAll(DynamicArray<T> c) {
for(int i = 0; i < c.size; i++) {
add(c.get(i));
}
}
通配符
在上面的那个示例中,addAll方法,我们使用了其他类型参数作为上界。但是这种写法有点繁琐,我们可以通过通配符写出更简洁的形式。
public void addAll(DynamicArray<? extends E> c) {
for(int i = 0; i < c.size; i++) {
add(c.get(i));
}
}
?
表示通配符,<? extends E>
表示有限定通配符
那么问题就来了<T extedns E>
和 <? extends E>
有什么关系区别呢?
<T extedns E>
用于定义类型参数,它声明了一个类型参数T,可以房子泛型类定义中类名后面、泛型方法返回值前面<? extends E>
用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类。
理解通配符
上面提到了有限定通配符,相应的还有无限定通配符,这里我们继续来扩展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 <T> int indexOf(DynamicArray<T> arr, Object elm) {
for(int i = 0; i < arr.size(); i++) {
if(arr.get(i).equals(elm)) {
return i;
}
}
return -1;
}
上面两种方法可以达到同样的作用,不过通配符形式更为简洁。 虽然通配符形式更为简洁,但上面两种通配符都有一个重要的限制:只能读,不能写
public static void main(String[] args) {
DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Integer a = 998;
ints.add(a);
numbers.add(a);
numbers.add((Number)a);
numbers.add((Object)a);
}
后面numbers.add()三种方式都是非法的。 因为<? extends Number>
表示的是Number的某个子类,但是不知道具体子类型,如果允许写入,Java就无法确保类型安全了,所以这样的写法就是非法的了。
既然无法写入,那么这种形式的实际意义是什么呢?
我们可以将公共API的方法使用通配符形式,但内部调用带类型参数的方法
private static <T> void swapInternal(DynamicArray<T> arr, int i, int j) {
T temp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, temp);
}
public static void swap(DynamicArray<?> arr, int i, int j) {
swap(arr, i, j);
}