Menu

Generics trong Java: lớp và phương thức an toàn kiểu

Generics trong Java là gì, cách viết lớp và phương thức generic, tham số kiểu có giới hạn, wildcard, và vì sao type erasure quan trọng.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Vì sao có generics

Kiểu generic cho phép bạn viết code một lần và dùng lại cho nhiều kiểu mà không đánh mất an toàn kiểu. Thay vì viết riêng IntBox, StringBox và UserBox, bạn viết một Box<T> duy nhất, trong đó T là chỗ trống để nơi gọi điền vào.

Bạn đã dùng generics mỗi lần viết ArrayList<String> hoặc một HashMap như HashMap<String, Integer>. Phần <...> là đối số kiểu (type argument). Trang này chỉ bạn cách tự viết kiểu generic.

Cách làm thay thế, lưu mọi thứ dưới dạng Object, sẽ vứt bỏ thông tin kiểu và buộc bạn ép kiểu một cách xấu xí, dễ sai:

Lệnh ép kiểu ở dòng cuối ném ClassCastException lúc chạy, đúng loại ngoại lệ mà generics được thiết kế để loại bỏ.

Lớp generic

Khai báo tham số kiểu trong dấu ngoặc nhọn ngay sau tên lớp. Theo quy ước, đó là một chữ cái in hoa: T cho "type", E cho "element", K/V cho key/value.

Bên trong Box, mọi T trở thành kiểu mà nơi gọi cung cấp. Box<String> là một chiếc hộp chỉ chứa và trả về String. Trình biên dịch từ chối name.set(99) trước khi chương trình kịp chạy.

Cặp <> rỗng ở vế phải (toán tử kim cương, diamond operator) cho phép trình biên dịch suy ra đối số kiểu từ vế trái, nên bạn không phải viết <String> hai lần.

Phương thức generic

Một phương thức có thể có tham số kiểu riêng, độc lập với lớp. Đặt tham số <T> trước kiểu trả về:

Bạn không bao giờ truyền T một cách tường minh: trình biên dịch tự suy ra từ đối số. Phương thức generic là cách các tiện ích như Collections.sort hay List.of giữ được an toàn kiểu với mọi kiểu phần tử.

Tham số kiểu có giới hạn

Đôi khi một kiểu generic chỉ có nghĩa với một số kiểu. extends ràng buộc tham số để bạn gọi được phương thức của kiểu giới hạn. Ở đây T extends Number nghĩa là T là Number hoặc bất kỳ lớp con nào (Integer, Double, ...), nên dùng được doubleValue():

Lưu ý extends ở đây nghĩa là "là kiểu con của", và nó dùng được cho cả lớp lẫn interface: <T extends Comparable<T>> cực kỳ phổ biến khi bạn cần so sánh các phần tử.

Wildcard: ? extends và ? super

Một cái bẫy tinh vi: List<Integer> không phải là List<Number>, dù Integer là Number. Generics là bất biến (invariant). Wildcard nới lỏng điều này khi bạn chỉ cần đọc hoặc chỉ cần ghi.

Dùng ? extends T cho nguồn cung cấp dữ liệu mà bạn đọc ra, và ? super T cho nơi tiêu thụ mà bạn ghi vào (quy tắc "PECS", Producer Extends, Consumer Super):

List ? extends Number cho phép bạn đọc phần tử dưới dạng Number nhưng không cho thêm vào (trình biên dịch không biết chính xác kiểu phần tử). List ? super Integer cho phép thêm Integer nhưng khi đọc thì nhận về Object. Hãy chọn wildcard khớp với hướng đi của dữ liệu.

Type erasure và giới hạn của nó

Generics là tính năng lúc biên dịch. Sau khi biên dịch, tham số kiểu bị xóa (erased): lúc chạy, Box<String> và Box<Integer> đều chỉ là Box. Điều này giúp generics tương thích ngược với code cũ, nhưng cũng đặt ra những giới hạn thật sự.

// None of these compile - the type parameter doesn't exist at runtime:
T value = new T();          // can't instantiate a type parameter
T[] array = new T[10];      // can't create a generic array
if (list instanceof List<String>) { } // can't test the type argument

Vì kiểu đã biến mất lúc chạy, bạn không thể hỏi "T là gì?" qua reflection, và không thể nạp chồng các phương thức chỉ khác nhau ở đối số generic (foo(List<String>) và foo(List<Integer>) bị xóa về cùng một chữ ký). Khi thực sự cần kiểu lúc chạy, hãy truyền một token Class<T> làm tham số của constructor hoặc phương thức.

Tiếp theo: biểu thức lambda

Bạn đã thấy phương thức generic nhận một kiểu làm tham số. Bước tiếp theo là coi hành vi như một tham số. Biểu thức lambda cho phép bạn truyền một đoạn code, tức một hàm, vào phương thức, và đó chính là cách bạn sẽ sắp xếp, lọc và biến đổi các collection generic mà bạn vừa học cách định kiểu an toàn.

Câu hỏi thường gặp

Generics trong Java là gì?

Generics cho phép bạn viết một lớp hoặc phương thức làm việc với một kiểu được chỉ định sau, thay vì gắn cứng với một kiểu cụ thể. Bạn khai báo tham số kiểu trong dấu ngoặc nhọn, class Box<T>, và nơi gọi điền kiểu thực tế, Box<String>. Sau đó trình biên dịch áp kiểu đó ở mọi chỗ, nên bạn phát hiện sai kiểu ngay lúc biên dịch và không cần ép kiểu thủ công.

Vì sao nên dùng generics thay vì Object?

Dùng Object làm mất hết thông tin kiểu: trình biên dịch không ngăn được bạn bỏ nhầm thứ vào, và bạn phải ép kiểu mọi giá trị lấy ra (có nguy cơ gặp ClassCastException lúc chạy). Generics đưa việc kiểm tra đó về thời điểm biên dịch. List<String> đơn giản là không nhận Integer, và get() đã trả về String sẵn: không cần ép kiểu, không có bất ngờ lúc chạy.

Type erasure trong generics của Java là gì?

Type erasure (xóa kiểu) nghĩa là thông tin kiểu generic chỉ tồn tại lúc biên dịch. Sau khi biên dịch, List<String> và List<Integer> đều chỉ là List lúc chạy: tham số kiểu đã bị xóa. Đó là lý do bạn không thể viết new T[10], gọi list instanceof List<String>, hay đọc tham số kiểu qua reflection. Generics cho bạn an toàn lúc biên dịch, không cho dữ liệu kiểu lúc chạy.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu