前言
JDK1.5號(hào)稱是Java最重要的版本更新,而泛型又是JDK1.5中一個(gè)最重要的特征。
使用泛型機(jī)制編寫的程序代碼要比哪些雜亂地使用Object變量,然后再進(jìn)行強(qiáng)制類型轉(zhuǎn)換的代碼具有更好的安全性和可讀性。泛型對(duì)于集合類尤其有用。
泛型涉及的內(nèi)部機(jī)制比較復(fù)雜,所有分開兩章來講。
泛型入門
1. 泛型解決了什么問題
先看下面的一段代碼
ppublic class GenericDemo {
public static void main(String[] args) {
List list = new ArrayList();
list.add("Winson");
list.add("Tom");
list.add(1);
list.forEach(str -> System.out.println(((String)str).length()));
}
}
我們創(chuàng)建了一個(gè)list,希望是用來保存String的,但“不小心”把整數(shù)1放入了list中,當(dāng)程序?qū)nteger轉(zhuǎn)成String的時(shí)候會(huì)報(bào)ClassCastException。
上面這種"不小心",編譯器不會(huì)檢查出來。
我們使用泛型改進(jìn)這個(gè)程序
public class GenericDemo {
public static void main(String[] args) {
List<String> list = new ArrayList();
list.add("Winson");
list.add("Tom");
list.add(1); //會(huì)提示該list不接受int類型
list.forEach(str -> System.out.println(str.length()));//無須強(qiáng)制轉(zhuǎn)換
}
}
- 我們?cè)贚ist后面加了尖括號(hào)并指定了String類型,表示這個(gè)List只能保存String。如果add其他類型,編譯時(shí)就會(huì)檢查出來。
- list會(huì)記住所有元素的數(shù)值類型,無須對(duì)集合元素進(jìn)行強(qiáng)制轉(zhuǎn)換。
2. 泛型接口和泛型類類
我們先定義個(gè)簡(jiǎn)單的泛型類和接口
class Apple<T>{
private T first;
private T second;
public Apple(T first,T second){
this.first=first;
this.second=second;
}
public T getFirst() {return first;}
public T getSecond() {return second;}
public void setSecond(T second) {this.second =second;}
public void setFirst(T first) {this.first = first;}
}
public interface List<E>{
void add(E x);
}
public interface Map<K,V>{
V put(K key,V value);
}
類型變量
泛型類可以有多個(gè)類型變量。例如:
class Apple<T,U>{...}
類型變量使用大寫單個(gè)大寫的字母:
E表示集合的元素類型
K和V表示關(guān)鍵字和值的類型
T、U和S表示任意類型
3. 泛型方法
class ArrayUtils{
public static <T> T getMiddle(T... a){
return a[a.length/2];
}
}
這個(gè)泛型方法是在一個(gè)普通類中定義的,當(dāng)然,也可以定義在泛型類中。
類型變量放在修飾符(這是public static)的后面,返回類型的前面。
調(diào)用泛型方法
System.out.println(ArrayUtils.<String>getMiddle("a","b","c"));
在這種情況(實(shí)際也是大多數(shù)情況)下,方法調(diào)用中可以省略<String>類型參數(shù),編譯器能自動(dòng)判斷。
System.out.println(ArrayUtils.getMiddle("a","b","c"));
4. 類型變量的限定
我們先看看下面這段代碼
class ArrayUtils{
public static <T> T min(T[] a){
if (a==null||a.length==0) return null;
T smallest =a[0];
for (int i=1;i<a.length;i++)
if (smallest.compareTo(a[i])>0) smallest = a[i];
return smallest;
}
}
這段代碼存在的問題是如何確定T所屬的類有compareTo方法。
解決這個(gè)問題的方法是將T限定為實(shí)現(xiàn)了Comparable接口的類:
public static <T extends Comparable> T min(T[] a)
為什么使用extends而不是implements呢?
T是綁定類型的子類型(subtype)
T和綁定類型可以是類、也可以是接口
選用關(guān)鍵字extends原因是更接近子類的概念,而且Java的設(shè)計(jì)者也不打算在語言中在添加一個(gè)新的關(guān)鍵字(如sub)
另外,一個(gè)類型變量可以有多個(gè)限定
限定類型用&分隔
類型變量用逗號(hào)分隔
T extends Comparable & Serializable
泛型進(jìn)階
1. 類型擦除
Java的泛型是偽泛型
虛擬機(jī)里沒有泛型類型對(duì)象,所有對(duì)象都是普通類。在編譯期間,所有的泛型信息都會(huì)被擦除(erased)。
什么是擦除?
Java中的泛型基本上都是在編譯器這個(gè)層次來實(shí)現(xiàn)的。在生成的Java字節(jié)碼中是不包含泛型中的類型信息的。使用泛型的時(shí)候加上的類型參數(shù),會(huì)在編譯器在編譯的時(shí)候去掉。這個(gè)過程就稱為類型擦除。
如在代碼中定義的List<object>和List<String>等類型,在編譯后都會(huì)編程List。JVM看到的只是List,而由泛型附加的類型信息對(duì)JVM來說是不可見的。
泛型類在Java源碼上看起來與一般的類不同,在執(zhí)行時(shí)被虛擬機(jī)翻譯成對(duì)應(yīng)的“原始類型”
例子
可以通過兩個(gè)簡(jiǎn)單的例子,來證明java泛型的類型擦除。
public class Test1 {
public static void main(String[] args) {
ArrayList<String> arrayList1=new ArrayList<String>();
arrayList1.add("a");
ArrayList<Integer> arrayList2=new ArrayList<Integer>();
arrayList2.add(1);
System.out.println(arrayList1.getClass()==arrayList2.getClass());
}
}
在這個(gè)例子中,我們定義了兩個(gè)ArrayList泛型數(shù)組,一個(gè)是ArrayList<String>,一個(gè)是ArrayList<Integer>。
最后,我們通過arrayList1對(duì)象和arrayList2對(duì)象的getClass方法獲取它們的類的信息,最后發(fā)現(xiàn)結(jié)果為true。說明泛型類型String和Integer都被擦除掉了,只剩下了原始類型。
什么是原始類型?
原始類型(raw type)就是擦除去了泛型信息,最后在字節(jié)碼中的類型變量的真正類型。擦除類型變量,并替換為限定定類型(無限定的變量用Object)。
例如,上面的Apple<T>的原始類型如下:
class Apple{
private Object first;
private Object second;
public Apple(Object first,Object second){
this.first=first;
this.second=second;
}
public Object getFirst() {return first;}
public Object getSecond() {return second;}
public void setSecond(Object second) {this.second =second;}
public void setFirst(Object first) {this.first = first;}
}
因T是一個(gè)無限定的變量,所以直接用Object替換。就是一個(gè)普通類,就像泛型引入前實(shí)現(xiàn)的那樣。
翻譯泛型表達(dá)式
我們結(jié)合上面Apple的泛型類,再來看下面的代碼
Apple<Date> apple = new Apple();
Date date = apple.getFirst();
擦除后,getFirst返回Object類型,為什么不需要執(zhí)行強(qiáng)制類型轉(zhuǎn)換就可以賦值給Date類型的變量呢?
原因是編譯器會(huì)自動(dòng)插入Date的強(qiáng)制類型轉(zhuǎn)換。 也就是說,編譯器把getFirst方法翻譯成兩條虛擬機(jī)指令:
- 對(duì)原始方法Apple.getFirst的調(diào)用;
- 將返回的Object類型強(qiáng)制轉(zhuǎn)換成Employee類型。
當(dāng)存取一個(gè)泛型域時(shí)也會(huì)插入強(qiáng)制類型轉(zhuǎn)換,如:
Date date = apple.first;
翻譯泛型方法
類型的擦除也會(huì)出現(xiàn)在泛型方法中。
我們來看下面的代碼
public class BridgeDemo {
public static class One<T> {
public T getT() {
return null;
}
}
public static class Two extends One<String> {
public String getT() {
return null;
}
}
}
在泛型擦除后的代碼類似于:
public class BridgeDemo {
public static class One {
public Object getT() {
return null;
}
}
public static class Two extends One {
public String getT() {
return null;
}
}
}
我們來反編譯下Two這個(gè)類
public BridgeDemo$Two();
Code:
0: aload_0
1: invokespecial #1 // Method BridgeDemo$One."<init>":()V
4: return
public java.lang.String getT();
Code:
0: aconst_null
1: areturn
public java.lang.Object getT();
Code:
0: aload_0
1: invokevirtual #2 // Method getT:()Ljava/lang/String;
4: areturn
}
在反編譯后的輸出中,可以看見有一個(gè)新的合成方法“java.lang.Object getT()”,這是源代碼中沒有的。
這個(gè)方法是一個(gè)橋方法,它負(fù)責(zé)將調(diào)用代理到“java.lang.String getT()”。因?yàn)樵贘VM里,方法的返回類型是方法簽名的一部分,而創(chuàng)建橋方法是實(shí)現(xiàn)協(xié)變返回類型的方式。
我們?cè)诎殉绦蚋囊幌?/p>
public class BridgeDemo {
public static class One<T> {
public T getT(T args) {
return args;
}
}
public static class Two extends One<String> {
public String getT(String args) {
return args;
}
}
}
看下反編譯的結(jié)果
public class BridgeDemo$Two extends BridgeDemo$One<java.lang.String> {
public BridgeDemo$Two();
Code:
0: aload_0
1: invokespecial #1 // Method BridgeDemo$One."<init>":()V
4: return
public java.lang.String getT(java.lang.String);
Code:
0: aload_1
1: areturn
public java.lang.Object getT(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #2 // class java/lang/String
5: invokevirtual #3 // Method getT:(Ljava/lang/String;)Ljava/lang/String;
8: areturn
}
在這里,橋方法重寫了基類One,它不僅做了有參數(shù)的調(diào)用,同時(shí)還執(zhí)行了到“java.lang.String”的類型轉(zhuǎn)換。這意味著在執(zhí)行下面的代碼忽略編譯器的“uncheck”警告時(shí),橋方法將拋出ClassCastException異常。
public static void main(String[] args) {
One one = new Two();
one.getT(new Object());
}
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String
你需要記住的有關(guān)Java泛型轉(zhuǎn)換的事實(shí)
- 虛擬機(jī)中沒有泛型,只有普通類的方法
- 所有的類型參數(shù)都用他們的限定類型替換
- 橋方法被合成來保持多態(tài)
- 為保持類型安全性,必要時(shí)插入強(qiáng)制類型轉(zhuǎn)換