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


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.