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

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

Каков наилучший способ справиться с проверенным 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 будут брошены

  1. Клонируемый класс не реализован Cloneable (при условии, что фактическое клонирование в конечном итоге Objectклоновый метод). Если класс вы пишете этот метод в реализует Cloneableэто никогда не произойдет (поскольку любые подклассы будут наследовать его соответствующим образом).
  2. Исключение явно генерируется реализацией - это рекомендуемый способ предотвращения клонируемости в подклассе, когда суперкласс 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 или другого хитрого кода.

Другие вопросы по тегам