Как правильно переопределить метод клонирования? - PullRequest
108 голосов
/ 24 февраля 2010

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

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

Ответы [ 9 ]

118 голосов
/ 24 февраля 2010

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

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

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

Более подробное обсуждение этой темы вы можете прочитать в его книге Effective Java 2nd Edition, Item 11:Переопределить clone разумно .Вместо этого он рекомендует использовать конструктор копирования или фабрику копирования.

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

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

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


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


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

55 голосов
/ 24 февраля 2010

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

public MyObject (MyObject toClone) {
}

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

11 голосов
/ 24 февраля 2010

Способ работы вашего кода довольно близок к "каноническому" способу его написания. Я бы бросил AssertionError в улове, хотя. Это сигнализирует, что эта линия никогда не должна быть достигнута.

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}
9 голосов
/ 24 февраля 2010

В двух случаях будет выброшено CloneNotSupportedException:

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

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

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


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

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

5 голосов
/ 24 февраля 2010

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

3 голосов
/ 24 февраля 2010

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

/* 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);
}
0 голосов
/ 04 ноября 2017

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

0 голосов
/ 17 августа 2016

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

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

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

0 голосов
/ 16 января 2013
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) {

                }
        }
    }
}
...