37

37


// 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. Если вы забудете инициализировать та­кую ссылку, то получите исключение во время выполнения программы. Ос­тальные данные заполняются нулями, а это обычно легко заметить по выход­ным данным программы.
С другой стороны, результат программы выглядит довольно жутко. Вроде бы все логично, а программ ведет себя загадочно и некорректно без малейших объяснений со стороны компилятора. (В языке С++ такие ситуации обрабаты­ваются более рациональным способом.) Поиск подобных ошибок занимает много времени.
При написании конструктора руководствуйтесь следующим правилом: не пытайтесь сделать больше для того, чтобы привести объект в нужное состоя­ние, и по возможности избегайте вызова каких-либо методов. Единственные методы, которые можно вызывать в конструкторе без опаски — неизменные (final) методы базового класса. (Сказанное относится и к закрытым (private) ме­тодам, поскольку они автоматически являются неизменными.) Такие методы невозможно переопределить, и поэтому они застрахованы от «сюрпризов».
Ковариантность возвращаемых типов
В Java SE5 появилась концепцияковариантности возвращаемых типов; этот термин означает, что переопределенный метод производного класса может вер­нуть тип, производный от типа, возвращаемого методом базового класса:
//: polymorph!sm/CovanantReturn java
class Grain {
public String toStringO { return "Grain"; }
}
class Wheat extends Grain {
public String toStringO { return "Wheat"; }

class Mill {
Grain process О { return new GrainO; }
}
class WheatMill extends Mill {
Wheat process О { return new WheatO; }
}
public class CovariantReturn {
public static void main(String[] args) { Mill m = new Mi 11(); Grain g = m.processO; System out println(g); m = new WheatMi 110; g = m process О, System out.println(g);
}
} /* Output Grain Wheat */// ~
Главное отличие Java SE5 от предыдущих версий Java заключается в том, что старые версии заставляли переопределение process() возвращать Grain вме­сто Wheat, хотя тип Wheat, производный от Grain, является допустимым возвра­щаемым типом. Ковариантность возвращаемых типов позволяет вернуть более специализированный тип Wheat.
Разработка с наследованием
После знакомства с полиморфизмом может показаться, что его следует приме­нять везде и всегда. Однако злоупотребление полиморфизмом ухудшит архи­тектуру ваших приложений.
Лучше для начала использовать композицию, пока вы точно не уверены в том, какой именно механизм следует выбрать. Композиция не стесняет разра­ботку рамками иерархии наследования. К тому же механизм композиции более гибок, так как он позволяет динамически выбирать тип (а следовательно, и по­ведение), тогда как наследование требует, чтобы точный тип был известен уже во время компиляции. Следующий пример демонстрирует это:
// polymorphi sm/Transmogrify.java // Динамическое изменение поведения объекта // с помощью композиции (шаблон проектирования «Состояние») • import static net.mindview.util.Print.*;
class Actor {
public void act О {}
}
class HappyActor extends Actor {
public void actO { pri nt ("HappyActor"), }
class SadActor extends Actor {
public void act() { printCSadActor"). }
}
class Stage {
private Actor actor = new HappyActor(); public void changeO { actor = new SadActorO. } public void performPlayO { actor.act(), }
}
public class Transmogrify {
public static void main(String[] args) { Stage stage = new StageO; stage. performPlayO; stage. changeO; stage. performPlayO;
}
} /* Output-
HappyActor
SadActor
*///:-
Объект Stage содержит ссылку на объект Actor, которая инициализируется объектом HappyActor. Это значит, что метод performPlayO имеет определенное поведение. Но так как ссылку на объект можно заново присоединить к другому объекту во время выполнения программы, ссылке actor назначается объект SadActor, и после этого поведение метода performPlayO изменяется. Таким обра­зом значительно улучшается динамика поведения на стадии выполнения про­граммы. С другой стороны, переключиться на другой способ наследования во время работы программы невозможно; иерархия наследования раз и навсе­гда определяется в процессе компиляции программы.
Нисходящее преобразование и динамическое определение типов
Так как при проведениивосходящего преобразования(передвижение вверх по иерархии наследования) теряется информация, характерная для определен­ного типа, возникает естественное желание восстановить ее с помощьюнисхо­дящего преобразования.Впрочем, мы знаем, что восходящее преобразование аб­солютно безопасно; базовый класс не может иметь «больший» интерфейс, чем производный класс, и поэтому любое сообщение, посланное базовому классу, гарантированно дойдет до получателя. Но при использовании нисходящего преобразования вы не знаете достоверно, что фигура (например) в действи­тельности является окружностью. С такой же вероятностью она может оказать­ся треугольником, прямоугольником или другим типом.

Report Page