范型简介
从JDK 5.0开始,范型作为一种新的扩展被引入到了java语言中。
有了范型,我们可以对类型(type=class+interface)进行抽象。最常见的例子是容器类型。
List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3
可以用范型对以上代码进行优化:
List<Integer>
myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'
优化带来两点改进:
- 省去了造型(cast)的麻烦。
- 除了代码上的整洁,范型还在
compile-time
保证了代码的类型正确。如果没有范型,无法保证放入list的对象是Integer型。
定义简单的范型
从package java.util中摘录下接口List和Iterator的定义:
public interface List <E> {
void add(E x);
Iterator<E> iterator();
}
public interface Iterator<E> {
E next();
boolean hasNext();
}
这里声明了type parameter:E
。Type parameters在范型的全部声明中都可以用,就像使用其他普通的类型一样。
调用
范型的时候,需要为type parameter E 指定一个真实的类型变量【1】(又称为parameterized type),例如:
List<Integer> myIntList = new LinkedList<Integer>();
可以想象List<Integer>是List的一个版本,在这个版本里面,所有的 type parameter (E)都被Integer替换了:
public interface IntegerList {
void add(Integer x);
Iterator<Integer> iterator();
}
这种想象很有帮助,因为parameterized type的List<Integer> 确实包含了类似的方法;但是也容易带来误导,因为每次调用
范型并不会生成代码的一个拷贝,通过编译,一个范型类型的声明只会编译一次,生成一个class文件;每次调用
范型,类似于给一个方法传入了一个argument,只是这里传入的是一个普通的类型。
【1】这里用的是argument
,即传给方法的值;区别parameter,parameter是作为方法签名的一部分,用于定义方法。
范型和子类型
假设Foo是Bar的子类型(class或者interface),G是一个范型类型声明,G<Foo> 不是G<Bar>的子类型,这点有些反直觉。
wildcards 通配符
接着上一节的讨论,假设Foo是Bar的子类型(class或者interface),G是一个范型类型声明,G<Foo> 不是G<Bar>的子类型。可是,如果我们确实需要在G<Foo> 和G<Bar>之间建立父子关系呢?具体来说,假设有以下一段代码:
void printCollection(Collection c) {
Iterator i = c.iterator();
for (k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}
用范型对其进行优化,这里是一种错误的方式:
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
这样写本身没有错误,但是他对Collection中元素的类型进行了限制,只能是Object!那么,所有collection的超类是神马呢?就是Collection<?>(读作"collection of unknown"),这个Collection的元素类型可以任意匹配,?
被称作wildcard type
。
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
嗯,不错,现在我们可以从c
中读取出任意类型的元素。可以,这样一来,又出现了新的问题:什么样的元素可以放到c
里面去呢?答案是:任何类型的元素都无法放到c
里面去!因为无法知道c
中的type parameter(也许写作E)是什么类型。
Bounded Wildcards 有界通配符
可能是考虑到?
过于宽泛,java引入了Bounded Wildcards
,有界通配符。假设有以下代码:
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x, y, radius;
public void draw(Canvas c) {
...
}
}
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) {
...
}
}
// These classes can be drawn on a canvas:
public class Canvas {
public void draw(Shape s) {
s.draw(this);
}
}
// Assuming that they are represented as a list,
// it would be convenient to have a method in Canvas that draws them all:
public void drawAll(List<Shape> shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}
看上去不错,但是问题又来了,类型方法drawAll的签名参数中的Shape
是Circle
的超类,尽管Circle
是Shape
的子类,但是List<Circle>
不是List<Shape>
的子类。所以要想drawAll可以处理List<Circle>
,可以将其定义为:
public void drawAll(List<? extends Shape> shapes) {
...
}
Bounded Wildcards 也面临着?
面临的问题,那就是他们都过于宽泛,因此无法
确定什么样的元素可以放到集合里面:
public void addRectangle(List<? extends Shape> shapes) {
// Compile-time error!
shapes.add(0, new Rectangle());
}
范型方法
前面讨论了范型type的声明,其实,同样可以声明范型方法:
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // Correct
}
}
所谓范型方法
,就是在方法签名内的修饰符和方法返回类型之间,加入了type parameter,例如<T>。在调用方法的时候,并不需要传入type argument,编译器会根据actual argument的类型推断(infer)出type argument。
较之于范型类型,范型方法的声明要稍微复杂一些。具体来说,范型方法包含返回值和若干parameter,而他们之间可能会存在着类型的依赖关系。而这种依赖关系就带来一个问题,什么时候应该使用通配符,什么时候应该使用范型方法呢?
比如,查看JDK文档:
interface Collection<E> {
public boolean containsAll(Collection<?> c);
public boolean addAll(Collection<? extends E> c);
}
为什么不写成:
interface Collection<E> {
public <T> boolean containsAll(Collection<T> c);
public <T extends E> boolean addAll(Collection<T> c);
// Hey, type variables can have bounds too!
}
在containsAll 和 addAll中,type parameter T 仅使用了1次。返回值和其他parameter并不依赖于它,这种情况下,应该使用通配符。只有当返回值和parameter之间存在依赖的情况下,才应该使用范型方法。例如:
class Collections {
public static <T> void copy(List<T> dest, List<? extends T> src) {
...
}
范型是如何实现的
范型是通过编译器对代码的erasure转换实现的。可以把这一过程想象成source-to-source的翻译。例如:
public String loophole(Integer x) {
List<String> ys = new LinkedList<String>();
List xs = ys;
xs.add(x); // Compile-time unchecked warning
return ys.iterator().next();
}
将被翻译成:
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return(String) ys.iterator().next(); // run time error
}
在第二段代码中,我们从list中取出一个元素,并试图通过将其cast(造型)把它当成String处理,这里会得到一个ClassCastException。
因为在编译阶段,编译器对代码进行了erasure,<>内的一切都被删除了,所以所有对范型类型的调用(Invocations,或者说实例)共享同一个run-time class,随之而来的,static变量和方法也被这些实例共享,所以在static方法中,也无法引用type parameter;同时,Cast 和InstanceOf操作也就都失去了意义。
Collection cs = new ArrayList<String>();
// Illegal.
if (cs instanceof Collection<String>) { ... }
// Unchecked warning,
Collection<String> cstr = (Collection<String>) cs;
//gives an unchecked warning, since this isn't something the runtime system is //going to check for you.
同理,对于方法来说,type variables(<T>在方法中叫type variables,在类型声明中叫parameterized type)也不存在于run-time:
// Unchecked warning.
<T> T badCast(T t, Object o) {
return (T) o;
}
如何定义范型数组
private E[] elements = (E[]) new Object[10];
- 数组和范型对类型的检查是不同的。
对于数组来说,下面的语句是合法的:
Object[] arr = new String[10];
Object[] 是 String[]的超类,因为Object是String的超类。然而,对于范型来说,就没有这样的继承关系,因此,以下声明无法通过编译:
List<Object> list = new ArrayList<String>(); // Will not compile. generics are invariant.
java中引入范型,是为了在编译阶段强化类型检查。同时,因为type erasure
,范型也没有runtime的任何信息。所以,List<String>
只有静态类型的 List<String>
,和一个动态类型 List
。
但是,数组携带了runtime的类型信息。在runtime,数组用Array Store Check
来检查将要插入的元素是否和真实的数组类型兼容。因此,以下代码能很好的编译,但是由于Array Store Check
,会在runtime失败:
Object[] arr = new String[10];
arr[0] = new Integer(10);
回到范型,编译器会提供编译阶段的检查,避免这种以这种方式创建索引,防止runtime的异常出现。
- 那么,创建范型数组有什么问题呢?
创建元素的类型是type parameter, parameterized type 或者bounded wildcard parameterized type的数组是type-unsafe
的。考虑如下代码:
public <T> T[] getArray(int size) {
T[] arr = new T[size]; // Suppose this was allowed for the time being.
return arr;
}
在rumtime,T的类型未知,实际上创建的数组是Object[],因此在runtime,上面的方法像是:
public Object[] getArray(int size) {
Object[] arr = new Object[size];
return arr;
}
假设,有以下调用:
Integer[] arr = getArray(10);
这就是问题,这里将Object[] 指派给了一个Integer[]类型的索引,这段代码编译没有问题,但是在runtime会失败。因此,创建范型数组是不合法的。