суббота, 4 февраля 2012 г.

Клонирование объектов. Интерфейс Cloneable.


Иногда необходимо получить копию объекта, которая не зависела бы от оригинала. С которой можно было бы производить манипуляции, при этом, не изменяя оригинал. При обыкновенном присваивание объектов (obj1 = obj2;) передаются ссылки на объект. В итоге два экземпляра ссылаются на один объект, и изменение одного приведет к изменению другого. Как мы видим это не то, что нам нужно. И в данном случае, нам на помощь придет интерфейс Cloneable и метод clone() класса Object.

И так, если нам необходимо получить независимый клон объекта, то необходимо вызвать метод clone(). Данный метод объявлен, как protected, а это значит, что метод защищен, и может быть доступен только при наследовании объекта. Как выясняется, это не является проблемой, потому как любой класс, является потомком класса Object. Однако при защищенном методе класс может клонировать только свои собственные объекты. Чтобы клонировать другие объекты, метод clone() необходимо расширить до public.
Пример расширения метода clone().

       public User clone() throws CloneNotSupportedException {
             return (User)super.clone();
       }

Как мы можем видеть метод clone() может выбрасывать исключение CloneNotSupportedException. Данное исключение возникает в случае, когда клонируемый класс не имеет реализации интерфейса Cloneable. Интерфейс Cloneable не реализует ни одного метода. Он является всего лишь маркером, говорящим, что данный класс реализует клонирование объекта.
Само клонирование осуществляется вызовом родительского метода clone(). Данный вид клонирования называется поверхностным клонированием. Его можно использовать только в том случае, если у клонируемого класса объявлены  неизменяемые типы объекты.
Пример реализации клонирования объекта.

package my.cloneable;

class User implements Cloneable {
       private String name;
       private int age;
      
       public String getName() {
             return name;
       }
       public void setName(String name) {
             this.name = name;
       }
       public int getAge() {
             return age;
       }
       public void setAge(int age) {
             this.age = age;
       }
      
       public User clone() throws CloneNotSupportedException {
             return (User)super.clone();
       }
}

public class App {

       public static void main(String[] args) {
            
             User user = new User();
            
             user.setName("Иванов");
             user.setAge(25);
            
             System.out.println("Данные до клонирования: " +
user.getName() + " - " + user.getAge() + "лет");
            
             User clone;
             try {
                    clone = user.clone();
                    clone.setName("Петров");
                    clone.setAge(30);
                    System.out.println("Клон после изменения данные: " +
                           clone.getName() + " - " + clone.getAge() + "лет");
                   
             } catch (CloneNotSupportedException e) {
                    System.out.println("Объект не может быть клонированным.");
             }
            
            
             System.out.println("Оригинал, после манипуляций с клоном: " +
 user.getName() + " - " + user.getAge() + "лет");
       }
} /* результат:
Данные до клонирования: Иванов - 25лет
Клон после изменения данные: Петров - 30лет
Оригинал, после манипуляций с клоном: Иванов - 25лет
 */

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

package my.cloneable;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

class User implements Cloneable {
       private String name;
       private int age;
       private GregorianCalendar birthday;
      
       public String getName() {
             return name;
       }
       public void setName(String name) {
             this.name = name;
       }
       public int getAge() {
             return age;
       }
       public void setAge(int age) {
             this.age = age;
       }
      
       public String getBirthday() {
             return birthday.get(Calendar.DAY_OF_MONTH) + "." +
                           birthday.get(Calendar.MONTH) + "." +
                           birthday.get(Calendar.YEAR);
       }
      
       public void setBirthday(int day, int month, int year) {
             birthday = new GregorianCalendar();
             birthday.set(Calendar.DAY_OF_MONTH, day);
             birthday.set(Calendar.MONTH, month);
             birthday.set(Calendar.YEAR, year);
       }
      
       public User clone() throws CloneNotSupportedException {
             User clone = (User)super.clone();
             clone.birthday = (GregorianCalendar) birthday.clone();
             return clone;
       }
}

public class App {

       public static void main(String[] args) {
            
             User user = new User();
             user.setName("Иванов");
             user.setAge(25);
             user.setBirthday(12, 03, 1975);

             System.out.println("Данные до клонирования: " + user.getName() + " - " + user.getAge() + "лет, день рождение: " + user.getBirthday());
            
             User clone;
             try {
                    clone = user.clone();
                    clone.setName("Петров");
                    clone.setAge(30);
                    clone.setBirthday(15, 11, 1992);

                    System.out.println("Клон после изменения данные: " + clone.getName() + " - " + clone.getAge() + "лет, день рождение: " + clone.getBirthday());
                   
             } catch (CloneNotSupportedException e) {
                    System.out.println("Объект не может быть клонированным.");
             }
            
            
             System.out.println("Оригинал, после манипуляций с клоном: " + user.getName() + " - " + user.getAge() + "лет, день рождение: " + user.getBirthday());
       }
}
 /* результат:
Данные до клонирования: Иванов - 25лет, день рождение: 12.3.1975
Клон после изменения данные: Петров - 30лет, день рождение: 15.11.1992
Оригинал, после манипуляций с клоном: Иванов - 25лет, день рождение: 12.3.1975
 */

В данном примере, в классе User, добавлена изменяемая переменная birthday, в которой хранится дата рождения.
Чтобы она нормально клонировалась необходимо переопределить метод clone() следующим образом :

       public User clone() throws CloneNotSupportedException {
             User clone = (User)super.clone();
             clone.birthday = (GregorianCalendar) birthday.clone();
             return clone;
       }

Как вы видите, добавилась одна строчка

             clone.birthday = (GregorianCalendar) birthday.clone();

в которой напрямую клонируется поле birthday.


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

Желаю хорошего клонирования ;).

11 комментариев:

  1. Илья=НесущийДобро=10 декабря 2015 г. в 12:17

    Нет нужды делать:
    clone.birthday = (GregorianCalendar) birthday.clone();

    достаточно реализовать интерфейс Cloneable во всех классах ссылки на которые используются в изначальном классе

    Из доков:
    A "deep" copy,
    * in contrast, would also recursively clone nested objects. A subclass that
    * needs to implement this kind of cloning should call {@code super.clone()}
    * to create the new instance and then create deep copies of the nested,
    * mutable objects.

    ОтветитьУдалить
    Ответы
    1. Если не делать:
      clone.birthday = (GregorianCalendar) birthday.clone();
      то у клона вместо нового объекта типа GregorianCalendar будет ссылка на уже существующий объект, поскольку тип GregorianCalendar является mutable.

      Удалить
  2. Также полное клонирование можно сделать если сериализовать и десериализовать объект.
    Но это скорее хак.

    ОтветитьУдалить
  3. Какие объекты могут быть клонированы?

    ОтветитьУдалить
  4. любые, класс которых реализует интерфейс Cloneable

    ОтветитьУдалить