Cóż, aby zrozumieć, jak statyczne i dynamiczne wiązanie faktycznie działa ? lub jak są identyfikowane przez kompilator i JVM?
Weźmy poniższy przykład, gdzie Mammal
jest klasą nadrzędną, która ma metodę speak()
i Human
klasę rozszerza Mammal
, zastępuje speak()
metodę, a następnie ponownie ją przeciąża 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
}
}
Kiedy kompilujemy powyższy kod i próbujemy spojrzeć na kod bajtowy za pomocą javap -verbose OverridingInternalExample
, widzimy, że kompilator generuje stałą tabelę, w której przypisuje kody całkowite do każdego wywołania metody i kodu bajtowego dla programu, który wyodrębniłem i włączyłem do samego programu ( zobacz komentarze poniżej każdego wywołania metody)
Patrząc na powyższym kodzie widzimy, że bytecodes o humanMammal.speak()
, human.speak()
i human.speak("Hindi")
są zupełnie różne ( invokevirtual #4
, invokevirtual #7
, invokevirtual #9
) ponieważ kompilator jest w stanie rozróżniać między nimi na podstawie listy argumentów i odniesienie klasy. Ponieważ wszystko to jest rozwiązywane statycznie w czasie kompilacji, dlatego przeciążanie metod jest znane jako statyczny polimorfizm lub statyczne wiązanie .
Ale kod bajtowy dla anyMammal.speak()
i humanMammal.speak()
jest taki sam ( invokevirtual #4
), ponieważ zgodnie z kompilatorem obie metody są wywoływane w Mammal
odwołaniu.
Więc teraz pojawia się pytanie, czy oba wywołania metod mają ten sam kod bajtowy, to skąd JVM wie, którą metodę wywołać?
Cóż, odpowiedź jest ukryta w samym kodzie bajtowym i jest to invokevirtual
zestaw instrukcji. JVM używainvokevirtual
instrukcji do wywołania Java odpowiednika metod wirtualnych C ++. W C ++, jeśli chcemy przesłonić jedną metodę w innej klasie, musimy zadeklarować ją jako wirtualną, ale w Javie wszystkie metody są domyślnie wirtualne, ponieważ możemy przesłonić każdą metodę w klasie potomnej (z wyjątkiem metod prywatnych, końcowych i statycznych).
W Javie każda zmienna referencyjna zawiera dwa ukryte wskaźniki
- Wskaźnik do tabeli, która ponownie zawiera metody obiektu i wskaźnik do obiektu Class. np. [speak (), speak (String) Class object]
- Wskaźnik do pamięci przydzielonej na stercie dla danych tego obiektu, np. Wartości zmiennych instancji.
Zatem wszystkie odwołania do obiektu pośrednio przechowują odwołanie do tabeli, która zawiera wszystkie odwołania do metod tego obiektu. Java zapożyczyła tę koncepcję z C ++ i ta tabela jest znana jako tabela wirtualna (vtable).
Tabela vtable jest strukturą przypominającą tablicę, która przechowuje nazwy metod wirtualnych i ich odniesienia w indeksach tablic. JVM tworzy tylko jedną tabelę vtable na klasę, kiedy ładuje klasę do pamięci.
Więc za każdym razem, gdy JVM napotyka invokevirtual
zestaw instrukcji, sprawdza tabelę vtable tej klasy pod kątem odwołania do metody i wywołuje określoną metodę, która w naszym przypadku jest metodą z obiektu, a nie z odwołania.
Ponieważ wszystko to jest rozwiązywane tylko w czasie wykonywania i podczas wykonywania, JVM dowiaduje się, którą metodę wywołać, dlatego przesłanianie metody jest znane jako dynamiczny polimorfizm lub po prostu polimorfizm lub dynamiczne wiązanie .
Możesz przeczytać więcej szczegółów w moim artykule Jak JVM obsługuje wewnętrzne przeciążanie i zastępowanie metod .