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


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, щоб побачити, чи є якісь інші відомості, які я можу включити. Дякую!


6
Якщо ви знаєте, що батьківський клас реалізує, Cloneableто кидання, AssertionErrorа не просто звичайне, Errorє трохи більш виразним.
Ендрю Даффі

Редагувати: Я б краще не використовував clone (), але проект вже заснований на ньому, і не варто було б переробляти всі посилання на цей момент.
Куга

Краще кусати кулю зараз, а не пізніше (ну, якщо тільки ви не збираєтесь вдарити виробництво).
Том Хотін - тайклін

У нас у графіку залишилось півтора місяця, поки це не буде завершено. Ми не можемо виправдати час.
Куга

Я думаю, що це рішення ідеальне. І не почувайтеся погано з використанням клону. Якщо у вас є клас, який використовується як транспортний об'єкт, і він містить лише примітиви та незмінні речовини, такі як String і Enums, все інше, ніж клон, було б повною тратою часу з догматичних причин. Просто майте на увазі, що робить клон, а що ні! (немає глибокого клонування)
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як деякі з них запропонували, здається розумним, але ви також можете додати коментар, який пояснює, чому блок ловлі ніколи не буде введений у цьому конкретному випадку .


Крім того, як і інші запропонували, ви можете, можливо, реалізувати cloneбез виклику super.clone.


1
На жаль, проект уже написаний навколо методом клонування, інакше я б абсолютно не користувався ним. Я цілком погоджуюся з вами, що реалізація клонів Java - це факакта.
Куга

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». В ОП вже висловились, що їх треба використовувати Cloneable. Тож я не маю уявлення, як інакше мою відповідь можна було б покращити, окрім можливо, видалити її прямо.
Кріс Єстер-Янг

9

Є два випадки, коли CloneNotSupportedExceptionзакид буде закинутий:

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

Останній випадок не може виникнути у вашому класі (оскільки ви безпосередньо викликаєте метод надкласу в tryблоці, навіть якщо виклик з виклику підкласу super.clone()), і перший не повинен, оскільки ваш клас явно повинен реалізовувати Cloneable.

В основному, ви повинні точно записати помилку, але в цьому конкретному випадку це станеться лише в тому випадку, якщо ви зіпсуєте визначення свого класу. Таким чином, ставитесь до нього як до перевіреної версії NullPointerException(або подібної) - вона ніколи не буде викинута, якщо ваш код функціональний.


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

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


Якщо об’єкт виявляє публічний метод клонування, будь-який похідний об'єкт, який не підтримував його, порушив би Принцип заміни Ліскова. Якщо метод клонування захищений, я думаю, що було б краще затінити його чимось іншим, ніж метод, що повертає належний тип, щоб запобігти навіть підкласу навіть не намагатися викликати super.clone().
supercat

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методу.
полігенмастильні речовини

Очевидно, що вам потрібно реалізувати цей шаблон на кожному об'єкті, який ви хочете мати можливість клонувати, або знайти інший спосіб глибокого клонування кожного члена.
Дельф

Так, це необхідна вимога.
полігенмастильні матеріали

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інтерфейс.

Крім того, немає необхідності переосмислювати метод, якщо ви не зробите нічого нового в методі, що переосмислюється. Її потрібно переосмислити лише тоді, коли потрібно зробити додаткові операції над об’єктом або потрібно оприлюднити його.

Зрештою, все-таки краще цього уникати і робити це іншим способом.


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

Тільки тому, що реалізація Cloneable від Java зламана, це не означає, що ви не можете створити власну.

Якщо справжньою метою ОП було створення глибокого клону, я думаю, що можливо створити інтерфейс, як це:

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 або іншого коду тривоги.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.