clone () проти конструктора копіювання проти заводського методу?


81

Я швидко погуглив про впровадження clone () в Java і знайшов: http://www.javapractices.com/topic/TopicAction.do?Id=71

У ньому є такий коментар:

конструктори копіювання та статичні фабричні методи забезпечують альтернативу клонуванню та набагато простіші у реалізації.

Все, що я хочу, - зробити глибоку копію. Здається, реалізація clone () має багато сенсу, але ця стаття з високим рейтингом Google змушує мене трохи побоюватися.

Ось проблеми, які я помітив:

Конструктори копіювання не працюють із Generics.

Ось псевдокод, який не компілюється.

public class MyClass<T>{
   ..
   public void copyData(T data){
       T copy=new T(data);//This isn't going to work.    
   }
   ..
}

Зразок 1: Використання конструктора копіювання у загальному класі.

Заводські методи не мають стандартних назв.

Цілком приємно мати інтерфейс для багаторазового використання коду.

public class MyClass<T>{
    ..
    public void copyData(T data){
        T copy=data.clone();//Throws an exception if the input was not cloneable
    }
    ..
}

Зразок 2: Використання clone () у загальному класі.

Я помітив, що клонування не є статичним методом, але чи все одно не потрібно робити глибокі копії всіх захищених полів? При реалізації clone () додаткові зусилля, спрямовані на створення винятків у неклонованих підкласах, здаються мені тривіальними.

Мені чогось не вистачає? Будь-яке розуміння буде вдячне.


Дивіться також stackoverflow.com/questions/1086929 / ...
finnw

Відповіді:


52

В основному, клон зламаний . Ніщо не буде працювати з генериками легко. Якщо у вас є щось подібне (скорочено, щоб зрозуміти суть):

public class SomeClass<T extends Copyable> {


    public T copy(T object) {
        return (T) object.copy();
    }
}

interface Copyable {
    Copyable copy();
}

Тоді за допомогою попередження компілятора ви зможете виконати роботу. Оскільки загальні засоби стираються під час виконання, щось, що робить копію, матиме попередження компілятора, що генерує привід. У цьому випадку цього не уникнути. . У деяких випадках цього можна уникнути (спасибі, kb304), але не у всіх. Розглянемо випадок, коли вам доведеться підтримувати підклас або невідомий клас, що реалізує інтерфейс (наприклад, ви переглядали колекцію копіюваних матеріалів, які не обов’язково генерували той самий клас).


2
Також можна обійтися без попередження компілятора.
akarnokd

@ kd304, якщо ви маєте на увазі, що ви пригнічуєте попередження компілятора, правда, але насправді не відрізняється. Якщо ви маєте на увазі, що можете взагалі уникнути необхідності попередження, будь ласка, уточніть.
Yishai

@Yishai: Подивіться на мою відповідь. І я не мав на увазі придушення попередження.
akarnokd

Проголосування проти, оскільки Copyable<T>у відповіді kd304 має набагато більше сенсу використовувати.
Саймон Форсберг

25

Існує також шаблон Builder. Детальнішу інформацію див. У розділі Ефективна Java.

Я не розумію вашої оцінки. У конструкторі копій ви повністю знаєте тип, чому існує потреба використовувати загальні засоби?

public class C {
   public int value;
   public C() { }
   public C(C other) {
     value = other.value;
   }
}

Нещодавно тут було подібне запитання .

public class G<T> {
   public T value;
   public G() { }
   public G(G<? extends T> other) {
     value = other.value;
   }
}

Вибірковий зразок:

public class GenTest {
    public interface Copyable<T> {
        T copy();
    }
    public static <T extends Copyable<T>> T copy(T object) {
        return object.copy();
    }
    public static class G<T> implements Copyable<G<T>> {
        public T value;
        public G() {
        }
        public G(G<? extends T> other) {
            value = other.value;
        }
        @Override
        public G<T> copy() {
            return new G<T>(this);
        }
    }
    public static void main(String[] args) {
        G<Integer> g = new G<Integer>();
        g.value = 1;
        G<Integer> f = g.copy();
        g.value = 2;
        G<Integer> h = copy(g);
        g.value = 3;
        System.out.printf("f: %s%n", f.value);
        System.out.printf("g: %s%n", g.value);
        System.out.printf("h: %s%n", h.value);
    }
}

1
+1 це найпростіший, але ефективний спосіб реалізації конструктора копій
dfa

Зверніть увагу, що MyClass вище є загальним, StackOverflow проковтнув <T>.
Користувач1

Виправлено форматування вашого запитання. Використовуйте чотири пробіли в кожному рядку замість тегів pre + code.
akarnokd

Я отримую помилку щодо методу копіювання G - "повинен перевизначити метод супер класу"
Carl Pritchett

3
(Я знаю, що це старе, але для нащадків) @CarlPritchett: ця помилка з'явиться в Java 1.5 і нижче. Позначення методів інтерфейсу як @ Override дозволено, починаючи з Java 1.6.
Carrotman42

10

Java не має конструкторів копіювання в тому ж сенсі, що і в C ++.

Ви можете мати конструктор, який приймає об’єкт того ж типу як аргумент, але мало класів це підтримує. (менше кількості, яка підтримує клонування)

Для загального клону у мене є допоміжний метод, який створює новий екземпляр класу та копіює поля з оригіналу (неглибока копія), використовуючи відображення (насправді щось на зразок відображень, але швидше)

Для глибокої копії простим підходом є серіалізація об’єкта та його десеріалізація.

BTW: Я пропоную використовувати незмінні об’єкти, тоді вам не потрібно їх клонувати. ;)


Не могли б ви пояснити, чому мені не потрібно було б клонування, якщо я використовував незмінні об’єкти? У моїй програмі мені потрібно мати інший об’єкт, який має точно такі ж дані, як і існуючий об’єкт. Тож мені потрібно якось скопіювати
Женя

2
@Ievgen, якщо вам потрібно два посилання на однакові дані, ви можете скопіювати посилання. Вам потрібно лише скопіювати вміст об’єкта, якщо він може змінитися (але для незмінних об’єктів ви знаєте, що це не відбудеться)
Пітер Лорі

Напевно, я просто не розумію переваг незмінних предметів. Припустимо, у мене є сутність JPA / hibernate, і мені потрібно створити іншу сутність JPA на основі існуючої, але мені потрібно змінити ідентифікатор нової сутності. Як я можу це зробити з незмінними об'єктами?
Женя

@Ievgen за визначенням ви не можете змінювати незмінні об'єкти. Stringнаприклад, це незмінний об'єкт, і ви можете передавати рядок, не копіюючи його.
Пітер Лорі

6

Я думаю, що відповідь на Yishai можна покращити, тому ми не можемо попереджати наступним кодом:

public class SomeClass<T extends Copyable<T>> {

    public T copy(T object) {
        return object.copy();
    }
}

interface Copyable<T> {
    T copy();
}

Таким чином клас, який повинен реалізувати інтерфейс, що копіюється, повинен бути таким:

public class MyClass implements Copyable<MyClass> {

    @Override
    public MyClass copy() {
        // copy implementation
        ...
    }

}

2
Майже. Інтерфейс для копіювання повинен бути оголошений як:, interface Copyable<T extends Copyable>що є найближчим до самотипу , який ви можете закодувати в Java.
Повтор

Звичайно, ви мали на увазі interface Copyable<T extends Copyable<T>>, так? ;)
Адоурат

3

Нижче наведено кілька мінусів, через які багато розробників не використовують Object.clone()

  1. Використання Object.clone()методу вимагає від нас додавання великої кількості синтаксису до нашого коду, наприклад Cloneableінтерфейсу реалізації , визначення clone()методу та дескриптора CloneNotSupportedExceptionі, нарешті, виклику Object.clone()та передачі його нашим об'єктом.
  2. Cloneableв інтерфейсі бракує clone()методу, він є інтерфейсом маркера, і в ньому немає жодного методу, і все-таки нам потрібно реалізувати його лише для того, щоб повідомити JVM, що ми можемо виконувати clone()свій об'єкт.
  3. Object.clone()захищений, тому ми маємо надавати власні clone()та непрямі дзвінки Object.clone()з нього.
  4. Ми не маємо жодного контролю над побудовою об’єкта, оскільки Object.clone()не викликаємо жодного конструктора.
  5. Якщо ми пишемо clone() метод у дочірньому класі, наприклад, Personтоді всі його суперкласи повинні визначати clone()метод у них або успадковувати його від іншого батьківського класу, інакше super.clone()ланцюг не вдасться.
  6. Object.clone()підтримують лише поверхневу копію, тому поля посилань на наш нещодавно клонований об’єкт все одно зберігатимуть об’єкти, в полях яких знаходився наш оригінальний об’єкт. Для того, щоб подолати це, нам потрібно застосувати clone()в кожному класі, хто посилається на наш клас, а потім клонувати їх окремо в нашому clone()методі, як у прикладі нижче.
  7. Ми не можемо маніпулювати кінцевими полями, Object.clone()оскільки кінцеві поля можна змінювати лише за допомогою конструкторів. У нашому випадку, якщо ми хочемо, щоб кожен Personоб'єкт був унікальним за ідентифікатором, ми отримаємо повторюваний об'єкт, якщо використовуємо, Object.clone()оскільки Object.clone()не буде викликати конструктор і finalid поле не може бути змінено зPerson.clone() .

Конструктори копіювання кращі за Object.clone() тому, що вони

  1. Не змушуйте нас реалізовувати будь-який інтерфейс або викидати будь-які винятки, але ми можемо це зробити, якщо це потрібно.
  2. Не вимагають ніяких зліпків.
  3. Не вимагайте від нас залежності від невідомого механізму створення об’єкта.
  4. Не вимагайте від батьківського класу дотримуватися будь-якого договору або реалізовувати щось.
  5. Дозвольте нам змінити кінцеві поля.
  6. Дозвольте нам мати повний контроль над створенням об'єкта, ми можемо записати в нього свою логіку ініціалізації.

Докладніше про клонування Java - конструктор копіювання проти клонування


1

Зазвичай clone () працює в тандемі з конструктором захищених копій. Це робиться тому, що clone (), на відміну від конструктора, може бути віртуальним.

У тілі класу для похідного від бази супер класу ми маємо

class Derived extends Base {
}

Отже, якнай спрощеніше, ви додали б до цього конструктор віртуальної копії з клоном (). (У C ++ Джоші рекомендує clone як конструктор віртуальної копії.)

protected Derived() {
    super();
}

protected Object clone() throws CloneNotSupportedException {
    return new Derived();
}

Це ускладнюється, якщо ви хочете викликати super.clone (), як це рекомендується, і вам потрібно додати цих членів до класу, ви можете спробувати це

final String name;
Address address;

/// This protected copy constructor - only constructs the object from super-class and
/// sets the final in the object for the derived class.
protected Derived(Base base, String name) {
   super(base);
   this.name = name;
}

protected Object clone() throws CloneNotSupportedException {
    Derived that = new Derived(super.clone(), this.name);
    that.address = (Address) this.address.clone();
}

Тепер, якщо страта, ви отримали

Base base = (Base) new Derived("name");

і ви тоді зробили

Base clone = (Base) base.clone();

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

Потім цей конструктор копіювання викликає конструктор копіювання супер-класу (якщо ви знаєте, що він є) і встановлює фінал.

Повернувшись до методу clone (), ви встановлюєте будь-яких непоточних членів.

Проникливі читачі помітять, що якщо у вас є конструктор копій у Базі, він буде викликаний super.clone () - і буде викликаний знову, коли ви викликаєте суперконструктор у захищеному конструкторі, тому ви можете викликати супер-конструктор копій двічі. Сподіваємось, якщо він блокує ресурси, він це знатиме.


0

Один із шаблонів, який може працювати для вас, - це копіювання на рівні компонента. В основному ви використовуєте конструктор no-arg і викликаєте різні сеттери для надання даних. Ви навіть можете використовувати різні бібліотеки властивостей компонента для відносного встановлення властивостей. Це не те саме, що робити клон (), але для багатьох практичних цілей це нормально.


0

Інтерфейс Cloneable пошкоджений, в тому сенсі, що він марний, але клон працює добре і може призвести до кращої продуктивності для великих об'єктів - 8 полів та більше, але тоді він не зможе пройти аналіз втечі. тому переважно використовувати конструктор копій більшу частину часу. Використання клону на масиві швидше, ніж Arrays.copyOf, оскільки довжина гарантовано однакова.

детальніше тут https://arnaudroger.github.io/blog/2017/07/17/deep-dive-clone-vs-copy.html


0

Якщо хтось на 100% не знає всіх примх clone(), то я б радив триматися подалі від цього. Я б цього не сказавclone() зламаний. Я б сказав: використовуйте його лише тоді, коли ви повністю впевнені, що це ваш найкращий варіант. Конструктор копіювання (або фабричний метод, який, на мою думку, не має значення) легко писати (можливо, тривалий, але простий), він копіює лише те, що ви хочете скопіювати, і копіює так, як ви хочете, щоб речі копіювались. Ви можете обрізати його відповідно до ваших потреб.

Плюс: легко налагодити те, що відбувається, коли ви викликаєте свій конструктор копіювання / фабричний метод.

І clone()не створює "глибоку" копію вашого об'єкта нестандартно, припускаючи, що ви маєте на увазі, що Collectionкопіюються не тільки посилання (наприклад, на a ). Але докладніше про глибокі та дрібні читайте тут: Глибока копія, неглибока копія, клон


-2

Вам не вистачає того, що клон створює неглибокі копії за замовчуванням і за домовленістю, і що змусити його створювати глибокі копії загалом неможливо.

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

Навіть якщо ваш власний об'єкт викликає .clone для всіх його учасників, він все одно не буде глибокою копією.


4
Можливо, неможливо виконати глибоке копіювання clone () для будь-якого довільного об'єкта, але на практиці це можливо для багатьох ієрархій об'єктів. Це лише залежить від того, які типи об’єктів у вас є, і які змінні їх членів.
Містер Блискучий та Новий 安 宇

Двозначності набагато менше, ніж стверджують багато людей. Якщо у когось є клонований SuperDuperList<T>або його похідне, тоді клонування повинно дати новий екземпляр того ж типу, що й клонований; його слід від'єднати від оригіналу, але він повинен посилатися на те саме Tв тому ж порядку, що і оригінал. Ніщо, що робиться для того чи іншого списку з цього моменту, не повинно впливати на ідентичність об'єктів, що зберігаються в іншому. Я не знаю жодної корисної "конвенції", яка вимагала б, щоб загальна колекція демонструвала будь-яку іншу поведінку.
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.