跳轉到

Java基礎:泛型機制

1. 簡介

泛型(Generics)是 Java 程式語言中的一個重要特性,允許在定義類別、介面和方法時使用類型參數。也就是說,泛型就是參數化類型,使得程式碼可以適用於多種資料類型,而不需要對每種類型都寫一次。

泛型的重要性體現在以下幾個方面:

  1. 類型安全:泛型在編譯時提供類型檢查,減少運行時錯誤的可能性。

  2. 程式碼重用:通過使用泛型,可以編寫出更通用、更靈活的程式碼,適用於多種資料類型。

  3. 性能提升:泛型消除許多顯式類型轉換的需要,提高程式的執行效率。

  4. 可讀性增強:泛型使得程式碼意圖更加明確,提高程式碼的可讀性和可維護性。

  5. API 設計:泛型為庫設計者提供更強大的工具,使得 API 更加靈活和易於使用。

2. 泛型的基本概念

泛型的核心思想是將類型參數化。在 Java 中,泛型主要應用於類別、介面和方法。讓我們來解泛型的基本概念:

泛型類別

泛型類別是在類別名稱後使用尖括號 <> 來定義一個或多個類型參數的類別。例如:

public class Box<T> {
    private T content;

    public void set(T content) {
        this.content = content;
    }

    public T get() {
        return content;
    }
}

在這個例子中,T 是類型參數,可以在創建 Box 物件時指定具體的類型。

泛型方法

泛型方法是在返回類型前使用 <> 聲明類型參數的方法。例如:

public static <E> void printArray(E[] array) {
    for (E element : array) {
        System.out.print(element + " ");
    }
    System.out.println();
}

這個方法可以印出任何類型的陣列。

類型參數命名慣例

雖然可以使用任何有效的標識符作為類型參數名,但通常遵循以下慣例:

  • E - Element(常用於集合)
  • T - Type
  • K - Key
  • V - Value
  • N - Number
  • S, U, V 等 - 第 2、3、4 個類型參數

這些命名慣例有助於提高程式碼的可讀性,特別是在處理多個類型參數時。

3. 泛型的優點

泛型為 Java 程式設計帶來許多顯著的優點,使得程式碼更加安全、高效和可重用。以下是泛型的主要優點:

類型安全

泛型提供編譯時的類型檢查,這意味著許多錯誤可以在編譯階段就被發現,而不是在運行時才出現,換言之,減少運行時錯誤,提高程式的穩定性。例如:

List<String> list = new ArrayList<>();
list.add("Hello");
list.add(123); // 編譯錯誤:不能添加整數到字串列表

消除類型轉換

在使用泛型之前,從集合中取出元素時常常需要進行顯式的類型轉換。泛型消除這種需要:

// 不使用泛型
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0); // 需要類型轉換

// 使用泛型
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 不需要類型轉換

可使程式碼更加簡潔,也消除因類型轉換錯誤而可能產生的 ClassCastException。

程式碼重用

泛型允許我們編寫更通用的程式碼,可以適用於多種類型,提高程式碼的重用性:

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    // getters and setters
}

Pair 類別可以用於任何類型的鍵值對,無需為每種類型組合都創建一個新的類別。

4. 泛型的使用

泛型在 Java 中有廣泛的應用,特別是在集合框架中。讓我們來看看泛型的幾種常見使用方式:

在集合中使用泛型

Java 集合框架大量使用泛型,這使得集合的使用更加安全和方便:

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// names.add(123); // 編譯錯誤

Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);

自定義泛型類別

我們可以創建自己的泛型類別,以增加程式碼的靈活性:

public class Pair<T, U> {
    private T first;
    private U second;

    public Pair(T first, U second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() { return first; }
    public U getSecond() { return second; }
}

// 使用
Pair<String, Integer> pair = new Pair<>("Hello", 42);
String first = pair.getFirst();  // "Hello"
int second = pair.getSecond();   // 42

泛型方法的實現

泛型方法可以獨立於類別而存在:

public class Utilities {
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    public static <T extends Comparable<T>> T findMax(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
}

// 使用
Integer[] numbers = {1, 2, 3, 4, 5};
Utilities.swap(numbers, 0, 4);  // 交換第一個和最後一個元素

String max = Utilities.findMax("apple", "banana");  // 返回 "banana"

例子中,swap 方法可以交換任何類型陣列的元素,而 findMax 方法可以比較任何實現 Comparable 介面的類型。

5. 泛型的限制

雖然泛型為 Java 程式設計帶來許多好處,但也有一些限制,以下是泛型的主要限制:

類型擦除

Java 的泛型是通過類型擦除(Type Erasure)實現的。這意味著泛型資訊只在編譯時存在,運行時會被擦除。例如:

List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

System.out.println(stringList.getClass() == intList.getClass()); // 輸出:true

這兩個 List 在運行時實際上是相同的類型。這種機制保證向後兼容性,但也帶來一些限制。

不能創建泛型陣列

由於類型擦除,不能直接創建泛型類型的陣列。例如:

// 這是不合法的
T[] array = new T[10]; // 編譯錯誤

// 但可以這樣做
T[] array = (T[]) new Object[10]; // 需要類型轉換,可能產生 ClassCastException

這個限制是因為陣列需要在運行時知道確切的元素類型,而泛型資訊在運行時已經被擦除。

不能使用基本類型作為類型參數

泛型不能使用基本數據類型(如 int, double, char 等)作為類型參數。必須使用對應的包裝類(如 Integer, Double, Character 等)。

// 不合法
List<int> numbers = new ArrayList<>(); // 編譯錯誤

// 正確的做法
List<Integer> numbers = new ArrayList<>();

泛型類型的靜態成員限制

泛型類型的靜態成員不能使用類的類型參數。例如:

public class GenericClass<T> {
    private static T staticMember; // 編譯錯誤

    public static T getStaticMember() { // 編譯錯誤
        return null;
    }
}

這是因為靜態成員屬於類本身,而不是類的實例,在類加載時就已經初始化,此時類型參數還未確定。

6. 泛型萬用字元

泛型萬用字元是 Java 泛型中的一個重要概念,提供更大的靈活性,特別是在處理不同但相關的泛型類型時。Java 中有三種主要的萬用字元:無界萬用字元、上界萬用字元和下界萬用字元。

無界萬用字元 (?)

無界萬用字元用問號 ? 表示,代表任何類型。當你只關心操作而不關心具體類型時,可以使用無界萬用字元。

public static void printList(List<?> list) {
    for (Object elem : list) {
        System.out.print(elem + " ");
    }
    System.out.println();
}

// 使用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> strList = Arrays.asList("Hello", "World");
printList(intList); // 輸出:1 2 3
printList(strList); // 輸出:Hello World

上界萬用字元 (? extends T)

上界萬用字元限制未知類型必須是指定類型 T 或其子類型。這在讀取具體元素時很有用。

public static double sumOfList(List<? extends Number> list) {
    double sum = 0.0;
    for (Number num : list) {
        sum += num.doubleValue();
    }
    return sum;
}

// 使用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
System.out.println(sumOfList(intList));    // 輸出:6.0
System.out.println(sumOfList(doubleList)); // 輸出:6.6

下界萬用字元 (? super T)

下界萬用字元的限制是,未知類型必須是指定類型 T 或其父類型 ->這在寫入元素時很有用。

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 5; i++) {
        list.add(i);
    }
}

// 使用
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // 輸出:[1, 2, 3, 4, 5]

萬用字元的使用可以增加程式碼的靈活性,但也需要注意: - 使用上界萬用字元 (? extends T) 的集合是只讀的,你不能添加元素到這樣的集合中。 - 使用下界萬用字元 (? super T) 的集合允許添加元素,但從中讀取元素時只能當作 Object 類型。

7. 泛型和繼承

泛型類別的繼承

當涉及到泛型類別的繼承時,需要注意以下幾點:

  1. 泛型類別可以擴展其他泛型類別或非泛型類別。
  2. 子類別可以添加自己的類型參數。

例如:

class GenericParent<T> {
    T value;
    // ...
}

class GenericChild<T, U> extends GenericParent<T> {
    U anotherValue;
    // ...
}

簡單來說,雖然 IntegerNumber 的子類,但 GenericParent<Integer> 並不是 GenericParent<Number> 的子類。這種情況稱為泛型不變性(invariance),也就是說,泛型類別的類型參數不會自動轉換。

泛型方法的覆寫

當覆寫泛型類別中的方法時,方法簽名必須完全匹配。例如:

class Animal {
    public <T> void feed(T food) {
        // ...
    }
}

class Dog extends Animal {
    @Override
    public <T> void feed(T food) {
        // 正確的覆寫
    }
}

如果嘗試改變類型參數,編譯器會報錯:

class Cat extends Animal {
    @Override
    public void feed(String food) {
        // 編譯錯誤:這不是一個有效的覆寫
    }
}

使用萬用字元處理繼承關係

為處理泛型和繼承之間的關係,我們經常需要使用萬用字元:

List<? extends Number> numbers = new ArrayList<Integer>();
// 可以讀取,但不能添加元素(除 null)
Number n = numbers.get(0);  // 可以
// numbers.add(Integer.valueOf(1));  // 編譯錯誤

List<? super Integer> integers = new ArrayList<Number>();
// 可以添加 Integer 或其子類型,但讀取時只能當作 Object
integers.add(Integer.valueOf(1));  // 可以
// Integer i = integers.get(0);  // 編譯錯誤
Object obj = integers.get(0);  // 可以

8. 泛型的實踐

以下是一些重要的泛型使用建議:

何時使用泛型

  1. 集合類:幾乎總是應該使用泛型來參數化集合類。
  2. 通用算法:當你的方法可以操作多種類型時,考慮使用泛型。
  3. 類型安全:當你需要在編譯時捕獲類型錯誤時。

泛型命名規範

遵循標準的泛型命名慣例可以提高程式碼的可讀性:

  • E - Element(常用於集合)
  • T - Type
  • K - Key
  • V - Value
  • N - Number
  • S, U, V 等 - 第 2、3、4 個類型參數

避免過度使用泛型

雖然泛型很強大,但過度使用可能會使程式碼變得複雜難懂。只在真正需要的地方使用泛型。

優先使用 List 而不是 T[]

由於泛型陣列創建的限制,通常建議使用 List<T> 而不是 T[]

// 避免這樣做
T[] array = (T[]) new Object[10]; // 可能導致 ClassCastException

// 推薦這樣做
List<T> list = new ArrayList<>(10);

使用菱形運算符

在 Java 7 及以後版本中,使用菱形運算符 <> 可以簡化泛型程式碼:

Map<String, List<String>> map = new HashMap<>(); // 而不是 new HashMap<String, List<String>>()

明智地使用萬用字元

  • 使用 ? extends T 當你只需要從結構中讀取。
  • 使用 ? super T 當你只需要寫入結構。
  • 使用無界萬用字元 ? 當你既不需要讀也不需要寫具體類型。

考慮泛型方法

如果只有方法需要類型參數,使用泛型方法而不是使整個類泛型化。

public static <T> void swap(T[] array, int i, int j) {
    T temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

本篇文章同步刊載iThome: iThome
筆者個人的網站: JUNYI