Giới thiệu về kiểu tổng quát
Thuộc phần Lập trình hướng đối tượng trong hành trình Java của Coddy. Bài học 53 trên 87.
Hãy tưởng tượng bạn cần tạo một lớp lưu trữ giá trị và lấy lại giá trị đó sau này. Nếu không dùng kiểu tổng quát, bạn sẽ phải viết các lớp riêng cho từng kiểu dữ liệu: một lớp cho Integer, một lớp cho String, một lớp cho Double, v.v. Điều này khiến code bị lặp và gây khó khăn khi bảo trì.
Generics giải quyết vấn đề này bằng cách cho phép bạn viết các lớp và phương thức hoạt động với mọi kiểu dữ liệu mà vẫn đảm bảo an toàn kiểu. Bạn định nghĩa một chỗ giữ chỗ (gọi là tham số kiểu), rồi chỗ đó sẽ được thay bằng một kiểu dữ liệu thực tế khi bạn dùng lớp:
// Không dùng generics: dùng Object, cần ép kiểu
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0); // Cần ép kiểu thủ công
// Dùng generics: an toàn về kiểu dữ liệu, không cần ép kiểu
List<String> list = new ArrayList<String>();
list.add("Hello");
String s = list.get(0); // Không cần ép kiểuDấu ngoặc nhọn <T> biểu thị một tham số kiểu dữ liệu. Khi viết List<String>, bạn đang cho trình biên dịch biết rằng danh sách này chỉ chứa các chuỗi. Nếu bạn thử thêm một số nguyên, trình biên dịch sẽ phát hiện lỗi ngay lập tức thay vì để chương trình gặp lỗi khi chạy.
Generics mang lại hai lợi ích chính: chúng loại bỏ nhu cầu ép kiểu tường minh và phát hiện lỗi kiểu dữ liệu khi biên dịch thay vì khi chạy. Nhờ đó, code của bạn an toàn và gọn gàng hơn. Trong các bài học tiếp theo, chúng ta sẽ tìm hiểu cách tạo lớp và phương thức generic của riêng bạn.
Thử thách
DễHãy xây dựng một hệ thống lưu trữ đơn giản để minh họa sức mạnh của kiểu tổng quát (generics) bằng cách so sánh cách cũ (dùng Object cùng với ép kiểu) và cách mới (dùng kiểu tổng quát để đảm bảo an toàn kiểu dữ liệu).
Bạn sẽ tạo ba tệp để tự mình thấy sự khác biệt:
OldBox.java: Tạo một lớp lưu trữ giá trị theo cách trước khi có kiểu tổng quát. OldBox cần có một trường privatecontentthuộc kiểuObject. Thêm hàm khởi tạo nhận một Object và lưu trữ nó, cùng phương thứcgetContent()trả về Object. Cách này hoạt động, nhưng bạn cần ép kiểu khi lấy giá trị.GenericBox.java: Bây giờ, hãy tạo phiên bản hiện đại, an toàn về kiểu dữ liệu bằng cách dùng kiểu tổng quát. GenericBox cần dùng tham số kiểuTtrong phần khai báo. Lớp cần có một trường privatecontentthuộc kiểuT, một hàm khởi tạo nhận giá trịTvà phương thứcgetContent()trả về kiểuT. Bạn không cần ép kiểu khi dùng lớp này!Main.java: Minh họa cả hai cách cạnh nhau. Bạn sẽ nhận hai đầu vào: một từ (String) và một số (số nguyên).Trước tiên, dùng cách cũ: tạo một OldBox lưu trữ từ đó, sau đó lấy giá trị bằng cách ép kiểu sang String và in ra:
OldBox (with cast): [value]Tiếp theo, dùng cách tổng quát: tạo một
GenericBox<String>lưu trữ cùng từ đó, lấy giá trị mà không ép kiểu và in ra:GenericBox (no cast): [value]Sau đó, cho thấy kiểu tổng quát hoạt động với số nguyên: tạo một
GenericBox<Integer>lưu trữ số đó, lấy giá trị và in ra:GenericBox Integer: [value]Cuối cùng, in một dòng trống rồi in:
Type safety: Generics catch errors at compile time!
Bạn sẽ nhận hai đầu vào theo thứ tự: một từ (String) và một số (số nguyên ở dạng String, bạn sẽ phân tích số này).
Hãy chú ý đến điểm khác biệt chính: với OldBox, bạn phải ép kiểu bằng (String) box.getContent() và hy vọng kiểu dữ liệu nhận được là chính xác. Với GenericBox, trình biên dịch biết kiểu dữ liệu nên bạn không cần ép kiểu. Đây là lợi ích cốt lõi của kiểu tổng quát: đảm bảo an toàn kiểu dữ liệu mà không cần phiền phức với việc ép kiểu!
Tự làm thử
import java.util.Scanner;
class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String word = scanner.nextLine();
int number = Integer.parseInt(scanner.nextLine());
// TODO: Tạo OldBox lưu từ đó
// Lấy giá trị, ép kiểu sang String rồi in ra: "OldBox (with cast): [value]"
// TODO: Tạo GenericBox<String> lưu từ đó
// Lấy giá trị mà không cần ép kiểu rồi in ra: "GenericBox (no cast): [value]"
// TODO: Tạo GenericBox<Integer> lưu số đó
// Lấy giá trị rồi in ra: "GenericBox Integer: [value]"
// TODO: In một dòng trống, sau đó in ra:
// "Type safety: Generics catch errors at compile time!"
}
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Kiến thức cơ bản về lập trình hướng đối tượng
Tệp bên ngoàiGiới thiệu về lập trình hướng đối tượngLớp và đối tượngTừ khóa thisPhương thứcTrường (thuộc tính)Phương thức khởi tạoNạp chồng phương thức khởi tạoÔn tập: Máy tính đơn giản4Kế thừa
Kế thừa cơ bản (extends)Từ khóa superGhi đè phương thức (@Override)Chuỗi gọi hàm khởi tạoLớp ObjectKế thừa đơn và đa cấpVì sao không hỗ trợ kế thừa nhiều lớpÔn tập: Phân cấp lớp Employee7Phương thức đặc biệt và lớp Object
Phương thức toString()equals() và hashCode()Phương thức clone()compareTo() và ComparableGiao diện ComparatorÔn tập: sắp xếp tùy chỉnh2Bộ điều chỉnh quyền truy cập và đóng gói
Tổng quan về các mức truy cậpPhương thức getter và setterẨn thông tinTừ khóa finalÔn tập: Trình quản lý tài khoản ngân hàng5Tính đa hình
Kiến thức cơ bản về nạp chồng phương thứcGhi đè phương thức (thời gian chạy)Chuyển kiểu lên và chuyển kiểu xuốngToán tử instanceofLớp và phương thức trừu tượngÔn tập: máy tính hình học8Khái niệm OOP nâng cao
Kết hợp và kế thừaKết tập và hợp thànhLớp bên trong, lớp lồng nhau và lớp ẩn danhKiểu enum và phương thức enumBản ghi (Java 16+)Lớp sealed (Java 17+)3Thuộc tính lớp và thành viên tĩnh
Biến của instance và biến tĩnhPhương thức tĩnhKhối tĩnhHằng số (static final)Ôn tập: bộ đếm và tiện ích6Giao diện và lớp trừu tượng
Giới thiệu về giao diệnTriển khai giao diệnTriển khai nhiều giao diệnPhương thức mặc định và tĩnh trong giao diệnLớp trừu tượng và giao diệnGiao diện hàmÔn tập: hệ thống thanh toán9Kiểu tổng quát
Giới thiệu về kiểu tổng quátLớp tổng quátPhương thức tổng quátTham số kiểu có giới hạnKý tự đại diện (?, extends, super)Ôn tập: vùng chứa tổng quát12Mẫu thiết kế, phần 2
Mẫu CommandMẫu AdapterMẫu DecoratorMẫu Template MethodMẫu StateMẫu CompositeMẫu IteratorTự luyện tập: Trình biên dịch Java online