背景:泛型這個知識點平時用的不多,但是在面試的時候很容就被問到,所以還是要准備一些基礎的知識儲備。
“泛型” 意味着編寫的代碼可以被不同類型的對象所重用。
泛型是在JDK1.5之后出現的。
泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。
可以看到,使用 Object 來實現通用、不同類型的處理,有這么兩個缺點:
- 每次使用時都需要強制轉換成想要的類型
- 在編譯時編譯器並不知道類型轉換是否正常,運行時才知道,不安全
根據《Java 編程思想》中的描述,泛型出現的動機在於:
有許多原因促成了泛型的出現,而最引人注意的一個原因,就是為了創建容器類。
實際上引入泛型的主要目標有以下幾點:
- 類型安全
- 泛型的主要目標是提高 Java 程序的類型安全
- 編譯時期就可以檢查出因 Java 類型不正確導致的 ClassCastException 異常
- 符合越早出錯代價越小原則
- 消除強制類型轉換
- 泛型的一個附帶好處是,使用時直接得到目標類型,消除許多強制類型轉換
- 所得即所需,這使得代碼更加可讀,並且減少了出錯機會
- 潛在的性能收益
- 由於泛型的實現方式,支持泛型(幾乎)不需要 JVM 或類文件更改
- 所有工作都在編譯器中完成
- 編譯器生成的代碼跟不使用泛型(和強制類型轉換)時所寫的代碼幾乎一致,只是更能確保類型安全而已
泛型類
泛型接口
泛型方法
泛型的通配符
無限制通配符
extends 關鍵字聲明了類型的上界,表示參數化的類型可能是所指定的類型,或者是此類型的子類
super 關鍵字聲明了類型的下界,表示參數化的類型可能是指定的類型,或者是此類型的父類
通配符比較
通過上面的例子我們可以知道,無限制通配符 < ?> 和 Object 有些相似,用於表示無限制或者不確定范圍的場景。
兩種有限制通配形式 < ? super E> 和 < ? extends E> 也比較容易混淆,我們再來比較下。
你了解泛型通配符與上下界嗎?
ps:上下限用圖說明
它們的目的都是為了使方法接口更為靈活,可以接受更為廣泛的類型。
- < ? super E> 用於靈活寫入或比較,使得對象可以寫入父類型的容器,使得父類型的比較方法可以應用於子類對象。
- < ? extends E> 用於靈活讀取,使得方法可以讀取 E 或 E 的任意子類型的容器對象。
用《Effective Java》 中的一個短語來加深理解:
為了獲得最大限度的靈活性,要在表示 生產者或者消費者 的輸入參數上使用通配符,使用的規則就是:生產者有上限、消費者有下限:
PECS: producer-extends, costumer-super
因此使用通配符的基本原則:
- 如果參數化類型表示一個 T 的生產者,使用 < ? extends T>;
- 如果它表示一個 T 的消費者,就使用 < ? super T>;
- 如果既是生產又是消費,那使用通配符就沒什么意義了,因為你需要的是精確的參數類型。
小總結一下:
- T 的生產者的意思就是結果會返回 T,這就要求返回一個具體的類型,必須有上限才夠具體;
- T 的消費者的意思是要操作 T,這就要求操作的容器要夠大,所以容器需要是 T 的父類,即 super T;
泛型的類型擦除
Java 中的泛型和 C++ 中的模板有一個很大的不同:
- C++ 中模板的實例化會為每一種類型都產生一套不同的代碼,這就是所謂的代碼膨脹。
- Java 中並不會產生這個問題。虛擬機中並沒有泛型類型對象,所有的對象都是普通類。
(摘自:blog.csdn.net/fw0124/arti…)
在 Java 中,泛型是 Java 編譯器的概念,用泛型編寫的 Java 程序和普通的 Java 程序基本相同,只是多了一些參數化的類型同時少了一些類型轉換。
實際上泛型程序也是首先被轉化成一般的、不帶泛型的 Java 程序后再進行處理的,編譯器自動完成了從 Generic Java 到普通 Java 的翻譯,Java 虛擬機運行時對泛型基本一無所知。
當編譯器對帶有泛型的java代碼進行編譯時,它會去執行類型檢查和類型推斷,然后生成普通的不帶泛型的字節碼,這種普通的字節碼可以被一般的 Java 虛擬機接收並執行,這在就叫做 類型擦除(type erasure)。
泛型的規則
- 泛型的參數類型只能是類(包括自定義類),不能是簡單類型。
- 同一種泛型可以對應多個版本(因為參數類型是不確定的),不同版本的泛型類實例是不兼容的。
- 泛型的類型參數可以有多個
- 泛型的參數類型可以使用 extends 語句,習慣上稱為“有界類型”
- 泛型的參數類型還可以是通配符類型,例如 Class
泛型的使用場景
當類中要操作的引用數據類型不確定的時候,過去使用 Object 來完成擴展,JDK 1.5后推薦使用泛型來完成擴展,同時保證安全性。
總結
1.上面說到使用 Object 來達到復用,會失去泛型在安全性和直觀表達性上的優勢,那為什么 ArrayList 等源碼中的還能看到使用 Object 作為類型?
根據《Effective Java》中所述,這里涉及到一個 “移植兼容性”:
泛型出現時,Java 平台即將進入它的第二個十年,在此之前已經存在了大量沒有使用泛型的 Java 代碼。人們認為讓這些代碼全部保持合法,並且能夠與使用泛型的新代碼互用,非常重要。
這樣都是為了兼容,新代碼里要使用泛型而不是原始類型。
2.泛型是通過擦除來實現的。因此泛型只在編譯時強化它的類型信息,而在運行時丟棄(或者擦除)它的元素類型信息。擦除使得使用泛型的代碼可以和沒有使用泛型的代碼隨意互用。
3.如果類型參數在方法聲明中只出現一次,可以用通配符代替它。
比如下面的 swap 方法,用於交換指定 List 中的兩個位置的元素:
private void swap(List list, int i, int j) {
//...
}
只出現了一次 類型參數,沒有必要聲明,完全可以用通配符代替:
private void swap(List list, int i, int j){
//...
}
對比一下,第二種更加簡單清晰吧。
4.數組中不能使用泛型
這可能是 Java 泛型面試題中最簡單的一個了,當然前提是你要知道 Array 事實上並不支持泛型,這也是為什么 Joshua Bloch 在 《Effective Java》一書中建議使用 List 來代替 Array,因為 List 可以提供編譯期的類型安全保證,而 Array 卻不能。
5.Java 中 List
和原始類型 List
之間的區別?
原始類型和帶參數類型 之間的主要區別是:
- 在編譯時編譯器不會對原始類型進行類型安全檢查,卻會對帶參數的類型進行檢查
- 通過使用 Object 作為類型,可以告知編譯器該方法可以接受任何類型的對象,比如String 或 Integer
- 你可以把任何帶參數的類型傳遞給原始類型 List,但卻不能把 List< String> 傳遞給接受 List< Object> 的方法,因為泛型的不可變性,會產生編譯錯誤。
這道題的考察點在於對泛型中原始類型的正確理解。
ps:這個地方還沒理解。。。。