Би могло да се помисли че за премахване на клонируемостта просто трябва да се направи метода clone( )да бъде private, но няма да стане така понеже не може да се направи метод в базовия клас по-private в извлечен клас. Така че не е толкова просто. И все пак, необходимо е да се управлява клонируемостта на обектите. Трябва да се обърне внимание на няколко неща в тази връзка:
-
Безразличие. Не правите нищо за да бъде вашият клас клонируем, но наследниците му могат да се направят да бъдат ако е необходимо. Това работи само ако Object.clone( ) по подразбиране прави необходимото с полетата във вашия клас.
-
Поддръжка на clone( ). Следвайте стандартната практика на прилагане на Cloneable и подтискане на clone( ). В подтиснатия clone( ) викате super.clone( ) и прихващате всички изключения (така че подтиснатия clone( ) не изхвърля изключения).
-
Условно поддържане на клонирането. Ако вашият клас съдържа манипулатори които могат да бъдат или да не бъдат клонируеми (един пример е клас-колекцията), може да се опитате да клонирате всички тях, а ако изхвърлят изключения - просто да ги отминете. Например да вземем специален вид ArrayList който се опитва да клонира всички съдържани в него обекти. Когато пишете такъв ArrayList, не знаете що за обекти ще сложи вътре клиент-програмистът във вашия ArrayList, така че не знаете дали са клонируеми.
-
Не прилагайте Cloneable ами подтиснете clone( ) като protected, давайки коректно поведение три копирането на всички полета. По този начин всеки който наследява този клас може да подтисне clone( ) и извика super.clone( ) за получаване на коректно поведение прикопирането. Забележете че вашата реализация може и трябва да вика super.clone( ) даже ако методът очаква Cloneable обект (иначе ще се изхвърли изключение), понеже никой няма да го вика от обект точно с вашия тип. Той ще се вика само от наследен клас, който, за да работи успешно, прилага Cloneable.
-
Опитайте се да предотвратите клонирането чрез неприлагане на Cloneable и подтискане на clone( ) за изхвърляне на изключение. Това е успешно само ако всеки извлечен клас вика super.clone( ) в своята повторна дефиниция на clone( ). Иначе програмистът би могъл да го заобиколи.
-
Предотвратете клонирането чрез правене на вашия клас final. Ако clone( ) не е бил подтиснат в някой от предшестващите ваши класове, не може и да бъде вече. Ако е бил, подтиснете го и изхвърлете CloneNotSupportedException. Правенето на класа final е единствения начин за гарантирана невъзможност за клониране. Освен това когато имате работа по сигурността или други случаи когато трябва да се управлява броя на създаваните обекти, ще направите всичките конструктори private и ще дадете един или повече специални методи за създаване на обекти. По този начин тези митоди могат да въведат ограничения върху броя на обектите и условията при които те се създават. (Частен случай на това е singleton шаблона описан в глава 16.)
Ето пример който показва как могат да се реализират различните начини за клониране и после, надолу по йерархията, да бъдат “изключени:”
//: c12:CheckCloneable.java
// Checking to see if a handle can be cloned
// Can't clone this because it doesn't
// override clone():
class Ordinary {}
// Overrides clone, but doesn't implement
// Cloneable:
class WrongClone extends Ordinary {
public Object clone()
throws CloneNotSupportedException {
return super.clone(); // Throws exception
}
}
// Does all the right things for cloning:
class IsCloneable extends Ordinary
implements Cloneable {
public Object clone()
throws CloneNotSupportedException {
return super.clone();
}
}
// Turn off cloning by throwing the exception:
class NoMore extends IsCloneable {
public Object clone()
throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
class TryMore extends NoMore {
public Object clone()
throws CloneNotSupportedException {
// Calls NoMore.clone(), throws exception:
return super.clone();
}
}
class BackOn extends NoMore {
private BackOn duplicate(BackOn b) {
// Somehow make a copy of b
// and return that copy. This is a dummy
// copy, just to make the point:
return new BackOn();
}
public Object clone() {
// Doesn't call NoMore.clone():
return duplicate(this);
}
}
// Can't inherit from this, so can't override
// the clone method like in BackOn:
final class ReallyNoMore extends NoMore {}
public class CheckCloneable {
static Ordinary tryToClone(Ordinary ord) {
String id = ord.getClass().getName();
Ordinary x = null;
if(ord instanceof Cloneable) {
try {
System.out.println("Attempting " + id);
x = (Ordinary)((IsCloneable)ord).clone();
System.out.println("Cloned " + id);
} catch(CloneNotSupportedException e) {
System.out.println(
"Could not clone " + id);
}
}
return x;
}
public static void main(String[] args) {
// Upcasting:
Ordinary[] ord = {
new IsCloneable(),
new WrongClone(),
new NoMore(),
new TryMore(),
new BackOn(),
new ReallyNoMore(),
};
Ordinary x = new Ordinary();
// This won't compile, since clone() is
// protected in Object:
//! x = (Ordinary)x.clone();
// tryToClone() checks first to see if
// a class implements Cloneable:
for(int i = 0; i < ord.length; i++)
tryToClone(ord[i]);
}
} ///:~
Първият клас, Ordinary, представя видовете кладове които сме срещали по протежение на книгата: без поддръжка на клониране, но както излиза, също и без предотвратяване на клонирането. Но ако имате манипулатор към Ordinary обект който може да е бил ъпкастнат от по-извлечен (по-нататък в йерархията - б.пр.) клас, не може да кажете дали е клонируем или не.
Класът WrongClone показва некоректен начин за реализиране на клонирането. Той подтиска Object.clone( ) и го прави public, но не прилага Cloneable, та когато се извика super.clone( ) (което резултира в извикване на Object.clone( )), CloneNotSupportedException се изхвърля така че клонирането не работи.
В IsCloneable може да видите всички правилни действия свързани с клонирането: clone( ) е подтиснато и Cloneable се прилага. Обаче този clone( ) метод и няколко други нататък в примера не прихващат CloneNotSupportedException, а вместо това го подават на извикващия метод, та трябва да има трай-хвани блок около него. Във вашия собствен clone( ) метод типично ще прихващате CloneNotSupportedException вътре в clone( ) наместо да го подавате нататък. Както ще видите, за този пример е по-информиращо да се предаде изключението.
Класът NoMore се опитва да “изключи” по начина желан от дизайнерите на Java: в извлечения клас clone( ) изхвърляте CloneNotSupportedException. Методът clone( ) в класа TryMore правилно вика super.clone( ), това довежда NoMore.clone( ), който изхвърля изключение и предотвратява клонирането.
Ами ако прогамистът не следва “правилния” начин за викане на super.clone( ) в подтиснатия метод clone( )? В BackOn може да видите това да се случи. Този клас използва отделен метод duplicate( ) за да направи копие на текущия обект и вика този метод вътре в clone( ) вместо да вика super.clone( ). Не се изхвърля изключение и новият клас е клонируем. Не може да разчитате на изхвърлянето на изключения за предотвратяването на клонируемостта на клас. Единственото истинско решение е посочено в ReallyNoMore, който е final и затова не може да бъде наследяван. Това значи че ако clone( ) изхвърля изключение във final клас това не може да се промени чрез наследяване и махавето на клонируемостта е гарантирано. (Не може явно да извикате Object.clone( ) от клас, който има произволно ниво на наследяване; ограничени сте до super.clone( ), който има достъп само до прекия базов клас.) Така, ако направите обекти които са свързани с въпроси на сигурността, ще вземете да ги направите final.
Първия метод който виждате в класа CheckCloneable е tryToClone( ), който взема Ordinary обект и проверява дали той е клонируем с instanceof. Акое, прави каст към IsCloneable, вика clone( ) и прави каст на резултатите обратно към Ordinary, прихващайки всички изхвърлени изключения. Забележете използването на идентификация на типа по време на изпълнение (виж глава 11) за извеждане на името на класа, та да видите какво става.
В main( ), различни типове от Ordinary се създават и ъпкастват Ordinary в дефиницията на масива. Първите две линии след това създават прост Ordinary обект и се опитват да го клонират. Този код обаче няма да се компилира понеже clone( ) е protected метод в Object. Останалата част от кода пробягва масива и се опитва да клонира всеки обект, докладвайки за успеха или неуспеха на всяко начинание. Изходът е:
Attempting IsCloneable
Cloned IsCloneable
Attempting NoMore
Could not clone NoMore
Attempting TryMore
Could not clone TryMore
Attempting BackOn
Cloned BackOn
Attempting ReallyNoMore
Could not clone ReallyNoMore
Като резюме, ако искате клас да бъде клонируем:
-
Прилагате Cloneable интерфейс.
-
Подтискате clone( ).
-
Викате super.clone( ) вътре в clone( ).
-
Хващате изключенията вътре в clone( ).
Това ще доведе до най-подходящия ефект.
Копи-конструкторът
Клонирането изглежда сложен за организиране процес. Може да изглежда, че е необходима алтернатива. Единият подход който може да ви хареса (особено ако сте C++ програмист) е да се направи специален конструктор, който да дублицира класовете. В C++ това се нарича копи(ращ) конструктор. Отначало всичко изглежда очевидно. Ето пример:
//: c12:CopyConstructor.java
// A constructor for copying an object
// of the same type, as an attempt to create
// a local copy.
class FruitQualities {
private int weight;
private int color;
private int firmness;
private int ripeness;
private int smell;
// etc.
FruitQualities() { // Default constructor
// do something meaningful...
}
// Other constructors:
// ...
// Copy constructor:
FruitQualities(FruitQualities f) {
weight = f.weight;
color = f.color;
firmness = f.firmness;
ripeness = f.ripeness;
smell = f.smell;
// etc.
}
}
class Seed {
// Members...
Seed() { /* Default constructor */ }
Seed(Seed s) { /* Copy constructor */ }
}
class Fruit {
private FruitQualities fq;
private int seeds;
private Seed[] s;
Fruit(FruitQualities q, int seedCount) {
fq = q;
seeds = seedCount;
s = new Seed[seeds];
for(int i = 0; i < seeds; i++)
s[i] = new Seed();
}
// Other constructors:
// ...
// Copy constructor:
Fruit(Fruit f) {
fq = new FruitQualities(f.fq);
seeds = f.seeds;
// Call all Seed copy-constructors:
for(int i = 0; i < seeds; i++)
s[i] = new Seed(f.s[i]);
// Other copy-construction activities...
}
// To allow derived constructors (or other
// methods) to put in different qualities:
protected void addQualities(FruitQualities q) {
fq = q;
}
protected FruitQualities getQualities() {
return fq;
}
}
class Tomato extends Fruit {
Tomato() {
super(new FruitQualities(), 100);
}
Tomato(Tomato t) { // Copy-constructor
super(t); // Upcast for base copy-constructor
// Other copy-construction activities...
}
}
class ZebraQualities extends FruitQualities {
private int stripedness;
ZebraQualities() { // Default constructor
// do something meaningful...
}
ZebraQualities(ZebraQualities z) {
super(z);
stripedness = z.stripedness;
}
}
class GreenZebra extends Tomato {
GreenZebra() {
addQualities(new ZebraQualities());
}
GreenZebra(GreenZebra g) {
super(g); // Calls Tomato(Tomato)
// Restore the right qualities:
addQualities(new ZebraQualities());
}
void evaluate() {
ZebraQualities zq =
(ZebraQualities)getQualities();
// Do something with the qualities
// ...
}
}
public class CopyConstructor {
public static void ripen(Tomato t) {
// Use the "copy constructor":
t = new Tomato(t);
System.out.println("In ripen, t is a " +
t.getClass().getName());
}
public static void slice(Fruit f) {
f = new Fruit(f); // Hmmm... will this work?
System.out.println("In slice, f is a " +
f.getClass().getName());
}
public static void main(String[] args) {
Tomato tomato = new Tomato();
ripen(tomato); // OK
slice(tomato); // OOPS!
GreenZebra g = new GreenZebra();
ripen(g); // OOPS!
slice(g); // OOPS!
g.evaluate();
}
} ///:~
Това изглежда малко странно на пръв поглед. Плодът има качества сигурно, но защо просто не се сложат даннови членове които ги представят направо в класа Fruit? Има две потенциални причини. Първата е че може да искате бързо и лесно да променяте количествата. Забележете че Fruit има protected метод addQualities( ) за да позволи на извлечените класове да направят това. (Може да помислите че е логично да има protected конструктор на Fruit който приема FruitQualities аргумент, но конструкторите не се наследяват и той ще бъде безполезен във второто и по-високо ниво на йерархията.) Чрез правене на качествата на плодовете отделен клас се получава голяма гъвкавост включително възможността да се променят по време на живота на конкретен обект.
Има втора причина да се направи FruitQualities отделен обект в случай че смятате да добавяте количества или да променяте поведението чрез наследяване и полиморфизъм. Забележете че за GreenZebra (което в действителност е вид домати – развъждал съм ги и те са баснословни), конструкторът вика addQualities( )и му подава ZebraQualities обект, който е извлечен от FruitQualities така че може да бъде присъединен към FruitQualities манипулатора на базовия клас. Разбира се, когато GreenZebra използва FruitQualities трябва да се направи даункаст към провилния тип (както се вижда в evaluate( )), но той винаги знае че типът е ZebraQualities.
Ще видите също че има Seed клас и че Fruit (който по дефиниция си има семена) съдържа масив от Seed-ове.
Накрая забележете че всеки клас си има копи-конструктор и че всеки копиращ конструктор трябва да се грижи за викането на копиконструктора на базовия клас и членовете необходими за дълбокото копиране. Копи конструкторът е пробван вътре в класа CopyConstructor. Методът ripen( ) взима Tomato аргумент и изпълнява копиращо конструиране с него за да дублицира обекта:
t = new Tomato(t);
докато slice( ) взима по-родовия Fruit и така го дублицира:
f = new Fruit(f);
Те са пробвани с радлични видове Fruit в main( ). Ето изхода:
In ripen, t is a Tomato
In slice, f is a Fruit
In ripen, t is a Tomato
In slice, f is a Fruit
Ето тука се показва проблемът. След копиращата конструкция която става с Tomato вътре в slice( ), резултатът не е вече Tomato обект, а просто Fruit. Той е загубил всичко доматено. По-нататък, когато вземате GreenZebra, както ripen( ) така и slice( ) се превръщат в Tomato и Fruit, респективно. Така, за нещастие, схемата на копиращите конструктори не е полезна за нас в Java когато се опитваме да направим локално копие на обект.
Защо работи в C++ и не работи в Java?
Копи констукторът е основна част от C++, понеже автоматично прави локално копие на обект. И все пак гопният пример показва че това не работи в Java. Защо? В Java всичко с което работим е манипулатор, докато в C++ може да имате прилични на манипулатори същности и също може да подадете обект направо. За това е копи-конструкторът в C++: когато искате да вземете обект и да го подадете по стойност, дублицирайки по този начин обекта. Това работи чудесно в C++, но ще запомните че тази схема се проваля в Java, така че не я използвайте.
Сподели с приятели: |