Обикновено с конструкторите е по-различно от другите методи. Това е така и в случая на полиморфизъм. Макар че конструкторите не са полиморфни (въпреки че може да има нещо като “виртуален конструктор,” както ще видите в глава 11), важно е да се разбере как действат конструкторите в големи йерархии и при полиморфизъм. Това разбиране ще ви предпази от навлизане в неприятни положения.
Ред на извикване на конструкторите
Редът на извикване на конструкторите беше накратко изложен в глава 4, но това беше преди да въведем наследяването и полиморфизма.
В конструктора на извлечения клас винаги се вика конструктор на базов клас, докато всичките конструктори на базови класове са извикани. Това има смисъл, понеже конструкторът има специална задача: да гледа дали обектът е построен правилно. Извлеченият клас има достъп само до собствените си членове, той няма достъп до тези на базовия клас (чиито членове типично са private). Само конструкторът на базовия клас има правата и достатъчно знания за да може да инициализира членовете на базовия клас. Така че е важно да се извикат всички конструктори, иначе обектът няма да се конструира правилно. Това е причината компилаторът да заставя да има извикване на конструктор за всяка порция на извлечения клас. Той тихичко ще извика конструктор по позразбиране ако не сте задали конструктор в тялото на класа. Ако няма конструктор по подразбиране, компилаторът ще се оплаква. (В случая когато класът няма конструктори компилаторът автоматично синтезира конструктор по подразбиране.)
Нека да разгледаме пример, който демонстрира ефектите от композицията, наследяването и полиморфизма върху реда на извикване на конструкторите:
//: c07:Sandwich.java
// Order of constructor calls
class Meal {
Meal() { System.out.println("Meal()"); }
}
class Bread {
Bread() { System.out.println("Bread()"); }
}
class Cheese {
Cheese() { System.out.println("Cheese()"); }
}
class Lettuce {
Lettuce() { System.out.println("Lettuce()"); }
}
class Lunch extends Meal {
Lunch() { System.out.println("Lunch()");}
}
class PortableLunch extends Lunch {
PortableLunch() {
System.out.println("PortableLunch()");
}
}
class Sandwich extends PortableLunch {
Bread b = new Bread();
Cheese c = new Cheese();
Lettuce l = new Lettuce();
Sandwich() {
System.out.println("Sandwich()");
}
public static void main(String[] args) {
new Sandwich();
}
} ///:~
Този пример създава сложен клас от други класове, всеки клас има конструктор който се обявява сам. Важният клас е Sandwich, който отразява три нива на наследяване (четири, ако смятате и неявното от Object) и три член-обекти. Когато е създаден Sandwich обект в main( ), изходът е:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
Това показва че редът на извикване на конструкторите в сложен обект е:
-
Вика се конструкторът на базовия клас. Тази стъпка се повтаря рекурсивно така, че конструкторът на кореновия клас се вика първо, после този на първия извлечен клас и т.н., докато се достигне последния извлечен клас.
-
Инициализаторите на членовете се викат по реда на декларирането.
-
Тялото на конструктора на извлечения клас се вика.
Редът на извикване на конструкторите е важен. Огато наследявате, знаете всичко за базовия клас и имате достъп до всички public и protected членове на базовия клас. Това значи, че трябва със сигурност всички членове на базовия клас да съществуват и да са инициализирани когато сте в извлечения клас. В нормален метод конструкцията вече е станала, така че всички членове от всички части на обекта са построени. В конструктора, обаче, трябва да е възможнж до сте сигурни, че всички членове, които ви трябват, ще са построени. Единствения начин да стане това е като се вика първо конструктора на базовия клас. Ака когато сте в конструктора на извлечения клас всичките членове, които може да използвате от базовия клас са вече инициализирани. “Знанието че всичкщи членове са валидни” вътре в конструктора е също причината че, навсякъде където е възможно, ще инициализирате всички член-обекти (т.е. обекти сложени в класа чрез композиция) в точката на дефинирането им в класа (като b, c, и l в горния пример). Ако следвате тази практика, ще помогнете да се осигури всички членове на базовия клас и член-обектите на текущия обект да са инициализирани. За нещастие това не може да стане във всички случаи, както ще видите в следващата секция.
Наследяването и finalize( )
Когато използвате композиция за създаване на нов клас никога не се грижите за финализирането на обекти от този клас. Всеки член е независим обект и като такъв се обработва от боклучаря и се финализира без значение дали се е случило да е член на вашия клас. С наследяването, обаче, трябва да се наследи finalize( ) в извлечения клас ако има специално почистване да се върши към събирането на боклука. Когато подтиснете finalize( ) в наследения клас, важно е да се помни да се извика версията от базовия клас на finalize( ), иначе няма да стане финализацията на базовия клас. Следващия пример показва нещата:
//: c07:Frog.java
// Testing finalize with inheritance
class DoBaseFinalization {
public static boolean flag = false;
}
class Characteristic {
String s;
Characteristic(String c) {
s = c;
System.out.println(
"Creating Characteristic " + s);
}
protected void finalize() {
System.out.println(
"finalizing Characteristic " + s);
}
}
class LivingCreature {
Characteristic p =
new Characteristic("is alive");
LivingCreature() {
System.out.println("LivingCreature()");
}
protected void finalize() {
System.out.println(
"LivingCreature finalize");
// Call base-class version LAST!
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
class Animal extends LivingCreature {
Characteristic p =
new Characteristic("has heart");
Animal() {
System.out.println("Animal()");
}
protected void finalize() {
System.out.println("Animal finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
class Amphibian extends Animal {
Characteristic p =
new Characteristic("can live in water");
Amphibian() {
System.out.println("Amphibian()");
}
protected void finalize() {
System.out.println("Amphibian finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
public class Frog extends Amphibian {
Frog() {
System.out.println("Frog()");
}
protected void finalize() {
System.out.println("Frog finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
public static void main(String[] args) {
if(args.length != 0 &&
args[0].equals("finalize"))
DoBaseFinalization.flag = true;
else
System.out.println("not finalizing bases");
new Frog(); // Instantly becomes garbage
System.out.println("bye!");
// Must do this to guarantee that all
// finalizers will be called:
System.runFinalizersOnExit(true);
}
} ///:~
Класът DoBaseFinalization просто съдържа флаг който казва на всеки клас от йерархията кога да вика super.finalize( ). Този флаг се слага според командния ред, така че може да видите поведението с и без финализация на базовия клас.
Всеки клас в йерархията също съдържа член-обект от тип Characteristic. Ще видите, че независимо как се викат финализаторите на базовия клас, Characteristic член-обектите винаги се финализират.
Всеки подтиснат finalize( ) трябва да има достъп най-малко до protected членовете понеже finalize( ) методът в класа Object е protected и компилаторът няма да позволи да се намали достъпът при наследяването. (“Приятелски” е с по-малък достъп от protected.)
В Frog.main( ) флагът DoBaseFinalization е конфигуриран и единствен Frog обект се създава. Помнете, че събирането на боклука и в частност финализацията биха могли и да не се случат, така че за да се направи да станат System.runFinalizersOnExit(true) добавя допълнителна работа във връзка с гарантирането на това. Без финализацията на базовия клас изходът е:
not finalizing bases
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
Може да видите че, разбира се, не се викат финализатори на базовите класове на Frog. Но ако добавите “finalize” на командния ред получавате:
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
Amphibian finalize
Animal finalize
LivingCreature finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
Макар и редът в който обектите да се финализират да е същия в който се създават, технически редът на финализация не се задава. С базовите класове, обаче, имате пълен контрол върху реда на финализацията. Най-добрият ред е показаният тук, което е обърнатия ред на инициализацията. Следвайки формата използвана за деструкторите в C++ първо ще финализирате извлечения клас, после базовия. Това е защото във финализацията на извлечения клас би могло да се викат методи от базовия, които трябва да са още живи за това и не бива да се извършва финализация на базовия клас предварително.
Поведение на полиморфни методи в конструкторите
Йерархията на извикване на конструкторите изважда наяве интересна дилема. Какво става, ако сте в конструктора и извикате динамично свързан метод на конструирания в момента обект? В обикновен обект може да си въобразим каквото става – динамично свързаното извикване се решава по време на изпълнение, понеже извикващия обек не може да знае дали методът е негов или на извлечен клас. Може за смисленост да се счита, че така става и в случая на конструктор.
Това обаче не е точно. Ако викате динамично свързан метод вътре в конструктор, подтиснатата дефиниция на метода се използва. Обаче ефектът може да бъде неочакван и може да докара трудни за улавяне грешки.
Концептуално задачата на конструктора е да докара обекта в живота (което едва ли е тривиално като постижение). През време на работа на конструктора целия обект може да е само частично построен – знае се само, че базовият клас е бил инициализиран, но не може да се знае кои класове са наследени. Динамичното свързване на метод обаче, търси “напред” или “навън” в йерархията на наследяването. То вика метод в извлечен клас. Ако превите това вътре в конструктор, викате член който манипулира членове които може още да не са инициализиране – сигурна рецепта за катастрофа.
Може да видите проблема в следния пример:
//: c07:PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect.
abstract class Glyph {
abstract void draw();
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println(
"RoundGlyph.RoundGlyph(), radius = "
+ radius);
}
void draw() {
System.out.println(
"RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
} ///:~
В Glyph методът draw( ) е abstract, така че е проектиран да бъде подтиснат. Разбира се ние сме принудени да го подтиснем в RoundGlyph. Но конструктора на Glyph вика този метод, извикването свършва в RoundGlyph.draw( ), което изглежда да е желаното. Но погледнете изхода:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
Когато конструктора на Glyph вика draw( ), стойността на radius даже не е началната стойност по подразбиране 1. Тя е нула. Това вероятно ще причини точка или нищо да се изобрази на екрана, а вие ще се чуците защо програмата не работи.
Редът на инициализация описан в предната секция не е съвсем пълен, а точно липсващата част е ключът към мистерията. Фактическият процес на инициализация е:
-
Паметта алокирана за обект се инициализира с нула преди да се направи каквото и да е друго.
-
Викат се конструкторите на базови класове както бе описано преди. В тази точка подтиснатия draw( ) метод се вика, (да, преди конструкторът на RoundGlyph да се извика), като стойността на radius е нула, поради т.1.
-
Инициализаторите на членове се викат по реда на декларирането.
-
Тялото на конструктора на извлечения клас се вика.
По-добре е всичко да се инициализира с нула (или каквото нулата значи за конкретния вид данни) отколкото да е просто боклук. Това включва манипулаторите на обекти които са вградени в обект чрез композиция. Така че ако забравите да инициализирате такъв манипулатор щяхте да полечите изключение по време на изпълнение. Всичко обаче става нула, която обикновено е издайническа стойност, видяна в изход от програма.
От друга страна, трябва множко да се уплашите от изхода на тази програма. Направихте логични предположения и все пак програмата е мистериозно неверна, без оплаквания от компилатора. (C++ има по-рационално поведение в такива ситуации.) Бъгове от този род лесно могат да бъдат погребани и да отнемат много време за откриването им.
Като резултат, ето добро правило за конструкторите: “Прави минималното за да стане обекта и ако е възможно, избягвай викането на методи.” Единствените безопасни за викане в конструктори методи са тези които са final в базовия клас. (Това също се отнася за private които са автоматично final.) Те не могат да бъдат подтиснати и не могат да донесат такива изненади.
Сподели с приятели: |