本文將從以下四個方面來系統的講解一下泛型,基本上涵蓋了泛型的主體內容,
- 什么是泛型?
- 為什么要使用泛型?
- 如何使用泛型?
- 泛型的特性
1. 什么是泛型?
泛型的英文是Generics,是指在定義方法、介面或類的時候,不預先指定具體的型別,而使用的時候再指定一個型別的一個特性,
寫過Java代碼的同學應該知道,我們在定義方法、介面或類的時候,都要指定一個具體的型別,比如:
public class test {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
上面代碼就定義了欄位name的型別為String,方法getName的回傳型別為String,這種寫法就是預先指定了具體的型別,而泛型就是不預先指定具體的型別,
Java中有一個型別叫ArrayList,相當于一個可變長度的陣列,在ArrayList型別中就沒有預先指定具體的型別,因為陣列可以存放任何型別的資料,如果要預先指定一個陣列型別的話,那要滿足大家對各種型別的需求,就要寫很多型別的ArrayList,要為每個class寫一個單獨的ArrayList,比如:
-
IntegerArrayList -
StringArrayList -
FloatArrayList -
LongArrayList -
...
這顯然不太現實,因為class有上千種,還有自己定義的class,那么在ArrayList中預先指定具體的型別就無法滿足需求,這個時候就需要使用泛型,即不指定存盤資料的具體的型別,這個型別由使用者決定,
為了解決型別的問題,我們必須把ArrayList變成一種模板:ArrayList<T>,代碼如下:
public class ArrayList<T> {
private T[] array;
private int size;
public void add(T e) {...}
public void remove(int index) {...}
public T get(int index) {...}
}
T可以是任何class,這樣一來,我們就實作了:撰寫一次模版,可以創建任意型別的ArrayList:
// 創建可以存盤String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 創建可以存盤Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 創建可以存盤Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();
因此,泛型也可以說是定義一種模板,例如ArrayList<T>,然后在代碼中為用到的類創建對應的ArrayList<型別>,(泛型是指在定義方法、介面或類的時候,不預先指定具體的型別,而使用的時候再指定一個型別的一個特性,)后面這種定義可能會更好理解其本質,
更為官方的定義是:泛型指“引數化型別”,泛型的本質是為了引數化型別(將型別引數化傳遞)(在不創建新的型別的情況下,通過泛型指定的不同型別來控制形參具體限制的型別),也就是說在泛型使用程序中,操作的資料型別被指定為一個引數,這種引數型別,可以在類、介面和方法中,分別被稱為泛型類,泛型介面,泛型方法,
2. 為什么要使用泛型?
參考自:Oracle 泛型檔案
與非泛型的代碼相比,使用泛型的代碼具有很多優點:
-
在編譯時會有更強的型別檢查
Java編譯器對泛型代碼進行強型別檢查,如果代碼違反型別安全,則會發出錯誤,修復編譯時的錯誤比修復運行時的錯誤會更加簡單,運行時的錯誤會更難找到,
說人話就是,使用泛型時,編譯器會對輸入的型別的進行檢查,型別與宣告的型別不一致時就會報錯,而不使用泛型,編譯器可能就檢測不到這個型別錯誤,就會在運行的時候報錯,
-
消除型別轉換
下面的代碼是沒有使用泛型的情況,這時候需要對型別進行轉換
List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0);使用泛型,就不需要對型別進行轉換
List<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); // no cast -
可以實作更通用的演算法
通過使用泛型,程式員可以對不同型別的集合進行自定義操作以實作通用演算法,并且代碼型別會更加安全、代碼更易讀
3. 如何使用泛型?
還是以ArrayList為例,如果不定義泛型型別時,泛型型別此時就是Object:
// 編譯器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);
此時,只能把<T>當作Object使用,沒有發揮泛型的優勢,
當我們定義泛型型別<String>后,List<T>的泛型介面變為強型別List<String>:
// 無編譯器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 無強制轉型:
String first = list.get(0);
String second = list.get(1);
編譯器看到泛型型別List<String>就可以自動推斷出后面的ArrayList<T>的泛型型別必須是ArrayList<String>,因此,可以把代碼簡寫為:
// 可以省略后面的Number,編譯器可以自動推斷泛型型別:
List<String> list = new ArrayList<>();
3.1 泛型類
泛型類的語法形式:
class name<T1, T2, ..., Tn> { /* ... */ }
泛型類的宣告和非泛型類的宣告類似,除了在類名后面添加了型別引數宣告部分,由尖括號(<>)分隔的型別引數部分跟在類名后面,它指定型別引數(也稱為型別變數)T1,T2,...和 Tn,
一般將泛型中的類名稱為原型,而將 <> 指定的引數稱為型別引數,
在泛型出現之前,一個類要想處理所有型別的資料,只能使用Object做資料轉換,實體如下:
public class Info {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = https://www.cnblogs.com/XiiX/p/value;
}
}
使用泛型之后,其實就是將Object換成T,并宣告<T>:
public class Info<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = https://www.cnblogs.com/XiiX/p/value;
}
}
在上面的例子中,在初始化一個泛型類時,使用 <> 指定了內部具體型別,在編譯時就會根據這個型別做強型別檢查,
實際上,不使用 <> 指定內部具體型別,語法上也是支持的(不推薦這么做),這樣的呼叫就失去泛型型別的優勢,如下所示:
public static void main(String[] args) {
Info info = new Info();
info.setValue(10);
System.out.println(info.getValue());
info.setValue("abc");
System.out.println(info.getValue());
}
上面是單個型別引數的泛型類,
下面我們看一下多個型別引數的泛型類該如何撰寫,
例如,我們定義Pair不總是存盤兩個型別一樣的物件,就可以使用型別<T, K>:
public class Pair<T, K> {
private T first;
private K last;
public Pair(T first, K last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public K getLast() {
return last;
}
}
使用的時候,需要指出兩種型別:
Pair<String, Integer> p = new Pair<>("test", 123);
Java標準庫的Map<K, V>就是使用兩種泛型型別的例子,它對Key使用一種型別,對Value使用另一種型別,
小結
撰寫泛型時,需要定義泛型型別<T>;
泛型可以同時定義多種型別,例如Map<K, V>,
3.2 泛型介面
介面也可以宣告泛型,
泛型介面語法形式:
public interface Content<T> {
T text();
}
泛型介面有兩種實作方式:
-
實作介面的子類明確宣告泛型型別
預先宣告繼承的具體型別的介面類,下面就是繼承的
Integer型別的介面類,public class IntContent implements Content<Integer> { private int text; public IntContent(int text) { this.text = text; } @Override public Integer text() { return text; } }因為子類并沒有泛型型別,所以正常使用就行,
InContent ic = new IntContent(10); -
實作介面的子類不明確宣告泛型型別
public class GenericsContent<T> implements Content<T> { private T text; public GenericsContent(T text) { this.text = text; } @Override public T text() { return text; } }此時子類也使用了泛型型別,就需要指定具體型別
Content<String> gc = new GenericsContent<>("ABC");
3.3 泛型方法
泛型方法是引入其自己的型別引數的方法,泛型方法可以是普通方法、靜態方法以及構造方法,
泛型方法語法形式如下:
public <T> T func(T obj) {}
注意:是否擁有泛型方法,與其所在的類是否是泛型沒有關系,
泛型方法的語法包括一個型別引數串列,在尖括號內,它出現在方法的回傳型別之前,對于靜態泛型方法,型別引數部分必須出現在方法的回傳型別之前,型別引數能被用來宣告回傳值型別,并且能作為泛型方法得到的實際型別引數的占位符,
使用泛型方法的時候,通常不必指明型別引數,因為編譯器會為我們找出具體的型別,這稱為型別引數推斷(type argument inference),型別推斷只對賦值操作有效,其他時候并不起作用,如果將一個泛型方法呼叫的結果作為引數,傳遞給另一個方法,這時編譯器并不會執行推斷,
編譯器會認為:呼叫泛型方法后,其回傳值被賦給一個 Object 型別的變數,
public class GenericsMethodDemo01 {
public static <T> void printClass(T obj) {
System.out.println(obj.getClass().toString());
}
public static void main(String[] args) {
printClass("abc");
printClass(10);
}
}
// Output:
// class java.lang.String
// class java.lang.Integer
泛型方法中也可以使用可變引數串列
public class GenericVarargsMethodDemo {
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<T>();
Collections.addAll(result, args);
return result;
}
public static void main(String[] args) {
List<String> ls = makeList("A");
System.out.println(ls);
ls = makeList("A", "B", "C");
System.out.println(ls);
}
}
// Output:
// [A]
// [A, B, C]
4. 泛型的特性
4.1 型別擦除(Type Erasure)
Java 語言引入泛型是為了在編譯時提供更嚴格的型別檢查,并支持泛型編程,不同于 C++ 的模板機制,Java 泛型是使用型別擦除來實作的,使用泛型時,任何具體的型別資訊都被擦除了,
那么,型別擦除做了什么呢?它做了以下作業:
- 把泛型中的所有型別引數替換為 Object,如果指定型別邊界,則使用型別邊界來替換,因此,生成的位元組碼僅包含普通的類,介面和方法,
- 擦除出現的型別宣告,即去掉
<>的內容,比如T get()方法宣告就變成了Object get();List<String>就變成了List,如有必要,插入型別轉換以保持型別安全, - 生成橋接方法以保留擴展泛型型別中的多型性,型別擦除確保不為引數化型別創建新類;因此,泛型不會產生運行時開銷,
Java 泛型的實作方式不太優雅,但這是因為泛型是在 JDK5 時引入的,為了兼容老代碼,必須在設計上做一定的折中,
簡單來說型別擦除是指,虛擬機對泛型其實一無所知,所有的作業都是編譯器做的,
例如,我們撰寫了一個泛型類Pair<T>,這是編譯器看到的代碼:
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
而虛擬機根本不知道泛型,這是虛擬機執行的代碼:
public class Pair {
private Object first;
private Object last;
public Pair(Object first, Object last) {
this.first = first;
this.last = last;
}
public Object getFirst() {
return first;
}
public Object getLast() {
return last;
}
}
因此,Java使用型別擦拭實作泛型,導致了:
- 編譯器把型別
<T>視為Object; - 編譯器根據
<T>實作安全的強制轉型,
因此,Java使用擦拭法實作泛型,導致了:
- 編譯器把型別
<T>視為Object; - 編譯器根據
<T>實作安全的強制轉型,
使用泛型的時候,我們撰寫的代碼也是編譯器看到的代碼:
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();
而虛擬機執行的代碼并沒有泛型:
Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();
所以,Java的泛型是由編譯器在編譯時實行的,編譯器內部永遠把所有型別T視為Object處理,但是,在需要轉型的時候,編譯器會根據T的型別自動為我們實行安全地強制轉型,
泛型的局限
了解了Java泛型的實作方式——型別擦除,我們就知道了Java泛型的局限:
局限一:<T>不能是基本型別,例如int,因為實際型別是Object,Object型別無法持有基本型別:
Pair<int> p = new Pair<>(1, 2); // compile error!
局限二:無法取得帶泛型的Class,觀察以下代碼:
public class test {
public static void main(String[] args) {
List<Object> list1 = new ArrayList<Object>();
List<String> list2 = new ArrayList<String>();
System.out.println(list1.getClass());
System.out.println(list2.getClass());
}
}
// Output:
// class java.util.ArrayList
// class java.util.ArrayList
因為T是Object,我們對ArrayList<Object>和ArrayList<String>型別獲取Class時,獲取到的是同一個Class,也就是ArrayList類的Class,
換句話說,所有泛型實體,無論T的型別是什么,getClass()回傳同一個Class實體,因為編譯后它們全部都是ArrayList<Object>,
局限三:無法判斷帶泛型的型別:
List<Integer> p = new ArrayList<>();
// Compile error:
if (p instanceof List<String>) {
}
原因和前面一樣,并不存在List<String>.class,而是只有唯一的List.class,
泛型和繼承
正是由于泛型時基于型別擦除實作的,所以,泛型型別無法向上轉型,
向上轉型是指用子類實體去初始化父類,這是面向物件中多型的重要表現,

Integer 繼承了 Object;ArrayList 繼承了 List;但是 List<Interger> 卻并非繼承了 List<Object>,
這是因為,泛型類并沒有自己獨有的 Class 類物件,比如:并不存在 List<Object>.class 或是 List<Interger>.class,Java 編譯器會將二者都視為 List.class,
4.2 上邊界
在使用泛型的時候,我們還可以為傳入的泛型型別實參進行上下邊界的限制,如:型別實參只準傳入某種型別的父類或某種型別的子類,
extend通配符
為泛型添加上邊界,即傳入的型別實參必須是指定型別的子型別
// 可以限制傳入方法的引數的型別
<? extends xxx>
// 也可以限制T的型別
<T extends XXX>
// 型別邊界可以設定多個,語法形式如下:
<T extends B1 & B2 & B3>
注意:extends 關鍵字后面的第一個型別引數可以是類或介面,其他型別引數只能是介面,
<? extends xxx>
舉個例子:
public class test {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);
}
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
通過使用<? extends Number>,我們可以傳入Number型別的子型別別的陣列,就可以執行數值型別的加法,
這種使用<? extends Number>的泛型定義稱之為上界通配符(Upper Bounds Wildcards),即把泛型型別T的上界限定在Number了,除了可以傳入Pair<Integer>型別,我們還可以傳入Pair<Double>型別,Pair<BigDecimal>型別等等,因為Double和BigDecimal都是Number的子類,
如果我們考察對Pair<? extends Number>型別呼叫getFirst()方法,實際的方法簽名變成了:
<? extends Number> getFirst();
接下來,我們再來考察一下Pair<T>的set方法:
public class test {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);
}
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
p.setFirst(new Integer(first.intValue() + 100));
p.setLast(new Integer(last.intValue() + 100));
return p.getFirst().intValue() + p.getFirst().intValue();
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
// 會得到一個編譯錯誤
// The method setFirst(capture#3-of ? extends Number) in the type Pair<capture#3-of ? extends Number> is not applicable for the arguments (int)Java(67108979)
編譯錯誤的原因在于,如果一開始我們傳入的p是Pair<Double>,顯然它滿足引數定義Pair<? extends Number>,然而,Pair<Double>的setFirst()顯然無法接受Integer型別,
這就是<? extends Number>通配符的一個重要限制:方法引數簽名setFirst(? extends Number)無法傳遞任何Number的子型別給setFirst(? extends Number),
這里唯一的例外是可以給方法引數傳入null:
p.setFirst(null); // ok, 但是后面會拋出NullPointerException
p.getFirst().intValue(); // NullPointerException
使用extends限定T型別
在定義泛型型別Pair<T>的時候,也可以使用extends通配符來限定T的型別:
public class Pair<T extends Number> { ... }
現在,我們只能定義:
Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Double> p3 = null;
因為Number、Integer和Double都符合<T extends Number>,
非Number型別將無法通過編譯:
Pair<String> p1 = null; // compile error!
Pair<Object> p2 = null; // compile error!
因為String、Object都不符合<T extends Number>,因為它們不是Number型別或Number的子類,
小結
使用類似<? extends Number>通配符作為方法引數時表示:
- 方法內部可以呼叫獲取
Number參考的方法,例如:Number n = obj.getFirst();; - 方法內部無法呼叫傳入
Number參考的方法(null除外),例如:obj.setFirst(Number n);,
即一句話總結:使用extends通配符表示可以讀,不能寫,
使用類似<T extends Number>定義泛型類時表示:
- 泛型型別限定為
Number以及Number的子類,
4.3 下邊界
super 下界通配符將未知型別限制為該型別的特定型別或超型別別,
和extends通配符相反,這次,我們希望接受Pair<Integer>型別,以及Pair<Number>、Pair<Object>,因為Number和Object是Integer的父類,setFirst(Number)和setFirst(Object)實際上允許接受Integer型別,
我們使用super通配符來改寫這個方法:
void set(Pair<? super Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}
注意到Pair<? super Integer>表示,方法引數接受所有泛型型別為Integer或Integer父類的Pair型別,
這里注意到我們無法使用Integer型別來接收getFirst()的回傳值,即下面的陳述句將無法通過編譯:
Integer x = p.getFirst();
因為如果傳入的實際型別是Pair<Number>,編譯器無法將Number型別轉型為Integer,
因此,使用<? super Integer>通配符表示:
- 允許呼叫
set(? super Integer)方法傳入Integer的參考; - 不允許呼叫
get()方法獲得Integer的參考,
唯一例外是可以獲取Object的參考:Object o = p.getFirst(),
換句話說,使用<? super Integer>通配符作為方法引數,表示方法內部代碼對于引數只能寫,不能讀,
對比extends和super通配符
我們再回顧一下extends通配符,作為方法引數,<? extends T>型別和<? super T>型別的區別在于:
<? extends T>允許呼叫讀方法T get()獲取T的參考,但不允許呼叫寫方法set(T)傳入T的參考(傳入null除外);<? super T>允許呼叫寫方法set(T)傳入T的參考,但不允許呼叫讀方法T get()獲取T的參考(獲取Object除外),
一個是允許讀不允許寫,另一個是允許寫不允許讀,
4.4 無限定通配符
我們已經討論了<? extends T>和<? super T>作為方法引數的作用,實際上,Java的泛型還允許使用無限定通配符(Unbounded Wildcard Type),即只定義一個?:
void sample(Pair<?> p) {
}
因為<?>通配符既沒有extends,也沒有super,因此:
- 不允許呼叫
set(T)方法并傳入參考(null除外); - 不允許呼叫
T get()方法并獲取T參考(只能獲取Object參考),
無界通配符有兩種應用場景:
- 可以使用 Object 類中提供的功能來實作的方法,
- 使用不依賴于型別引數的泛型類中的方法,
語法形式:<?>
public class GenericsUnboundedWildcardDemo {
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.print(elem + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
}
}
// Output:
// 1 2 3
// one two three
小結
使用類似<? super Integer>通配符作為方法引數時表示:
- 方法內部可以呼叫傳入
Integer參考的方法,例如:obj.setFirst(Integer n);; - 方法內部無法呼叫獲取
Integer參考的方法(Object除外),例如:Integer n = obj.getFirst();,
即使用super通配符表示只能寫不能讀,
無限定通配符<?>很少使用,可以用<T>替換,同時它是所有<T>型別的超類,
4.5 泛型命名
泛型一些約定俗成的命名(實際并無意義,但是建議對應著來命名泛型):
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
5. end
理解泛型之后可以方便我們更好的閱讀Java框架的原始碼,實際編程來說不一定會用到,但是可以用到泛型編程的地方,建議使用,可以簡化代碼,
6. 參考資料
- 廖雪峰Java
- 深入理解Java泛型
- Oracle Java檔案
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/281569.html
標籤:Java
下一篇:自定義注解,你會了嗎?
