Giải pháp chuyển đổi số trọn gói cho doanh nghiệp Việt

Ép Kiểu Trong OOP - Lập trình Java

Lập trình Java

← Toàn bộ tài liệu


Bài 31: Ép Kiểu Trong OOP

Bài 31/56 Xem dạng sách

Chắc các bạn còn nhớ, chúng ta đã nói về ép kiểu ở bài học số 6. Và hiển nhiên bạn đã biết khái niệm ép kiểu là gì rồi, bài này chúng ta không cần nhắc đến.

Và ép kiểu ở bài học đó chính là ép kiểu trên các dữ liệu nguyên thủy. Việc ép kiểu lúc bấy giờ được phân biệt làm hai trường hợp riêng biệt, đó là ép kiểu ngầm định và ép kiểu tường minh. Xem ra thì ép kiểu mà bạn đã biết cũng không có gì phức tạp lắm nhỉ. Vậy thì hôm nay, khi biết về OOP, chúng ta sẽ xem việc ép kiểu trên các dữ liệu không-phải-nguyên-thủy sẽ trông như thế nào nhé.

Ép Kiểu Ngầm Định

Vâng, trong OOP, chúng ta cũng có thể phân loại ép kiểu ra làm hai dạng, tường minh và ngầm định.

Ép kiểu ngầm định với các lớp trong OOP cũng tương tự như ép kiểu ngầm định với các biến theo kiểu dữ liệu nguyên thủy. Đó là nếu như không có sự mất mát dữ liệu, hay có thể nới rộng khả năng lưu trữ của dữ liệu, thì xem như hệ thống sẽ hoàn toàn ngầm định giúp chúng ta ép kiểu.

Thế nhưng, với OOP, chúng ta có hai trường hợp ép kiểu ngầm định sau đây, mời bạn cùng đi đến hai mục nhỏ bên dưới.

Ép Kiểu Ngầm Định Với Các Đối Tượng Cùng Một Lớp

Thực sự vấn đề này cũng quen thuộc thôi. Nó như là bạn khai báo hai biến int a và int b, rồi bạn gán b = a, và kết quả là a và b sẽ có cùng một giá trị. Với OOP thì sao, cũng vậy, nó sẽ như là bạn khai báo NhanVien a và NhanVien b, rồi bạn gán b = a, kết quả là b với a sẽ mang cùng một nội dung.

Tuy vậy nhưng cũng không hẳn là vậy. Với việc gán hai biến nguyên thủy int vào int như trên thì hết sức bình thường. Nhưng việc bạn gán hai đối tượng NhanVien vào NhanVien thì được xem như một phép ép kiểu, vì sao vậy, chúng ta thử đến ví dụ bên dưới đây.

Giả sử có một lớp NhanVien cực kỳ đơn giản, lớp này chỉ có một thuộc tính ten và các phương thức getter/setter cho nó, và một phương thức giúp xuất thông tin ten ra console.

public class NhanVien {
 
protected String ten;
 
public String getTen() {
return ten;
}
 
public void setTen(String ten) {
this.ten = ten;
}
 
public void xuatThongTin() {
System.out.println("Nhân viên: " + ten);
}
}

Đến phương thức main(). Chúng ta sẽ khai báo hai đối tượng nhanVien1 và nhanVien2 từ lớp NhanVien này, và bạn hãy xem hệ thống gán, hay ép kiểu ngầm định hai đối tượng này cho nhau như thế nào với các dòng code sau nhé.

public class MainClass {
 
public static void main(String[] args) {
// Khai báo hai đối tượng nhanVien1 và nhanVien2 từ một lớp NhanVien
NhanVien nhanVien1 = new NhanVien();
NhanVien nhanVien2 = new NhanVien();
 
// Set tên cho hai nhân viên
nhanVien1.setTen("Hùng");
nhanVien2.setTen("Trang");
 
// Xuất thông tin hai đối tượng lần 1: bạn có thể đoán ra nội dung xuất đúng không
nhanVien1.xuatThongTin(); // In ra "Nhân viên: Hùng"
nhanVien2.xuatThongTin(); // In ra "Nhân viên: Trang"
 
// Gán hai đối tượng nhân viên, hệ thống cũng sẽ ép kiểu ngầm định nhanVien1 về nhanVien2
nhanVien2 = nhanVien1;
 
// Xuất thông tin hai đối tượng lần 2: kết quả không gì lạ, vì nhanVien2 sẽ mang nội dung giống với nhanVien1
nhanVien1.xuatThongTin(); // In ra "Nhân viên: Hùng"
nhanVien2.xuatThongTin(); // In ra "Nhân viên: Hùng"
 
// Thay đổi thông tin giá trị ten bên trong nhanVien2
nhanVien2.setTen("Khải");
 
// Xuất thông tin hai đối tượng lần 3: lạ chưa, cả hai đối tượng đều bị thay đổi giá trị thuộc tính
nhanVien1.xuatThongTin(); // In ra "Nhân viên: Khải"
nhanVien2.xuatThongTin(); // In ra "Nhân viên: Khải"
}
 
}

Bạn chú ý vào comment “xuất thông tin hai đối tượng lần 3”. Trước khi xuất thông tin ở lần này cho cả hai đối tượng nhanVien1 và nhanVien2, thì chỉ có mỗi nhanVien2 là có sự thay đổi giá trị đến thuộc tính ten mà thôi.

// Thay đổi thông tin giá trị ten bên trong nhanVien2
nhanVien2.setTen("Khải");

Vậy cớ sự làm sao mà ten ở nhanVien1 cũng bị chung số phận? Vì bạn nên nhớ rằng, trong OOP, khi bạn thực hiện phép gán hai đối tượng cho nhau, như ví dụ là phép gán 

nhanVien2 = nhanVien1;

 , thì hệ thống sẽ ép kiểu dữ liệu, và cả tham chiếu, của cả hai đối tượng vào nhau, làm cho chúng giờ đây trở thành một. Điều này khác hoàn toàn với việc sử dụng phép gán ở kiểu dữ liệu nguyên thủy, bạn hãy ghi nhớ điều này nhé.

Ép Kiểu Ngầm Định Từ Lớp Con Sang Lớp Cha

Cũng giống như ép kiểu ngầm định dạng mở rộng khả năng lưu trữ ở các kiểu dữ liệu nguyên thủy, hệ thống sẽ tự thực hiện ép kiểu khi mà có sự chuyển đổi dữ liệu từ kiểu dữ liệu có kích thước nhỏ sang kiểu dữ liệu có kích thước lớn hơn, như từ int sang float chẳng hạn. Thì hệ thống cũng sẽ làm tương tự vậy với OOP, nếu có sự chuyển đổi dữ liệu từ lớp con sang lớp cha.

Giờ giả sử chúng ta xây dựng một lớp con của NhanVien, chính là lớp NhanVienFullTime. Lớp con này có override lại phương thức xuatThongTin() của lớp cha. Code của lớp NhanVienFullTime này như sau.

public class NhanVienFullTime extends NhanVien {
 
@Override
public void xuatThongTin() {
System.out.println("Nhân viên toàn thời gian: " + ten);
}
}

Rồi. Như vậy chúng ta cùng xem ở phương thức main(), xem việc ép kiểu ngầm định diễn ra như thế nào nhé. Bạn có thể thấy rằng kết cục của phép gán, và ép kiểu giữa các lớp trong OOP đều mang đến kết quả là hai đối tượng sẽ trở thành một (vì cùng một tham chiếu). Sau khi gán, hễ bạn thay đổi giá trị của một lớp, thì lớp cùng trong phép gán kia cũng sẽ bị thay đổi giá trị tương tự.

public class MainClass {
public static void main(String[] args) {
// Khai báo hai đối tượng nhanVien và nhanVienFullTime
NhanVien nhanVien = new NhanVien();
NhanVienFullTime nhanVienFullTime = new NhanVienFullTime();
 
// Set tên cho hai nhân viên
nhanVien.setTen("Hùng");
nhanVienFullTime.setTen("Trang");
 
// Xuất thông tin hai đối tượng lần 1
nhanVien.xuatThongTin(); // In ra "Nhân viên: Hùng"
nhanVienFullTime.xuatThongTin(); // In ra "Nhân viên toàn thời gian: Trang"
 
// Ép kiểu ngầm định từ NhanVienFullTime về NhanVien, hoàn toàn tự động
nhanVien = nhanVienFullTime;
 
// Xuất thông tin hai đối tượng lần 2
nhanVien.xuatThongTin(); // In ra "Nhân viên toàn thời gian: Trang"
nhanVienFullTime.xuatThongTin(); // In ra "Nhân viên toàn thời gian: Trang"
 
// Thay đổi thông tin giá trị ten bên trong nhanVienFullTime
nhanVienFullTime.setTen("Khải");
 
// Xuất thông tin hai đối tượng lần 3
nhanVien.xuatThongTin(); // In ra "Nhân viên toàn thời gian: Khải"
nhanVienFullTime.xuatThongTin(); // In ra "Nhân viên toàn thời gian: Khải"
}
}

Ép Kiểu Tường Minh

Bạn hoàn toàn có thể đoán được khi nào chúng ta cần thiết phải ép kiểu tường minh rồi đúng không nào. Đó là khi mà hệ thống phát hiện thấy bạn đang muốn chuyển dữ liệu từ kiểu dữ liệu có kích thước lớn hơn sang kiểu dữ liệu có kích thước nhỏ hơn. Với OOP thì từ lớp cha sang lớp con.

Chúng ta đến với ví dụ với phép gán ngược lại với ví dụ ngay trên đây.

public class MainClass {
 
public static void main(String[] args) {
// Khai báo hai đối tượng nhanVien và nhanVienFullTime
NhanVien nhanVien = new NhanVien();
NhanVienFullTime nhanVienFullTime = new NhanVienFullTime();
 
// Set tên cho hai nhân viên
nhanVien.setTen("Hùng");
nhanVienFullTime.setTen("Trang");
 
// Xuất thông tin hai đối tượng lần 1
nhanVien.xuatThongTin(); // In ra "Nhân viên: Hùng"
nhanVienFullTime.xuatThongTin(); // In ra "Nhân viên toàn thời gian: Trang"
 
// Ép kiểu tường minh từ NhanVien sang NhanVienFullTime
nhanVienFullTime = (NhanVienFullTime) nhanVien;
}
 
}

Tại sao code lần này ít thế. Thực ra bạn không nên code nữa, có lỗi xảy ra ở dòng cuối cùng rồi. Dù cho sau khi bạn code, trình biên dịch không hề báo lỗi, nhưng nếu ngay bây giờ bạn thực thi ứng dụng, sẽ nhận được một exception ClassCastException như sau.

Ép kiểu lỗi runtime
Ép kiểu lỗi runtime

Lỗi này có nghĩa là khi bạn ép kiểu tường minh từ nhanVien về nhanVienFullTime, lúc này trình biên dịch vẫn thấy có lý, vì chúng quan hệ cha con mà. Nhưng khi thực thi ở môi trường thực tế, thì lớp nhanVien vốn dĩ không biết đến nhanVienFullTime là gì, nên không thể thực hiện việc ép kiểu được.

Vậy thì ép kiểu tường minh đối với OOP là như thế nào? Thực ra, nếu như chúng ta có áp dụng tính đa hình, tức là ở lúc nào đó nhanVien phải “đặt mình” vào vai trò là một nhanVienFullTime, thì chúng mới hiểu nhau khi “cùng nhau sánh bước trong đường đời” phía trước. Code như thế này sẽ chạy tốt.

public class MainClass {
public static void main(String[] args) {
// Khai báo hai đối tượng nhanVien1 và nhanVien2 từ một lớp NhanVien
NhanVien nhanVien = new NhanVien();
NhanVienFullTime nhanVienFullTime = new NhanVienFullTime();
 
// Set tên cho hai nhân viên
nhanVien.setTen("Hùng");
nhanVienFullTime.setTen("Trang");
 
// Xuất thông tin hai đối tượng lần 1
nhanVien.xuatThongTin(); // In ra "Nhân viên: Hùng"
nhanVienFullTime.xuatThongTin(); // In ra "Nhân viên toàn thời gian: Trang"
 
// Ép kiểu tường minh từ NhanVien sang NhanVienFullTime, nhưng NhanVien phải có tính đa hình trước đó
nhanVien = new NhanVienFullTime();
nhanVienFullTime = (NhanVienFullTime) nhanVien;
 
// Thay đổi thông tin giá trị ten bên trong nhanVien
nhanVien.setTen("Khải");
 
// Xuất thông tin hai đối tượng lần 2
nhanVien.xuatThongTin(); // In ra "Nhân viên toàn thời gian: Khải"
nhanVienFullTime.xuatThongTin(); // In ra "Nhân viên toàn thời gian: Khải"
}
}

Từ khóa instanceof

Bạn đã thấy một ví dụ dẫn đến một exception có tên ClassCastException được tung ra ở ví dụ trên kia của mình. Đó là một cảnh báo nhãn tiền cho việc sử dụng ép kiểu tường minh đối với OOP đấy. Nó cho thấy nếu như bạn sử dụng viện ép kiểu không khéo, dẫn đến việc ép sai lớp của đối tượng, ứng dụng của bạn sẽ chết, hay crash.

Để đảm bảo việc ép kiểu tường minh được diễn ra an toàn, một yêu cầu đối với các lập trình viên là phải luôn dùng instanceof để kiểm tra đối tượng có thuộc lớp cụ thể nào không trước khi chính thức sử dụng ép kiểu.

Cách sử dụng instanceof cũng khá đơn giản, bạn hãy nhìn vào code sau sẽ rõ cách dùng. Mình thêm dòng if vào ví dụ bạn vừa code trên đây.

// Ép kiểu tường minh từ NhanVien sang NhanVienFullTime, nhưng NhanVien phải có tính đa hình trước đó
nhanVien = new NhanVienFullTime();
// Trước khi ép kiểu, nên kiểm tra xem nhanVien có đúng là lớp NhanVienFullTime (chấp nhận đa hình) hay không
if (nhanVien instanceof NhanVienFullTime) {
nhanVienFullTime = (NhanVienFullTime) nhanVien;
}

Ứng Dụng Của Ép Kiểu Tường Minh

Nếu bạn đọc đến đây của bài viết, mình chắc bạn đã nắm được thế nào là ép kiểu ngầm định và thế nào là ép kiểu tường minh rồi. Thế nhưng cũng sẽ có bạn thắc mắc rằng những sự ép kiểu này sẽ được sử dụng ở tình huống nào trong thực tế?

Nếu như với ép kiểu ngầm định thì với việc gán các đối tượng cho nhau là đã thực hiện một phép ép kiểu dễ dàng và an toàn rồi, nên mình sẽ không nói đến việc ứng dụng của kiểu ép này. Nhưng với ép kiểu tường minh, ứng dụng của nó khá hẹp, nếu muốn nói rằng hầu như bạn cũng chả cần đến. Nhưng nếu như bạn muốn nhìn code được tường minh hơn, hay muốn chỉ định rõ ràng phương thức mà chỉ có ở lớp con, khi đó bạn có thể dùng đến ép kiểu tường minh về lớp con như ví dụ sau.

Đầu tiên chúng ta phải làm cho lớp con NhanVienFullTime có phương thức mà lớp cha nó không có, như sau mình thêm vào thuộc tính thuong (thưởng) và phương thức setThuong().

public class NhanVienFullTime extends NhanVien {
private float thuong;
 
public void setThuong(float thuong) {
this.thuong = thuong;
}
 
@Override
public void xuatThongTin() {
System.out.println("Nhân viên toàn thời gian: " + ten + ", thưởng: " + thuong);
}
}

Ở main() lần này chúng ta khai báo mảng các NhanVien có tên là mangNhanVien. Trong đó mangNhanVien[0] mang tính đa hình để có thể trở thành NhanVienFullTime, các thành phần mangNhanVien còn lại đều là kiểu lớp cha NhanVien.

Bạn thấy trong vòng lặp đầu tiên, nếu mong muốn rằng mangNhanVien[i] nếu là NhanVienFullTime, thì ngoài việc setTen() ra nó còn phải gọi thêm setThuong() nữa. Khi này ép kiểu tường minh phát huy tác dụng.

public class MainClass {
public static void main(String[] args) {
// Khai báo mảng các NhanVien
NhanVien[] mangNhanVien = new NhanVien[3];
mangNhanVien[0] = new NhanVienFullTime(); // Nhân viên này là NhanVienFullTime
mangNhanVien[1] = new NhanVien(); // Nhân viên này là NhanVien
mangNhanVien[2] = new NhanVien(); // Nhân viên này là NhanVien
 
// Gán tên và thiết lập thưởng cho các nhân viên
for (int i = 0; i < mangNhanVien.length; i++) {
mangNhanVien[i].setTen("Nhan viên " + i);
if (mangNhanVien[i] instanceof NhanVienFullTime) {
// Nếu nhân viên kiểu NhanVienFullTime
// Thì ép kiểu tường minh về NhanVienFullTime để
// dùng đến phương thức đặc biệt của lớp con này
((NhanVienFullTime) mangNhanVien[i]).setThuong(100);
}
}
 
// Dùng foreach để duyệt qua từng nhân viên để xuất thông tin
for (NhanVien nhanVien : mangNhanVien) {
nhanVien.xuatThongTin();
}
}
}

Kết quả thực thi chương trình của ví dụ trên đây.

Nhân viên toàn thời gian: Nhan viên 0, thưởng: 100.0
Nhân viên: Nhan viên 1
Nhân viên: Nhan viên 2

Sơ đồ quan hệ các bảng

Table Name: {{table.name}}
Num Name Type
{{field_index+1}}
{{field.name}} {{field.type}}

Hỏi đáp

Gửi với tên {{comment_full_name}}

Chưa có câu hỏi nào cho bài này.

Zalo 0936.006.058 Hotline 0936.006.058