Лекция №1. Введение
Вид материала | Лекция |
- С. В. Шадрина Лекция 5 сентября, 15: 00-16: 30, Введение в геометрию пространства модулей, 5.97kb.
- Первая лекция. Введение 6 Вторая лекция, 30.95kb.
- Текст лекций н. О. Воскресенская Оглавление Лекция 1: Введение в дисциплину. Предмет, 1185.25kb.
- А. И. Мицкевич Догматика Оглавление Введение Лекция, 2083.65kb.
- Лекция введение в экологию (В. И. Торшин), 1146.79kb.
- Конспект лекций н. О. Воскресенская Москва 2008 Оглавление: Лекция Введение в дисциплину, 567.5kb.
- План лекций педиатрический факультет 1 семестр 1 лекция. Введение в анатомию человека., 216.63kb.
- Сидоров Сергей Владимирович Планы лекций Введение в профессионально-педагогическую, 19.81kb.
- Русской Православной Церкви и их особенности. 22 сентября лекция, 30.24kb.
- План лекций: Лекция №1. Введение в тему, общие сведения. Введение, 99.54kb.
В языке программирования Java методы вызываются посредством операции, аналогичной доступу к полям данных. Синтаксическая форма состоит из получателя, за которым следует селектор – функция точка, селектора сообщения, который соответствует имени метода и списка аргументов в круглых скобках. Java не поддерживает методов с переменным количеством параметров. Если MyCard является объектом класса Card, то предложение
MyCard.suit( )
приказывает карте сообщить свою масть. Все аналогично языку «С++», за исключением использования псевдопеременной this. В языке Java this представляет из себя не указатель, а объект. Это связано с тем, что в Java указатели просто отсутствуют. Объект this используется для передачи ссылки на текущий объект в качестве параметра для других методов, например:
int color ( )
{
if (suit(this) == heart || suit(this) == diamond)
return red;
return black
} .
Обычно this используется в случае необходимости, то есть, когда имя поля, к которому происходит обращение, скрывается объявлением некоторой переменной или параметра. Ссылка this указывает, что поле принадлежит текущему объекту, например:
this.r
Таким образом, идеология передачи сообщений в ОО- языках практически одинакова. Разница заключается лишь в обозначениях псевдопеременных и использовании этих псевдопеременных.
Способы создания объектов.
Интуитивно ясно, что после объявления класса необходимо создать его экземпляры – объекты, чтобы можно было с ними работать. Это можно делать по – разному, но, вообще говоря, каждому вновь создаваемому объекту необходимо выделять память. Память может быть выделена либо статическая, либо динамическая (в пуле). Ранние версии ОО – языков программирования предоставляли программисту выбор. Статическое распределение памяти заключалось в объявлении соответствующего типа, а затем – переменной этого типа. Например, в языке Borland – Pascal (не Delphi!!!) это могло выглядеть так:
Type
Location = Object
X,Y:integer;
Procedure (………);
……………….
end;
Далее можно было объявлять статические переменные типа Location, например:
Var
My_Location: Location;
Далее можно было в теле программы передавать сообщения объекту My_Location в соответствии с синтаксисом, рассмотренным выше. Это переменная (объект) статического типа. Можно было объявить еще один тип:
Location_Ptr = Location; затем переменную – указатель My_Location_Ptr : Location_Ptr; создать с помощью функции New объект
My_Location_Ptr := New(Location_Ptr); и далее работать с полями и методами объекта через указатель на него, например:
My_X := My_Location_Ptr.X; и т. п.
В первом случае говорят об автоматических переменных объектного типа, а во втором – о динамических переменных объектного типа.
Существенная разница между автоматическими и динамическими переменными заключается в следующем: память для автоматических переменных создается при входе в некоторый блок, управляющий этими переменными. При выходе из блока память автоматически освобождается. В момент, когда создаются автоматические переменные, происходит связывание идентификатора и определенного участка памяти, и эта связь не может быть изменена в течении времени существования переменной.
Динамические переменные создаются соответствующими вызовами языковых процедур типа malloc или new. В результате вызова таких процедур выделяется новый участок памяти в пуле, а в качестве результата получается указатель на этот участок. Процессы выделения памяти и привязки ее к имени взаимосвязаны. Динамические объекты очень удобны для создания изменяющихся структур – списков, деревьев и т. п.
Восстановление памяти.
Уничтожение автоматических переменных всегда происходит автоматически по выходу из блока, в которых они были объявлены. Уничтожение динамических переменных, как правило, необходимо выполнять в явном виде. Программист, пишущий на «С++» или Delphi, должен сам отслеживать, какие данные ему более не нужны, и в явном виде освобождать эту память с помощью процедуры free.
В языке Java восстановление памяти происходит автоматически. Если к какой – либо переменной, в частности, к объекту, больше нет доступа, т. е., он изолирован от последующих вычислений, то он автоматически уничтожается, а выделенная память помечается как свободная. Этот процесс называется сборкой мусора. Java выполняет всю сборку мусора автоматически и избавляет от необходимости явного освобождения объектов. Это означает, что память, занимаемая объектом, может быть возвращена в систему. Объект считается неиспользуемым (изолированным), если на него отсутствуют ссылки в статических данных, когда не удается найти ссылку на него посредством отслеживания полей и элементов статических данных и переменных методов, и т. п. Объекты создаются оператором new, но соответствующего ему оператора освобождения в Java не существует. После завершения работы с объектом на него перестают ссылаться, например, изменением ссылки на другой объект или на null.
Когда ссылок на объект более нигде не остается, за исключением других изолированных объектов, данный объект может быть уничтожен сборщиком мусора. Выражение «может быть» присутствует, потому, что память освобождается только в том случае, если ее недостаточно, или сборщик мусора захочет предотвратить ее фрагментацию.
Автоматическая сборка мусора избавляет от проблемы «зависших ссылок». В тех системах, где предусмотрено освобождение объектов в явном виде, допускается удаление объектов, на которые ссылаются другие объекты. В этом случае ссылка становится «зависшей», т. е. указывает на область памяти, которая свободна. Эта свободная память может быть использована для создания нового объекта, и тогда «зависшая» ссылка будет указывать на то, что совершенно отлично от предполагаемого. Такая ситуация может привести к развалу не только программы, но и всей системы в целом. Java решает эту проблему, поскольку объект, на который имеется хотя бы одна ссылка никогда не будет уничтожен.
Лекция №6. Неизменяемые объекты.
Внутри объектов может возникнуть необходимость иметь поля данных, которые, будучи однажды определены, не изменяли бы свое значение во время работы программы. Переменные такого типа называют переменными с однократным присваиванием, или неизменяемыми переменными. Объект, у которого все переменные являются неизменяемыми, называется неизменяемым объектом. Неизменяемые переменные следует отличать от констант, хотя в значительной степени разница состоит только во времени связывания и области видимости. Константа должна быть известна на момент компиляции, иметь глобальную область видимости и оставаться неизменной. Неизменяемым переменным можно присваивать значения, но только однажды. Таким образом, значение остается неопределенным вплоть до момента выполнения программы, т.е., до момента создания и инициализации объекта, содержащего это значение. Языки программирования «С++», Delphi и Java обеспечивают возможность создания неизменяемых объектов.
Создание и инициализация объектов в Delphi.
В языке Delphi – Pascal все объекты являются динамическими. Для создания объектов используется специальный метод, называемый конструктором. Он объявляется в определении класса с помощью ключевого слова Constructor. В Delphi – Рascal конструктор обычно имеет название Create. С помощью вызова конструктора выделяется память под новое значение (создается динамический объект). Внутри конструктора могут содержаться операторы, присваивающие начальные значения нужным полям создаваемого объекта. Таким образом объект создается путем использования метода – конструктора в виде сообщения, посылаемого собственно классу, например:
MyCard:=Card.Create(5, Spade);
создает объект класса Card со значением «пятерка пик». Следует отметить, что MyCard является указателем на объект класса Card, а не собственно объектом. Однако синтаксис языка Delphi – Pascal таков, что при обращениям к полям и методам объекта, на который указывает указатель, не нужно и неправильно пользоваться оператором . Обращение к полям и методам объекта происходит через селектор – функцию «точка».
Язык Delphi – Pascal не поддерживает в явном виде неизменяемые поля данных, однако они могут быть смоделированы с помощью конструкции называемой «свойство». Эта конструкция объявляется с помощью ключевого слова «property». Поле property объявляется и обрабатывается подобно полям данных (доступ к значению осуществляется по имени, а запись – через оператор присваивания). Однако синтаксис скрывает истинный механизм доступа. И присваивание, и доступ осуществляются через специальную функцию. Например:
Property ReadOnly: integer; read ReadArgument;
Property Read_Write: integer; read ReadArgument;
write CheckArgument;
………………………………………………………………….
Private
Procedure ReadArgument(Arg:integer);
Procedure CheckArgument(Arg:integer);
Если поле property содержит ключевое слово read, то оно имеет статус «только для чтения», если ключевое слово write, то статус «только для записи». Эти действия на самом деле выполняются с помощью специальных методов, доступных только внутри объекта.
Язык Delphi – Pascal не поддерживает автоматической сборки мусора, поэтому в нем используется специальный метод освобождения памяти. Этот метод имеет ключевое слово Destructor. Метод – деструктор обычно называется Destroy. Отсюда следует, что любой класс в Delphi – Pascal обязан иметь хотя бы два собственных (ненаследуемых) метода: конструктор и деструктор.
Создание и инициализация объектов в С++.
Объекты в С++ всегда динамические переменные. Инициализация в С++ производится с помощью конструкторов. В С++ конструктор – это метод, имеющий то же имя что и класс, к которому принадлежит создаваемый объект. Конструктор автоматически и неявно вызывается каждый раз, когда создается объект, принадлежащий к соответствующему классу. Это происходит, когда объект создается с помощью оператора new. В языке С++ реализована возможность перегружать имена функций – членов. Функция называется перегруженной, если имеются две или более функции с одним и тем же именем. Возникающая при этом неоднозначность при вызове функций в С++ разрешается за счет различий в списке аргументов. Это свойство перегружаемости справедливо и для конструкторов. Пусть, например, задан класс комплексных чисел:
class Complex
{
public
Complex( );
Complex(double);
Complex(double, double);
private
double re;
double im;
………………………………………..
};
В этом примере определено три конструктора класса. Какой конструктор будет вызван, зависит от аргументов, используемых при создании переменной. Объявление без аргументов приведет к вызову первого конструктора. Его описание может быть, например, таким:
Complex::Complex( )
{
re=0.0; im=0.0
};
В случае выполнения следующего оператора произойдет вызов конструктора с одним аргументом:
Сomplex pi = 3.14;
Тело такого конструктора может иметь такой вид:
Complex :: Complex (double r)
{ re = r; im = 0.0 };
Соответствующим образом может быть устроен и двухаргументный конструктор.
Тело конструктора часто представляет собой последовательность необходимых операторов присваивания. Эти операторы могут быть заменены инициализаторами в заголовке функции. Каждый инициализатор представляет собой просто имя переменной экземпляра, а в круглых скобках стоит список значений, которые используются для инициализации переменной. Значения в С++ могут быть объявлены неизменяемыми с помощью ключевого слова const. Такие данные становятся константами и их не разрешается изменять. Переменные объекта, описанные как константы, обслуживаются только инициализаторами.
В языке С++ отсутствует автоматическая сборка мусора, поэтому могут определяться функции, которые автоматически вызываются при освобождении памяти, выделенной под объект. Такие функции называются деструкторами. Функция – деструктор получает имя класса с предшествующим символом «тильда» (). Она не имеет аргументов и редко вызывается в явном виде. Освобождение памяти в С++ производится с помощью оператора delete. Оператор delete в случае объекта автоматически вызовет деструктор. В теле деструктора должны присутствовать необходимые действия.
Создание и инициализация объектов в Java.
В языке Java поддерживаются только динамические объекты. Они создаются с помощью оператора new. При создании объекта этим оператором необходимо указать тип конструируемого объекта и необходимые параметры. Runtime -–система выделяет область памяти, достаточную для хранения полей объекта и инициализирует ее в соответствии с правилами, рассмотренными ниже. После завершения инициализации runtime - система возвращает ссылку на созданный объект. Если системе не удается выделить память, достаточную для создания объекта, она запускает процесс сборки мусора, который освободит неиспользуемую память. Если и после этого памяти все равно не хватает, оператор new возбуждает исключение OutOfMemoryError. Пример создать «пятерку пик» выглядит следующим образом:
Card aCard = new Card(Card.spade, 5);
Каждый вновь создаваемый объект обладает некоторым исходным состоянием. Значения полей могут инициализироваться при их объявлении, а именно: после имени переменной необходимо поставить оператор присваивания, а после него – нужное выражение.
Если при объявлении поля класса не инициализируются, то Java присвоит им значения по умолчанию. Java не присваивает никаких исходных значений локальным переменным конструктора. Отсутствие исходного значения у локальной переменной конструктора представляет собой программную ошибку, поэтому, перед использованием локальных переменных их необходимо инициализировать.
При определении исходного состояния объекта простой инициализации может оказаться недостаточно. В таких случаях используются конструкторы. Имя конструктора, как и в С++, совпадает с именем класса, объект которого он инициализирует. Но, в отличие от С++, конструкторы в Java не поддерживают инициализаторов. Кроме того, конструктор в Java может вызвать другие конструкторы того же объекта. Это свойство позволяет исключить из нескольких конструкторов общие операторы. Конструктор при этом должен вызываться с помощью ключевого слова this, а именно:
class NewClass
{ NewClass (int i)
// инициализация первого типа
…………………………………………….
}
NewClass (int i, int j)
{ this (i);
// продолжение инициализации
…………………………………….
}
}
Такой метод называется явным вызовом конструктора. Конструктор, не имеющий при вызове никаких аргументов, называется безаргументным конструктором. Если в классе не будут объявлены никакие конструкторы, то по умолчанию создается безаргументный конструктор. Такой конструктор является «пустым», т. е., ничего не делает.
Также как и С++, Java поддерживает перегрузку методов. В Java каждый метод обладает определенной сигнатурой, которая представляет собой совокупность имени с определенным количеством и типом параметров. Два или более метода могут иметь одинаковые имена, если их сигнатуры отличаются. Это справедливо и для конструкторов.
Деструкторы в Java отличаются и от С++, и от Delphi – Pascal. Обычно уничтожение неиспользуемых объектов в Java происходит автоматически, вызовом метода finalize. Он также автоматически вызывается при завершении работы виртуальной машины. Кроме того, метод finalize дает возможность использовать удаление объекта для освобождения других, не связанных с Java ресурсов. Он объявляется следующим образом:
protected void finalize ( ) throws Throwable
{ super. finalize ( );
…………………………………..
}
Роль метода finalize становится особенно существенной при работе с внешними ресурсами, например, с файлами. Открытые файлы, количество которых обычно ограничено, не могут дождаться завершающей фазы finalize, так как нет никакой гарантии, что объект, содержащий открытый файл, будет уничтожен сборщиком мусора до того, как израсходуются все ресурсы по открытию файлов. Поэтому, в объекты, владеющие внешними ресурсами, необходимо включать метод finalize, освобождающий ресурсы и предотвращающий их утечку. Например, в классе, который открывает файл для своих целей, следует включить метод закрытия файла, чтобы можно было явным образом управлять количеством открытых в системе файлов. Однако может случиться так, что метод закрытия не будет вызван, несмотря на то, что работа с файлом уже закончена. Этого можно избежать, если включить в класс метод finalize, внутри которого вызывается метод закрытия файла.
public class ProcessFile
{ private Stream File;
public ProcessFile( string FileName);
{ File = new Stream(FileName);
}
………………………………………….
public void close ( )
{ if (File ! = null )
{ File.Close ( );
File = null;
}
}
protected void finalize ( ) thtows Throwable
{ super.finalize ( );
close ( );
}
}
Метод close написан так, что может вызываться несколько раз. В противном случае, если бы метод close был бы вызван ранее, при вызове finalize происходила бы попытка повторного закрытия файла.
При завершении работы приложения для всех существующих объектов вызываются методы finalize. Это происходит независимо от причин, вызвавших завершение работы приложения. Поэтому, некоторые системные ошибки могут привести к тому, что часть методов finalize не будет запущена. Например, если программа аварийно завершилась из – за нехватки памяти, то сборщику мусора может не хватить памяти для поиска всех объектов и вызова их метода finalize. Но в общем случае принято считать, что деструктор будет вызван для всех объектов.
Лекция №7. Наследование.
Одним из самых важных и мощных свойств технологии ООП является наследование. Под наследованием понимается возможность доступа представителей подкласса к данным и методам надкласса, возможно, отстоящего на несколько ступеней в иерархии. В языках программирования наследование означает, что поведение и данные, связанные с подклассами, всегда являются расширением (т. е., большим множеством) свойств, связанных с надклассом.
Наследование всегда транзитивно, так что класс может наследовать свойства надклассов, отстоящих от него на несколько уровней в иерархии. В языках программирования Delphi – Pascal, С++ и Java определен некоторый базовый (абстрактный) класс. Все создаваемые классы являются его наследниками. Свойство наследования дает возможность легко строить новые классы, наделяя их всеми свойствами надклассов.
Подкласс, подтип и принцип подстановки.
Для связей надкласса и подклассов справедливы следующие утверждения:
- Представители (объекты) подкласса должны владеть всеми областями данных надкласса.
- Объекты подкласса должны обеспечивать выполнение, по крайней мере, через наследование, всех функций надкласса, если только не имеет место явное переопределение. Однако у подкласса могут появиться и новые свойства.
- Объект подкласса должен имитировать поведение надкласса и должен быть неотличим от объекта надкласса в сходных ситуациях.
Эти утверждения можно формализовать в виде следующего принципа подстановки.
Если есть два класса С1 и С2 такие, что С2 является подклассом С1, возможно отстоящим от него на несколько ступеней в иерархии, то всегда существует возможность подставить объект класса С2 вместо объекта класса С1 в любой ситуации.
Языки программирования с автоматическими типами данных, такие, как С++ и Delphi – Pascal, делают более сильный упор на принцип подстановки, чем языки с динамическими данными (Java). Это происходит в силу того, что первые характеризуют объекты через приписанные им классы, а вторые – через их поведение. Например, полиморфная функция (функция, которая может принимать в виде аргументов объекты различных классов) в языке с автоматическими типами данных требует, чтобы все аргументы были бы подклассами нужного класса. В языках с динамическими типами данных аргументы не имеют типа, поэтому сформулированное выше требование просто означает, что должен уметь отвечать на определенный набор сообщений. Полиморфизм будет рассмотрен ниже.
Формы наследования.
Наследование применяется по-разному. Иногда происходит так, что различные методы класса реализуют наследование по-разному.
Наиболее часто порождение подклассов и наследование используются для специализации. При порождении подкласса для специализации новый класс является специализированной формой надкласса и удовлетворяет спецификациям надкласса во всех существенных моментах. Например, класс «окна» предоставляет общие операции с окнами (сдвиг, изменение размеров и. т. п.). Специализированный подкласс текстовых окон наследует операции с окнами и дополнительно обеспечивает средства, позволяющие окну отображать и редактировать текстовую информацию. Поскольку класс текстовых окон удовлетворяет всем свойствам, ожидаемым от окон общего вида, то он является подтипом класса окон общего вида. Таким образом, для данной формы наследования полностью выполняется принцип подстановки. Специализация является наиболее предпочтительной формой наследования, к которой и необходимо стремиться.
Второй формой наследования является порождение классов для спецификации. Данный вариант используется для того, чтобы гарантировать поддержку классами определенного общего интерфейса, т. е., реализацию ими одних и тех же методов. Надкласс может быть комбинацией реализованных методов, и методов, осуществление которых будет доверено подклассам. Таким образом, нет никаких различий в интерфейсе между надклассом и подклассами. Последние просто обеспечивают выполнение описанного, но не реализованного в родительском классе поведения. Вообще говоря, это специальный случай порождения специализированного подкласса, за исключением того, что подклассы являются не усовершенствованием существующего класса, а скорее реализацией абстрактной спецификации. В таких случаях надкласс называют абстрактно – специфицированным классом. В общем случае, порождение подклассов для спецификации распознается по тому, что фактическое поведение не определено в надклассе – оно только описано, и будет реализовано ниже.
Еще одна форма наследования – это порождение подкласса с целью конструирования. Класс наследует почти все функции надкласса, изменяя только имена методов или определенным образом модифицируя их аргументы. Это может происходить даже в том случае, когда новому и родительскому классам не удается сохранить между собой отношение «быть экземпляром». В языках программирования с автоматическими типами данных порождение подкласса с целью конструирования нарушает принцип подстановки, а именно: появляются подклассы, не являющиеся подтипами. Эта форма чаще используется в языках с динамическими типами данных, так как является быстрым и легким способом построения новых абстракций. Например, пусть создается класс, записывающий значения в двоичный файл. Родительский класс обеспечивает, как правило, запись только неструктурированных двоичных данных. Подкласс строится для каждой структуры, требующей сохранения. Он реализует процедуру хранения для определенного типа данных, которая использует методы родительского класса для непосредственной записи. Этот пример, вообще говоря, иллюстрирует некоторую расплывчатость категорий. Если подкласс реализует процедуру хранения, используя другое имя метода, то говорят о наследовании для конструирования. Если же дочерний класс пользуется тем же именем, что и надкласс, то говорят о наследовании для спецификации. Язык С++ предоставляет механизм закрытого наследования, позволяющий порождать подклассы для конструирования без нарушения принципа подстановки.
Использование наследования при порождении подклассов для обобщения представляет собой еще одну форму наследования. В этом случае подкласс расширяет надкласс для создания объекта более общего типа. Порождение подкласса для обобщения часто используется в случае, когда общий проект основывается в первую очередь на значениях данных, и в меньшей степени на функциональном поведении. Пусть например, имеется система графического отображения, в которой определен класс окон для черно – белого фона. Необходимо создать тип цветных окон. Цвет фона в таком окне будет отличаться от белого за счет добавления нового поля, содержащего цвет. В этом случае придется переопределить наследуемую процедуру отображения окна, в которой происходит фоновая заливка. В случае порождения для обобщения происходит переворачивание иерархии, поэтому такого наследования следует избегать.
Еще одна форма наследования – это порождение подкласса с целью расширения. В этом случае подклассу добавляются совершенно новые свойства. Его можно отличит по тому, что порождение для обобщения обязано переопределить хотя бы один метод надкласса. При этом функциональные возможности подкласса остаются привязанными к родительским. Расширение просто добавляет новые методы. Например, пусть есть класс множество текстовых строк, наследующий свойства общего класса множеств. Подкласс предназначен для хранения строковых величин. Он может предоставить дополнительные методы для строковых операций, например, найти по префиксу, который возвращал бы подмножество всех элементов множества, начинающихся с определенной подстроки. Такие операции имеют смысл для подкласса, но не для родительского класса. Поскольку функциональные возможности надкласса остаются нетронутыми, такие подклассы всегда будут подтипами и принцип подстановки не нарушается.
Иногда появляется необходимость иметь подкласс, представляющий собой комбинацию двух или более родительских классов. Например, аспирант имеет характерные особенности как преподавателя (ведет занятия), так и студента (сам ходит на занятия и сдает экзамены). Следовательно, он может вести себя двояко. Способность наследовать от двух или более родительских классов называется множественным наследованием.
Резюме.
- Специализация. Дочерний класс является более конкретным, частным или специализированным случаем родительского класса, или, подтипом родительского класса.
- Спецификация. Родительский класс описывает поведение, которое потом реализуется в подклассе. В родительском классе оно не реализовано.
- Конструирование. Дочерний класс использует методы, предоставляемые родительским классом, но не является подтипом родительского класса. Принцип подстановки нарушается.
- Обобщение. Дочерний класс переопределяет или модифицирует некоторые методы родительского класса с целью получения объекта более общей категории.
- Расширение. Дочерний класс добавляет некоторые новые функциональные возможности к родительскому классу, но не меняет наследуемое поведение.
- Комбинирование. Дочерний класс наследует черты более чем одного родительского класса. Это – множественное наследование.
Наследование в различных языках программирования.
Между всеми ОО – языками программирования существует следующее различие, делящее их на два «класса». Одни языки программирования допускают наследование только от некоторого общего родительского класса, например, Object, другие допускают различные независимые иерархии классов. Преимущество наследования с единым предком в том, что функциональные возможности последнего наследуются всеми классами. Таким образом гарантируется, что каждый объект обладает общим минимальным уровнем функциональности. Недостаток состоит в том, что единая иерархия зацепляет все классы друг с другом.
В случае нескольких независимых иерархий наследования приложению не приходится тащить за собой большую библиотеку классов, из которой лишь немногие будут использоваться в каждой конкретной программе. Это означает отсутствие функциональности, которой гарантированно обладают все объекты.
В языках программирования с динамическими типами данных объекты в основном характеризуются теми сообщениями, которые они принимают. Если два объекта принимают одно и то же множество сообщений и реагируют на них сходным образом, они неразличимы с практической точки зрения. В этом случае разумно, чтобы все объекты наследовали большую часть своего поведения от общего базового класса.
Наследование в Delphi – Pascal.
В Delphi – Pascal имеется класс TObject, который является общим предком всех классов. Любой объявляемый класс должен иметь в качестве базового класс TObject. При объявлении подкласса имя наследуемого класса берется в круглые скобки. Конструктор подкласса обязан в явном виде вначале вызывать конструктор родительского класса. Ключевое слово virtual, помещенное после объявления метода, означает, что он может быть переопределен в дочернем классе. Порождение подкласса для спецификации формализовано с помощью квалификатора abstract. Если метод объявлен как abstract, то он определяется в дочернем классе.
Наследование в С++.
В С++ новый класс не обязан происходить от уже существующего класса. Наследование указывается в заголовке описания класса с помощью ключевого слова public, за которым следует имя родительского класса. Ключевое слово public может быть заменено на ключевое слово private, указывающее на порождение класса для конструирования, т. е., на форму наследования, не создающую подтипа. Язык С++ поддерживает множественное наследование, т. е., новый класс может быть определен как потомок двух или более надклассов. Ключевое слово virtual, предшествующее описанию функции – члена, означает, что эта функция будет переопределена в подклассе или что эта функция сама переопределяет функцию надкласса.
Наследование в Java.
Подклассы в Java объявляются с помощью ключевого слова extends.
Class Location
{
………
}
class Point extends Location
{
………………
}
Все классы происходят от единого предка Object. Если родительский класс явно не указан, то предполагается класс Object. Все подклассы являются подтипами, т. е., объект подкласса может быть присвоен переменной, объявленной с типом надкласса. В языке Java идея порождения подкласса для спецификации формализована с помощью ключевого слова abstract. Если класс объявлен как abstract, то из него должны порождаться подклассы. Объекты класса abstract создавать нельзя.
Лекция №8. Подклассы и подтипы.
Одной из важных особенностей ОО – языков программирования является тот факт, что фактический тип переменной может не совпадать с типом, заявленным в ее описании. Это является одним из аспектов полиморфизма, который будет рассмотрен подробно далее. В традиционных языках программирования (Pascal, С), если переменная описана как integer (int), то содержимое области памяти, отведенное под эту переменную, гарантированно будет интерпретироваться как целая величина. В ОО – языках программирования это, вообще говоря, не всегда так.
Статическим называется тип переменной, присвоенный ей при ее описании. Динамическим типом называется тип, характеризующий ее фактическое значение. В ООП фактический и динамический типы могут и не совпадать.
Переменная, для которой динамический тип может не совпадать с ее статическим типом, называется полиморфной. (Полиморфизм – множество форм!!).
Говорят, что тип В есть подтип типа А, если в любой ситуации можно подставить объект класса В вместо объекта класса А без каких – либо видимых изменений в поведении.
Понятие подтипа соответствует идеальному принципу подстановки, однако понятие подтипа и подкласса не идентичны. Так как подклассы могут переопределять методы родительских классов, то нет никаких гарантий того, что подкласс будет также и подтипом.
Связывание методов и сообщения.
Говорят, что сообщение, направляемое объекту, связано с методом. Очень важным является вопрос о способе связывания. Нужно ли связывать метод и сообщение, основываясь на статическом типе переменной, или следует принимать во внимание ее динамический тип? Существование полиморфной переменной подразумевает наличие двух представлений о ней. Переменную можно рассматривать с точки зрения ее описания, т. е., статически, или с точки зрения ее текущего значения, т. е., динамически. Это различие становится особенно важным, если имеется метод, определенный в родительском классе и переопределенный в подклассе. Пусть, например, имеется класс «Окно», в котором определен метод получения координат мыши при нажатии на ее клавишу. Класс «Окно», в свою очередь, имеет подкласс «Текстовое окно», в котором тоже определен метод получения координат мыши с тем же названием, т.е., имеет место переопределение методов. Когда полиморфная переменная получает сообщение о нажатии на кнопку мыши, то возникает вопрос, с каким из методов должно связываться это сообщение. В случае, если бы метод получения координат не был бы определен в подклассе, то был бы однозначно вызван метод родительского класса. (В силу принципа наследования – вверх по иерархии классов). Такое связывание может быть разрешено однозначно и выполнено на уровне компилятора. Оно называется ранним связыванием. Если же имеется переопределение методов, то выполнить связывание на этапе компиляции невозможно. Поэтому решение откладывается до времени выполнения, когда можно будет запросить уже конкретный объект. Такое связывание называется поздним связыванием.
Обращение полиморфизма.
Принцип подстановки гласит, что переменной, описанной как объект родительского класса, можно присвоить значение подкласса. Возможно ли обратное, т. е., если есть переменная Х типа «Окно» , и ей присвоено значение переменной Y типа «Текстовое окно», то возможно ли переменной Z типа «Текстовое окно» присвоить значение переменной Х? Этот вопрос содержит в себе две тесно связанные проблемы. Пусть, например, есть класс бильярдных шаров Ball и два его дочерних класса: BlackBall и WhiteBall – черные и белые шары. Далее пусть имеется некий программный эквивалент коробки, в которую можно «положить» двух представителей класса Ball, один из которых затем случайно извлекается обратно. В «коробку» кладутся два шара: черный и белый и вынимается один шар по вышеприведенному правилу. Необходимо определить, какой шар извлечен.
Очевидно, что вынутый из коробки объект однозначно является объектом родительского класса Ball и поэтому может быть присвоен переменной этого типа. Но совершенно не ясно, к какому дочернему типу он принадлежит. Этот пример, несмотря на кажущуюся искусственность, является иллюстрацией общей проблемы.
Пусть проектируются классы для часто используемых структур данных: списки, деревья и т. п., которые используются для хранения совокупности объектов. Такие классы называют контейнерными классами. Контейнеры при определенных обстоятельствах похожи на пример с шарами. Если программа помещает в контейнер некоторые объекты, а затем извлекает их оттуда, то должен существовать механизм, позволяющий определить принадлежность извлеченного объекта. Эта проблема называется обращением полиморфизма и является вполне разрешимой. Все ОО – языки имеют в своем распоряжении механизмы обращения полиморфизма, т. е., распознавания динамического класса объекта.
Связывание в Delphi – Pascal.
Язык Delphi – Pascal является языком программирования со статическими типами данных в том смысле, что каждый идентификатор в программе должен быть описан. Понятия подкласса и подтипа в нем объединены. Предполагается, что подклассы являются подтипами и что идентификатор, описанный как объект, может иметь значение этого типа или любого другого, полученного из него путем наследования. Объекты в Delphi – Pascal несут с собой информацию об их собственном динамическом типе. Класс объекта в Delphi – Pascal может быть протестирован с помощью оператора is. Он возвращает значение true, когда класс левого аргумента совпадает с именем класса справа, или же тип является его подклассом. Оператор as осуществляет безопасное приведение динамического типа данных. Если левый аргумент не является экземпляром правого класса, то возникает исключение. В противном случае он приводится к типу правого аргумента. Эти механизмы используются при работе с обращением полиморфизма. Например:
If aBall is BlackBall then
bBall:= aBall as BlackBall
else { Нельзя преобразовать шар в черный }
Истинный класс объекта, т. е., действительное имя класса, а не с точностью до наследования, можно узнать из поля ClassInfo, имеющегося в каждом объекте:
if aBall.ClassInfo = BlackBall then……………
В Delphi – Pascal всегда используется динамическое связывание, т. е., исходя из динамического типа данных. Законность пересылки сообщения определяется статическим классом получателя. Только если статический класс понимает пересылаемое сообщение, компилятор буден генерировать код для обработки сообщения.
Связывание в С++.
Язык С++ не поддерживает принципа подстановки, за исключением использования указателей и значений – ссылок. Связывание методов в С++ является достаточно сложным. Для обычных переменных (не указателей или ссылок) оно осуществляется статически. Но когда объекты объявляются с помощью указателей или ссылок, то используется динамическое связывание. В последнем случае решение о выборе метода статического или динамического типа диктуется тем, описан ли соответствующий метод с ключевым словом virtual. Если он объявлен именно так, то поиск сообщения базируется на динамическом классе, если нет, то на статическом. Даже в тех случаях, когда используется динамическое связывание, правильность любого запроса определяется компилятором на основе статического класса получателя. Пример:
class Mammal
{
public:
void speak ( )
{
printf(“Не умеет говорить!”);
}
};
class Dog: public Mammal
{
public:
void speak( )
{
printf(“Умеет лаять!”);
}
};
Mammal Fred;
Dog Artur;
Mammal *fido = new Dog;
Выражение fred.speak( ) напечатает « не может говорить!», выражение Artur.speak( ) напечатает «умеет лаять!». Однако вызов fido speak( ) также напечатает «не может говорить!», поскольку соответствующий метод в классе Mammal не объявлен виртуальным. Если добавить ключевое слово virtual в объявление метода speak в родительском классе, то вышеприведенное выражение напечатает «умеет лаять!».
В С++ имеются средства для распознавания динамического класса объекта. Они образуют так называемую RTTI – систему идентификации типа во время выполнения. (Run Time Type Identification). В системе RTTI каждый класс имеет связанную с ним структуру typeinfo, которая содержит различную информацию о классе. В этой структуре имеется поле данных name, содержащее имя класса в виде текстовой строки. Функция typeid может использоваться для анализа информации о типе данных. Например, следующая команда напечатает строку «Dog» – динамический тип данных для fido.
cout <<”fido is a “ << typeid(*fido).name( ) << endl;
Здесь выполняется разыменование переменной – указателя fido, чтобы аргумент был значением, на которое ссылается указатель, а не самим указателем. Можно также с помощью функции – члена before узнать, соответствует ли одна структура с информацией о типе данных подклассу класса, соотносящегося с другой структурой:
if (typeid(*fido).before(typeid(fred)))… true
if (typeid(fred).before(typeid(Artur)))…false
Система RTTI позволяет определить, является ли текущим значением переменной fido величина типа Dog с помощью команды: fido isaDog( ). Если возвращается ненулевое значение, то можно привести тип переменной к нужному типу данных. Еще одна часть системы RTTI, называемая dynamic_cast, обеспечивает проверку на принадлежность к подклассу и приведение типа. Функция шаблона dynamic_cast берет тип в качестве аргумента шаблона и возвращает либо значение аргумента, если приведение типа законно, либо нулевое значение, если это не так. Например:
// конвертировать только в том случае, если fido является собакой
Artur = dynamic_cast
// Проверка выполнения приведения
if (Artur )…..
Лекция №9 Связывание в языке Java.
В языке Java способы связывания сообщений и поиска подходящих методов значительно проще, чем в С++. Все переменные в Java знают свой динамический тип данных. Предполагается, что все подклассы являются подтипами, поэтому значение может быть присвоено типу родительского класса без явного преобразования. Обратное присваивание (обращение полиморфизма) допускается с явным приведением типа. Для определения допустимости присваивания во время выполнения программы производится проверка, и если присваивание недопустимо, инициируется исключение. Можно проверить динамический тип значения с помощью оператора instatnOf:
if (aBall instantOf BlackBall )
……………………………..
else……………..
Сообщения всегда связываются с методами на основе динамического типа данных получателя. В отличие от С++ и Delphi ключевое слово virtual отсутствует. Хотя Java не различает понятия подкласса и подтипа, предполагая что все подклассы являются подтипами, из всех строго типизированных языков она наиболее четко разделяет эти концепции, предлагая понятие интерфейса.
Интерфейсы обеспечивают иерархическую организацию, подобную классам, но не зависят от последних. Они определяют только протокол выполнения операций, но не их реализацию. Используя ключевое слово extends, можно строить новые интерфейсы поверх существующих, также, как и для классов. Таким образом строится иерархия подтипов, полностью независимая от иерархии подклассов. При разработке интерфейса решается вопрос о том, какие методы должны поддерживаться в классах, реализующих данный интерфейс, и что эти методы должны делать. Интерфейсы могут использоваться для определения переменных, которые будут принимать значения любых типов, заявляющих, что они реализуют интерфейс. Тем самым получается, что статический тип – это тип интерфейса, а динамический тип – это тип класса. Связывание основывается на динамическом типе.
Резюме. Были рассмотрены два принципа связывания методов и сообщений: раннее и позднее связывание. Первое называется статическим связыванием, второе – динамическим связыванием. Статические типы данных и статическое связывание более эффективны, динамические типы данных и динамическое связывание обладают значительно большей гибкостью. Динамические типы данных подразумевают, что каждый объект должен отслеживать свой собственный тип данных. Если рассматривать объекты с точки зрения теории ООП, то динамические типы данных наиболее адекватно отвечают этой теории. Их применение сильно упрощает, например, разработку структур данных общего назначения. Однако во время выполнения происходит постоянный поиск подходящего метода, т. е., возникают дополнительные накладные расходы.
Статические типы данных упрощают реализацию языка, даже если (как в Java или Delphi) используется динамическое связывание метода с сообщением. Когда компилятору известны статические типы данных, выделение памяти под переменные (в том числе, и под объекты) происходит более эффективно, а для простых операторов генерируется оптимальный код. Все статические типы данных упрощают реализацию языков программирования. Если соответствие между методом и сообщением может быть обнаружено компилятором, то пересылка сообщений вырождается в традиционный вызов процедуры: во время выполнения уже не требуется поиск подходящего метода.
Динамическое связывание всегда требует некоторого механизма времени выполнения для сопоставления метода и сообщения. В языках, которые используют динамические типы данных, и, как следствие, позднее связывание, вообще говоря, нельзя определить заранее, будет ли пересылаемое сообщение восприниматься получателем. Если сообщение не распознается, то генерируется ошибка выполнения. Ясно, что большинство таких ошибок может быть отловлено на этапе компиляции, если использовать статические данные и раннее связывание.
Таким образом, приходится выбирать между простой и эффективностью с одной стороны и гибкостью с другой стороны. Однозначного ответа на этот вопрос пока не существует. Все определяется конкретной ситуацией.
Замещение и уточнение.
До сих пор предполагалось, что данные и методы, которые добавляются в подклассы, отличаются от унаследованных из родительского класса. Другими словами, методы и значения данных, доопределенные в подклассе, отличаются от значений и методов родительских классов. Однако дочерний класс может определить некоторый метод по тем же именем, что и в родительском классе. В этом случае говорят, метод подкласса переопределяет метод надкласса (override).
Метод дочернего класса может переопределить наследуемый метод одним из двух способов: замещением или уточнением. В случае замещения метод родительского класса замещается целиком во время работы программы, т. е., код родительского класса никогда не исполняется при обработке объектов дочернего класса. Уточняющий метод включает в себя как часть своих действий вызов метода родительского класса. Таким образом, поведение надкласса сохраняется и присоединяется.
Замещение методов.
Языки программирования используют различные подходы к указанию на то, что некоторый метод переопределяется. В языке С++ базовый класс должен иметь специальные указания о возможности переопределения. В языке Delphi – Pascal это указание помещается как в родительский, так и в дочерний классы. Язык Java вообще не требует указания на переопределение. Основная трудность при использовании замещения методов в качестве фундаментальной модели наследования – предположение, что подклассы являются подтипами. Основой принципа подстановки является то, что объекты подкласса ведут себя во всех существенных отношениях подобно представителям родительского класса. Но если подклассы могут переопределять методы, то поведение дочернего класса может существенно отличаться от поведения родительского класса. Пусть, например, в некотором классе определен метод вычисления квадратного корня. Если подкласс слишком радикально изменит поведение унаследованного метода, например, так, чтобы он вычислял логарифм, то возникнет большой беспорядок. Существует несколько путей разрешения конфликта между замещением методов и принципом подстановки.
- Возложить на программиста заботу о том, чтобы подклассы не нарушали принцип подстановки. Этот подход реализован во всех рассматриваемых ОО – языках программирования.
- Разделить понятия подкласса и подтипа. Подклассы затем могут использовать семантику замещения, при этом не обязательно подразумевая, что порожденный класс будет подтипом первоначального класса.
- Отбросить семантику замещения, а использовать уточнение методов.
Расположение какого – либо указания в родительском классе обычно облегчает реализацию замещения, поскольку если нет возможности переопределить метод, то посылка сообщения реализуется просто через вызов процедуры. Динамический поиск в этом случае выполнять не нужно. С другой стороны, снятие предварительного уведомления делает язык более гибким, так как позволяет из любого класса порождать подклассы даже если такая возможность не была предусмотрена.
В языке Delphi – Pascal метод, переопределяемый в подклассах, должен снабжаться модификатором virtual в определении метода в надклассе. Если метод объявлен виртуальным в родительском классе, то все методы в подклассах с тем же именем также должны быть объявлены виртуальными. Переопределяемые в подклассах методы снабжаются модификатором override. Справедливы следующие правила замещения методов:
- имя переопределяемого в дочернем классе метода совпадает с именем соответствующего метода в родительском классе;
- порядок, типы, имена параметров и тип результата методов надкласса и подклассов в точности совпадают;
- за формальным описанием метода в подклассе следует модификатор override.
Замещение в С++ подчиняется следующим правилам:
- имя метода пишется точно также, как и в родительском классе;
- сигнатура метода подкласса идентична сигнатуре метода родительского класса;
- при описании методов в родительском классе и подклассах используется модификатор virtual.
Для компилятора С++ есть смысловой нюанс в том, что метод, объявленный виртуальным в надклассе, был бы объявлен виртуальным и в подклассе. Вообще говоря, модификатор virtual не обязателен в описании подкласса. Метод, объявленный виртуальным в каком – либо классе, становится виртуальным и во всех подклассах. Однако для целей документирования принято повторять этот модификатор и в подклассах. Если модификатор virtual не задан, метод по-прежнему будет замещать одноименный метод надкласса, однако связывание вызова невиртуального метода будет происходить на этапе компиляции.
В языке Java для переопределения методов достаточно, чтобы новый метод имел то же имя и сигнатуру, что и метод надкласса. Имеется специальный модификатор final. Если он применяется к имени метода, то последующее переопределение становится невозможным.
Уточнение методов.
Часто возникает необходимость не полностью заменять код родительского класса, а комбинировать его с некоторыми действиями в подклассах. Тем самым гарантируется, что действия родительского класса будут выполняться всегда. Чаще всего такая потребность возникает при инициализации объекта. В этом случае необходимо осуществить действия по инициализации надкласса, а затем – некоторые другие действия, специфические для подкласса. Поэтому нужен какой – то механизм внутри переопределяемого метода, который вызывал бы метод – предшественник из надкласса и таким образом повторно использовал бы код переопределяемого метода. Когда метод дочернего класса вызывает таким образом переопределяемый метод надкласса, принято говорить об уточнении родительского метода.
В языке Delphi – Pascal уточнение осуществляется методом дочернего класса, который явным образом вызывает переопределяемый метод родительского класса. Вызову метода надкласса предшествует квалификатор inherited.
В языке С++ вызов метода может иметь расширенный синтаксис составного имени, при котором вместо используемой по умолчанию процедуры поиска подходящего метода точно указывается, из какого класса должен браться вызываемый метод. Это составное имя записывается как имя класса, за которым следует «::» и затем – имя метода. Механизм составного имени применяется в С++ для моделирования уточнения при переопределении. Замещаемый метод явным образом вызывает родительский метод, тем самым гарантируя, что оба метода будут выполнены.
Для уточнения в Java используется модификатор super. При вызове методов он представляет собой ссылку на текущий объект как на объект надкласса.
Лекция №10. Следствия наследования.
Наследование оказывает большое влияние на все аспекты ОО – языков программирования. Над объектами, как языковыми структурами, вообще говоря, должны выполняться базовые операции – присваивания и проверки на равенство. Ранее был рассмотрен принцип основополагающий в ООП подстановки. В соответствии с этим принципом, если некоторая переменная win описана как объект некоторого класса «Окно», то она может содержать значения этого типа. Если объявлен класс «Текстовое окно», являющийся подклассом типа «Окно», то поскольку «Текстовое окно» является экземпляром «Окно», то переменной win можно присвоить значение типа «Текстовое окно». Отношение «быть экземпляром» - это средство, связывающее тип данных в смысле типа переменной и набор значений, которые могут законным образом содержаться в этой переменной. В то время как сам принцип имеет интуитивно понятный смысл, с практической точки зрения существуют некоторые трудности в его реализации.