% Cloneable
Интерфейс: java.lang.Cloneable
Интерфейс без методов, обозначающий, что объект поддерживает клонирование — глубокое копирование, при котором создаётся новый объект с копией состояния текущего объекта, желательно полностью независимый от исходного.
Вот полное определение этого интерфейса:
public interface Cloneable { }
Клонирование используется в первую очередь затем, чтобы после него работать с двумя копиями данных независимо, так, чтобы они не портили друг друга. Это полезно, например, при защитном копировании (defensive copying) данных, передаваемых во внешний код. (См. Effective Java.)
Лучше — никак. Неизменяемые объекты клонировать нет смысла, для изменяемых же предпочтительнее идиома конструктора копии. Даже стандартные классы, поддерживающие клонирование, далеко не всегда удовлетворяют принципу глубокого копирования. Массивы и стандартные контейнеры при клонировании просто копируют ссылки на свои элементы в новый объект, не применяя к ним глубокого копирования.
Сам по себе интерфейс Cloneable
не содержит методов, а метод Object.clone
, который переопределяют реализующие
Cloneable
классы, объявлен как protected
. Поэтому классы, реализующие Cloneable
, нельзя даже клонировать
единообразным образом без использования рефлексии.
Кроме того, механизм клонирования был спроектирован в Java 1.0. Как и многие компоненты стандартной библиотеки Java 1.0, он во многом спроектирован ужасно, и его не стоит рассматривать как пример для подражания.
Вы точно хотите продолжать?
Если всё-таки хотите — читайте дальше.
Поскольку в интерфейсе Cloneable
по историческим причинам отсутствует публичный метод clone
, нам придётся объявить
его самостоятельно.
Сложность клонирования заключается в том, что оно должно возвращать объект того же класса, что и клонируемый объект. В классах, спроектированных для наследования, это требование обеспечить не так просто, ведь производные классы должны возвращать экземпляры производного класса, а не базового.
Для классов, не предназначенных для наследования, метод clone
гарантированно будет возвращать экземпляр самого класса,
а не какого-то из его (отсутствующих!) подклассов, поэтому для них легко реализовать метод clone
через конструктор
копии:
public final class Point implements Cloneable {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
// Конструктор копии
public Point(Point p) {
this(p.x, p.y);
}
@Override
public Point clone() {
return new Point(this);
}
}
(Почему @Override
? Что мы здесь переопределяем? До этого мы доберёмся.)
Поскольку класс Point
объявлен как final
, он гарантирует, что любой объект, возвращаемый его методом clone
, будет
экземпляром самого класса Point
, и что сам метод clone
не будет переопределён в подклассах. Такой контроль над
иерархией классов значительно упрощает и проектирование, и реализацию. Ещё лучше, кстати, было бы определить класс
Point
как неизменяемый, после чего отпала бы даже необходимость в копировании.
Если же класс проектируется для наследования, реализация метода clone
становится сложнее и обрастает нюансами.
В случае наследования нам нужен способ создать внутри метода clone
экземпляр именно того класса, для которого вызван
метод — это может быть либо наш класс, либо какой-то его подкласс. Эта задача — полиморфное клонирование
— решается с помощью псевдоконструктора Object.clone
. Этот метод объявлен в классе Object
следующим образом:
protected native Object clone() throws CloneNotSupportedException;
На самом деле наш публичный метод clone
, объявленный выше в классе Point
, переопределяет метод Object.clone
,
поэтому он и помечен аннотацией @Override
.
Метод Object.clone
ужасен. Нет, правда, он действительно ужасен. Он сочетает в себе сразу несколько плохих
архитектурных решений, которые не стоит воспроизводить в своём коде.
Object.clone
создаёт экземпляр того же класса, что и вызывающий класс, не вызывая никакой его конструктор. Поэтому он и называется псевдоконструктором.Object.clone
возвращает поверхностную копию (shallow copy) вызывающего объекта с копиями всех его полей. Поля объектного типа копируются по ссылке и продолжают ссылаться на те же объекты, что и поля исходного объекта. Это может нарушить некоторые предположения, которые объект делает о своём состоянии.- Если вызывающий класс не реализует интерфейс
Cloneable
, тоObject.clone
бросает исключениеCloneNotSupportedException
. Это грубое нарушение основной цели интерфейсов — описание контракта, который должны поддерживать реализующие классы. В интерфейсеCloneable
вообще нет методов — он не определяет контракт, а изменяет поведение базового класса, и это единственный пример такого использования интерфейсов во всей стандартной библиотеке. Здесь была бы уместнее аннотация, но, к сожалению, в Java 1.0 аннотаций ещё не было. - Исключение
CloneNotSupportedException
является проверяемым — несмотря на то, что методObject.clone
являетсяprotected
, доступен только из подклассов, и выброс этого интерфейса сигнализирует об ошибке в коде, а не о нештатной ситуации в среде исполнения. Здесь уместнее было бы непроверяемое исключение. - Поскольку публичные методы
clone
переопределяютObject.clone
, к методуObject.clone
нельзя непосредственно доступиться из классов, производных от классов с публичным методомclone
. Если в суперклассе публичный методclone
реализован неправильно — tough cookies.
В языке Java есть два псевдоконструктора:
clone
иreadObject
. Со вторым из них мы познакомимся позже, при рассмотрении встроенного механизма сериализации.
Тем не менее в реализации clone
для классов, спроектированных для наследования, нам придётся использовать
Object.clone
, потому что лучше всё равно ничего нет: мы не можем обязать каждый подкласс переопределять метод clone
,
как и не можем обязать каждый подкласс реализовывать конструктор копии, чтобы вызывать его рефлексией.
Итак, как реализовать свой метод clone
через Object.clone
?
public class Line implements Cloneable {
private Point start;
private Point end;
@Override
public Line clone() {
// что-то там с Object.clone()
}
}
По шагам:
-
Сначала разберёмся с типом возвращаемого значения. Метод
Object.clone
возвращаетObject
, но объект, возвращаемыйLine.clone
, является как минимум экземпляромLine
(возможно — одного из подклассовLine
), поэтому можно объявить наш методclone
как возвращающий объект типаLine
. Вообще считается хорошим тоном при реализацииclone
объявлять его как возвращающий объект того же класса. (К сожалению, по историческим причинам в стандартных классахDate
,ArrayList
и т.д. этого не сделано.) -
Следующая неприятность — исключение
CloneNotSupportedException
. Поскольку наш класс реализуетCloneable
, то оно никогда не выбросится, поэтому имеет смысл обернуть его в идиому "невозможное исключение" (см. статью поThrowable
):
@Override
public Line clone() {
Line result;
try {
// Вызываем Object.clone() через синтаксис super
result = (Line) super.clone();
} catch (CloneNotSupportedException e) {
// Идиома "невозможное исключение"
throw new AssertionError(e);
}
// Что-то ещё?
return result;
}
- Если объект не содержит ссылок на изменяемые объекты, на этом можно и остановиться — поверхностное копирование,
выполняемое по умолчанию методом
Object.clone
, нам подходит. В противном случае, чтобы сделать состояние оригинала и копии полностью независимым, нужно скопировать и эти объекты тоже — методомclone
, конструктором копии, либо, в худшем случае, ручным копированием их состояния. В нашем случае поля классаLine
включают две ссылки на объекты классаPoint
, который сам является изменяемым, поэтому нужно скопировать и их:
@Override
public Line clone() {
Line result;
try {
// Вызываем Object.clone() через синтаксис super
result = (Line) super.clone();
} catch (CloneNotSupportedException e) {
// Идиома "невозможное исключение"
throw new AssertionError(e);
}
// В этом месте пока ещё start и result.start ссылаются на один и тот же объект,
// аналогично end и result.end, поэтому копируем
result.start = start.clone();
result.end = end.clone();
return result;
}
Что было бы, если бы поля класса Line
были объявлены как final
? Как бы мы реализовали в этом случае полиморфное
клонирование?
public class Line implements Cloneable {
private final Point start;
private final Point end;
@Override
public Line clone() {
<...>
// Ошибка компиляции!
result.start = start.clone();
result.end = end.clone();
return result;
}
}
...А никак.
То есть, серьёзно, никак.
Это фундаментальное ограничение полиморфного клонирования: оно несовместимо со стандартными языковыми средствами
обеспечения неизменяемости. Псевдоконструктор Object.clone
является внеязыковым средством создания новых экземпляров
класса, и компилятор ничего не знает про особенности его работы. Компилятор видит только попытку перезаписать ссылки,
помеченные как final
, и отказывается компилировать такой код.
К сожалению, единственный способ заставить этот код компилироваться — это убрать с полей ограничение final
. Это
ещё одна причина, мешающая сочетать полиморфное клонирование с хорошим проектированием иерархии классов.
Одно хорошее свойство полиморфного клонирования состоит в том, что в подклассах, не добавляющих новых ссылок на изменяемые объекты, мы "бесплатно" получаем правильную поддержку клонирования.
Допустим, мы объявили подкласс класса Line
с дополнительным полем — цветом линии:
public class ColorLine extends Line {
private Color color;
}
Если класс Color
является неизменяемым, то полиморфное клонирование вернёт объект типа ColorLine
, а не Line
:
ColorLine original = <...>;
Line copy = original.clone();
System.out.println(copy.getClass()); // class ColorLine
Здесь есть одна тонкость. Класс ColorLine
наследует метод clone
от класса Line
, поэтому он объявлен как
возвращающий Line
, а не ColorLine
. Чтобы пользователю не приходилось каждый раз делать приведение типа, имеет смысл
переопределить его тривиальной реализацией:
public class ColorLine extends Line {
private Color color;
@Override
public ColorLine clone() {
// Вызываем Line.clone() через синтаксис super
return (ColorLine) super.clone();
}
}
Конечно, любые ссылки на изменяемые объекты, добавленных в подклассе, нужно будет дополнительно скопировать явно, потому
что реализация метода clone
в суперклассе этого не сделает. Так, если бы класс Color
был изменяемым, нам нужно было
бы дополнительно скопировать объект color
:
public class ColorLine extends Line {
private Color color;
@Override
public ColorLine clone() {
// Вызываем Line.clone() через синтаксис super
ColorLine result = (ColorLine) super.clone();
result.color = color.clone();
return result;
}
}
Обратите внимание, что при переопределении публичного метода clone
нам не приходится ловить исключение
CloneNotSupportedException
, потому что ранее при переопределении метода Object.clone
в классе Line
мы убрали это
исключение из объявления метода.