Java中克隆与拷贝构造函数

参考A Guide to Object Cloning in Java
Which is better option: Cloning or Copy constructors?

Cloning 含义

当我们使用clone()方法时,JVM做如下两件事:
1. 如果这个类只包含原始数据类型成员,则创建一个对象的拷贝,原始数据成员被依次拷贝,返回指向新对象的Reference
2. 如果类还包括引用数据类型则只拷贝了它的引用,没有开辟新的空间。

Java中的Clone

java中的类要支持clone需要实现如下两条
1. 实现Cloneable接口
2. 重写clone()方法
Java docs about clone() method are given below (formatted and extract).

/*
Creates and returns a copy of this object. The precise meaning of "copy" may depend on the class of the object.
The general intent is that, for any object x, the expression:
1) x.clone() != x will be true
2) x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.
3) x.clone().equals(x) will be true, this is not an absolute requirement.
*/
protected native Object  [More ...] clone() throws CloneNotSupportedException;
  1. 第一条保证克隆对象和原对象在内存的不同地方(内存地址不一样)
  2. 第二条建议克隆对象和原始对象有相同的类类型,但不强制
  3. 第三条建议克隆对象和原始对象使用equals比较是形同,但也不强制

例子: string也会被拷贝是因为String类是不可变的,对String类的任何改变,都是返回一个新的String类对象
Our first class is Employee class with 3 attributes. Id, name and department.

public class Employee implements Cloneable{
    
    private int employeeId;
    private String employeeName;
    private Department department;
    
    public Employee(int employeeId, String employeeName, Department department) {
        this.employeeId = employeeId;
        this.employeeName = employeeName;
        this.department = department;
    }
    @Override
    public Object clone() throws CloneNotSupportedException
    {
        return super.clone();
    }
    public int getEmployeeId() {
        return employeeId;
    }
    public void setEmployeeId(int employeeId) {
        this.employeeId = employeeId;
    }
    public String getEmployeeName() {
        return employeeName;
    }
    public void setEmployeeName(String employeeName) {
        this.employeeName = employeeName;
    }
    public Department getDepartment() {
        return department;
    }
    
    public void setDepartment(Department department) {
        this.department = department;
    }
}
class Department
{
    private int id;
    private String name;
    
    public Department(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }   
}

测试类

public class TestCloning {
    
    public static void main(String[] args) throws CloneNotSupportedException {
        Department dept = new Department(1,"Human Resource");
        Employee original = new Employee(1,"Admin",dept);
        
        //clone original 
        Employee cloned = (Employee)original.clone();
        System.out.println(cloned.getEmployeeId());
        
        System.out.println(original != cloned);
        
        System.out.println(original.getClass()== cloned.getClass());
        System.out.println(original.equals(cloned));
    }
}

QQ截图20160424181932

把String改为StringBuilder就不能实现拷贝

现在我们修改Department会发现 original数据也会被修改

public class TestCloning {
    
    public static void main(String[] args) throws CloneNotSupportedException {
        Department dept = new Department(1,"Human Resource");
        Employee original = new Employee(1,"Admin",dept);
        
        //clone original 
        Employee cloned = (Employee)original.clone();
        
        cloned.getDepartment().setName("Software Development");
        System.out.println(original.getDepartment().getName());
    }
}

QQ截图20160424182455

Cloned object changes are visible in original also. This way cloned objects can make havoc in system if allowed to do so. Anybody can come and clone your application objects and do whatever he likes.

浅拷贝

是Java中默认实现方式,上面例子实现的就是浅拷贝,因为我们在Employee类的克隆方法中没有克隆 Department.

深拷贝

使在克隆对象上的改变不影响original

修改Employee的克隆方法

@Override
    public Object clone() throws CloneNotSupportedException
    {
        Employee cloned = (Employee)super.clone();
        cloned.setDepartment((Department)cloned.getDepartment().clone());
        return cloned;
    }

同时Department也要实现clone()方法

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

测试:

public class TestCloning {
    
    public static void main(String[] args) throws CloneNotSupportedException {
        Department dept = new Department(1,"Human Resource");
        Employee original = new Employee(1,"Admin",dept);
        
        //clone original 
        Employee cloned = (Employee)original.clone();
        
        cloned.getDepartment().setName("Software Development");
        System.out.println(original.getDepartment().getName());
        
        System.out.println(cloned.getDepartment().getName());
    }
}

QQ截图20160424183921

这时改变cloned的状态 不改变original的状态

实现深拷贝需要满足如下规则
1. No need to separately copy primitives.
2. All the member classes in original class should support cloning and in clone method of original class in context should call super.clone() on all member classes.
3. If any member class does not support cloning then in clone method, one must create a new instance of that member class and copy all its attributes one by one to new member class object. This new member class object will be set in cloned object.

使用拷贝构造函数

public class PointOne {
    
    private Integer x;
    private Integer y;
    public PointOne (PointOne point)
    {
        this.x = point.x;
        this.y = point.y;
    }
}

通过拷贝构造函数可以得到状态一致的对象

详见参考链接一

通过序列化克隆

只要对应的类是可序列化的可以通过序列化机制实现克隆,做法: 直接将对象序列化到输出流中,然后将其读回。这样产生的新对象是对现有对象的一个深拷贝。可用ByteArrayOutputStream将数据保存到字节数组中。
但是 它通常会比显示地构建新对象并复制或克隆数据域的克隆方法慢的多,并且并不是所有对象是可序列化的,最后使类可序列化是很困难的,并不是所有的类都可以指望得到它的权利。

 class Employee extends SerialCloneable
{
    private String name;
    private double salary;
    private Date hireDay;
    
    public Employee(String n,double s,int year,int month,int day)
    {
        name = n;
        salary  = s;
        GregorianCalendar calendar = new GregorianCalendar(year,month-1,day);
        hireDay = calendar.getTime();
    }
    public String getName()
    {
        return name;
    }
    public double getSalary()
    {
        return salary;
    }
    public Date getHireDay()
    {
        return hireDay;
    }
    public void raiseSalay(double byPercent)
    {
        double raise = salary*byPercent/100.0;
        salary += raise;
    }
    public String toString()
    {
        return getClass().getName()+
                "[name="+name
                +",salary="+salary
                +",hireDay="+hireDay
                +"]";
    }
}
 
 class SerialCloneable implements Cloneable,Serializable
{
    @Override
    public Object clone()
    {
        try{
        //save the object to a byte array
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(bout);
        out.writeObject(this);
        out.close();
        
        //read a clone of the object from the byte array
        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream in = new ObjectInputStream(bin);
        Object ret = in.readObject();
        in.close();
        return ret;
        }catch(Exception e)
        {
            e.printStackTrace();
            return null;
        }
    }
}
 
 public class SerialCloneTest {
    
    public static void main(String[] args) {
        Employee harry = new Employee("Harry Hacker",35000,1989,10,1);
        
        Employee harry2 = (Employee)harry.clone();
        harry2.raiseSalay(10);
        
        System.out.println(harry);
        System.out.println(harry2);
    }

}
 

2016-04-24_195601

使用Apache工具包(using Apache commons)

Apache commons has also utility function for deep cloning. If you feel interested the follow their official docs. Below is sample usage of cloning facility using Apache commons:

 SomeObject cloned = org.apache.commons.lang.SerializationUtils.clone(someObject);
 

Best practices

  • When you don’t know whether you can call the clone() method of a particular class as you are not sure if it is implemented in that class, you can check with checking if the class is instance of “Cloneable” interface as below.
if(obj1 instanceof Cloneable){
    obj2 = obj1.clone();
} 
//Dont do this. Cloneabe dont have any methods
obj2 = (Cloneable)obj1.clone();
  • No constructor is called on the object being cloned. As a result, it is your responsibility, to make sure all the members have been properly set. Also, if you are keeping track of number of objects in system by counting the invocation of constructors, you got a new additional place to increment the counter.

发表评论

电子邮件地址不会被公开。 必填项已用*标注