Показаны сообщения с ярлыком invokespecial. Показать все сообщения
Показаны сообщения с ярлыком invokespecial. Показать все сообщения

6 июл. 2015 г.

Полиморфизм. Поля и статические методы.

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

Полиморфизм поддерживается только для вызова обычных методов. А, например, прямое обращение к полю или статическому методу будет обработано на стадии компиляции. Данная статья это по существу продолжение этой.

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

FS001

Вывод у программы следующий:

FS002

Ссылка sup у нас имеет тип Super и ей присваивается ссылка на объект типа Sub. Обращение к полю происходит по версии ссылки, а обращение к методу по версии объекта. Во втором выводе работают все те же правила, а доступ к полю суперкласса мы получили через метод getSuperClass.

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

Как видно из скриншота ниже прямое обращение к полям компилируется в инструкцию invokespecial.

Обычные же методы – в инстуркцию invokevirtual.

Для sub все происходит точно так же, как и для sup.

FS003

Прошу обратить внимание, что объекты класса Sub фактически имеют два поля str: одно свое, другое унаследованное от суперкласса. Поэтому использовать одно имя для поля базового и производного класса вообще не рекомендуется, потому что это создает путаницу и вообще это плохая идея. Но знать о подобных граблях надо и мы еще к ним вернемся когда будем изучать внутренние классы. Там это может проявиться не явно. Поэтому сейчас мы уже начали готовится к новому удару граблями :)

Теперь перейдем к статическим методам. Статические методы не поддерживают полиморфного поведения.

Еще раз повторюсь, что во первых статические методы правильно вызывать по имени класса, и во вторых статические методы всегда вызываются по версии ссылки.

Демонстрационный код по этому вопросу писать просто лень. К текущему моменту у вас уже и так должно быть ясное представление чем статические методы и поля отличаются от обычных.

Полиморфизм – позднее связывание.

В первой статье по полиморфизму мы кратко познакомились с тем что это такое. По существу это просто переопределение методов суперкласса в подклассах. Но наверное вся мощь и красота этого еще не совсем понятна. И может не совсем ясно для чего все это нужно. Теперь попробуем разобраться более глубже. Приготовились к глубокой медитации. Оммммм…. Ну и погнали! :)

Возьмем затертый до дыр пример с фигурами. Не будем отклонятся от классиков жанра :)

И так общим суперклассом у нас будет класс Shape, и у него будут наследники царь, царевич, король, королевич, Circle, Square, Triangle. Но мы пойдем чуть дальше заезженного примера :) и образуем еще парочку наследников. Oval у нас будет наследником Circle, а Rect наследником Square.

На диаграмме все можно изобразить примерно так:

S0001

Методы drow() в каждом классе будут переопределены, а метод erase() будет просто наследоваться от Shape. Теперь осталось всю эту красоту забабахать в коде :)

S0002

Код у нас вышел очень красивый :) Буквочка к буквочке :) и вывод у него такой же :)

S0003

Теперь внимательно посмотрим на код. У нас есть одномерный массив shape классов Shape размером 6. И первому элементу массива мы присвоили ссылку на объект Shape (созадется new Shape()). А вот далее начинается магия, которую вы уже видели и должны понимать. Это называется восходящее преобразование. Я уже про это говорил, что ссылка суперкласса может указывать на объекты подклассов. И так далее мы присваиваем следующим элементам массива shape ссылки на подклассы. Но затем в выводе работает вообще сумасшедшая магия полиморфизма – вызываются методы подклассов, хотя ссылка имеет тип суперкласса.

Теперь вопрос от куда компилятор знает метод какого объекта должен быть вызван?

А компилятор то и не знает… :) Ну а кто же тогда знает?

Кто, кто? Дракон в пальто!

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

Но я это сделал для простоты понимания и наглядности того что происходит.

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

S0006Слева как раз и приведен пример измененного отрывка этой же программы, но массив уже заполняется случайным образом, что видно из вывода программы:

S0004

S0005

Встает все тот же вопрос – кто знает метод какого объекта надо вызывать в каждом конкретном случае? А знает это JVM. Но как она узнает? И тут начинается серьезная магия виртуальной машины Java вкупе с компилятором Java.

Сам компилятор не знает, но может подсказать JVM как надо обрабатывать инструкции вызова методов.

Чтобы в полной мере разобраться в сути про­исходящего, необходимо рассмотреть понятие связывания (binding).

Присоединение вызова метода к телу метода называется связыванием. Если связывание проводится перед запуском программы (компилятором и компоновщиком, если он есть), оно называется ранним связыванием (early binding). В процедурных языках никакого выбора связывания не было. Компиляторы C поддерживают только один тип вызова — раннее связывание.

Проблема определения метод какого объекта вызывать в нашей программе решается благодаря позднему связыванию (late binding), то есть связыванию, проводимому во время выполнения программы, в зависимости от типа объекта. Позднее связывание также называют динамическим (dynamic binding) или связыванием на стадии выполнения (runtime binding).

В языках, реализующих позднее связывание, должен существовать механизм определения фактического типа объекта во время работы программы, для вызова подходящего метода. Иначе говоря, компилятор не знает тип объекта, но механизм вызова методов определяет его и вызывает соответствующее тело метода. Механизм позднего связывания зависит от конкретного языка, но нетрудно предположить, что для его реализации в объекты должна включаться какая-то дополнительная информация. Теперь мы попытаемся выяснить, что же это за информация.

В прошлом посте, мы уже вкратце коснулись этого вопроса. Теперь постараемся понять более глубоко.

Для всех методов Java используется механизм позднего связывания, если только метод не был объявлен как private. Вызов private метода компилируется в инструкцию байт-кода invokespecial, которая вызывает реализацию метода из конкретного класса, определенного в момент компиляции. Вызов метода с другим уровнем доступа компилируется в invokevirtual, которая уже смотрит на тип объекта по ссылке в момент исполнения. Финальные неприватные методы тоже вызываются через invokevirtual.

В инструкцию байт-кода invokespesial компилируются:

  • Инициализационный вызов (<init>) при создании объекта
  • Вызов private метода
  • Вызов метода с использованием ключевого слова super

Есть конечно еще несколько других инструкций байт-кода для вызова методов: invokedynamic, invokeinterface и invokestatic. Но хотя об их использовании и говорят их названия, пока мы их обсуждать не будем. Если кому-то сильно хочется то можно почитать эту статью на враждебном каждому правоверному программисту буржуйском языке :) Чтиво полезное, но для понимания того о чём сейчас речь, достаточно того, что я тут уже написал. Так же можно почитать эту статью на родном языке.

И так, надо уже переходить к практике. Модифицируем программу из этого поста, следующим образом:

S0007

Я подсветил private и final модификаторы чтобы вы обратили на них внимание и затем на то, какой байт-код для них создаст компилятор. Вывод у нашей программы сейчас следующий:

Root
Branch

Заострю внимание на том, что ссылка root имеет тип Root, но указывает на объект типа Branch. И как я уже не однократно писал, обычные методы вызываются по версии объекта на который указывает ссылка. Именно через это свойство и реализуется полиморфизм.

Но в нашем случае, не смотря на это, первая команда вывела на консоль Root, а не Branch.

Теперь заглянем под капот этой программе при помощи команды: javap -c -p -v Root.class

Эта команда сгенерирует достаточно длинный вывод, но нам нужна только эта часть:

S0008

Как видно из вывода команда root.prt() была преобразована в вызов типа invokespecial, а команда branch.prt() в invokevirtual.

Вот мы и раскрыли магию всего этого действа. Надеюсь вам понравилось представление :) и теперь вы стали чуть больше понимать как работают полиморфные методы в Java.