Back to blog

Tìm Hiểu Sâu: Giải Thích Java Generics

javagenericstype-safetyadvancedbackend
Tìm Hiểu Sâu: Giải Thích Java Generics

Generics là một trong những tính năng mạnh mẽ nhất của Java, được giới thiệu trong Java 5 để mang lại type safety tại thời điểm biên dịch và khả năng tái sử dụng code cho ngôn ngữ. Tuy nhiên, chúng thường bị hiểu sai hoặc chưa được tận dụng hết. Trong bài viết này, chúng ta sẽ khám phá Java generics từ cơ bản đến các mẫu nâng cao, với các ví dụ thực tế mà bạn sẽ sử dụng trong các ứng dụng Spring Boot.

Tại Sao Cần Generics?

Trước khi có generics (trước Java 5), các collections và utility classes dựa vào kiểu Object, yêu cầu ép kiểu tường minh và có nguy cơ lỗi runtime:

// Java trước generics (nguy hiểm!)
List names = new ArrayList();
names.add("Alice");
names.add(123); // Biên dịch thành công nhưng sai về mặt ngữ nghĩa
 
String name = (String) names.get(0); // Yêu cầu ép kiểu tường minh
String invalid = (String) names.get(1); // ClassCastException tại runtime!

Vấn đề:

  • Không có type safety: Compiler không thể ngăn chặn sai lệch kiểu dữ liệu
  • Ép kiểu tường minh: Dài dòng và dễ lỗi
  • Lỗi runtime: Bugs được phát hiện muộn trong quá trình phát triển

Generics giải quyết các vấn đề này bằng cách cho phép parameterized types:

// Với generics (an toàn!)
List<String> names = new ArrayList<>();
names.add("Alice");
// names.add(123); // Lỗi biên dịch - type safety!
 
String name = names.get(0); // Không cần ép kiểu

Lợi ích:

  1. Kiểm tra kiểu tại compile-time: Phát hiện lỗi sớm
  2. Loại bỏ casts: Code sạch hơn
  3. Tái sử dụng code: Viết một lần, dùng với bất kỳ kiểu nào
  4. Tài liệu tốt hơn: Các kiểu truyền đạt ý định

Generic Classes

Một generic class là một class chấp nhận một hoặc nhiều type parameters:

public class Box<T> {
    private T content;
 
    public void set(T content) {
        this.content = content;
    }
 
    public T get() {
        return content;
    }
}
 
// Sử dụng
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get(); // Không cần cast
 
Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer number = intBox.get();

Nhiều Type Parameters

Classes có thể chấp nhận nhiều type parameters:

public class Pair<K, V> {
    private K key;
    private V value;
 
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
 
    public K getKey() { return key; }
    public V getValue() { return value; }
}
 
// Sử dụng
Pair<String, Integer> pair = new Pair<>("age", 25);
String key = pair.getKey();
Integer value = pair.getValue();

Ví Dụ Thực Tế: API Response Wrapper

Trong các ứng dụng Spring Boot, generic response wrappers rất phổ biến:

public class ApiResponse<T> {
    private boolean success;
    private String message;
    private T data;
    private LocalDateTime timestamp;
 
    public ApiResponse(boolean success, String message, T data) {
        this.success = success;
        this.message = message;
        this.data = data;
        this.timestamp = LocalDateTime.now();
    }
 
    // Static factory methods
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(true, "Success", data);
    }
 
    public static <T> ApiResponse<T> error(String message) {
        return new ApiResponse<>(false, message, null);
    }
 
    // Getters...
}
 
// Sử dụng trong Controller
@GetMapping("/users/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
    User user = userService.findById(id);
    return ResponseEntity.ok(ApiResponse.success(user));
}
 
@GetMapping("/posts")
public ResponseEntity<ApiResponse<List<Post>>> getPosts() {
    List<Post> posts = postService.findAll();
    return ResponseEntity.ok(ApiResponse.success(posts));
}

Generic Interfaces

Interfaces cũng có thể là generic:

public interface Repository<T, ID> {
    T save(T entity);
    Optional<T> findById(ID id);
    List<T> findAll();
    void delete(T entity);
}
 
// Implementation
public class UserRepository implements Repository<User, Long> {
    @Override
    public User save(User entity) {
        // Implementation
    }
 
    @Override
    public Optional<User> findById(Long id) {
        // Implementation
    }
 
    // Các methods khác...
}

Spring Data JPA's JpaRepository sử dụng pattern này rộng rãi.

Generic Methods

Methods có thể khai báo type parameters của riêng chúng, độc lập với class:

public class Util {
    // Generic method
    public static <T> T getFirst(List<T> list) {
        if (list.isEmpty()) {
            return null;
        }
        return list.get(0);
    }
 
    // Nhiều type parameters
    public static <K, V> Map<K, V> createMap(K key, V value) {
        Map<K, V> map = new HashMap<>();
        map.put(key, value);
        return map;
    }
}
 
// Sử dụng
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
String first = Util.getFirst(names); // Type inference
 
Map<String, Integer> map = Util.createMap("age", 25);

Type Inference

Compiler của Java thường có thể suy ra type parameters:

// Explicit type argument (dài dòng)
List<String> names = Util.<String>getFirst(someList);
 
// Type inference (ưu tiên)
List<String> names = Util.getFirst(someList); // Compiler suy ra <String>

Quy Ước Đặt Tên Type Parameter

Java có các quy ước đã được thiết lập để đặt tên type parameters:

  • T - Type (mục đích chung)
  • E - Element (sử dụng trong collections)
  • K - Key (trong maps)
  • V - Value (trong maps)
  • N - Number
  • S, U, V - Kiểu thứ 2, 3, 4
public class Cache<K, V> { /* ... */ }
public interface List<E> { /* ... */ }
public class Result<T, E extends Exception> { /* ... */ }

Sử dụng tên mô tả khi các ký tự đơn không rõ ràng:

public class ServiceResponse<ResponseType, ErrorType> { /* ... */ }

Bounded Type Parameters

Đôi khi bạn cần hạn chế các kiểu có thể được sử dụng với generics.

Upper Bounds (extends)

Hạn chế các kiểu thành subclasses của một class hoặc interface cụ thể:

// T phải là Number hoặc subclass của nó
public class NumberBox<T extends Number> {
    private T value;
 
    public NumberBox(T value) {
        this.value = value;
    }
 
    public double doubleValue() {
        return value.doubleValue(); // An toàn - Number có method này
    }
}
 
// Sử dụng
NumberBox<Integer> intBox = new NumberBox<>(42);
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
// NumberBox<String> stringBox = new NumberBox<>("text"); // Lỗi biên dịch!

Multiple Bounds

Một type parameter có thể có nhiều bounds (một class, nhiều interfaces):

// T phải extend Number VÀ implement Comparable
public class SortedNumberBox<T extends Number & Comparable<T>> {
    private T value;
 
    public boolean isGreaterThan(T other) {
        return value.compareTo(other) > 0; // Comparable method
    }
 
    public double asDouble() {
        return value.doubleValue(); // Number method
    }
}
 
// Sử dụng
SortedNumberBox<Integer> box = new SortedNumberBox<>(); // OK
SortedNumberBox<BigDecimal> decimalBox = new SortedNumberBox<>(); // OK

Quy tắc cú pháp: Class phải đứng đầu, sau đó là interfaces: <T extends ClassType & Interface1 & Interface2>

Wildcards: Ký Tự ?

Wildcards đại diện cho các kiểu không xác định trong method parameters hoặc fields.

Unbounded Wildcard (?)

Chấp nhận bất kỳ kiểu nào:

public static void printList(List<?> list) {
    for (Object elem : list) {
        System.out.println(elem);
    }
}
 
// Hoạt động với bất kỳ list nào
printList(Arrays.asList(1, 2, 3));
printList(Arrays.asList("A", "B", "C"));

Hạn chế: Bạn không thể thêm elements (ngoại trừ null) vì kiểu không xác định:

public static void addToList(List<?> list) {
    // list.add("text"); // Lỗi biên dịch!
    // list.add(123);    // Lỗi biên dịch!
    list.add(null);      // Chỉ null được phép
}

Upper Bounded Wildcard (? extends T)

Chấp nhận kiểu T hoặc bất kỳ subtype nào của T (producer/read-only):

// Chấp nhận List<Number>, List<Integer>, List<Double>, etc.
public static double sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number num : numbers) {
        total += num.doubleValue(); // An toàn - tất cả đều là Numbers
    }
    return total;
}
 
// Sử dụng
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
 
double sumInts = sum(ints);       // OK
double sumDoubles = sum(doubles); // OK

Use case: Khi bạn muốn đọc từ một generic type.

Hạn chế: Bạn không thể thêm elements (ngoại trừ null):

public static void addNumber(List<? extends Number> numbers) {
    // numbers.add(123);       // Lỗi biên dịch!
    // numbers.add(3.14);      // Lỗi biên dịch!
    Number n = numbers.get(0); // Đọc thì OK
}

Lower Bounded Wildcard (? super T)

Chấp nhận kiểu T hoặc bất kỳ supertype nào của T (consumer/write-only):

// Chấp nhận List<Integer>, List<Number>, List<Object>
public static void addIntegers(List<? super Integer> list) {
    list.add(1);    // OK - Integer được chấp nhận
    list.add(2);    // OK
    list.add(3);    // OK
}
 
// Sử dụng
List<Integer> ints = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
 
addIntegers(ints);    // OK
addIntegers(numbers); // OK
addIntegers(objects); // OK

Use case: Khi bạn muốn ghi vào một generic type.

Hạn chế: Bạn chỉ có thể đọc như Object:

public static void processList(List<? super Integer> list) {
    list.add(42); // Ghi thì OK
    
    // Integer val = list.get(0); // Lỗi biên dịch!
    Object val = list.get(0);     // Chỉ có thể đọc như Object
}

Nguyên Tắc PECS: Producer Extends, Consumer Super

Ghi nhớ: PECS (Producer Extends, Consumer Super)

  • Producer (đọc): Sử dụng ? extends T
  • Consumer (ghi): Sử dụng ? super T
// Producer - sao chép TỪ source (đọc)
public static <T> void copy(
    List<? extends T> source,      // Producer: extends
    List<? super T> destination    // Consumer: super
) {
    for (T item : source) {
        destination.add(item);
    }
}
 
// Sử dụng
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Number> numbers = new ArrayList<>();
copy(integers, numbers); // OK - Integer extends Number

Ví dụ thực tế từ Collections:

public static <T> void copy(
    List<? super T> dest,
    List<? extends T> src
) {
    // Implementation
}

Type Erasure

Type erasure là cơ chế của Java để implement generics trong khi vẫn duy trì bytecode compatibility với code trước generics.

Điều Gì Xảy Ra Tại Compile Time

Compiler:

  1. Thay thế type parameters bằng bounds của chúng (hoặc Object nếu unbounded)
  2. Chèn type casts khi cần
  3. Tạo bridge methods để bảo toàn polymorphism

Ví Dụ

// Source code
public class Box<T> {
    private T value;
    
    public void set(T value) {
        this.value = value;
    }
    
    public T get() {
        return value;
    }
}
 
// Sau type erasure (tương đương bytecode)
public class Box {
    private Object value; // T được xóa thành Object
    
    public void set(Object value) {
        this.value = value;
    }
    
    public Object get() {
        return value;
    }
}
 
// Sử dụng với erasure
Box<String> box = new Box<>();
box.set("Hello");
String s = (String) box.get(); // Compiler chèn cast

Hệ Quả Của Type Erasure

1. Không có thông tin kiểu runtime:

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
 
// Cả hai có cùng class tại runtime
System.out.println(strings.getClass() == integers.getClass()); // true
 
// Không thể kiểm tra generic type tại runtime
// if (obj instanceof List<String>) { } // Lỗi biên dịch!
if (obj instanceof List<?>) { } // OK - unchecked

2. Không thể tạo generic arrays:

// Generic<String>[] array = new Generic<String>[10]; // Lỗi biên dịch!
 
// Workarounds:
List<String>[] array1 = (List<String>[]) new List<?>[10]; // Unchecked cast
List<String>[] array2 = new ArrayList[10]; // Raw type (không khuyến khích)
ArrayList<String>[] array3 = new ArrayList[10]; // Concrete type (tốt hơn)
 
// Tốt hơn: Sử dụng List<List<String>>
List<List<String>> listOfLists = new ArrayList<>();

3. Không thể khởi tạo type parameters:

public class Factory<T> {
    // public T create() {
    //     return new T(); // Lỗi biên dịch!
    // }
    
    // Workaround: Sử dụng Class<T>
    private Class<T> type;
    
    public Factory(Class<T> type) {
        this.type = type;
    }
    
    public T create() throws Exception {
        return type.getDeclaredConstructor().newInstance();
    }
}
 
// Sử dụng
Factory<User> factory = new Factory<>(User.class);
User user = factory.create();

4. Static fields không thể sử dụng type parameters:

public class Box<T> {
    // private static T staticValue; // Lỗi biên dịch!
    private static Object staticValue; // OK
}

Bridge Methods

Bridge methods là các synthetic methods được tạo ra bởi compiler để bảo toàn polymorphism sau type erasure.

public class Node<T> {
    public T data;
    
    public Node(T data) {
        this.data = data;
    }
    
    public void setData(T data) {
        this.data = data;
    }
}
 
public class MyNode extends Node<Integer> {
    public MyNode(Integer data) {
        super(data);
    }
    
    @Override
    public void setData(Integer data) {
        super.setData(data);
    }
}
 
// Sau type erasure, Node trở thành:
// public class Node {
//     public void setData(Object data) { ... }
// }
 
// MyNode có:
// public void setData(Integer data) { ... } // Method do user viết
// public void setData(Object data) { // Bridge method
//     this.setData((Integer) data);
// }

Bridge methods đảm bảo rằng MyNode.setData(Object) ghi đè đúng Node.setData(Object).

Generic Arrays (Hạn Chế)

Do type erasure, việc tạo generic array bị hạn chế:

// Những cái này không hoạt động:
// T[] array = new T[10]; // Error
// List<String>[] lists = new List<String>[10]; // Error
 
// Workarounds:
 
// 1. Sử dụng ArrayList thay thế
List<List<String>> lists = new ArrayList<>();
 
// 2. Sử dụng raw types (không khuyến khích)
List<String>[] lists = new List[10]; // Unchecked warning
 
// 3. Sử dụng @SuppressWarnings (khi cần thiết)
@SuppressWarnings("unchecked")
List<String>[] lists = (List<String>[]) new List<?>[10];
 
// 4. Cho các trường hợp đơn giản, sử dụng varargs
public static <T> List<T> asList(T... elements) {
    // T[] array được tạo internally
    return Arrays.asList(elements);
}

Ví Dụ Thực Tế: Generic DAO Pattern

Một pattern thực tế cho các ứng dụng Spring Boot:

// Generic DAO interface
public interface GenericDao<T, ID> {
    T save(T entity);
    Optional<T> findById(ID id);
    List<T> findAll();
    void delete(ID id);
    long count();
}
 
// Abstract base implementation
public abstract class AbstractJpaDao<T, ID> implements GenericDao<T, ID> {
    
    @PersistenceContext
    protected EntityManager entityManager;
    
    private final Class<T> entityClass;
    
    @SuppressWarnings("unchecked")
    protected AbstractJpaDao() {
        // Lấy actual type tại runtime qua reflection
        Type type = getClass().getGenericSuperclass();
        ParameterizedType paramType = (ParameterizedType) type;
        this.entityClass = (Class<T>) paramType.getActualTypeArguments()[0];
    }
    
    @Override
    public T save(T entity) {
        entityManager.persist(entity);
        return entity;
    }
    
    @Override
    public Optional<T> findById(ID id) {
        return Optional.ofNullable(entityManager.find(entityClass, id));
    }
    
    @Override
    public List<T> findAll() {
        CriteriaQuery<T> query = entityManager
            .getCriteriaBuilder()
            .createQuery(entityClass);
        query.select(query.from(entityClass));
        return entityManager.createQuery(query).getResultList();
    }
    
    @Override
    public void delete(ID id) {
        findById(id).ifPresent(entityManager::remove);
    }
    
    @Override
    public long count() {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Long> query = cb.createQuery(Long.class);
        query.select(cb.count(query.from(entityClass)));
        return entityManager.createQuery(query).getSingleResult();
    }
}
 
// Concrete implementation
@Repository
@Transactional
public class UserDao extends AbstractJpaDao<User, Long> {
    // Kế thừa tất cả CRUD methods
    
    // Thêm custom queries
    public List<User> findByEmail(String email) {
        return entityManager
            .createQuery("SELECT u FROM User u WHERE u.email = :email", User.class)
            .setParameter("email", email)
            .getResultList();
    }
}
 
// Sử dụng trong service
@Service
public class UserService {
    private final UserDao userDao;
    
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
    
    public User createUser(User user) {
        return userDao.save(user);
    }
    
    public Optional<User> getUserById(Long id) {
        return userDao.findById(id);
    }
    
    public List<User> findByEmail(String email) {
        return userDao.findByEmail(email);
    }
}

Best Practices

1. Ưu Tiên Generic Types Hơn Raw Types

// Không tốt - raw type (không có type safety)
List list = new ArrayList();
list.add("text");
list.add(123); // Ối!
 
// Tốt - generic type
List<String> list = new ArrayList<>();
list.add("text");
// list.add(123); // Lỗi biên dịch

2. Sử Dụng Wildcards Cho Tính Linh Hoạt API

// Quá hạn chế - chỉ chấp nhận List<String>
public void process(List<String> list) { }
 
// Linh hoạt hơn - chấp nhận bất kỳ list nào
public void process(List<?> list) { }
 
// Tốt nhất - linh hoạt với type safety
public <T> void process(List<T> list) { }

3. Tránh Generic Exceptions

// Không tốt - không thể catch generic exceptions
// public class GenericException<T> extends Exception { }
 
// Tốt - sử dụng concrete exception types
public class UserNotFoundException extends RuntimeException { }

4. Tài Liệu Hóa Type Parameter Constraints

/**
 * Cache để lưu trữ các cặp key-value.
 * 
 * @param <K> kiểu của keys (phải immutable và có equals/hashCode đúng)
 * @param <V> kiểu của cached values
 */
public class Cache<K, V> {
    // Implementation
}

5. Sử Dụng Bounded Wildcards Cho API Parameters

// Tốt - caller có thể truyền List<Integer>, List<Double>, etc.
public double sum(List<? extends Number> numbers) {
    return numbers.stream()
        .mapToDouble(Number::doubleValue)
        .sum();
}

6. Ưu Tiên Lists Hơn Arrays Cho Generics

// Có vấn đề - arrays và generics không hợp nhau tốt
// List<String>[] arrayOfLists = new List<String>[10]; // Lỗi biên dịch
 
// Tốt hơn - sử dụng List<List<String>>
List<List<String>> listOfLists = new ArrayList<>();

7. Sử Dụng @SafeVarargs Cho Generic Varargs

// Warning nếu không có @SafeVarargs
// @SafeVarargs // Loại bỏ warning
public static <T> List<T> asList(T... elements) {
    List<T> list = new ArrayList<>();
    for (T element : elements) {
        list.add(element);
    }
    return list;
}

Những Lỗi Thường Gặp

1. Trộn Lẫn Raw Types Và Generics

// Đừng làm thế này!
List<String> strings = new ArrayList<>();
List raw = strings; // Raw type assignment
raw.add(123); // Heap pollution!
String s = strings.get(0); // ClassCastException tại runtime!

2. Giả Định Thông Tin Kiểu Tại Runtime

public <T> void process(T value) {
    // if (T == String.class) { } // Lỗi biên dịch!
    // if (value instanceof T) { } // Lỗi biên dịch!
}
 
// Giải pháp: Truyền Class<T>
public <T> void process(T value, Class<T> type) {
    if (type == String.class) {
        // OK
    }
}

3. Overloading Với Erasure Conflicts

// Cả hai xóa thành cùng signature!
// public void method(List<String> list) { }
// public void method(List<Integer> list) { } // Lỗi biên dịch!
 
// Giải pháp: Sử dụng tên method khác hoặc parameters khác
public void methodForStrings(List<String> list) { }
public void methodForIntegers(List<Integer> list) { }

Hạn Chế Và Workarounds

Hạn Chế: Không Có Primitive Type Parameters

// List<int> numbers = new ArrayList<>(); // Lỗi biên dịch!
List<Integer> numbers = new ArrayList<>(); // Sử dụng wrapper

Cho code quan trọng về performance, sử dụng specialized collections:

  • IntStream, LongStream, DoubleStream
  • Eclipse Collections' IntList, LongSet, etc.
  • Trove collections

Hạn Chế: Không Thể Tạo Generic Exceptions

// class MyException<T> extends Exception { } // Không được phép
 
// Workaround: Sử dụng composition
class DataException extends Exception {
    private final Object failedData;
    
    public <T> DataException(T data) {
        this.failedData = data;
    }
}

Kết Nối Với Spring Boot (Phase 2)

Generics có mặt ở khắp nơi trong Spring Boot:

1. Repository Layer:

public interface JpaRepository<T, ID> extends Repository<T, ID> {
    // Spring Data sử dụng generics rộng rãi
}
 
public interface UserRepository extends JpaRepository<User, Long> {
    // Kế thừa tất cả CRUD methods với type safety
}

2. REST Controllers:

@GetMapping
public ResponseEntity<List<User>> getUsers() {
    return ResponseEntity.ok(userService.findAll());
}
 
@PostMapping
public ResponseEntity<ApiResponse<User>> createUser(@RequestBody User user) {
    return ResponseEntity.ok(ApiResponse.success(userService.save(user)));
}

3. Service Layer:

public interface GenericService<T, ID> {
    T save(T entity);
    Optional<T> findById(ID id);
    List<T> findAll();
}

4. DTO Mappers:

public interface Mapper<E, D> {
    D toDto(E entity);
    E toEntity(D dto);
}

Kết Luận

Java generics cung cấp type safety tại compile-time và khả năng tái sử dụng code mà không ảnh hưởng đến performance. Những điểm chính:

  • Sử dụng generics cho type-safe collections và APIs
  • Hiểu nguyên tắc PECS cho wildcards
  • Nhận thức về các hạn chế của type erasure
  • Tuân theo quy ước đặt tên (T, E, K, V)
  • Tận dụng bounded types cho constrained generics
  • Áp dụng generic patterns trong Spring Boot (repositories, services, DTOs)

Làm chủ generics là điều cần thiết để viết các ứng dụng Java robust và maintainable. Khi bạn xây dựng các dự án Spring Boot, bạn sẽ sử dụng generic types liên tục—hiểu sâu về chúng giúp bạn trở thành một developer hiệu quả hơn.

Các Bước Tiếp Theo:

  • Khám phá Bắt Đầu Với Spring Boot để áp dụng generics trong các dự án thực tế
  • Nghiên cứu generic repository implementations của Spring Data
  • Thực hành với generic DAO và service patterns

Tài Nguyên:


Thuộc series Lộ Trình Học Java

Quay lại: Giai Đoạn 2: Lập Trình Hướng Đối Tượng

Các Deep Dive Liên Quan:

Bước Tiếp Theo: Bắt Đầu Với Spring Boot

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.