6 июл. 2015 г.

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

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

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

И так общим суперклассом у нас будет класс 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.

5 комментариев:

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

    ОтветитьУдалить
    Ответы
    1. Потому что он не был переопределен. Вы не внимательно прочитали предыдущий пост. А так же не внимательно читали этот пост http://pr0java.blogspot.ru/2015/07/blog-post_6.html

      Удалить
    2. final - методы не могут быть переопределены и поэтому они вызываются во время компиляции. Книга: Шилдт Г. - Java8. Полное руководство. 9-е издание

      Удалить
  2. "Для всех методов Java используется механизм позднего связывания, если только метод не был объявлен как private"
    Но статические методы связываются во время компиляции и на них не распространяется механизм позднего связывания не так ли?

    ОтветитьУдалить
    Ответы
    1. Так точно, для них работает раннее связывание, т.е. по типу объектной ссылки идет весь процесс.

      Удалить