CodeGym /Các khóa học /JAVA 25 SELF /Generics wildcards

Generics wildcards

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Tính bất biến của generics: List<Number> ≠ List<Integer>

Trong Java, generics được thiết kế rất nghiêm ngặt: chúng bất biến. Điều này có nghĩa là ngay cả khi một kiểu là kiểu con của kiểu khác (ví dụ, Integer là kiểu con của Number), thì các collection với các kiểu này cũng không liên quan gì đến nhau.

Hãy tưởng tượng: bạn có một hộp táo (List<Integer>), và ai đó nói rằng đó là “hộp trái cây” (List<Number>). Nghe có vẻ hợp lý, vì táo là trái cây. Nhưng khi đó người ta có thể bỏ chuối (Double) vào hộp, và mọi thứ sẽ hỏng — vì hộp thực ra là “hộp táo”.

Ví dụ mã:

List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Lỗi biên dịch!

Trình biên dịch cố ý nghiêm ngặt ở đây: nó không cho phép biến danh sách “táo” thành danh sách “trái cây”. Nếu không, ta sẽ có thể thêm bất cứ thứ gì vào đó, và chương trình sẽ sập ở thời điểm chạy.

Vì vậy, List<Number>List<Integer> là hai kiểu hoàn toàn khác nhau, mặc dù bản thân Integer là kiểu con của Number.

Khác với mảng:

Integer[] intArr = new Integer[10];
Number[] numArr = intArr; // Được phép (mảng đồng biến)
numArr[0] = 3.14; // Lỗi lúc chạy (ArrayStoreException)

Kết luận: generics bất biến để đảm bảo an toàn kiểu ngay tại thời điểm biên dịch.

2. Ranh giới (bounds) của tham số kiểu

Đôi khi cần giới hạn các kiểu mà class hoặc phương thức generic có thể làm việc. Để làm điều đó, dùng ràng buộc (bounds).

Ranh giới trên (extends)

class Stats<T extends Number> {
    private T[] nums;
    // ...
}

Giờ Stats chỉ có thể làm việc với các kiểu là hậu duệ của Number (Integer, Double, Float, v.v.). Cố gắng tạo Stats<String> sẽ gây lỗi biên dịch.

Ràng buộc với interface

class Sorter<T extends Comparable<T>> {
    void sort(List<T> list) { /* ... */ }
}

Giờ Sorter chỉ làm việc với các kiểu triển khai interface Comparable.

Nhiều ràng buộc

Có thể chỉ định nhiều ràng buộc cùng lúc bằng &:

class MyClass<T extends Number & Comparable<T>> { /* ... */ }

T phải là lớp con của Number triển khai Comparable<T>.

Thứ tự quan trọng: lớp trước, rồi đến các interface.

3. Làm quen với wildcard: ?

Wildcard là kiểu “đại diện” nói rằng: “Ở đây có thể là một kiểu nào đó, nhưng tôi không biết chính xác là kiểu nào”.

Ví dụ:

  • List<?> — danh sách của bất cứ thứ gì.
  • List<? extends Number> — danh sách của bất kỳ kiểu nào là hậu duệ của Number (ví dụ: Integer, Double).
  • List<? super Integer> — danh sách của bất kỳ kiểu nào là tổ tiên của Integer (ví dụ: Integer, Number, Object).

Tại sao cần wildcards?

Chúng cho phép viết các phương thức làm việc với các collection của nhiều kiểu khác nhau nhưng có liên hệ.

Ví dụ: chỉ để đọc (producer)

void printNumbers(List<? extends Number> list) {
    for (Number n : list) {
        System.out.println(n);
    }
    // list.add(123); // Lỗi! Không thể thêm phần tử
}
  • Có thể đọc phần tử dưới dạng Number.
  • Không thể thêm phần tử (ngoại trừ null).

Ví dụ: chỉ để ghi (consumer)

void addIntegers(List<? super Integer> list) {
    list.add(42); // OK
    // Integer x = list.get(0); // Lỗi! Không biết kiểu nào sẽ được trả về
}
  • Có thể thêm các phần tử kiểu Integer (hoặc các kiểu con của nó).
  • Không thể đọc an toàn các phần tử dưới dạng Integer (chỉ có thể như Object).

Quy tắc PECS

PECS — “Producer Extends, Consumer Super”:

  • Producer — Extends: nếu collection chỉ xuất dữ liệu (producer), hãy dùng ? extends T.
  • Consumer — Super: nếu collection chỉ nhận dữ liệu (consumer), hãy dùng ? super T.

Dễ nhớ: extends — để đọc (producer), super — để ghi (consumer).

So sánh: mảng đồng biến, generics bất biến

  • Mảng đồng biến: có thể gán Integer[] cho biến kiểu Number[].
  • Generics bất biến: không thể gán List<Integer> cho biến kiểu List<Number>.

Wildcards cho phép “nới lỏng” phần nào tính bất biến của generics.

5. Phương thức generic và suy luận kiểu; hạn chế (type erasure)

Phương thức generic

Có thể khai báo các phương thức generic làm việc với mọi kiểu:

public static <T> void printList(List<T> list) {
    for (T elem : list) {
        System.out.println(elem);
    }
}

Suy luận kiểu (type inference)

Java có thể “đoán” kiểu tham số:

List<String> strings = List.of("a", "b");
printList(strings); // T = String

Hạn chế của generics: xóa kiểu

  • Trong Java, generics được triển khai bằng cơ chế xóa kiểu: thông tin về tham số kiểu bị loại bỏ sau khi biên dịch.
  • Trong bytecode, không có khác biệt giữa List<String>List<Integer>.
  • Không thể tạo mảng của các kiểu generic: new List<String>[10] — lỗi.
  • Không thể dùng instanceof với tham số kiểu: obj instanceof List<String> — lỗi.

6. Thực hành với collections và Stream API

Sao chép phần tử giữa các danh sách

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) {
        dest.add(item);
    }
}
  • src — producer (? extends T)
  • dest — consumer (? super T)

Cách dùng:

List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints); // OK: Number là siêu kiểu của Integer

Stream API và wildcards

List<Integer> ints = List.of(1, 2, 3);
List<? extends Number> numbers = ints;

numbers.stream()
    .map(Number::doubleValue)
    .forEach(System.out::println);

Lọc với wildcard

public static void printAll(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

7. Lỗi thường gặp

Lỗi số 1: Raw types (kiểu thô).
Việc dùng kiểu thô sẽ tắt kiểm tra kiểu và dẫn đến lỗi thời điểm chạy.

List list = new ArrayList(); // raw type — không tốt!
list.add("chuỗi");
list.add(123); // Có thể thêm bất cứ thứ gì

String s = (String) list.get(1); // ClassCastException!

Không bao giờ dùng raw types. Luôn chỉ rõ tham số kiểu: List<String>, List<Integer>.

Lỗi số 2: Chuyển đổi không an toàn với extends.
Cố gắng thêm phần tử vào collection với ? extends ... sẽ gây lỗi biên dịch.

List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // Lỗi biên dịch!

Lỗi số 3: “Bị xóa” kiểu khi overload.
Không thể overload phương thức chỉ khác ở tham số generic — sau khi xóa kiểu, chữ ký sẽ trùng nhau.

public void process(List<String> list) { /* ... */ }
public void process(List<Integer> list) { /* ... */ } // Lỗi biên dịch!

Lỗi số 4: Mảng của kiểu generic.
Không thể tạo mảng của các kiểu tham số hóa do cơ chế xóa kiểu.

List<String>[] arr = new List<String>[10]; // Lỗi biên dịch!
1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Giao diện của collections
Giao diện của collections
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION