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

Введение Эта статья является продолжением серии статей, описывающих часто забываемые методы базового класса Object языка Java. Ниже приведены методы базового объекта Java, которые присутствуют во всех объектах Java из-за неявного наследования объекта. * toString [/ javas-object-methods-tostring /] * toClass [/ javas-object-methods-getclass /] * равно [/ javas-object-methods-equals-object /] * hashCode [/ javas-object-methods -hashcode /] * clone (вы здесь)

Вступление

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

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

Почему существует необходимость 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 "<Person: firstName=" + firstName + ", lastName=" + lastName + ", dob=" + dob + ">"; 
 } 
 
 @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 после чего меняю поле firstName 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 = <Person: firstName=Joe, lastName=McQuistan, dob=1987-09-23> 
 me2 = <Person: firstName=Joe, lastName=McQuistan, dob=1987-09-23> 

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

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

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

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

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

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

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

Для простого класса, такого как 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(); 
 } 
 } 

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

 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 = <Person: firstName=Adam, lastName=McQuistan, dob=1987-09-23> 
 me2 = <Person: firstName=Joe, lastName=McQuistan, dob=1987-09-23> 

Создан клон voilà a me Теперь, когда я обновляю свойство firstName me2 используя предыдущий пример, поле в объекте me Обязательно обратите внимание на явное приведение возвращаемого клона типа Object к типу Person, что необходимо, поскольку интерфейс требует возврата ссылки типа Object.

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

Чтобы продемонстрировать это, мне нужно обновить мой 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; 
 } 
 
 // 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; 
 } 
 } 

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

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

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

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

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

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

Вот пример использования конструктора копирования для класса 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() праву заслужило небольшую плохую репутацию. Это связано с тем, что интерфейс изменяет видимость самого clone() вместе с часто недооцененной необходимостью «глубокого» клонирования изменяемых полей ссылочного типа. По этим причинам я предпочитаю использовать конструкторы копирования и фабричные методы для создания копий объектов. Только когда я работаю с классом, в котором специально реализован Cloneable , я буду использовать метод clone() .

Заключение

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

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

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus