Java基础:泛型
泛型不协变
数组是协变的,即如果Integer是Number的子类型,则Integer[]也是Number[]的子类型
Integer[] is = new Integer[] {1, 2, 3};
Number[] ns = is;
ns[0] = new Integer(0);
ns[0] = new Double(0); // will cause java.lang.ArrayStoreException in runtime
以上代码可以通过编译,但是在运行时会抛出异常,这就是数组协变的代价,它需要再运行时才能正真的检测到类型异常。下面是类似的逻辑的但是用泛型实现的代码
List<Integer> is = new ArrayList<Integer>();
is.add(1);is.add(2);
List<Number> ns = is; // compile error
List<Object> os = is; // compile error
虽然一般来说List
通配符与上下界
泛型不协变的特性增加了类型安全性,但是使用上却似乎更加不便了。除了使用严格的类型参数外我们还可以使用类型通配符如
List<Integer> is = new ArrayList<Integer>();
List<?> xs = is;
int len = xs.size();
for (int i=0; i<len; i++) {
System.out.println((Integer)xs.get(i) + 10086);
}
xs.add(new Integer(1)); // compile error
此处我们实现了不同类型参数之间的一个(比较特别的)赋值,然而经过这次赋值后,xs
变量代表的List
已经失去了其在编译期的类型,因为它使用了类型通配符?
。从其中再取出元素时需要进行显式的转换,编译器不会再帮你检查。更糟糕的是我们不能在通过xs.add
向列表添加元素了。
幸亏除了单独使用通配符之外还可以给他确定一个上界或者下界。
List<Integer> is = new ArrayList<Integer>();
List<? extends Number> xs = is;
System.out.println(xs.get(0).intValue() + 10086);
extends确定了泛型容器内元素类型的一个上界,也就是说保证容器内部的元素都是Number或者Number的子类,所以从容器中再次获取元素时我们可以直接以Number类型来对待这个元素。但是要想以Integer的方式操作元素还是需要进行强制转换,因为根据定义我们只能确定用Number类型对待它时是绝对安全的,但是至于它到底是那个子类,重新取出元素时我们并不知道。这个就和数组的处理非常相似了
Number[] x = new Integer[]{1, 2, 3};
x[0]; // process as type Number, cast needed if want Integer type
注意如果同时还要向容器内放入元素那么这个容器声明就有问题,这和数组是不一样的,参考PECS规则。
List<Integer> is = new ArrayList<Integer>();
List<? extends Number> xs = is;
xs.add(new Integer(1)); // compile error
<?>与 的区别
public static void process(List<?> list) {
list.add(list.get(0)); // compile error
}
public static <T> void process(List<T> list ){
list.add(list.get(0));
}
PECS规则
producer-extends,consumer super. Effective Java p120
通配符可以使用extends与super来给其定类型的上下界,什么时候使用extends什么时候使用super可以用PECS这个规则来概括。即
- 当需要从某个泛型对象获取元素时(该对象作为一个生产者),使用extends来说明其泛型类型的上界
- 当需要把元素存入对象时(该对象所为一个消费者),使用super来说明其泛型类型的下界
extends
比如要从某个容器内获取元素时,或者这个泛型对象并不是一个容器但是它的值需要由别人提供并且被你用到,如果普通的一个函数参数。它们都是生产者,比较常见的有容器和迭代器。大部分情况下我们规定泛型界限时都用extends即可。
super
它用于充当消费者角色的对象类型说明。消费者,即它们会用到你提供的值。Comparable,Comparator均是这种类型。还有就是用来存储运算结果的一些容器类。
static void inflate(List<? super Integer> out) {
out.add(1);
out.add(2);
}
public static void main(String args[]) {
List<Object> d1 = new ArrayList<Object>();
List<Number> d2 = new ArrayList<Number>();
List<Integer> d3 = new ArrayList<Integer>();
inflate(d1);
inflate(d2);
inflate(d3);
}
运行之后d1, d2, d3
三个列表内均存储了inflate
函数产生的两个数字。如果我们把inflate
的参数类型改为
static void inflate(List<? extends Integer> out); // List<Object>, List<Number> conflict
static void inflate(List<Integer> out);
static void inflate(List<?> out);
static void inflate(List<Object> out);
都无法实现上述功能(使得d1,d2,d3得到填充)。
容器泛型
从PECS规则来讲,容器类所支持的一些操作使得容器本身既是一个生产者也是一个消费者。所以当需要在一个函数内同时对容器进行添加和获取这两类操作时,上界和下界限制都存在,也就是说相关的函数参数中泛型参数不再需要界限限定,它肯定是一个明确的类型。
Comparable泛型
有关Comparable泛型相比一般的泛型定义复杂许多,可以参照下面这个范式:
public static <T extends Comparable<? super T>> T max(List<? extends T> a) {
...
}
为什么使用Comparable<? super T>
而不是诸如:
Comparable<T> // too strict
Comparable<? extends T>
第一个要求过于严格,需要刚好是该类型实现了相应Comparable接口,如果一个类继承了一个已经实现了Comparable接口的类,但它自己没有重新去实现,这个类就无法被第一个Comparable定义使用。
public static void main(String args[]) {
List<Sedan> sedans = new ArrayList<Sedan>();
max(sedans); // compile error, invalid type bound, no Comparator<Sedan>
}
public static <T extends Comparator<T>> T max(List<? extends T> list) {
return null;
}
class Vehicle implements Comparator<Vehicle> {
private int speed;
public int compare(Vehicle o1, Vehicle o2) {
return o1.speed - o2.speed;
}
}
class Sedan extends Vehicle {
}
? extends T
就更不行了,? super T
就是让函数接受父类实现Comparable的情况。来看Comparable<E>
接口定义:
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
Comparable<? super T> x =
Comparable<T> y =
Comparable<? extends T> z =
其中的compare
需要用到类型参数,可以想象一下使用extends和super时的不同情况,如果有个Comparable