Generic Classes và Methods
Sau bài này, bạn sẽ:
- Tạo được generic classes với một hoặc nhiều type parameters
- Hiểu và áp dụng naming conventions cho type parameters (T, E, K, V, N)
- Viết được generic methods trong cả generic và non-generic classes
- Sử dụng generic constructors để tạo objects linh hoạt hơn
- Implement các cấu trúc dữ liệu generic như Box, Pair, Stack
Bài trước: Giới thiệu Generics — Đã học vấn đề của raw types và lợi ích của Generics. Bài này sẽ hướng dẫn tạo generic classes và methods của riêng bạn.
Generic Class là gì?
Generic Class là class có một hoặc nhiều type parameters - cho phép class hoạt động với nhiều kiểu dữ liệu khác nhau mà vẫn đảm bảo type safety.
Cú pháp cơ bản
// Generic class với một type parameter
class ClassName<T> {
private T data;
public void setData(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
Type parameter (như T) là placeholder cho kiểu dữ liệu thực tế sẽ được chỉ định khi tạo instance.
Type Parameter Naming Conventions
Java có quy ước đặt tên cho type parameters:
| Convention | Ý nghĩa | Sử dụng | Ví dụ |
|---|---|---|---|
| T | Type | Generic type chung | Box<T>, Optional<T> |
| E | Element | Element trong collection | List<E>, Set<E> |
| K | Key | Key trong map | Map<K,V> |
| V | Value | Value trong map | Map<K,V> |
| N | Number | Numeric types | Calculator<N> |
| S, U, V | 2nd, 3rd, 4th types | Multiple parameters | Triple<S,U,V> |
Luôn dùng chữ IN HOA cho type parameters để phân biệt với class names thông thường!
Ví dụ: Generic Box Class
/**
* Generic Box có thể chứa bất kỳ kiểu dữ liệu nào
*/
public class Box<T> {
private T content;
public Box() {
}
public Box(T content) {
this.content = content;
}
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public boolean isEmpty() {
return content == null;
}
@Override
public String toString() {
return "Box[" + content + "]";
}
}
Sử dụng Generic Box
public class BoxDemo {
public static void main(String[] args) {
// Box chứa String
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello Generics");
String str = stringBox.getContent(); // Không cần casting
System.out.println(str); // Hello Generics
// Box chứa Integer
Box<Integer> intBox = new Box<>(42);
Integer num = intBox.getContent();
System.out.println(num * 2); // 84
// Box chứa custom object
Box<Person> personBox = new Box<>(new Person("Alice", 25));
Person person = personBox.getContent();
System.out.println(person.getName()); // Alice
// Type safety
// stringBox.setContent(123); // ❌ Compile error
}
}
Một class Box<T> có thể dùng cho mọi kiểu dữ liệu thay vì phải viết StringBox, IntegerBox, PersonBox...
Invariance: Tại sao Box<Object> ≠ Box<String>?
Biểu đồ Invariance
Giải thích: Dù String extends Object, nhưng Box<String> KHÔNG extends Box<Object>!
Một điều ngạc nhiên: Generic types là invariant (bất biến)!
// Integer là subtype của Object
Integer num = 42;
Object obj = num; // ✅ OK - Integer là Object
// NHƯNG: Box<Integer> KHÔNG phải subtype của Box<Object>!
Box<Integer> intBox = new Box<>(42);
Box<Object> objBox = intBox; // ❌ Compile error!
// Error: incompatible types: Box<Integer> cannot be converted to Box<Object>
Tại sao? (WHY?)
// Giả sử cho phép: Box<Integer> → Box<Object>
Box<Integer> intBox = new Box<>(42);
Box<Object> objBox = intBox; // Giả sử OK
// Bây giờ objBox reference đến Box<Integer>
objBox.setContent("String"); // Box<Object> nhận String
// → Box<Integer> bây giờ chứa String! 💥 Type safety bị phá vỡ!
Integer value = intBox.getContent(); // Lấy String ra như Integer → CRASH!
OCP Exam Trap: Invariance phá vỡ "is-a" relationship!
Box<Integer> intBox = new Box<>(10);
Box<Number> numBox = intBox; // ❌ Compile error!
// Dù Integer extends Number, Box<Integer> KHÔNG extends Box<Number>
// Tương tự với List:
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // ❌ Error!
// Fix: Dùng wildcards (học bài sau)
List<? extends Object> objects = strings; // ✅ OK
Metaphor (Ẩn dụ - Head First Java style):
Hộp táo không phải hộp trái cây
- Táo là trái cây (Apple is-a Fruit) ✅
- Nhưng hộp táo KHÔNG PHẢI hộp trái cây!
- Tại sao? Vì nếu cho phép:
- Bạn cho tôi "hộp táo" nhưng nói là "hộp trái cây"
- Tôi bỏ cam vào "hộp trái cây"
- Bạn mở "hộp táo" ra thấy cam → sai rồi! 💥
Kết luận: Generic types phải exact match (trùng khớp chính xác), không chấp nhận subtype!
Generic Class với nhiều Type Parameters
/**
* Pair lưu trữ cặp key-value
*/
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;
}
public void setKey(K key) {
this.key = key;
}
public void setValue(V value) {
this.value = value;
}
@Override
public String toString() {
return key + " = " + value;
}
}
Sử dụng Pair
public class PairDemo {
public static void main(String[] args) {
// Pair<String, Integer>
Pair<String, Integer> ageEntry = new Pair<>("Alice", 25);
System.out.println(ageEntry); // Alice = 25
// Pair<Integer, String>
Pair<Integer, String> idToName = new Pair<>(101, "Bob");
System.out.println("ID: " + idToName.getKey());
System.out.println("Name: " + idToName.getValue());
// Pair<String, List<String>>
List<String> courses = Arrays.asList("Java", "Python", "JavaScript");
Pair<String, List<String>> studentCourses =
new Pair<>("Charlie", courses);
System.out.println(studentCourses.getKey() + " studies: " +
studentCourses.getValue());
}
}
Generic Triple Class
/**
* Triple lưu trữ 3 giá trị có thể khác kiểu
*/
public class Triple<A, B, C> {
private A first;
private B second;
private C third;
public Triple(A first, B second, C third) {
this.first = first;
this.second = second;
this.third = third;
}
// Getters và setters
public A getFirst() { return first; }
public B getSecond() { return second; }
public C getThird() { return third; }
@Override
public String toString() {
return "(" + first + ", " + second + ", " + third + ")";
}
}
// Sử dụng
Triple<String, Integer, Boolean> student =
new Triple<>("Alice", 25, true);
System.out.println(student); // (Alice, 25, true)
Generic Methods
Generic method là method có type parameters riêng, độc lập với class.
Cú pháp Generic Method
public <T> T methodName(T parameter) {
// method body
}
Type parameter <T> phải đứng trước return type!
// ✅ Đúng
public <T> T method(T param)
// ❌ Sai
public T <T> method(T param)
Ví dụ: Generic Method cơ bản
public class GenericMethods {
/**
* Generic method in array
*/
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
/**
* Generic method swap elements
*/
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
/**
* Generic method tìm element trong array
*/
public static <T> boolean contains(T[] array, T element) {
for (T item : array) {
if (item.equals(element)) {
return true;
}
}
return false;
}
public static void main(String[] args) {
// Integer array
Integer[] numbers = {1, 2, 3, 4, 5};
printArray(numbers); // 1 2 3 4 5
swap(numbers, 0, 4);
printArray(numbers); // 5 2 3 4 1
// String array
String[] words = {"Java", "Python", "JavaScript"};
printArray(words); // Java Python JavaScript
System.out.println(contains(words, "Java")); // true
System.out.println(contains(words, "C++")); // false
}
}
Generic Method Type Inference Flow
Generic Method với return type
public class ArrayUtils {
/**
* Lấy phần tử đầu tiên của array
*/
public static <T> T getFirst(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[0];
}
/**
* Lấy phần tử cuối cùng của array
*/
public static <T> T getLast(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[array.length - 1];
}
/**
* Tạo array từ varargs
*/
@SafeVarargs
public static <T> List<T> createList(T... elements) {
List<T> list = new ArrayList<>();
for (T element : elements) {
list.add(element);
}
return list;
}
}
// Sử dụng
String[] names = {"Alice", "Bob", "Charlie"};
String first = ArrayUtils.getFirst(names); // Alice
String last = ArrayUtils.getLast(names); // Charlie
List<Integer> numbers = ArrayUtils.createList(1, 2, 3, 4, 5);
System.out.println(numbers); // [1, 2, 3, 4, 5]
Varargs + Generics = Heap Pollution
Varargs với generics tạo ra heap pollution (ô nhiễm heap) — một nguy cơ tiềm ẩn!
// Varargs với generic type → Warning!
public static <T> void addToList(List<T> list, T... elements) {
// Warning: Possible heap pollution from parameterized vararg type
for (T element : elements) {
list.add(element);
}
}
Tại sao có heap pollution?
// Varargs được compile thành array
<T> void method(T... args) // Biến thành → void method(T[] args)
// Nhưng generic arrays KHÔNG an toàn!
// Vì type erasure: T[] thành Object[]
Ví dụ nguy hiểm:
public class HeapPollutionExample {
// ⚠️ Unsafe varargs method
static void dangerousMethod(List<String>... stringLists) {
Object[] array = stringLists; // List<String>[] → Object[]
List<Integer> tmpList = Arrays.asList(42);
array[0] = tmpList; // ⚠️ Pollutes heap! List<Integer> vào List<String>[]
String s = stringLists[0].get(0); // 💥 ClassCastException!
// Expected String, got Integer
}
public static void main(String[] args) {
List<String> list1 = Arrays.asList("A", "B");
dangerousMethod(list1); // CRASH!
}
}
@SafeVarargs: Khi nào an toàn?
// ✅ SAFE: Chỉ đọc elements, không modify array
@SafeVarargs
public static <T> List<T> asList(T... elements) {
List<T> list = new ArrayList<>();
for (T elem : elements) {
list.add(elem);
}
return list;
}
// ✅ SAFE: Không leak varargs array ra ngoài
@SafeVarargs
public static <T> void printAll(T... elements) {
for (T elem : elements) {
System.out.println(elem);
}
}
// ❌ UNSAFE: Return varargs array
static <T> T[] returnArray(T... elements) {
return elements; // Heap pollution! Caller nhận Object[]
}
// ❌ UNSAFE: Store varargs array in field
static class Holder<T> {
T[] array;
@SafeVarargs // ❌ SẼ SAI NẾU DÙNG @SafeVarargs!
Holder(T... elements) {
this.array = elements; // Leak ra ngoài → unsafe!
}
}
OCP Exam: Khi nào method với varargs generics an toàn?
- ✅ Không modify varargs array
- ✅ Không return hoặc leak array ra ngoài
- ✅ Chỉ đọc elements từ varargs
// Câu hỏi: Method nào KHÔNG nên có @SafeVarargs?
@SafeVarargs
static <T> T[] toArray(T... elements) { // ❌ UNSAFE!
return elements; // Leak array → caller nhận Object[]
}
String[] arr = toArray("A", "B"); // 💥 ClassCastException!
// Expected String[], got Object[]
@SafeVarargs = "Tôi hứa không làm xấu varargs array"
3 điều KHÔNG được làm:
- KHÔNG modify array (
array[i] = ...) - KHÔNG return array
- KHÔNG store array vào field/collection
Chỉ READ elements → Safe!
Static vs Instance Type Parameter
Static Methods không thể access Class Type Parameter
public class Box<T> {
private T content;
// ❌ STATIC method KHÔNG thể dùng T của class!
// public static T createDefault() { // Compile error!
// return null;
// }
// Tại sao? Static belongs to CLASS, không có instance
// T chỉ biết khi có instance: Box<String>, Box<Integer>
// Static method gọi: Box.createDefault() → T là gì? Không biết!
// ✅ OK: Static method với own type parameter
public static <E> Box<E> create(E content) {
return new Box<>(content);
}
// ✅ OK: Instance method dùng T
public T getContent() {
return content;
}
}
// Sử dụng
Box<String> box = Box.<String>create("Hello"); // Explicit type
Box<String> box2 = Box.create("Hello"); // Type inference
"A static member class, interface, or method of a generic class or interface cannot refer to type variables of any enclosing class or interface."
Static context không có access đến instance type parameters!
Generic Interfaces
Generic interfaces hoạt động như generic classes:
// Generic interface
interface Container<T> {
void add(T item);
T get();
boolean isEmpty();
}
// Implement với concrete type
class StringContainer implements Container<String> {
private String item;
@Override
public void add(String item) {
this.item = item;
}
@Override
public String get() {
return item;
}
@Override
public boolean isEmpty() {
return item == null;
}
}
// Implement với generic type parameter
class GenericContainer<E> implements Container<E> {
private E item;
@Override
public void add(E item) {
this.item = item;
}
@Override
public E get() {
return item;
}
@Override
public boolean isEmpty() {
return item == null;
}
}
Ví dụ thực tế: Comparable interface
// Comparable<T> trong JDK
public interface Comparable<T> {
int compareTo(T o);
}
// Implement
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
// Sử dụng
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Bob", 30);
System.out.println(p1.compareTo(p2)); // -1 (p1 < p2)
Constructor với Type Parameter
Constructor có thể có type parameter riêng (khác class type parameter):
public class AdvancedBox<T> {
private T content;
// Constructor thông thường - dùng T của class
public AdvancedBox(T content) {
this.content = content;
}
// Generic constructor với type parameter riêng!
public <U> AdvancedBox(U initialValue, Function<U, T> converter) {
this.content = converter.apply(initialValue);
}
public T getContent() {
return content;
}
}
// Sử dụng
// Constructor 1: Direct
AdvancedBox<String> box1 = new AdvancedBox<>("Hello");
// Constructor 2: Với converter
AdvancedBox<String> box2 = new AdvancedBox<>(
42,
num -> "Number: " + num // Integer → String
);
System.out.println(box2.getContent()); // "Number: 42"
// Constructor 2: Complex conversion
AdvancedBox<Integer> box3 = new AdvancedBox<>(
"12345",
str -> str.length() // String → Integer
);
System.out.println(box3.getContent()); // 5
Method Type Inference Rules
Compiler tự động infer type parameters trong nhiều trường hợp:
public class TypeInference {
// Generic method
public static <T> List<T> createList(T first, T second) {
List<T> list = new ArrayList<>();
list.add(first);
list.add(second);
return list;
}
public static void main(String[] args) {
// ✅ Type inference tự động
List<String> strings = createList("A", "B");
// Compiler infers T = String
List<Integer> numbers = createList(1, 2);
// Compiler infers T = Integer
// ⚠️ Conflicting types → infer common supertype
List<Number> mixed = createList(1, 2.5);
// T = Number (common supertype of Integer và Double)
// ❌ Không có common type → error
// List<Object> objects = createList("A", 1);
// Error: incompatible types
// ✅ Fix: Explicit type argument
List<Object> objects = TypeInference.<Object>createList("A", 1);
// Explicitly: T = Object
}
}
Khi cần explicit type arguments:
// Trường hợp phức tạp: Compiler không infer được
Collections.<String>emptyList(); // Explicit type
// Method chaining mơ hồ
List<String> result = Collections.<String>singletonList("A")
.stream()
.collect(Collectors.toList());
// Ambiguous context
Collections.<Integer>sort(list); // Explicit để rõ ràng
OCP Exam: Conflicting type inference!
<T> T mystery(T a, T b) {
return a;
}
String s = mystery("Hello", 42); // ❌ Error!
// T không thể vừa String vừa Integer
Object o = mystery("Hello", 42); // ✅ OK - infer T = Object
// Explicit type
String s = mystery.<String>("Hello", "World"); // ✅ OK
Generic Methods trong Non-Generic Class
Generic methods có thể tồn tại trong non-generic class:
/**
* Non-generic class với generic methods
*/
public class Utilities {
/**
* Generic method để reverse list
*/
public static <T> void reverse(List<T> list) {
int size = list.size();
for (int i = 0; i < size / 2; i++) {
T temp = list.get(i);
list.set(i, list.get(size - 1 - i));
list.set(size - 1 - i, temp);
}
}
/**
* Generic method để fill collection
*/
public static <T> void fill(List<T> list, T value, int count) {
for (int i = 0; i < count; i++) {
list.add(value);
}
}
/**
* Generic method để tạo pair
*/
public static <K, V> Pair<K, V> makePair(K key, V value) {
return new Pair<>(key, value);
}
}
// Sử dụng
List<String> names = new ArrayList<>(Arrays.asList("A", "B", "C"));
Utilities.reverse(names);
System.out.println(names); // [C, B, A]
List<Integer> numbers = new ArrayList<>();
Utilities.fill(numbers, 0, 5);
System.out.println(numbers); // [0, 0, 0, 0, 0]
Pair<String, Integer> pair = Utilities.makePair("Age", 25);
Generic Constructors
Constructor cũng có thể là generic:
public class GenericConstructorDemo {
/**
* Class với generic constructor
*/
static class Container {
private Object content;
// Generic constructor
public <T> Container(T content) {
this.content = content;
}
public Object getContent() {
return content;
}
}
/**
* Generic class với generic constructor khác type parameter
*/
static class AdvancedBox<T> {
private T content;
// Constructor thông thường
public AdvancedBox(T content) {
this.content = content;
}
// Generic constructor với type khác
public <U> AdvancedBox(U initialValue, Function<U, T> converter) {
this.content = converter.apply(initialValue);
}
public T getContent() {
return content;
}
}
public static void main(String[] args) {
// Sử dụng Container với generic constructor
Container c1 = new Container("Hello");
Container c2 = new Container(123);
// Sử dụng AdvancedBox
AdvancedBox<String> box1 = new AdvancedBox<>("Direct");
// Constructor với converter
AdvancedBox<String> box2 = new AdvancedBox<>(
123,
num -> "Number: " + num
);
System.out.println(box2.getContent()); // Number: 123
}
}
Ví dụ thực tế: Generic Stack
/**
* Generic Stack implementation
*/
public class Stack<T> {
private List<T> elements;
private int maxSize;
public Stack() {
this(Integer.MAX_VALUE);
}
public Stack(int maxSize) {
this.elements = new ArrayList<>();
this.maxSize = maxSize;
}
/**
* Push element lên stack
*/
public void push(T element) {
if (elements.size() >= maxSize) {
throw new IllegalStateException("Stack is full");
}
elements.add(element);
}
/**
* Pop element từ stack
*/
public T pop() {
if (isEmpty()) {
throw new IllegalStateException("Stack is empty");
}
return elements.remove(elements.size() - 1);
}
/**
* Peek element trên đỉnh stack
*/
public T peek() {
if (isEmpty()) {
throw new IllegalStateException("Stack is empty");
}
return elements.get(elements.size() - 1);
}
public boolean isEmpty() {
return elements.isEmpty();
}
public int size() {
return elements.size();
}
public void clear() {
elements.clear();
}
@Override
public String toString() {
return "Stack" + elements;
}
}
Sử dụng Generic Stack
public class StackDemo {
public static void main(String[] args) {
// Stack of Integers
Stack<Integer> intStack = new Stack<>();
intStack.push(10);
intStack.push(20);
intStack.push(30);
System.out.println("Stack: " + intStack);
System.out.println("Pop: " + intStack.pop()); // 30
System.out.println("Peek: " + intStack.peek()); // 20
// Stack of Strings
Stack<String> stringStack = new Stack<>(3);
stringStack.push("First");
stringStack.push("Second");
stringStack.push("Third");
// stringStack.push("Fourth"); // Exception: Stack is full
while (!stringStack.isEmpty()) {
System.out.println(stringStack.pop());
}
// Output: Third, Second, First
}
}
Ví dụ thực tế: Generic Utility Methods
public class CollectionUtils {
/**
* Tìm max element trong collection
*/
public static <T extends Comparable<T>> T findMax(Collection<T> collection) {
if (collection == null || collection.isEmpty()) {
throw new IllegalArgumentException("Collection is empty");
}
T max = null;
for (T element : collection) {
if (max == null || element.compareTo(max) > 0) {
max = element;
}
}
return max;
}
/**
* Count occurrences of element
*/
public static <T> int countOccurrences(Collection<T> collection, T target) {
int count = 0;
for (T element : collection) {
if (element.equals(target)) {
count++;
}
}
return count;
}
/**
* Filter collection by predicate
*/
public static <T> List<T> filter(Collection<T> collection,
Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T element : collection) {
if (predicate.test(element)) {
result.add(element);
}
}
return result;
}
/**
* Transform collection
*/
public static <T, R> List<R> map(Collection<T> collection,
Function<T, R> mapper) {
List<R> result = new ArrayList<>();
for (T element : collection) {
result.add(mapper.apply(element));
}
return result;
}
}
// Sử dụng
List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 3);
Integer max = CollectionUtils.findMax(numbers); // 9
List<String> words = Arrays.asList("java", "python", "java", "c++");
int javaCount = CollectionUtils.countOccurrences(words, "java"); // 2
List<Integer> evenNumbers = CollectionUtils.filter(
numbers,
n -> n % 2 == 0
); // [2, 8]
List<String> numberStrings = CollectionUtils.map(
numbers,
n -> "Number: " + n
); // ["Number: 5", "Number: 2", ...]
Mermaid Diagram: Generic Class Compilation Flow
Giải thích flow:
- Source code có
Box<T>với type parameter - Compiler kiểm tra type safety tại compile time
- Type erasure xóa
<T>, thay bằngObject(hoặc bound) - Bytecode chỉ chứa raw type
BoxvớiObject - Runtime JVM không biết về
<String>hay<Integer>
OCP Traps Summary
1. Invariance trap:
Box<Integer> intBox = new Box<>(10);
Box<Number> numBox = intBox; // ❌ Error! Không có subtyping
2. Varargs unsafe:
<T> T[] toArray(T... elements) { return elements; } // ❌ Unsafe!
String[] arr = toArray("A", "B"); // 💥 ClassCastException!
3. Static method trap:
class Box<T> {
static T getValue() { return null; } // ❌ Error! Static không có T
}
4. Generic method inference conflict:
<T> T method(T a, T b) { return a; }
String s = method("Hello", 42); // ❌ Error! T conflict
Tóm tắt
| Khái niệm | Cú pháp | Ví dụ |
|---|---|---|
| Generic Class | class Name<T> | Box<T>, Pair<K,V> |
| Generic Method | <T> T method(T p) | <T> void print(T item) |
| Multiple Parameters | <K, V> | Pair<String, Integer> |
| Generic Constructor | <T> ClassName(T p) | <T> Box(T content) |
- Sử dụng naming conventions: T, E, K, V, N
- Generic methods cho logic không phụ thuộc vào class type
- Generic classes khi toàn bộ class hoạt động với type parameter
- Type safety: Generics giúp phát hiện lỗi tại compile time
- Code reuse: Viết một lần, dùng với mọi kiểu dữ liệu
Bài tập thực hành
Bài 1: Generic Repository
Tạo một generic Repository<T> với các methods CRUD cơ bản:
public class Repository<T> {
// TODO: Implement
// - add(T item)
// - remove(T item)
// - findById(int id) - giả sử T có getId()
// - findAll()
// - count()
}
Bài 2: Generic Pair Utilities
Viết các generic methods để làm việc với Pair:
// Swap key và value
public static <K, V> Pair<V, K> swapPair(Pair<K, V> pair)
// So sánh hai pairs
public static <K, V> boolean pairsEqual(Pair<K, V> p1, Pair<K, V> p2)
Bài 3: Generic Queue
Implement generic Queue với các operations:
enqueue(T element)dequeue()peek()isEmpty()size()
Trong bài tiếp theo, chúng ta sẽ học về Bounded Type Parameters - cách giới hạn type parameters!