Как правильно переопределить метод клонирования?

Мне нужно реализовать глубокий клон в одном из моих объектов, у которого нет суперкласса.

Каков наилучший способ обработки отмеченного 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;
}

Это кажется хорошим решением для меня, но я хотел выдать его сообществу StackOverflow, чтобы узнать, есть ли другие идеи, которые я могу включить. Спасибо!

Ответ 1

Вам нужно использовать clone? Большинство людей согласны с тем, что Java clone не работает.

Джош Блох по дизайну - Копировать конструктор против клонирования

Если вы прочитали статью о клонировании в моей книге, особенно если вы читаете между строками, вы будете знать, что я думаю, что clone глубоко нарушен. [...] Позор, что Cloneable сломан, но это происходит.

Вы можете прочитать больше дискуссий по этой теме в своей книге "Эффективное Java 2-е издание", пункт 11: разумно переопределить clone. Он рекомендует вместо этого использовать конструктор копирования или копию factory.

Он продолжал писать страницы страниц о том, как, если вы считаете, вы должны реализовать clone. Но он закрыл это:

Неужели все эти сложности необходимы? Редко. Если вы расширяете класс, реализующий Cloneable, у вас мало выбора, кроме как реализовать хорошо выполненный метод clone. В противном случае вам лучше предоставить альтернативные способы копирования объектов или просто не предоставлять возможности.

Акцент был его, а не моего.


Поскольку вы ясно дали понять, что у вас мало выбора, кроме как реализовать clone, вот что вы можете сделать в этом случае: убедитесь, что MyObject extends java.lang.Object implements java.lang.Cloneable. В этом случае вы можете гарантировать, что вы НИКОГДА не поймаете CloneNotSupportedException. Throwing AssertionError, как некоторые из них предложили, кажется разумным, но вы также можете добавить комментарий, который объясняет, почему блок catch никогда не будет введен в этом конкретном случае.


В качестве альтернативы, как и другие, вы также можете предложить clone без вызова super.clone.

Ответ 2

Иногда проще реализовать конструктор копирования:

public MyObject (MyObject toClone) {
}

Это избавит вас от проблем с обработкой CloneNotSupportedException, работает с полями final, и вам не нужно беспокоиться о возвращаемом типе.

Ответ 3

Как работает ваш код, он близок к "каноническому" способу его записи. Тем не менее, я бы выбрал AssertionError в catch. Он сигнализирует, что эта линия никогда не должна быть достигнута.

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}

Ответ 4

Есть два случая, в которых будет выбрано CloneNotSupportedException:

  • Класс, клонированный, не реализован Cloneable (предполагая, что фактическое клонирование в конечном итоге отбрасывает метод Object clone). Если класс, который вы пишете этот метод, реализует Cloneable, этого никогда не произойдет (поскольку любые подклассы наследуют его соответствующим образом).
  • Исключением явно выдается реализация - это рекомендуемый способ предотвращения клонирования в подклассе, когда суперкласс является Cloneable.

Последний случай не может произойти в вашем классе (поскольку вы напрямую вызываете метод суперкласса в блоке try, даже если он вызван из подкласса, вызывающего super.clone()), а первый не должен, так как ваш класс явно должен реализовать Cloneable.

В принципе, вы должны обязательно регистрировать ошибку, но в этом конкретном случае это произойдет, только если вы испортите определение своего класса. Таким образом, рассматривайте его как проверенную версию NullPointerException (или аналогичную) - она ​​никогда не будет выброшена, если ваш код будет функционален.


В других ситуациях вам нужно будет подготовиться к этой возможности - нет гарантии, что данный объект является клонируемым, поэтому, если вы поймаете исключение, вы должны предпринять соответствующие действия в зависимости от этого условия (продолжить с существующим объектом, взять альтернативная стратегия клонирования, например serialise-deserialise, бросает исключение IllegalParameterException, если ваш метод требует параметра клонированием и т.д. и т.д.).

Изменить. Хотя в целом я должен указать, что да, clone() действительно сложно реализовать правильно и сложно для вызывающих абонентов, чтобы узнать, будет ли возвращаемое значение тем, что они хотят, вдвойне, когда вы рассмотрите глубокие и мелкие клоны. Часто лучше просто полностью избежать всего этого и использовать другой механизм.

Ответ 5

Используйте сериализацию, чтобы сделать глубокие копии. Это не самое быстрое решение, но оно не зависит от типа.

Ответ 6

Вы можете реализовать защищенные конструкторы копирования следующим образом:

/* 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);
}

Ответ 7

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) {

                }
        }
    }
}

Ответ 8

Насколько мне известно большинство ответов, я должен сказать, что ваше решение также является тем, как это делают фактические разработчики 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.

Кроме того, вам не нужно переопределять метод, если вы не делаете ничего нового в переопределенном методе. Вам нужно только переопределить его, когда вам нужно выполнить дополнительные операции над объектом или вам нужно сделать его общедоступным.

В конечном счете, все же лучше избегать этого и делать это, используя другой способ.

Ответ 9

Просто потому, что реализация Java Cloneable нарушена, это не значит, что вы не можете создать свою собственную.

Если бы реальная цель OP заключалась в создании глубокого клона, я думаю, что можно создать такой интерфейс:

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 или другого трюкового кода.