java基礎學習總結(九):深入理解Java泛型
一、什麼是泛型
“泛型” 意味著編寫的程式碼可以被不同型別的物件所重用。泛型的提出是為了編寫重用性更好的程式碼。泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。
比如常見的集合類 LinkedList:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>,Deque<E>,Cloneable,Serializable{ //..... transient Link<E> voidLink; //..... }
可以看到,LinkedList<E> 類名及其實現的介面名後有個特殊的部分<E>,而且它的成員的型別 Link<E> 也包含一個<E>,這個符號的就是型別引數,它使得在執行中,建立一個 LinkedList 時可以傳入不同的型別。
二、為什麼引入泛型
在引入泛型之前,要想實現一個通用的、可以處理不同型別的方法,你需要使用 Object 作為屬性和方法引數,比如這樣:
public class Generic{ private Object[] mData; public Generic(int capacity){ mData = new Object[capacity]; } public Object getData(int index){ //..... return mData[index]; } public void add(int index,Object item){ //..... mData[index] = item; } }
它使用一個 Object 陣列來儲存資料,這樣在使用時可以新增不同型別的物件:
Generic generic = new Generic(10);
generic.add(0,"fangxing");
generic.add(1,23);
Object 是所有類的父類,所有的類都可以作為成員被新增到上述類中;當需要使用的時候,必須進行強制轉換,而且這個強轉很有可能出現轉換異常:
String item1 = (String) generic.getData(0); String item2 = (String) generic.getData(1);
第二行程式碼將一個 Integer 強轉成 String,執行時會報錯 :
可以看到,使用 Object 來實現通用、不同型別的處理,有這麼兩個缺點:
- 每次使用時都需要強制轉換成想要的型別
- 在編譯時編譯器並不知道型別轉換是否正常,執行時才知道,不安全
根據《Java 程式設計思想》中的描述,泛型出現的動機在於:
有許多原因促成了泛型的出現,而最引人注意的一個原因,就是為了建立容器類。
在 JDK 1.5 出現泛型以後,許多集合類都使用泛型來儲存不同型別的元素,比如 Collection:
public interface Collection<E> extends Iterable<E>{
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collecion<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
}
實際上引入泛型的主要目標有以下幾點:
型別安全
- 泛型的主要目標是提高 Java 程式的型別安全
- 編譯時期就可以檢查出因 Java 型別不正確導致的 ClassCastException 異常
- 符合越早出錯代價越小原則
消除強制型別轉換
- 泛型的一個附帶好處是,使用時直接得到目標型別,消除許多強制型別轉換
- 所得即所需,這使得程式碼更加可讀,並且減少了出錯機會
潛在的效能收益
- 由於泛型的實現方式,支援泛型(幾乎)不需要 JVM 或類檔案更改
- 所有工作都在編譯器中完成
- 編譯器生成的程式碼跟不使用泛型(和強制型別轉換)時所寫的程式碼幾乎一致,只是更能確保型別安全而已
三、泛型的使用方式
泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。
型別引數的意義是告訴編譯器這個集合中要存放例項的型別,從而在新增其他型別時做出提示,在編譯時就為型別安全做了保證。
引數型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面、泛型方法。
public class GenericClass<F>{
private F mContent;
public GenericClass(F content){
mContent = content;
}
/*
泛型方法
*/
public F getContent(){
return mContent;
}
public void setContent(F content){
mcontent = content;
}
/*
泛型介面
*/
public interface GenericInterface<T>{
void doSomething(T t);
}
}
泛型類
泛型類和普通類的區別就是類名後有型別引數列表 <E>,既然叫“列表”了,當然這裡的型別引數可以有多個,比如 public class HashMap<K, V>,引數名稱由開發者決定。
類名中宣告引數型別後,內部成員、方法就可以使用這個引數型別,比如上面的 GenericClass<F> 就是一個泛型類,它在類名後聲明瞭型別 F,它的成員、方法就可以使用 F 表示成員型別、方法引數/返回值都是 F 型別。
泛型類最常見的用途就是作為容納不同型別資料的容器類,比如 Java 集合容器類。
泛型介面
和泛型類一樣,泛型介面在介面名後新增型別引數,比如上面的 GenericInterface<T>,介面宣告型別後,介面方法就可以直接使用這個型別。
實現類在實現泛型介面時需要指明具體的引數型別,不然預設型別是 Object,這就失去了泛型介面的意義。
未指明型別的實現類,預設是 Object 型別:
public class Generic implements GenericInterface{
@Override
public void doSomething(Object o){
//...
}
}
指明瞭型別的實現:
public class Generic implements GericInterface<String>{
@Override
public void doSomething(String s){
//.....
}
}
泛型介面比較實用的使用場景就是用作策略模式的公共策略, Comparator就是一個泛型介面:
public interface Comparator<T>{
public int compare(T lhs, Trhs);
public bollean equals(Object object);
}
泛型介面定義基本的規則,然後作為引用傳遞給客戶端,這樣在執行時就能傳入不同的策略實現類。
泛型方法
泛型方法是指使用泛型的方法,如果它雖在的類是一個泛型類,那就很簡單了,直接使用類宣告的引數。
如果一個方法所在的類不是泛型類,或者他想要處理不同於泛型類宣告型別的資料,那它就需要自己宣告型別。
/*
傳統的方法,會有unchecked ... raw type 的警告
*/
public Set union(Set s1, Set s2){
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
/*
泛型方法,介於方法修飾符和返回值之間的稱作 型別引數列表<A,V,F,E....>(可以有多個)
型別引數列表 指定引數、返回值中泛型的引數類型範圍,命名慣例與泛型相同。
*/
public <E> Set<E> union2(Set<E> s1, Set<E> s2){
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
四、泛型的萬用字元
萬用字元:傳入的型別有一個指定的範圍,從而可以進行一些特定的操作
泛型中有三種萬用字元形式:
1.<?>無限制萬用字元
2.<? extends E> extends 關鍵字聲明瞭型別的上界,表示引數化的型別可能是所指定的型別,或者是此型別的子類。
3.<? super E> super 關鍵字聲明瞭型別的下界,表示引數化型別可能是指定型別,或者是此型別的父類。
無限制萬用字元 < ?>
要使用泛型,但是不確定或者不關心實際要操作的型別,可以使用無限制萬用字元(尖括號裡一個問號,即 <?> ),表示可以持有任何型別。
? 和 Object 不一樣,List<?> 表示未知型別的列表,而 List<Object> 表示任意型別的列表。
如傳入個 List<String> ,這時 List 的元素型別就是 String,想要往 List 裡新增一個 Object,這當然是不可以的。
上界萬用字元 < ? extends E>
在型別引數中使用 extends 表示這個泛型中的引數必須是 E 或者 E 的子類,這樣有兩個好處:
- 如果傳入的型別不是 E 或者 E 的子類,編輯不成功
- 泛型中可以使用 E 的方法,要不然還得強轉成 E 才能使用
下界萬用字元 < ? super E>
在型別引數中使用 super 表示這個泛型中的引數必須是 E 或者 E 的父類。
private <E> void add(List<? super E> dst, List<E> Src){
for (E e : src){
dst.add(e);
}
}
上面的 dst 型別 “大於等於” src 的型別,這裡的“大於等於”是指 dst 表示的範圍比 src 要大,因此裝得下 dst 的容器也就能裝 src。
萬用字元比較
無限制萬用字元 < ?> 和 Object 有些相似,用於表示無限制或者不確定範圍的場景。
< ? super E> 用於靈活寫入或比較,使得物件可以寫入父型別的容器,使得父型別的比較方法可以應用於子類物件。
< ? extends E> 用於靈活讀取,使得方法可以讀取 E 或 E 的任意子型別的容器物件。
因此使用萬用字元的基本原則:
- 如果引數化型別表示一個 T 的生產者,使用 < ? extends T>;(T 的子類)
- 如果它表示一個 T 的消費者,就使用 < ? super T>;(T 的父類)
- 如果既是生產又是消費,那使用萬用字元就沒什麼意義了,因為你需要的是精確的引數型別。
小總結一下:
- T 的生產者的意思就是結果會返回 T,這就要求返回一個具體的型別,必須有上限才夠具體;
- T 的消費者的意思是要操作 T,這就要求操作的容器要夠大,所以容器需要是 T 的父類,即 super T;
舉個例子:
private <E extends Comparable<? super E>> E max(List<? extends E> e1){
if(e1 == null){
return null;
}
//迭代器返回的元素屬於 E 的某個子型別
Iterator<? extends E> iterator = e1.iterator();
E result = iterator.next();
while (iterator.hasNext()){
E next = iterator.next();
if(next.compareTo(result)>0){
result = next;
}
}
return result;
}
1.要進行比較,所以 E 需要是可比較的類,因此需要 extends Comparable<…>(注意這裡不要和繼承的 extends 搞混了,不一樣)
2.Comparable< ? super E> 要對 E 進行比較,即 E 的消費者,所以需要用 super
3.而引數 List< ? extends E> 表示要操作的資料是 E 的子類的列表,指定上限,這樣容器才夠大
五、泛型的型別擦除
Java 中的泛型和 C++ 中的模板有一個很大的不同:
- C++ 中模板的例項化會為每一種型別都產生一套不同的程式碼,這就是所謂的程式碼膨脹。
- Java 中並不會產生這個問題。虛擬機器中並沒有泛型型別物件,所有的物件都是普通類。
在 Java 中,泛型是 Java 編譯器的概念,用泛型編寫的 Java 程式和普通的 Java 程式基本相同,只是多了一些引數化的型別同時少了一些型別轉換。
實際上泛型程式也是首先被轉化成一般的、不帶泛型的 Java 程式後再進行處理的,編譯器自動完成了從 Generic Java 到普通 Java 的翻譯,Java 虛擬機器執行時對泛型基本一無所知。
當編譯器對帶有泛型的java程式碼進行編譯時,它會去執行型別檢查和型別推斷,然後生成普通的不帶泛型的位元組碼,這種普通的位元組碼可以被一般的 Java 虛擬機器接收並執行,這在就叫做 型別擦除(type erasure)。
實際上無論你是否使用泛型,集合框架中存放物件的資料型別都是 Object,這一點不僅僅從原始碼中可以看到,通過反射也可以看到。
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(Strings.getClass()==integers.getClass());//true
上面程式碼輸出結果並不是預期的false,而是true。其原因就是泛型的擦除。
六、擦除的實現原理
一直有個疑問,Java 編譯器在編譯期間擦除了泛型的資訊,那執行中怎麼保證新增、取出的型別就是擦除前宣告的呢?
Java 編輯器會將泛型程式碼中的型別完全擦除,使其變成原始型別。當然,這時的程式碼型別和我們想要的還有距離,接著 Java 編譯器會在這些程式碼中加入型別轉換,將原始型別轉換成想要的型別。這些操作都是編譯器後臺進行,可以保證型別安全。總之泛型就是一個語法糖,它執行時沒有儲存任何型別資訊。
擦除導致的泛型不可變性
泛型中沒有邏輯上的父子關係,如 List 並不是 List 的父類。兩者擦除之後都是List,所以形如下面的程式碼,編譯器會報錯:
/*
兩者並不是方法的過載,擦除之後就是同一方法,所以編譯不會通過。
擦除之後:
void m(List numbers){}
void m(List Strings){} //編譯不通過,已經存在形同方法簽名
*/
void method(List<Object> numbers){}
void method(List<String> strings){}
泛型的這種情況稱為 不可變性,與之對應的概念是 協變、逆變:
- 協變:如果 A 是 B 的父類,並且 A 的容器(比如 List< A>) 也是 B 的容器(List< B>)的父類,則稱之為協變的(父子關係保持一致)
- 逆變:如果 A 是 B 的父類,但是 A 的容器 是 B 的容器的子類,則稱之為逆變(放入容器就篡位了)
- 不可變:不論 A B 有什麼關係,A 的容器和 B 的容器都沒有父子關係,稱之為不可變
Java 中陣列是協變的,泛型是不可變的。
擦除的拯救者:邊界
我們知道,泛型執行時被擦除成原始型別,這使得很多操作無法進行.
如果沒有指明邊界,型別引數將被擦除為 Object。
如果我們想要讓引數保留一個邊界,可以給引數設定一個邊界,泛型引數將會被擦除到它的第一個邊界(邊界可以有多個),這樣即使執行時擦除後也會有範圍。
比如:
public class GenericErasure {
interface Game{
void play();
}
interface Program{
void code();
}
public static calss People<T extends Program & Game>{
private T mPeople;
public People(T people){
mPeople = people;
}
public void habit(){
mPeople.code();
mPeople.play();
}
}
}
上述程式碼中, People 的型別引數 T 有兩個邊界,編譯器事實上會把型別引數替換為它的第一個邊界的型別。
七、泛型的規則
- 泛型的引數型別只能是類(包括自定義類),不能是簡單型別。
- 同一種泛型可以對應多個版本(因為引數型別是不確定的),不同版本的泛型類例項是不相容的。
- 泛型的型別引數可以有多個
- 泛型的引數型別可以使用 extends 語句,習慣上稱為“有界型別”
- 泛型的引數型別還可以是萬用字元型別,例如 Class
泛型的使用場景
當類中要操作的引用資料型別不確定的時候,過去使用 Object 來完成擴充套件,JDK 1.5後推薦使用泛型來完成擴充套件,同時保證安全性。
八、總結
1.上面說到使用 Object 來達到複用,會失去泛型在安全性和直觀表達性上的優勢,那為什麼 ArrayList 等原始碼中的還能看到使用 Object 作為型別?
泛型出現時,Java 平臺即將進入它的第二個十年,在此之前已經存在了大量沒有使用泛型的 Java 程式碼。人們認為讓這些程式碼全部保持合法,並且能夠與使用泛型的新程式碼互用,非常重要。
這樣都是為了相容,新程式碼裡要使用泛型而不是原始型別。
2.泛型是通過擦除來實現的。因此泛型只在編譯時強化它的型別資訊,而在執行時丟棄(或者擦除)它的元素型別資訊。擦除使得使用泛型的程式碼可以和沒有使用泛型的程式碼隨意互用。
3.如果型別引數在方法宣告中只出現一次,可以用萬用字元代替它。
private <E> void swap(List<E> list, int i, int j){
//....
}
只出現了一次 型別引數,沒有必要宣告,完全可以用萬用字元代替:
private void swap(List<?> list, int i, int j){
//...
}
對比一下,第二種更加簡單清晰吧。
4.陣列中不能使用泛型
Array 事實上並不支援泛型,這也是為什麼 Joshua Bloch 在 《Effective Java》一書中建議使用 List 來代替 Array,因為 List 可以提供編譯期的型別安全保證,而 Array 卻不能。
5.Java 中 List<Object> 和原始型別 List 之間的區別?
- 在編譯時編譯器不會對原始型別進行型別安全檢查,卻會對帶引數的型別進行檢查
- 通過使用 Object 作為型別,可以告知編譯器該方法可以接受任何型別的物件,比如String 或 Integer
- 你可以把任何帶引數的型別傳遞給原始型別 List,但 卻不能把 List< String> 傳遞給接受 List< Object> 的方法,因為泛型的不可變性,會產生編譯錯誤。
九、補充
靜態資源不認識泛型
接上一個話題,如果把<T>去掉,那麼:
private static T ifThenElse(boolean b, T first, T second){
return b ? first : second;
}
報錯,T未定義。但是如果我們再把static去掉:
public class TestMain<T>{
public static void main(String[] args){}
@SuppressWarnings("unused")
private List<T> ifThenElse(boolean b,T first, T second){
return null;
}
}
這並不會有任何問題。兩相對比下,可以看出static方法並不認識泛型,所以我們要加上一個<T>,告訴static方法,後面的T是一個泛型。既然static方法不認識泛型,那我們看一下static變數是否認識泛型:
public class TestMain<T>{
private List<T> notStaticList;
private static List<T> staticList;
}
這證明了,static變數也不認識泛型,其實不僅僅是staic方法、static變數、static塊,也不認識泛型,可以自己試一下。總結起來就是一句話:靜態資源不認識泛型。