- Разница между ранним и поздним связыванием в Java
- Как полиморфизм реализован внутри JVM
- Логическая точка зрения
- Физическая точка зрения
- Почему перегрузка методов называется статическим связыванием
- Почему переопределение методов называется динамическим связыванием
- Связывание в Java: динамическое – позднее, статическое – раннее
- Статическое связывание
- Пример
- Итог
- Динамическое связывание
- Пример
- Итог
Разница между ранним и поздним связыванием в Java
В этом посте будет обсуждаться разница между ранним и поздним связыванием (статическим и динамическим связыванием) в Java.
Связывание обычно относится к отображению одной вещи на другую. В контексте скомпилированных языков программирования привязка — это связь вызова метода с определением метода. Проще говоря, когда метод вызывается в Java, элемент управления программы привязывается к адресу памяти, где этот метод определен.
В Java есть два типа связывания — раннее (или статическое) связывание и позднее (или динамическое) связывание. В этом посте представлен обзор различий между ними.
- The рано привязка происходит во время компиляции и поздно привязка происходит во время выполнения.
- При раннем связывании определение метода и вызов метода связываются во время компиляции. Это происходит, когда вся информация, необходимая для вызова метода, доступна во время компиляции. В отличие от раннего связывания, вызовы методов не разрешаются до времени выполнения в позднем связывании, поскольку мы не можем определить всю информацию, необходимую для вызова метода, во время компиляции. Таким образом, определение метода и вызов метода не связаны до времени выполнения.
- Обычные вызовы методов и вызовы перегруженных методов являются примерами раннего связывания, а отражение и переопределение методов (полиморфизм времени выполнения) — примерами позднего связывания.
- Привязка частных, статических и конечных методов происходит во время компиляции, поскольку их нельзя переопределить.
- Поскольку вся информация, необходимая для вызова метода, доступна до начала выполнения, раннее связывание приводит к более быстрому выполнению программы. В то время как для более поздней привязки вызов метода не разрешается до времени выполнения, что приводит к несколько более медленному выполнению кода.
- Основным преимуществом позднего связывания является его гибкость, поскольку один метод может обрабатывать различные типы объектов во время выполнения. Это значительно уменьшает размер кодовой базы и делает код более читабельным.
Как полиморфизм реализован внутри 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 >>
На вопрос о полиморфизме мы можем посмотреть с двух сторон: с “логической” и “физической”. Давайте сначала рассмотрим логическую сторону вопроса.
Логическая точка зрения
С логической точки зрения, на этапе компиляции вызываемый метод рассматривается как относящийся к типу ссылки. Но во время выполнения будет вызываться метод объекта, на который указывает ссылка.
Например, в строке 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, представляющий тип объекта, а другая на область данных в куче, содержащую данные объекта.
Это означает, что каждая ссылочная переменная содержит два скрытых указателя:
- Указатель на таблицу, которая содержит методы объекта и указатель на объект Class , например, [speak(), speak(String) Class object]
- Указатель на память в куче, выделенную для данных объекта, таких как значения полей объекта.
Из приведенных выше рассуждений можно сделать вывод, что ссылка на объект косвенно содержит ссылку/указатель на таблицу, которая содержит все ссылки на методы этого объекта. 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: динамическое – позднее, статическое – раннее
Ассоциация вызова метода с телом метода известна как связывание в Java. Есть два вида связывания.
Статическое связывание
В статическом связывании вызов метода связан с телом метода во время компиляции. Это также известно как раннее связывание. Делается с помощью статических, частных и, окончательных методов.
Пример
class Super < public static void sample()< System.out.println("This is the method of super class"); >> Public class Sub extends Super < Public static void sample()< System.out.println("This is the method of sub class"); >Public static void main(String args[]) < Sub.sample() >>
Итог
This is the method of sub class
Динамическое связывание
В динамическом связывании вызов метода связан с телом метода во время выполнения. Это также известно как позднее связывание. Делается с помощью методов экземпляра.
Пример
class Super < public void sample()< System.out.println("This is the method of super class"); >> Public class extends Super < Public static void sample()< System.out.println("This is the method of sub class"); >Public static void main(String args[]) < new Sub().sample() >>
Итог
This is the method of sub class
Ниже перечислены существенные различия.
- Это происходит во время компиляции.
- Это наблюдается в частных, статических и конечных методах.
- Он известен как раннее связывание.
- Происходит во время выполнения.
- Это наблюдается в методах экземпляра.
- Он известен как позднее связывание.
Средняя оценка 1.4 / 5. Количество голосов: 57
Спасибо, помогите другим — напишите комментарий, добавьте информации к статье.
Видим, что вы не нашли ответ на свой вопрос.
Напишите комментарий, что можно добавить к статье, какой информации не хватает.