clone () против конструктора копирования против фабричного метода?


81

Я быстро погуглил по реализации clone () на Java и нашел: http://www.javapractices.com/topic/TopicAction.do?Id=71

Он имеет следующий комментарий:

конструкторы копирования и статические фабричные методы предоставляют альтернативу клонированию, и их намного проще реализовать.

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

Вот проблемы, которые я заметил:

Конструкторы копирования не работают с универсальными шаблонами.

Вот какой-то псевдокод, который не компилируется.

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 () дополнительные усилия по созданию исключений в неклонируемых подклассах кажутся мне тривиальными.

Я что-то упускаю? Приветствуются любые идеи.


Ответы:


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: Посмотри на мой ответ. И я не имел в виду предупреждение о подавлении.
akarnokd

Голосование вниз, потому что Copyable<T>в ответе kd304 гораздо больше смысла использовать.
Саймон Форсберг

25

Также есть паттерн Строитель. См. Подробности в Эффективной 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>.
User1

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

Я получаю сообщение об ошибке в методе копирования G - «необходимо переопределить метод суперкласса»
Карл Притчетт

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

10

В Java нет конструкторов копирования в том же смысле, что и в C ++.

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

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

Для глубокой копии простой подход - сериализовать объект и десериализовать его.

Кстати: я предлагаю использовать неизменяемые объекты, тогда вам не нужно их клонировать. ;)


Не могли бы вы объяснить, почему мне не нужно было бы клонировать, если бы я использовал неизменяемые объекты? В моем приложении мне нужен другой объект, который имеет точно такие же данные, что и существующий объект. Так что мне нужно как-то скопировать
Женя

2
@Ievgen, если вам нужно две ссылки на одни и те же данные, вы можете скопировать ссылку. Вам нужно скопировать содержимое объекта только в том случае, если оно может измениться (но для неизменяемых объектов вы знаете, что это не так)
Питер Лоури,

Наверное, я просто не понимаю преимуществ неизменяемых объектов. Предположим, у меня есть объект JPA / hibernate, и мне нужно создать еще один объект JPA на основе существующего, но мне нужно изменить идентификатор нового объекта. Как бы я сделал это с неизменяемыми объектами?
Женя

@Ievgen по определению не может изменять неизменяемые объекты. Stringнапример, это неизменяемый объект, и вы можете передавать String без его копирования.
Питер Лоури 02

6

Я думаю, что ответ Ишая можно улучшить, чтобы мы не могли получить предупреждение с помощью следующего кода:

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

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

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

Таким образом, класс, который должен реализовывать интерфейс Copyable, должен быть таким:

public class MyClass implements Copyable<MyClass> {

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

}

2
Почти. Интерфейс Copyable должен быть объявлен как:, interface Copyable<T extends Copyable>что ближе всего к типу self, который вы можете кодировать в Java.
Рекурсия

Конечно, вы имели в виду interface Copyable<T extends Copyable<T>>, правда ? ;)
Adowrath 05

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()что не будет вызывать конструктор, и последнее idполе не может быть изменено Person.clone().

Конструкторы копирования лучше, чем Object.clone()потому, что они

  1. Не заставляйте нас реализовывать какой-либо интерфейс или генерировать исключение, но мы, безусловно, можем сделать это, если это потребуется.
  2. Не требует броска.
  3. Не требуйте от нас зависимости от механизма создания неизвестного объекта.
  4. Не требуйте от родительского класса соблюдения каких-либо контрактов или реализации чего-либо.
  5. Позвольте нам изменить окончательные поля.
  6. Позвольте нам иметь полный контроль над созданием объекта, мы можем написать в нем нашу логику инициализации.

Узнайте больше о клонировании Java - конструктор копирования против клонирования


1

Обычно clone () работает в тандеме с конструктором защищенного копирования. Это сделано потому, что clone (), в отличие от конструктора, может быть виртуальным.

В теле класса для производного от суперкласса Base у нас есть

class Derived extends Base {
}

Итак, в самом упрощенном виде, вы должны добавить к этому конструктор виртуальной копии с помощью clone (). (В C ++ Джоши рекомендует клонировать в качестве конструктора виртуального копирования.)

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 () в классе Derived (тот, что выше), это вызовет super.clone () - который может быть реализован или не реализован, но вам рекомендуется вызывать его. Затем реализация передает вывод super.clone () конструктору защищенной копии, который принимает Base, и вы передаете ему любые final члены.

Затем этот конструктор копирования вызывает конструктор копирования суперкласса (если вы знаете, что он есть) и устанавливает финал.

Когда вы вернетесь к методу clone (), вы установите любые не конечные члены.

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


0

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


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 ()), и поэтому создает только мелкие копии.

Даже если ваш собственный объект вызывает .clone для всех своих членов, он все равно не будет полной копией.


4
Может оказаться невозможным выполнить глубокое копирование clone () для любого произвольного объекта, но на практике это выполнимо для многих иерархий объектов. Это просто зависит от того, какие у вас есть объекты и каковы их переменные-члены.
Mr. Shiny and New 安 宇

Здесь гораздо меньше двусмысленности, чем утверждают многие. Если у кого-то есть клонируемый SuperDuperList<T>или производный от него, то его клонирование должно дать новый экземпляр того же типа, что и тот, который был клонирован; он должен быть отделен от оригинала, но должен ссылаться на те же самые Tв том же порядке, что и оригинал. С этого момента ничего, что делается с одним списком, не должно влиять на идентичность объектов, хранящихся в другом. Я не знаю ни одного полезного «соглашения», которое требовало бы от универсальной коллекции для демонстрации любого другого поведения.
supercat
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.