Опубликован: 04.12.2009 | Доступ: свободный | Студентов: 8416 / 657 | Оценка: 4.30 / 3.87 | Длительность: 27:27:00
Лекция 6:

Начальные сведения об объектном программировании

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

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

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

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

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

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

public void moveTo(int x, int y){
 hide();
 this.x=x;
 this.y=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.

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

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

6.8. Базовый класс Object

Класс Object является базовым для всех классов Java. Поэтому все его поля и методы наследуются и содержатся во всех классах. В классе Object содержатся следующие методы:

  • public Boolean equals(Object obj) – возвращает true в случае, когда равны значения объекта, из которого вызывается метод, и объекта, передаваемого через ссылку obj в списке параметров. Если объекты не равны, возвращается false. В классе Object равенство рассматривается как равенство ссылок и эквивалентно оператору сравнения "==". Но в потомках этот метод может быть переопределен, и может сравнивать объекты по их содержимому. Например, так происходит для объектов оболочечных числовых классов. Это легко проверить с помощью такого кода:
    Double d1=1.0,d2=1.0;
    System.out.println("d1==d2 ="+(d1==d2));
    System.out.println("d1.equals(d2) ="+(d1.equals(d2)));

    Первая строка вывода даст d1==d2 =false, а вторая d1.equals(d2) =true

  • public int hashCode() – выдает хэш-код объекта. Хэш-кодом называется условно уникальный числовой идентификатор, сопоставляемый какому-либо элементу. Из соображений безопасности выдавать адрес объекта прикладной программе нельзя. Поэтому в Java хэш-код заменяет адрес объекта в тех случаях, когда для каких-либо целей надо хранить таблицы адресов объектов.
  • protected Object clone() throws CloneNotSupportedException – метод занимается копированием объекта и возвращает ссылку на созданный клон (дубликат) объекта. В наследниках класса Object его обязательно надо переопределить, а также указать, что класс реализует интерфейс Clonable. Попытка вызова метода из объекта, не поддерживающего клонирования, вызывает возбуждение исключительной ситуации CloneNotSupportedException ("Клонирование не поддерживается"). Про интерфейсы и исключительные ситуации будет рассказано в дальнейшем.

    Различают два вида клонирования: мелкое ( shallow ), когда в клон один к одному копируются значения полей оригинального объекта, и глубокое ( deep ), при котором для полей ссылочного типа создаются новые объекты, клонирующие объекты, на которые ссылаются поля оригинала. При мелком клонировании и оригинал, и клон будут ссылаться на одни и те же объекты. Если объект имеет поля только примитивных типов, различия между мелким и глубоким клонированием нет. Реализацией клонирования занимается программист, разрабатывающий класс, автоматического механизма клонирования нет. И именно на этапе разработки класса следует решить, какой вариант клонирования выбирать. В подавляющем большинстве случаев требуется глубокое клонирование.

  • public final Class getClass() – возвращает ссылку на метаобъект типа класс. С его помощью можно получать информацию о классе, к которому принадлежит объект, и вызывать его методы класса и поля класса.
  • protected void finalize() throws Throwable – вызывается перед уничтожением объекта. Должен быть переопределен в тех потомках Object, в которых требуется совершать какие-либо вспомогательные действия перед уничтожением объекта (закрыть файл, вывести сообщение, отрисовать что-либо на экране, и т.п.). Подробнее об этом методе говорится в соответствующем параграфе.
  • public String toString() – возвращает строковое представление объекта (настолько адекватно, насколько это возможно). В классе Object этот метод реализует выдачу в строку полного имени объекта (с именем пакета), после которого следует символ '@', а затем в шестнадцатеричном виде хэш-код объекта. В большинстве стандартных классов этот метод переопределен. Для числовых классов возвращается строковое представление числа, для строковых – содержимое строки, для символьного – сам символ (а не строковое представление его кода!). Например, следующий фрагмент кода
    Object obj=new Object();
    System.out.println(" obj.toString() дает "+obj.toString());
    Double d=new Double(1.0);
    System.out.println(" d.toString()дает "+d.toString());
    Character c='A';
    System.out.println("c.toString() дает "+c.toString());

    обеспечит вывод

    obj.toString() дает java.lang.Object@fa9cf
    d.toString()дает 1.0
    c.toString()дает A

Также имеются методы notify(), notifyAll(), и несколько перегруженных вариантов метода wait, предназначенные для работы с потоками (threads). О них говорится в разделе, посвященном потокам.

6.9. Конструкторы. Зарезервированные слова super и this. Блоки инициализации

Как уже говорилось, объекты в Java создаются с помощью зарезервированного слова new, после которого идет конструктор – специальная подпрограмма, занимающаяся созданием объекта и инициализацией полей создаваемого объекта. Для него не указывается тип возвращаемого значения, и он не является ни методом объекта (вызывается через имя класса когда объекта еще нет), ни методом класса (в конструкторе доступен объект и его поля через ссылку this ). На самом деле конструктор в сочетании с оператором new возвращает ссылку на создаваемый объект и может считаться особым видом методов, соединяющим в себе черты методов класса и методов объекта.

Если в объекте при создании не нужна никакая дополнительная инициализация, можно использовать конструктор, который по умолчанию присутствует для каждого класса. Это имя класса, после которого ставятся пустые круглые скобки – без списка параметров. Такой конструктор при разработке класса задавать не надо, он присутствует автоматически.

Если требуется инициализация, обычно применяют конструкторы со списком параметров. Примеры таких конструкторов рассматривались нами для классов Dot и Circle. Классы Dot и Circle были унаследованы от абстрактных классов, в которых не было конструкторов. Если же идет наследование от неабстрактного класса, то есть такого, в котором уже имеется конструктор (пусть даже и конструктор по умолчанию), возникает некоторая специфика. Первым оператором в конструкторе должен быть вызов конструктора из суперкласса. Но его делают не через имя этого класса, а с помощью зарезервированного слова super (от "superclass"), после которого идет необходимый для прародительского конструктора список параметров. Этот конструктор инициализирует поля данных, которые наследуются от суперкласса (в том числе и от всех более ранних прародителей). Например, напишем класс FilledCircle - наследник от Circle, экземпляр которого будет отрисовываться как цветной круг.

package java_gui_example;
import java.awt.*;

public class FilledCircle extends Circle{

    /** Creates a new instance of FilledCircle */
    public FilledCircle(Graphics g,Color bgColor, int r,Color color) {
        super(g,bgColor,r);
        this.color=color;
    }
    
        public void show(){
        Color oldC=graphics.getColor();
        
        graphics.setColor(color);
        graphics.setXORMode(bgColor);
        graphics.fillOval(x,y,size,size);
        graphics.setColor(oldC);
        graphics.setPaintMode();
    }
    
    public void hide(){
        Color oldC=graphics.getColor();
        graphics.setColor(color);
        graphics.setXORMode(bgColor);
        graphics.fillOval(x,y,size,size);
        graphics.setColor(oldC);
        graphics.setPaintMode();
    }}

Вообще, логика создания сложно устроенных объектов: родительская часть объекта создается и инициализируется первой, начиная от части, доставшейся от класса Object, и далее по иерархии, заканчивая частью, относящейся к самому классу. Именно поэтому обычно первым оператором конструктора является вызов прародительского конструктора super( список параметров ), так как обращение к неинициализированной части объекта, относящейся к ведению прародительского класса, может привести к непредсказуемым последствиям.

В данном классе мы применяем более совершенный способ отрисовки и "скрывания" фигур по сравнению с предыдущими классами. Он основан на использовании режима рисования XOR ("исключающее или"). Установка этого режима производится методом setXORMode. При этом повторный вывод фигуры на то же место приводит к восстановлению первоначального изображения в области вывода. Переход в обычный режим рисования осуществляется методом setPaintMode.

В конструкторах очень часто используют зарезервированное слово this для доступа к полям объекта, видимость имен которых перекрыта переменными из списка параметров конструктора. Но в конструкторах оно имеет еще одно применение - для обращения из одного варианта конструктора к другому, имеющему другой список параметров. Напомним, что наличие таких вариантов называется перегрузкой конструкторов. Например, пусть мы первоначально задали в классе Circle конструктор, в котором значение полей x, y и r задается случайным образом:

Circle(Graphics g, Color bgColor){
    graphics=g;
    this.bgColor=bgColor;
    size=(int)Math.round(Math.random()*40);
}

Тогда конструктор, в котором случайным образом задаются значения полей x и y, а значение size задается через список параметров конструктора, можно написать так:

Circle(Graphics g, Color bgColor, int r){
  this(g, bgColor);
  size=r;
}

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

В отличие от языка C++ в Java не разрешается использование имени конструктора, отличающегося от имени класса.

Порядок вызовов при создании объекта некого класса (будем называть его дочерним классом):

  • Создается объект, в котором все поля данных имеют значения по умолчанию (нули на двоичном уровне представления).
  • Вызывается конструктор дочернего класса.
    • Конструктор дочернего класса вызывает конструктор родителя (непосредственного прародителя), а также по цепочке все прародительские конструкторы и инициализации полей, заданных в этих классах, вплоть до класса Object.
    • Проводится инициализация полей родительской части объекта значениями, заданными в декларации родительского класса.
    • Выполняется тело конструктора родительского класса.
  • Проводится инициализация полей дочерней части объекта значениями, заданными в декларации дочернего класса.
  • Выполняется тело конструктора дочернего класса.

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

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

Модификаторы class ИмяКласса extends ИмяРодителя {
 Задание полей;
 static {
 тело блока инициализации класса
 }

{
 тело блока инициализации объекта
 }

 Задание подпрограмм - методов класса, методов объекта, конструкторов
}

Блоков инициализации класса и блоков инициализации объекта может быть несколько.

Порядок выполнения операторов при наличии блоков инициализации главного класса приложения (содержащего метод main ):

  • инициализация полей данных и выполнение блоков инициализации класса (в порядке записи в декларации класса);
  • метод main ;
  • выполнение блоков инициализации объекта;
  • выполнение тела конструктора класса.

Для других классов порядок аналогичен, но без вызова метода main:

  • инициализация полей данных и выполнение блоков инициализации класса (в порядке записи в декларации класса);
  • выполнение блоков инициализации объекта;
  • выполнение тела конструктора класса.

Чем лучше пользоваться, блоками инициализации или конструкторами? Ответ, конечно, неоднозначен: в одних ситуациях – конструкторами, в других – блоками инициализации. Для придания начальных значений переменным класса в случаях, когда для этого требуются сложные алгоритмы, можно пользоваться только статическими блоками инициализации. Для инициализации полей объектов в общем случае лучше пользоваться конструкторами, но если необходимо выполнить какой-либо код инициализации до вызова унаследованного конструктора, можно воспользоваться блоком динамической инициализации.

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

Код с перемещением фигур не стирает старую фигуру, а просто рисует новую в новом месте. Точку, круг.