Книга е още в много ранна фаза на написване


Конструктори и полиморфизъм



страница42/73
Дата25.07.2016
Размер13.53 Mb.
#6732
1   ...   38   39   40   41   42   43   44   45   ...   73

Конструктори и полиморфизъм


Обикновено с конструкторите е по-различно от другите методи. Това е така и в слу­чая на полиморфизъм. Макар че конструкторите не са полиморфни (въ­пре­ки че може да има нещо като “виртуален конструктор,” както ще видите в глава 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()

Това показва че редът на извикване на конструкторите в сложен обект е:


  1. Вика се конструкторът на базовия клас. Тази стъпка се повтаря рекурсивно та­ка, че конструкторът на кореновия клас се вика първо, после този на пър­вия извлечен клас и т.н., докато се достигне последния извлечен клас.

  2. Инициализаторите на членовете се викат по реда на декларирането.

  3. Тялото на конструктора на извлечения клас се вика.

Редът на извикване на конструкторите е важен. Огато наследявате, знаете всич­ко за базовия клас и имате достъп до всички 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. Тя е нула. Това вероятно ще причини точ­ка или нищо да се изобрази на екрана, а вие ще се чуците защо програмата не ра­боти.

Редът на инициализация описан в предната секция не е съвсем пълен, а точ­но липсващата част е ключът към мистерията. Фактическият процес на ини­циа­ли­зация е:


  1. Паметта алокирана за обект се инициализира с нула преди да се направи ка­кво­то и да е друго.

  2. Викат се конструкторите на базови класове както бе описано преди. В тази точ­ка подтиснатия draw( ) метод се вика, (да, преди конструкторът на RoundGlyph да се извика), като стойността на radius е нула, поради т.1.

  3. Инициализаторите на членове се викат по реда на декларирането.

  4. Тялото на конструктора на извлечения клас се вика.

По-добре е всичко да се инициализира с нула (или каквото нулата значи за кон­крет­ния вид данни) отколкото да е просто боклук. Това включва ма­ни­пу­ла­то­ри­те на обекти които са вградени в обект чрез композиция. Така че ако забравите да инициализирате такъв манипулатор щяхте да полечите изключение по време на изпълнение. Всичко обаче става нула, която обикновено е издайническа стой­ност, видяна в изход от програма.

От друга страна, трябва множко да се уплашите от изхода на тази програма. На­правихте логични предположения и все пак програмата е мистериозно не­вер­на, без оплаквания от компилатора. (C++ има по-рационално поведение в та­ки­ва ситуации.) Бъгове от този род лесно могат да бъдат погребани и да отнемат мно­го време за откриването им.

Като резултат, ето добро правило за конструкторите: “Прави минималното за да стане обекта и ако е възможно, избягвай викането на методи.” Единствените безо­пасни за викане в конструктори методи са тези които са final в базовия клас. (Това също се отнася за private които са автоматично final.) Те не могат да бъдат подтиснати и не могат да донесат такива изненади.




Сподели с приятели:
1   ...   38   39   40   41   42   43   44   45   ...   73




©obuch.info 2024
отнасят до администрацията

    Начална страница