Таблица виртуальных методов java

Как полиморфизм реализован внутри JVM

В моей предыдущей статье Everything About Method Overloading vs Method Overriding (“Все о перегрузке и переопределении методов”) были рассмотрены правила и различия перегрузки и переопределения методов. В этой статье мы посмотрим, как обрабатывается перегрузка и переопределение методов внутри JVM.

Для примера возьмем классы из предыдущей статьи: родительский Mammal (млекопитающее) и дочерний Human (человек).

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"); >// Допустимая перегрузка 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"; >> // Код ниже содержит вывод метода и байткод для вызова метода 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 >>

На вопрос о полиморфизме мы можем посмотреть с двух сторон: с “логической” и “физической”. Давайте сначала рассмотрим логическую сторону вопроса.

Логическая точка зрения

С логической точки зрения, на этапе компиляции вызываемый метод рассматривается как относящийся к типу ссылки. Но во время выполнения будет вызываться метод объекта, на который указывает ссылка.

Читайте также:  Http transport error java net connectexception connection timed out connect

Например, в строке humanMammal.speak(); компилятор думает, что будет вызван Mammal.speak() , так как humanMammal объявлен как Mammal . Но во время выполнения JVM будет знать, что в humanMammal содержится объект Human и фактически вызовет метод Human.speak() .

Это все довольно просто, пока мы остаемся на концептуальном уровне. Но как же JVM обрабатывает это все внутри? Как JVM вычисляет, какой метод должен быть вызван?

Также мы знаем, что перегруженные методы (overload) не называются полиморфными и резолвятся во время компиляции. Хотя иногда перегрузку методов называют полиморфизмом времени компиляции или ранним/статическим связыванием.

Переопределенные методы (override) резолвятся во время выполнения, так как компилятор не знает, есть ли переопределенные методы в объекте, который присваивается ссылке.

Физическая точка зрения

В этом разделе мы попытаемся найти “физические” доказательства для всех вышеперечисленных утверждений. Для этого посмотрим на байткод, который мы можем получить, запустив javap -verbose OverridingInternalExample . Параметр -verbose позволит нам получить более наглядный байткод, соответствующий нашей java-программе.

Команда выше покажет две секции байткода.

1. Пул констант. Содержит почти все, что необходимо для выполнения программы. Например, ссылки на методы ( #Methodref ), классы ( #Class ), литералы строк ( #String ).

2. Байткод программы. Выполняемые инструкции байткода.

Почему перегрузка методов называется статическим связыванием

В приведенном выше примере компилятор думает, что метод humanMammal.speak() будет вызван из класса Mammal , хотя во время выполнения он будет вызываться из объекта, ссылка на который содержится в humanMammal — это будет объект класса Human .

Посмотрев на наш код и результат javap , мы видим, что для вызова методов humanMammal.speak() , human.speak() и human.speak(«Hindi») используется разный байткод, так как компилятор может различить их на основании ссылки на класс.

Таким образом, в случае перегрузки метода, компилятор способен идентифицировать инструкции байткода и адреса методов во время компиляции. Именно поэтому это называют статическим связыванием или полиморфизмом времени компиляции.

Почему переопределение методов называется динамическим связыванием

Для вызова методов anyMammal.speak() и humanMammal.speak() байткод одинаковый, так как с точки зрения компилятора оба метода вызываются для класса Mammal :

invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V

Итак, теперь возникает вопрос, если у обоих вызовов одинаковый байткод, как JVM узнает, какой метод вызвать?

Ответ спрятан в самом байткоде и в инструкции invokevirtual . Согласно спецификации JVM (прим. переводчика: ссылка на JVM spec 2.11.8):

Инструкция invokevirtual вызывает метод экземпляра через диспетчеризацию по (виртуальному) типу объекта. Это нормальная диспетчеризация методов в языке программирования Java.

JVM использует инструкцию invokevirtual для вызова в Java методов, эквивалентных виртуальным методам C++. В C++ для переопределения метода в другом классе, метод должен быть объявлен как виртуальный (virtual). Но в Java по умолчанию все методы виртуальные (кроме final и static методов), поэтому в дочернем классе мы можем переопределить любой метод.

Инструкция invokevirtual принимает указатель на метод, который нужно вызвать ( #4 — индекс в пуле констант).

invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V

Но ссылка #4 ссылается дальше на другой метод и Class.

#4 = Methodref #2.#27 // org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V #2 = Class #25 // org/programming/mitra/exercises/OverridingInternalExample$Mammal #25 = Utf8 org/programming/mitra/exercises/OverridingInternalExample$Mammal #27 = NameAndType #35:#17 // speak:()V #35 = Utf8 speak #17 = Utf8 ()V

Все эти ссылки используются совместно для получения ссылки на метод и класс, в котором находится нужный метод. Это также упоминается в спецификации JVM (прим. переводчика: ссылка на JVM spec 2.7):

В некоторых реализациях Java Virtual Machine, выполненных компанией Oracle, ссылка на экземпляр класса представляет собой ссылку на обработчик, который сам по себе состоит из пары ссылок: одна указывает на таблицу методов объекта и указатель на объект Class, представляющий тип объекта, а другая на область данных в куче, содержащую данные объекта.

Это означает, что каждая ссылочная переменная содержит два скрытых указателя:

  1. Указатель на таблицу, которая содержит методы объекта и указатель на объект Class , например, [speak(), speak(String) Class object]
  2. Указатель на память в куче, выделенную для данных объекта, таких как значения полей объекта.

Из приведенных выше рассуждений можно сделать вывод, что ссылка на объект косвенно содержит ссылку/указатель на таблицу, которая содержит все ссылки на методы этого объекта. Java позаимствовала эту концепцию из C ++. Эта таблица известна под различными именами, такими как таблица виртуальных методов (VMT), таблица виртуальных функций (vftable), виртуальная таблица (vtable), таблица диспетчеризации.

Мы не можем быть уверены в том, как vtable реализован в Java, потому что это зависит от конкретной JVM. Но мы можем ожидать, что стратегия будет примерно такая же, как и в C ++, где vtable — это структура, похожая на массив, которая содержит имена методов и их ссылки. Всякий раз, когда JVM пытается выполнить виртуальный метод, она запрашивает его адрес в vtable.

Для каждого класса существует только одна vtable, это означает, что таблица уникальна и одинакова для всех объектов класса, аналогично объекту Class. Объекты Class подробнее рассмотрены в статьях Why an outer Java class can’t be static и Why Java is Purely Object-Oriented Language Or Why Not.

Таким образом, существует только одна vtable для класса Object , которая содержит все 11 методов (если не учитывать registerNatives ) и ссылки, соответствующие их реализации.

Когда JVM загружает класс Mammal в память, она создает для него объект Class и создает vtable, которая содержит все методы из vtable класса Object с такими же ссылками (поскольку Mammal не переопределяет методы из Object ) и добавляет новую запись для метода speak() .

Потом наступает очередь класса Human , и JVM копирует все записи из vtable класса Mammal в vtable класса Human и добавляет новую запись для перегруженной версии speak(String) .

JVM знает, что класс Human переопределил два метода: toString() из Object и speak() из Mammal . Теперь для этих методов, вместо создания новых записей с обновленными ссылками, JVM изменит ссылки на уже существующие методы в том же индексе, в котором они присутствовали ранее, и сохранит те же имена методов.

Инструкция invokevirtual заставляет JVM обрабатывать значение в ссылке на метод # 4 не как адрес, а как имя метода, которое нужно искать в vtable для текущего объекта.
Я надеюсь, теперь стало более понятно то, как JVM использует пул констант и таблицу виртуальных методов для определения того, какой метод вызывать.
Код примера вы можете найти в репозитории Github.

Источник

Java 8 наследование

В предыдущих статьях я уже несколько раз упоминал наследование. Настало время написать подробную статью про эту вещь.

В Java класс может наследоваться от другого класса, получая его методы и поля, который в свою очередь может наследоваться от ещё одного класса и т. д. В Java нет множественного наследования классов. Один класс может наследоваться напрямую только от одного другого класса.

Класс, который наследуется от другого класса, называется подклассом (subclass), дочерним классом (child class), потомком или расширенным классом (extended class).

Класс, от которого наследуется дочерний класс, называется родительским классом (parent class), предком, суперклассом (superclass) или базовым классом (base class).

В самой вершине иерархии наследования находится класс Object , от которого наследуются все классы, для которых не указан явно суперкласс. Таким образом все классы (кроме самого Object ) напрямую или через какое-либо количество уровней наследования наследуются от класса Object .

Идея наследования классов состоит в том, что когда вы хотите создать новый класс, например Goblin , и уже существует какой-нибудь класс, который уже реализует часть функциональности, необходимой нашему классу, например Monster , то вы можете указать этот класс в качестве родительского класса, унаследовав таким образом все его члены (поля, вложенные классы и методы экземпляров). Конструкторы не наследуются и не являются членами классов, но можно вызвать конструктор базового класса из конструктора дочернего класса.

java inheritance object

Дочерний класс наследует все public и protected члены своего родителя независимо от пакета, в котором расположен родительский класс. Если дочерний и родительский класс находятся в одном пакете, то дочерний класс наследует также package-private члены своего родителя.

  • Унаследованные поля можно использовать напрямую, как все другие поля.
  • Можно объявить в дочернем классе поле с таким же именем, как и поле в родительском классе, тогда это поле скроет (hide) поле родительского класса (НЕ рекомендуется так делать).
  • В дочернем классе можно объявлять поля, которых нет в родительском классе.
  • Унаследованные методы можно использовать напрямую.
  • Можно объявить метод экземпляров в дочернем классе с точно такой же сигнатурой, что и метод экземпляров в родительском классе, тогда этот метод переопределит (override) метод суперкласса.
  • Можно объявить в дочернем классе статический метод с точно такой же сигнатурой, что и статический метод в родительском классе, тогда этот метод скроет (hide) метод родительского класса.
  • В дочернем классе можно объявлять новые методы, которых нет в родительском классе.
  • В дочернем классе можно объявить конструктор, который будет явно (с помощью ключевого слова super ) или неявно вызывать конструктор базового класса.

Дочерний класс не наследует private члены родительского класса, однако если в родительском классе есть protected , public или package-private (для случая нахождения дочернего и родительского класса в одном пакете) методы для доступа к private полям, то они могут использоваться дочерним классом.

Приведение типов

Посмотрите на создание экземпляра объекта Goblin :

Источник

Оцените статью