logo
Методичка Java

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

Данный параграф, несмотря на краткость, является очень важным – практически всё профессиональное программирование в Java основано на использовании полиморфизма. В то же время эта тема является одной из наиболее сложной для понимания учащимися. Поэтому рекомендуется внимательно перечитать этот параграф несколько раз.

Методы классов помечаются модификатором static не случайно – для них при компиляции программного кода действует статическое связывание. Это значит, что в контексте какого класса указано имя метода в исходном коде, на метод того класса в скомпилированном коде и ставится ссылка. То есть осуществляется связывание имени метода в месте вызова с исполняемым кодом этого метода. Иногда статическое связывание называют ранним связыванием, так как оно происходит на этапе компиляции программы. Статическое связывание в Java используется ещё в одном случае – когда класс объявлен с модификатором final (“финальный”, “окончательный”),

Методы объектов в Java являются динамическими, то есть для них действует динамическое связывание. Оно происходит на этапе выполнения программы непосредственно во время вызова метода, причём на этапе написания данного метода заранее неизвестно, из какого класса будет проведён вызов. Это определяется типом объекта, для которого работает данный код - какому классу принадлежит объект, из того класса вызывается метод. Такое связывание происходит гораздо позже того, как был скомпилирован код метода. Поэтому такой тип связывания часто называют поздним связыванием.

Программный код, основанный на вызове динамических методов, обладает свойством полиморфизма – один и тот же код работает по-разному в зависимости от того, объект какого типа его вызывает, но делает одни и те же вещи на уровне абстракции, относящейся к исходному коду метода.

Для пояснения этих не очень понятных при первом чтении слов рассмотрим пример из предыдущего параграфа – работу метода moveTo. Неопытным программистам кажется, что этот метод следует переопределять в каждом классе-наследнике. Это действительно можно сделать, и всё будет правильно работать. Но такой код будет крайне избыточным – ведь реализация метода будет во всех классах-наследниках Figure совершенно одинаковой:

public void moveTo(int x, int y){

hide();

x=this.x;

y=this.y;

show();

};

Кроме того, в этом случае не используются преимущества полиморфизма. Поэтому мы не будем так делать.

Ещё часто вызывает недоумение, зачем в абстрактном классе Figure писать реализацию данного метода. Ведь используемые в нём вызовы методов hide и show, на первый взгляд, должны быть вызовами абстрактных методов – то есть, кажется, вообще не могут работать!

Но методы hide и show являются динамическими, а это, как мы уже знаем, означает, что связывание имени метода и его исполняемого кода производится на этапе выполнения программы. Поэтому то, что данные методы указаны в контексте класса Figure, вовсе не означает, что они будут вызываться из класса Figure! Более того, можно гарантировать, что методы hide и show никогда не будут вызываться из этого класса. Пусть у нас имеются переменные dot1 типа Dot и circle1 типа Circle, и им назначены ссылки на объекты соответствующих типов. Рассмотрим, как поведут себя вызовы dot1.moveTo(x1,y1) и circle1.moveTo(x2,y2).

При вызове dot1.moveTo(x1,y1) происходит вызов из класса Figure метода moveTo. Действительно, этот метод в классе Dot не переопределён, а значит, он наследуется из Figure. В методе moveTo первый оператор – вызов динамического метода hide. Реализация этого метода берётся из того класса, экземпляром которого является объект dot1, вызывающий данный метод. То есть из класса Dot. Таким образом, скрывается точка. Затем идет изменение координат объекта, после чего вызывается динамический метод show. Реализация этого метода берётся из того класса, экземпляром которого является объект dot1, вызывающий данный метод. То есть из класса Dot. Таким образом, на новом месте показывается точка.

Для вызова circle1.moveTo(x2,y2) всё абсолютно аналогично – динамические методы hide и show вызываются из того класса, экземпляром которого является объект circle1, то есть из класса Circle. Таким образом, скрывается на старом месте и показывается на новом именно окружность.

То есть если объект является точкой, перемещается точка. А если объект является окружностью - перемещается окружность. Более того, если когда-нибудь кто-нибудь напишет, например, класс Ellipse, являющийся наследником Circle, и создаст объект Ellipse ellipse=new Ellipse(…), то вызов ellipse.moveTo(…) приведёт к перемещению на новое место эллипса. И происходить это будет в соответствии с тем, каким образом в классе Ellipse реализуют методы hide и show. Заметим, что работать будет давным-давно скомпилированный полиморфный код класса Figure . Полиморфизм обеспечивается тем, что ссылки на эти методы в код метода moveTo в момент компиляции не ставятся – они настраиваются на методы с такими именами из класса вызывающего объекта непосредственно в момент вызова метода moveTo.

В объектно-ориентированных языках программирования различают две разновидности динамических методов – собственно динамические и виртуальные. По принципу работы они совершенно аналогичны и отличаются только особенностями реализации. Вызов виртуальных методов быстрее. Вызов динамических медленнее, но служебная таблица динамических методов (DMT – Dynamic Methods Table) занимает чуть меньше памяти, чем таблица виртуальных методов (VMT – Virtual Methods Table).

Может показаться, что вызовы динамических методов неэффективен с точки зрения затрат по времени из-за длительности поиска имён. На самом деле во время вызова поиска имён не делается, а используется гораздо более быстрый механизм, использующий упомянутую таблицу виртуальных (динамических) методов. Но мы на особенностях реализации этих таблиц останавливаться не будем, так как в Java нет различения этих видов методов.