Giới thiệu Generics
Sau bài này, bạn sẽ:
- Hiểu được vấn đề của Java trước khi có Generics và lý do cần Generics
- Nắm được khái niệm type parameter và type safety tại compile time
- Phân biệt được raw types vs generic types và tại sao phải tránh raw types
- Sử dụng diamond operator
<>để viết code ngắn gọn hơn - Áp dụng Generics với các Collections phổ biến (List, Map, Set)
Vấn đề trước khi có Generics
Trước Java 5 (2004), khi làm việc với Collections, chúng ta phải sử dụng kiểu Object và casting thủ công:
// Code trước Java 5 - không có Generics
List list = new ArrayList();
list.add("Hello");
list.add("World");
list.add(123); // Có thể thêm bất kỳ kiểu nào - không có kiểm tra!
// Phải casting thủ công
String first = (String) list.get(0); // OK
String second = (String) list.get(2); // ClassCastException tại runtime! 💥
- Không có type safety: Có thể thêm bất kỳ kiểu dữ liệu nào vào collection
- ClassCastException tại runtime: Lỗi chỉ xuất hiện khi chạy chương trình, không phải lúc compile
- Phải casting thủ công: Code dài dòng và dễ gây lỗi
- Khó maintain: Không biết collection chứa kiểu dữ liệu gì
Ví dụ thực tế về vấn đề
// Giả sử bạn có một shopping cart
List shoppingCart = new ArrayList();
shoppingCart.add(new Product("Laptop", 1000));
shoppingCart.add(new Product("Mouse", 20));
// Một developer khác vô tình thêm String
shoppingCart.add("Discount Code"); // Compiler không báo lỗi! ⚠️
// Khi tính tổng giá
double total = 0;
for (Object item : shoppingCart) {
Product product = (Product) item; // Crash khi gặp String! 💥
total += product.getPrice();
}
Generics là gì?
Generics (được giới thiệu trong Java 5) cho phép bạn định nghĩa type parameters - tham số hóa kiểu dữ liệu. Điều này mang lại type safety tại compile time.
Generics = Cơ chế cho phép classes, interfaces và methods hoạt động với các kiểu dữ liệu được chỉ định tại compile time, đảm bảo type safety và loại bỏ casting thủ công.
Tại sao Generics tồn tại? (Why Generics Exist)
Generics được tạo ra để giải quyết 3 vấn đề cốt lõi:
- Type safety tại compile-time — Lỗi phát hiện sớm, không phải đợi runtime
- Loại bỏ casting thủ công — Code ngắn gọn, ít lỗi hơn
- Enable generic algorithms — Viết một thuật toán cho mọi kiểu dữ liệu
Java Language Specification §4.5 định nghĩa parameterized types: "A generic class or interface declaration defines a set of parameterized types... to allow type safety to be checked at compile time."
Generics KHÔNG thêm overhead tại runtime vì sử dụng type erasure (xóa thông tin type sau compile).
Ví dụ: Generic algorithms
// Một thuật toán sort có thể dùng cho mọi kiểu Comparable
public static <T extends Comparable<T>> void sort(List<T> list) {
// Sort logic áp dụng cho Integer, String, Date, custom classes...
}
// Trước Generics, phải viết sortIntegers(), sortStrings(), sortDates()...
// Với Generics, chỉ cần một method sort<T>!
Lợi ích của Generics
| Lợi ích | Mô tả | Ví dụ |
|---|---|---|
| Type Safety | Compiler kiểm tra kiểu dữ liệu | List<String> chỉ chấp nhận String |
| Phát hiện lỗi sớm | Lỗi xuất hiện tại compile time | Không phải đợi runtime mới biết |
| Không cần casting | Compiler tự động xử lý | Không cần (String) list.get(0) |
| Code rõ ràng hơn | Dễ đọc và maintain | Nhìn vào là biết chứa kiểu gì |
| Tái sử dụng code | Viết một lần, dùng với nhiều kiểu | Box<T> dùng cho mọi kiểu |
So sánh: Code không dùng vs có dùng Generics
Biểu đồ so sánh: Generic vs Raw Type
Không dùng Generics (trước Java 5)
// Không type safety
List names = new ArrayList();
names.add("Alice");
names.add("Bob");
names.add(123); // Compiler không báo lỗi! ⚠️
// Phải casting thủ công
String name = (String) names.get(0);
String wrong = (String) names.get(2); // ClassCastException! 💥
// Không biết list chứa gì
public void printList(List list) { // List của gì?
for (Object obj : list) {
System.out.println((String) obj); // Hy vọng là String!
}
}
Có dùng Generics (từ Java 5 trở đi)
// Type safety - compiler kiểm tra
List<String> names = new ArrayList<String>();
names.add("Alice");
names.add("Bob");
names.add(123); // ❌ Compile error: cannot add Integer to List<String>
// Không cần casting
String name = names.get(0); // Tự động là String
String name2 = names.get(1); // Không cần (String)
// Rõ ràng và type-safe
public void printList(List<String> list) { // Rõ ràng là List<String>
for (String str : list) {
System.out.println(str); // Không cần casting
}
}
Code với Generics ngắn gọn hơn, an toàn hơn và dễ hiểu hơn!
Generic Type trong Collections
ArrayList với Generics
// Khai báo ArrayList chứa String
List<String> fruits = new ArrayList<String>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Không cần casting
String firstFruit = fruits.get(0); // Tự động là String
// Type safety - compile error
fruits.add(123); // ❌ Error
fruits.add(true); // ❌ Error
fruits.add(new Object()); // ❌ Error
Map với nhiều type parameters
// Map với key là String, value là Integer
Map<String, Integer> scores = new HashMap<String, Integer>();
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Charlie", 92);
// Không cần casting
Integer aliceScore = scores.get("Alice"); // Tự động là Integer
// Type safety
scores.put("David", "A+"); // ❌ Error: String không phải Integer
scores.put(123, 90); // ❌ Error: Integer không phải String
Set với Generics
// Set chứa Integer - không có duplicate
Set<Integer> numbers = new HashSet<Integer>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(2); // Duplicate - sẽ bị ignore
// Iteration type-safe
for (Integer num : numbers) {
System.out.println(num * 2); // Trực tiếp dùng như Integer
}
Raw Types và tại sao nên tránh
Raw Type = Generic type được sử dụng mà không chỉ định type parameter.
// Raw type - KHÔNG NÊN DÙNG!
List rawList = new ArrayList(); // Thiếu <String> hoặc <Integer>
// Vấn đề với raw types
rawList.add("String");
rawList.add(123);
rawList.add(new Object());
// Compiler chỉ cảnh báo, không báo lỗi! ⚠️
// Nguy hiểm khi sử dụng
String str = (String) rawList.get(1); // ClassCastException!
So sánh Raw Type vs Generic Type
| Khía cạnh | Raw Type | Generic Type |
|---|---|---|
| Khai báo | List list | List<String> list |
| Type Safety | ❌ Không có | ✅ Có |
| Compile Check | ❌ Chỉ warning | ✅ Error khi sai |
| Casting | ✅ Bắt buộc | ❌ Không cần |
| Khuyến nghị | ❌ Tránh dùng | ✅ Luôn dùng |
Raw types chỉ được giữ lại để backward compatibility với code Java cũ (trước Java 5). KHÔNG BAO GIỜ sử dụng raw types trong code mới!
Raw Types Danger: Heap Pollution
Heap pollution (ô nhiễm heap) xảy ra khi raw type gặp generic code — một variable của generic type trỏ đến object sai kiểu!
// Ví dụ heap pollution nghiêm trọng
public class HeapPollutionDanger {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
strings.add("Java");
strings.add("Python");
// Truyền vào method dùng raw type
contaminate(strings); // ⚠️ Compiler warning: unchecked call
// Heap pollution đã xảy ra!
// List<String> bây giờ chứa Integer
String first = strings.get(0); // OK - "Java"
String second = strings.get(1); // OK - "Python"
String third = strings.get(2); // 💥 ClassCastException!
// Runtime cố cast Integer thành String → CRASH!
}
// Method dùng raw type - nguy hiểm!
private static void contaminate(List list) {
list.add(Integer.valueOf(42)); // Thêm Integer vào List<String>
// Compiler CHỈ warning, KHÔNG error!
}
}
// Output: Exception in thread "main" java.lang.ClassCastException:
// java.lang.Integer cannot be cast to java.lang.String
OCP Exam Trap: Code sau compile thành công nhưng crash runtime!
List<String> names = new ArrayList<>();
addToList(names); // ⚠️ Warning nhưng compile OK
String name = names.get(0); // 💥 Runtime crash!
static void addToList(List list) { // Raw type!
list.add(123); // Thêm Integer vào List<String>
}
Nhớ: Raw type bypasses type checking → compile OK, runtime crash!
Ví dụ về vấn đề với Raw Types
// BAD: Mixing raw types và generic types
public class RawTypeProblems {
public static void main(String[] args) {
List<String> strings = new ArrayList<String>();
addToList(strings);
String s = strings.get(0); // ClassCastException! 💥
}
// Raw type parameter - nguy hiểm!
private static void addToList(List list) {
list.add(123); // Thêm Integer vào List<String>!
}
}
// GOOD: Sử dụng generics đúng cách
public class GenericSolution {
public static void main(String[] args) {
List<String> strings = new ArrayList<String>();
addToList(strings);
String s = strings.get(0); // An toàn! ✅
}
// Generic method - type safe!
private static void addToList(List<String> list) {
list.add("Hello");
// list.add(123); // ❌ Compile error
}
}
Diamond Operator <> (Java 7+)
Diamond Operator Type Inference Flow
Từ Java 7, bạn có thể sử dụng diamond operator <> để tránh lặp lại type parameters:
// Java 5-6: Phải lặp lại type parameters
List<String> list1 = new ArrayList<String>();
Map<String, List<Integer>> map1 = new HashMap<String, List<Integer>>();
// Java 7+: Diamond operator - ngắn gọn hơn! 💎
List<String> list2 = new ArrayList<>();
Map<String, List<Integer>> map2 = new HashMap<>();
Diamond Operator Internals: Type Inference
Type inference (suy luận kiểu) là cách compiler tự động xác định type parameters từ context.
JLS §15.9 (Class Instance Creation) quy định: "If the class instance creation expression ends with <>, the type arguments are inferred from the context." Compiler sử dụng target typing để suy luận type.
// Compiler type inference hoạt động như sau:
List<String> names = new ArrayList<>();
// ^^^^^^^ Target type ^^^^^^^ Diamond - compiler infer từ target
// Bước inference:
// 1. Compiler thấy target type: List<String>
// 2. Diamond <> báo: "hãy infer type parameters"
// 3. Compiler suy luận: ArrayList phải là ArrayList<String>
// 4. Kết quả: new ArrayList<String>() (như khi viết tường minh)
Khi Type Inference Thất Bại
// ❌ Lỗi: Không có target type
var list = new ArrayList<>(); // Error trước Java 10
// Compiler không biết phải infer thành ArrayList<gì>?
// ✅ Fix 1: Chỉ định type tường minh
var list = new ArrayList<String>();
// ✅ Fix 2: Khai báo với type
List<String> list = new ArrayList<>();
// ❌ Lỗi: Chained calls mơ hồ
List<String> result = new ArrayList<>().subList(0, 10);
// Error: Cannot infer type arguments for ArrayList<>
// ✅ Fix: Chỉ định type
List<String> result = new ArrayList<String>().subList(0, 10);
Diamond với anonymous classes KHÔNG hoạt động (trước Java 9):
// ❌ Java 8: Compile error
List<String> list = new ArrayList<>() {
{ add("Item"); } // Anonymous class initializer
};
// Error: '<>' cannot be used with anonymous classes
// ✅ Java 9+: OK với anonymous classes
List<String> list = new ArrayList<>() {
{ add("Item"); }
};
Diamond = Compiler làm hộ bạn
- Bạn viết
<>→ Compiler điền type tự động - Giống như "to be determined" trong tiếng Anh
- Diamond chỉ dùng phía bên phải (constructor call)
Autoboxing + Generics Interaction
Autoboxing (tự động boxing primitive → wrapper) hoạt động với Generics, nhưng có bẫy nguy hiểm!
// Autoboxing với List<Integer>
List<Integer> numbers = new ArrayList<>();
numbers.add(42); // int → Integer (autoboxing)
numbers.add(10); // Autoboxing tự động
int first = numbers.get(0); // Integer → int (unboxing)
// Output: 42
// ✅ Tiện lợi: Không cần boxing thủ công
// Trước Java 5:
// list.add(Integer.valueOf(42));
// int x = ((Integer) list.get(0)).intValue();
Bẫy NullPointerException với Unboxing:
public class AutoboxingTrap {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(null); // ⚠️ Legal - Integer là object, nhận null
// 💥 NullPointerException khi unboxing!
int value = numbers.get(0); // null.intValue() → CRASH!
// Exception: java.lang.NullPointerException
}
}
OCP Exam Trap: Unboxing null gây NullPointerException!
List<Integer> scores = Arrays.asList(100, null, 90);
int total = 0;
for (int score : scores) { // 💥 Crash khi gặp null!
total += score;
}
// Exception in thread "main" java.lang.NullPointerException
// Fix: Check null trước khi unbox
int total = 0;
for (Integer score : scores) {
if (score != null) { // Check null TRƯỚC unboxing
total += score;
}
}
Performance impact:
// ⚠️ Performance issue: Boxing/Unboxing overhead
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
numbers.add(i); // 100,000 boxing operations!
}
int sum = 0;
for (int num : numbers) {
sum += num; // 100,000 unboxing operations!
}
// Mỗi boxing/unboxing tạo overhead → Chậm với large datasets
// Alternative: Primitive collections (Trove, FastUtil, Eclipse Collections)
Ví dụ thực tế với Diamond Operator
public class StudentManager {
// Clean và modern
private List<Student> students = new ArrayList<>();
private Map<String, Student> studentById = new HashMap<>();
private Set<String> emails = new HashSet<>();
public void addStudent(Student student) {
students.add(student);
studentById.put(student.getId(), student);
emails.add(student.getEmail());
}
// Method với generic return type
public List<Student> getStudentsByGrade(int grade) {
List<Student> result = new ArrayList<>(); // Diamond operator
for (Student s : students) {
if (s.getGrade() == grade) {
result.add(s);
}
}
return result;
}
}
Type Safety Timeline: Java 1.0 → Modern
Xem lịch sử phát triển type safety trong Java:
Metaphor (Ẩn dụ):
Generics giống như hộp có nhãn (labeled box)
- Java 1.0: Hộp không nhãn — bỏ gì vào cũng được, lấy ra không biết là gì (phải đoán)
- Java 5 Generics: Hộp có nhãn "Táo" — chỉ bỏ táo vào được, lấy ra chắc chắn là táo
- Java 7 Diamond: Không cần viết "Táo" hai lần, nhìn nhãn một lần là đủ
Kết quả: Ít lỗi hơn, an toàn hơn, code rõ ràng hơn!
Tóm tắt
| Khái niệm | Mô tả | Ví dụ |
|---|---|---|
| Generics | Tham số hóa kiểu dữ liệu | List<String>, Map<K,V> |
| Type Safety | Kiểm tra kiểu tại compile time | Lỗi sớm, không crash runtime |
| Type Parameter | Tham số kiểu trong <> | <T>, <E>, <K,V> |
| Raw Type | Generic không có type parameter | List - TRÁNH DÙNG! |
| Diamond Operator | <> để tránh lặp lại | new ArrayList<>() |
- Luôn luôn sử dụng generics với Collections
- Không bao giờ sử dụng raw types
- Luôn luôn dùng diamond operator
<>(Java 7+) - Generics giúp lỗi xuất hiện sớm hơn (compile time thay vì runtime)
Bài tập thực hành
Bài 1: Phát hiện lỗi
Tìm và sửa lỗi trong đoạn code sau:
List names = new ArrayList();
names.add("Alice");
names.add(123);
names.add(true);
for (Object obj : names) {
String name = (String) obj;
System.out.println(name.toUpperCase());
}
Đáp án
// Sử dụng generics
List<String> names = new ArrayList<>();
names.add("Alice");
// names.add(123); // ❌ Compile error
// names.add(true); // ❌ Compile error
for (String name : names) { // Không cần casting
System.out.println(name.toUpperCase());
}
Bài 2: Refactor code cũ
Refactor code sau để sử dụng generics:
Map productPrices = new HashMap();
productPrices.put("Laptop", 1000);
productPrices.put("Mouse", 20);
productPrices.put("Keyboard", 50);
Object price = productPrices.get("Laptop");
int laptopPrice = (Integer) price;
Đáp án
Map<String, Integer> productPrices = new HashMap<>();
productPrices.put("Laptop", 1000);
productPrices.put("Mouse", 20);
productPrices.put("Keyboard", 50);
Integer price = productPrices.get("Laptop"); // Không cần casting
int laptopPrice = price;
Bài 3: Tạo StudentGradeManager
Viết một class StudentGradeManager sử dụng generics để quản lý điểm của học sinh:
public class StudentGradeManager {
// TODO: Khai báo Map để lưu tên học sinh và điểm
public void addGrade(String studentName, Integer grade) {
// TODO: Implement
}
public Integer getGrade(String studentName) {
// TODO: Implement
return null;
}
public List<String> getStudentsWithGradeAbove(int threshold) {
// TODO: Implement - trả về danh sách học sinh có điểm > threshold
return null;
}
}
Đáp án
public class StudentGradeManager {
private Map<String, Integer> grades = new HashMap<>();
public void addGrade(String studentName, Integer grade) {
grades.put(studentName, grade);
}
public Integer getGrade(String studentName) {
return grades.get(studentName);
}
public List<String> getStudentsWithGradeAbove(int threshold) {
List<String> result = new ArrayList<>();
for (Map.Entry<String, Integer> entry : grades.entrySet()) {
if (entry.getValue() > threshold) {
result.add(entry.getKey());
}
}
return result;
}
}
Kết luận
Generics là một tính năng quan trọng giúp code Java type-safe, dễ đọc và ít lỗi hơn. Trong các bài tiếp theo, chúng ta sẽ tìm hiểu cách tạo generic classes và methods của riêng bạn!