Композиция беше изпозвана доста често до този момент. Просто се слагат манипулатори на обекти в новия клас. Например нека искаме обект, който съдържа няколко String обекта, двойка примитиви и обект от друг клас. За непримитивните обекти само слагаме манипулатора в новия клас, а за примитивите просто ги декларираме в новия клас: (Виж стр. 89 ако има проблеми с пускането на тази програма.)
//: c06:SprinklerSystem.java
// Composition for code reuse
package c06;
class WaterSource {
private String s;
WaterSource() {
System.out.println("WaterSource()");
s = new String("Constructed");
}
public String toString() { return s; }
}
public class SprinklerSystem {
private String valve1, valve2, valve3, valve4;
WaterSource source;
int i;
float f;
void print() {
System.out.println("valve1 = " + valve1);
System.out.println("valve2 = " + valve2);
System.out.println("valve3 = " + valve3);
System.out.println("valve4 = " + valve4);
System.out.println("i = " + i);
System.out.println("f = " + f);
System.out.println("source = " + source);
}
public static void main(String[] args) {
SprinklerSystem x = new SprinklerSystem();
x.print();
}
} ///:~
Един от методите определени в WaterSource е специален: toString( ). Ще научите по-късно в тази глава, че всеки непримитивен обект има toString( ) метод и той се вика в специални ситуации когато компилаторът иска String но работи с един от тези обекти. Така в израза:
System.out.println("source = " + source);
Компилаторът вижда че се опитваме да добавим String обект (“source = “) към WaterSource. Това е безсмислено за него, понеже може само да се “събира” String с друг стринг String, така че казва “Ще превърна source в String чрез извикване на toString( )!” След като свърши това той може да комбинира двата Stringа и да прати резултата String на System.out.println( ). Всеки път когато искате да осигурите такова поведение за писан от вас клас трябва само да напишете toString( ) метод.
На пръв поглед би могло да се предположи че компилаторът автоматично ще конструира обекти за всеки от манипулаторите в горния код, например извиквайки конструктор по подразбиране за WaterSource за да инициализира source. Изходът е в действителност:
valve1 = null
valve2 = null
valve3 = null
valve4 = null
i = 0
f = 0.0
source = null
Примитивите които са полета в клас автоматично са инициализирани с нула, както отбелязахме в глава 2. Но манипулаторите на обекти се инициализират с null и ако се опитате да извикате метод на кийто и да е от тях ще получите изключение. Много добре е наистина (и полезно) че все пак може да ги изпечатате без изхвърляне на изключение.
Смисълът да не се създават обекти за всеки манипулатор веднага е че това често би било напразно. Ако искате да се инициализират манипулаторите, може да го направите:
-
В точката където се декларира обектът. Това значи че винаги ще бъдат инициализирани преди да се извика конструкторът на обекта.
-
В конструктора на съответния клас
-
Непосредствено преди употребата на съответния обект. Това може да намали ненужната работа в случаи, когато обектът може и да не се наложи да бъде създаден.
Всичките три подхода са показани тук:
//: c06:Bath.java
// Constructor initialization with composition
class Soap {
private String s;
Soap() {
System.out.println("Soap()");
s = new String("Constructed");
}
public String toString() { return s; }
}
public class Bath {
private String
// Initializing at point of definition:
s1 = new String("Happy"),
s2 = "Happy",
s3, s4;
Soap castille;
int i;
float toy;
Bath() {
System.out.println("Inside Bath()");
s3 = new String("Joy");
i = 47;
toy = 3.14f;
castille = new Soap();
}
void print() {
// Delayed initialization:
if(s4 == null)
s4 = new String("Joy");
System.out.println("s1 = " + s1);
System.out.println("s2 = " + s2);
System.out.println("s3 = " + s3);
System.out.println("s4 = " + s4);
System.out.println("i = " + i);
System.out.println("toy = " + toy);
System.out.println("castille = " + castille);
}
public static void main(String[] args) {
Bath b = new Bath();
b.print();
}
} ///:~
Забележете че в Bath се изпълнява оператор преди всички инициализации да са направени. Когато не инициализирате при декларацията, няма гаранция, че ще извършите инициализация преди да подадете съобщение към обектовия манипулатор – освен за неизбежното изключение по време на изпълнение.
Ето изходът от програмата:
Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castille = Constructed
Когато print( ) се извика той попълва s4 така че всички полета са правилно инициализирани преди да са използвани.
Синтаксис за наследяването
Наследяването е толкова интегрална част от Java (и ООП езици изобщо), че беше въведена в глава 1 и биваше използвана на места преди тази глава в ситуации където е необхадима. Освен това винаги се прави наследяване, когато се създава клас, понеже ако и да не се пише нищо специално се наследява стандартния коренен клас Object на Java.
Синтаксисът за композиция е очевиден, но за наследяване е различно. Когато се наследява, казваме “Този нов клас прилича на оня стар клас.” Това се изрича в програмата чрез даване на име на новия клас както обикновено, но преди отварящата скоба на тялото на класа се слага ключовата дума extends следвана от името на базовия клас. Като направите така автоматично взимате всички членове-данни и методи на базовия клас. Ето пример:
//: c06:Detergent.java
// Inheritance syntax & properties
class Cleanser {
private String s = new String("Cleanser");
public void append(String a) { s += a; }
public void dilute() { append(" dilute()"); }
public void apply() { append(" apply()"); }
public void scrub() { append(" scrub()"); }
public void print() { System.out.println(s); }
public static void main(String[] args) {
Cleanser x = new Cleanser();
x.dilute(); x.apply(); x.scrub();
x.print();
}
}
public class Detergent extends Cleanser {
// Change a method:
public void scrub() {
append(" Detergent.scrub()");
super.scrub(); // Call base-class version
}
// Add methods to the interface:
public void foam() { append(" foam()"); }
// Test the new class:
public static void main(String[] args) {
Detergent x = new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
x.print();
System.out.println("Testing base class:");
Cleanser.main(args);
}
} ///:~
Това демонстрира няколко неща. Първо, в Cleanser append( ) метода Stringовете се конкатенират в s чрез използване на += оператор, който е един от операторите (заедно с ‘+’) които проектантите на Java “претовариха” за да работят със Stringове.
Второ, и Cleanser и Detergent съдържат main( ) метод. Може да създадете main( ) за всеки от вашите класове и често се препоръчва да се програмира по този начин за да се тества кодът затворен в класа. Даже и да имате много класове в програмата само main( ) за public класа извикан в командния ред на програмата ще бъде извикан. (И може да имате само един public клас на файл.) Така в този случай като напишем java Detergent, Detergent.main( ) ще бъде извикано. Но може също да се напише java Cleanser за да се извика Cleanser.main( ), въпреки че Cleanser не е public клас. Тази техника на слагане на main( ) във всеки клас позволява лесно тестване на единиците във всеки клас. И не е необходимо да махате main( ) когато приключите тестването; може да го оставите за бъдещо тестване.
Може да видите как Detergent.main( ) вика Cleanser.main( ) явно.
Важно е че всички методи в Cleanser са public. Помнете че ако не сложите никакъв спецификатор класът става “приятелски,” което позволява достъп само до членовете в пакета. Така в същия пакет всеки би могъл да използва тези методи, ако нямат спецификатор на достъп. Detergent не би имал проблеми, например. Обаче ако клас от друг пакет наследи Cleanser той би имал достъп само до public членовете. Така че при планиране на наследяването правете всички членове private и всички методи public. (protected членовете също дават достъп на извлечения клас; ще изучите това по-късно.) Разбира се, за конкретен клас ще се постъпи конкретно, тук даваме едн полезна насока.
Забележете че Cleanser има множество методи в интерфейса си: append( ), dilute( ), apply( ), scrub( ) и print( ). Понеже Detergent е извлечен от Cleanser (чрез ключовата дума extends) той автоматично взима всички тези методи в своя интерфейс, даже и да не ги виждате явно декларирани в Detergent. При това положение може да мислите за наследяването като за повторно използване на интерфейса. (Реализацията я имате безплатно, но тази част не е по-важната.)
Както се вижда в scrub( ), възможно е да се вземе метод от базовия клас и да се модифицира. В този случай може да поискате да използвате версията от базовия клас. Но в scrub( ) не може просто да извикате scrub( ), тъъй като това би произвело рекурсивно извикване, което не е желаното от вас. За да реши този проблем Java има ключовата дума super която идва от “superclass” което е класът от който е извлечен въпросният клас. Така изразът super.scrub( ) извиква метода scrub( ) във версията на базовия клас.
Когато наследявате не сте ограничени до методите на базовия клас. Може също да добавите нови методи в новия клас точно както се прави това в клас: просто ги дефинирате. Ключовата дума extends предполага че ще добавите нови методи в интерфейса на класа и методът foam( ) е пример за това.
В Detergent.main( ) може да видите че за Detergent може да викате всички методи достъпни в Cleanser както и в Detergent (т.е.. foam( )).
Инициализиране на базовия клас
Тъй като сега участват два класа – базовия клас и извлечения клас – вместо само един, може да е малко смущаващо ако се опитаме да си представим обект, създаден от извлечения клас. Погледнато отвън може да изглежда, че новият метод има интерфейса на стария и евентуално нови методи. Но наследяването не просто копира интерфеса на стария клас. Когато създавате обект от извлечения клас той съдържа в себе си подобект от базовия клас. Този обект е същият както ако го създадете от самия базов клас. Погледнато отвън това е точно базовият клас да бъде обгърнат от извлечения.
Разбира се, че съдържаният обект трябва да се инициализира правилно и това може да стане само по един начин: езпълнява се инициализация в конструктора, като се извика конструктора на базовия клас, който е с достатъчно знание и привилегии за да се свърши необходимото. Java автоматично вмъква извиквания на конструктора на базовия клас. Следващия пример показва как става това за три нива на наследяване:
//: c06:Cartoon.java
// Constructor calls during inheritance
class Art {
Art() {
System.out.println("Art constructor");
}
}
class Drawing extends Art {
Drawing() {
System.out.println("Drawing constructor");
}
}
public class Cartoon extends Drawing {
Cartoon() {
System.out.println("Cartoon constructor");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
} ///:~
Изходът от тази програма показва автоматичните извиквания:
Art constructor
Drawing constructor
Cartoon constructor
Може да се види, че конструирането става “външно”, за базовия клас, така че базовият клас се инициализира преди конструкторът на извлечения клас да има достъп до него.
Даже ако не създадете конструктор за Cartoon( ), компилаторът ще синтезира сам конструктор по подразбиране, който ще вика конструктора на базовия клас.
Конструктори с аргументи
Горният пример има конструктори по подразбиране; тоест те нямат никакви аргументи. Лесно е за компилатора да вика такива конструктори, понеже не стои въпросът какви аргументи да се дадат. Ако вашият канструктор има аргументи или искате да извикате конструктора на базовия клас с аргументи, използвате ключовата дума super и съответния списък аргументи:
//: c06:Chess.java
// Наследяване, конструктори и аргументи
class Game {
Game(int i) {
System.out.println("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
super(i);
System.out.println("BoardGame constructor");
}
}
public class Chess extends BoardGame {
Chess() {
super(11);
System.out.println("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
} ///:~
Ако не викате конструктора на базовия клас в BoardGame( ), компилаторът ще се оплаква че не може да намери конструктор за Game( ). В добавка извикването на конструктора на базовия клас трябва да бъде първото нещо, което се прави в конструктора на извлечения клас. (Компилаторът ще ви напомни ако не го направите както трябва.)
Хващане на изключенията на базовия канструктор
Както току-що беше отбелязано, компилаторът ви принуждава да сложите извикването на конструктора на базовия клас като първо нещо в тялото на конструктора. Това значи че нищо друго не може да се появи преди него. Както ще видите в глава 9, това също предотвратява прихващането на изключение изхвърлено от базовия клас от извлечения клас. Това може да бъде неудобно в някои случаи.
Сподели с приятели: |