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

114

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

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

Cuga
источник
6
Если вы знаете, что родительский класс реализует, Cloneableтогда бросание AssertionErrorпростого, а не простого Errorбудет немного более выразительным.
Эндрю Даффи,
Изменить: я бы предпочел не использовать clone (), но проект уже основан на нем, и на этом этапе не стоит проводить рефакторинг всех ссылок.
Cuga,
Лучше укусить пулю сейчас, чем позже (ну, если только вы не собираетесь начать производство).
Том Хотин - tackline
У нас осталось полтора месяца до завершения. Мы не можем оправдать время.
Cuga
Я считаю это решение идеальным. И не расстраивайтесь из-за использования клона. Если у вас есть класс, который используется в качестве транспортного объекта и содержит только примитивы и неизменяемые объекты, такие как String и Enums, все остальное, кроме clone, будет пустой тратой времени по догматическим причинам. Просто имейте в виду, что клон делает, а что нет! (без глубокого клонирования)
TomWolk

Ответы:

125

Вы обязательно должны использовать 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.

полигенные смазочные материалы
источник
1
К сожалению, проект уже написан с использованием метода клонирования, иначе я бы его категорически не использовал. Я полностью согласен с вами, что реализация клона в Java является факактом.
Cuga,
5
Если класс и все его суперклассы вызывают super.clone()внутри своих методов клонирования, подкласс, как правило, должен переопределить, только clone()если он добавляет новые поля, содержимое которых необходимо клонировать. Если какой-либо суперкласс использует newвместо super.clone(), то все подклассы должны переопределить clone(), добавляют ли они какие-либо новые поля.
supercat
56

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

public MyObject (MyObject toClone) {
}

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

Аарон Дигулла
источник
11

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

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}
Крис Джестер-Янг
источник
См. Ответ @polygenelubricants, почему это может быть плохой идеей.
Карл Рихтер
@KarlRichter Я читал их ответ. Он не говорит, что это плохая идея, кроме того, что Cloneableв целом это неверная идея, как объясняется в Эффективной Java. OP уже выразил, что они должны использовать Cloneable. Поэтому я понятия не имею, как еще можно улучшить свой ответ, кроме, возможно, его полного удаления.
Крис Джестер-Янг
9

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

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

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

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


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

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

Анджей Дойл
источник
Если объект предоставляет общедоступный метод клонирования, любой производный объект, который его не поддерживает, будет нарушать принцип подстановки Лискова. Если метод клонирования защищен, я бы подумал, что было бы лучше затенять его чем-то другим, а не методом, возвращающим правильный тип, чтобы предотвратить даже попытку вызова подкласса super.clone().
supercat 05
5

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

Михал Зибер
источник
Это было отличное и очевидное решение, учитывая, что я уже использовал GSON.
Джейкоб Табак
3

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

/* 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);
}
Дольф
источник
3
В большинстве случаев это не работает. Вы не можете вызвать clone()объект, возвращаемый, getMySecondMember()если у него нет public cloneметода.
polygenelubricants
Очевидно, вам необходимо реализовать этот шаблон для каждого объекта, который вы хотите клонировать, или найти другой способ глубокого клонирования каждого члена.
Дольф,
Да, это необходимое требование.
polygenelubricants
1

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

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

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

Haggra
источник
0
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) {

                }
        }
    }
}
Квант_хв
источник
Итак, вы только что записали его в файловую систему и прочитали объект обратно. Хорошо, это лучший способ справиться с клоном? Может ли кто-нибудь из сообщества SO прокомментировать этот подход? Полагаю, это излишне связывает клонирование и сериализацию - две совершенно разные концепции. Я подожду, чтобы узнать, что другие скажут по этому поводу.
Саураб Патил
2
Это не в файловой системе, это в основной памяти (ByteArrayOutputStream). для глубоко вложенных объектов это решение работает хорошо. Особенно, если он вам нужен не часто, например в модульном тесте. где производительность не является главной целью.
AlexWien
0

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

Франческо Рого
источник