概述
泛型是一種將類型參數化的動態機制,使用得到的話,可以從以下的方面提升的你的程序:
- 安全性:使用泛型可以使代碼更加安全可靠,因爲泛型提供了編譯時的類型檢查,使得編譯器能夠在編譯階段捕捉到類型錯誤。通過在編譯時檢查類型一致性,可以避免在運行時出現類型轉換錯誤和
ClassCastException
等異常。減少由於類型錯誤引發的bug。 - 複用和靈活性:泛型可以使用佔位符
<T>
定義抽象和通用的對象,你可以在使用的時候再來決定具體的類型是什麼,從而使得代碼更具通用性和可重用性。 - 簡化代碼,增強可讀性:可以減少類型轉換的需求,簡化代碼,可以使代碼更加清晰和易於理解。通過使用具有描述性的泛型類型參數,可以更準確地表達代碼的意圖,還可以避免使用原始類型或Object類型,從而提供更多的類型信息,使代碼更加具有表達力
這就是泛型的概念,是 Java 後期的重大變化之一。泛型實現了參數化類型,可以適用於多種類型。泛型爲 Java 的動態類型機制提供很好的補充,但是 Java 的泛型本質上是一種高級語法糖,也存在類型擦除導致的信息丟失等多種缺點,我們可以在本篇文章中深度探討和分析。
簡單的示例
泛型在 Java 的主要作用就是創建類型通用的集合類,我們創建一個容器類,然後通過三個示例來展示泛型的使用:
- 沒有使用泛型的情況
- 使用 Object 類型作爲容器對象
- 使用泛型作爲容器對象
示例1:沒有使用泛型的情況
public class IntList {
private int[] arr; // 只能存儲整數類型的數據
private int size;
public IntList() {
arr = new int[10];
size = 0;
}
public void add(int value) {
arr[size++] = value;
}
public int get(int index) {
return arr[index];
}
public int size() {
return size;
}
public static void main(String[] args) {
IntList list = new IntList();
list.add(1);
list.add(2);
list.add(3);
int value = list.get(1); // 需要顯式進行類型轉換
System.out.println(value); // 輸出: 2
}
}
在上述示例中,使用了一個明確的 int
類型存儲整數的列表類 IntList
,但是該類只能存儲整數類型的數據。如果想要存儲其他類型的數據,就需要編寫類似的類,導致類的複用度較低。
示例2:使用 Object 類型作爲持有對象的容器
public class ObjectList {
private Object[] arr;
private int size;
public ObjectList() {
arr = new Object[10];
size = 0;
}
public void add(Object value) {
arr[size++] = value;
}
public Object get(int index) {
return arr[index];
}
public int size() {
return size;
}
public static void main(String[] args) {
// 示例使用
ObjectList list = new ObjectList();
list.add(1);
list.add("Hello");
list.add(true);
int intValue = (int) list.get(0); // 需要顯式進行類型轉換
String stringValue = (String) list.get(1); // 需要顯式進行類型轉換
boolean boolValue = (boolean) list.get(2); // 需要顯式進行類型轉換
}
}
在上述示例中,使用了一個通用的列表類 ObjectList
,它使用了 Object 類型作爲持有對象的容器。當從列表中取出對象時,需要顯式進行類型轉換,而且不小心類型轉換錯誤程序就會拋出異常,這會帶來代碼的冗餘、安全和可讀性的降低。
示例3:使用泛型實現通用列表類
public class GenericList<T> {
private T[] arr;
private int size;
public GenericList() {
arr = (T[]) new Object[10]; // 創建泛型數組的方式
size = 0;
}
public void add(T value) {
arr[size++] = value;
}
public T get(int index) {
return arr[index];
}
public int size() {
return size;
}
public static void main(String[] args) {
// 存儲 Integer 類型的 List
GenericList<Integer> intList = new GenericList<>();
intList.add(1);
intList.add(2);
intList.add(3);
int value = intList.get(1); // 不需要進行類型轉換
System.out.println(value); // 輸出: 2
// 存儲 String 類型的 List
GenericList<String> stringList = new GenericList<>();
stringList.add("Hello");
stringList.add("World");
String str = stringList.get(0); // 不需要進行類型轉換
System.out.println(str); // 輸出: Hello
}
}
在上述示例中,使用了一個通用的列表類 GenericList
,通過使用泛型類型參數 T
,可以在創建對象時指定具體的類型。這樣就可以在存儲和取出數據時,不需要進行類型轉換,代碼更加通用、簡潔和類型安全。
通過上述三個示例,可以清楚地看到泛型在提高代碼複用度、簡化類型轉換和提供類型安全方面的作用。使用泛型可以使代碼更具通用性和可讀性,減少類型錯誤的發生,並且提高代碼的可維護性和可靠性。
組合類型:元組
在某些情況下需要組合多個不同類型的值的需求,而不希望爲每種組合創建專門的類或數據結構。這就需要用到元組(Tuple)。
元組(Tuple)是指將一組不同類型的值組合在一起的數據結構。它可以包含多個元素,每個元素可以是不同的類型。元組提供了一種簡單的方式來表示和操作多個值,而不需要創建專門的類或數據結構。
下面是一個使用元組的簡單示例:
class Tuple<T1, T2> {
private T1 first;
private T2 second;
public Tuple(T1 first, T2 second) {
this.first = first;
this.second = second;
}
public T1 getFirst() {
return first;
}
public T2 getSecond() {
return second;
}
}
public class TupleExample {
public static void main(String[] args) {
Tuple<String, Integer> person = new Tuple<>("Tom", 18);
System.out.println("Name: " + person.getFirst());
System.out.println("Age: " + person.getSecond());
Tuple<String, Double> product = new Tuple<>("Apple", 2.99);
System.out.println("Product: " + product.getFirst());
System.out.println("Price: " + product.getSecond());
}
}
在上述示例中,定義了一個簡單的元組類 Tuple
,它有兩個類型參數 T1
和 T2
,以及相應的 first
和 second
字段。在 main
方法中,使用元組存儲了不同類型的值,並通過調用 getFirst
和 getSecond
方法獲取其中的值。
你也們可以利用繼承機制實現長度更長的元組:
public class Tuple2<T1, T2, T3> extends Tuple<T1, T2>{
private T3 t3;
public Tuple2(T1 first, T2 second, T3 t3) {
super(first, second);
this.t3 = t3;
}
}
繼續擴展:
public class Tuple3<T1, T2, T3, T4> extends Tuple2<T1, T2, T3> {
private T4 t4;
public Tuple3(T1 first, T2 second, T3 t3) {
super(first, second, t3);
}
}
如上所述,元組提供了一種簡潔而靈活的方式來組合和操作多個值,適用於需要臨時存儲和傳遞多個相關值的場景。但需要注意的是,元組並不具備類型安全的特性,因爲它允許不同類型的值的組合。
泛型接口
將泛型應用在接口,是在接口設計時常常需要考慮的,泛型可以提供接口的複用性和安全性。
下面是一個示例,展示泛型在接口上的使用:
// 定義一個泛型接口
interface Container<T> {
void add(T item);
T get(int index);
}
// 實現泛型接口
public class ListContainer<T> implements Container<T> {
private List<T> list;
public ListContainer() {
this.list = new ArrayList<>();
}
@Override
public void add(T item) {
list.add(item);
}
@Override
public T get(int index) {
return list.get(index);
}
public static void main(String[] args) {
// 示例使用
Container<String> container = new ListContainer<>();
container.add("Apple");
container.add("Banana");
container.add("Orange");
String fruit1 = container.get(0);
String fruit2 = container.get(1);
String fruit3 = container.get(2);
System.out.println(fruit1); // 輸出: Apple
System.out.println(fruit2); // 輸出: Banana
System.out.println(fruit3); // 輸出: Orange
}
}
在上述示例中,我們定義了一個泛型接口 Container<T>
,它包含了兩個方法:add
用於添加元素,get
用於獲取指定位置的元素。然後,我們通過實現泛型接口的類 ListContainer<T>
,實現了具體的容器類,這裏使用了 ArrayList
來存儲元素。在示例使用部分,我們創建了一個 ListContainer<String>
的實例,即容器中的元素類型爲 String
。我們可以使用 add
方法添加元素,使用 get
方法獲取指定位置的元素。
通過在接口上使用泛型,我們可以定義出具有不同類型的容器類,提高代碼的可複用性和類型安全性。泛型接口允許我們在編譯時進行類型檢查,並提供了更好的類型約束和編碼規範。
泛型方法
泛型方法是一種在方法聲明中使用泛型類型參數的特殊方法。它允許在方法中使用參數或返回值的類型參數化,從而實現方法在不同類型上的重用和類型安全性。
泛型方法具有以下特點:
- 泛型方法可以在方法簽名中聲明一個或多個類型參數,使用尖括號
<T>
來表示 - 類型參數可以在方法內部用作方法參數類型、方法返回值類型、局部變量類型
方法泛型化要比將整個類泛型化更清晰易懂,所以在日常使用中請儘可能的使用泛型方法。
以下展示泛型方法的示例:
public class GenericMethodExample {
// 帶返回值的泛型方法
public static <T> T getFirstElement(T[] array) {
if (array != null && array.length > 0) {
return array[0];
}
return null;
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strings = {"Hello", "World"};
System.out.println("First element in intArray: " + getFirstElement(intArray));
System.out.println("First element in strings: " + getFirstElement(strings));
}
}
可以看到通過泛型方法,讓 getFirstElement()
更具備通用性,無需爲每個不同的類型編寫單獨的獲取方法。
再來看一個帶可變參數的泛型方法:
public class GenericMethodExample {
// 帶返回值的泛型方法,接受變長參數列表
public static <T> List<T> createList(T... elements) {
List<T> list = new ArrayList<>();
for (T element : elements) {
list.add(element);
}
return list;
}
public static void main(String[] args) {
List<String> stringList = createList("Apple", "Banana", "Orange");
List<Integer> intList = createList(1, 2, 3, 4, 5);
System.out.println("String List: " + stringList); // 輸出: String List: [Apple, Banana, Orange]
System.out.println("Integer List: " + intList); // 輸出: Integer List: [1, 2, 3, 4, 5]
}
}
泛型信息的擦除
當你深入瞭解泛型的時候,你會發現它沒有你想象的那麼安全,它只是編譯過程的語法糖,因爲泛型並不是 Java 語言的特性,而是後期加入的功能特性,屬於編譯器層面的功能,而且由於要兼容舊版本的緣故,所以 Java 無法實現真正的泛型。
泛型擦除是指在編譯時期,泛型類型參數會被擦除或替換爲它們的上界或限定類型。這是由於Java中的泛型是通過類型擦除來實現的,編譯器在生成字節碼時會將泛型信息擦除,以確保與舊版本的Java代碼兼容。
以下是一個代碼示例,展示了泛型擦除的效果:
public class GenericErasureExample {
public static void main(String[] args) {
// 定義一個 String 類型的集合
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");
// 定義一個 Integer 類型的集合
List<Integer> intList = new ArrayList<>();
intList.add(10);
intList.add(20);
// 你無法通過反射獲取泛型的類型參數,因爲泛型信息會在編譯時被擦除
System.out.println(stringList.getClass()); // 輸出: class java.util.ArrayList
System.out.println(intList.getClass()); // 輸出: class java.util.ArrayList
// 原本不同的類型,輸出結果卻相等
System.out.println(stringList.getClass() == intList.getClass()); // 輸出: true
// 使用原始類型List,可以繞過編譯器的類型檢查,但會導致類型轉換錯誤
List rawList = stringList;
rawList.add(30); // 添加了一個整數,導致類型轉換錯誤
// 從rawList中取出元素時,會導致類型轉換錯誤
String str = stringList.get(0); // 類型轉換錯誤,嘗試將整數轉換爲字符串
}
}
通過上述代碼,我們演示類的泛型信息是怎麼被擦除的,並且演示由於泛型信息的擦除所導致的安全和轉換錯誤。這也是爲什麼在泛型中無法直接使用基本類型(如 int、boolean 等),而只能使用其包裝類的原因之一。
爲什麼要擦除 ?
Java 在設計泛型時選擇了擦除泛型信息的方式,主要是爲了保持與現有的非泛型代碼的兼容性,並且提供平滑的過渡。泛型是在 Java 5 中引入的,泛型類型參數被替換爲它們的上界或限定類型,這樣可以確保舊版本的 Java 虛擬機仍然可以加載和執行這些類。
儘管泛型擦除帶來了一些限制,如無法在運行時獲取泛型類型參數的具體類型等,但通過類型通配符、反射和其他技術,仍然可以在一定程度上處理泛型類型的信息。擦除泛型信息是 Java 泛型的設計妥協,爲了在保持向後兼容性和類型安全性的同時,提供了一種靈活且高效的泛型機制。
擦除會引發哪些問題 ?
設計的本質就是權衡,Java 設計者爲了兼容性不得已選擇的擦除泛型信息的方式,雖然完成了對歷史版本的兼容,但付出的代價也是顯著的,擦除泛型信息對於 Java 代碼可能引發以下問題:
- 無法在運行時獲取泛型類型參數的具體類型:由於擦除泛型信息,無法在運行時獲取泛型類型參數的具體類型。(如上所示)
- 類型轉換和類型安全性:擦除泛型信息可能導致類型轉換錯誤和類型安全性問題。(如上所示)
- 無法創建具體的泛型類型實例:由於擦除泛型信息,無法直接創建具體的泛型類型的實例。例如,無法使用
new T()
的方式 - 與原始類型的混淆:擦除泛型信息可能導致與原始類型的混淆。並且泛型無法使用基本數據類型,只能依賴自動拆箱和裝箱機制
Class 信息丟失
這是一段因爲擦除導致沒有任何意義的代碼:
public class ArrayMaker<T> {
private Class<T> kind;
public ArrayMaker(Class<T> kind) {
this.kind = kind;
}
@SuppressWarnings("unchecked")
T[] create(int size) {
return (T[]) java.lang.reflect.Array.newInstance(kind, size);
}
public static void main(String[] args) {
ArrayMaker<String> stringMaker = new ArrayMaker<>(String.class);
String[] stringArray = stringMaker.create(10);
System.out.println(Arrays.toString(stringArray));
}
}
輸出結果:
[null, null, null, null, null, null, null, null, null, null]
泛型邊界
泛型邊界(bounds)是指對泛型類型參數進行限定,以指定其可以接受的類型範圍。泛型邊界可以通過指定上界(extends)或下界(super)來實現。泛型邊界允許我們在泛型代碼中對類型參數進行限制,它們有助於確保在使用泛型類或方法時,只能使用符合條件的類型。
泛型邊界的使用場景包括:
- 類型限定:當我們希望泛型類型參數只能是特定類型或特定類型的子類時,可以使用泛型邊界。
- 調用特定類型的方法:通過泛型邊界,我們可以在泛型類或方法中調用特定類型的方法,訪問其特定的屬性。
- 擴展泛型類型的功能:通過泛型邊界,我們可以限制泛型類型參數的範圍,以擴展泛型類型的功能。
上界(extends)
用於設定泛型類型參數的上界,即,類型參數必須是特定類型或該類型的子類,示例
public class MyExtendsClass<T extends Number> {
public static void main(String[] args) {
MyExtendsClass<Integer> integerMyExtendsClass = new MyExtendsClass<>(); // 可以,因爲 Integer 是 Number 的子類
MyExtendsClass<Double> doubleMyExtendsClass = new MyExtendsClass<>(); // 可以,因爲 Double 是 Number 的子類
// MyClass<String> myStringClass = new MyClass<>(); // 編譯錯誤,因爲 String 不是 Number 的子類
}
}
在泛型方法中,extends
關鍵字在泛型的讀取模式(Producer Extends,PE)中常用到。比如,一個方法返回的是 List<? extends Number>
,你可以確定這個 List 中的元素都是 Number 或其子類,可以安全地讀取爲 Number,但不能向其中添加任何元素(除了 null),示例:
public void doSomething(List<? extends Number> list) {
Number number = list.get(0); // 可以讀取
// list.add(3); // 編譯錯誤,不能寫入
}
下界(super)
用於設定類型參數的下界,即,類型參數必須是特定類型或該類型的子類。示例:
public void addToMyList(List<? super Number> list) {
Object o1 = new Object();
list.add(3); // 可以,因爲 Integer 是 Number 的子類
list.add(3.14); // 可以,因爲 Double 是 Number 的子類
// list.add("String"); // 編譯錯誤,因爲 String 不是 Number 的子類
}
在泛型方法中,super
關鍵字在泛型的寫入模式(Consumer Super,CS)中常用到。比如,一個方法參數的類型是 List<? super Integer>
,你可以向這個 List 中添加 Integer 或其子類的對象,但不能從中讀取具體類型的元素(只能讀取爲 Object),示例:
public void doSomething(List<? super Integer> list) {
list.add(3); // 類型符合,可以寫入
// Integer number = list.get(0); // 編譯錯誤,不能讀取具體類型
Object o = list.get(0); // 可以讀取 Object
}
熟練和靈活的運用 PECS
原則(Producer Extends, Consumer Super)我們也可以輕鬆實現 Collection 裏面的通用類型集合的 Copy 方法,示例:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T t : src) {
dest.add(t);
}
}
public static void main(String[] args) {
List<Object> objectList = new ArrayList<>();
List<Integer> integerList = Arrays.asList(1, 2, 3);
copy(objectList, integerList);
System.out.println(objectList); // [1, 2, 3]
}
記住,無論是 extends
還是 super
,它們都只是對編譯時類型的約束,實際的運行時類型信息在類型擦除過程中已經被刪除了。
無界(?)
無界通配符 <?>
是一種特殊的類型參數,可以接受任何類型。它常被用在泛型代碼中,當代碼可以工作在不同類型的對象上,並且你可能不知道或者不關心具體的類型是什麼。你可以使用它,示例:
public static void printList(List<?> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
public static void main(String[] args) {
List<Integer> li = Arrays.asList(1, 2, 3, 4, 5);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
}
那麼,問題來了。
那我爲什麼不直接使用 Object ? 而要使用 <?> 無界通配符 ?
它們好像都可以容納任何類型的對象。但實際上,List<Object>
和 List<?>
在類型安全性上有很大的不同。
例如,List<Object>
是一個具體類型,你可以向 List<Object>
中添加任何類型的對象。但是,List<Object>
不能接受其他類型的 List
,例如 List<String>
或 List<Integer>
。
相比之下,List<?>
是一個通配符類型,表示可以是任何類型的 List
。你不能向 List<?>
中添加任何元素(除了 null
),因爲你並不知道具體的類型,但你可以接受任何類型的 List
,包括 List<Object>
、List<String>
、List<Integer>
等等。
示例代碼:
public static void printListObject(List<Object> list) {
for (Object obj : list)
System.out.println(obj);
}
public static void printListWildcard(List<?> list) {
for (Object obj : list)
System.out.println(obj);
}
public static void main(String[] args) {
List<String> stringList = Arrays.asList("Hello", "World");
printListWildcard(stringList); // 有效
// printListObject(stringList); // 編譯錯誤
}
因此,當你需要編寫能接受任何類型 List
的代碼時,應該使用 List<?>
而不是 List<Object>
目前存在的問題
在 Java 引入泛型之前,已經有大量的 Java 代碼在生產環境中運行。爲了讓這些代碼在新版本的 Java 中仍然可以運行,Java 的設計者選擇了一種叫做 “類型擦除” 的方式來實現泛型,這樣就不需要改變 JVM
和已存在的非泛型代碼。
但這樣的設計解決了向後兼容的問題,但也引入很多問題需要大多數的 Java 程序員來承擔,例如:
- 類型擦除:這是Java泛型中最主要的限制。這意味着在運行時你不能查詢一個泛型對象的真實類型
- 不能實例化泛型類型的類:你不能使用
new T()
,new E()
這樣的語法來創建泛型類型的對象,還是因爲類型被擦除 - 不能使用基本類型作爲類型參數:因爲是編譯器的語法糖,所以只能使用包裝類型如
Integer
,Double
等作爲泛型類型參數 - 通配符的使用可能會導致代碼複雜:如
? extends T
和? super T
在理解和應用時需要小心 - 因爲類型擦除,泛型類不能繼承自或者實現同一泛型接口的不同參數化形式
儘管 Java 的泛型有這些缺點,但是它仍然是一個強大和有用的工具,可以幫助我們編寫更安全、更易讀的代碼。
總結
在泛型出現之前,集合類庫並不能在編譯時期檢查插入集合的對象類型是否正確,只能在運行時期進行檢查,這種情況下一旦出錯就會在運行時拋出一個類型轉換異常。這種運行時錯誤的出現對於開發者而言,既不友好,也難以定位問題。泛型的引入,讓開發者可以在編譯時期檢查類型,增加了代碼的安全性。並且可以編寫更爲通用的代碼,提高了代碼的複用性。
然而,泛型設計並非完美,主要的問題出在類型擦除上,爲了保持與老版本的兼容性所做的妥協。因爲類型擦除,Java 的泛型喪失了一些強大的功能,例如運行時類型查詢,創建泛型數組等。
儘管 Java 泛型存在一些限制,但是 Java 語言仍然在不斷的發展中,例如在 Java 10 中,引入了局部變量類型推斷的特性,使得在使用泛型時可以更加方便。對於未來,Java 可能會在泛型方面進行更深入的改進。