天天看點

一文搞懂 java 泛型,也有可能搞不懂,畢竟講得太全面了

一、引言

Java泛型的背景和作用

Java泛型是Java程式設計語言中的一個特性,引入泛型的目的是為了增強代碼的類型安全性和重用性。在沒有泛型之前,Java中的集合類(如ArrayList、HashMap等)隻能存儲Object類型的對象,這使得在使用集合時需要進行強制類型轉換,容易出現類型錯誤。

泛型的背景:在Java 5版本之前,Java的類型是靜态的,在編譯時确定,并且在運作時擦除類型資訊。這種情況下,編譯器無法對集合的元素類型進行驗證,是以可能會導緻運作時類型錯誤。為了解決這個問題,Java引入了泛型機制。

泛型的作用:

  1. 類型安全:泛型使得在編譯時就能夠檢測到類型錯誤,避免了在運作時出現類型轉換異常。
  2. 代碼重用:通過使用泛型,可以編寫通用的代碼,适用于多種不同類型的資料,提高了代碼的靈活性和複用性。
  3. API設計:泛型使得API的設計更加清晰和一緻,可以定義泛型接口、類和方法,提供更加靈活的參數類型和傳回值類型。
  4. 增強集合類:泛型使集合類更加類型安全和簡潔,不再需要顯式進行類型轉換。

在使用泛型時,可以定義類、接口、方法和變量等具有泛型參數,并通過使用具體的類型實參來指定泛型的具體類型。例如,可以定義一個泛型類ArrayList,其中的T表示類型參數,可以在建立ArrayList對象時指定具體的類型,如ArrayList表示存儲整數的ArrayList。

泛型的基本概念和好處

基本概念:

  • 類型參數:在泛型中,使用類型參數來表示一個未知的類型。類型參數可以用任意辨別符來表示,通常使用單個大寫字母作為慣例,如T、E、K等。
  • 實際類型參數:在使用泛型時,需要指定具體的類型給類型參數,這些具體的類型被稱為實際類型參數。例如,在建立一個泛型類的執行個體時,可以将Integer作為實際類型參數傳遞給類型參數T,進而建立一個存儲整數的對象。

好處:

  • 類型安全性:泛型提供了更嚴格的類型檢查,在編譯時就能夠發現類型錯誤。通過指定具體的類型參數,可以在編譯期間捕獲不相容的類型操作,避免了在運作時出現類型轉換錯誤和相關的異常。
  • 代碼重用性:泛型使得我們可以編寫通用的代碼邏輯,可以在多種類型上進行操作,而無需為每種類型都編寫相應的代碼。這樣可以減少代碼的重複,提高代碼的可維護性和可讀性。
  • 高效性:泛型在編譯時進行類型擦除,将泛型類型轉換為它們的邊界類型(通常是Object類型)。這意味着在運作時并不需要保留泛型的類型資訊,進而避免了額外的開銷,提高了程式的性能。

二、泛型類型和方法

泛型類

定義泛型類的文法和使用方法

在許多程式設計語言中,如Java和C#,泛型類是一種特殊類型的類,它可以接受不同類型的參數進行執行個體化。泛型類提供了代碼重用和類型安全性的好處,因為它們可以與各種資料類型一起使用,而無需為每種類型編寫單獨的類。

下面是定義泛型類的文法:

java複制代碼public class GenericClass<T> {
    // 類成員和方法定義
}
           

在上面的示例中,GenericClass 是一個泛型類的名稱,<T> 表示類型參數,T 可以替換為任何合法的辨別符,用于表示實際類型。

要使用泛型類,可以通過指定實際類型來執行個體化它。例如,假設我們有一個名為 MyClass 的泛型類,我們可以按以下方式使用它:

java複制代碼GenericClass<Integer> myInstance = new GenericClass<Integer>();
           

在上面的示例中,我們使用整數類型執行個體化了 GenericClass 泛型類。這樣,myInstance 将是一個隻能存儲整數類型的對象。

在執行個體化泛型類後,可以使用該類中定義的成員和方法,就像普通的類一樣。不同之處在于,泛型類中的成員或方法可以使用類型參數 T,并且會根據實際類型進行類型檢查和處理。

如果需要在泛型類中使用多個類型參數,可以通過逗号分隔它們:

java複制代碼public class MultiGenericClass<T, U> {
    // 類成員和方法定義
}
           

上面的示例定義了一個具有兩個類型參數的泛型類 MultiGenericClass。

總結起來,定義泛型類的文法是在類名後面使用 <T> 或其他類型參數,并在類中使用這些類型參數。然後,可以通過指定實際類型來執行個體化泛型類,并可以使用泛型類中定義的成員和方法。

類型參數的限定和通配符的使用

類型參數的限定:

類型參數的限定允許我們對泛型類或方法的類型參數進行限制,以確定隻能使用特定類型或滿足特定條件的類型。

在 Java 中,可以使用關鍵字 extends 來限定類型參數。有兩種類型參數的限定方式:

  1. 單一限定(Single Bound):指定類型參數必須是某個類或接口的子類。
  2. java複制代碼
  3. public class MyClass<T extends SomeClass> { // 類成員和方法定義 }
  4. 在上面的示例中,類型參數 T 必須是 SomeClass 類的子類或實作了 SomeClass 接口的類型。
  5. 多重限定(Multiple Bounds):指定類型參數必須是多個類或接口的子類,并且隻能有一個類(如果有)。
  6. java複制代碼
  7. public class MyClass<T extends ClassA & InterfaceB & InterfaceC> { // 類成員和方法定義 }
  8. 在上面的示例中,類型參數 T 必須是 ClassA 類的子類,并且還要實作 InterfaceB 和 InterfaceC 接口。

通過類型參數的限定,可以在泛型類或方法中對類型進行更精确的控制和限制,以提高代碼的類型安全性和靈活性。

通配符的使用:

通配符是一種特殊的類型參數,用于在泛型類或方法中表示未知類型或不确定的類型。有兩種通配符可以使用:

  1. 無限定通配符(Unbounded Wildcard):使用問号 ? 表示,表示可以比對任何類型。
  2. java複制代碼
  3. public void myMethod(List<?> myList) { // 方法實作 }
  4. 在上面的示例中,myMethod 方法接受一個類型為 List 的參數,但是該清單的元素類型是未知的,可以是任何類型。
  5. 有限定通配符(Bounded Wildcard):使用 extends 和具體類或接口來限定通配符所能比對的類型範圍。
  6. java複制代碼
  7. public void myMethod(List<? extends SomeClass> myList) { // 方法實作 }
  8. 在上面的示例中,myMethod 方法接受一個類型為 List 的參數,但是該清單的元素類型必須是 SomeClass 類或其子類。

通過使用通配符,可以編寫更通用的泛型代碼,允許處理各種類型的參數。它提供了更大的靈活性,尤其是當你不關心具體類型時或需要對多個類型進行操作時。

需要注意的是,在使用通配符時,不能對帶有通配符的泛型對象進行添加元素的操作,因為無法确定通配符表示的具體類型。但是可以進行讀取元素的操作。如果需要同時支援添加和讀取操作,可以使用有限定通配符來解決這個問題。

執行個體化泛型類和類型推斷

在Java中,泛型類是能夠對類型進行參數化的類。通過使用泛型,我們可以編寫更加通用和可複用的代碼,同時提高類型安全性。在執行個體化泛型類時,我們需要指定具體的類型參數。

以下是執行個體化泛型類的一般文法:

java複制代碼ClassName<DataType> objectName = new ClassName<>();
           

在上面的文法中,ClassName 是泛型類的名稱,DataType 是實際類型參數的占位符。通過将适當的類型替換為 DataType,我們可以建立一個特定類型的對象。例如,如果有一個泛型類 Box<T>,其中 T 是泛型類型參數,我們可以執行個體化它如下:

java複制代碼Box<Integer> integerBox = new Box<>();
           

在這個例子中,我們将泛型類型參數 T 替換為 Integer,然後建立了一個 Box 類型的整數對象。

另一方面,類型推斷是指編譯器根據上下文資訊自動推斷出泛型類型參數的過程。在某些情況下,我們可以省略泛型類型參數,并讓編譯器自動推斷它們。這樣可以簡化代碼,使其更具可讀性。

以下是一個示例,展示了類型推斷的用法:

java複制代碼Box<Integer> integerBox = new Box<>();  // 類型推斷

List<String> stringList = new ArrayList<>();  // 類型推斷
           

在這些示例中,我們沒有顯式地指定泛型類型參數,而是使用了 <> 運算符。編譯器會根據變量的聲明和初始化值來推斷出正确的類型參數。

需要注意的是,類型推斷隻在Java 7及更高版本中才可用。在舊版本的Java中,必須顯式指定泛型類型參數。

泛型方法

定義泛型方法的文法和使用方法

泛型方法是指具有泛型類型參數的方法。通過使用泛型方法,我們可以在方法級别上使用類型參數,使方法能夠處理不同類型的資料,并提高代碼的靈活性和複用性。

以下是定義泛型方法的一般文法:

java複制代碼public <T> ReturnType methodName(T parameter) {
    // 方法體
}
           

在上面的文法中,<T> 表示類型參數的占位符,可以是任意辨別符(通常使用單個大寫字母)。T 可以在方法參數、傳回類型和方法體内部使用。ReturnType 是方法的傳回類型,可以是具體類型或者也可以是泛型類型。

下面是一個簡單的示例,展示了如何定義和使用泛型方法:

java複制代碼public <T> void printArray(T[] array) {
    for (T element : array) {
        System.out.println(element);
    }
}

// 調用泛型方法
Integer[] intArray = { 1, 2, 3, 4, 5 };
printArray(intArray);

String[] stringArray = { "Hello", "World" };
printArray(stringArray);
           

在上面的示例中,我們定義了一個名為 printArray 的泛型方法。它接受一個泛型數組作為參數,并列印出數組中的每個元素。我們可以使用這個方法列印不同類型的數組,例如整數數組和字元串數組。

需要注意的是,泛型方法可以獨立于泛型類存在,并且可以在任何類中定義和使用。它們提供了更大的靈活性,使我們能夠對特定的方法進行泛型化,而不僅僅是整個類。

調用泛型方法和類型推斷

在調用泛型方法時,我們需要注意幾個關鍵點:

  1. 顯式指定類型參數:如果泛型方法的類型參數沒有被編譯器自動推斷出來,我們需要顯式地指定類型參數。可以在方法名前使用尖括号(<>)并提供具體的類型參數。
  2. java複制代碼
  3. // 顯式指定類型參數為String String result = myGenericMethod.<String>genericMethod(argument);
  4. 自動類型推斷:Java編譯器在某些情況下能夠自動推斷泛型方法的類型參數,使代碼更簡潔易讀。可以省略顯式指定類型參數。
  5. java複制代碼
  6. // 自動類型推斷,根據參數類型推斷類型參數為Integer Integer result = myGenericMethod.genericMethod(argument);
  7. 編譯器通過方法參數的類型和上下文資訊來推斷類型參數。這種類型推斷對于簡化代碼和提高可讀性非常有用。
  8. 通配符類型參數:在某些情況下,我們可能希望泛型方法能夠接受不特定類型的參數。這時可以使用通配符作為類型參數。
  9. 無限制通配符(Unbounded wildcard):使用問号(?)表示,可以接受任意類型的參數。
  10. java複制代碼
  11. // 泛型方法接受任意類型的參數 void myGenericMethod(List<?> list) { // 方法體 }
  12. 有限制通配符(Bounded wildcard):使用 extends 關鍵字指定上界或者使用 super 關鍵字指定下界,限制了泛型方法接受的參數類型範圍。
  13. java複制代碼
  14. // 泛型方法接受 Number 及其子類的參數 void myGenericMethod(List<? extends Number> list) { // 方法體 } // 泛型方法接受 Integer 及其父類的參數 void myGenericMethod(List<? super Integer> list) { // 方法體 }

需要注意的是,調用泛型方法時,編譯器會根據傳遞的參數類型和上下文進行類型檢查。如果類型不比對,将産生編譯錯誤。

三、泛型接口和通配符

泛型接口

定義泛型接口的文法和使用方法

泛型接口是具有泛型類型參數的接口。通過使用泛型接口,我們可以在接口級别上使用類型參數,使得實作類能夠處理不同類型的資料,并提高代碼的靈活性和複用性。

以下是定義泛型接口的一般文法:

java複制代碼public interface InterfaceName<T> {
    // 接口方法和常量聲明
}
           

在上面的文法中,<T> 表示類型參數的占位符,可以是任意辨別符(通常使用單個大寫字母)。T 可以在接口方法、常量和内部類中使用。

下面是一個簡單的示例,展示了如何定義和使用泛型接口:

java複制代碼public interface Box<T> {
    void add(T item);
    T get();
}

// 實作泛型接口
public class IntegerBox implements Box<Integer> {
    private Integer item;

    public void add(Integer item) {
        this.item = item;
    }

    public Integer get() {
        return item;
    }
}

// 使用泛型接口
Box<Integer> box = new IntegerBox();
box.add(10);
Integer value = box.get();
           

在上面的示例中,我們定義了一個名為 Box 的泛型接口。它包含了一個 add 方法和一個 get 方法,分别用于添加和擷取泛型類型的資料。然後,我們實作了這個泛型接口的一個具體類 IntegerBox,并在其中指定了具體的類型參數為 Integer。

最後,我們使用泛型接口建立了一個 Box<Integer> 類型的對象,通過 add 方法添加整數值,并通過 get 方法擷取整數值。

需要注意的是,實作泛型接口時可以選擇具體地指定類型參數,也可以繼續使用泛型。

實作泛型接口的方式

  1. 具體類型參數實作:在實作類中顯式指定具體的類型參數。這将使實作類隻能處理特定類型的資料。
  2. java複制代碼
  3. public class IntegerBox implements Box<Integer> { private Integer item; public void add(Integer item) { this.item = item; } public Integer get() { return item; } }
  4. 在上面的示例中,IntegerBox 類實作了泛型接口 Box<Integer>,并明确指定了類型參數為 Integer。是以,IntegerBox 類隻能處理整數類型的資料。
  5. 保留泛型類型參數:在實作類中繼續使用泛型類型參數。這将使實作類具有與泛型接口相同的類型參數,進而保持靈活性。
  6. java複制代碼
  7. public class GenericBox<T> implements Box<T> { private T item; public void add(T item) { this.item = item; } public T get() { return item; } }
  8. 在上面的示例中,GenericBox<T> 類實作了泛型接口 Box<T>,并保留了類型參數 T。這意味着 GenericBox 類可以處理任意類型的資料,具有更大的靈活性。

使用以上兩種方式中的一種,您可以根據需要選擇實作泛型接口的方式。具體取決于實作類在處理資料時需要限定特定類型還是保持靈活性。

另外,無論使用哪種方式來實作泛型接口,都需要確定實作類中的方法簽名與泛型接口中定義的方法完全比對。這包括方法名稱、參數清單和傳回類型。

通配符

上界通配符和下界通配符的概念

上界通配符(Upper Bounded Wildcard)

上界通配符用于限制泛型類型參數必須是指定類型或指定類型的子類。使用 extends 關鍵字指定上界。

文法:

java複制代碼<? extends Type>
           

例如,假設我們有一個泛型方法 printList,它接受一個清單,并列印清單中的元素。但我們希望該方法隻能接受 Number 類型或其子類的清單,可以使用上界通配符來實作:

java複制代碼public static void printList(List<? extends Number> list) {
    for (Number element : list) {
        System.out.println(element);
    }
}

// 調用示例
List<Integer> integerList = Arrays.asList(1, 2, 3);
printList(integerList); // 可以正常調用

List<String> stringList = Arrays.asList("Hello", "World");
printList(stringList); // 編譯錯誤,String 不是 Number 的子類
           

在上面的示例中,printList 方法使用 <? extends Number> 定義了一個上界通配符,表示方法接受一個 Number 類型或其子類的清單。是以,我們可以傳遞一個 Integer 類型的清單作為參數,但不能傳遞一個 String 類型的清單。

下界通配符(Lower Bounded Wildcard)

下界通配符用于限制泛型類型參數必須是指定類型或指定類型的父類。使用 super 關鍵字指定下界。

文法:

java複制代碼<? super Type>
           

例如,假設我們有一個泛型方法 addToList,它接受一個清單和一個要添加到清單中的元素。但我們希望該方法隻能接受 Object 類型或其父類的元素,可以使用下界通配符來實作:

java複制代碼public static void addToList(List<? super Object> list, Object element) {
    list.add(element);
}

// 調用示例
List<Object> objectList = new ArrayList<>();
addToList(objectList, "Hello");
addToList(objectList, 42);

List<String> stringList = new ArrayList<>();
addToList(stringList, "World"); // 編譯錯誤,String 不是 Object 的父類
           

在上面的示例中,addToList 方法使用 <? super Object> 定義了一個下界通配符,表示方法接受一個 Object 類型或其父類的清單,并且可以向清單中添加任意類型的元素。是以,我們可以将字元串和整數添加到 objectList 中,但不能将字元串添加到 stringList 中。

需要注意的是,上界通配符和下界通配符主要用于靈活地處理泛型類型參數,以便在泛型代碼中處理不同類型的資料。它們提供了更大的靈活性和複用性。

在泛型方法和泛型接口中使用通配符的場景

泛型方法中使用通配符的場景:

  1. 讀取操作:當方法隻需要從泛型參數中擷取值時,可以使用上界通配符 ? extends T,以表示該方法适用于任何 T 類型或其子類。
  2. java複制代碼
  3. public static <T> void printList(List<? extends T> list) { for (T element : list) { System.out.println(element); } } // 調用示例 List<Integer> integerList = Arrays.asList(1, 2, 3); printList(integerList); // 可以正常調用 List<String> stringList = Arrays.asList("Hello", "World"); printList(stringList); // 可以正常調用
  4. 寫入操作:當方法需要向泛型參數中寫入值時,可以使用下界通配符 ? super T,以表示該方法适用于任何 T 類型或其父類。
  5. java複制代碼
  6. public static <T> void addToList(List<? super T> list, T element) { list.add(element); } // 調用示例 List<Object> objectList = new ArrayList<>(); addToList(objectList, "Hello"); addToList(objectList, 42); List<Number> numberList = new ArrayList<>(); addToList(numberList, 3.14); addToList(numberList, 123);

泛型接口中使用通配符的場景:

  1. 定義靈活的容器:當定義一個容器類時,希望該容器可以存儲任意類型的資料,可以使用無限制通配符 <?>。
  2. java複制代碼
  3. public interface Container<E> { void add(E element); E get(); } // 實作示例 public class AnyContainer implements Container<?> { private Object element; public void add(Object element) { this.element = element; } public Object get() { return element; } }
  4. 限制類型範圍:當希望泛型接口隻能處理特定範圍内的類型時,可以使用上界或下界通配符。
  5. java複制代碼
  6. public interface Box<T extends Number> { void addItem(T item); T getItem(); } // 實作示例 public class NumberBox<T extends Number> implements Box<T> { private T item; public void addItem(T item) { this.item = item; } public T getItem() { return item; } } public class IntegerBox implements Box<Integer> { private Integer item; public void addItem(Integer item) { this.item = item; } public Integer getItem() { return item; } }

上述場景中,使用通配符的目的是提供更大的靈活性和複用性。通配符允許我們在泛型方法和泛型接口中處理多種類型的資料,而不需要與具體類型綁定。這樣可以使代碼更通用、可擴充,并且适用于更廣泛的場景。

四、泛型和集合架構

泛型集合架構的詳細介紹

泛型集合架構是Java中提供的一組用于存儲和操作資料的容器類,它們支援泛型類型參數。泛型集合架構在 JDK 中的 java.util 包下提供了豐富的實作,包括清單(List)、集合(Set)、映射(Map)等。

核心接口:

  1. List 接口:表示一個有序的可重複集合。允許按照索引通路元素,并可以包含重複元素。常見的實作類有 ArrayList、LinkedList 和 Vector。
  2. Set 接口:表示一個不允許重複元素的無序集合。保證元素的唯一性。常見的實作類有 HashSet、TreeSet 和 LinkedHashSet。
  3. Queue 接口:表示一個先進先出(FIFO)的隊列。常見的實作類有 LinkedList 和 PriorityQueue。
  4. Map 接口:表示一個鍵值對的映射表。每個鍵都是唯一的,可以使用鍵來擷取相關聯的值。常見的實作類有 HashMap、TreeMap 和 LinkedHashMap。

泛型的優勢:

泛型集合架構的主要優勢是提供了類型安全和編譯時類型檢查的功能。通過指定泛型類型參數,我們可以在編譯時捕獲許多類型錯誤,并避免在運作時出現類型轉換異常。泛型還提供了更好的代碼可讀性和可維護性,因為它們明确地指定了容器中存儲的元素類型。

示例用法:

以下是一些常見的泛型集合架構的示例用法:

java複制代碼// 建立一個泛型清單,并添加元素
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");

// 使用疊代器周遊清單
for (String element : stringList) {
    System.out.println(element);
}

// 建立一個泛型集合,并添加元素
Set<Integer> integerSet = new HashSet<>();
integerSet.add(1);
integerSet.add(2);
integerSet.add(3);

// 判斷集合是否包含特定元素
boolean containsTwo = integerSet.contains(2);
System.out.println(containsTwo);  // 輸出: true

// 建立一個鍵值對映射表,并添加元素
Map<String, Integer> stringToIntegerMap = new HashMap<>();
stringToIntegerMap.put("One", 1);
stringToIntegerMap.put("Two", 2);
stringToIntegerMap.put("Three", 3);

// 根據鍵擷取值
int value = stringToIntegerMap.get("Two");
System.out.println(value);  // 輸出: 2
           

通過使用泛型集合架構,我們可以輕松地建立和操作不同類型的集合,并且在編譯時獲得類型安全和檢查的好處。

五、類型擦除和橋方法

類型擦除的原理和影響

泛型類型擦除(Type Erasure)是Java中泛型的實作方式之一。它是在編譯期間将泛型類型轉換為非泛型類型的一種機制。在泛型類型擦除中,泛型類型參數被擦除為它們的上界或 Object 類型,并且類型檢查主要發生在編譯時而不是運作時。

泛型類型擦除的原理:

  1. 類型擦除:在編譯過程中,所有泛型類型參數都被替換為它們的上界或 Object 類型。例如,List<String> 在編譯後會變成 List<Object>。
  2. 類型擦除後的轉換:由于類型擦除,原始的泛型類型資訊在運作時不可用。是以,在使用泛型類型時,會進行必要的轉換來確定類型安全性。
  3. 向上轉型:如果泛型類型參數是一個子類,那麼它會被轉換為其上界類型。例如,List<String> 被轉換為 List<Object>。
  4. 向下轉型:如果我們需要從泛型類型中擷取具體的類型參數,我們需要進行類型轉換。但這可能導緻運作時類型異常(ClassCastException)。

泛型類型擦除的影響:

  1. 可相容性:泛型類型擦除確定了與原始非泛型代碼的相容性。這意味着可以将使用泛型類型的代碼與不使用泛型的舊代碼進行互動。
  2. 無法獲得具體類型參數:由于類型擦除,無法在運作時擷取泛型類型參數的詳細資訊。例如,無法在運作時判斷一個 List 對象是 List<String> 還是 List<Integer>。
  3. 類型安全性:類型擦除導緻泛型在運作時失去了類型檢查。編譯器隻能在編譯時進行類型檢查,如果存在類型不比對的情況,可能在運作時出現 ClassCastException 異常。
  4. 限制反射操作:通過反射機制,可以繞過泛型類型擦除的限制,在運作時擷取泛型類型的資訊。但是,反射的使用複雜且性能較低,不推薦頻繁使用。

示例影響:

以下示例說明了泛型類型擦除的影響:

java複制代碼// 定義一個泛型類
public class GenericClass<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

// 使用泛型類
GenericClass<String> stringGeneric = new GenericClass<>();
stringGeneric.setValue("Hello");
String value = stringGeneric.getValue();

// 編譯後的泛型類型擦除
GenericClass stringGeneric = new GenericClass();
stringGeneric.setValue("Hello");
String value = (String) stringGeneric.getValue();  // 需要進行類型轉換

// 運作時類型異常示例
GenericClass<String> stringGeneric = new GenericClass<>();
GenericClass<Integer> integerGeneric = new GenericClass<>();

System.out.println(stringGeneric.getClass() == integerGeneric.getClass());  // 輸出: true

stringGeneric.setValue("Hello");

try {
    Integer value = integerGeneric.getValue();  // 運作時抛出 ClassCastException 異常
} catch (ClassCastException e) {
    System.out.println("ClassCastException: " + e.getMessage());
}
           

橋方法的概念和作用

泛型橋方法(Generic Bridge Method)是Java編譯器為了保持泛型類型的安全性而自動生成的方法。它的作用是在繼承或實作帶有泛型類型參數的類或接口時,確定類型安全性和相容性。

概念:

當一個類或接口定義了帶有泛型類型參數的方法,并且該類或接口被子類或實作類繼承或實作時,由于泛型類型擦除的原因,編譯器需要生成額外的橋方法來確定類型安全性。這些橋方法具有相同的方法簽名,但使用原始類型作為參數和傳回值類型,以保持與繼承層次結構中的其他非泛型方法的相容性。

作用:

  1. 類型安全:泛型橋方法的主要作用是保持類型安全性。通過添加橋方法,可以在運作時防止對不相容的類型進行通路。這樣可以避免在編譯期間無法檢測到的類型錯誤。
  2. 維護繼承關系:泛型橋方法還用于維護泛型類或接口之間的繼承關系。它們確定子類或實作類能夠正确地覆寫父類或接口的泛型方法,并使用正确的類型參數。

示例:

考慮以下示例:

java複制代碼public class MyList<T> {
    public void add(T element) {
        // 添加元素的邏輯
    }
}

// 子類繼承泛型類,并覆寫泛型方法
public class StringList extends MyList<String> {
    @Override
    public void add(String element) {
        // 添加元素的邏輯
    }
}
           

在這個示例中,由于Java的泛型類型擦除機制,編譯器會生成一個橋方法來確定類型安全性和相容性。上述代碼實際上被編譯器轉換為以下内容:

java複制代碼public class MyList {
    public void add(Object element) {
        // 添加元素的邏輯
    }
}

public class StringList extends MyList {
    @Override
    public void add(Object element) {
        add((String) element);
    }

    public void add(String element) {
        // 添加元素的邏輯
    }
}
           

在這個轉換後的代碼中,StringList 類包含了一個橋方法 add(Object element),它調用了真正的泛型方法 add(String element)。這樣就保持了類型安全性,并且與父類的非泛型方法相容。

通過生成泛型橋方法,Java編譯器可以在繼承和實作泛型類型時保持類型安全性和相容性。這些橋方法在内部轉換和維護泛型類型擦除的同時,提供了更好的類型檢查和運作時類型安全性。

六、泛型的局限性和注意事項

泛型中的類型安全性和運作時異常

在泛型中,類型安全性是指編譯器對類型進行檢查以確定程式在運作時不會出現類型錯誤。通過使用泛型,可以在編譯時捕獲許多類型錯誤,并避免在運作時出現類型轉換異常。

類型安全性的優勢:

  1. 編譯時類型檢查:Java編譯器對泛型進行類型檢查,以確定代碼的類型安全性。它可以驗證泛型類型參數是否與聲明的類型參數比對,并拒絕不正确的類型操作。
  2. 避免強制類型轉換:在使用泛型時,不再需要手動進行強制類型轉換,因為編譯器可以自動插入類型轉換代碼。
  3. 提高代碼可讀性和可維護性:通過使用泛型,可以明确指定容器中存儲的元素類型,使代碼更易讀和了解。它也可以提供更好的代碼維護性,因為類型資訊是顯式的。

類型安全性的實作:

  1. 編譯期類型檢查:編譯器會對泛型進行類型檢查,以確定在編譯時不會出現類型錯誤。如果存在類型不比對的情況,編譯器會報告錯誤并阻止代碼的編譯。
  2. 類型擦除機制:Java中的泛型是通過類型擦除實作的,即在編譯時将泛型類型擦除為原始類型(如 Object)。類型擦除確定了與原始非泛型代碼的相容性,并且可以維護向後相容性。
  3. 橋方法:為了維護泛型類和接口之間的繼承關系和類型安全性,編譯器會生成橋方法。橋方法用于在繼承或實作帶有泛型類型參數的類或接口時,確定正确的類型轉換和方法調用。

運作時異常:

盡管泛型增強了類型安全性,但在某些情況下仍可能發生運作時異常。這些異常通常發生在以下情況:

  1. 類型擦除引起的資訊丢失:由于類型擦除,無法在運作時擷取泛型類型參數的詳細資訊。是以,在進行類型轉換時,如果類型不比對,可能會導緻 ClassCastException 異常。
  2. 與原始類型互動:如果使用原始類型與泛型類型進行互動,例如将泛型集合指派給未經參數化的集合,可能會在編譯時沒有警告,但在運作時會導緻類型錯誤。
  3. 反射操作:通過反射機制,可以繞過泛型的類型安全性。在使用反射時,需要額外的注意,以避免類型錯誤和運作時異常。

示例:

java複制代碼List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");

// 編譯時類型檢查,不允許添加非 String 類型的元素
stringList.add(123);  // 編譯錯誤

// 擷取元素時不需要進行類型轉換
String firstElement = stringList.get(0);

// 疊代器周遊時可以確定元素類型的安全性
for (String element : stringList) {
    System.out.println(element);
}

// 類型擦除引起的運作時異常示例
List<Integer> integerList = new ArrayList<>();
integerList.add(10);

List rawList = integerList;  // 原始類型與泛型類型互動

List<String> stringList = rawList;  // 編譯通過,但在運作時會導緻類型錯誤

String firstElement = stringList.get(0);  // 運作時抛出 ClassCastException 異常
           

在這個示例中,原始類型 rawList 在編譯時可以與泛型類型 List<String> 互相指派。但在運作時,當我們嘗試從 stringList 中擷取元素時,由于類型擦除并且實際存儲的是整數類型,會導緻 ClassCastException 異常。

是以,盡管泛型提供了類型安全性和編譯時類型檢查的優勢,但仍需小心處理類型擦除和與原始類型的互動,以避免可能的運作時異常。

泛型數組的限制和解決方案

泛型數組是指使用泛型類型參數建立的數組。然而,Java中存在一些限制,不允許直接建立具有泛型類型參數的數組。這是由于Java泛型的類型擦除機制導緻的。

限制:

  1. 無法建立具有泛型類型參數的數組:在Java中,不能直接建立具有泛型類型參數的數組,例如 List<String>[] 或者 T[]。
  2. 編譯器警告:如果嘗試建立一個泛型數組,編譯器會發出警告,提示“泛型數組建立可能引起未經檢查或不安全的操作”。

問題原因:

泛型的類型擦除機制是導緻不能直接建立泛型數組的主要原因。泛型在編譯時被擦除為原始類型,是以無法在運作時擷取泛型類型的具體資訊。這就導緻了無法确定數組的确切類型。

解決方案:

雖然直接建立具有泛型類型參數的數組是受限制的,但可以通過以下兩種解決方案來處理泛型數組的問題:

1. 使用通配符或原始類型數組:

可以使用通配符(?)或原始類型數組來代替具體的泛型類型參數。例如,可以建立 List<?>[] 或者 Object[] 類型的數組。這種方式雖然不會得到類型安全性,但可以繞過編譯時的限制。

java複制代碼List<?>[] arrayOfLists = new List<?>[5];
Object[] objects = new Object[5];
           

需要注意的是,由于無法确定數組的确切類型,是以在通路數組元素時可能需要進行顯式的類型轉換。

2. 使用集合或其他資料結構:

可以使用集合(如 ArrayList、LinkedList 等)或其他資料結構代替數組來存儲泛型類型參數。這樣可以避免直接使用泛型數組帶來的限制和問題。

java複制代碼List<List<String>> listOfLists = new ArrayList<>();
           

使用集合的好處是它們提供了更靈活的操作和類型安全性,并且不受泛型數組的限制。

泛型和反射的相容性問題

泛型和反射之間存在一些相容性問題,這是由于Java泛型的類型擦除機制和反射的特性所導緻的。

1. 類型擦除導緻的資訊丢失: 泛型在Java中是通過類型擦除實作的,即在運作時,泛型類型參數會被擦除為原始類型(如 Object)。這意味着在使用反射時,無法擷取泛型類型參數的具體資訊,隻能得到原始類型。

解決方案: 可以使用反射操作擷取泛型類、泛型方法或泛型字段的中繼資料(例如名稱、修飾符、泛型參數等),但無法準确獲得泛型類型參數的具體類型。在某些情況下,可以結合使用泛型标記接口來傳遞類型資訊,進而在反射操作中擷取更多的類型資訊。

2. 泛型數組的限制: 無法直接建立具有泛型類型參數的數組。這是由于類型擦除機制導緻的,無法在運作時确定泛型類型參數的具體類型。

解決方案: 可以通過使用通配符(?)或原始類型數組來代替具體泛型類型參數的數組。然而,在通路數組元素時可能需要進行顯式的類型轉換。

3. 泛型方法的反射調用: 反射調用泛型方法時需要注意類型安全性。由于反射操作是在運作時動态執行的,編譯器無法進行靜态類型檢查,是以可能會導緻類型錯誤。

解決方案: 在使用反射調用泛型方法時,可以通過傳遞正确的參數類型來確定類型安全性,并對傳回值進行合适的類型轉換。

4. Class 對象的泛型資訊限制: 對于具體的泛型類型,無法通過 Class 對象擷取其泛型類型參數的具體資訊。例如,對于 List<String> 類型,無法直接從 List.class 中擷取到泛型類型參數為 String 的資訊。

解決方案: 可以使用 TypeToken 類庫等第三方庫來繞過該限制。TypeToken 可以通過子類化和匿名内部類的方式捕獲泛型類型參數的具體資訊。

七、泛型程式設計實踐和最佳實踐

泛型程式設計常見模式和技巧

1. 泛型類和接口: 定義帶有類型參數的泛型類或接口,可以使代碼适用于不同類型的資料。通過在類或接口中使用類型參數,可以在執行個體化時指定具體的類型。

java複制代碼public class GenericClass<T> {
    private T value;
    
    public void setValue(T value) {
        this.value = value;
    }
    
    public T getValue() {
        return value;
    }
}
           

2. 泛型方法: 定義帶有類型參數的泛型方法,可以使方法在調用時根據傳入的參數類型進行類型推斷,并傳回相應的類型。

java複制代碼public <T> T genericMethod(T value) {
    // 方法邏輯
    return value;
}
           

3. 通配符: 使用通配符(?)可以表示未知類型或限定類型範圍,增加代碼的靈活性。

  • 無界通配符:List<?> 表示可以存儲任意類型的 List。
  • 上界通配符:List<? extends Number> 表示可以存儲 Number 及其子類的 List。
  • 下界通配符:List<? super Integer> 表示可以存儲 Integer 及其父類的 List。

4. 類型限定和限制: 使用類型限定和限制可以限制泛型類型參數的範圍,提供更精确的類型資訊。

java複制代碼public <T extends Number> void processNumber(T number) {
    // 方法邏輯
}
           

5. 泛型與繼承關系: 泛型類和接口可以繼承、實作其他泛型類和接口,通過繼承關系可以建構更豐富的泛型層次結構。

java複制代碼public interface MyInterface<T> {
   // 接口定義
}

public class MyClass<T> implements MyInterface<T> {
   // 類定義
}
           

6. 泛型數組和集合: 使用泛型數組和集合可以處理不同類型的資料集合,提供更安全和靈活的資料存儲和操作方式。

java複制代碼List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");

String value = stringList.get(0);  // 擷取元素,無需轉換類型
           

7. 類型推斷: Java 7 引入的鑽石操作符(<>)可以根據上下文自動推斷類型參數,使代碼更簡潔。

java複制代碼Map<String, List<Integer>> map = new HashMap<>();  // 類型推斷
           

避免常見的泛型錯誤和陷阱

1. 混淆原始類型和泛型類型: 在使用泛型時,應確定正确區分原始類型和泛型類型。原始類型不具有類型參數,并喪失了泛型的好處。

避免方法: 使用泛型類型參數聲明類、接口和方法,并在代碼中明确指定類型參數。

2. 忽略類型檢查警告: 在使用泛型時,編譯器可能會生成類型檢查警告,如果忽略這些警告,可能導緻類型安全問題。

避免方法: 盡量避免直接忽略類型檢查警告,可以通過合理的類型限定、類型轉換或使用 @SuppressWarnings 注解來解決或抑制警告。

3. 建立泛型數組: 無法直接建立泛型數組,因為Java中的數組具有固定的類型(協變性)。如果嘗試建立泛型數組,可能會導緻編譯時錯誤或運作時異常。

避免方法: 可以使用通配符或原始類型數組代替具體的泛型數組。例如,使用 List<?> 或 List<Object> 代替 List<T>。

4. 泛型類型擦除: 在運作時,泛型類型參數會被擦除為原始類型(如 Object),導緻無法擷取泛型類型參數的具體資訊。

避免方法: 可以通過傳遞類型标記或使用第三方庫(如 TypeToken)來繞過泛型類型擦除問題,進而擷取更多的類型資訊。

5. 靜态上下文中的泛型: 靜态字段、靜态方法和靜态初始化塊不能引用泛型類型參數,因為它們在類加載時就存在,并且與執行個體化無關。

避免方法: 如果需要在靜态上下文中使用泛型類型,可以将泛型參數聲明為靜态方法内部的局部變量。

6. 範型和可變參數方法: 當調用可變參數方法時,在泛型方法中使用 <T...> 文法可能會導緻編譯錯誤。

避免方法: 可以使用邊界類型通配符(T[] 或 List<T>)作為參數類型,或者使用非泛型類型參數。

7. 泛型類型參數的邊界限定: 當泛型類型參數受到邊界限定時,要注意在代碼中合理使用這些限制,并防止類型轉換錯誤。

避免方法: 在合适的情況下,使用邊界限定來限制泛型類型參數,并在代碼中根據邊界類型進行相應的操作和轉換。

作者:蜀山劍客李沐白

連結:https://juejin.cn/post/7249913673215836218