面试复盘:Java实现深拷贝与浅拷贝
在最近的一次面试中,我被问到了一个经典的Java问题:"如何实现深拷贝和浅拷贝?如果一个对象中还嵌套了其他对象的实例,如何实现深拷贝?"这个问题看似简单,但深入探讨后发现涉及到对象克隆的底层原理和多种实现方式。以下是我的复盘和详细解答。
一、浅拷贝与深拷贝的区别
1. 浅拷贝 (Shallow Copy)
- 定义:浅拷贝会创建一个新对象,新对象复制了原始对象的基本数据类型字段和引用类型字段的值。但对于引用类型字段,新对象和原始对象指向的是同一个内存地址。
- 特点 :
- 基本数据类型(如int、double)是独立的副本,修改不会影响原对象。
- 引用类型(如对象、数组)只是复制了引用地址,修改会影响原对象。
- 典型场景 :Java中
Object.clone()
默认实现的就是浅拷贝。
2. 深拷贝 (Deep Copy)
- 定义:深拷贝不仅复制基本数据类型字段,还会递归地复制引用类型字段的所有内容,确保新对象与原对象完全独立,修改一方不会影响另一方。
- 特点 :
- 所有字段(包括嵌套对象)都是独立的副本。
- 实现复杂,需要处理对象图中的每一层引用。
- 典型场景:需要完全独立的对象副本时使用,比如在多线程环境或数据持久化中。
3. 举个例子
假设有一个Person
类,里面嵌套了一个Address
对象:
java
class Address {
String city;
public Address(String city) {
this.city = city;
}
}
class Person {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
- 浅拷贝 :新
Person
对象的address
字段和原对象指向同一个Address
实例。 - 深拷贝 :新
Person
对象的address
字段是一个全新的Address
实例,与原对象无关。
二、Java实现浅拷贝
1. 使用Object.clone()
Java中的Object
类提供了clone()
方法,默认实现是浅拷贝。要使用它,类需要实现Cloneable
接口并重写clone()
方法。
java
class Address implements Cloneable {
String city;
public Address(String city) {
this.city = city;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 浅拷贝
}
public static void main(String[] args) throws CloneNotSupportedException {
Address addr = new Address("Beijing");
Person p1 = new Person("Alice", addr);
Person p2 = (Person) p1.clone();
System.out.println(p1.address == p2.address); // true,说明address是同一个对象
p2.address.city = "Shanghai";
System.out.println(p1.address.city); // Shanghai,修改p2影响了p1
}
}
问题 :p2.address
和p1.address
指向同一个Address
对象,修改一方会影响另一方。
三、Java实现深拷贝
深拷贝需要确保嵌套对象也被复制。以下是几种常见实现方式:
1. 重写clone()
实现深拷贝
在Person
类的clone()
方法中,手动克隆address
字段。
java
class Address implements Cloneable {
String city;
public Address(String city) {
this.city = city;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone(); // 先浅拷贝
cloned.address = (Address) address.clone(); // 再深拷贝嵌套对象
return cloned;
}
public static void main(String[] args) throws CloneNotSupportedException {
Address addr = new Address("Beijing");
Person p1 = new Person("Alice", addr);
Person p2 = (Person) p1.clone();
System.out.println(p1.address == p2.address); // false,address是独立对象
p2.address.city = "Shanghai";
System.out.println(p1.address.city); // Beijing,未受影响
}
}
优点 :简单直观,适合对象结构不复杂的情况。 缺点 :需要为每个嵌套类实现clone()
,代码冗余且不灵活。
2. 使用序列化实现深拷贝
通过将对象序列化为字节流,再反序列化回来,可以实现深拷贝。这种方式无需手动克隆每个字段。
java
import java.io.*;
class Address implements Serializable {
String city;
public Address(String city) {
this.city = city;
}
}
class Person implements Serializable {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
public Person deepCopy() throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Person) ois.readObject();
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Address addr = new Address("Beijing");
Person p1 = new Person("Alice", addr);
Person p2 = p1.deepCopy();
System.out.println(p1.address == p2.address); // false
p2.address.city = "Shanghai";
System.out.println(p1.address.city); // Beijing
}
}
优点:
- 通用性强,适用于复杂对象图。
- 无需为每个嵌套类手动实现克隆逻辑。 缺点:
- 需要实现
Serializable
接口。 - 性能开销较大(涉及IO操作)。
- 如果对象包含不可序列化的字段,会抛异常。
3. 使用构造方法或手动复制
通过构造方法或setter手动复制所有字段,包括嵌套对象。
java
class Address {
String city;
public Address(String city) {
this.city = city;
}
public Address deepCopy() {
return new Address(this.city);
}
}
class Person {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
public Person deepCopy() {
return new Person(this.name, this.address.deepCopy());
}
public static void main(String[] args) {
Address addr = new Address("Beijing");
Person p1 = new Person("Alice", addr);
Person p2 = p1.deepCopy();
System.out.println(p1.address == p2.address); // false
p2.address.city = "Shanghai";
System.out.println(p1.address.city); // Beijing
}
}
优点 :代码清晰,控制粒度高。 缺点:需要为每个类手动编写复制逻辑,维护成本高。
4. 使用第三方库(如Gson或Jackson)
通过JSON序列化和反序列化实现深拷贝。
java
import com.google.gson.Gson;
class Address {
String city;
public Address(String city) {
this.city = city;
}
}
class Person {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
public Person deepCopy() {
Gson gson = new Gson();
String json = gson.toJson(this);
return gson.fromJson(json, Person.class);
}
public static void main(String[] args) {
Address addr = new Address("Beijing");
Person p1 = new Person("Alice", addr);
Person p2 = p1.deepCopy();
System.out.println(p1.address == p2.address); // false
p2.address.city = "Shanghai";
System.out.println(p1.address.city); // Beijing
}
}
优点 :简单易用,适合复杂对象。 缺点:依赖外部库,性能不如手动实现。
四、面试中的注意点
- 澄清需求:先问清楚面试官是否需要处理循环引用(如对象A引用B,B又引用A),因为序列化等方式可能抛出栈溢出异常。
- 复杂度分析 :
clone()
:O(n),n为对象字段数。- 序列化:O(n) + IO开销。
- JSON:O(n) + 反射开销。
- 异常处理 :实现时要注意
CloneNotSupportedException
或IO异常。 - 扩展性:如果对象结构复杂,推荐序列化或JSON方式。
五、总结
浅拷贝适用于简单场景,而深拷贝是面试中的重点。实现深拷贝时:
- 简单对象 :用
clone()
或手动复制。 - 复杂对象:用序列化或JSON库。