实现函数克隆
这就是我开始撰写本文的方式。 我已经读过很多次这样的声明: “当对象引用可变的最终字段时,克隆变得很困难。” 每次我在Google上搜索它时,都要了解它的确切含义,并且在此过程中也忘了它。 因此以为我会将其写在博客上,以便作为我的直接参考。
克隆对象(我可以从我的研究生课程的OOP课程中回忆到)正在创建对象的类似副本,该副本基本上应符合以下规则:
- x.clone()!= x
- x.clone()。getClass()== x.getClass()
- x.clone()。equals(x)
注意,在所有情况下都必须始终满足条件(1)。 尽管条件(2)和(3)并不是绝对要求,但最好以这样的方式设计克隆方法,使其保持良好状态。 在继续讨论之前,这是Object类中clone方法的方法签名:
protected native Object clone() throws CloneNotSupportedException;
因此,正如您注意到protected修饰符一样,我们不可能直接在任何对象上调用clone()方法。 我们必须重写此方法作为公共方法,并在我们的类中提供对其的实现才能访问它。 如果不需要特定的实现,我们可以返回super.clone()。 由于在Java 5之后可以进行协变返回,因此我们可以修改clone的返回值以返回类的对象。 因此,如果我们正在编写员工类,则这是clone()方法的方式:
@Override
public Employee clone() throws CloneNotSupportedException {return (Employee) super.clone();
}
但是请注意,Object类中的clone方法检查我们的类是否实现Cloneable接口。 如果未实现,则抛出CloneNotSupportedException。 否则,它将创建一个新副本。 但是请注意,克隆方法从不调用构造函数来创建对象的副本。 因此,如果您想通过增加构造函数内部的静态计数器来跟踪为类创建的实例数量,则此方法将不会起作用,因为永远不会调用构造函数。 相反,clone方法从对象内存中逐字段复制实例属性,然后将其返回给调用方。 因此,如果类必须提供一个克隆其选项而不导致CloneNotSupportedException的类,则必须实现一个标记接口Cloneable。 但是请注意,调用clone()的代码应处理此异常。 否则会导致编译器错误。 是的,这是一个痛点,对此受到批评。
现在让我们举一个例子: 案例(1) :
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;}@Overridepublic 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: com.pramati.test.Employee@19821f
Employee{name:=Ram, id:=1}
2: com.pramati.test.Employee@de6ced
Employee{name:=Ram, id:=1}
从上面的示例可以看出,clone()方法创建了一个新Employee,其值是从现有对象中复制的。 这很简单,并且可以正常工作,因为Employee类中没有对象引用。 让我们这样修改类: 案例(2):
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;}@Overridepublic 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: 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}
这可以。 现在说,我们修改了我们的主要方法,如下: 案例(3):
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();
}
现在您认为employee1的薪水是多少? 随着我们增加了克隆员工的薪水,我们自然希望为他增加薪水。 但是这里出乎意料的转折是,employee1的薪水也增加了。 这是输出或此:
Employee{name:=Ram, id:=1, package:=750000.0}
Employee{name:=Krish, id:=2, package:=750000.0}
请注意,当我们克隆对象时,不会调用构造函数。 宁愿对原始对象的地址位置中存在的所有成员变量进行逐域复制。 现在,当有对象引用时,该引用将被复制,而不是原始对象。 因此,原始对象和克隆对象都指向同一成员对象。 因此,对一个对象所做的更改将自动对另一对象可见。 那么如何解决这个问题呢?
最简单的解决方案是也为PayPackDetails实现克隆方法,并从Employee的克隆方法中调用它。 情况(4):
@Override
public Employee clone() throws CloneNotSupportedException {Employee employee = (Employee)super.clone();employee.packDetails = packDetails.clone();return employee;
}
现在运行main()方法,它将按预期给出正确的结果:
Employee{name:=Ram, id:=1, package:=550000.0}
Employee{name:=Krish, id:=2, package:=750000.0}
但是,如果PayPackDetails由其他对象引用组成,则我们也必须重写该对象的克隆方法,并在PayPackDetails内部调用其克隆方法。 同样,当我们在PayPackDetails中组成新对象时,除了在新组成的对象中实现clone()方法外,我们还必须在PayPackDetails中修改clone方法。 组合对象类还应该实现Cloneable接口。 与往常一样,我们还必须处理CloneNotSupportedException。
现在考虑将PayPackDetails声明为final的另一种情况,这会使情况更加糟糕: 情况(5):
public class Employee implements Cloneable {private String name;private String identifier;private final PayPackDetails packDetails;// -- Rest of the methods
}
由于该字段被声明为final,因此我们无法在clone方法中为其分配新值,因为该字段被声明为final。 那么如何处理呢? 解决方案如下:使用复制构造函数并从克隆中返回新实例。
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();}@Overridepublic 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):
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}
到目前为止,这是最好的情况,这是该程序的输出:
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):
public class AdvancedPayPackDetails extends PayPackDetails {private double flexiblePayPercent = 10d;public AdvancedPayPackDetails(AdvancedPayPackDetails details) {super(details);flexiblePayPercent = details.getFlexiblePayPercentage();}@Overridepublic 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方法,它将为我们提供输出:
Employee{name:=Ram, id:=1, package:=600000.0}
Employee{name:=Ram, id:=1, package:=550000.0}
原因很明显。 Employee的副本构造函数不知道创建的这个新类(AdvancedPayPackDetails)。 实际上,我们可以修改Employee构造函数以包括对PayPackDetails的instanceOf检查,但这不是正确的方法。 相反,最好回到先前的解决方案,即在使用final字段的情况下使用copy构造函数,并对具有继承层次结构的类使用clone方法(案例(5)的解决方案)。
结束语:正如我们在本文中一直看到的那样,以正确的方式实现克隆方法非常复杂。 因此最好尽量远离克隆。 只要复制的对象没有任何继承层次结构,最好使用复制构造函数。
翻译自: https://www.javacodegeeks.com/2014/01/which-is-better-option-cloning-or-copy-constructors.html
实现函数克隆