Статьи

Какой вариант лучше: клонирование или копирование конструкторов?

Вот как я начал писать эту статью. Я читал это утверждение много раз: «Клонирование становится трудным, когда у объекта есть ссылки на изменяемые конечные поля». И каждый раз, когда я гуглю об этом, понимаю, что именно это означает, и как часть процесса забываю об этом тоже. Поэтому я подумал, что я буду вести блог, чтобы это послужило моей непосредственной ссылкой.

Клонирование объекта, которое я мог вспомнить из моего курса ООП в аспирантуре, — это создание аналогичной копии объекта, которая в основном должна соответствовать следующим правилам:

  1. x.clone ()! = x
  2. x.clone (). getClass () == x.getClass ()
  3. x.clone (). равно (х)

Обратите внимание, что условие (1) всегда должно выполняться во всех случаях. Хотя условия (2) и (3) не являются абсолютными требованиями, хорошо спроектировать метод клонирования таким образом, чтобы он оставался в силе. Прежде чем продолжить обсуждение, вот сигнатура метода клона в классе Object:

1
protected native Object clone() throws CloneNotSupportedException;

Итак, как вы заметили, защищенный модификатор, мы не можем вызвать метод clone () непосредственно для любого объекта. Мы должны переопределить этот метод как открытый метод и обеспечить реализацию для него в нашем классе, чтобы получить к нему доступ. Если никакой конкретной реализации не требуется, мы можем просто вернуть super.clone (). Поскольку ковариантные возвраты возможны после Java 5, мы можем изменить возвращаемое значение clone, чтобы вернуть объект нашего класса. Итак, если мы пишем наш класс работника, вот как метод clone () хотел бы:

1
2
3
4
@Override
public Employee clone() throws CloneNotSupportedException {
    return (Employee) super.clone();
}

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

Давайте рассмотрим пример: Case (1) :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Employee implements Cloneable{
    private String name;
    private String identifier;
 
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getIdentifier() {
        return identifier;
    }
    public void setIdentifier(String identifier) {
        this.identifier = identifier;
    }
 
    @Override
    public Employee clone() throws CloneNotSupportedException {
        return (Employee)super.clone();
    }
 
    public void print() {
        System.out.println(Objects.toStringHelper(this).add("name:", name).add("id:", identifier).toString());
    }
 
    public static void main(String[] args) throws CloneNotSupportedException {
        Employee employee1 = new Employee();
        employee1.setName("Ram");
        employee1.setIdentifier("1");
        System.out.println("1: "+employee1);
        employee1.print();
 
        Employee employee2 = employee1.clone();
        System.out.println("2: "+employee2);
        employee2.print();
    }
}

Вот результат этого:

1
2
3
4
1: com.pramati.test.Employee@19821f
Employee{name:=Ram, id:=1}
2: com.pramati.test.Employee@de6ced
Employee{name:=Ram, id:=1}

Как видно из приведенного выше примера, метод clone () создал нового сотрудника со значениями, скопированными из существующего объекта. Это довольно просто и работает нормально, так как в классе Employee нет ссылок на объекты. Давайте изменим наш класс следующим образом: Case (2):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class PayPackDetails{
    private double basicSalary = 500000d;
    private double incentive = 50000d;
 
    public double getSalary() {
        return getBasicSalary()+getIncentive();
    }
 
    public double getBasicSalary() {
        return basicSalary;
    }
 
    public double getIncentive() {
        return incentive;
    }
 
    public void setBasicSalary(double basicSalary) {
        this.basicSalary = basicSalary;
    }
 
    public void setIncentive(double incentive) {
        this.incentive = incentive;
    }
}
 
public class Employee implements Cloneable {
 
    private String name;
    private String identifier;
    private PayPackDetails packDetails;
 
    public Employee(String name, String identifier, PayPackDetails packDetails) {
        this.name = name;
        this.identifier = identifier;
        this.packDetails = packDetails;
    }
 
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getIdentifier() {
        return identifier;
    }
    public void setIdentifier(String identifier) {
        this.identifier = identifier;
    }
    public PayPackDetails getPackDetails() {
        return packDetails;
    }
 
    @Override
    public Employee clone() throws CloneNotSupportedException {
        return (Employee)super.clone();
    }
 
    public void print() {
        System.out.println(Objects.toStringHelper(this).add("name:", name).add("id:", identifier).add("package:", packDetails.getSalary()).toString());
    }
 
    public static void main(String[] args) throws CloneNotSupportedException {
        Employee employee1 = new Employee("Ram","1",new PayPackDetails());
        System.out.println("1: "+employee1);
        employee1.print();
 
        Employee employee2 = employee1.clone();
        System.out.println("2: "+employee2);
        employee2.print();
    }
}

При запуске метода main мы получим следующие результаты:

1
2
3
4
1: com.pramati.clone.Employee@addbf1
Employee{name:=Ram, id:=1, package:=550000.0}
2: com.pramati.clone.Employee@de6ced
Employee{name:=Ram, id:=1, package:=550000.0}

Это хорошо. Допустим, мы изменили наш метод main следующим образом: Case (3):

1
2
3
4
5
6
7
8
public static void main(String[] args) throws CloneNotSupportedException {
    Employee employee1 = new Employee("Ram","1",new PayPackDetails());
    Employee employee2 = employee1.clone();
    employee2.setName("Krish"); employee2.setIdentifier("2");
    employee2.getPackDetails().setBasicSalary(700000d);
    employee1.print();
    employee2.print();
}

Как вы думаете, какой будет зарплата сотрудника1? Поскольку мы увеличили зарплату клонированного сотрудника, мы, естественно, ожидаем, что зарплата будет увеличена для него. Но неожиданным поворотом здесь является повышение зарплаты сотрудника1. Вот вывод или это:

1
2
Employee{name:=Ram, id:=1, package:=750000.0}
Employee{name:=Krish, id:=2, package:=750000.0}

Обратите внимание, что когда мы клонируем объект, конструктор не вызывается. Скорее было бы сделать полевую копию всех переменных-членов, присутствующих в расположении адреса исходного объекта. И теперь, когда есть ссылки на объекты, ссылка копируется, но не исходный объект. Следовательно, как исходные, так и клонированные объекты указывают на один и тот же объект-член. Таким образом, изменения, сделанные в одном объекте, будут автоматически видны другому. Так как решить эту проблему?

Самое простое решение — реализовать метод клонирования для PayPackDetails и вызвать его из метода клонирования Сотрудника. Дело (4):

1
2
3
4
5
6
@Override
public Employee clone() throws CloneNotSupportedException {
    Employee employee = (Employee)super.clone();
    employee.packDetails = packDetails.clone();
    return employee;
}

Теперь при запуске метода main () он даст правильные результаты, как и ожидалось:

1
2
Employee{name:=Ram, id:=1, package:=550000.0}
Employee{name:=Krish, id:=2, package:=750000.0}

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

Теперь рассмотрим другой случай, когда PayPackDetails объявлен финальным, это еще больше ухудшит ситуацию: Case (5):

1
2
3
4
5
6
public class Employee implements Cloneable {
    private String name;
    private String identifier;
    private final PayPackDetails packDetails;
    // -- Rest of the methods
}

Поскольку поле объявлено как final, мы не можем присвоить ему новое значение в методе clone, так как оно объявлено как final. Так как с этим бороться? Вот решение: используйте конструктор копирования и верните новый экземпляр из клона.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Employee implements Cloneable {
 
    private String name;
    private String identifier;
    private final PayPackDetails packDetails;
 
    public Employee(String name, String identifier, PayPackDetails packDetails) {
        this.name = name;
        this.identifier = identifier;
        this.packDetails = packDetails;
    }
 
    protected Employee(Employee emp) throws CloneNotSupportedException{
        name = emp.name;
        identifier = emp.identifier;
        packDetails = emp.packDetails.clone();
    }
 
    @Override
    public Employee clone() throws CloneNotSupportedException {
        return new Employee(this);
    }
 
    public void print() {
        System.out.println(Objects.toStringHelper(this).add("name:", name).add("id:", identifier).add("package:", packDetails.getSalary()).toString());
    }
}

Обратите внимание, что модификатор доступа к конструктору копирования защищен. Теперь возникает вопрос: почему мы не можем использовать конструктор копирования для PayPackDetails вместо метода клонирования? И ответ: да, мы можем использовать это. Дело (6):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class PayPackDetails {
 
    private double basicSalary = 500000d;
    private double incentive = 50000d;
 
    public PayPackDetails(PayPackDetails details){
        basicSalary = details.getBasicSalary();
        incentive = details.getIncentive();
    }
 
    public static void main(String[] args) {
        Employee employee1 = new Employee("Ram","1",new PayPackDetails());
        employee1.print();
        Employee employee2 = new Employee(employee1);
        employee2.print();
    }
}
public class Employee {
 
    private String name;
    private String identifier;
    private final PayPackDetails packDetails;
 
    protected Employee(Employee emp) {
        name = emp.name;
        identifier = emp.identifier;
        packDetails = new PayPackDetails(emp.packDetails);
    }
 
    // .. Other methods
 
}

Пока это лучший случай, и вот результат для этой программы:

1
2
Employee{name:=Ram, id:=1, package:=550000.0}
Employee{name:=Ram, id:=1, package:=550000.0}

На самом деле это лучший способ сделать это, поскольку он решает многие проблемы с ошибочным методом клонирования:

1. Ни один из классов не должен реализовывать интерфейс маркера Cloneable
2. Поскольку клон не нужен, нет необходимости ловить CloneNotSupportedException
3. Поскольку клонирование не требуется, нет необходимости типизировать объект при вызове super.clone ().

Но тут возникает проблема: допустим, у вас есть подкласс для PayPackDetails. Дело (7):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class AdvancedPayPackDetails extends PayPackDetails {
    private double flexiblePayPercent = 10d;
 
    public AdvancedPayPackDetails(AdvancedPayPackDetails details) {
        super(details);
        flexiblePayPercent = details.getFlexiblePayPercentage();
    }
 
    @Override
    public double getSalary() {
        return super.getSalary()+(getBasicSalary()*getFlexiblePayPercentage()/100);
    }
 
    public double getFlexiblePayPercentage() {
        return flexiblePayPercent;
    }
 
    public void setFlexiblePayPercent(double flexiblePayPercent) {
        this.flexiblePayPercent = flexiblePayPercent;
    }
 
    public static void main(String[] args) throws CloneNotSupportedException {
        Employee employee1 = new Employee("Ram","1",new AdvancedPayPackDetails());
        employee1.print();
        Employee employee2 = employee1.clone();
        employee2.print();
    }
 
}

А теперь, запустив метод main, он выдаст нам вывод:

1
2
Employee{name:=Ram, id:=1, package:=600000.0}
Employee{name:=Ram, id:=1, package:=550000.0}

И причина очевидна. Конструктор копирования Employee не знал об этом созданном новом классе (AdvancedPayPackDetails). На самом деле мы можем изменить конструктор Employee, включив в него проверки instanceOf для PayPackDetails, но это неправильный способ действий. Скорее лучше, если мы вернемся к нашему более раннему решению, в котором мы использовали конструктор копирования в случае конечных полей и использовали метод клонирования для классов, которые имеют иерархию наследования (решение case (5)).

Заключение. Как мы уже видели в этой статье, правильная реализация метода клонирования очень сложна. Так что лучше держаться подальше от клонов, насколько это возможно. Лучше использовать конструкторы копирования, если у составных объектов нет иерархии наследования.