泛型是一種將型別引數化的動態機制,使用得到的話,可以從以下的方面提升的你的程式:
ClassCastException
等異常。減少由於型別錯誤引發的bug。<T>
定義抽象和通用的物件,你可以在使用的時候再來決定具體的型別是什麼,從而使得程式碼更具通用性和可重用性。這就是泛型的概念,是 Java 後期的重大變化之一。泛型實現了引數化型別,可以適用於多種型別。泛型為 Java 的動態型別機制提供很好的補充,但是 Java 的泛型本質上是一種高階語法糖,也存在型別擦除導致的資訊丟失等多種缺點,我們可以在本篇文章中深度探討和分析。
泛型在 Java 的主要作用就是建立型別通用的集合類,我們建立一個容器類,然後通過三個範例來展示泛型的使用:
範例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()
的方式這是一段因為擦除導致沒有任何意義的程式碼:
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)來實現。泛型邊界允許我們在泛型程式碼中對型別引數進行限制,它們有助於確保在使用泛型類或方法時,只能使用符合條件的型別。
泛型邊界的使用場景包括:
用於設定泛型型別引數的上界,即,型別引數必須是特定型別或該型別的子類,範例
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); // 編譯錯誤,不能寫入
}
用於設定型別引數的下界,即,型別引數必須是特定型別或該型別的子類。範例:
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 程式設計師來承擔,例如:
new T()
,new E()
這樣的語法來建立泛型型別的物件,還是因為型別被擦除Integer
,Double
等作為泛型型別引數? extends T
和 ? super T
在理解和應用時需要小心儘管 Java 的泛型有這些缺點,但是它仍然是一個強大和有用的工具,可以幫助我們編寫更安全、更易讀的程式碼。
在泛型出現之前,集合類庫並不能在編譯時期檢查插入集合的物件型別是否正確,只能在執行時期進行檢查,這種情況下一旦出錯就會在執行時丟擲一個型別轉換異常。這種執行時錯誤的出現對於開發者而言,既不友好,也難以定位問題。泛型的引入,讓開發者可以在編譯時期檢查型別,增加了程式碼的安全性。並且可以編寫更為通用的程式碼,提高了程式碼的複用性。
然而,泛型設計並非完美,主要的問題出在型別擦除上,為了保持與老版本的相容性所做的妥協。因為型別擦除,Java 的泛型喪失了一些強大的功能,例如執行時型別查詢,建立泛型陣列等。
儘管 Java 泛型存在一些限制,但是 Java 語言仍然在不斷的發展中,例如在 Java 10 中,引入了區域性變數型別推斷的特性,使得在使用泛型時可以更加方便。對於未來,Java 可能會在泛型方面進行更深入的改進。