Eine Übersetzung dieses Artikels wurde speziell für Studenten des Java Developer- Kurses erstellt .
In meinem vorherigen Artikel
Alles über das Überladen von Methoden im Vergleich zum Überschreiben von Methoden haben wir uns mit den Regeln und Unterschieden beim Überladen und Überschreiben von Methoden befasst. In diesem Artikel werden wir sehen, wie das Überladen und Überschreiben von Methoden in der JVM behandelt wird.
Nehmen Sie zum Beispiel die Klassen aus dem vorherigen Artikel: das Eltern-
Mammal
(Säugetier) und das Kind
Human
(Mensch).
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"); }
Wir können die Frage des Polymorphismus von zwei Seiten betrachten: von der „logischen“ und der „physischen“. Schauen wir uns zunächst die logische Seite des Problems an.
Logische Sichtweise
Aus logischer Sicht wird die aufgerufene Methode in der Kompilierungsphase als mit der Art der Referenz verbunden angesehen. Zur Laufzeit wird jedoch die Methode des referenzierten Objekts aufgerufen.
Zum Beispiel in der Zeile
humanMammal.speak();
Der Compiler glaubt, dass
Mammal.speak()
aufgerufen wird, da
humanMammal
als
Mammal
humanMammal
.
humanMammal
Laufzeit weiß die JVM jedoch, dass
humanMammal
ein
Human
Objekt enthält, und
Human.speak()
tatsächlich die
Human.speak()
-Methode auf.
Es ist alles ziemlich einfach, solange wir auf einer konzeptionellen Ebene bleiben. Aber wie geht die JVM intern damit um? Wie berechnet die JVM, welche Methode aufgerufen werden soll?
Wir wissen auch, dass überladene Methoden nicht als polymorph bezeichnet werden und zur Kompilierungszeit aufgelöst werden. Obwohl manchmal das Überladen von Methoden als
Polymorphismus zur Kompilierungszeit oder frühe / statische Bindung bezeichnet wird .
Überschriebene Methoden (Überschreiben) werden zur Laufzeit aufgelöst, da der Compiler nicht weiß, ob das Objekt, das dem Link zugewiesen ist, überschriebene Methoden enthält.
Physikalische Sicht
In diesem Abschnitt werden wir versuchen, „physische“ Beweise für alle oben genannten Aussagen zu finden. Schauen Sie sich dazu den Bytecode an, den Sie durch Ausführen von
javap -verbose OverridingInternalExample
. Mit
-verbose
Parameter
-verbose
wir einen intuitiveren Bytecode, der unserem Java-Programm entspricht.
Der obige Befehl zeigt zwei Abschnitte des Bytecodes.
1. Der Pool von Konstanten . Es enthält fast alles, was zum Ausführen des Programms erforderlich ist. Zum Beispiel Methodenreferenzen (
#Methodref
), Klassen (
#Class
), String-Literale (
#String
).
2. Der Bytecode des Programms. Ausführbare Bytecode-Anweisungen.

Warum das Überladen von Methoden als statische Bindung bezeichnet wird
Im obigen Beispiel glaubt der Compiler, dass die Methode
humanMammal.speak()
von der
Mammal
Klasse aufgerufen wird, obwohl sie zur Laufzeit von dem Objekt aufgerufen wird, auf das in
humanMammal
- es wird ein Objekt der
Human
Klasse sein.
Wenn wir unseren Code und das Ergebnis von
javap
, sehen wir, dass unterschiedliche Bytecodes verwendet werden, um die Methoden
humanMammal.speak()
,
human.speak()
und
human.speak("Hindi")
, da der Compiler sie anhand der Klassenreferenz unterscheiden kann .
Somit kann der Compiler im Falle einer Methodenüberladung zur Kompilierungszeit Bytecode-Anweisungen und Methodenadressen identifizieren. Aus diesem Grund wird dies als
statische Verknüpfung oder Polymorphismus zur Kompilierungszeit bezeichnet.Warum das Überschreiben von Methoden als dynamische Bindung bezeichnet wird
Um die
anyMammal.speak()
und
humanMammal.speak()
, ist der Bytecode derselbe, da aus Sicht des Compilers beide Methoden für die
Mammal
Klasse aufgerufen werden:
invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
Die Frage ist nun, ob beide Aufrufe denselben Bytecode haben. Woher weiß die JVM, welche Methode aufgerufen werden soll?
Die Antwort ist im Bytecode selbst und in der Anweisung
invokevirtual
versteckt. Gemäß der JVM-Spezifikation
(Anmerkung des Übersetzers: Verweis auf JVM-Spezifikation 2.11.8 ) :
Die Anweisung invokevirtual ruft die Instanzmethode durch Versenden des (virtuellen) Objekttyps auf. Dies ist der normale Versand von Methoden in der Programmiersprache Java.
Die JVM verwendet die
invokevirtual
, um Java-Methoden aufzurufen, die den virtuellen C ++ - Methoden entsprechen. In C ++ muss die Methode als virtuell deklariert werden, um eine Methode in einer anderen Klasse zu überschreiben. In Java sind standardmäßig alle Methoden virtuell (mit Ausnahme der endgültigen und statischen Methoden), sodass wir in der untergeordneten Klasse jede Methode überschreiben können.
Die Anweisung
invokevirtual
nimmt einen Zeiger auf die
invokevirtual
Methode (# 4 ist der Index im Konstantenpool).
invokevirtual #4
Referenz Nr. 4 bezieht sich jedoch weiter auf eine andere Methode und Klasse.
#4 = Methodref #2.#27
Alle diese Links werden zusammen verwendet, um einen Verweis auf die Methode und die Klasse zu erhalten, in der sich die gewünschte Methode befindet. Dies wird auch in der JVM-Spezifikation erwähnt (
Anmerkung des Übersetzers: Verweis auf JVM-Spezifikation 2.7 ):
Die Java Virtual Machine erfordert keine spezifische interne Struktur von Objekten.
In einigen Java Virtual Machine-Implementierungen von Oracle ist ein Verweis auf eine Instanz einer Klasse ein Verweis auf einen Handler, der selbst aus zwei Verknüpfungen besteht: Einer verweist auf eine Tabelle mit Objektmethoden und ein Zeiger auf ein Klassenobjekt, das den Typ des Objekts darstellt, und der andere auf den Bereich Daten auf dem Heap, die Objektdaten enthalten.
Dies bedeutet, dass jede Referenzvariable zwei versteckte Zeiger enthält:
- Ein Zeiger auf eine Tabelle, die die Methoden des Objekts enthält, und ein Zeiger auf das
Class
Objekt, z. B. [speak(), speak(String) Class object]
- Ein Zeiger auf den Speicher auf dem Heap, der Objektdaten zugewiesen ist, z. B. Objektfeldwerte.
Aber wieder stellt sich die Frage: Wie funktioniert
invokevirtual
damit? Leider kann niemand diese Frage beantworten, da alles von der Implementierung der JVM abhängt und von JVM zu JVM variiert.
Aus den obigen Überlegungen können wir schließen, dass ein Verweis auf ein Objekt indirekt einen Link / Zeiger auf eine Tabelle enthält, die alle Verweise auf Methoden dieses Objekts enthält. Java hat dieses Konzept von C ++ übernommen. Diese Tabelle ist unter verschiedenen Namen bekannt, z. B.
VMT (Virtual Method Table), Virtual Function Table (vftable), Virtual Table (vtable) und Dispatch Table .
Wir können nicht sicher sein, wie vtable in Java implementiert ist, da dies von der spezifischen JVM abhängt. Wir können jedoch erwarten, dass die Strategie ungefähr dieselbe ist wie in C ++, wo vtable eine Array-ähnliche Struktur ist, die Methodennamen und deren Referenzen enthält. Immer wenn die JVM versucht, eine virtuelle Methode auszuführen, fordert sie ihre Adresse in der vtable an.
Für jede Klasse gibt es nur eine vtable. Dies bedeutet, dass die Tabelle eindeutig und für alle Objekte der Klasse gleich ist, ähnlich wie das Class-Objekt.
Klassenobjekte werden in den Artikeln ausführlicher erläutert.
Warum eine äußere Java-Klasse nicht statisch sein kann und
warum Java eine rein objektorientierte Sprache ist oder warum nicht .
Daher gibt es nur eine vtable für die
Object
Klasse, die alle 11 Methoden (sofern
registerNatives
nicht berücksichtigt werden) und Links enthält, die ihrer Implementierung entsprechen.

Wenn die JVM die Mammal-Klasse in den Speicher lädt, erstellt sie ein
Class
Objekt dafür und erstellt eine vtable, die alle Methoden aus der vtable der
Object
Klasse mit denselben Referenzen enthält (da
Mammal
die Methoden aus
Object
nicht überschreibt) und fügt einen neuen Eintrag für die
speak()
-Methode hinzu.

Dann kommt die Klasse der
Human
Klasse herein, und die JVM kopiert alle Einträge aus der vtable der
Mammal
Klasse in die vtable der
Human
Klasse und fügt einen neuen Eintrag für die überladene Version von
speak(String)
.
Die JVM weiß, dass die
Human
Klasse zwei Methoden überschrieben hat:
toString()
von
Object
und
speak()
von
Mammal
. Anstatt für diese Methoden neue Datensätze mit aktualisierten Links zu erstellen, ändert die JVM die Links zu vorhandenen Methoden in demselben Index, in dem sie zuvor vorhanden waren, und behält dieselben Methodennamen bei.

Die Anweisung
invokevirtual
veranlasst die JVM, den Wert im Verweis auf Methode 4 nicht als Adresse, sondern als Namen der Methode zu verarbeiten, nach der in der vtable für das aktuelle Objekt gesucht werden soll.
Ich hoffe, es ist jetzt klarer, wie die JVM den konstanten Pool und die virtuelle Methodentabelle verwendet, um zu bestimmen, welche Methode aufgerufen werden soll.
Sie finden den Beispielcode im
Github- Repository.