MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java中的深拷贝与浅拷贝

2021-09-066.4k 阅读

一、浅拷贝

在Java中,对象的拷贝分为浅拷贝和深拷贝。浅拷贝是指创建一个新对象,新对象的属性值与原对象相同,但是对于引用类型的属性,新对象和原对象共享该引用,即指向同一个内存地址。

1.1 实现浅拷贝的方式

在Java中,要实现浅拷贝,类需要实现Cloneable接口,并覆盖clone()方法。Cloneable接口是一个标记接口,本身不包含任何方法,只是告诉Java虚拟机该类可以被克隆。

以下是一个简单的示例:

class Address {
    private String city;
    private String street;

    public Address(String city, String street) {
        this.city = city;
        this.street = street;
    }

    public String getCity() {
        return city;
    }

    public String getStreet() {
        return street;
    }
}

class Person implements Cloneable {
    private String name;
    private int age;
    private Address address;

    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Address getAddress() {
        return address;
    }

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

在上述代码中,Person类实现了Cloneable接口并重写了clone()方法。Person类包含一个Address类型的引用属性。

1.2 浅拷贝的问题

浅拷贝存在的问题在于对于引用类型的属性,新对象和原对象共享该引用。这意味着如果在新对象或原对象中修改了引用类型属性的值,另一个对象也会受到影响。

例如,以下代码展示了浅拷贝的这种问题:

public class ShallowCopyExample {
    public static void main(String[] args) {
        Address address = new Address("Beijing", "Chaoyang Street");
        Person person1 = new Person("Alice", 30, address);

        try {
            Person person2 = (Person) person1.clone();
            System.out.println("person1 address: " + person1.getAddress().getCity());
            System.out.println("person2 address: " + person2.getAddress().getCity());

            // 修改person2的地址
            person2.getAddress().setCity("Shanghai");
            System.out.println("After modification:");
            System.out.println("person1 address: " + person1.getAddress().getCity());
            System.out.println("person2 address: " + person2.getAddress().getCity());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,person1person2是通过浅拷贝得到的两个对象。当修改person2Address对象的city属性时,person1Address对象的city属性也会发生变化。这是因为person1person2address属性指向同一个Address对象。

二、深拷贝

深拷贝是指创建一个新对象,新对象的属性值与原对象相同,并且对于引用类型的属性,新对象会创建一个新的引用,指向一个新的与原对象内容相同的对象,即新对象和原对象的所有属性(包括引用类型属性)都完全独立。

2.1 实现深拷贝的方式

实现深拷贝有多种方式,下面介绍几种常见的方法。

2.1.1 手动递归实现深拷贝

手动递归实现深拷贝需要在clone()方法中对每个引用类型的属性进行递归克隆。以之前的PersonAddress类为例,修改如下:

class Address implements Cloneable {
    private String city;
    private String street;

    public Address(String city, String street) {
        this.city = city;
        this.street = street;
    }

    public String getCity() {
        return city;
    }

    public String getStreet() {
        return street;
    }

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

class Person implements Cloneable {
    private String name;
    private int age;
    private Address address;

    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Address getAddress() {
        return address;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person cloned = (Person) super.clone();
        cloned.address = (Address) address.clone();
        return cloned;
    }
}

在上述代码中,Address类也实现了Cloneable接口并重写了clone()方法。Person类的clone()方法中,不仅克隆了Person本身,还对Address类型的address属性进行了克隆,从而实现了深拷贝。

以下是测试代码:

public class DeepCopyExample1 {
    public static void main(String[] args) {
        Address address = new Address("Beijing", "Chaoyang Street");
        Person person1 = new Person("Alice", 30, address);

        try {
            Person person2 = (Person) person1.clone();
            System.out.println("person1 address: " + person1.getAddress().getCity());
            System.out.println("person2 address: " + person2.getAddress().getCity());

            // 修改person2的地址
            person2.getAddress().setCity("Shanghai");
            System.out.println("After modification:");
            System.out.println("person1 address: " + person1.getAddress().getCity());
            System.out.println("person2 address: " + person2.getAddress().getCity());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,修改person2Address对象的city属性不会影响person1Address对象的city属性,因为它们是两个独立的Address对象。

2.1.2 使用序列化和反序列化实现深拷贝

Java的序列化机制可以将对象转换为字节流,然后再从字节流中恢复对象。通过这种方式,可以实现深拷贝。

首先,相关的类需要实现Serializable接口。修改AddressPerson类如下:

import java.io.*;

class Address implements Serializable {
    private static final long serialVersionUID = 1L;
    private String city;
    private String street;

    public Address(String city, String street) {
        this.city = city;
        this.street = street;
    }

    public String getCity() {
        return city;
    }

    public String getStreet() {
        return street;
    }
}

class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    private Address address;

    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Address getAddress() {
        return address;
    }

    public Object deepClone() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);

            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

在上述代码中,AddressPerson类都实现了Serializable接口,并定义了serialVersionUIDPerson类提供了一个deepClone()方法,通过序列化和反序列化实现深拷贝。

以下是测试代码:

public class DeepCopyExample2 {
    public static void main(String[] args) {
        Address address = new Address("Beijing", "Chaoyang Street");
        Person person1 = new Person("Alice", 30, address);

        Person person2 = (Person) person1.deepClone();
        System.out.println("person1 address: " + person1.getAddress().getCity());
        System.out.println("person2 address: " + person2.getAddress().getCity());

        // 修改person2的地址
        person2.getAddress().setCity("Shanghai");
        System.out.println("After modification:");
        System.out.println("person1 address: " + person1.getAddress().getCity());
        System.out.println("person2 address: " + person2.getAddress().getCity());
    }
}

这种方式实现的深拷贝比较通用,不需要手动递归处理每个引用类型的属性。但是,它要求类及其所有属性类都必须实现Serializable接口,并且序列化和反序列化的过程可能会带来一定的性能开销。

2.1.3 使用第三方库实现深拷贝

一些第三方库,如Apache Commons Lang,提供了方便的深拷贝工具。使用Apache Commons Lang的SerializationUtils类可以很容易地实现深拷贝。

首先,需要在项目中添加Apache Commons Lang的依赖。如果使用Maven,可以在pom.xml中添加以下依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

然后,修改Person类如下:

import org.apache.commons.lang3.SerializationUtils;

class Address {
    private String city;
    private String street;

    public Address(String city, String street) {
        this.city = city;
        this.street = street;
    }

    public String getCity() {
        return city;
    }

    public String getStreet() {
        return street;
    }
}

class Person {
    private String name;
    private int age;
    private Address address;

    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Address getAddress() {
        return address;
    }

    public Object deepClone() {
        return SerializationUtils.clone(this);
    }
}

在上述代码中,Person类通过SerializationUtils.clone(this)实现深拷贝。

以下是测试代码:

public class DeepCopyExample3 {
    public static void main(String[] args) {
        Address address = new Address("Beijing", "Chaoyang Street");
        Person person1 = new Person("Alice", 30, address);

        Person person2 = (Person) person1.deepClone();
        System.out.println("person1 address: " + person1.getAddress().getCity());
        System.out.println("person2 address: " + person2.getAddress().getCity());

        // 修改person2的地址
        person2.getAddress().setCity("Shanghai");
        System.out.println("After modification:");
        System.out.println("person1 address: " + person1.getAddress().getCity());
        System.out.println("person2 address: " + person2.getAddress().getCity());
    }
}

使用第三方库实现深拷贝相对简单,代码量少,并且库经过了广泛的测试和优化。但同样需要相关类实现Serializable接口,并且引入第三方库可能会增加项目的依赖管理复杂度。

三、浅拷贝与深拷贝的适用场景

  1. 浅拷贝适用场景

    • 当对象的引用类型属性不会被修改,或者多个对象共享这些引用类型属性不会产生问题时,浅拷贝是一个不错的选择。例如,一些只读的对象,或者对象的生命周期较短,在其生命周期内不会修改引用类型属性的值。浅拷贝的优点是实现简单,性能开销相对较小,因为不需要对引用类型属性进行额外的克隆操作。
    • 对于一些数据结构,如链表,如果只需要复制链表的结构而不复制节点内部的对象(假设节点内部对象在整个过程中不会被修改),浅拷贝可以满足需求。
  2. 深拷贝适用场景

    • 当需要确保新对象和原对象完全独立,互不影响时,必须使用深拷贝。例如,在多线程环境中,不同线程可能会对对象进行修改,如果使用浅拷贝,可能会导致数据不一致的问题。深拷贝可以保证每个线程操作的对象都是独立的副本。
    • 对于一些包含敏感信息的对象,如用户密码等,为了防止在其他地方误修改或泄露,也应该使用深拷贝,确保原对象和新对象之间没有共享的引用。
    • 在一些复杂的数据结构,如树形结构中,如果需要复制整个树及其所有节点包含的对象,深拷贝是必要的,以保证新树和原树完全独立。

四、注意事项

  1. Cloneable接口的局限性Cloneable接口是一个标记接口,本身没有定义任何方法。它只是告诉Java虚拟机该类可以被克隆。但是,实现了Cloneable接口并不意味着一定能成功克隆对象。Object类的clone()方法是受保护的,需要在子类中重写并将其访问修饰符改为public才能在外部调用。此外,如果类的继承层次结构比较复杂,实现浅拷贝或深拷贝可能会遇到一些问题,需要仔细处理。
  2. 深拷贝的性能开销:无论是手动递归实现深拷贝,还是使用序列化和反序列化或第三方库实现深拷贝,都比浅拷贝有更高的性能开销。手动递归实现深拷贝可能会因为递归调用导致栈溢出,尤其是在对象的引用层次非常深的情况下。序列化和反序列化以及第三方库使用的序列化机制也会占用一定的时间和空间。因此,在选择深拷贝方式时,需要根据对象的复杂度和性能要求进行权衡。
  3. 类的不变性:在进行深拷贝或浅拷贝时,需要考虑类的不变性。如果一个类有一些不变的属性或约束条件,在克隆过程中需要确保这些不变性仍然成立。例如,一个表示日期范围的类,在克隆后新对象的日期范围也应该是有效的,不能因为克隆操作导致日期范围出现错误。
  4. 集合类型的拷贝:如果对象中包含集合类型的属性,如ListSetMap等,在实现深拷贝时需要特别注意。对于ListSet,可以遍历集合,对每个元素进行深拷贝,然后创建一个新的集合。对于Map,需要对键和值都进行深拷贝。一些集合类本身提供了克隆方法,但这些方法通常是浅拷贝,需要额外处理才能实现深拷贝。

五、总结浅拷贝与深拷贝在Java中的差异

  1. 对象复制的深度:浅拷贝只复制对象的基本类型属性值,对于引用类型属性,新对象和原对象共享引用;而深拷贝不仅复制基本类型属性值,还会递归地复制引用类型属性所指向的对象,使得新对象和原对象的所有属性都完全独立。
  2. 实现方式:浅拷贝通过实现Cloneable接口并重写clone()方法,直接调用super.clone()即可;深拷贝可以通过手动递归克隆引用类型属性、使用序列化和反序列化,或者借助第三方库来实现。
  3. 性能与资源消耗:浅拷贝性能开销较小,因为不需要额外处理引用类型属性的复制;深拷贝性能开销较大,尤其是在对象结构复杂、引用层次深的情况下,手动递归可能导致栈溢出,序列化和反序列化也会占用较多时间和空间。
  4. 适用场景:浅拷贝适用于对象的引用类型属性不会被修改或共享引用不会产生问题的情况;深拷贝适用于需要确保新对象和原对象完全独立,互不影响的场景,如多线程环境或涉及敏感信息的对象。

在实际的Java编程中,正确选择浅拷贝或深拷贝对于保证程序的正确性和性能至关重要。开发者需要根据具体的业务需求、对象的结构和特点,仔细权衡并选择合适的拷贝方式。同时,在实现拷贝时,要注意遵循相关的规范和注意事项,以避免出现错误和性能问题。