范型簡(jiǎn)介
從JDK 5.0開始,范型作為一種新的擴(kuò)展被引入到了java語言中。
有了范型,我們可以對(duì)類型(type=class+interface)進(jìn)行抽象。最常見的例子是容器類型。
List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3
可以用范型對(duì)以上代碼進(jìn)行優(yōu)化:
List<Integer>
myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'
優(yōu)化帶來兩點(diǎn)改進(jìn):
- 省去了造型(cast)的麻煩。
- 除了代碼上的整潔,范型還在
compile-time保證了代碼的類型正確。如果沒有范型,無法保證放入list的對(duì)象是Integer型。
定義簡(jiǎn)單的范型
從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在范型的全部聲明中都可以用,就像使用其他普通的類型一樣。
調(diào)用范型的時(shí)候,需要為type parameter E 指定一個(gè)真實(shí)的類型變量【1】(又稱為parameterized type),例如:
List<Integer> myIntList = new LinkedList<Integer>();
可以想象List<Integer>是List的一個(gè)版本,在這個(gè)版本里面,所有的 type parameter (E)都被Integer替換了:
public interface IntegerList {
void add(Integer x);
Iterator<Integer> iterator();
}
這種想象很有幫助,因?yàn)閜arameterized type的List<Integer> 確實(shí)包含了類似的方法;但是也容易帶來誤導(dǎo),因?yàn)槊看?code>調(diào)用范型并不會(huì)生成代碼的一個(gè)拷貝,通過編譯,一個(gè)范型類型的聲明只會(huì)編譯一次,生成一個(gè)class文件;每次調(diào)用范型,類似于給一個(gè)方法傳入了一個(gè)argument,只是這里傳入的是一個(gè)普通的類型。
【1】這里用的是argument,即傳給方法的值;區(qū)別parameter,parameter是作為方法簽名的一部分,用于定義方法。
范型和子類型
假設(shè)Foo是Bar的子類型(class或者interface),G是一個(gè)范型類型聲明,G<Foo> 不是G<Bar>的子類型,這點(diǎn)有些反直覺。
wildcards 通配符
接著上一節(jié)的討論,假設(shè)Foo是Bar的子類型(class或者interface),G是一個(gè)范型類型聲明,G<Foo> 不是G<Bar>的子類型。可是,如果我們確實(shí)需要在G<Foo> 和G<Bar>之間建立父子關(guān)系呢?具體來說,假設(shè)有以下一段代碼:
void printCollection(Collection c) {
Iterator i = c.iterator();
for (k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}
用范型對(duì)其進(jìn)行優(yōu)化,這里是一種錯(cuò)誤的方式:
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
這樣寫本身沒有錯(cuò)誤,但是他對(duì)Collection中元素的類型進(jìn)行了限制,只能是Object!那么,所有collection的超類是神馬呢?就是Collection<?>(讀作"collection of unknown"),這個(gè)Collection的元素類型可以任意匹配,?被稱作wildcard type。
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
嗯,不錯(cuò),現(xiàn)在我們可以從c中讀取出任意類型的元素??梢?,這樣一來,又出現(xiàn)了新的問題:什么樣的元素可以放到c里面去呢?答案是:任何類型的元素都無法放到c里面去!因?yàn)闊o法知道c中的type parameter(也許寫作E)是什么類型。
Bounded Wildcards 有界通配符
可能是考慮到?過于寬泛,java引入了Bounded Wildcards,有界通配符。假設(shè)有以下代碼:
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);
}
}
看上去不錯(cuò),但是問題又來了,類型方法drawAll的簽名參數(shù)中的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的聲明,其實(shí),同樣可以聲明范型方法:
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // Correct
}
}
所謂范型方法,就是在方法簽名內(nèi)的修飾符和方法返回類型之間,加入了type parameter,例如<T>。在調(diào)用方法的時(shí)候,并不需要傳入type argument,編譯器會(huì)根據(jù)actual argument的類型推斷(infer)出type argument。
較之于范型類型,范型方法的聲明要稍微復(fù)雜一些。具體來說,范型方法包含返回值和若干parameter,而他們之間可能會(huì)存在著類型的依賴關(guān)系。而這種依賴關(guān)系就帶來一個(gè)問題,什么時(shí)候應(yīng)該使用通配符,什么時(shí)候應(yīng)該使用范型方法呢?
比如,查看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并不依賴于它,這種情況下,應(yīng)該使用通配符。只有當(dāng)返回值和parameter之間存在依賴的情況下,才應(yīng)該使用范型方法。例如:
class Collections {
public static <T> void copy(List<T> dest, List<? extends T> src) {
...
}
范型是如何實(shí)現(xiàn)的
范型是通過編譯器對(duì)代碼的erasure轉(zhuǎn)換實(shí)現(xiàn)的??梢园堰@一過程想象成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中取出一個(gè)元素,并試圖通過將其cast(造型)把它當(dāng)成String處理,這里會(huì)得到一個(gè)ClassCastException。
因?yàn)樵诰幾g階段,編譯器對(duì)代碼進(jìn)行了erasure,<>內(nèi)的一切都被刪除了,所以所有對(duì)范型類型的調(diào)用(Invocations,或者說實(shí)例)共享同一個(gè)run-time class,隨之而來的,static變量和方法也被這些實(shí)例共享,所以在static方法中,也無法引用type parameter;同時(shí),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.
同理,對(duì)于方法來說,type variables(<T>在方法中叫type variables,在類型聲明中叫parameterized type)也不存在于run-time:
// Unchecked warning.
<T> T badCast(T t, Object o) {
return (T) o;
}
如何定義范型數(shù)組
private E[] elements = (E[]) new Object[10];
- 數(shù)組和范型對(duì)類型的檢查是不同的。
對(duì)于數(shù)組來說,下面的語句是合法的:
Object[] arr = new String[10];
Object[] 是 String[]的超類,因?yàn)镺bject是String的超類。然而,對(duì)于范型來說,就沒有這樣的繼承關(guān)系,因此,以下聲明無法通過編譯:
List<Object> list = new ArrayList<String>(); // Will not compile. generics are invariant.
java中引入范型,是為了在編譯階段強(qiáng)化類型檢查。同時(shí),因?yàn)?code>type erasure,范型也沒有runtime的任何信息。所以,List<String> 只有靜態(tài)類型的 List<String>,和一個(gè)動(dòng)態(tài)類型 List。
但是,數(shù)組攜帶了runtime的類型信息。在runtime,數(shù)組用Array Store Check來檢查將要插入的元素是否和真實(shí)的數(shù)組類型兼容。因此,以下代碼能很好的編譯,但是由于Array Store Check,會(huì)在runtime失敗:
Object[] arr = new String[10];
arr[0] = new Integer(10);
回到范型,編譯器會(huì)提供編譯階段的檢查,避免這種以這種方式創(chuàng)建索引,防止runtime的異常出現(xiàn)。
- 那么,創(chuàng)建范型數(shù)組有什么問題呢?
創(chuàng)建元素的類型是type parameter, parameterized type 或者bounded wildcard parameterized type的數(shù)組是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的類型未知,實(shí)際上創(chuàng)建的數(shù)組是Object[],因此在runtime,上面的方法像是:
public Object[] getArray(int size) {
Object[] arr = new Object[size];
return arr;
}
假設(shè),有以下調(diào)用:
Integer[] arr = getArray(10);
這就是問題,這里將Object[] 指派給了一個(gè)Integer[]類型的索引,這段代碼編譯沒有問題,但是在runtime會(huì)失敗。因此,創(chuàng)建范型數(shù)組是不合法的。