为啥阿里java规范中说到慎用Object的clone方法来拷贝对象?

阿里java规范OOP规约第20条

我们来研讨下为啥这么说,如果使用需要注意什么。

说到对象拷贝,我们得先弄清楚浅拷贝和深拷贝两个概念。根据拷贝的深度和广度,对象拷贝可以大致分为浅拷贝(Shallow Copy)和深拷贝(Deep Copy)两种类型。

什么是浅拷贝(Shallow Copy)

        浅拷贝是指创建一个新对象,这个新对象的属性和原始对象的属性值相同(对于非基本数据类型的属性,它们指向的是同一个对象)。换句话说,如果原始对象中的属性是引用类型(比如对象、数组等),浅拷贝后的对象中的对应属性将会引用原始对象中的相同对象。因此,对浅拷贝后的对象中引用类型属性的修改会反映到原始对象上。

在Java中,实现浅拷贝的常用方式包括:

  1. 使用Object类的clone()方法(注意:该方法受保护,因此你的类需要实现Cloneable接口,并重新clone()方法)。
  2. 通过构造函数、拷贝构造函数或者工厂方法来创建一个新的对象,并显式地将原始对象的属性值赋给新对象的属性(对于引用类型的属性,这仍然只是复制了引用)。

什么是深拷贝(Deep Copy)

        深拷贝是指创建一个新对象,这个新对象的属性和原始对象的属性值相同,但不同的是,对于非基本数据类型的属性,深拷贝会创建一个新的对象,并将原始对象中对应属性的内容复制到这个新对象中。因此,对深拷贝后的对象中引用类型属性的修改不会反映到原始对象上。

在Java中,实现深拷贝通常需要:

  1. 自定义方法来递归地复制对象中的所有属性,包括引用类型的属性。这需要编写大量的代码,并且需要了解对象图中的所有类和它们之间的关系。
  2. 使用序列化:将对象序列化到字节流中,然后反序列化这个字节流,从而得到一个新的对象实例。这种方法简单易行,但可能不是最高效的,并且要求对象及其所有属性都实现了Serializable接口。

简单说其实区别就在于对象中的属性是引用类对象是,比如List,Array,其他对象时,浅拷贝是拷贝的对象引用,深拷贝是拷贝的具体对象内容。不理解的话,下面会通过代码演示给大家看。

 Object类的clone()方法说明

        对象的clone()方法提供了一种创建并返回对象的一个副本的方式。默认情况下,当我们在一个类中调用Object类的clone()方法时,执行的是浅拷贝(Shallow Copy)。浅拷贝意味着它会创建一个新对象,并复制当前对象的非静态字段到该新对象,但如果字段是对象类型,则只复制它们的引用,而不复制引用的对象本身。这意呀着,原始对象和新对象将引用同一个对象作为其某些字段的值。

说了这么多,如果还是不理解,直接上代码。

浅拷贝

clone()方法代码演示

现在有一个Person对象,对象中有一个Address对象的属性,他们都实现了Cloneable接口,重写了clone()方法

@Data
class Address implements Cloneable {
    private String street;
    private int number;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

@Data
class Person implements Cloneable {
    private String name;
    private Address address; // Person类中包含一个Address类型的对象

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

现在对Person对象进行拷贝,看看结果:

public class ObjectCloneTest {
    public static void main(String[] args) {
        try {
            // 原始Person对象
            Person original = new Person();
            original.setName("John Doe");
            // 原始Address对象
            Address originalAddress = new Address();
            originalAddress.setStreet("123 Elm Street");
            originalAddress.setNumber(100);
            original.setAddress(originalAddress);

            //对Person对象进行拷贝得到拷贝的clonedPerson对象
            Person clonedPerson = (Person) original.clone();
            System.out.println(clonedPerson.toString());

            // 修改原始Person对象对象的Address信息
            original.getAddress().setStreet("456 Oak Street");

            // 检查克隆clonedPerson对象的Address信息是否也被修改
            System.out.println(clonedPerson.toString());

        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

这里对Person设置相应属性值后,调用clone()方法进行拷贝得到一个新的clonedPerson对象,然后再对原Person对象地址属性类型进行修改,看看新的clonedPerson对象是不是也相应修改了。得到结果如下:

Person(name=John Doe, address=Address(street=123 Elm Street, number=100))
Person(name=John Doe, address=Address(street=456 Oak Street, number=100))

这里会发现,clonedPerson对象地址也做了修改,这就说明clonedPerson对象中Address对象其实不是新创建的Address对象,只是原Person的一个引用,实际内容还是在原Person中,所以改了原Person,新的也会随之改变,这就是所谓的浅拷贝。

浅拷贝特别说明之BeanUtils.copyProperties方法

        我们实际工作中会经常遇到这个方法,Apache Commons BeanUtils库还是Spring框架中的BeanUtils类都有一个copyProperties方法,需要注意的是这个方法进行的是浅拷贝,使用BeanUtils.copyProperties时,如果源对象中包含有引用类型的字段,那么这些字段在目标对象中将会引用相同的对象实例。

copyProperties方法代码演示
public static void main(String[] args) {
        try {
            // 原始Person对象
            Person original = new Person();
            original.setName("John Doe");
            // 原始Address对象
            Address originalAddress = new Address();
            originalAddress.setStreet("123 Elm Street");
            originalAddress.setNumber(100);
            original.setAddress(originalAddress);

            //对Person对象进行拷贝得到拷贝的clonedPerson对象
            Person clonedPerson = Person.class.newInstance();
            BeanUtils.copyProperties(original,clonedPerson);
            System.out.println(clonedPerson.toString());

            // 修改原始Person对象对象的Address信息
            original.getAddress().setStreet("456 Oak Street");

            // 检查克隆clonedPerson对象的Address信息是否也被修改
            System.out.println(clonedPerson.toString());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

同样的对象,使用BeanUtils.copyProperties进行拷贝,得到的结果如下

Person(name=John Doe, address=Address(street=123 Elm Street, number=100))
Person(name=John Doe, address=Address(street=456 Oak Street, number=100))

由此可见改变了原对象属性值,新的拷贝对象会随之改变,因为是浅拷贝,开发过程中需注意。

深拷贝

clone()方法代码演示

实现深拷贝(Deep Copy),我们需要在类中重写clone()方法,并确保对于类中的所有对象类型字段,我们也创建并复制这些对象,而不仅仅是复制它们的引用。这通常需要对类中的所有对象类型字段递归地调用clone()方法(假设这些字段的类也支持clone())。

因此我们需要对Person类的clone()方法做相应的改造了,增加对Address对象的额外处理

@Data
class Person implements Cloneable {
    private String name;
    private Address address; // Person类中包含一个Address类型的对象
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person cloned = (Person) super.clone(); // 调用super.clone()进行浅拷贝
        // 深度拷贝Address对象
        cloned.address = (Address) address.clone();
        return cloned;
    }
}

改造完成后,再执行上面的测试代码,发现结果如下
 

Person(name=John Doe, address=Address(street=123 Elm Street, number=100))
Person(name=John Doe, address=Address(street=123 Elm Street, number=100))

此时对原对象的修改不会影响拷贝的新对象了。

深拷贝额外扩展之序列化实现方式

        针对于浅拷贝,其实直接用clone()方法实现还是比较简单的,但是深拷贝由于需要对对象属性做额外的处理,如果一个类中的对象属性很多,那其实也挺麻烦的。有没有其他方法呢,这里再介绍一下序列化的方式。

        通过序列化实现深拷贝是一种相对简单但可能效率不是最高的方法。它依赖于对象的Serializable接口,并将对象的状态写入到一个字节流中,然后再从这个字节流中重新构造出对象的一个新实例。这种方法的一个主要优点是它自动处理了对象图中的所有对象,包括嵌套的对象和循环引用。

序列化实现深拷贝代码演示

针对上面的示例做相应改造,首先Person和Address需要实现序列化接口

@Data
class Address implements Serializable {
    private static final long serialVersionUID = 1L; // 用于版本控制
    private String street;
    private int number;
}

@Data
class Person implements Serializable {
    private static final long serialVersionUID = 1L; // 用于版本控制
    private String name;
    private Address address; // Person类中包含一个Address类型的对象
}

封装一个序列化拷贝工具类:

public class SerializationUtils {
    @SuppressWarnings("unchecked")
    public static <T> T deepCopy(T object) {
        try {
            // 写入字节流
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(object);

            // 从字节流中读取
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);

            // 返回新构造的对象
            return (T) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

执行深拷贝测试类,对象内容同上面的示例一样

public class DeepCloneTest {
    public static void main(String[] args) {
        try {
            // 原始Person对象
            Person original = new Person();
            original.setName("John Doe");
            // 原始Address对象
            Address originalAddress = new Address();
            originalAddress.setStreet("123 Elm Street");
            originalAddress.setNumber(100);
            original.setAddress(originalAddress);

            //对Person对象进行拷贝得到拷贝的clonedPerson对象
            Person clonedPerson = (Person) SerializationUtils.deepCopy(original);;
            System.out.println(clonedPerson.toString());

            // 修改原始Person对象对象的Address信息
            original.getAddress().setStreet("456 Oak Street");

            // 检查克隆clonedPerson对象的Address信息是否也被修改
            System.out.println(clonedPerson.toString());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试结果如下

Person(name=John Doe, address=Address(street=123 Elm Street, number=100))
Person(name=John Doe, address=Address(street=123 Elm Street, number=100))

        序列化拷贝方式也实现了对象的深拷贝,并且不考虑对象嵌套的深度和广度,比较通用。但这种方式不是最高效的,因为序列化和反序列化操作相对较慢,并且会产生额外的内存和IO开销。此外,不是所有的类都可以或应该被序列化(例如,包含非瞬态非序列化字段的类)。因此,在选择使用序列化进行深拷贝之前,请确保这是最适合你需求的方法。 

总结

现在回过头来看阿里规约中的说明就能明白里面的含义了吧,就是使用clone()方法时一定要注意你要的是浅拷贝还是深拷贝,并作相应的处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chen2017sheng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值