1. 為什么需要泛型
2. 泛型類型
2.1. 泛型類
2.2. 泛型接口
3. 泛型方法
4. 類型擦除
5. 泛型和繼承
6. 類型邊界
7. 類型通配符
7.1. 上界通配符
7.2. 下界通配符
7.3. 無(wú)界通配符
7.4. 通配符和向上轉(zhuǎn)型
8. 泛型的約束
9. 泛型最佳實(shí)踐
9.1. 泛型命名
9.2. 使用泛型的建議
10. 小結(jié)
11. 參考資料
#1. 為什么需要泛型
JDK5 引入了泛型機(jī)制。
為什么需要泛型呢?回答這個(gè)問(wèn)題前,先讓我們來(lái)看一個(gè)示例。
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)
示例說(shuō)明:
在上面的示例中,List 容器沒(méi)有指定存儲(chǔ)數(shù)據(jù)類型,這種情況下,可以向 List 添加任意類型數(shù)據(jù),編譯器不會(huì)做類型檢查,而是默默的將所有數(shù)據(jù)都轉(zhuǎn)為 Object。
假設(shè),最初我們希望向 List 存儲(chǔ)的是整形數(shù)據(jù),假設(shè),某個(gè)家伙不小心存入了其他數(shù)據(jù)類型。當(dāng)你試圖從容器中取整形數(shù)據(jù)時(shí),由于 List 當(dāng)成 Object 類型來(lái)存儲(chǔ),你不得不使用類型強(qiáng)制轉(zhuǎn)換。在運(yùn)行時(shí),才會(huì)發(fā)現(xiàn) List 中數(shù)據(jù)不存儲(chǔ)一致的問(wèn)題,這就為程序運(yùn)行帶來(lái)了很大的風(fēng)險(xiǎn)(無(wú)形傷害最為致命)。
而泛型的出現(xiàn),解決了類型安全問(wèn)題。
泛型具有以下優(yōu)點(diǎn):
編譯時(shí)的強(qiáng)類型檢查
泛型要求在聲明時(shí)指定實(shí)際數(shù)據(jù)類型,Java 編譯器在編譯時(shí)會(huì)對(duì)泛型代碼做強(qiáng)類型檢查,并在代碼違反類型安全時(shí)發(fā)出告警。早發(fā)現(xiàn),早治理,把隱患扼殺于搖籃,在編譯時(shí)發(fā)現(xiàn)并修復(fù)錯(cuò)誤所付出的代價(jià)遠(yuǎn)比在運(yùn)行時(shí)小。
避免了類型轉(zhuǎn)換
未使用泛型:
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
泛型編程可以實(shí)現(xiàn)通用算法
通過(guò)使用泛型,程序員可以實(shí)現(xiàn)通用算法,這些算法可以處理不同類型的集合,可以自定義,并且類型安全且易于閱讀。
#2. 泛型類型
泛型類型是被參數(shù)化的類或接口。
#2.1. 泛型類
泛型類的語(yǔ)法形式:
class name<T1, T2, ..., Tn> { /* ... */ }
泛型類的聲明和非泛型類的聲明類似,除了在類名后面添加了類型參數(shù)聲明部分。由尖括號(hào)(<>)分隔的類型參數(shù)部分跟在類名后面。它指定類型參數(shù)(也稱為類型變量)T1,T2,...和 Tn。
一般將泛型中的類名稱為原型,而將 <> 指定的參數(shù)稱為類型參數(shù)。
未應(yīng)用泛型的類
在泛型出現(xiàn)之前,如果一個(gè)類想持有一個(gè)可以為任意類型的數(shù)據(jù),只能使用 Object 做類型轉(zhuǎn)換。示例如下:
public class Info {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
單類型參數(shù)的泛型類
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
在上面的例子中,在初始化一個(gè)泛型類時(shí),使用 <> 指定了內(nèi)部具體類型,在編譯時(shí)就會(huì)根據(jù)這個(gè)類型做強(qiáng)類型檢查。
實(shí)際上,不使用 <> 指定內(nèi)部具體類型,語(yǔ)法上也是支持的(不推薦這么做),如下所示:
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());
}
示例說(shuō)明:
上面的例子,不會(huì)產(chǎn)生編譯錯(cuò)誤,也能正常運(yùn)行。但這樣的調(diào)用就失去泛型類型的優(yōu)勢(shì)。
多個(gè)類型參數(shù)的泛型類
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. 泛型接口
接口也可以聲明泛型。
泛型接口語(yǔ)法形式:
public interface Content<T> {
? ? T text();
}
泛型接口有兩種實(shí)現(xiàn)方式:
實(shí)現(xiàn)接口的子類明確聲明泛型類型
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
實(shí)現(xiàn)接口的子類不明確聲明泛型類型
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. 泛型方法
泛型方法是引入其自己的類型參數(shù)的方法。泛型方法可以是普通方法、靜態(tài)方法以及構(gòu)造方法。
泛型方法語(yǔ)法形式如下:
public <T> T func(T obj) {}
是否擁有泛型方法,與其所在的類是否是泛型沒(méi)有關(guān)系。
泛型方法的語(yǔ)法包括一個(gè)類型參數(shù)列表,在尖括號(hào)內(nèi),它出現(xiàn)在方法的返回類型之前。對(duì)于靜態(tài)泛型方法,類型參數(shù)部分必須出現(xiàn)在方法的返回類型之前。類型參數(shù)能被用來(lái)聲明返回值類型,并且能作為泛型方法得到的實(shí)際類型參數(shù)的占位符。
使用泛型方法的時(shí)候,通常不必指明類型參數(shù),因?yàn)榫幾g器會(huì)為我們找出具體的類型。這稱為類型參數(shù)推斷(type argument inference)。類型推斷只對(duì)賦值操作有效,其他時(shí)候并不起作用。如果將一個(gè)返回類型為T的泛型方法調(diào)用的結(jié)果作為參數(shù),傳遞給另一個(gè)方法,這時(shí)編譯器并不會(huì)執(zhí)行推斷。編譯器會(huì)認(rèn)為:調(diào)用泛型方法后,其返回值被賦給一個(gè) 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
泛型方法中也可以使用可變參數(shù)列表
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 語(yǔ)言引入泛型是為了在編譯時(shí)提供更嚴(yán)格的類型檢查,并支持泛型編程。不同于 C++ 的模板機(jī)制,Java 泛型是使用類型擦除來(lái)實(shí)現(xiàn)的,使用泛型時(shí),任何具體的類型信息都被擦除了。
那么,類型擦除做了什么呢?它做了以下工作:
把泛型中的所有類型參數(shù)替換為 Object,如果指定類型邊界,則使用類型邊界來(lái)替換。因此,生成的字節(jié)碼僅包含普通的類,接口和方法。
擦除出現(xiàn)的類型聲明,即去掉 <> 的內(nèi)容。比如 T get() 方法聲明就變成了 Object get() ;List<String> 就變成了 List。如有必要,插入類型轉(zhuǎn)換以保持類型安全。
生成橋接方法以保留擴(kuò)展泛型類型中的多態(tài)性。類型擦除確保不為參數(shù)化類型創(chuàng)建新類;因此,泛型不會(huì)產(chǎn)生運(yùn)行時(shí)開銷。
讓我們來(lái)看一個(gè)示例:
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
示例說(shuō)明:
上面的例子中,雖然指定了不同的類型參數(shù),但是 list1 和 list2 的類信息卻是一樣的。
這是因?yàn)椋菏褂梅盒蜁r(shí),任何具體的類型信息都被擦除了。這意味著:ArrayList<Object> 和 ArrayList<String> 在運(yùn)行時(shí),JVM 將它們視為同一類型。
Java 泛型的實(shí)現(xiàn)方式不太優(yōu)雅,但這是因?yàn)榉盒褪窃?JDK5 時(shí)引入的,為了兼容老代碼,必須在設(shè)計(jì)上做一定的折中。
#5. 泛型和繼承
泛型不能用于顯式地引用運(yùn)行時(shí)類型的操作之中,例如:轉(zhuǎn)型、instanceof 操作和 new 表達(dá)式。因?yàn)樗嘘P(guān)于參數(shù)的類型信息都丟失了。當(dāng)你在編寫泛型代碼時(shí),必須時(shí)刻提醒自己,你只是看起來(lái)好像擁有有關(guān)參數(shù)的類型信息而已。
正是由于泛型時(shí)基于類型擦除實(shí)現(xiàn)的,所以,泛型類型無(wú)法向上轉(zhuǎn)型。
向上轉(zhuǎn)型是指用子類實(shí)例去初始化父類,這是面向?qū)ο笾卸鄳B(tài)的重要表現(xiàn)。
img
Integer 繼承了 Object;ArrayList 繼承了 List;但是 List<Interger> 卻并非繼承了 List<Object>。
這是因?yàn)椋盒皖惒](méi)有自己獨(dú)有的 Class 類對(duì)象。比如:并不存在 List<Object>.class 或是 List<Interger>.class,Java 編譯器會(huì)將二者都視為 List.class。
List<Integer> list = new ArrayList<>();
List<Object> list2 = list; // Erorr
#6. 類型邊界
有時(shí)您可能希望限制可在參數(shù)化類型中用作類型參數(shù)的類型。類型邊界可以對(duì)泛型的類型參數(shù)設(shè)置限制條件。例如,對(duì)數(shù)字進(jìn)行操作的方法可能只想接受 Number 或其子類的實(shí)例。
要聲明有界類型參數(shù),請(qǐng)列出類型參數(shù)的名稱,然后是 extends 關(guān)鍵字,后跟其限制類或接口。
類型邊界的語(yǔ)法形式如下:
<T extends XXX>
示例:
public class GenericsExtendsDemo01 {
? ? static <T extends Comparable<T>> T max(T x, T y, T z) {
? ? ? ? T max = x; // 假設(shè)x是初始最大值
? ? ? ? if (y.compareTo(max) > 0) {
? ? ? ? ? ? max = y; //y 更大
? ? ? ? }
? ? ? ? if (z.compareTo(max) > 0) {
? ? ? ? ? ? max = z; // 現(xiàn)在 z 更大
? ? ? ? }
? ? ? ? return max; // 返回最大對(duì)象
? ? }
? ? 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
示例說(shuō)明:
上面的示例聲明了一個(gè)泛型方法,類型參數(shù) T extends Comparable<T> 表明傳入方法中的類型必須實(shí)現(xiàn)了 Comparable 接口。
類型邊界可以設(shè)置多個(gè),語(yǔ)法形式如下:
<T extends B1 & B2 & B3>
注意:extends 關(guān)鍵字后面的第一個(gè)類型參數(shù)可以是類或接口,其他類型參數(shù)只能是接口。
示例:
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> { /* ... */ } // 編譯報(bào)錯(cuò)
? ? 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<>(); // 編譯報(bào)錯(cuò)
? ? }
}
#7. 類型通配符
類型通配符一般是使用 ? 代替具體的類型參數(shù)。例如 List<?> 在邏輯上是 List<String> ,List<Integer> 等所有 List<具體類型實(shí)參> 的父類。
#7.1. 上界通配符
可以使用**上界通配符**來(lái)縮小類型參數(shù)的類型范圍。
它的語(yǔ)法形式為:<? 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. 下界通配符
**下界通配符**將未知類型限制為該類型的特定類型或超類類型。
注意:上界通配符和下界通配符不能同時(shí)使用。
它的語(yǔ)法形式為:<? 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. 無(wú)界通配符
無(wú)界通配符有兩種應(yīng)用場(chǎng)景:
可以使用 Object 類中提供的功能來(lái)實(shí)現(xiàn)的方法。
使用不依賴于類型參數(shù)的泛型類中的方法。
語(yǔ)法形式:<?>
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. 通配符和向上轉(zhuǎn)型
前面,我們提到:泛型不能向上轉(zhuǎn)型。但是,我們可以通過(guò)使用通配符來(lái)向上轉(zhuǎn)型。
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
? ? }
}
擴(kuò)展閱讀:Oracle 泛型文檔(opens new window)
#8. 泛型的約束
泛型類型的類型參數(shù)不能是值類型(opens new window)
Pair<int, char> p = new Pair<>(8, 'a');? // 編譯錯(cuò)誤
不能創(chuàng)建類型參數(shù)的實(shí)例(opens new window)
public static <E> void append(List<E> list) {
? ? E elem = new E();? // 編譯錯(cuò)誤
? ? list.add(elem);
}
不能聲明類型為類型參數(shù)的靜態(tài)成員(opens new window)
public class MobileDevice<T> {
? ? private static T os; // error
? ? // ...
}
類型參數(shù)不能使用類型轉(zhuǎn)換或 instanceof(opens new window)
public static <E> void rtti(List<E> list) {
? ? if (list instanceof ArrayList<Integer>) {? // 編譯錯(cuò)誤
? ? ? ? // ...
? ? }
}
List<Integer> li = new ArrayList<>();
List<Number>? ln = (List<Number>) li;? // 編譯錯(cuò)誤
不能創(chuàng)建類型參數(shù)的數(shù)組(opens new window)
List<Integer>[] arrayOfLists = new List<Integer>[2];? // 編譯錯(cuò)誤
不能創(chuàng)建、catch 或 throw 參數(shù)化類型對(duì)象(opens new window)
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ }? ? // 編譯錯(cuò)誤
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // 編譯錯(cuò)誤
public static <T extends Exception, J> void execute(List<J> jobs) {
? ? try {
? ? ? ? for (J job : jobs)
? ? ? ? ? ? // ...
? ? } catch (T e) {? // compile-time error
? ? ? ? // ...
? ? }
}
僅僅是泛型類相同,而類型參數(shù)不同的方法不能重載(opens new window)
public class Example {
? ? public void print(Set<String> strSet) { }
? ? public void print(Set<Integer> intSet) { } // 編譯錯(cuò)誤
}
#9. 泛型最佳實(shí)踐
#9.1. 泛型命名
泛型一些約定俗成的命名:
E - Element
K - Key
N - Number
T - Type
V - Value
S,U,V etc. - 2nd, 3rd, 4th types
#9.2. 使用泛型的建議
消除類型檢查告警
List 優(yōu)先于數(shù)組
優(yōu)先考慮使用泛型來(lái)提高代碼通用性
優(yōu)先考慮泛型方法來(lái)限定泛型的范圍
利用有限制通配符來(lái)提升 API 的靈活性
優(yōu)先考慮類型安全的異構(gòu)容器
#10. 小結(jié)
img
#11. 參考資料
Java 編程思想(opens new window)
Java 核心技術(shù)(卷 1)(opens new window)
Effective java(opens new window)
Oracle 泛型文檔(opens new window)
Java 泛型詳解(opens new window)
幫助我們改善此頁(yè)面! (opens new window)
上次更新: 5 days ago