Рассмотрите этот простой класс Java:
class MyClass {
public void bar(MyClass c) {
c.foo();
}
}
Я хочу обсудить то, что происходит на строке c.foo ().
Исходный, вводящий в заблуждение вопрос
Примечание: Не все это на самом деле происходит с каждым отдельным invokevirtual кодом операции. Подсказка: Если Вы хотите понять вызов метода Java, не читайте просто документацию для invokevirtual!
На уровне байт-кода суть c.foo () будет invokevirtual кодом операции, и, согласно документации для invokevirtual, более или менее следующее произойдет:
Один только шаг № 3 кажется достаточным для выяснения, какой метод назвать и проверяя, который сказал, что метод имеет корректные типы аргумента/возврата. Таким образом, мой вопрос состоит в том, почему шаг № 1 выполняется во-первых. Возможные ответы, кажется:
Пересмотренный вопрос
Ядро javac выхода компилятора для строки c.foo () будет инструкцией как это:
invokevirtual i
где я - индекс к пулу константы этапа выполнения MyClass. Та постоянная запись пула будет иметь тип CONSTANT_Methodref_info и укажет (возможно, косвенно) A) на название названного метода (т.е. нечто), B) на сигнатуру метода и C) на название класса времени компиляции, что к методу обращаются (т.е. MyClass).
Вопрос, почему ссылка к типу времени компиляции необходимый (MyClass)? С тех пор invokevirtual движение должен сделать динамическую отправку на типе выполнения c, не это избыточный для хранения ссылки на класс времени компиляции?
Все дело в производительности. Когда, выясняя тип времени компиляции (также известный как статический тип), JVM может вычислить индекс вызванного метода в таблице виртуальных функций типа среды выполнения (также известного как динамический тип). Шаг 3 с помощью этого индекса просто превращается в доступ к массиву, который может быть выполнен за постоянное время. Никакого зацикливания не требуется.
Пример:
class A {
void foo() { }
void bar() { }
}
class B extends A {
void foo() { } // Overrides A.foo()
}
По умолчанию A
расширяет Object
, который определяет эти методы (последние методы опущены, поскольку они вызываются через invokespecial
):
class Object {
public int hashCode() { ... }
public boolean equals(Object o) { ... }
public String toString() { ... }
protected void finalize() { ... }
protected Object clone() { ... }
}
Теперь рассмотрим этот вызов:
A x = ...;
x.foo();
Выяснив, что статическим типом x является A
, JVM может также выяснить список методов, доступных на этом сайте вызова: hashCode
, равно
, toString
, finalize
, clone
, foo
, bar
. В этом списке foo
является 6-й записью ( hashCode
является 1-м, равно
2-м и т. Д.). Этот расчет индекса выполняется один раз - когда JVM загружает файл класса.
После этого, когда JVM обрабатывает x.foo ()
, достаточно получить доступ к 6-й записи в списке методов, которые предлагает x, что эквивалентно x.getClass (). GetMethods [5]
, (который указывает на A.foo ()
, если динамический тип x - A
) и вызвать этот метод. Нет необходимости тщательно перебирать этот массив методов.
Обратите внимание, что индекс метода остается неизменным независимо от динамического типа x.То есть: даже если x указывает на экземпляр B, шестой метод по-прежнему будет foo
(хотя на этот раз он будет указывать на B.foo ()
).
Обновление
[В свете вашего обновления]: Вы правы. Для выполнения диспетчеризации виртуального метода все, что нужно JVM, - это имя + подпись метода (или смещение в таблице vtable). Однако JVM не выполняет вслепую. Сначала он проверяет правильность загруженных в него файлов cass в процессе, называемом проверка (см. Также здесь ).
Проверка выражает один из принципов проектирования JVM: Она не полагается на компилятор для создания правильного кода . Он проверяет сам код, прежде чем разрешить его выполнение. В частности, верификатор проверяет, действительно ли каждый вызываемый виртуальный метод определен статическим типом объекта-получателя. Очевидно, что для такой проверки необходим статический тип приемника.
Это не то, как я понял после прочтения документации. Я думаю, что у вас транспонированы шаги 2 и 3, что сделало бы всю серию событий более логичной.
Думаю, ответ "Б".
Проверки модификаторов связывания или доступа, выполненные в пунктах №1 и №2, необходимы для предотвращения возникновения определенных неприятностей, и эти проверки должны выполняться на основе типа времени компиляции, а не иерархии типов времени выполнения. (Пожалуйста, объясните.)
№1 описывается в 5.4.3.3 Разрешение метода , в котором выполняются некоторые важные проверки. Например, # 1 проверяет доступность метода в типе времени компиляции и может вернуть IllegalAccessError, если это не так:
... В противном случае, если указанный метод недоступен (§5.4.4) для D , разрешение метода вызывает ошибку IllegalAccessError. ...
Если вы проверили только тип времени выполнения (через # 3), то тип времени выполнения может незаконно расширить доступность замещаемого метода (он же «плохая вещь»). Это правда, что компилятор должен предотвращать такой случай, но JVM, тем не менее, защищает себя от мошеннического кода (например, созданного вручную вредоносного кода).
Чтобы полностью разобраться в этом материале, вам нужно понять, как работает разрешение методов в Java. Если вы ищете подробное объяснение, я предлагаю посмотреть книгу «Внутри виртуальной машины Java». Следующие разделы из главы 8 «Модель связывания» доступны в Интернете и кажутся особенно актуальными:
(записи CONSTANT_Methodref_info являются записями в файле класса заголовок, описывающий методы, вызываемые этим классом.)
Спасибо Итаю за то, что вдохновил меня на поиск в Google, необходимый, чтобы найти это.
Предположительно, # 1 и # 2 уже произошли компилятором. Я подозреваю, что, по крайней мере, отчасти цель состоит в том, чтобы убедиться, что они все еще сохраняются с версией класса в среде выполнения, которая может отличаться от версии, для которой был скомпилирован код.
Я не переваривал документацию invokevirtual
, чтобы проверить ваше резюме, так что Роб Хейзер может быть прав.