📓 本文已歸檔到:「javacore」
🔁 本文中的示例代碼已歸檔到:「javacore」
1. 為什么需要泛型
JDK5 引入了泛型機制。
為什么需要泛型呢?回答這個問題前,先讓我們來看一個示例。
public class NoGenericsDemo {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add("abc");
list.add(18);
list.add(new double[] {1.0, 2.0});
Object obj1 = list.get(0);
Object obj2 = list.get(1);
Object obj3 = list.get(2);
System.out.println("obj1 = [" + obj1 + "]");
System.out.println("obj2 = [" + obj2 + "]");
System.out.println("obj3 = [" + obj3 + "]");
int num1 = (int)list.get(0);
int num2 = (int)list.get(1);
int num3 = (int)list.get(2);
System.out.println("num1 = [" + num1 + "]");
System.out.println("num2 = [" + num2 + "]");
System.out.println("num3 = [" + num3 + "]");
}
}
// Output:
// obj1 = [abc]
// obj2 = [18]
// obj3 = [[D@47089e5f]
// Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
// at io.github.dunwu.javacore.generics.NoGenericsDemo.main(NoGenericsDemo.java:23)
示例說明:
在上面的示例中,
List
容器沒有指定存儲數據類型,這種情況下,可以向List
添加任意類型數據,編譯器不會做類型檢查,而是默默的將所有數據都轉為Object
。假設,最初我們希望向
List
存儲的是整形數據,假設,某個家伙不小心存入了其他數據類型。當你試圖從容器中取整形數據時,由於List
當成Object
類型來存儲,你不得不使用類型強制轉換。在運行時,才會發現List
中數據不存儲一致的問題,這就為程序運行帶來了很大的風險(無形傷害最為致命)。
而泛型的出現,解決了類型安全問題。
泛型具有以下優點:
- 編譯時的強類型檢查
泛型要求在聲明時指定實際數據類型,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
- 泛型編程可以實現通用算法
通過使用泛型,程序員可以實現通用算法,這些算法可以處理不同類型的集合,可以自定義,並且類型安全且易於閱讀。
2. 泛型類型
泛型類型
是被參數化的類或接口。
2.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 = value;
}
}
- 單類型參數的泛型類
public class Info<T> {
private T value;
public Info() { }
public Info(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
@Override
public String toString() {
return "Info{" + "value=" + value + '}';
}
}
public class GenericsClassDemo01 {
public static void main(String[] args) {
Info<Integer> info = new Info<>();
info.setValue(10);
System.out.println(info.getValue());
Info<String> info2 = new Info<>();
info2.setValue("xyz");
System.out.println(info2.getValue());
}
}
// Output:
// 10
// xyz
在上面的例子中,在初始化一個泛型類時,使用 <>
指定了內部具體類型,在編譯時就會根據這個類型做強類型檢查。
實際上,不使用 <>
指定內部具體類型,語法上也是支持的(不推薦這么做),如下所示:
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());
}
示例說明:
上面的例子,不會產生編譯錯誤,也能正常運行。但這樣的調用就失去泛型類型的優勢。
- 多個類型參數的泛型類
public class MyMap<K,V> {
private K key;
private V value;
public MyMap(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return "MyMap{" + "key=" + key + ", value=" + value + '}';
}
}
public class GenericsClassDemo02 {
public static void main(String[] args) {
MyMap<Integer, String> map = new MyMap<>(1, "one");
System.out.println(map);
}
}
// Output:
// MyMap{key=1, value=one}
- 泛型類的類型嵌套
public class GenericsClassDemo03 {
public static void main(String[] args) {
Info<String> info = new Info("Hello");
MyMap<Integer, Info<String>> map = new MyMap<>(1, info);
System.out.println(map);
}
}
// Output:
// MyMap{key=1, value=Info{value=Hello}}
2.2. 泛型接口
接口也可以聲明泛型。
泛型接口語法形式:
public interface Content<T> {
T text();
}
泛型接口有兩種實現方式:
- 實現接口的子類明確聲明泛型類型
public class GenericsInterfaceDemo01 implements Content<Integer> {
private int text;
public GenericsInterfaceDemo01(int text) {
this.text = text;
}
@Override
public Integer text() { return text; }
public static void main(String[] args) {
GenericsInterfaceDemo01 demo = new GenericsInterfaceDemo01(10);
System.out.print(demo.text());
}
}
// Output:
// 10
- 實現接口的子類不明確聲明泛型類型
public class GenericsInterfaceDemo02<T> implements Content<T> {
private T text;
public GenericsInterfaceDemo02(T text) {
this.text = text;
}
@Override
public T text() { return text; }
public static void main(String[] args) {
GenericsInterfaceDemo02<String> gen = new GenericsInterfaceDemo02<>("ABC");
System.out.print(gen.text());
}
}
// Output:
// ABC
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. 類型擦除
Java 語言引入泛型是為了在編譯時提供更嚴格的類型檢查,並支持泛型編程。不同於 C++ 的模板機制,Java 泛型是使用類型擦除來實現的,使用泛型時,任何具體的類型信息都被擦除了。
那么,類型擦除做了什么呢?它做了以下工作:
- 把泛型中的所有類型參數替換為 Object,如果指定類型邊界,則使用類型邊界來替換。因此,生成的字節碼僅包含普通的類,接口和方法。
- 擦除出現的類型聲明,即去掉
<>
的內容。比如T get()
方法聲明就變成了Object get()
;List<String>
就變成了List
。如有必要,插入類型轉換以保持類型安全。 - 生成橋接方法以保留擴展泛型類型中的多態性。類型擦除確保不為參數化類型創建新類;因此,泛型不會產生運行時開銷。
讓我們來看一個示例:
public class GenericsErasureTypeDemo {
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
示例說明:
上面的例子中,雖然指定了不同的類型參數,但是 list1 和 list2 的類信息卻是一樣的。
這是因為:使用泛型時,任何具體的類型信息都被擦除了。這意味着:
ArrayList<Object>
和ArrayList<String>
在運行時,JVM 將它們視為同一類型。
Java 泛型的實現方式不太優雅,但這是因為泛型是在 JDK5 時引入的,為了兼容老代碼,必須在設計上做一定的折中。
5. 泛型和繼承
泛型不能用於顯式地引用運行時類型的操作之中,例如:轉型、instanceof 操作和 new 表達式。因為所有關於參數的類型信息都丟失了。當你在編寫泛型代碼時,必須時刻提醒自己,你只是看起來好像擁有有關參數的類型信息而已。
正是由於泛型時基於類型擦除實現的,所以,泛型類型無法向上轉型。
向上轉型是指用子類實例去初始化父類,這是面向對象中多態的重要表現。
Integer
繼承了 Object
;ArrayList
繼承了 List
;但是 List<Interger>
卻並非繼承了 List<Object>
。
這是因為,泛型類並沒有自己獨有的 Class
類對象。比如:並不存在 List<Object>.class
或是 List<Interger>.class
,Java 編譯器會將二者都視為 List.class
。
List<Integer> list = new ArrayList<>();
List<Object> list2 = list; // Erorr
6. 類型邊界
有時您可能希望限制可在參數化類型中用作類型參數的類型。類型邊界
可以對泛型的類型參數設置限制條件。例如,對數字進行操作的方法可能只想接受 Number
或其子類的實例。
要聲明有界類型參數,請列出類型參數的名稱,然后是 extends
關鍵字,后跟其限制類或接口。
類型邊界的語法形式如下:
<T extends XXX>
示例:
public class GenericsExtendsDemo01 {
static <T extends Comparable<T>> T max(T x, T y, T z) {
T max = x; // 假設x是初始最大值
if (y.compareTo(max) > 0) {
max = y; //y 更大
}
if (z.compareTo(max) > 0) {
max = z; // 現在 z 更大
}
return max; // 返回最大對象
}
public static void main(String[] args) {
System.out.println(max(3, 4, 5));
System.out.println(max(6.6, 8.8, 7.7));
System.out.println(max("pear", "apple", "orange"));
}
}
// Output:
// 5
// 8.8
// pear
示例說明:
上面的示例聲明了一個泛型方法,類型參數
T extends Comparable<T>
表明傳入方法中的類型必須實現了 Comparable 接口。
類型邊界可以設置多個,語法形式如下:
<T extends B1 & B2 & B3>
🔔 注意:extends 關鍵字后面的第一個類型參數可以是類或接口,其他類型參數只能是接口。
示例:
public class GenericsExtendsDemo02 {
static class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
static class D1 <T extends A & B & C> { /* ... */ }
static class D2 <T extends B & A & C> { /* ... */ } // 編譯報錯
static class E extends A implements B, C { /* ... */ }
public static void main(String[] args) {
D1<E> demo1 = new D1<>();
System.out.println(demo1.getClass().toString());
D1<String> demo2 = new D1<>(); // 編譯報錯
}
}
7. 類型通配符
類型通配符
一般是使用 ?
代替具體的類型參數。例如 List<?>
在邏輯上是 List<String>
,List<Integer>
等所有 List<具體類型實參>
的父類。
7.1. 上界通配符
可以使用上界通配符
來縮小類型參數的類型范圍。
它的語法形式為:<? extends Number>
public class GenericsUpperBoundedWildcardDemo {
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list) {
s += n.doubleValue();
}
return s;
}
public static void main(String[] args) {
List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));
}
}
// Output:
// sum = 6.0
7.2. 下界通配符
下界通配符
將未知類型限制為該類型的特定類型或超類類型。
🔔 注意:上界通配符和下界通配符不能同時使用。
它的語法形式為:<? super Number>
public class GenericsLowerBoundedWildcardDemo {
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
addNumbers(list);
System.out.println(Arrays.deepToString(list.toArray()));
}
}
// Output:
// [1, 2, 3, 4, 5]
7.3. 無界通配符
無界通配符有兩種應用場景:
- 可以使用 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
7.4. 通配符和向上轉型
前面,我們提到:泛型不能向上轉型。但是,我們可以通過使用通配符來向上轉型。
public class GenericsWildcardDemo {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Error
List<? extends Integer> intList2 = new ArrayList<>();
List<? extends Number> numList2 = intList2; // OK
}
}
擴展閱讀:Oracle 泛型文檔
8. 泛型的約束
Pair<int, char> p = new Pair<>(8, 'a'); // 編譯錯誤
public static <E> void append(List<E> list) {
E elem = new E(); // 編譯錯誤
list.add(elem);
}
public class MobileDevice<T> {
private static T os; // error
// ...
}
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // 編譯錯誤
// ...
}
}
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // 編譯錯誤
List<Integer>[] arrayOfLists = new List<Integer>[2]; // 編譯錯誤
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ } // 編譯錯誤
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // 編譯錯誤
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // compile-time error
// ...
}
}
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { } // 編譯錯誤
}
9. 泛型最佳實踐
9.1. 泛型命名
泛型一些約定俗成的命名:
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
9.2. 使用泛型的建議
- 消除類型檢查告警
- List 優先於數組
- 優先考慮使用泛型來提高代碼通用性
- 優先考慮泛型方法來限定泛型的范圍
- 利用有限制通配符來提升 API 的靈活性
- 優先考慮類型安全的異構容器