36

36


Новый метод what() возвращает строку (String) с информацией о классе, а метод adjust() предназначен для настройки инструментов.
В методе main() сохранение любого объекта в массиве orchestra автоматиче­ски приводит к выполнению восходящего преобразования к типу Instrument.
Вы можете видеть, что метод tune() изолирован от окружающих изменений кода, но при этом все равно работает правильно. Для достижения такой функ­циональности и используется полиморфизм. Изменения в коде не затрагивают те части программы, которые не зависят от них. Другими словами, полимор­физм помогает отделить «изменяемое от неизменного».
Проблема: «переопределение» закрытых методов
Перед вами одна из ошибок, совершаемых по наивности:
//: polymorph!sm/PrivateOverride.java
// Попытка переопределения приватного метода
package polymorphism;
import static net.mindview.util.Print.*;
public class PrivateOverride {
private void f() { printCprivate f(D; } public static void main(String[] args) {
Pri vateOverride po = new DerivedO; po.fO:
}
class Derived extends PrivateOverride {
public void f() { print("public f()"). } } /* Output
private f() *///-
Вполне естественно было бы ожидать, что программа выведет сообщение public f(), но закрытый (private) метод автоматически является неизменным (final), а заодно и скрытым от производного класса. Так что метод f() класса Derived в нашем случае является полностью новым — он даже не был перегру­жен, так как метод f() базового класса классу Derived недоступен.
Из этого можно сделать вывод, что переопределяются только методы, не яв­ляющиеся закрытыми. Будьте внимательны: компилятор в подобных ситуаци­ях не выдает сообщений об ошибке, но и не делает того, что вы от него ожидаете. Иными словами, методам производного класса следует присваивать имена, от­личные от имен закрытых методов базового класса.
Конструкторы и полиморфизм
Конструкторы отличаются от обычных методов, и эти отличия проявляются и при использовании полиморфизма. Хотя конструкторы сами по себе не по­лиморфны (фактически они представляют собой статические методы, только ключевое слово static опущено), вы должны хорошо понимать, как работают конструкторы в сложных полиморфных иерархиях. Такое понимание в даль­нейшем поможет избежать некоторых затруднительных ситуаций.
Порядок вызова конструкторов
Порядок вызова конструкторов коротко обсуждался в главах 5 и 7, но в то вре­мя мы еще не рассматривали полиморфизм.
Конструктор базового класса всегда вызывается в процессе конструирова­ния производного класса. Вызов автоматически проходит вверх по цепочке на­следования, так что в конечном итоге вызываются конструкторы всех базовых классов по всей цепочке наследования. Это очень важно, поскольку конструк­тору отводится особая роль — обеспечивать правильное построение объектов. Производный класс обычно имеет доступ только к своим членам, но не к чле­нам базового класса (которые чаще всего объявляются со спецификатором private). Только конструктор базового класса обладает необходимыми зна­ниями и правами доступа, чтобы правильно инициализировать свои внут­ренние элементы. Именно поэтому компилятор настаивает на вызове конст­руктора для любой части производного класса. Он незаметно подставит конструктор по умолчанию, если вы явно не вызовете конструктор базового класса в теле конструктора производного класса. Если конструктора по умол­чанию не существует, компилятор сообщит об этом. (Если у класса вообще нет пользовательских конструкторов, компилятор автоматически генерирует кон­структор по умолчанию.)
Следующий пример показывает, как композиция, наследование и полимор­физм влияют на порядок конструирования:
// polymorphism/Sandwich.java
// Порядок вызова конструкторов.
package polymorphism,
import static net mindview.util.Print.*;
class Meal {
Meal О { printCMealO"). }
}
class Bread {
BreadO { printCBreadO"). }
}
class Cheese {
CheeseO { printC'CheeseO"). }
}
class Lettuce {
LettuceO { print("Lettuce()"); }
}
class Lunch extends Meal {
Lunch0 { printC'LunchO"). }
}
class PortableLunch extends Lunch {
PortableLunchO { printC'PortableLunchO");}
}
public class Sandwich extends PortableLunch { private Bread b = new BreadO, private Cheese с = new CheeseO, private Lettuce 1 = new LettuceO; public Sandwich0 { print("Sandwich()"); } public static void main(String[] args) { new SandwichO;
}
} /* Output: Meal О LunchO
PortableLunchO BreadO CheeseO LettuceO SandwichO *///:-
В этом примере создается сложный класс, собранный из других классов, и в каждом классе имеется конструктор, который сообщает о своем выполне­нии. Самый важный класс — Sandwich, с тремя уровнями наследования (четырьмя, если считать неявное наследование от класса Object) и тремя встроенными объ­ектами. Результат виден при создании объекта Sandwich в методе main(). Это значит, что конструкторы для сложного объекта вызываются в следующей по­следовательности:
•        Сначала вызывается конструктор базового класса. Этот шаг повторяется рекурсивно: сначала конструируется корень иерархии, затем следующий за ним класс, затем следующий за этим классом класс и т. д., пока не дос­тигается «низший» производный класс.
•        Проводится инициализация членов класса в порядке их объявления.
•        Вызывается тело конструктора производного класса.
Порядок вызова конструкторов немаловажен. При наследовании вы распо­лагаете полной информацией о базовом классе и можете получить доступ к лю­бому из его открытых (public) или защищенных (protected) членов. Следова­тельно, при этом подразумевается, что все члены базового класса являются действительными в производном классе. При вызове нормального метода из­вестно, что конструирование уже было проведено, поэтому все части объекта инициализированы. Однако в конструкторе вы также должны быть уверены в том, что все используемые члены уже проинициализированы. Это можно га­рантировать только одним способом — сначала вызывать конструктор базового класса. В дальнейшем при выполнении конструктора производного класса можно быть уверенным в том, что все члены базового класса уже инициализи­рованы. Гарантия действительности всех членов в конструкторе — важная при­чина, по которой все встроенные объекты (то есть объекты, помещенные в класс посредством композиции) инициализируются на месте их определения (как в рассмотренном примере сделано с объектами Ь, с и I). Если вы будете следовать этому правилу, это усилит уверенность в том, что все члены базового класса и объекты-члены были проинициализированы. К сожалению, это помо­гает не всегда, в чем вы убедитесь в следующем разделе.
Наследование и завершающие действия
Если при создании нового класса используется композиция и наследование, обычно вам не приходится беспокоиться о проведении завершающих действий — подобъекты уничтожаются сборщиком мусора. Но если вам необходимо провес­ти завершающие действия, создайте в своем классе метод dispose() (в данном разделе я решил использовать такое имя; возможно, вы придумаете более удач­ное название). Переопределяя метод dispose() в производном классе, важно помнить о вызове версии этого метода из базового класса, поскольку иначе не будут выполнены завершающие действия базового класса. Следующий пример доказывает справедливость этого утверждения:
//: polymorphism/Frog.java
// Наследование и завершающие действия.
package polymorphism;
import static net.mindview util.Print.*;
class Characteristic { private String s;

CharacteristicCString s) { this s = s;
print("Создаем Characteristic " + s);
}
protected void disposeO {
print("Завершаем Characteristic " + s);



class Description {
private String s;
Description(String s) { this s = s.
print("Создаем Description " + s).
}
protected void disposeO {
print("Завершаем Description " + s);
}
}
// живое существо class LivingCreature {
private Characteristic p =
new Characteristic"живое существо");
private Description t =
new Description("обычное живое существо");
LivingCreatureO {
printCLivingCreatureO");
}
protected void disposeO {
print("dispose() в LivingCreature "), t.disposeO; p.disposeO;



// животное
class Animal extends LivingCreature { private Characteristic p =
new Characteristic("имеет сердце"); private Description t =
new Descripti0n(">khb0th0e. не растение"); Animal О { print("Animal()"); } protected void disposeO {
print("disposeO в Animal "); t.disposeO; p.disposeO; super, di sposeO;



// земноводное
class Amphibian extends Animal { private Characteristic p =
new Characteristic"может жить в воде"); private Description t =
new Descriptions в воде, и на земле"); Amphibian О {продолжение &// лягушка
public class Frog extends Amphibian {
private Characteristic p = new CharacteristicC'KBaKaei"). private Description t = new Description"ест жуков"), public FrogO { printC'FrogO"), } protected void disposeO {
print С завершение Frog"), t disposeO; p disposeO; super.disposeO;
}
public static void main(String[] args) { Frog frog = new FrogO; print("Пока!"); frog. disposeO;
}
} /* Output:
Создаем Characteristic живое существо Создаем Description обычное живое существо LivingCreatureO
Создаем Characteristic имеет сердце Создаем Description животное, не растение Animal О
Создаем Characteristic может жить в воде Создаем Description и в воде, и на земле Amphibian О
Создаем Characteristic квакает Создаем Description ест жуков FrogO Пока!
завершение Frog
Завершаем Description ест жуков Завершаем Characteristic квакает disposeO в Amphibian
Завершаем Description и в воде, и на земле Завершаем Characteristic может жить в воде disposeO в Animal
Завершаем Description животное, не растение Завершаем Characteristic имеет сердце disposeO в LivingCreature Завершаем Description обычное живое существо Завершаем Characteristic живое существо *///:-
print ("Amphibian (Г);
}
protected void disposeO {
print ("disposeO в Amphibian "); t.disposeO; p.disposeO; super.disposeO,

Каждый класс в иерархии содержит объекты классов Characteristic и De­scription, которые также необходимо «завершать». Очередность завершения должна быть обратной порядку инициализации в том случае, если объекты
зависят друг от друга. Для полей это означает порядок, обратный последова­тельности объявления полей в классе (инициализация соответствует порядку объявления). В базовых классах сначала следует выполнять финализацию для производного класса, а затем — для базового класса. Это объясняется тем, что завершающий метод производного класса может вызывать некоторые методы базового класса, для которых необходимы действительные компоненты базово­го класса. Из результатов работы программы видно, что все части объектаFrogбудут финализованы в порядке, противоположном очередности их создания.
Также обратите внимание на то, что в описанном примере объектFrogявля­ется «владельцем» встроенных объектов. Он создает их, определяет продолжи­тельность их существования (до тех пор, пока существуетFrog)и знает, когда вызыватьdispose()для встроенных объектов. Но если встроенный объект ис­пользуется совместно с другими объектами, ситуация усложняется и вы уже не можете просто вызватьdispose().В таких случаях для отслеживания количе­ства объектов, работающих со встроенным объектом, приходится использоватьподсчет ссылок.Вот как это выглядит:

Report Page