Что такое «связывание» и что делает его поздним? – Невероятные приключения в коде. Раннее и позднее связывание. Динамический полиморфизм
--- Сборки.NET --- Позднее связывание
Поздним связыванием (late binding) называется технология, которая позволяет создавать экземпляр определенного типа и вызывать его члены во время выполнения без кодирования факта его существования жестким образом на этапе компиляции. При создании приложения, в котором предусмотрено позднее связывание с типом из какой-то внешней сборки, добавлять ссылку на эту сборку нет никакой причины, и потому в манифесте вызывающего кода она непосредственно не указывается.
На первый взгляд увидеть выгоду от позднего связывания не просто. Действительно, если есть возможность выполнить раннее связывание с объектом (например, добавить ссылку на сборку и разместить тип с помощью ключевого слова new), следует обязательно так и поступать. Одна из наиболее веских причин состоит в том, что раннее связывание позволяет выявлять ошибки во время компиляции, а не во время выполнения. Тем не менее, позднее связывание тоже играет важную роль в любом создаваемом расширяемом приложении.
Класс System.Activator
Класс System. Activator (определенный в сборке mscorlib.dll) играет ключевую роль в процессе позднего связывания в.NET. В текущем примере интересует пока что только его метод Activator.CreateInstance() , который позволят создавать экземпляр подлежащего позднему связыванию типа. Этот метод имеет несколько перегруженных версий и потому обеспечивает довольно высокую гибкость. В самой простой версии CreateInstance() принимает действительный объект Type, описывающий сущность, которая должна размещаться в памяти на лету.
Чтобы увидеть, что имеется в виду, давайте создадим новый проект типа Console Application, импортируем в него пространства имен System.I0 и System.Reflection с помощью ключевого слова using и затем изменим класс Program, как-показано ниже:
Using System; using System.Reflection; using System.IO; namespace ConsoleApplication1 { class Program { static void Main() { Assembly ass = null; try { ass = Assembly.Load("fontinfo"); } catch (FileNotFoundException ex) { Console.WriteLine(ex.Message); } if (ass != null) CreateBinding(ass); Console.ReadLine(); } static void CreateBinding(Assembly a) { try { Type color1 = a.GetType("FontColor"); // Используем позднее связывание object obj = Activator.CreateInstance(color1); Console.WriteLine("Объект создан!"); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } }
Прежде чем запускать данное приложение, необходимо вручную скопировать сборку fontinfo.dll в подкаталог bin\Debug внутри каталога этого нового приложения с помощью проводника Windows. Дело в том, что здесь вызывается метод Assembly.Load(), а это значит, что CLR-среда будет зондировать только папку клиента (при желании можно было бы воспользоваться методом Assembly.LoadFrom() и указывать полный путь к сборке, но в данном случае в этом нет никакой необходимости).
Прежде чем коснуться самого применения виртуальных функций необходимо рассмотреть такие понятия как раннее и позднее связывание. Сравним два подхода к покупке, к примеру, килограмма апельсинов. В первом случае мы заранее знаем, что нам надо купить 1 кг. апельсинов. Поэтому мы берем небольшой пакет, не много, но достаточно денег, чтобы хватило на этот килограмм. Во втором случае, мы, выходя из дома, не знаем что и как много нам надо купить. Поэтому мы берем машину (а вдруг будет много всего и тяжелое), запасаемся пакетами больших и малых размеров и берем как можно больше денег. Едем на рынок и выясняется, что надо купить только 1 кг. апельсинов.
Приведенный пример в определенной мере отражает смысл применения раннего и позднего связывания, соответственно. Очевидно, что для данного примера первый вариант оптимален. Во втором случае мы слишком много всего предусмотрели, но нам это не понадобилось. С другой стороны, если по дороге на рынок мы решим, что апельсины нам не нужны и решим купить 10 кг. яблок, то в первом случае мы уже не сможем этого сделать. Во втором же случае - легко.
А теперь рассмотрим этот пример с точки зрения программирования. При применении раннего связывания, мы как бы говорим компилятору: "Я точно знаю, чего я хочу. Поэтому жестко(статически) связывай все вызовы функций". При применении механизма позднего связывания мы как бы говорим компилятору: "Я пока не знаю чего я хочу. Когда придет время, я сообщу что и как я хочу".
Таким образом, во время раннего связывания вызывающий и вызываемый методы связываются при первом удобном случае, обычно при компиляции.
При позднем связывании вызываемого метода и вызывающего метода они не могут быть связаны во время компиляции. Поэтому реализован специальный механизм, который определяет как будет происходить связывание вызываемого и вызывающего методов, когда вызов будет сделан фактически.
Очевидно, что скорость и эффективность при раннем связывании выше, чем при использовании позднего связывания. В то же время, позднее связывание обеспечивает некоторую универсальность связывания.
Наконец-то мы добрались и до самих виртуальных функций и методов. К сожалению, для иллюстрации виртуальных методов достаточно сложно провести какую-либо аналогию с физическим миром. Поэтому сразу рассмотрим этот вопрос с точки зрения программирования.
Итак, для чего же применяются виртуальные методы. Виртуальные методы существуют для того, чтобы "наследник" вел себя отлично от "предка", сохраняя при этом свойство совместимости с ним.
Приведем определение виртуальных методов:
Виртуальный метод - это метод, который, будучи описан в потомках, замещает собой соответствующий метод везде, даже в методах, описанных для предка, если он вызывается для потомка .
Адрес виртуального метода известен только в момент выполнения программы. Когда происходит вызов виртуального метода, его адрес берется из таблицы виртуальных методов своего класса. Таким образом вызывается то, что нужно.
Преимущество применения виртуальных методов заключается в том, что при этом используется именно механизм позднего связывания, который допускает обработку объектов, тип которых неизвестен во время компиляции.
Для иллюстрации применения виртуальных методов приведу пример на языке С++ , который я позаимствовал из одного C++ Tutorial . Даже если вы не очень разбираетесь в этом языке, надеюсь, что мои пояснения хоть как-то объяснят его смысл.
#include
Результаты работы программы(вывод на экран):
Транспортное средство Легковая машина Транспортное средство Лодка
Рассмотрим приведенный пример. У нас есть три класса car , truck и boat , которые являются производными от базового класса vehicle . В базовом классе vehicle описана виртуальная функция message . В двух из трех классов(car , boat ) также описаны свои функции message , а в классе truck нет описания своей функции message . Все строки, к которым я не приводил комментарии, не имеют принципиального для данного примера значения. Теперь пробежимся по основному блоку программы - функции main() . Описываем переменную unicycle , как указатель на объект типа vehicle . Не буду вдаваться в подробности, почему именно указатель на объект. Так надо. В данном случае воспринимайте работу с указателем, как с самим объектом. Подробности работы с указателями можно найти в описаниях конкретного языка ООП. Затем, создаем объект класса vehicle , переменная unicycle указывает на этот объект. После этого вызываем метод message объекта unicycle , а в следующей строке удаляем этот объект. В следующих трех блоках по 3 строки проводим аналогичные операции, с той лишь разницей, что работаем с объектами классов car , truck , boat . Применение указателя позволяет нам использовать один и этот же указатель для всех производных классов. Нас интересует вызов функции message для каждого из объектов. Если бы мы не указали, что функция message класса vehicle является виртуальной(virtual ), то компилятор статически(жестко) связал бы любой вызов метода объекта указателя unicycle с методом message класса vehicle , т.к. при описании мы сказали, что переменная unicycle указывает на объект класса vehicle . Т.е. произвели бы раннее связывание. Результатом работы такой программы был бы вывод четырех строк "Транспортное средство". Но за счет применения виртуальной функции в классе мы получили несколько другие результаты.
При работе с объектами классов car и boat вызываются их собственные методы message , что и подтверждается выводом на экран соответствующих сообщений. У класса truck нет своего метода message , по этой причине производится вызов соответствующего метода базового класса vehicle .
Очень часто класс, содержащей виртуальный метод называют полиморфным классом. Самое главное отличие заключается в том, что полиморфные классы допускают обработку объектов, тип которых неизвестен во время компиляции. Функции, описанные в базовом классе как виртуальные, могут быть модифицированы в производных классах, причем связывание произойдет не на этапе компиляции (то, что называется ранним связыванием), а в момент обращения к данному методу (позднее связывание).
Виртуальные методы описываются с помощью ключевого слова virtual в базовом классе. Это означает, что в производном классе этот метод может быть замещен методом, более подходящим для этого производного класса. Объявленный виртуальным в базовом классе, метод останется виртуальным для всех производных классов. Если в производном классе виртуальный метод не будет переопределен, то при вызове будет найден метод с таким именем вверх по иерархии классов (т.е. в базовом классе).
Последнее, о чем необходимо рассказать, говоря о виртуальных функциях, это понятие абстрактных классов. Но мы это рассмотрим на следующем шаге.
I. Основные принципы ООП
1. Инкапсуляция – принцип ООП, который заключается в объединении в единое целое данных и алгоритмов их обработки.
Данные объектав ООП называются полями объекта , а алгоритмы, т. е. действия над данными объекта,называются методами объекта , которые оформляются в виде подпрограмм.
2. Наследование – принцип ООП, который заключается в свойстве объектов порождать своих потомков.
Объект-потомок автоматически наследует от родителя все поля и методы, может дополнять объекты новыми полями и заменять или дополнять методы родителя.
3. Полиморфизм – это свойство объектов, имеющих одного общего родителя, решать разными способами схожие по смыслу задачи.
Полиморфизм методов выражается в наличии у предка и потомка методов с одинаковыми именами, имеющих разную реализацию.
II. Структура объявления объектного типа
<имя_типа>=OBJECT
<имя_поля>:<тип_поля>;
<методы>;
После определения типа объекта должны следовать описания всех методов, перечисленных в объектном типе, которые представляют собой тексты процедур и функций. Отличие от обычного описания заключается в том, что имя подпрограммы , являющейся методом объекта, указываемое в её заголовке, состоит из 2 частей:
<имя_типа>.<имя_подпрограммы>
III. Свойство наследования
родительским типом , от которого наследуются все поля и методы. Для того, чтобы произошло наследование, в определение объектного типа после OBJECT в скобках необходимо указать имя родительского типа.
Правила наследования.
1. В определении типа наследника не должно быть полей, совпадающих по именам с полями родителя; имена методов у наследников и родителя могут совпадать – в этом проявляется свойство полиморфизма .
2. При построении объектного типа наследника, в первую очередь, наследуются поля родителя, затем добавляются поля наследника; после полей наследуются все методы родителя, затем добавляются методы наследника, если их имена не совпадают с именами методов родителя; если совпадения существуют, то это означает, что методы наследника являются полиморфными методами и замещают одноименные методы предка.
Свойство полиморфизма.
Свойство полиморфизма заключается в том, что в определении типа наследника могут быть методы, одноименные с именами методов родителя.
Правила наследования при полиморфизме:
· одноименные методы наследника заменяют методы родителя;
· методы наследника, не совпадающие по именам с методами родителя, добавляются после методов родителя.
IV. Виртуальные методы
Методы объекта являются статическими методами , если компилятор размещает их и разрешает все ссылки на них во время компиляции и компоновки. Процесс, с помощью которого вызовы статических методов однозначно разрешаются компилятором во время компиляции, называется ранним связыванием .
При позднем связываниивызывающий и вызываемый методы не могут быть связаны по адресам во время компиляции, поэтому включается механизм, позволяющий выполнить связывание в тот момент, когда вызов действительно произойдет, т. е. во время выполнения программы.
Виртуальные методы и таблица виртуальных методов (ТВМ).
Позднее связывание реализуется с помощью виртуальных методов , признаком которых является наличие ключевого слова VIRTUAL после заголовка метода в определении объектного типа.
Позднее связывание осуществляется с помощью таблиц виртуальных методов (ТВМ), которые строятся компилятором в сегменте данных программы для всех виртуальных методов, описанных в программе. ТВМ представляет собой таблицу адресов процедур, которые являются виртуальными методами. Для любого объекта, содержащего виртуальные методы, требуется, чтобы в памяти находился экземпляр таблицы виртуальных методов.Для каждого объектного типа строится только одна ТВМ. Указатель на таблицу виртуальных методов автоматически заносится в специальное поле, которое имеется в каждом экземпляре объектного типа при выполнении конструктора. Обращение к ТВМ через этот указатель происходит при каждом вызове виртуального метода.
Конструктор.
метод-конструктор
Конструктор – это специальный вид процедуры, которая выполняет установочную работу для обеспечения позднего связывания. Все типы объектов, имеющих виртуальные методы, должны содержать конструктор, который всегда вызывается до первого вызова виртуального метода. Вызов виртуального метода без предшествующего обращения к конструктору может вызвать блокировку системы.
Конструктором может быть только процедура, в заголовке которой вместо служебного слова PROCEDURE необходимо написать CONSTRUCTOR.
Конструктор не может быть виртуальным.
После выполнения метода-конструктора при вызове виртуальных методов их адреса определяются из ТВМ.
Деструктор.
Деструктор служит для очистки памяти и удаления динамически размещенного объекта. Обычно в деструкторе указываются завершающие действия с объектом. В заголовке деструктора служебное слово PROCEDURE заменяется на DESTRUCTOR Для одного и того же типа объекта можно определить несколько деструкторов. Деструкторы можно наследовать, они могут быть статическими или виртуальными.
V. Пример объектного типа
1. Постановка задачи: реализовать движение точки на экране.
2. Математическая модель: каждая точка на экране характеризуется координатами x, y и состоянием v –видима/невидима.
3. Объявление объектного типа точка:
POINT=object
X, Y: integer; {координаты}
V: boolean; {признак видимости: TRUE-видима; FALSE-невидима}
function GET_X: integer; { получение координаты Х }
function GET_Y: integer; { получение координаты Y }
function GET_V: boolean; { получение признака видимости }
procedure INIT(X0, Y0: integer); { задание координат }
procedure TURN_ON; { рисование точки – получение видимой точки }
procedure TURN_OFF; { стирание точки – получение невидимой точки}
procedure MOVE(XN, YN: integer); { перемещение точки }
4. Определение методов объекта POINT:
function POINT.GET_X; { получение координаты Х }
GET_X:=X;
function POINT.GET_Y; { получение координаты Y }
GET_Y:=Y;
function POINT.GET_V; { получение признака видимости }
GET_V:=V;
procedure POINT.INIT; { задание координат }
V:=false;
procedure POINT.TURN_ON; { получение видимой точки }
if not V then
PutPixel(X,Y,GetColor); { GetColor возвращает текущий цвет }
V:=true;
procedure POINT.TURN_OFF; { получение невидимой точки – стирание точки }
if V then
PutPixel(X,Y,GetBkColor); { GetBkColor возвращает фоновый цвет }
V:=false;
procedure POINT.MOVE; { перемещение точки }
F: boolean;
if F then
TURN_OFF;
if F then
TURN_ON;
Свойство наследования.
Кроме точки, можно задать другие объекты, описывающие геометрические фигуры, например, объект типа окружность , который определяется радиусом, координатой центра, признаком видимости , и с ним возможны такие же действия, как с точкой: получение видимой окружности; получение невидимой окружности; перемещение окружности (изменение координат центра). Чтобы этот объект привязать к координатной сетке экрана, необходимо определить объектный тип PLACE (место ), который будет иметь данные, общие для всех геометрических фигур, – координаты привязки объекта к экрану X и Y :
PLACE=object
X, Y: integer;
function GET_X: integer;
function GET_Y: integer;
Каждый новый объектный тип может строиться на основе ранее определенного типа, называемого родительским типом , от которого наследуются все поля и методы. Чтобы произошло наследование, в объявлении объектного типа после OBJECT в скобках указывается имя родительского типа.
Используя тип PLACE как родительский, можно определить объектный тип POINT таким образом:
POINT=object(PLACE)
V: boolean;
procedure INIT(X0, Y0: integer);
function GET_V:boolean;
procedure TURN_ON;
procedure TURN_OFF;
6. Свойство полиморфизма заключается в том, что в объявлении типа наследника могут быть методы, одноименные с именами методов родителя.
Объявление типа окружность c использованием в качестве родителя типа POINT будет следующим:
CIRCL=object(POINT)
R: integer;
procedure INIT(X0, Y0, R0: integer); { задание окружности }
procedure TURN_ON; { получение видимой окружности }
procedure TURN_OFF; { получение невидимой окружности }
procedure MOVE(XN, YN: integer); { перемещение окружности}
function GET_R: integer; { получение радиуса }
Определение методов с использованием наследования при полиморфизме:
procedure CIRCL.INIT;
X:=X0; Y:=Y0;
V:=false;
procedure CIRCL.TURN_ON;
V:=true;
CIRCLE(X,Y,R);
procedure CIRCL.TURN_OFF;
C: byte;
C:=GetColor;
SetColor(GetBkColor); { установка цвета рисования }
Circle(X,Y,R);
V:=false;
SetColor(C);
procedure CIRCL.MOVE;
F: boolean;
if F then
TURN_OFF;
if F then
TURN_ON;
function CIRCL.GET_R;
GET_R:=R;
В результате текст процедуры CIRCL.MOVE совпадает с текстом процедуры POINT.MOVE , но машинные коды у этих процедур будут разные; CIRCL.MOVE при своем исполнении обращается к адресам процедур CIRCL.TURN_ON и CIRCL.TURN_OFF , а процедура POINT.MOVE – к адресам процедур POINT.TURN_ON и POINT.TURN_OFF .
Раннее и позднее связывание методов.
Ранее рассмотренные статические методы компилятор размещает и разрешает все ссылки на них во время компиляции и компоновки (раннее связывание ).
Для примера, описанного ранее, в объекте CIRCL можно наследовать метод MOVE у объекта POINT. Методы TURN_ON и TURN_OFF должны быть объявлены виртуальными, чтобы с ними произошло позднее связывание.
Компилятор оставляет после компиляции неразрешенными ссылки к тем методам, которые объявлены виртуальными. Для описанного ранее примера в методе MOVE неразрешенными ссылками будут адреса методов TURN_ON, TURN_OFF , т. е. процедура MOVE будет не готова к исполнению после компиляции, т. к. ее машинный код полностью не определен.
Во время выполнения программы до исполнения виртуальных методов любого объекта должен выполниться метод-конструктор для данного экземпляра объекта, в который компилятор вставляет действия по связи неразрешенных ссылок с ТВМ.
Объявления объектных типов PLACE, POINT и CIRCL с использованием конструктора и виртуальных методов:
PLACE=object
X,Y: integer;
function GET_X: integer;
function GET_Y: integer;
POINT=object(PLACE)
V: boolean;
constructor INIT(X0, Y0: integer);
function GET_V: boolean;
procedure TURN_ON; virtual;
procedure TURN_OFF;virtual;
procedure MOVE(XN, YN: integer);
CIRCL=object(POINT)
R: integer;
constructor INIT(X0, Y0, R0: integer);
function GET_R: integer;
procedure TURN_ON; virtual;
procedure TURN_OFF; virtual;
Деструктор необходим только для динамически размещенных объектов, так как он определяют фактический размер объекта для корректного освобождения памяти от динамического объекта, причем эти действия выполняются деструктором по умолчанию в дополнение к действиям, описанным в теле деструктора.
Связывание в языке C++
Двумя основными целями при разработке языка программирования С++ были эффективное использование памяти и скорость выполнения. Он был задуман как усовершенствование языка С, в частности, для объектно-ориентированных приложений. Основной принцип С++: никакое свойство языка не должно приводить к возникновению дополнительных издержек (как по памяти, так и по скорости), если данное свойство программистом не используется. Например, если вся объектная ориентированность С++ игнорируется, то оставшаяся часть должна работать так же быстро, как и традиционный С. Поэтому неудивительно что большинство методов в С++ связываются статически (во время компиляции), а не динамически (во время выполнения).
Связывание методов в этом языке является довольно сложным. Для обычных переменных (не указателей или ссылок) оно осуществляется статически. Но когда объекты обозначаются с помощью указателей или ссылок, используется динамическое связывание. В последнем случае решение о выборе метода статического или динамического типа диктуется тем, описан ли соответствующий метод с помощью ключевого слова virtual. Если он объявлен именно так, то метод поиска сообщения базируется на динамическом классе, если нет на статическом. Даже в тех случаях, когда используется динамическое связывание, законность любого запроса определяется компилятором на основе статического класса получателя.
Рассмотрим, например, следующее описание классов и глобальных переменных: class Mammal
printf («cant speak»);
class Dog: public Mammal
printf («wouf wouf»);
printf («wouf wouf, as well»);
Mammal *fido = new Dog;
Выражение fred.speak() печатает «cant speak», однако вызов fido->speak() также напечатает «cant speak», поскольку соответствующий метод в классе Mammal не объявлен как виртуальный. Выражение fido->bark() не допускается компилятором, даже если динамический тип для fido класс Dog. Тем не менее статический тип переменной всего лишь класс Mammal.
Если мы добавим слово virtual:
virtual void speak()
printf («cant speak»);
то получим на выходе для выражения fido->speak() ожидаемый результат.
Относительно недавнее изменение в языке С++ добавление средств для распознавания динамического класса объекта. Они образуют систему RTTI (Run-Time Type Identification идентификация типа во время выполнения).
В системе RTTI каждый класс имеет связанную с ним структуру типа typeinfo, которая кодирует различную информацию о классе. Поле данных name одно из полей данных этой структуры содержит имя класса в виде текстовой строки. Функция typeid может использоваться для анализа информации о типе данных. Следовательно, следующая ниже команда будет печатать строку «Dog» динамический тип данных для fido. В этом примере необходимо разыменовывать переменную-указатель fido, чтобы аргумент был значением, на которое ссылается указатель, а не самим указателем:
cout << «fido is a» << typeid(*fido).name() << endl;
Можно также спросить, используя функцию-член before, соответствует ли одна структура с информацией о типе данных подклассу класса, соотносящегося с другой структурой. Например, следующие два оператора выдают true и false:
if (typeid(*fido).before (typeid(fred)))…
if (typeid(fred).before (typeid(lassie)))…
До появления системы RTTI стандартный программистский трюк состоял в том, чтобы явным образом закодировать в иерархии класса методы быть экземпляром. Например, для проверки значения переменных типа Animal на принадлежность к типу Cat или к типу Dog можно было бы определить следующую систему методов:
virtual int isaDog()
virtual int isaCat()
class Dog: public Mammal
virtual int isaDog()
class Cat: public Mammal
virtual int isaCat()
Теперь для определения того, является ли текущим значением переменной fido величина типа Dog, можно использовать команду fido->isaDog(). Если возвращается ненулевое значение, то можно привести тип переменной к нужному типу данных.
Возвращая указатель, а не целое число, мы объединяем проверку на принадлежность к подклассу и приведение типа. Это аналогично другой части системы RTTI, называемой dynamic_cast, которую мы вкратце опишем. Если некая функция в классе Mammal возвращает указатель на Dog, то класс Dog должен быть предварительно описан. Результатом присваивания является либо нулевой указатель, либо правильная ссылка на класс Dog. Итак, проверка результата все еще должна осуществляться, но мы исключаем необходимость приведения типа. Это показано в следующем примере:
class Dog; // предварительное описание
virtual Dog* isaDog()
virtual Cat* isaCat()
class Dog: public Mammal
virtual Dog* isaDog()
class Cat: public Mammal
virtual Cat* isaCat()
Оператор lassie = fido->isaDog(); теперь выполним всегда. В результате переменная lassie получает ненулевое значение, только если fido имеет динамический класс Dog. Если fido не принадлежит Dog, то переменной lassie будет присвоен нулевой указатель.
lassie = fido->isaDog();
… // fido и в самом деле относится к типу Dog
… // присваивание не сработало
… // fido не принадлежит к типу Dog
Хотя программист и может использовать этот метод для обращения полиморфизма, недостаток такого способа состоит в том, что требуется добавление методов как в родительский, так и в дочерний классы. Если из одного общего родительского класса проистекает много дочерних, метод становится громоздким. Если изменения в родительском классе не допускаются, такая техника вообще невозможна.
Поскольку подобные проблемы встречаются часто, было найдено их общее решение. Функция шаблона dynamic_cast берет тип в качестве аргумента шаблона и, в точности как функция, определенная выше, возвращает либо значение аргумента (если приведение типа законно), либо нулевое значение (если приведение типа неразрешено). Присваивание, эквивалентное сделанному в предыдущем примере, может быть записано так:
// конвертировать только в том случае, если fido является собакой
lassie = dynamic_cast < Dog* > (fido);
// затем проверить, выполнено ли приведение
В язык C++ были добавлены еще три типа приведения (static_cast, const_cast и reinterpret_cast), но они используются в особых случаях и поэтому здесь не описываются. Программистам рекомендуется применять их как более безопасные средства вместо прежнего механизма приведения типов.
2. Проектная часть
Данный параграф , несмотря на краткость, является очень важным – практически все профессиональное программирование в 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 .
В объектно-ориентированных языках программирования различают две разновидности динамических методов – собственно динамические и виртуальные . По принципу работы они совершенно аналогичны и отличаются только особенностями реализации. Вызов виртуальных методов быстрее. Вызов динамических медленнее, но служебная таблица динамических методов ( DMT – Dynamic Methods Table ) занимает чуть меньше памяти, чем таблица виртуальных методов ( VMT – Virtual 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 .
В конструкторах очень часто используют