Как правильно переопределить метод клонирования?
Мне нужно реализовать глубокий клон в одном из моих объектов, который не имеет суперкласса.
Каков наилучший способ справиться с проверенным CloneNotSupportedException
брошенный суперклассом (который Object
)?
Коллега посоветовал мне справиться с этим следующим образом:
@Override
public MyObject clone()
{
MyObject foo;
try
{
foo = (MyObject) super.clone();
}
catch (CloneNotSupportedException e)
{
throw new Error();
}
// Deep clone member fields here
return foo;
}
Мне кажется, это хорошее решение, но я хотел поделиться им с сообществом Stackru, чтобы узнать, есть ли какие-то другие идеи, которые я могу включить. Спасибо!
9 ответов
Вы обязательно должны использовать clone
? Большинство людей согласны с тем, что Java clone
сломано.
Джош Блох о дизайне - копирование конструктора против клонирования
Если вы читали статью о клонировании в моей книге, особенно если вы читаете между строк, вы знаете, что я думаю,
clone
глубоко сломан. [...] Обидно, чтоCloneable
сломан, но это случается.
Более подробное обсуждение этой темы вы можете прочитать в его книге " Эффективное Java, 2-е издание", пункт 11: "Переопределение" clone
разумно. Вместо этого он рекомендует использовать конструктор копирования или фабрику копирования.
Он продолжал писать страницы страниц о том, как, если вы чувствуете, что должны, вы должны реализовать clone
, Но он закончил с этим:
Действительно ли все эти сложности необходимы? Редко. Если вы расширяете класс, который реализует
Cloneable
, у вас мало выбора, кроме как вести себя хорошоclone
метод. В противном случае вам лучше предоставить альтернативные средства копирования объектов или просто не предоставить такую возможность.
Акцент был его, а не мой.
Так как вы дали понять, что у вас мало выбора, кроме как реализовать clone
вот что вы можете сделать в этом случае: убедитесь, что MyObject extends java.lang.Object implements java.lang.Cloneable
, Если это так, то вы можете гарантировать, что вы НИКОГДА не поймаете CloneNotSupportedException
, Бросание AssertionError
как некоторые предлагали, кажется разумным, но вы также можете добавить комментарий, объясняющий, почему блок catch никогда не будет введен в данном конкретном случае.
В качестве альтернативы, как другие также предложили, вы можете реализовать clone
без звонка super.clone
,
Иногда проще реализовать конструктор копирования:
public MyObject (MyObject toClone) {
}
Это избавляет вас от проблем обработки CloneNotSupportedException
, работает с final
поля, и вам не нужно беспокоиться о типе для возврата.
То, как работает ваш код, очень близко к "каноническому" способу его написания. Я бы бросил AssertionError
хотя в улове. Это говорит о том, что эта линия никогда не должна быть достигнута.
catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
Есть два случая, когда CloneNotSupportedException
будут брошены
- Клонируемый класс не реализован
Cloneable
(при условии, что фактическое клонирование в конечном итогеObject
клоновый метод). Если класс вы пишете этот метод в реализуетCloneable
это никогда не произойдет (поскольку любые подклассы будут наследовать его соответствующим образом). - Исключение явно генерируется реализацией - это рекомендуемый способ предотвращения клонируемости в подклассе, когда суперкласс
Cloneable
,
Последний случай не может возникнуть в вашем классе (так как вы напрямую вызываете метод суперкласса в try
блок, даже если вызван из вызова подкласса super.clone()
) и первый не должен, так как ваш класс должен четко реализовать Cloneable
,
По сути, вы должны регистрировать ошибку наверняка, но в данном конкретном случае это произойдет, только если вы испортите определение вашего класса. Таким образом относитесь к нему как к проверенной версии NullPointerException
(или аналогичный) - он никогда не будет выдан, если ваш код функционален.
В других ситуациях вам необходимо быть готовым к такой возможности - нет гарантии, что данный объект является клонируемым, поэтому при отлове исключения вы должны предпринять соответствующие действия в зависимости от этого условия (продолжить работу с существующим объектом, принять альтернативную стратегию клонирования). например, сериализация-десериализация, бросить IllegalParameterException
если ваш метод требует параметр cloneable и т. д. и т. д.).
Изменить: Хотя в целом я должен отметить, что да, clone()
действительно трудно реализовать правильно и трудно для вызывающих, чтобы знать, будет ли возвращаемое значение тем, что они хотят, вдвойне, если учесть глубокие и мелкие клоны. Часто лучше просто избегать всего этого и использовать другой механизм.
Используйте сериализацию, чтобы делать глубокие копии. Это не самое быстрое решение, но оно не зависит от типа.
Вы можете реализовать конструкторы защищенных копий следующим образом:
/* This is a protected copy constructor for exclusive use by .clone() */
protected MyObject(MyObject that) {
this.myFirstMember = that.getMyFirstMember(); //To clone primitive data
this.mySecondMember = that.getMySecondMember().clone(); //To clone complex objects
// etc
}
public MyObject clone() {
return new MyObject(this);
}
Поскольку большинство ответов здесь верны, я должен сказать, что ваше решение так же, как это делают настоящие разработчики Java API. (Либо Джош Блох, либо Нил Гафтер)
Вот выдержка из openJDK, класс ArrayList:
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
Как вы заметили, и другие упоминали, CloneNotSupportedException
почти не имеет шансов быть брошенным, если вы заявили, что реализуете Cloneable
интерфейс.
Кроме того, вам не нужно переопределять метод, если вы не делаете ничего нового в переопределенном методе. Вам нужно переопределить его только тогда, когда вам нужно выполнить дополнительные операции над объектом или вам нужно сделать его публичным.
В конечном счете, все же лучше избегать этого и делать это другим способом.
public class MyObject implements Cloneable, Serializable{
@Override
@SuppressWarnings(value = "unchecked")
protected MyObject clone(){
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
ByteArrayOutputStream bOs = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bOs);
oos.writeObject(this);
ois = new ObjectInputStream(new ByteArrayInputStream(bOs.toByteArray()));
return (MyObject)ois.readObject();
} catch (Exception e) {
//Some seriouse error :< //
return null;
}finally {
if (oos != null)
try {
oos.close();
} catch (IOException e) {
}
if (ois != null)
try {
ois.close();
} catch (IOException e) {
}
}
}
}
То, что Java-реализация Cloneable не работает, не означает, что вы не можете создать свою собственную.
Если реальной целью ОП было создание глубокого клона, я думаю, что возможно создать такой интерфейс:
public interface Cloneable<T> {
public T getClone();
}
затем используйте конструктор прототипа, упомянутый ранее, чтобы реализовать его:
public class AClass implements Cloneable<AClass> {
private int value;
public AClass(int value) {
this.vaue = value;
}
protected AClass(AClass p) {
this(p.getValue());
}
public int getValue() {
return value;
}
public AClass getClone() {
return new AClass(this);
}
}
и другой класс с полем объекта AClass:
public class BClass implements Cloneable<BClass> {
private int value;
private AClass a;
public BClass(int value, AClass a) {
this.value = value;
this.a = a;
}
protected BClass(BClass p) {
this(p.getValue(), p.getA().getClone());
}
public int getValue() {
return value;
}
public AClass getA() {
return a;
}
public BClass getClone() {
return new BClass(this);
}
}
Таким образом, вы можете легко и глубоко клонировать объект класса BClass без необходимости @SuppressWarnings или другого хитрого кода.