Рубрики
Без рубрики

Методы объектов Java: clone()

Автор оригинала: Adam McQuistan.

Вступление

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

  • Струна
  • в класс
  • равняется
  • Хэш-код
  • клон (вы здесь)
  • завершать
  • ждать и уведомлять

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

Почему существует необходимость клонировать() объект

Сначала я хотел бы начать с того, почему в первую очередь может потребоваться создать клон или копию объекта. Я снова буду использовать свой класс Person из предыдущих статей этой серии для демонстраций, особенно важно то, что это изменяемая его версия, иначе копирование было бы спорным вопросом.

Код показан ниже:

import java.time.LocalDate;

public class Person {
    private String firstName;
    private String lastName;
    private LocalDate dob;

    public Person(String firstName, String lastName, LocalDate dob) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
    }

    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }

    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }


    public LocalDate getDob() { return dob; }
    public void setDob(LocalDate dob) { this.dob = dob; }

    @Override
    public String toString() {
        return "";
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((dob == null) ? 0 : dob.hashCode());
        result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
        result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Person)) {
            return false;
        }
        Person p = (Person)o;
        return firstName.equals(p.firstName)
                && lastName.equals(p.lastName)
                && dob.equals(p.dob);
    }
}

Я начинаю свое обсуждение с создания пары целочисленных переменных x и y вместе с экземпляром Person и назначаю его переменной с именем me . Затем я назначаю me другой переменной с именем me2 , которую затем изменяю в поле Имя в me2 и показываю содержимое обеих переменных, например:

import java.time.LocalDate;

public class Main {
    public static void main(String[] args) {
        int x = 10;
        int y = x;
        y = 20;
        System.out.println("x = " + x);
        System.out.println("y = " + y);

        Person me = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));
        Person me2 = me;
        me2.setFirstName("Joe");
        System.out.println("me = " + me);
        System.out.println("me2 = " + me2);
    }
}

Выход:

x = 10
y = 20
me = 
me2 = 

Теперь есть хороший шанс, что многие из вас уловили это маленькое ” ой ” … но, чтобы все были на одном уровне понимания, позвольте мне объяснить, что там только что произошло. В Java у вас есть две основные категории типов данных: типы значений (они же примитивы) и ссылочные типы (они же объекты). В моем примере выше объекты Person, такие как me и me2 , относятся к ссылочному типу объекта Person. В отличие от типов ссылок на человека x и y являются типами значений примитивов int.

Как только что стало очевидно, присвоение ссылочных типов обрабатывается иначе, чем, скажем, целое число или, возможно, более точно указано int на языке Java. Когда вы назначаете ссылочную переменную другой ссылочной переменной, вы просто указываете ей местоположение, в котором на этот объект можно ссылаться в памяти, что сильно отличается от фактического копирования содержимого, которое происходит, когда вы делаете то же самое с типами значений.

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

Как клонировать() объект

Как я упоминал ранее, метод clone() класса объектов вызывает некоторые разногласия в сообществе разработчиков Java. Причины этого в том, что для реализации метода clone() вам необходимо реализовать причудливый интерфейс, называемый Cloneable из пакета “java.lang”, который предоставляет вашему классу возможность предоставлять общедоступный clone() метод. Это необходимо, поскольку метод clone() в классе объектов защищен и, следовательно, недоступен из клиентского кода, работающего с вашим классом. Кроме того, поведение при создании объекта довольно необычно в том смысле, что экземпляр создается без вызова желанного оператора new , что вызывает у многих, включая меня, некоторое беспокойство.

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

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

Для простого класса, такого как Person, который не содержит никаких изменяемых полей, все, что требуется для создания клона, – это вернуть вызов метода клонирования объекта базового класса, например:

public class Person implements Cloneable {
    private String firstName;
    private String lastName;
    private LocalDate dob;

    public Person(String firstName, String lastName, LocalDate dob) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
    }

    // omitting other sections for brevity

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

В этом примере создание клона человека довольно просто и выполняется примерно так:

public class Main {
    public static void main(String[] args) {
        Person me = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));
        Person me2 = null;
        try {
            me2 = (Person) me.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        me2.setFirstName("Joe");
        System.out.println("me = " + me);
        System.out.println("me2 = " + me2);
    }
}

Выход:

me = 
me2 = 

Вуаля, я клон сделан. Теперь, когда я обновляю Имя свойство me2 , используя предыдущий пример, поле в объекте me остается неизменным. Обязательно обратите внимание на явное приведение возвращенного клона объекта типа к типу Person, которое необходимо, поскольку интерфейс требует возврата ссылки на объект типа.

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

Чтобы продемонстрировать это, мне нужно обновить свой Человек класс вот так.

public class Person implements Cloneable {
    private String firstName;
    private String lastName;
    private LocalDate dob;
    private Person mother;
    private Person[] family;

    public Person(String firstName, String lastName, LocalDate dob) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
    }

    // omitting other methods for brevity

    public Person getMother() { return mother; }
    public void setMother(Person mother) { this.mother = mother; }

    public Person[] getFamily() { return family; }
    public void setFamily(Person[] family) { this.family = family; }

    @Override
    public Object clone() throws CloneNotSupportedException {
        Person personClone = (Person) super.clone();
        Person motherClone = (Person) mother.clone();
        Person[] familyClone = family.clone();
        personClone.setMother(motherClone);
        personClone.setFamily(familyClone);
        return personClone;
    }
}

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

Если бы я специально не потратил время на индивидуальное создание клонов этих изменяемых полей, то два результирующих объекта Person ссылались бы на одни и те же мать и семья изменяемые экземпляры объектов, что было бы ужасным беспорядком для отладки в будущем. Это явное копирование изменяемых элементов объекта из поля в поле называется глубоким копированием.

Альтернативные методы создания копий экземпляров

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

Для начала я расскажу о методе конструктора копирования. Этот способ создания копий объектов с помощью конструктора основан на подписи, которая содержит только один параметр собственного типа, представляющий копируемый объект, такой как публичное лицо(Лицо p) .

Git Essentials

Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!

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

Вот пример использования конструктора копирования для класса Person :

public class Person implements Cloneable {
    private String firstName;
    private String lastName;
    private LocalDate dob;
    private Person mother;
    private Person[] family;

    public Person(String firstName, String lastName, LocalDate dob) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
    }

    public Person(Person p) {
        this.firstName = new String(p.firstName);
        this.lastName = new String(p.lastName);
        this.dob = LocalDate.of(p.dob.getYear(),
                p.dob.getMonth(),
                p.dob.getDayOfMonth());
        if (p.mother != null) {
            this.mother = new Person(p.mother);
        }
        if (p.family != null) {
            this.family = new Person[p.family.length];
            for (int i = 0; i < p.family.length; i++) {
                if (p.family[i] != null) {
                    this.family[i] = new Person(p.family[i]);
                }
            }
        }
    }

    // omitting other methods for brevity

}

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

public class Person implements Cloneable {
    private String firstName;
    private String lastName;
    private LocalDate dob;
    private Person mother;
    private Person[] family;

    public Person(String firstName, String lastName, LocalDate dob) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
    }

    public static Person makeCopy(Person p) {
        Person copy = new Person(new String(p.firstName),
                new String(p.lastName),
                LocalDate.of(p.dob.getYear(), p.dob.getMonth(), p.dob.getDayOfMonth()));
        if (p.mother != null) {
            copy.mother = Person.makeCopy(p.mother);
        }
        if (p.family != null) {
            copy.family = new Person[p.family.length];
            for (int i = 0; i < p.family.length; i++) {
                if (p.family[i] != null) {
                    copy.family[i] = Person.makeCopy(p.family[i]);
                }
            }
        }
        return copy;
    }

    // omitting other methods for brevity

}

Сравнение различий в реализации

Создание копий объекта Java с помощью реализации Cloneable и переопределения clone() по праву получило плохую репутацию. Это связано с тем, что интерфейс witch изменяет видимость самого метода clone () , а также с часто недооцениваемой необходимостью “глубокого” клонирования изменяемых ссылочных типизированных полей класса. По этим причинам я предпочитаю использовать конструкторы копирования и фабричные методы для создания копий объектов. Только когда я работаю с классом, который специально реализовал интерфейс Cloneable , я продолжу использовать метод clone () .

Вывод

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

Как всегда, спасибо за чтение и не стесняйтесь комментировать или критиковать ниже.