Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Секреты программирования для Internet на Java

.pdf
Скачиваний:
181
Добавлен:
02.05.2014
Размер:
3.59 Mб
Скачать

Объектная ориентация в Java

Преимущества объектной ориентации Затенение данных Повторное использование через наследование

Возможности обслуживания и сопровождения Особенности объектов Java

Иерархия классов Java Специальные переменные Реализация классов Правила доступа

Как работает наследование Структурирование иерархий классов Абстрактные классы и методы

Полиморфизм и интерфейсы Java Обзор понятий и пример

В предыдущей главе мы касались объектно-ориентированных свойств языка Java при обсуждении переменных и классов. Объектно-ориентированное программирование (Object Oriented Programming, OOP) - настолько важная часть языка Java, что даже при написании очень простой программы на Java нам пришлось ввести некоторые понятия OOP. Прежде чем двигаться дальше, рассмотрим понятие объектной ориентации более подробно.

Начнем мы с общего объяснения того, что такое объектная ориентация и чем она улучшает Java. Мы введем также некоторые термины для описания классов, упоминавшиеся в главе 2, "Основы программирования на Java". После того как станет понятнее, что такое объектная ориентация и это понимание станет несколько более формализованным, мы сможем вернуться к вопросам, обсуждавшимся в главе 2, и рассмотреть их на новом уровне. После этого мы углубимся в специфические для Java объектно-ориентированные свойства этого языка.

Преимущества объектной ориентации

Объектная ориентация - возможно, самое популярное крылатое выражение в программировании. Как и у всех крылатых выражений, у него существует масса различных толкований. В главе 1 мы давали определение, которое принимается обычно: объектная ориентация - это подход, который упрощает решение задач. Но это определение описывает сам "продукт", который нам пытается продать тысяча напористых книг по менеджменту, видеозаписей и курсов. Давайте воспользуемся тем, что мы уже узнали из главы 2, и выработаем определение программистского уровня.

Классы - это альфа и омега объектной ориентации. Как вы помните, класс описывает тип, содержащий как подпрограммы (или методы), так и данные. Раз класс описывает тип, значит, мы можем создать переменные, содержащие и методы и переменные. Такие переменные являются объектами. Объекты отличаются от переменных в процедурных языках программирования тем, что могут определять, как можно менять данные. Подпрограммы в объектно-ориентированных языках отличаются от своих аналогов в процедурных языках тем, что они содержат наборы данных (элементы которых определены в том же классе), которые могут менять только эти подпрограммы, а другие методы не могут.

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

Таблица 3-1. Ключевые термины Java, относящиеся к объектной ориентации

Термин

Определение

Пакет

Набор структурных единиц языка Java, в том числе классов.

Класс

Тип данных, содержащий данные и подпрограммы.

Метод

Реализация подпрограммы на Java.

Конструкция

Создание класса в переменной; появляется в процессе выполнения

 

программы.

Экземпляр, объект, Переменная типа класса, которая была создана.

реализация Модификатор Описывает, какие классы имеют доступ к элементу класса. Модификатор

Ⱦɚɧɧɚɹ ɜɟɪɫɢɹ ɤɧɢɝɢ ɜɵɩɭɳɟɧɚ ɷɥɟɤɬɪɨɧɧɵɦ ɢɡɞɚɬɟɥɶɫɬɜɨɦ %RRNV VKRS Ɋɚɫɩɪɨɫɬɪɚɧɟɧɢɟ ɩɪɨɞɚɠɚ ɩɟɪɟɡɚɩɢɫɶ ɞɚɧɧɨɣ ɤɧɢɝɢ ɢɥɢ ɟɟ ɱɚɫɬɟɣ ɁȺɉɊȿɓȿɇɕ Ɉ ɜɫɟɯ ɧɚɪɭɲɟɧɢɹɯ ɩɪɨɫɶɛɚ ɫɨɨɛɳɚɬɶ ɩɨ ɚɞɪɟɫɭ piracy@books-shop.com

доступа доступа нужен также для указания того, что доступ к классу возможен извне пакета.

Модификатор доступа static

Вы, возможно, заметили, что табл. 3-1 не содержит ключевого слова static, описанного в главе 2, "Основы программирования на Java". Хотя объектная ориентация обладает множеством преимуществ, бывает, что простую подпрограмму не нужно жестко привязывать к конкретному набору данных. Это аналогично тому, что вы можете захотеть определить переменную, которая всегда будет иметь одно и то же значение, и нет никакого смысла возиться с реализацией объекта только для того, чтобы получить это значение. В таких случаях, когда объектная ориентация не дает преимуществ, используется модификатор static. Поскольку эти случаи не вписываются в объектно-ориентированный подход, мы в этой главе не рассматриваем статические методы и переменные.

Затенение данных

Помните модификатор доступа private, которым мы пользовались в главе 2? Переопределяя переменную с модификатором private, мы затеняем данные от всех подпрограмм в нашей программе, кроме подпрограмм, определенных в том же классе. Когда же имеет смысл затенять данные? Рассмотрим парадокс, часто возникающий при процедурном программировании.

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

Это вполне справедливо. Но что если вы пишете программу, в которой, допустим, восемь подпрограмм, и в четырех из них используется одна и та же переменная? Если соблюдать запрет на глобальные переменные, вы должны пропустить эту переменную через четыре использующих ее метода. Но на самом деле это просто обходной путь вместо того, чтобы сделать переменную глобальной. В действительности все, что вам нужно, - это сделать переменную глобальной для тех четырех подпрограмм, в которых она используется.

ВJava мы просто помещаем переменную в класс, переопределяем ее с модификатором private

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

Инкапсуляция

В процессе затенения данных мы косвенно описали взаимоотношения между переменной и методами, содержащимися в том же классе. Если методу, не включенному в класс, требуется изменить эту переменную, он должен вызвать один из методов, определенных в классе. Такие взаимоотношения между элементами класса входят в понятие, называемое инкапсуляцией (encapsulation). Это понятие очень близко к затенению данных.

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

public class printLines { private int linesToPrint=0; public void printSomeLines() {

for (int i=0;i<<=linesToPrint;i++) { System.out.println("");}

}

public void setLinesToPrint(int j) { if (j>>0) {

linesToPrint=j;}

www.books-shop.com

}

Поскольку переменная может измениться только в методе setLineToPrint, нам нужно проверить ее на отрицательность только в этом методе. Таким образом, нам не придется писать несколько лишних строк в программе. Если в нашем классе содержится несколько переменных, преимущества такого подхода станут еще очевиднее.

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

В результате мы получаем запутанный набор данных - каждый элемент этого набора связан со всеми остальными элементами. Это значит, что в процедурном языке каждая подпрограмма, использующая какой-то элемент этого набора данных, отвечает за сохранение связи этого элемента с другими. Поскольку каждая подпрограмма просто оперирует с данными, понимать, как выдерживаются эти связи, очень непросто. Если в каком-то месте связь порвалась, будет очень трудно определить, при каком именно действии это случилось, - совсем как с отношениями между людьми!

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

public class replaceChars { private char myArray[]; private int curPos=0;

public reolaceChars(char someArray[]) { myArray=someArray;}

public boolean replaceNetChar(char c, char d) { if (newPositionSet(c)) { myArray[curPos]=d;

return true;} else {return false;}

}

private boolean newPositionSet(chae c) { int i=curPos;

while (I<<myArray.length) { if (c==myArray[i]) {

curPos=i; return true;}

else {i++;}

}

return false;

}

public boolean atEnd() { return(curPos==myArray.length-1);}

// вычитаем 1, потому что позиции в массиве начинаются с нуля

}

Написав несколько строк программы, мы сможем полностью решить нашу задачу. Заметим также, что новый метод PositionSet задан с модификатором private. Это снова возвращает нас к понятию затенения данных. Вместо того чтобы затенять данные, мы затеняем метод, который меняет данные. Возможно, мы не хотим менять позицию элемента во всех случаях, кроме тех, когда производится замена символа.

Теперь рассмотрим трудности, возникающие при решении нашей задачи в процедурном языке. Во-первых, мы не можем затенить наши данные или любую из участвующих подпрограмм. Это означает, что мы должны постоянно делать проверку того, что наша переменная curPos не вышла за границы значений. Во-вторых, у нас нет возможности следить непосредственно за текущей позицией в массиве.

Приведенная выше простая задача разрешима и средствами процедурных языков. Однако наш класс имеет то преимущество, что он является на самом деле определением типа. Когда мы

www.books-shop.com

инкапсулируем наши методы и переменные в класс, мы фактически инкапсулируем наше решение. Раз решение - это тип, его легко снова использовать, просто реализовав другую переменную

replaceChars solution1=new replaceChars("Java - это здорово!") replaceChars solution2=new replaceChars("Я хочу больше узнать о Java!"); while (!solution1.atEnd())

{solution1.replaceNextChar('a','x');} while (!solution2.atEnd())

{solution2.replaceNextChar('o','y');}

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

Разумеется, абстракция - это не новшество при работе с OOP. Процедурные языки определяют последовательность действий в подпрограммах, и, таким образом, эти подпрограммы можно снова использовать. Кроме того, простые типы данных, например целочисленные переменные, суть абстрактное выражение того, как биты хранятся в памяти компьютера. Объектная ориентация просто поднимает эту абстракцию на новый уровень, связывая воедино абстракции типов данных и подпрограмм, в результате чего отношения между данными и действиями над ними могут быть повторно использованы.

Повторное использование через наследование

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

Рассмотрим процедуру наследования в действии. Во фрагменте кода, приведенном в предыдущем разделе, мы снова и снова вызывали replaceNextChar для двух одинаковых символов. Разве не удобнее было бы сделать это, используя какой-нибудь метод в классе replaceChar? Мы бы просто добавили этот метод в класс и снова откомпилировали программу. Но предположим, что кто-то еще использует начальный класс replaceChar. Тогда нам нужно поддерживать два класса с тем же именем, что может привести к путанице. Вместо этого мы можем создать новый класс, который унаследует характеристики нашего класса replaceNextChar:

class betterReplaceNextChar extends ReplaceNetChar { public int replaceAllChar(char c, char d) { int i=0;

while(!atEnd()) { replaceNetChar(c,d); i++;}

return i;}

}

Теперь мы получили новый класс, содержащий все методы класса ReplaceNext-Char плюс один дополнительный метод, который мы только что определили. Мы сумели инкапсулировать решение в новую задачу, расширив класс. Как мы видели при написании нашего первого апплета

вглаве 2, наследование - очень важное понятие в программировании на Java. Немножко дальше

вэтой главе мы рассмотрим его подробнее.

Возможности обслуживания и сопровождения

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

www.books-shop.com

Например, программа, изначально предназначавшаяся для того, чтобы следить за потребностями служащих предприятия, должна быть модернизирована с учетом заботы о здоровье людей. Или сетевая система, изначально созданная для того, чтобы просто передавать сообщения на соседние машины, теперь нуждается в том, чтобы ею можно было управлять с сервера, находящегося на Уолл-стрит. Можно было бы привести очень длинный список примеров, но наш главный тезис заключается в том, что программное обеспечение живет в сложном и бесконечно меняющемся мире. Возникают проблемы, которые программист, решавший начальную задачу, не предвидел, или на систему накладываются новые требования. Редко бывает, чтобы производственная компьютерная программа не изменялась в течение нескольких лет. Когда программа меняется, бывает, что изменения вносит новый человек или сам программист уже давно забыл хитросплетения собственной программы. В любом случае, кто бы ни вносил изменения, он предпочтет не начинать с разбора черновиков. Современные языки программирования должны давать возможность людям, занимающимся сопровождением программы, легко модифицировать программу для удовлетворения возникших новых потребностей.

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

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

Инкапсуляция имеет еще одно очевидное преимущество. Поскольку другие объекты в программе могут взаимодействовать с данным объектом только через общие методы и конструкторы, можно менять частные части системы и текст, создающий общие методы и конструкторы, не нарушая систему в целом.

Почему это является преимуществом? Рассмотрим задачу о 2000 годе. При наступлении нового тысячелетия многие хорошие программисты рассчитывают заработать по 500<|>$ в час, проверяя ошибки, допущенные компьютерами в разных организациях при распознавании смены тысячелетия. Почему? Существует огромное множество очень важных программ, которые не смогут правильно интерпретировать наступление нового тысячелетия, потому что они используют только две цифры для задания года. Это означает, что ваш банк может начать считать, что вам - 73 года, или телефонный разговор между восточным и западным побережьем, начавшийся 31 декабря в 23:59, будет считаться продолжавшимся в течение 99 лет!

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

public class Year {

private byte decadeDigit; private byte yearDigit; public Year(int thisYear) {

byte yearsSince1900=(byte)thisYear-1900; decadeDigit=yearsSince1900/10; yearDigit=yearsSince1900-(decadeDigit*10);}

public int getYear() {

return decadeDigit*yearDigit;} // другие методы

}

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

www.books-shop.com

консультанта за 500 $ в час? Конечно, нет! Все, что нам нужно, - это переписать заново реализацию класса. Если мы не будем менять описания общих методов, все в этих системах будет работать правильно:

public class Year {

private byte centuryDigit; private byte decadeDigit; private byte yearDigit; public Year(int thisYear) {

centuryDigit=(byte)thisYear/100;

int lastTwo=thisYear-(centuryDigit*100); decadeDigit=(byte)lastTwo/10; yearDigit=(byte)(lastTwo-(decadeDigit*10)); }

public int getYear() {

return decadeDigit*yeaaDigit*centuryDigit;} // другие методы

}

Теперь мы можем жить спокойно до 12799 года, и никому не придется нанимать человека для выполнения нудной работы по исправлению нашей программы!

СОВЕТ Java API, обсуждавшееся в главе 6, содержит класс Date, который не пострадает при смене тысячелетия.

Особенности объектов Java

Мы рассмотрели понятия, лежащие в основе некоторых частей программы, которые мы писали в главе 2, и надеемся, что убедили вас в том, что эти понятия отражают преимущества языка

Java.

Иерархия классов Java

Используя термин "иерархия классов", мы описываем то, что происходит при наследовании. Допустим, у нас есть три класса: Mom, Son и Daughter (мама, сын и дочь). Классы Son и Daughter наследуют от Mom. Наша программа будет иметь следующий вид:

class Mom {

//описания, определения

}

class Son extends Mom {

//описания, определения

}

class Daughter extends Mom {

//описания, определения

}

Итак, мы создали иерархию классов. Точно так же, как и организационную иерархию, ее легко представить визуально.

Втабл. 3-2 приведены некоторые термины, необходимые для описания нашей иерархии. Mom

-это базовый класс, то есть класс, на котором базируются другие классы. Son и Daughter - это подклассы класса Mom, а Mom является суперклассом для Son и Daughter.

 

Таблица 3-2. Термины, связанные с иерархией классов

Термин

Определение

Иерархия

Группа классов, связанных наследованием.

классов

 

Суперкласс

Класс, расширяемый неким другим классом.

Подкласс

Класс, расширяющий некий другой класс.

www.books-shop.com

Базовый класс Класс в иерархии, являющийся суперклассом для всех остальных классов в этой иерархии.

Теперь, когда у нас уже есть некоторый словарь необходимых для работы понятий, мы можем поговорить конкретно об иерархии классов в Java. Во-первых, все классы в Java имеют ровно один непосредственный суперкласс. Как обсуждалось в главе 1, эта характеристика Java на языке объектной ориентации известна как единичное наследование. Разумеется, у класса может быть и больше одного суперкласса. Например, и Mom и Daughter являются суперклассами другого класса Granddaughter (внучка).

"Минуточку, - возможно, скажете вы, - но если все классы имеют в точности один непосредственный суперкласс, то каков же суперкласс класса Mom?" Дело в том, что иерархия классов, которую мы здесь описали, на самом деле является подмножеством другой огромной иерархии классов, которая содержит каждый единичный класс, когда-либо написанный на Java. На рис. 3-1 показано, как созданная нами маленькая иерархия классов встраивается в гораздо большую иерархию. На вершине этого класса находится специальный класс, называемый классом Object.

Рис. 3.1.

В том случае, когда мы объявляем класс, не указывая явно, расширением какого класса он является, компилятор Java подразумевает, то наш класс является расширением класса Object. Поэтому следующее объявление нашего класса Mom эквивалентно объявлению, данному выше:

class Mom extends Object { // определения и объявления

Так чем же хороша эта всеобъемлющая иерархия классов? Поскольку все классы наследуют из класса Object, мы знаем, что всегда можно воспользоваться его методами. В состав методов класса Object входят, например, методы для установления равенства и методы, предназначенные для поддержки многопотоковости. Кроме того, нам не нужно заботиться об объединении нескольких различных иерархий объектов между собой, поскольку мы знаем, что все они - подмножества одной и той же глобальной иерархии. Наконец, иерархия классов гарантирует, что у каждого класса есть суперкласс, а это очень важно, как мы увидим в главе 6, когда будем рассматривать специальные классы-контейнеры.

www.books-shop.com

Что входит в глобальную иерархию классов

Как мы уже говорили, любой класс в языке Java принадлежит одной и той же глобальной иерархии. Более того, чрезвычайно важная иерархия классов Java входит в состав JDK. Она называется "интерфейс прикладного программирования", или Java API. В главе 6 мы познакомимся с API подробнее.

Специальные переменные

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

Переменная null

Как вы помните из главы 2, "Основы программирования на Java", прежде чем использовать какой-то класс, его нужно реализовать. До этого класс имеет значение переменной null, и мы говорим, что объект равен нулю. Если объект равен нулю, доступ к его элементам не разрешен, потому что не был создан объект, с которым могли бы ассоциироваться эти элементы. Если мы попытаемся обратиться к элементам до того, как они были созданы, мы рискуем вызвать исключение NullPointerException, что остановит выполнение программы. Приведенный ниже метод действует довольно рискованно, потому что он принимает ReplaceNextChar в качестве параметра и использует его, не проверив на равенство нулю:

public void someMethod(ReplaceChars A) { A.replaceNextChar('a','b');}

Следующая программа, вызывающая someMethod, приведет к NullPointerException, потому что не был создан ReplaceNextChar:

ReplaceChars B; someMethod(B);

Чтобы уберечь программу от сбоев, необходимо перед использованием объектов проверять их на равенство нулю. Переписанный заново someMethod выполняет такую проверку, чтобы убедиться, что A не равно нулю, прежде чем обращаться к его элементам:

public void someMethod(replaceChars A) { if (A==null) {

System.out.println("A пусто !!!");}

else {

A.replaceNextChar('a','b');

}

Переменная this

Иногда бывает необходимо передать другой подпрограмме ссылку на текущий объект. Это можно сделать, просто передав переменную this. Скажем, наши классы Son и Daughter определяют конструктор, который заключает переменную Mom в свой конструктор. Переменная this позволяет классам Son и Daughter следить за классом Mom, сохраняя ссылку на него в переменной, определенной с модификатором private:

public class Son { Mom myMommy;

public Son(Mom mommy) { myMommy=mommy;}

// методы

}

public class Daughter { myMommy=mommy;}

www.books-shop.com

public Daughter(Mom Mommy) { myMommy=mommy;}

Когда класс Mom создает свои подклассы Son и Daughter, ему нужно передать своим конструкторам ссылку на себя. Mom делает это, используя переменную this:

public class Mom { Son firstSon; Son secondSon;

Daughter fistDaughter; Daughter secondDaughter;

public Mom() { firstSon=newSon(this); secondSon=newSon(this);

fistDaughter=newDaudther(this);

secondDaughter=newDaudther(this);} // другие методы

}

Для Mom, сконструированного таким образом: Mom BigMama=new Mom();

рис. 3-2 представляет все взаимоотношения нашей семьи:

Рис. 3.2.

Переменная super

Вам часто придется обращаться к родительскому экземпляру метода. Допустим, вы реализовали конструктор, определенный в вашем классе-родителе. Возможно, вы решили присвоить начальные значения нескольким переменным, определенным в новом классе с модификатором private, а теперь хотите вызвать родительский конструктор. Именно здесь вам будет полезно воспользоваться переменной super. В следующем примере мы определим класс, который замещает свой родительский конструктор, а затем вызывает его, используя переменную super.

Обратимся снова к нашей иерархии Mom, Son и Daughter. Пусть Mom определяет метод мытья комнаты, называемый cleanUpRoom. Предполагается, что Son моет комнату в точности так, как определила Mom, после чего он должен сказать: "Моя комната вымыта!" Поскольку Mom определила метод мытья комнаты, Son может вызвать этот метод, используя переменную super, после чего выполнить дополнительное действие по выводу сообщения:

public class Mom {

//переменные, конструкторы public void cleanUpRoom() {

//код для мытья комнаты

}

// другие методы

}

public class Son {

www.books-shop.com

//переменные, конструкторы public void cleanUpRoom() { super.cleanUpRoom();

System.out.println("Моя комната вымыта!");}

//другие методы

}

СОВЕТ Внимание! Не следует считать, что переменная super указывает на совершенно отдельный объект. Чтобы ее использовать, не нужно реализовывать суперкласс. На самом деле это просто способ выполнения методов и конструкторов, определенных в суперклассе.

Конструкторы, так же как и методы, тоже могут использовать переменную super, как видно из следующего примера:

public class SuperClass { private int onlyInt;

public SuperClass(int i) { onlyInt=i;}

public int getOnlyInt() { return onlyInt;}

}

Воспользовавшись переменной super, наш подкласс может повторно использовать программу, написанную для конструктора:

public class SubClass extends SuperClass { private int anotherInt;

public SubClass(int i, int j) { super(i); anotherInt=j;}

public int getAnotherInt() { return anotherInt;}

}

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

Реализация классов

Начиная с главы 2 мы занимались реализацией классов. Используя оператор new, мы оживляем наш класс как объект и присваиваем его переменной. Теперь рассмотрим некоторые спорные вопросы, касающиеся реализации, которые мы еще не обсуждали.

При первом описании реализации классов мы использовали задаваемый по умолчанию конструктор:

someClass A=new someClass();

Затем мы показали, что можно передать переменные конструктору. При этом мы воспользовались преимуществами совмещения конструкторов (constructor overloading), при котором класс определяет множество конструкторов с различными списками параметров. Поскольку конструкторы на самом деле являются просто методами специального типа, совмещение конструкторов работает так же, как совмещение методов, описанное в главе 2. Определенный ниже класс использует совмещение конструкторов:

public class Box { int boxWidth; int boxLength; int boxHeight;

public Box(int i) { boxWidth=i; boxLength=i;

www.books-shop.com