Menu

Đa hình trong Java: một giao diện, nhiều hình thái

Cách đa hình trong Java cho phép một biến tham chiếu tới nhiều kiểu, vì sao phương thức ghi đè được chọn lúc chạy, và cách dùng upcasting, downcasting và instanceof an toàn.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Một tham chiếu, nhiều kiểu

Đa hình (polymorphism) là thành quả của kế thừa và interface. Nó có nghĩa là một biến duy nhất có kiểu là lớp cha (hoặc một interface) có thể giữ đối tượng thuộc bất kỳ kiểu con nào, và khi bạn gọi một phương thức trên nó, Java chạy phiên bản thuộc về lớp thật của đối tượng, không phải phiên bản suy ra từ kiểu đã khai báo của biến.

Bạn đã thấy phần chuẩn bị ở trang kế thừa: một lớp con ghi đè một phương thức của lớp cha. Đa hình là thứ khiến việc ghi đè đó đáng làm: nó cho phép bạn viết code theo kiểu tổng quát và để mỗi đối tượng cụ thể hành xử theo cách riêng.

Cả a và b đều được khai báo là Animal, vậy mà mỗi biến in ra tiếng kêu riêng. Lựa chọn đó diễn ra lúc chạy, dựa trên đối tượng thật.

Dynamic method dispatch

Cơ chế đằng sau điều này là dynamic method dispatch (điều phối phương thức động): với một phương thức instance đã bị ghi đè, JVM nhìn vào lớp lúc chạy của đối tượng để quyết định gọi bản cài đặt nào. Trình biên dịch chỉ kiểm tra phương thức có tồn tại trên kiểu đã khai báo hay không; việc chọn thật sự được hoãn đến khi chương trình chạy.

Đây là thứ cho phép một vòng lặp xử lý cả một tập hỗn hợp các kiểu mà không bao giờ phải hỏi từng phần tử là gì:

Vòng lặp chỉ biết đến Shape. Sau này thêm một Triangle extends Shape thì đoạn code này vẫn chạy mà không cần sửa: đó chính là mục đích. Code phụ thuộc vào trừu tượng, không phụ thuộc vào danh sách cụ thể các kiểu.

Upcasting và downcasting

Lưu một Dog vào một biến Animal là upcasting: đi lên trong cây phân cấp tới một kiểu tổng quát hơn. Việc này luôn an toàn và Java tự làm ngầm định, vì mọi Dog đều là Animal.

Đi theo chiều ngược lại là downcasting: lấy một tham chiếu kiểu cha và coi nó như một kiểu con cụ thể. Đây là một dạng ép kiểu giữa các kiểu tham chiếu, và nó chỉ hợp lệ nếu đối tượng thật sự là kiểu con đó, nên bạn phải viết phép ép kiểu tường minh, và có nguy cơ gặp ClassCastException nếu bạn sai:

Phép ép cuối cùng biên dịch bình thường, vì trình biên dịch không chứng minh được nó sai, nhưng sập khi chạy vì một Cat không phải là Dog. Đừng bao giờ downcast dựa trên niềm tin.

Bảo vệ downcast bằng instanceof

Trước khi downcast, hãy kiểm tra kiểu thật bằng instanceof. Java hiện đại cho phép bạn gán kết quả ngay trong cùng biểu thức (pattern matching cho instanceof), nên bạn bỏ được phép ép kiểu riêng:

instanceof trả về false với null, nên phép kiểm tra này cũng bảo vệ bạn khỏi NullPointerException. Dù vậy, nếu bạn thấy mình viết những chuỗi instanceof dài, đó thường là dấu hiệu hành vi đó nên nằm bên trong các lớp dưới dạng một phương thức ghi đè: hãy để đa hình rẽ nhánh giúp bạn.

Ghi đè và nạp chồng

Hai khái niệm này nghe giống nhau nhưng không liên quan, và nhầm lẫn chúng là nguồn bối rối kinh điển.

Ghi đè (overriding) là lớp con thay thế một phương thức của lớp cha bằng phương thức có chữ ký giống hệt. Nó được quyết định lúc chạy theo kiểu của đối tượng: đây là đa hình mà chúng ta đang dùng.

Nạp chồng (overloading) là một lớp có nhiều phương thức cùng tên nhưng khác danh sách tham số. Nó được quyết định lúc biên dịch theo kiểu của đối số, không có điều phối nào lúc chạy:

Trình biên dịch chọn describe phù hợp hoàn toàn dựa trên kiểu tĩnh của đối số. Không có đối tượng cha/con nào tham gia, nên đây không phải đa hình lúc chạy: nó chỉ dùng lại một tên phương thức.

Một bẫy thường gặp: thuộc tính không đa hình

Chỉ phương thức instance mới được điều phối lúc chạy. Thuộc tính và phương thức static được quyết định theo kiểu đã khai báo, điều này khiến rất nhiều người vấp:

p.name() chạy phiên bản của Child (đa hình), nhưng p.label đọc thuộc tính của Parent, vì thuộc tính bị che (hidden) chứ không bị ghi đè. Cách sửa rất đơn giản: giữ thuộc tính là private và chỉ truy cập chúng qua phương thức, để lời gọi đa hình luôn thắng.

Tiếp theo: access modifier

Đa hình chỉ hoạt động gọn gàng khi lớp con nhìn thấy và ghi đè được đúng các thành viên, trong khi phần còn lại của code không thể chọc vào và phá vỡ các bất biến. Sự cân bằng đó được điều khiển bởi các mức truy cập public, protected, private và package-private, tức các access modifier, nội dung tiếp theo.

Câu hỏi thường gặp

Đa hình trong Java là gì?

Đa hình (polymorphism) nghĩa là một kiểu tham chiếu có thể trỏ tới đối tượng của nhiều lớp khác nhau, và phương thức thực sự chạy được chọn theo kiểu thật của đối tượng lúc chạy, không phải theo kiểu đã khai báo của biến. Vì vậy một biến Shape shape có thể giữ một Circle hoặc một Square, và gọi shape.area() sẽ tự động chạy đúng phiên bản.

Ghi đè và nạp chồng trong Java khác nhau thế nào?

Ghi đè (overriding) là khi lớp con thay thế một phương thức của lớp cha bằng phương thức có cùng chữ ký: đây là thứ tạo nên đa hình lúc chạy. Nạp chồng (overloading) là khi một lớp có nhiều phương thức cùng tên nhưng khác danh sách tham số; trình biên dịch chọn một phương thức lúc biên dịch dựa trên các đối số. Ghi đè được quyết định lúc chạy theo kiểu của đối tượng; nạp chồng được quyết định lúc biên dịch theo kiểu của đối số.

Upcasting và downcasting trong Java khác nhau thế nào?

Upcasting coi một đối tượng lớp con như kiểu cha của nó (Animal a = new Dog();): luôn an toàn và thường diễn ra ngầm định. Downcasting đi theo chiều ngược lại (Dog d = (Dog) a;) và chỉ an toàn nếu đối tượng thật sự là kiểu con đó; nếu không, nó ném ra ClassCastException. Hãy kiểm tra mọi lần downcast bằng instanceof trước.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu