Чтобы отключить возможность клонирования, можно было бы подумать, что достаточно сделать метод clone()
приватным (private
). Однако это не сработает, так как невозможно сделать базовый класс метод более "приватным" в производном классе. Поэтому всё гораздо сложнее. Кроме того, нам нужно контролировать возможность клонирования объекта. Для нашего класса существует множество подходов:
(1) Оставаться нейтральным и ничего не делать относительно клонирования. То есть, хотя наш класс сам не может быть клонирован, производный от него класс может решить, стоит ли его клонировать. Это возможно только тогда, когда Object.clone()
выполняет какие-то разумные действия над полями класса.
(2) Поддерживать clone()
, реализуя стандартное поведение Cloneable
и переопределяя clone()
. В переопределенной версии clone()
можно вызвать super.clone()
и поймать все исключения (чтобы clone()
не выбрасывал никаких исключений).
(3) Условно поддерживать клонирование. Если класс содержит ссылки на другие объекты, которые могут быть клонируемыми (например, коллекционные классы), то можно попытаться клонировать все содержащиеся объекты; если они выбрасывают исключение, просто передать его дальше. Например, если есть специальный Vector
, который пытается клонировать все свои содержимое, но при этом не знает, какие именно объекты будут помещены клиентским программистом в этот Vector
.(4) Не реализуйте Cloneable()
, но сделайте clone()
защищённым (protected
) таким образом, чтобы все поля имели правильное поведение при копировании. Таким образом, любой производный класс сможет переопределить clone()
и вызвать super.clone()
, чтобы обеспечить правильное поведение при копировании. Обратите внимание, что в нашем подходе следует и должен вызываться super.clone()
— даже если ожидалось, что метод будет принимать Cloneable
объект (иначе он выбросил бы исключение), потому что никто не будет вызывать этот метод напрямую на наших типах объектов. Он будет вызван только через производный класс, который должен реализовать Cloneable
, чтобы гарантировать корректность работы.(5) Не реализовывайте Cloneable
, чтобы попытаться запретить клонирование, а также переопределяйте clone()
, чтобы выбрасывать исключение. Чтобы эта стратегия работала, любому производному классу должно быть позволено вызывать super.clone()
из переопределенного clone()
.(6) Установите класс как final
, чтобы предотвратить клонирование. Если метод clone()
ещё не был переопределен ни в одном из наших родительских классов, эта идея не будет успешной. Если же он уже был переопределен, то снова переопределяйте его и "бросьте" исключение CloneNotSupportedException
. Для обеспечения запрета на клонирование установка класса как final
является единственным способом. Кроме того, если объекты конфиденциальны или требуется контроль за количеством создаваемых объектов, все конструкторы должны быть объявлены как private
, а также предоставлен один или более специальных методов для создания объектов. Такой подход позволяет ограничивать количество создаваемых объектов и условия их создания — одним из таких случаев является синглтон (singleton).Ниже приведен пример, который демонстрирует различные способы реализации клонирования и затем отключает его:
//: CheckCloneable.java
// Проверка возможности клонирования объекта
// Клонировать этот объект невозможно,
// так как метод clone() не переопределен:
class Обычный {}
// Переопределяет метод clone(),
// но не реализует интерфейс Cloneable:
class НеправильныйКлон extends Обычный {
public Object clone()
throws CloneNotSupportedException {
return super.clone(); // Вызывает исключение
}
}
// Реализует все необходимое для корректного клонирования:
class ЯВоКлонируемый extends Обычный
implements Cloneable {
public Object clone()
throws CloneNotSupportedException {
return super.clone();
}
}
// Отключает клонирование путем вызова исключения:
class БольшеНеКлонируется extends ЯВоКлонируемый {
public Object clone()
throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
class ПопробоватьОтключитьКлонирование extends БольшеНеКлонируется {
public Object clone()
throws CloneNotSupportedException {
// Вызывает метод clone() из класса БольшеНеКлонируется,
// что приводит к выбросу исключения:
return super.clone();
}
}
class ВозвращениеВКлонирование extends БольшеНеКлонируется {
private ВозвращениеВКлонирование duplicate(ВозвращениеВКлонирование b) {
// Каким-то образом создайте копию объекта b
// и верните эту копию. Это просто демонстрация:
return new ВозвращениеВКлонирование();
}
public Object clone() {
// Не вызывает метод clone() из класса БольшеНеКлонируется:
return duplicate(this);
}
}
``````java
// Наследование от этого класса невозможно,
// поэтому метод clone() нельзя переопределить:
final class СовсемНеКлонируется extends БольшеНеКлонируется {}
``````markdown
публичный класс CheckCloneable {
статический Обычный попытаться_клонировать(Обычный ord) {
Строка id = ord.getClass().getName();
Обычный x = null;
если(ord instanceof Клонируемый) {
попробовать {
вывод.println("Попытка клонирования " + id);
x = (Обычный)(((Клонируемый)ord).клонировать());
вывод.println("Успешное клонирование " + id);
} захватить(Неподдерживаемое_клонирование e) {
вывод.println("Не удалось клонировать " + id);
}
}
вернуть x;
}
публичный статический void основной(Строка[] аргументы) {
// Поднятие типа:
Обычный[] ord = {
новый Клонируемый(),
новый Неправильный_клон(),
новый Больше_нет(),
новый Попробуйте_ещё(),
новый Вернулись(),
новый Опять_больше_нет(),
};
Обычный x = новый Обычный();
// Это не скомпилируется, так как метод клонировать()
// является защищенным в классе Объект:
//! x = (Обычный)x.клонировать();
// метод попытаться_клонировать() сначала проверяет,
// реализует ли класс интерфейс Клонируемый:
для(инт i = 0; i < ord.длина; i++)
попытаться_клонировать(ord[i]);
}
} ///:~
Первый класс `Ordinary` представляет собой наиболее часто встречающийся тип класса в книге: он не поддерживает клонирование, но после его официального использования не запрещает его выполнение.
Однако если имеется ссылка на объект типа Ordinary
, который может быть преобразован из более производного класса, нельзя точно сказать, будет ли этот объект клонируемым.Класс WrongClone
демонстрирует неправильный подход к реализации клонирования. Он действительно переопределяет метод Object.clone()
, делая его публичным, но не реализует интерфейс Cloneable
. Поэтому при вызове метода super.clone()
(что происходит из-за вызова Object.clone()
) выбрасывается исключение CloneNotSupportedException
.
В классе IsCloneable
показаны правильные действия для выполнения клонирования: метод clone()
переопределен, а также реализован интерфейс Cloneable
. Однако метод clone()
и некоторые другие методы этого примера не перехватывают исключение CloneNotSupportedException
, а позволяют ему распространиться до вызывающего кода. В свою очередь, вызывающий код должен использовать блок try-catch
для обработки этого исключения. Обычно в методе clone()
следует перехватывать исключение CloneNotSupportedException
внутри самого метода clone()
, а не позволять ему распространяться дальше. Как вы поймёте позже, для данного примера это является правильной практикой.Класс NoMore
пытается "запретить" клонирование таким образом, как это было задумано разработчиками Java: в методе clone()
производного класса выбрасывается исключение CloneNotSupportedException
. Метод clone()
в классе TryMore
правильно вызывает super.clone()
, что приводит к вызову NoMore.clone()
, которое выбрасывает исключение и запрещает клонирование. Однако, если в переопределенном методе clone()
, программист не будет следовать "правильному" подходу вызова super.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
. Оставшаяся часть кода проходит по массиву и пытается склонировать каждый объект, отдельно сообщая о каждом успехе или неудаче. Выходные данные следующие:
Попытка склонировать IsCloneable
Склонирован IsCloneable
Попытка склонировать NoMore
Не удалось склонировать NoMore
Попытка склонировать TryMore
Не удалось склонировать TryMore
Попытка склонировать BackOn
Склонирован BackOn
Попытка склонировать ReallyNoMore
Не удалось склонировать ReallyNoMore
В целом, если вы хотите, чтобы ваш класс мог клонироваться, вам следует:
(1) Реализовать интерфейс Cloneable
.
(2) Переопределить метод clone()
.
(3) В своём методе clone()
вызвать super.clone()
.
(4) В своём методе clone()
отлавливать исключения.
Эти шаги обеспечат наилучший результат.
class Семечко { // Члены... Семечко() { /* Конструктор по умолчанию / } Семечко(Семечко s) { / Конструктор копирования */ } }
class Фрукт { private КачествоФрукта fq; private int семена; private Семечко[] s;
Фрукт(КачествоФрукта q, int количество_семян) { fq = q; семена = количество_семян; s = new Семечко[семена]; for (int i = 0; i < семена; i++) s[i] = new Семечко(); }
// Другие конструкторы: // ...
// Конструктор копирования: Фрукт(Фрукт f) { fq = new КачествоФрукта(f.fq); семена = f.семена;
// Вызов всех конструкторов копирования Семечка:
for (int i = 0; i < семена; i++)
s[i] = new Семечко(f.s[i]);
// Другие действия копирования...
}
// Чтобы позволить производным конструкторам (или другим // методам) вводить различные качества: protected void добавитьКачества(КачествоФрукта q) { fq = q; }
protected КачествоФрукта получитьКачества() { return fq; } }
class Томат extends Фрукт {
Томат() {
super(new КачествоФрукта(), 100);
}
Томат(Томат t) { // Конструктор копирования
super(t); // Преобразование базового типа для конструктора копирования
// Другие действия копирования...
}
}
class КачестваЗебры extends КачествоФрукта {
private int полосатость;
КачестваЗебры() { // Конструктор по умолчанию
// сделать что-то значимое...
}
КачестваЗebra(КачестваЗebra z) {
super(z);
полосатость = z.полосатость;
}
}
class ГринЗебра extends Томат {
ГринЗебра() {
добавитьКачества(new КачестваЗебры());
}
ГринЗебра(ГринЗебра g) {
super(g); // Вызывает Томат(Томат)
// Восстановление правильных качеств:
добавитьКачества(new КачестваЗебры());
}
void оценить() {
КачестваЗебры zq =
(КачестваЗебры)получитьКачества();
// Сделать что-то с качествами
// ...
}
}
```
В данном случае, единственное изменение — замена `КачестваЗебры(КачестваЗебры z)` на `КачестваЗebra(КачестваЗebra z)` для корректного отображения имени класса. Однако, поскольку это имя класса, его лучше оставить без изменений. Таким образом, остальной текст оставлен без изменений.```Этот пример сначала может показаться немного странным. Качество различных фруктов действительно различается, но почему эти члены, представляющие эти качества, просто помещены в класс `Fruit`? Есть два возможных объяснения. Первое заключается в том, что мы можем хотеть легко вставлять или изменять качество. Обратите внимание, что у `Fruit` есть защищённый метод `addQualities()`, который позволяет производным классам выполнять эти операции вставки или изменения (вы можете подумать, что логичнее было бы использовать защищённый конструктор в `Fruit`, чтобы передать параметр `FruitQualities`, но конструкторы не могут наследоваться, поэтому они недоступны для второго уровня или более глубоких уровней классов). Размещение качеств фрукта в отдельном классе обеспечивает большую гибкость, включая возможность изменения качества во время существования конкретного объекта `Fruit`.
```Ещё одна причина, по которой `FruitQualities` представлен как отдельный объект, заключается в возможности добавления новых качеств или изменения поведения через наследование и полиморфизм. Обратите внимание, что для `GreenZebra` (что фактически является типом томата — я успешно вырастил его, он просто невероятен) конструктор вызывает `addQualities()` и передаёт ему объект `ZebraQualities`. Этот объект наследуется от `FruitQualities`, так что он связан с базовым классом `FruitQualities`. Конечно, после того как `GreenZebra` использует `FruitQualities`, его следует преобразовать в правильный тип (как это показано в `evaluate()`), но он точно знает, что тип — это `ZebraQualities`.
Вы также видите наличие класса `Seed` (зерна), а `Fruit` (как всем известно, фрукты содержат свои семена) содержит массив `Seeds`.
Наконец, обратите внимание, что каждый класс имеет конструктор копий, и каждый конструктор копий должен заботиться о вызове конструктора копий для базового класса и членов объекта, чтобы получить эффект "глубокой копии". Тестирование конструктора копий выполняется внутри класса `CopyConstructor`. Метод `ripen()` требует получения параметра типа `Tomato` и выполнения копирования объекта:
```
t = new Tomato(t);
```
А метод `slice()` требует получения обычного параметра типа `Fruit` и выполнения копирования объекта:
```
f = new Fruit(f);
```Оба метода тестируются вместе с различными типами фруктов в методе `main()`. Вот вывод:
```
В методе ripen, t — это Tomato
В методе slice, f — это Fruit
В методе ripen, t — это Tomato
В методе slice, f — это Fruit
```
Из этого можно сделать вывод о проблеме. После выполнения процесса копирования объекта `Tomato` внутри метода `slice()`, результат больше не является объектом типа `Tomato`, а представляет собой лишь объект типа `Fruit`. В результате все уникальные характеристики объекта `Tomato` (томата) были потеряны. Кроме того, если использовать объект типа `GreenZebra`, то методы `ripen()` и `slice()` превратят его соответственно в объект типа `Tomato` и типа `Fruit`. Таким образом, очень жаль, но механизм конструктора копий в Java не подходит для создания локальной копии объекта.
(1) Почему конструктор копий имеет большее значение в C++, чем в Java?Конструктор копий является важной частью C++, так как он автоматически создаёт локальную копию объекта. Однако пример выше показывает, что этот подход не подходит для использования в Java. Причина заключается в том, что во всех операциях в Java мы работаем со ссылками, тогда как в C++ есть возможность работы с аналогичными ссылками и непосредственной передачи объекта. В этом случае используется конструктор копий C++: чтобы получить объект и передать его по значению, достаточно скопировать объект. Поэтому он хорошо работает в C++, однако важно отметить, что эта механика совершенно не применима в Java, поэтому её следует избегать.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )