Generics trong Java cung cấp một cách để định nghĩa các lớp, giao diện và phương thức có thể hoạt động trên nhiều kiểu dữ liệu khác nhau mà vẫn đảm bảo an toàn kiểu tại thời điểm biên dịch.
Ưu điểm của Generics
- An toàn kiểu tuyệt đối: Giúp ngăn chặn các lỗi kiểu dữ liệu xảy ra tại thời điểm chạy.
- Loại bỏ ép kiểu tường minh: Không cần thực hiện ép kiểu thủ công, làm cho mã nguồn sạch sẽ và dễ đọc hơn.
Lớp Generic (Generic Class)
Lớp Generic cho phép bạn định nghĩa một lớp mà các thành viên của nó có thể sử dụng một hoặc nhiều tham số kiểu.
Định nghĩa và Sử dụng
Khi định nghĩa một lớp generic, bạn sử dụng các dấu ngoặc nhọn <T> để chỉ định tham số kiểu. Tham số kiểu này sau đó có thể được sử dụng để xác định kiểu của các biến thành viên, tham số phương thức và kiểu trả về.
Ví dụ về Lớp Generic
// Định nghĩa một lớp Generic với tham số kiểu T
public class GenericContainer<T> {
private T data; // Biến thành viên có kiểu T
public void setData(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
Sử dụng Lớp Generic
// Tạo một đối tượng GenericContainer với kiểu String
GenericContainer<String> stringContainer = new GenericContainer<String>();
stringContainer.setData("Xin chào!");
String message = stringContainer.getData(); // Kiểu trả về là String, không cần ép kiểu
// Tạo một đối tượng GenericContainer với kiểu Integer
GenericContainer<Integer> integerContainer = new GenericContainer<Integer>();
integerContainer.setData(123);
Integer number = integerContainer.getData(); // Kiểu trả về là Integer
Lưu ý khi không chỉ định kiểu
Nếu bạn tạo một đối tượng của lớp generic mà không chỉ định rõ tham số kiểu, kiểu mặc định sẽ là Object.
// Không chỉ định kiểu, mặc định là Object
GenericContainer genericContainer = new GenericContainer();
genericContainer.setData("Dữ liệu bất kỳ");
// Phải ép kiểu về Object hoặc kiểu cụ thể
Object obj = genericContainer.getData();
Kế thừa trong Lớp Generic
Khi kế thừa từ lớp generic, có hai trường hợp:
- Lớp con cũng là lớp generic: Cả lớp cha và lớp con có thể có các tham số kiểu riêng, nhưng phải đảm bảo sự tương thích.
- Lớp con không phải lớp generic: Lớp con phải chỉ định rõ ràng kiểu cho tham số kiểu của lớp cha.
Trường hợp 1: Lớp con là lớp generic
// Lớp cha Generic
public class ParentGeneric<T> {
private T value;
public void setValue(T value) { this.value = value; }
public T getValue() { return value; }
}
// Lớp con kế thừa và cũng là generic
public class ChildGeneric<T, K, E> extends ParentGeneric<T> {
private K subValue1;
private E subValue2;
@Override
public void setValue(T value) { // Ghi đè phương thức từ lớp cha
super.setValue(value);
}
// Các phương thức getter/setter cho subValue1, subValue2
}
// Sử dụng
public class Main {
public static void main(String[] args) {
ChildGeneric<String, Integer, Double> child = new ChildGeneric<String, Integer, Double>();
child.setValue("Dữ liệu chuỗi");
System.out.println(child.getValue());
}
}
Trường hợp 2: Lớp con không phải lớp generic
// Lớp con không phải generic, chỉ định kiểu cho lớp cha
public class NonGenericChild extends ParentGeneric<String> {
@Override
public void setValue(String value) { // Ghi đè với kiểu cụ thể
super.setValue(value);
}
}
// Sử dụng
public class Main {
public static void main(String[] args) {
NonGenericChild child = new NonGenericChild();
child.setValue("Dữ liệu cụ thể");
System.out.println(child.getValue());
}
}
Giao diện Generic (Generic Interface)
Tương tự như lớp generic, giao diện generic cũng cho phép định nghĩa các phương thức với tham số kiểu.
Định nghĩa Giao diện Generic
// Giao diện Generic với tham số kiểu T
public interface GenericDataHandler<T> {
void process(T data);
T retrieve();
}
Các trường hợp triển khai
- Lớp triển khai là lớp generic: Lớp con có thể kế thừa tham số kiểu từ giao diện.
- Lớp triển khai không phải lớp generic: Lớp con phải chỉ định rõ ràng kiểu cho tham số kiểu của giao diện.
Trường hợp 1: Lớp triển khai là lớp generic
// Lớp triển khai là generic
public class GenericDataProcessor<T> implements GenericDataHandler<T> {
private T currentData;
@Override
public void process(T data) {
this.currentData = data;
System.out.println("Đã xử lý: " + data);
}
@Override
public T retrieve() {
return this.currentData;
}
}
Trường hợp 2: Lớp triển khai không phải lớp generic
// Lớp triển khai không phải generic, chỉ định kiểu String
public class StringDataProcessor implements GenericDataHandler<String> {
private String data;
@Override
public void process(String data) {
this.data = data;
System.out.println("Đã xử lý chuỗi: " + data);
}
@Override
public String retrieve() {
return this.data;
}
}
Sử dụng các lớp triển khai
// Sử dụng lớp StringDataProcessor (không phải generic)
StringDataProcessor stringProcessor = new StringDataProcessor();
stringProcessor.process("Dữ liệu chuỗi");
System.out.println("Lấy ra: " + stringProcessor.retrieve());
// Sử dụng lớp GenericDataProcessor (là generic)
GenericDataProcessor<Integer> integerProcessor = new GenericDataProcessor<Integer>();
integerProcessor.process(100);
System.out.println("Lấy ra: " + integerProcessor.retrieve());
Phương thức Generic (Generic Method)
Phương thức generic cho phép bạn khai báo một phương thức độc lập với kiểu của lớp chứa nó.
Phân biệt
- Nếu phương thức sử dụng tham số kiểu của lớp chứa nó, đó không phải là phương thức generic.
- Nếu phương thức khai báo tham số kiểu riêng của nó, đó là phương thức generic.
public class MethodExamples<T> {
// Phương thức này sử dụng tham số kiểu T của lớp, không phải phương thức generic
public T processData(T input) {
return input;
}
// Phương thức này là phương thức generic, khai báo tham số kiểu E riêng
public <E> E convertData(E input) {
return input;
}
}
Phương thức Generic với Varargs
Các phương thức generic hoàn toàn có thể sử dụng tham số biến đổi (varargs).
public class VarargsExample {
// Phương thức generic có thể nhận nhiều tham số kiểu K
public <K> void printElements(K... elements) {
for (K element : elements) {
System.out.print(element + " ");
}
System.out.println();
}
}
// Sử dụng
public class Main {
public static void main(String[] args) {
VarargsExample example = new VarargsExample();
example.printElements("A", "B", "C"); // In ra: A B C
example.printElements(1, 2, 3, 4); // In ra: 1 2 3 4
}
}
Wildcards (Ký tự đại diện)
Wildcards thường được sử dụng kết hợp với extends hoặc super để giới hạn kiểu dữ liệu mà một tham số generic có thể đại diện.
Giới hạn trên (Upper Bound)
Sử dụng ? extends T để chỉ định rằng kiểu có thể là T hoặc bất kỳ lớp con nào của T.
// Giả sử có lớp Box<T> tương tự GenericContainer ở trên
public class Box<T> {
private T item;
public void setItem(T item) { this.item = item; }
public T getItem() { return item; }
}
public class WildcardExample {
public static void main(String[] args) {
Box<Number> numberBox = new Box<Number>();
numberBox.setItem(10);
displayBoxContent(numberBox); // Hợp lệ
Box<Integer> integerBox = new Box<Integer>();
integerBox.setItem(20);
displayBoxContent(integerBox); // Hợp lệ vì Integer là con của Number
Box<String> stringBox = new Box<String>(); // Sẽ gây lỗi nếu gọi displayBoxContent
}
// Hàm này chấp nhận Box chứa Number hoặc bất kỳ lớp con nào của Number
public static void displayBoxContent(Box<? extends Number> box) {
System.out.println("Nội dung hộp: " + box.getItem());
// Không thể gọi box.setItem(...) với một giá trị cụ thể trừ khi là null
// vì kiểu không xác định chính xác tại thời điểm biên dịch
}
}
Giới hạn dưới (Lower Bound)
Sử dụng ? super T để chỉ định rằng kiểu có thể là T hoặc bất kỳ lớp cha nào của T.
public class WildcardLowerBound {
public static void main(String[] args) {
Box<Integer> intBox = new Box<Integer>();
intBox.setItem(5);
processBox(intBox); // Hợp lệ
Box<Number> numberBox = new Box<Number>();
numberBox.setItem(3.14);
processBox(numberBox); // Hợp lệ
}
// Hàm này chấp nhận Box chứa Integer hoặc bất kỳ lớp cha nào của Integer
public static void processBox(Box<? super Integer> box) {
// Có thể thêm Integer hoặc lớp con của Integer vào
// box.setItem(10); // Hợp lệ
// Không thể lấy ra và gán cho kiểu Integer vì có thể là lớp cha
Object obj = box.getItem(); // Phải lấy ra dưới dạng Object
System.out.println("Đã xử lý hộp: " + obj);
}
}
Sử dụng trong JDK
Các lớp như Comparator sử dụng wildcard để cho phép linh hoạt hơn trong việc so sánh các đối tượng.
Type Erasure (Xóa kiểu)
Generics trong Java được triển khai thông qua cơ chế "Type Erasure". Điều này có nghĩa là thông tin về tham số kiểu sẽ bị xóa trong quá trình biên dịch và chỉ còn lại kiểu cơ sở (thường là Object hoặc kiểu giới hạn trên).
- Không giới hạn: Tham số kiểu được thay thế bằng
Object. - Có giới hạn trên: Tham số kiểu được thay thế bằng kiểu giới hạn trên.
Bridge Methods: Khi implement giao diện generic mà lớp implement không phải là generic, một "bridge method" có thể được tạo ra để đảm bảo tương thích ngược. Phương thức này sẽ gọi một phương thức khác với kiểu cụ thể của lớp implement.
Generics và Mảng
Việc tạo mảng generic trực tiếp có thể gặp vấn đề. Thông thường, người ta sử dụng reflection với java.lang.reflect.Array.newInstance() để tạo mảng generic.
import java.lang.reflect.Array;
public class GenericArray<T> {
private T[] array;
// Sử dụng reflection để tạo mảng generic
@SuppressWarnings("unchecked")
public GenericArray(Class<T> componentType, int size) {
this.array = (T[]) Array.newInstance(componentType, size);
}
public void put(int index, T item) {
if (index >= 0 && index < array.length) {
array[index] = item;
}
}
public T get(int index) {
if (index >= 0 && index < array.length) {
return array[index];
}
return null;
}
public T[] getArray() {
return array;
}
}
// Sử dụng
public class Main {
public static void main(String[] args) {
GenericArray<String> stringArray = new GenericArray<String>(String.class, 5);
stringArray.put(0, "Apple");
stringArray.put(1, "Banana");
String[] fruits = stringArray.getArray();
for (String fruit : fruits) {
if (fruit != null) {
System.out.println(fruit);
}
}
}
}
Generics và Reflection
Sử dụng generic với Class có thể giúp xác định kiểu chính xác hơn khi làm việc với reflection.
public class Person {
// Một lớp đơn giản
}
public class ReflectionExample {
public static void main(String[] args) {
// Sử dụng generic Class để chỉ định kiểu Person
Class<Person> personClass = Person.class;
try {
Person person = personClass.getDeclaredConstructor().newInstance();
System.out.println("Đã tạo đối tượng Person thành công bằng generic Class.");
} catch (Exception e) {
e.printStackTrace();
}
// Sử dụng Class không generic
Class> rawClass = Person.class;
try {
Object obj = rawClass.getDeclaredConstructor().newInstance();
System.out.println("Đã tạo đối tượng Object thành công bằng raw Class.");
// obj là kiểu Object, cần ép kiểu nếu muốn sử dụng phương thức của Person
} catch (Exception e) {
e.printStackTrace();
}
}
}