Cloneable - маркерный интерфейс без методов. Он не объявляет clone(), а меняет поведение protected Object.clone(): если класс реализует Cloneable, super.clone() возвращает пофайловую копию; если нет - бросается CloneNotSupportedException.
Механизм сломан по дизайну: конфликтует с final полями, бросает лишнее checked-исключение, требует приведения типов, делает shallow copy по умолчанию. Для нового кода предпочтительны copy constructor и copy factory.
Почему Cloneable считают ошибкой дизайна
| Обычный интерфейс | Cloneable |
|---|---|
| Объявляет методы, которые класс умеет | Не объявляет ничего |
| Говорит "что класс может делать" | Меняет поведение protected-метода суперкласса |
| Реализация - обязанность класса | Реализация - магия внутри Object |
Ключевой парадокс: Cloneable не даёт публичного clone(). Просто написав implements Cloneable, вы не получаете возможность клонировать объект извне - clone() остаётся protected. Нужно ещё переопределить его как public.
Контракт clone()
Спецификация Object.clone() формулирует его нестрого:
x.clone() != x // true
x.clone().getClass() == x.getClass() // "обычно" true, не обязательно
x.clone().equals(x) // "обычно" true, не обязательно
Плюс соглашение: объект должен создаваться через super.clone(), а не через конструктор.
Почему super.clone(), а не new? Если класс B extends A, и A.clone() вернёт new A(...), то B.clone() получит объект типа A - и приведение к B упадёт с ClassCastException. Цепочка super.clone() доходит до Object.clone(), который создаёт объект фактического класса через рантайм-механизм.
Случай 1: только примитивы и immutable-поля
public final class PhoneNumber implements Cloneable {
private final int areaCode;
private final int prefix;
private final int lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNum = lineNum;
}
@Override
public PhoneNumber clone() { // ковариантный возвращаемый тип (Java 5+)
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // не может случиться: мы Cloneable
}
}
}
Три момента, которые спрашивают:
- Ковариантный возвращаемый тип - возвращаем
PhoneNumber, а неObject. Клиенту не нужен каст. Доступно с Java 5. - Приведение внутри метода -
super.clone()объявлен какObject, поэтому каст нужен, но он всегда безопасен. try/catchвокругCloneNotSupportedException- это checked-исключение, которое здесь никогда не выбросится. Оборачиваем вAssertionError. Чистый шум в коде - и это аргумент противclone().
Случай 2: изменяемое поле - проблема shallow copy
Это главная ловушка. super.clone() копирует поля побитово: для ссылочного поля копируется ссылка, а не объект.
Сломанная версия
public class Stack implements Cloneable {
private Object[] elements;
private int size = 0;
@Override
public Stack clone() {
try {
return (Stack) super.clone(); // ОШИБКА: массив общий!
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
Оригинал и клон делят один массив elements. push в одном стеке испортит другой - инвариант нарушен.
Правильная версия
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone(); // рекурсивно клонируем массив
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
Следствие: clone() несовместим с final
Полю elements нельзя поставить final - иначе result.elements = ... не скомпилируется. Приходится жертвовать иммутабельностью ради clone(). Это структурный изъян, не вопрос стиля.
Случай 3: глубокая структура - рекурсивный deep copy
Если поле - массив ссылок на объекты, которые сами содержат ссылки, array.clone() не спасёт: он скопирует массив, но не узлы внутри.
public class HashTable implements Cloneable {
private Entry[] buckets;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
// рекурсивная копия связного списка
Entry deepCopy() {
return new Entry(key, value,
next == null ? null : next.deepCopy());
}
}
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++) {
if (buckets[i] != null) {
result.buckets[i] = buckets[i].deepCopy();
}
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
Замечание, которое отличает сильного кандидата: рекурсивный deepCopy() тратит один кадр стека на каждый элемент списка и может дать StackOverflowError на длинных бакетах. Итеративный вариант:
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next) {
p.next = new Entry(p.next.key, p.next.value, p.next.next);
}
return result;
}
Ещё один нюанс: result.buckets = new Entry[buckets.length] создаёт новый пустой массив, а не клонирует старый - иначе мы бы сначала скопировали чужие ссылки, а потом их перезаписали.
Ещё подводные камни
Не вызывайте переопределяемые методы из clone()
Если clone() вызовет нефинальный метод, подкласс отработает до того, как его собственное состояние восстановлено, - получится повреждённый клон, из-за динамической диспетчеризации. Все вспомогательные методы, вызываемые из clone(), должны быть final или private.
Та же логика, что и запрет на вызов переопределяемых методов из конструктора.
Публичный clone() не должен объявлять throws
Хотя Object.clone() объявляет CloneNotSupportedException, ваш публичный clone() должен его проглатывать - иначе клиенты вынуждены писать бессмысленный try/catch.
Наследование
Класс, спроектированный для наследования, не должен реализовывать Cloneable. Два варианта:
- Повторить поведение
Object:protected clone()сthrows CloneNotSupportedException, дав подклассам свободу выбора. - Заблокировать вовсе:
@Override protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); }
Потокобезопасность
Если класс потокобезопасен, его clone() тоже нужно синхронизировать. Object.clone() не синхронизирован.
Пустой clone() для абстракций
Если Cloneable реализует абстрактный класс, вся тяжесть корректной реализации ложится на подклассы - они могут о ней не знать.
Правильная альтернатива: copy constructor / copy factory
// Copy constructor (conversion constructor)
public Yum(Yum yum) {
this.field = yum.field;
this.list = new ArrayList<>(yum.list);
}
// Copy factory (conversion factory)
public static Yum newInstance(Yum yum) {
return new Yum(yum);
}
Почему это лучше
Проблема clone() |
Copy constructor |
|---|---|
| Внеязыковой механизм создания объекта | Обычный конструктор |
Конфликт с final полями |
final работает нормально |
Checked CloneNotSupportedException |
Никаких лишних исключений |
| Требует приведения типов | Типобезопасен |
| Возвращает объект того же класса | Может принимать интерфейсный тип |
Про последний пункт - козырь на интервью
Copy constructor может принимать интерфейс, а не конкретный класс. Это позволяет конвертировать реализацию:
TreeSet<String> tree = new TreeSet<>(...);
HashSet<String> hash = new HashSet<>(tree); // конверсия реализации
С clone() так нельзя: он всегда вернёт TreeSet.
Именно поэтому все коллекции в JDK предоставляют conversion constructor, принимающий Collection.
Итоговое правило
- Для нового класса - не реализуйте
Cloneable. Используйте copy constructor / copy factory. - Реализуйте
Cloneableтолько если наследуетесь от класса, который его уже требует, и нет разумной альтернативы. - Для массивов
clone()- идиоматичный и предпочтительный способ копирования. Это единственное убедительное исключение.int[] copy = original.clone(); // без каста: массивы имеют ковариантный clone() - Интерфейсы никогда не должны расширять
Cloneable.
Типичные вопросы на интервью
В чём разница между shallow и deep copy?
Shallow - копируются значения полей, включая ссылки; вложенные объекты общие. Deep - вложенные изменяемые объекты копируются рекурсивно. Object.clone() по умолчанию делает shallow.
Почему Cloneable называют маркерным интерфейсом и что в нём необычного?
Нет методов. Необычно то, что он не декларирует возможность, а меняет поведение метода суперкласса. Прецедент почти уникальный в JDK.
Что будет, если вызвать clone(), не реализовав Cloneable?
CloneNotSupportedException из Object.clone().
Почему CloneNotSupportedException - checked, хотя не должно?
Историческое решение. Если класс реализует Cloneable, оно никогда не выбросится, поэтому checked-статус только добавляет шум. Bloch отмечает это как изъян API.
Можно ли клонировать объект с final изменяемым полем?
Корректно - нет: нельзя присвоить новую глубокую копию. Придётся убирать final.
Почему clone() в Object объявлен protected?
Чтобы не навязывать клонирование всем классам. Класс должен явно решить, поддерживать ли его, и повысить видимость до public.
Что вернёт super.clone() в подклассе?
Объект того класса, которым объект является на самом деле, - а не того, в чьём коде написан вызов.
Если у объекта класса B (наследника A) вызвать clone(), то даже когда super.clone() срабатывает внутри метода класса A, вернётся B. Object.clone() смотрит на this.getClass() - реальный тип объекта - и копирует все его поля, включая те, что объявлены в B. Поэтому каст к B безопасен.
Именно поэтому копию нельзя создавать конструктором: new A(...) внутри A.clone() всегда даст A, и наследник получит ClassCastException.
Альтернативы clone() для deep copy?
Copy constructor, copy factory, сериализация (медленно, требует Serializable), сторонние библиотеки. Для value-объектов - сделать класс immutable, тогда копирование вообще не нужно.
Чек-лист "если всё-таки пишу clone()"
implements Cloneablepublicoverride с ковариантным возвращаемым типом- Первым делом -
super.clone(), никогда неnew CloneNotSupportedExceptionпойман и завёрнут вAssertionError- Каждое изменяемое ссылочное поле скопировано глубоко
- Изменяемые поля не помечены
final - Никаких вызовов переопределяемых методов
- Рекурсия проверена на глубину (или заменена итерацией)
- Синхронизация, если класс потокобезопасен
throwsв сигнатуре отсутствует
Комментарии
Пока нет комментариев. Будьте первым.