Chuyển tới nội dung chính

Giới thiệu Generics

Mục tiêu bài học

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! 💥
Vấn đề nghiêm trọng
  1. Không có type safety: Có thể thêm bất kỳ kiểu dữ liệu nào vào collection
  2. 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
  3. Phải casting thủ công: Code dài dòng và dễ gây lỗi
  4. 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.

Định nghĩa

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:

  1. Type safety tại compile-time — Lỗi phát hiện sớm, không phải đợi runtime
  2. Loại bỏ casting thủ công — Code ngắn gọn, ít lỗi hơn
  3. Enable generic algorithms — Viết một thuật toán cho mọi kiểu dữ liệu
📖 Theo JLS §4.5

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 íchMô tảVí dụ
Type SafetyCompiler kiểm tra kiểu dữ liệuList<String> chỉ chấp nhận String
Phát hiện lỗi sớmLỗi xuất hiện tại compile timeKhông phải đợi runtime mới biết
Không cần castingCompiler tự động xử lýKhông cần (String) list.get(0)
Code rõ ràng hơnDễ đọc và maintainNhìn vào là biết chứa kiểu gì
Tái sử dụng codeViết một lần, dùng với nhiều kiểuBox<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
}
}
Kết quả

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ạnhRaw TypeGeneric Type
Khai báoList listList<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
Tại sao Raw Types vẫn tồn tại?

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
🔥 Bẫy OCP

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.

📖 Theo JLS §15.9

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);
🔥 Bẫy OCP

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"); }
};
💡 Cách nhớ

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
}
}
🔥 Bẫy OCP

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ệmMô tảVí dụ
GenericsTham số hóa kiểu dữ liệuList<String>, Map<K,V>
Type SafetyKiểm tra kiểu tại compile timeLỗi sớm, không crash runtime
Type ParameterTham số kiểu trong <><T>, <E>, <K,V>
Raw TypeGeneric không có type parameterList - TRÁNH DÙNG!
Diamond Operator<> để tránh lặp lạinew ArrayList<>()
Nguyên tắc vàng
  1. Luôn luôn sử dụng generics với Collections
  2. Không bao giờ sử dụng raw types
  3. Luôn luôn dùng diamond operator <> (Java 7+)
  4. 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í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!

Đọc thêm