Брюс Эккель - Философия Java3
• Проводится инициализация членов класса в порядке их объявления.
• Вызывается тело конструктора производного класса.
Порядок вызова конструкторов немаловажен. При наследовании вы располагаете полной информацией о базовом классе и можете получить доступ к любому из его открытых (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 живое существо *///:-
Каждый класс в иерархии содержит объекты классов Characteristic и Description, которые также необходимо «завершать». Очередность завершения должна быть обратной порядку инициализации в том случае, если объекты
print ("Amphibian (Г);
}
protected void disposeO {
print ("disposeO в Amphibian "); t.disposeO; p.disposeO; super.disposeO,
зависят друг от друга. Для полей это означает порядок, обратный последовательности объявления полей в классе (инициализация соответствует порядку объявления). В базовых классах сначала следует выполнять финализацию для производного класса, а затем — для базового класса. Это объясняется тем, что завершающий метод производного класса может вызывать некоторые методы базового класса, для которых необходимы действительные компоненты базового класса. Из результатов работы программы видно, что все части объекта 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() будет следить за состоянием счетчика ссылок и сам решит, когда нужно выполнить завершающие действия. Подсчет ссылок требует дополнительных усилий со стороны программиста, но при совместном использовании объектов, требующих завершения, у вас нет особого выбора.
Поведение полиморфных методов при вызове из конструкторов
В иерархиях конструкторов возникает интересный вопрос. Что происходит, если вызвать в конструкторе динамически связываемый метод конструируемого объекта?
В обычных методах представить происходящее нетрудно — динамически связываемый вызов обрабатывается во время выполнения, так как объект не знает, принадлежит ли этот вызов классу, в котором определен метод, или классу, производному от этого класса. Казалось бы, то же самое должно происходить и в конструкторах.
Но ничего подобного. При вызове динамически связываемого метода в конструкторе используется переопределенное описание этого метода. Однако последствия такого вызова могут быть весьма неожиданными, и здесь могут крыться некоторые коварные ошибки.
По определению, задача конструктора — дать объекту жизнь (и это отнюдь не простая задача). Внутри любого конструктора объект может быть сформирован лишь частично — известно только то, что объекты базового класса были проини-циализированы. Если конструктор является лишь очередным шагом на пути построения объекта класса, производного от класса данного конструктора, «производные» части еще не были инициализированы на момент вызова текущего конструктора. Однако динамически связываемый вызов может перейти во «внешнюю» часть иерархии, то есть к производным классам. Если он вызовет метод производного класса в конструкторе, это может привести к манипуляциям с неинициализированными данными — а это наверняка приведет к катастрофе. Следующий пример поясняет суть проблемы: