Được chình sửa ngày 07/02/2023.
Chào mừng các bạn đến với bài học Java số 31, bài học về ép kiểu trong OOP. Bài học này nằm trong chuỗi bài học lập trình ngôn ngữ Java của Yellow Code Books.
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.

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
Kết Luận
Bài học hôm nay chỉ có như vậy thôi. Cũng tương đối phức tạp đúng không nào, đặc biệt là ép kiểu tường minh. Tuy nhiên bạn đừng nên lo lắng quá, như mình có nói, sự xuất hiện của ép kiểu tường minh trong OOP là không nhiều. Việc của bạn bây giờ là đọc và hiểu các tình huống ép kiểu của bài hôm nay. Để rồi sau này khi bạn rơi vào tình huống cụ thể, hoặc bạn xem source code ở đâu đó có thể hiện sự ép kiểu trong OOP như thế này, thì bạn sẽ nắm bắt vấn đề ngay thôi.
Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:
– Đánh giá 5 sao ở mỗi bài viết nếu thấy thích.
– Comment bên dưới mỗi bài viết nếu có thắc mắc.
– Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
– Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.
– Ủng hộ blog theo hướng dẫn ở thanh bên phải để blog ngày càng phát triển hơn.
Bài Kế Tiếp
Chúng ta sẽ xem đến một trong những đặc tính cốt lõi tiếp theo của OOP, đó là tính Trừu tượng, hay còn được gọi với cái tên Abstraction.
cảm ơn Admin bài viết rất hay và rõ ràng, mong được đọc các bài viết tiếp theo
Rất hay và bổ ích.
Mong sớm có bài viết tiếp theo.
Cám ơn ad rất nhiều.
Ép Kiểu Ngầm Định Từ Lớp Con Sang Lớp Cha
>> theo mình thì phải từ cha sang con chứ ta
tại vì cha ít method sang con nhiều method hơn thì nó ko mất dữ liệu thì mới ngầm định được chứ ta
giống như short sang int
>> z phải là nhanVienFullTime= nhanVien phải hông ta 😀 😀
mong ad giải đáp thắc mắc dùm mình
Mình cũng nghĩ như bạn, mình cũng cảm thấy ad viết ngược
Hình như mình viết ngược thật 😀
Cơ mà để mình kiểm tra lại, khi nào cập nhật lại rồi mình thông báo các bạn nhé. Cảm ơn phản hồi của bạn Nhật Linh.
Như các bình luận khác cũng xác nhận là không ngược nhé bạn. Mình đã review lại bài viết và có bổ sung thêm một vài thông tin hữu ích khác liên quan đến ép kiểu OOP này.
mình nghĩ không ngược đâu bạn ơi,Bà cô mình dạy cũng y như v lúc đầu mình cũng nghĩ là bị ngược như bạn :))
sau khi nghe bà cô ns thì mình cũng đã hiểu đc.
Nếu như lớp cha có nhiều lớp con thì cùng 1 lúc chúng ta k thể nào ép kiểu từ cha sang con đc
Thay vì ép cha vào con với 1 đống dòng code thì ta nên nghĩ theo hướng ngươc lại.
Ta lấy ví dụ : chúng ta có lớp cha là DienThoai(), và các lớp con Nokia() ,Samsung()… thì khi ta ép 2 lớp Nokia(),SamSung() vào lớp cha là DienThoai() thì mọi việc sẽ trở nên đơn giản hơn chẳng hạn như chúng ta muốn xuất danh sách điện thoại thì
chỉ cần xuất thông tin của lớp cha DienThoai() thôi .
khoonng ngược đâu bạn ơi, chính xác đó thày mình cũng dậy vậy
Seri này của anh rất hữu ích ạ. Em cảm ơn anh nhiều
Bài giảng dễ hỉu quá đọc phát bk lun thanks admin nhé
cho e hỏi nếu cái ten khả năng truy cập là protected rồi thì cần set,get gì nữa ad không phải chỉ cần nhanVien.a là được rồi hả??
Trừ khi a là publish thì mới truy xuất kiểu nhanVien.a thôi bạn. Còn protected thì cũng sẽ có nhiều lớp không truy cập thẳng được, nên vẫn phải có get/set cho nó.
Phép gán Object cho Object chỉ đơn giản là thay đổi địa chỉ của Object đó ở bộ nhớ Head, và do đó việc gán nhân viên 2 cho nhân viên 1 chỉ đơn giản là gán địa chỉ bộ nhớ object của nhận viên 1 thành địa chỉ của nhân viên 2 (trong bộ nhớ Head). Do vậy bạn nói ép kiểu nv1 thành nv2 mình nghĩ là chưa đúng, đơn giản là 2 thằng này trỏ đến cùng 1 object nên sẽ là 1, khi thực hiện thay đổi trên nv2 thì sẽ thay đổi object đó, và nv1 lúc này cũng bị trỏ vào object này nên cũng sẽ bị đổi là hiển nhiên,