32

32


•        Сначала вызывается конструктор базового класса. Этот шаг повторяется рекурсивно: сначала конструируется корень иерархии, затем следующий за ним класс, затем следующий за этим классом класс и т. д., пока не дос­тигается «низший» производный класс.
•        Проводится инициализация членов класса в порядке их объявления.
•        Вызывается тело конструктора производного класса.
Порядок вызова конструкторов немаловажен. При наследовании вы распо­лагаете полной информацией о базовом классе и можете получить доступ к лю­бому из его открытых (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().В таких случаях для отслеживания количе­ства объектов, работающих со встроенным объектом, приходится использоватьподсчет ссылок.Вот как это выглядит:
// polymorphism/ReferenceCounting.java
11 Уничтожение совместно используемых встроенных объектов
import static net mindview.util.Print.*;
class Shared {
private int refcount = 0; private static long counter = 0, private final long id = counter++, public SharedO {
print("Создаем " + this);
}
public void addRefO { refcount++; } protected void disposeO { if(--refcount == 0)
printODisposing " + this),
}
public String toStringO { return "Shared " + id; }
}
class Composing {
private Shared shared; private static long counter = 0. private final long id = counter++, public Composing(Shared shared) { print("Создаем " + this); this.shared = shared, this shared addRefO.
}
protected void disposeO {
printC'disposing " + this), shared disposeO,
}
public String toStringO { return "Composing " + id; }
}
public class ReferenceCounting {
public static void main(String[] args) {
Shared shared = new SharedO;
Composing[] composing = { new Composing(shared).
new Composing(shared), new Composing(shared), new Composing(shared), new Composing(shared) }; for(Composing с • composing) с disposeO.
}
} /* Output: Создаем Shared 0 Создаем Composing 0 Создаем Composing 1 Создаем Composing 2 Создаем Composing 3 Создаем Composing 4 уничтожаем Composing 0 уничтожаем Composing 1 уничтожаем Composing 2 уничтожаем Composing 3 уничтожаем Composing 4 уничтожаем Shared 0 *///:-
В переменной static long counter хранится количество созданных экземпля­ров Shared. Для счетчика выбран тип long вместо int для того, чтобы предотвра­тить переполнение (это всего лишь хороший стиль программирования; в рас­сматриваемых примерах переполнение вряд ли возможно). Поле id объявлено со спецификатором final, поскольку его значение остается постоянным на про­тяжении жизненного цикла объекта
Присоединяя к классу общий объект, необходимо вызвать addRef(), но метод dispose() будет следить за состоянием счетчика ссылок и сам решит, когда нуж­но выполнить завершающие действия. Подсчет ссылок требует дополнитель­ных усилий со стороны программиста, но при совместном использовании объ­ектов, требующих завершения, у вас нет особого выбора.
Поведение полиморфных методов при вызове из конструкторов
В иерархиях конструкторов возникает интересный вопрос. Что происходит, если вызвать в конструкторе динамически связываемый метод конструируемо­го объекта?
В обычных методах представить происходящее нетрудно — динамически связываемый вызов обрабатывается во время выполнения, так как объект не знает, принадлежит ли этот вызов классу, в котором определен метод, или классу, производному от этого класса. Казалось бы, то же самое должно проис­ходить и в конструкторах.
Но ничего подобного. При вызове динамически связываемого метода в кон­структоре используется переопределенное описание этого метода. Однако по­следствия такого вызова могут быть весьма неожиданными, и здесь могут крыть­ся некоторые коварные ошибки.

По определению, задача конструктора — дать объекту жизнь (и это отнюдь не простая задача). Внутри любого конструктора объект может быть сформирован лишь частично — известно только то, что объекты базового класса были проини- циализированы. Если конструктор является лишь очередным шагом на пути по­строения объекта класса, производного от класса данного конструктора, «про­изводные» части еще не были инициализированы на момент вызова текущего конструктора. Однако динамически связываемый вызов может перейти во «внешнюю» часть иерархии, то есть к производным классам. Если он вызовет метод производного класса в конструкторе, это может привести к манипуляциям с неинициализированными данными — а это наверняка приведет к катастрофе. Следующий пример поясняет суть проблемы:
// polymorphism/PolyConstructors java // Конструкторы и полиморфизм дают не тот // результат, который можно было бы ожидать import static net mindview util Print *.
class Glyph {
void drawO { print("Glyph drawO"), } GlyphO {
printCGlyphO перед вызовом drawO");
drawO.
print ("GlyphO после вызова drawO").



class RoundGlyph extends Glyph { private int radius = 1; RoundGlyph(int r) { radius = r.
print("RoundGlyph RoundGlyph(). radius = " + radius);
}
void drawO {
print ("RoundGlyph. drawO, radius = " + radius);



public class PolyConstructors {
public static void main(String[] args) { new RoundGlyph(5);
}
} /* Output-
GlyphO перед вызовом drawO RoundGlyph drawO, radius = 0 GlyphO после вызова drawO RoundGlyph RoundGlyphO, radius = 5 *///:-
Метод Glyph.draw() изначально предназначен для переопределения в произ­водных классах, что и происходит в RoundGlyph. Но конструктор Glyph вызывает этот метод, и в результате это приводит к вызову метода RoundGlyph.draw(), что вроде бы и предполагалось. Однако из результатов работы программы видно — когда конструктор класса Glyph вызывает метод draw(), переменной radius еще не присвоено даже значение по умолчанию 1. Переменная равна 0. В итоге класс может не выполнить свою задачу, а вам придется долго всматриваться в код программы, чтобы определить причину неверного результата.
Порядок инициализации, описанный в предыдущем разделе, немного непо­лон, и именно здесь кроется ключ к этой загадке. На самом деле процесс ини­циализации проходит следующим образом:
•        Память, выделенная под новый объект, заполняется двоичными нулями.
•        Конструкторы базовых классов вызываются в описанном ранее порядке. В этот момент вызывается переопределенный метод draw() (да,передвы­зовом конструктора класса RoundGlyph), где обнаруживается, что пере­менная radius равна нулю из-за первого этапа.
•        Вызываются инициализаторы членов класса в порядке их определения.
•        Исполняется тело конструктора производного класса.
У происходящего есть и положительная сторона — по крайней мере, данные инициализируются нулями (или тем, что понимается под нулевым значением для определенного типа данных), а не случайным «мусором» в памяти. Это от­носится и к ссылкам на объекты, внедренные в класс с помощью композиции. Они принимают особое значение null. Если вы забудете инициализировать та­кую ссылку, то получите исключение во время выполнения программы. Ос­тальные данные заполняются нулями, а это обычно легко заметить по выход­ным данным программы.

Report Page