clone () против конструктора копирования против метода фабрики? - PullRequest
75 голосов
/ 09 июля 2009

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

Я что-то упустил? Любые идеи будут оценены.

Ответы [ 10 ]

49 голосов
/ 10 июля 2009

В основном, клон сломан . Ничто не будет легко работать с генериками. Если у вас есть что-то вроде этого (сокращено, чтобы понять смысл):

public class SomeClass<T extends Copyable> {


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

interface Copyable {
    Copyable copy();
}

Затем с предупреждением компилятора вы можете выполнить работу. Поскольку генерики стираются во время выполнения, то, что делает копию, будет иметь приведенное в генерации предупреждение компилятора. В этом случае этого не избежать. . Этого можно избежать в некоторых случаях (спасибо, kb304), но не во всех. Рассмотрим случай, когда вам необходимо поддерживать подкласс или неизвестный класс, реализующий интерфейс (например, вы перебираете коллекцию копируемых объектов, которые необязательно генерируют один и тот же класс).

25 голосов
/ 10 июля 2009

Существует также шаблон 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);
    }
}
8 голосов
/ 09 июля 2009

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

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

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

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

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

5 голосов
/ 16 апреля 2013

Я думаю, что ответ 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 голосов
/ 11 января 2017

Ниже приведены некоторые недостатки, из-за которых многие разработчики не используют 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 Cloning - конструктор копирования и клонирование

0 голосов
/ 13 сентября 2017

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

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

И clone() не создает «глубокую» копию вашего объекта из коробки, предполагая, что вы имеете в виду, что копируются не только ссылки (например, на Collection). Но читайте больше о глубоком и мелком здесь: Глубокая копия, мелкая копия, клон

0 голосов
/ 19 июля 2017

Интерфейс Cloneable не работает, в том смысле, что он бесполезен, но клон работает хорошо и может привести к повышению производительности для больших объектов - 8 полей и более, но тогда он не пройдёт анализ выхода. поэтому предпочтительнее использовать конструктор копирования большую часть времени. Использование клона в массиве быстрее, чем в Arrays.copyOf, поскольку длина гарантированно будет одинаковой.

подробнее здесь https://arnaudroger.github.io/blog/2017/07/17/deep-dive-clone-vs-copy.html

0 голосов
/ 02 ноября 2015

Обычно 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, и вы передаете ему все конечные элементы.

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

Когда вы возвращаетесь к методу clone (), вы устанавливаете все не финальные элементы.

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

0 голосов
/ 10 июля 2009

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

0 голосов
/ 10 июля 2009

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

Проблема в том, что вы не можете создавать глубокие копии циклических графов объектов, не имея возможности отслеживать, какие объекты были посещены. clone () не обеспечивает такое отслеживание (поскольку это должно быть параметром для .clone ()), и, следовательно, создает только полные копии.

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

...