Để hiểu rõ ràng buộc tĩnh và động thực sự hoạt động như thế nào? hoặc làm thế nào chúng được xác định bởi trình biên dịch và JVM?
Hãy lấy ví dụ dưới đây, đâu Mammal
là lớp cha có một phương thức speak()
và Human
lớp mở rộng Mammal
, ghi đè speak()
phương thức và sau đó lại nạp chồng bằng speak(String language)
.
public class OverridingInternalExample {
private static class Mammal {
public void speak() { System.out.println("ohlllalalalalalaoaoaoa"); }
}
private static class Human extends Mammal {
@Override
public void speak() { System.out.println("Hello"); }
// Valid overload of speak
public void speak(String language) {
if (language.equals("Hindi")) System.out.println("Namaste");
else System.out.println("Hello");
}
@Override
public String toString() { return "Human Class"; }
}
// Code below contains the output and bytecode of the method calls
public static void main(String[] args) {
Mammal anyMammal = new Mammal();
anyMammal.speak(); // Output - ohlllalalalalalaoaoaoa
// 10: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
Mammal humanMammal = new Human();
humanMammal.speak(); // Output - Hello
// 23: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
Human human = new Human();
human.speak(); // Output - Hello
// 36: invokevirtual #7 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:()V
human.speak("Hindi"); // Output - Namaste
// 42: invokevirtual #9 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:(Ljava/lang/String;)V
}
}
Khi chúng ta biên dịch đoạn mã trên và cố gắng xem xét mã bytecode bằng cách sử dụng javap -verbose OverridingInternalExample
, chúng ta có thể thấy trình biên dịch đó tạo ra một bảng hằng số, nơi nó gán mã số nguyên cho mọi lệnh gọi phương thức và mã byte cho chương trình mà tôi đã trích xuất và đưa vào chính chương trình ( xem các bình luận bên dưới mỗi lần gọi phương thức)
Bằng cách nhìn vào đoạn code trên chúng ta có thể thấy rằng các bytecode của humanMammal.speak()
, human.speak()
và human.speak("Hindi")
là hoàn toàn khác nhau ( invokevirtual #4
, invokevirtual #7
, invokevirtual #9
) vì trình biên dịch có thể phân biệt giữa chúng dựa trên danh sách đối số và tham chiếu lớp. Bởi vì tất cả những điều này được giải quyết tại thời điểm biên dịch tĩnh, đó là lý do tại sao Phương pháp Nạp chồng được gọi là Đa hình tĩnh hoặc Liên kết tĩnh .
Nhưng bytecode cho anyMammal.speak()
và humanMammal.speak()
giống nhau ( invokevirtual #4
) bởi vì theo trình biên dịch, cả hai phương thức đều được gọi trên Mammal
tham chiếu.
Vì vậy, bây giờ câu hỏi đặt ra là nếu cả hai cuộc gọi phương thức có cùng một mã bytecode thì làm thế nào JVM biết phương thức nào để gọi?
Chà, câu trả lời được ẩn trong chính mã bytecode và nó là invokevirtual
tập lệnh. JVM sử dụng invokevirtual
lệnh để gọi Java tương đương với các phương thức ảo C ++. Trong C ++, nếu chúng ta muốn ghi đè một phương thức trong một lớp khác, chúng ta cần khai báo nó là ảo, Nhưng trong Java, tất cả các phương thức đều là ảo theo mặc định vì chúng ta có thể ghi đè mọi phương thức trong lớp con (trừ phương thức private, final và static).
Trong Java, mọi biến tham chiếu đều chứa hai con trỏ ẩn
- Một con trỏ tới một bảng giữ các phương thức của đối tượng và một con trỏ tới đối tượng Lớp. ví dụ: [speak (), speak (Chuỗi) Đối tượng lớp]
- Một con trỏ tới bộ nhớ được cấp phát trên heap cho dữ liệu của đối tượng đó, ví dụ như giá trị của các biến cá thể.
Vì vậy, tất cả các tham chiếu đối tượng gián tiếp giữ một tham chiếu đến một bảng chứa tất cả các tham chiếu phương thức của đối tượng đó. Java đã mượn khái niệm này từ C ++ và bảng này được gọi là bảng ảo (vtable).
Một vtable là một cấu trúc giống như mảng chứa các tên phương thức ảo và các tham chiếu của chúng trên các chỉ số mảng. JVM chỉ tạo một vtable cho mỗi lớp khi nó tải lớp đó vào bộ nhớ.
Vì vậy, bất cứ khi nào JVM gặp phải một invokevirtual
tập lệnh, nó sẽ kiểm tra vtable của lớp đó để tìm tham chiếu phương thức và gọi phương thức cụ thể mà trong trường hợp của chúng ta là phương thức từ một đối tượng không phải là tham chiếu.
Bởi vì tất cả những điều này chỉ được giải quyết trong thời gian chạy và trong thời gian chạy, JVM biết phương thức nào cần gọi, đó là lý do tại sao Ghi đè phương thức được gọi là Đa hình động hoặc đơn giản là Đa hình hoặc Liên kết động .
Bạn có thể đọc thêm chi tiết trên bài viết của tôi Cách xử lý quá tải và ghi đè phương pháp JVM trong nội bộ .