Generics cơ bản — class và method nhận kiểu làm tham số
Vì sao cần generics, cú pháp class/method generic, type parameter T, bounded type parameter (T extends Comparable), và ví dụ tự viết generic collection đơn giản.
Bạn đã dùng List<String>, Map<String, Integer> trong nhiều bài trước. Dấu <...> là generics — cơ chế Java cho phép class/method nhận kiểu làm tham số. Thay vì viết StringList, IntegerList, UserList riêng, bạn viết một List<E> duy nhất, compiler thay E bằng kiểu cụ thể ở call site.
Bài này giải thích tại sao generics cần thiết (pre-Java 5 không có), cú pháp class/method generic, bounded type parameter, và viết một Box<T> đơn giản để nắm cơ chế.
1. Trước Java 5 — thế giới không generics
// Java 4 style — khong generics
List list = new ArrayList();
list.add("hello");
list.add(42); // cast vo tu — compiler khong can
String s = (String) list.get(0); // phai cast moi lan
String broken = (String) list.get(1); // ClassCastException RUNTIME
Vấn đề:
- Compiler không biết list chứa gì → không check type at compile time.
- Add nhầm kiểu → ra runtime mới biết → bug tinh vi.
- Caller cast mọi lần → code nặng nề, dễ sai.
Sơ đồ so sánh cơ chế kiểm tra kiểu (Type Casting vs Generics):
Cơ chế cũ (Pre-Java 5: Ép kiểu thủ công - Type Casting)
======================================================
[ Client Code ] --(add Object)--> [ ArrayList (raw) ]
| |
| (Lấy dữ liệu ra) | (Chứa Object)
v v
[ Ép kiểu thủ công (String) ] <----(trả Object)---
|
+---> Thành công (nếu đúng kiểu)
+---> LỖI RUNTIME ClassCastException (nếu sai kiểu)
Cơ chế mới (Java 5+: Tự động kiểm tra an toàn - Generics)
======================================================
[ Client Code ]
|
|--(Truyền String)--> [ Kiểm tra kiểu (Compile-time) ] --(Hợp lệ)--> [ ArrayList<String> ]
| | |
| v (Báo lỗi nếu sai kiểu) | (Lưu trữ & Xóa kiểu)
| [ COMPILE ERROR! ] |
| |
|<----(Tự động chèn cast ẩn)-------------------------------------------------+
Java 5 (2004) giới thiệu generics để fix:
List<String> list = new ArrayList<>();
list.add("hello");
list.add(42); // COMPILE ERROR — int khong phai String
String s = list.get(0); // khong can cast
Compiler bắt kiểu tại compile time, bỏ cast runtime, type-safe từ đầu đến cuối.
2. Cú pháp generic class
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// Su dung:
Box<String> s = new Box<>();
s.set("hello");
String v = s.get(); // tu dong la String, khong cast
Box<Integer> i = new Box<>();
i.set(42);
int n = i.get(); // auto-unbox Integer -> int
<T>khai báo type parameter — chữ viết hoa đơn, conventionT,E,K,V,R.- Trong class body,
Tdùng như kiểu bình thường — compiler thay bằng kiểu cụ thể ở call site. new Box<>()— diamond operator (Java 7+), compiler suyString/Integertừ biến khai báo.
2.1 Convention tên type parameter
Để giữ cho mã nguồn dễ đọc và nhất quán, Java quy ước sử dụng các ký tự đơn, viết hoa để đại diện cho tham số kiểu:
| Ký tự | Tên viết tắt đầy đủ | Ý nghĩa thực tế |
|---|---|---|
T | Type | Kiểu dữ liệu chung (General Type parameter). |
E | Element | Phần tử trong một Collection (ví dụ: List<E>, Set<E>). |
K | Key | Khóa của bản đồ (thường đi cặp trong Map<K, V>). |
V | Value | Giá trị đi kèm với khóa trong Map<K, V>. |
R | Return / Result | Kiểu dữ liệu trả về của một hàm (Functional Interface). |
N | Number | Giới hạn kiểu số (thường dùng làm bound T extends Number). |
? | Wildcard | Kiểu không xác định (sẽ được tìm hiểu sâu ở Bài 3). |
Không bắt buộc theo — nhưng đọc quen mắt với JDK.
2.2 Nhiều type parameter
public class Pair<A, B> {
private final A first;
private final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
public A getFirst() { return first; }
public B getSecond() { return second; }
}
Pair<String, Integer> p = new Pair<>("Alice", 30);
3. Generic method
Không chỉ class — method cũng có thể generic riêng:
public class Utils {
public static <T> T firstOrDefault(List<T> list, T defaultValue) {
return list.isEmpty() ? defaultValue : list.get(0);
}
}
String s = Utils.firstOrDefault(List.of("a", "b"), "default"); // "a"
Integer n = Utils.firstOrDefault(List.<Integer>of(), 0); // 0
Cú pháp: <T> khai trước return type. Compiler suy T từ argument.
// Compiler infer T = String
firstOrDefault(List.of("a"), "default");
// Explicit:
Utils.<String>firstOrDefault(List.of("a"), "default");
3.1 Generic method trong generic class
Class có type T, method có type riêng R:
public class Container<T> {
private T value;
public <R> R transform(Function<T, R> mapper) {
return mapper.apply(value);
}
}
Container<Integer> c = new Container<>();
// c.value = ...
String s = c.transform(n -> "value=" + n);
T là class-level, R là method-level.
3.2 Target Type Inference (Java 8+, JEP 101)
Trong các phiên bản Java ban đầu (Java 5 đến 7), khả năng tự động suy luận kiểu (Type Inference) của compiler còn rất sơ khai. Đối với các phương thức generic, dev thường phải khai báo Type Witness tường minh ở call site khi compiler gặp khó khăn trong việc đoán kiểu:
// Cu phap nap kieu tuong minh (Explicit Type Witness) truoc Java 8:
String s = Utils.<String>firstOrDefault(Arrays.asList("a", "b"), "default");
Từ Java 8 (JEP 101 - Generalized Target-Type Inference), JDK đã cải tiến vượt bậc công cụ suy luận kiểu của trình biên dịch. Compiler giờ đây không chỉ nhìn vào các đối số truyền vào phương thức, mà còn phân tích Target Type (Kiểu đích) từ ngữ cảnh xung quanh, bao gồm:
- Kiểu biến được gán (Variable Assignment Target):
// Compiler tu luan T = Integer dua vao kieu nhan cua bien n (Integer) Integer n = Utils.firstOrDefault(Collections.emptyList(), 0); - Kiểu đối số truyền vào phương thức lồng nhau (Nested Method Calls Context):
// Compiler tự biết Collections.emptyList() cần trả về List<String> // để tương thích với signature nhận vào của firstOrDefault String s = Utils.firstOrDefault(Collections.emptyList(), "default");
Nhờ JEP 101, cú pháp <Kiểu> (Type Witness) hầu như không còn cần thiết trong code hàng ngày, giúp code trở nên tinh gọn, tự nhiên mà vẫn đảm bảo tính an toàn kiểu ở mức tối đa tại thời điểm compile.
4. Bounded type parameter — T extends X
Mặc định T có thể là bất kỳ kiểu nào. Nếu cần gọi method của kiểu cụ thể (vd compareTo), giới hạn T bằng extends:
public class SortedBox<T extends Comparable<T>> {
private final List<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
Collections.sort(items); // can T co compareTo
}
}
SortedBox<Integer> ints = new SortedBox<>(); // OK — Integer implement Comparable
SortedBox<Book> books = new SortedBox<>(); // compile error neu Book khong Comparable
T extends Comparable<T> đọc là "T là một kiểu có thể compare với T". Trong body, compiler biết T có compareTo(T) → cho phép gọi.
4.1 Multi-bound
public class Task<T extends Runnable & AutoCloseable> {
private final T task;
public Task(T task) { this.task = task; }
public void runAndClose() {
task.run(); // co tu Runnable
task.close(); // co tu AutoCloseable
}
}
Cú pháp: T extends A & B & C. Class chỉ cho 1 (nếu có); interface nhiều. Throwable không được.
5. Ví dụ — tự viết Stack<E> đơn giản
public class Stack<E> {
private final List<E> items = new ArrayList<>();
public void push(E item) {
items.add(item);
}
public E pop() {
if (items.isEmpty()) throw new IllegalStateException("Stack is empty");
return items.remove(items.size() - 1);
}
public E peek() {
if (items.isEmpty()) throw new IllegalStateException("Stack is empty");
return items.get(items.size() - 1);
}
public int size() { return items.size(); }
public boolean isEmpty() { return items.isEmpty(); }
}
// Su dung:
Stack<Integer> s = new Stack<>();
s.push(1);
s.push(2);
int top = s.pop(); // 2
Viết 1 lần, dùng được với mọi kiểu E — không lặp IntStack, StringStack.
Trong ví dụ trên, lớp Stack<E> được thiết kế bằng cơ chế Composition (Ủy thác - Delegation) bằng cách bao bọc một List<E> bên trong. Đây là một tư duy thiết kế cấu trúc dữ liệu mới chuẩn mực, khắc phục hoàn toàn những sai lầm nghiêm trọng của lớp legacy java.util.Stack trong bộ JDK gốc:
- Lỗi kế thừa sai lầm (Inheritance Anti-Pattern):
java.util.Stackcủa JDK kế thừa trực tiếp từjava.util.Vector(một cấu trúc mảng động cổ xưa). Do kế thừa (extends),java.util.Stackkế thừa luôn tất cả các phương thức củaVectornhưinsertElementAt(obj, index),removeElementAt(index),clear()... Điều này vi phạm nghiêm trọng nguyên lý đóng gói (Encapsulation) và nguyên lý thay thế Liskov (Liskov Substitution Principle - LSP), cho phép client can thiệp trực tiếp vào giữa Stack vốn chỉ được phép truy xuất dạng LIFO (push/pop). - Hiệu năng kém do lạm dụng đồng bộ (Synchronized Overhead):
Vectorđược đồng bộ hóa mọi phương thức để chạy an toàn đa luồng. Do đó,java.util.Stackcũng bị khóa (synchronized) ở mọi thao tác. Điều này dẫn đến sự sụt giảm hiệu năng nặng nề trong các ứng dụng đơn luồng hoặc khi sử dụng các giải pháp concurrency hiện đại.
Định hình tư duy thiết kế: Khi cần cấu trúc Stack trong thực tế:
- Hãy ưu tiên sử dụng
Deque<E> stack = new ArrayDeque<>()của JDK (không bị synchronized và kế thừa đúng đặc tả). - Hoặc tự viết một lớp bọc (Composition) như
Stack<E>ở trên để bảo vệ hoàn toàn tính chất LIFO của cấu trúc dữ liệu.
6. Generic interface
public interface Converter<FROM, TO> {
TO convert(FROM input);
}
public class StringToIntConverter implements Converter<String, Integer> {
@Override
public Integer convert(String input) {
return Integer.parseInt(input);
}
}
Interface generic dùng khắp JDK: Comparator<T>, Predicate<T>, Function<T, R>, Supplier<T>, Iterable<T>...
7. Kiểu raw — legacy, tránh
List list = new ArrayList(); // RAW TYPE — khong khai kieu
list.add("hello");
list.add(42); // compile ok, type-unsafe
Java cho phép dùng raw type để backward compat với code pre-Java 5. Compiler warning "unchecked" — đừng bỏ qua. Raw type bypass type checking.
Luôn khai rõ: List<String>, Map<String, Integer>. Nếu không biết kiểu chính xác, dùng wildcard List<?> (bài 3) — type-safe, rõ ý.
8. Generic không dùng được cho primitive
List<int> list = ...; // COMPILE ERROR
List<Integer> list = ...; // OK — wrapper
Generic chỉ chấp nhận reference type. Primitive (int, double...) phải dùng wrapper (Integer, Double). Auto-boxing tự động chuyển:
List<Integer> list = new ArrayList<>();
list.add(42); // auto-box int -> Integer
int x = list.get(0); // auto-unbox
Vấn đề perf: mỗi auto-box tạo Integer object trên heap — tốn memory + GC. Trong hot loop tính toán, dùng primitive stream (IntStream, LongStream, DoubleStream) hoặc mảng int[] thay vì List<Integer>.
Java 21 đang bàn Valhalla — generic trên primitive (như C#). Chưa stable.
9. Pitfall tổng hợp
❌ Nhầm 1: Dùng raw type trong code mới.
List list = new ArrayList(); // warning "unchecked"
✅ List<String> list = new ArrayList<>(); với diamond <>.
❌ Nhầm 2: Generic trên primitive.
Box<int> b = new Box<>(); // COMPILE ERROR
✅ Box<Integer>. Nhớ cost auto-box.
❌ Nhầm 3: new T() trong class generic — không được.
class Container<T> {
T createDefault() { return new T(); } // COMPILE ERROR
}
✅ Pass factory: Container<T> constructor nhận Supplier<T>.
❌ Nhầm 4: Static field type-generic — không được.
class Container<T> {
static T shared; // COMPILE ERROR
}
✅ Static thuộc class, không instance → không có type parameter. Dùng Object hoặc tách ra class non-generic.
❌ Nhầm 5: Exception không generic.
class MyException<T> extends Exception { } // COMPILE ERROR
✅ Exception không được generic (liên quan type erasure + catch clauses).
10. 📚 Deep Dive Oracle
Spec / reference chính thức:
- JLS §8.1.2 — Generic Classes and Type Parameters
- JLS §8.4.4 — Generic Methods
- JLS §4.4 — Type Variables
- JLS §4.5 — Parameterized Types
- Oracle Generics Tutorial — beginner-friendly.
- Effective Java Item 26: "Don't use raw types"; Item 29: "Favor generic types"; Item 30: "Favor generic methods".
Ghi chú: Generics được thêm vào Java năm 2004 với JDK 5 — một trong những thay đổi lớn nhất. Thiết kế dùng type erasure (bài 2) để backward compat với code pre-Java 5 — đây vừa là điểm mạnh (không phá code cũ) vừa là điểm yếu (không có runtime type info cho generic). Project Valhalla đang thử sửa: generics specialization cho primitive và value type.
11. Tóm tắt
- Generics = class/method nhận kiểu làm tham số.
- Trước Java 5: không generics → cast runtime, type-unsafe. Sau: compiler bắt kiểu tại compile time.
- Cú pháp class:
class Box<T> { T value; }. Cú pháp method:<T> T foo(T x). - Diamond
<>(Java 7+):new Box<>()— compiler suy kiểu từ context. - Convention:
Tgeneric,Eelement,Kkey,Vvalue,Rreturn. - Bounded:
T extends X— giới hạn kiểu, dùng method của X trong body. - Generic interface dùng khắp JDK (
Comparator,Predicate,Function). - Raw type (
Listkhông khai kiểu) — legacy, compiler warning, tránh. - Generic chỉ cho reference type. Primitive phải dùng wrapper (auto-box cost).
- Không được
new T(), không được static field generic, không được generic exception.
12. Tự kiểm tra
Q1Viết class Pair<A, B> có 2 field final + method swap() trả Pair<B, A>.▸
Pair<A, B> có 2 field final + method swap() trả Pair<B, A>.public class Pair<A, B> {
private final A first;
private final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
public A getFirst() { return first; }
public B getSecond() { return second; }
public Pair<B, A> swap() {
return new Pair<>(second, first);
}
}
// Su dung:
Pair<String, Integer> p = new Pair<>("alice", 30);
Pair<Integer, String> swapped = p.swap();Điểm chú ý:
- Type parameter order trong
swap(): returnPair<B, A>— A và B đổi chỗ. Compiler kiểm tra type safety. - Diamond
<>:new Pair<>(second, first)— compiler suy<B, A>từ return type. - Với Java 16+, có thể viết gọn hơn bằng record:
public record Pair<A, B>(A first, B second) { public Pair<B, A> swap() { return new Pair<>(second, first); } }
Q2Đoạn sau compile không? Vì sao?public class Container<T> {
public T createDefault() {
return new T();
}
}
▸
public class Container<T> {
public T createDefault() {
return new T();
}
}Không compile. new T() không hợp lệ — Java không biết T có constructor nào (do type erasure, runtime T thành Object).
Workaround — pass factory qua argument:
public class Container<T> {
private final Supplier<T> factory;
public Container(Supplier<T> factory) { this.factory = factory; }
public T createDefault() { return factory.get(); }
}
// Su dung:
Container<User> c = new Container<>(User::new); // method reference
User u = c.createDefault();Hoặc nhận Class<T> rồi dùng reflection — phức tạp hơn, dễ fail runtime, chỉ dùng khi bắt buộc.
Q3Khi nào dùng bounded type parameter <T extends X>? Cho ví dụ.▸
<T extends X>? Cho ví dụ.Khi trong body cần gọi method của kiểu cụ thể mà T không đảm bảo có. Ví dụ:
// Khong bound -> khong goi duoc compareTo
public static <T> T max(List<T> list) {
// list.stream().sorted(...) -> KHONG biet T co compareTo khong
}
// Bound <T extends Comparable<T>>
public static <T extends Comparable<T>> T max(List<T> list) {
T maxVal = list.get(0);
for (T item : list) {
if (item.compareTo(maxVal) > 0) maxVal = item;
}
return maxVal;
}
max(List.of(3, 1, 4, 1, 5)); // OK — Integer implements Comparable<Integer>
max(List.of(new Object())); // COMPILE ERROR — Object khong ComparableBound thu hẹp tập kiểu T xuống những kiểu "có compareTo" → compiler cho phép gọi.
Các case khác:
<T extends Number>— T phải là số (Integer, Double, ...). CóintValue(),doubleValue().<T extends Closeable>— T cóclose().<T extends Runnable & AutoCloseable>— T phải có cả 2 (multi-bound).
Q4Vì sao List<int> không compile?▸
List<int> không compile?Java generics chỉ chấp nhận reference type, không chấp nhận primitive (int, double, boolean, v.v.).
Lý do: type erasure (bài 2) — runtime List<T> thành List (raw) với mọi element là Object. Primitive không phải Object, không wrap được trực tiếp.
Workaround: dùng wrapper class:
List<Integer> list = new ArrayList<>();
list.add(42); // auto-box: int -> Integer
int x = list.get(0); // auto-unbox: Integer -> intCost: mỗi Integer là object trên heap (~16 bytes overhead mỗi instance). Với 1M phần tử, List<Integer> tốn ~40MB, còn int[] chỉ ~4MB.
Cho hot path số học: dùng int[] / IntStream / LongStream / DoubleStream — primitive specialized, không auto-box.
Project Valhalla (đang phát triển) nhắm cho phép List<int> qua value type — hiện chưa production-ready.
Q5Đoạn sau compile không?public class Cache<T> {
private static List<T> items = new ArrayList<>();
public static void add(T item) { items.add(item); }
}
▸
public class Cache<T> {
private static List<T> items = new ArrayList<>();
public static void add(T item) { items.add(item); }
}Không. Static member không thấy type parameter class.
Lý do: T là parameter của instance — mỗi new Cache() chỉ định T khác nhau (Cache<String>, Cache<Integer>). Static field thuộc class, không instance — không biết T nào để dùng.
Compile error: "non-static type variable T cannot be referenced from a static context".
Fix (chọn theo ý đồ):
- Không cần generic, đổi static dùng
Object:static List<Object>items — bạn mất type safety. - Static generic method với type parameter riêng:
public static <U> void add(U item, List<U> list). - Bỏ static, biến thành instance:
private List<T> items. - Nếu muốn một cache toàn cục theo kiểu: dùng
Map<Class<?>, List<?>>(pattern heterogeneous map).
Bài tiếp theo: Type erasure — sự thật không dễ chịu của generics Java
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên