从实际编程示例中看java中对象的浅拷贝和深拷贝,因此可以说,我们操作
从实际编程示例中看java中对象的浅拷贝和深拷贝,因此可以说,我们操作
浅拷贝(克隆)与深拷贝(克隆)
先来看一个简单的例子,我们希望复制一个set对象,在修改这个复制对象的时候,原有的set对象不应该改变
接下来举两种复制方法,我们应该选择哪一个呢?
Set<String> copiedSet = originalSet;
Set<String> copiedSet = new HashSet<>(originalSet);
显然我们应当选择第二种:
-
Set<String> copiedSet = originalSet;
使copiedSet
和originalSet
指向同一个内存地址,两者本质上是同一个对象的两个别名。因此可以说,我们操作copiedSet
就是操作originalSet
。 -
Set<String> copiedSet = new HashSet<>(originalSet);
通过构造函数创建一个新的HashSet
对象,我们操作copiedSet
不会影响originalSet
。但这也仅仅只是浅拷贝。
那么什么是深拷贝呢?以上例子中的Set
是java自带的封装类,如果我们设定的集合中元素是自定义类且未实现深拷贝逻辑,修改元素内部属性会导致原集合和新集合共享变更
class Person {
String name;
}
Set<Person> original = new HashSet<>();
orginal.put(new Person("Alice"));
Set<Person> copied = new HashSet<>(original);
for(Person p:conpied){
p.name = "Bob"; // 原集合中的Person对象name也被修改
}
浅拷贝
-
定义:仅复制对象及对象的顶层结构(如基本类型字段的值、引用类型字段的内存地址),不复制引用指向的实际对象,引用类型字段仍指向原对象的内存地址。修改其中一个对象的引用类型属性会影响另一个对象。
-
特点:
- 实现简单,资源消耗低。
- 适用于只读场景或结构简单的对象。
-
示例代码:
class Person implements Cloneable { String name; Address address; // Address为引用类型 @Override public Person clone() throws CloneNotSupportedException { return (Person) super.clone(); // 默认浅拷贝 } }
深拷贝
-
定义:递归复制对象及其所有引用类型字段,生成完全独立的副本。修改新对象不会影响原对象。
-
特点:
- 实现复杂,资源消耗高。
- 适用于需要完全独立副本的场景(如多线程修改、数据隔离)。
-
示例代码:
class Person implements Cloneable { String name; Address address; @Override public Person clone() throws CloneNotSupportedException { Person cloned = (Person) super.clone(); cloned.address = this.address.clone(); // 递归拷贝引用类型 return cloned; } }
方式 | 浅拷贝 | 深拷贝 |
---|---|---|
复制内容 | 仅复制对象本身及其字段的引用 | 递归复制对象及其所有引用指向的子对象 |
内存占用 | 低(共享子对象) | 高(独立副本) |
修改影响 | 原对象和拷贝对象相互影响 | 完全独立 |
典型实现 | Object.clone() (默认未修改时) |
手动递归复制、序列化工具、第三方库 |
实现方式
方法 | 浅拷贝 | 深拷贝 | 说明 |
---|---|---|---|
Object.clone() |
✔️ | ❌ | 默认浅拷贝,需实现Cloneable 接口。 |
手动递归拷贝 | ❌ | ✔️ | 需为每个引用类型字段显式调用拷贝方法(如clone() 或构造函数)。 |
序列化与反序列化 | ❌ | ✔️ | 要求所有类实现Serializable 接口,利用IO流生成新对象。 |
第三方库(如Gson/Jackson) | ❌ | ✔️ | 通过JSON序列化实现深拷贝,无需修改原有类结构。 |
1. 浅拷贝实现
-
默认
clone()
方法:class Person implements Cloneable { String name; List<String> hobbies; @Override public Person clone() { try { return (Person) super.clone(); // 浅拷贝 } catch (CloneNotSupportedException e) { throw new AssertionError(); } } }
2. 深拷贝实现
(1) 手动递归复制
class Person implements Cloneable {
String name;
List<String> hobbies;
// 深拷贝方法
@Override
public Person clone() {
Person copy = new Person();
copy.name = this.name;//String类型,是不可变类,无需深拷贝
copy.hobbies = new ArrayList<>(this.hobbies); // 复制List内容
return copy;
}
}
(2) 序列化与反序列化
需实现Serializable
接口,借助IO流完成深拷贝:
import java.io.*;
public static <T> T deepCopy(T obj) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (T) ois.readObject();
}
// 使用
Person p2 = deepCopy(p1);
(3) 第三方工具库
-
Apache Commons Lang:
import org.apache.commons.lang3.SerializationUtils; Person p2 = SerializationUtils.clone(p1);
-
JSON序列化(如Gson):
Gson gson = new Gson(); Person p2 = gson.fromJson(gson.toJson(p1), Person.class);
应用场景分析
按方式
- 浅拷贝适用场景
- 对象仅包含基本类型或不可变类(如
String
)。 - 数据仅用于读取,无后续修改需求。
- 资源敏感场景(如高频调用的简单对象)。
- 对象仅包含基本类型或不可变类(如
- 深拷贝适用场景
- 对象包含多层嵌套引用类型(如
List<Map<String, Object>>
)。 - 需要副本与原对象完全隔离(如缓存数据副本、事务回滚)。
- 多线程环境下操作独立数据。
- 对象包含多层嵌套引用类型(如
按对象
- 简单对象
使用Cloneable
接口重写clone()
方法(浅拷贝)或手动递归(深拷贝)。 - 复杂对象
优先选择序列化(需Serializable
接口)或第三方库(如Apache Commons Lang、Gson)。
注意事项
-
引用类型递归问题
深拷贝需确保所有嵌套的引用类型均实现拷贝逻辑,否则可能残留浅拷贝链路。// 错误示例:Address未实现深拷贝 class Person { Address address; public Person clone() { Person cloned = new Person(); cloned.address = this.address; // address不是不可变类,仍然是浅拷贝 //这行代码直接将原对象this.address的引用赋值给了新对象cloned.address,导致两个Person对象的address字段指向同一个Address实例。因此,修改任一Person对象的address属性时,另一个对象的address也会被同步修改 return cloned; } }
我们需要修改为:
class Address implements Cloneable {//首先把Address的拷贝逻辑修改正确 private String city;//不可变对象 @Override public Address clone() { try { return (Address) super.clone(); // 若字段均为基本类型或不可变对象,则已足够,如果还有引用还需要继续嵌套 } catch (CloneNotSupportedException e) { throw new AssertionError(); } } } class Person { Address address; public Person clone() { Person cloned = new Person(); cloned.address = this.address.clone(); // 调用Address的深拷贝方法 return cloned; } }
-
性能与复杂度
- 深拷贝的递归层级越深,性能开销越大。(上面的代码以可以看出)
- 序列化方式可能因对象复杂度导致效率低下。
-
若引用类型为不可变类(如
String
、Integer
),可直接赋值,无需深拷贝。知识补充:不可变类(Immutable Class)
不可变类是指实例一旦创建后,其状态(字段值)不能被修改的类。Java中的不可变类具有线程安全、缓存优化等优势。
不可变类的特点
- 状态不可变:所有字段在对象创建后不可修改。
- 线程安全:无需同步机制,多线程共享时无竞态条件。
- 哈希稳定:对象的
hashCode()
在生命周期内不变,适合作为HashMap
的键。
识别方法
-
类声明为
final
:防止子类覆盖方法破坏不可变性。 -
字段为
private final
:确保字段只能在构造函数中初始化。 -
无
setter
方法:避免外部修改字段值。 -
防御拷贝:如果类包含字段是引用类型(如集合),那么这些引用也应该被封装为不可变对象或提供只读的访问方式。
public final class ImmutablePerson { private final List<String> hobbies; public ImmutablePerson(List<String> hobbies) { this.hobbies = new ArrayList<>(hobbies); // 深拷贝 } public List<String> getHobbies() { return Collections.unmodifiableList(hobbies); // 返回不可修改集合 } }
常见不可变类示例
-
String
:当你对String对象进行修改时(如拼接操作),实际上Java会创建一个新的String对象,而不是修改原有的对象。 -
基本类型的包装类:
Java的八个基本数据类型(
byte
,short
,int
,long
,float
,double
,char
,boolean
)的包装类(Byte
,Short
,Integer
,Long
,Float
,Double
,Character
,Boolean
)都是不可变的。这意味着当你创建一个这些类型的对象后,你不能改变其内部的值。 -
Java 8时间API:
LocalDate
、ZonedDateTime
。 -
不可变的集合类
Java集合框架提供了一些不可变的集合实现,如
Collections.unmodifiableList()
、Collections.unmodifiableSet()
等。这些方法返回的是原有集合的不可变视图,任何对它们的修改操作都会抛出UnsupportedOperationException
异常。 -
枚举类
在Java中,大多数枚举类也是不可变的。枚举类型的实例在JVM中只有一个,且不能被修改。
-
其他
Java中还有其他一些常用的不可变类,如BigDecimal、BigInteger等。此外,
java.lang.StackTraceElement
用于构建异常的堆栈跟踪,也是不可变的。
-
高安全性场景:采用构造函数逐层复制,避免依赖
clone()
方法的潜在漏洞。浅拷贝导致数据污染风险
class User implements Cloneable { private List<String> permissions = new ArrayList<>(); public User clone() { return super.clone(); } // 浅拷贝 } User user1 = new User(Arrays.asList("read")); User user2 = user1.clone(); user2.getPermissions().add("delete"); // 原user1的permissions也被修改
Cloneable接口的脆弱性
Cloneable 是一个空标记接口,未强制要求类实现 clone() 方法。若开发者未正确覆盖 clone() 方法,可能导致以下问题:- 未受控的克隆行为:攻击者可通过继承并覆写 clone() 方法,绕过安全校验直接复制对象。
- 异常处理漏洞:默认 clone() 可能抛出 CloneNotSupportedException,若未妥善处理,程序可能因异常中断或暴露内部状态。
绕过构造函数的安全校验
对象初始化不可控:
clone()
方法通过内存复制直接创建对象,不调用构造函数,可能绕过构造函数中的安全检查或初始化逻辑。例如:- 若构造函数包含权限验证、加密初始化等逻辑,克隆对象可能处于未经验证的状态
class SecureConfig { private String key = generateEncryptedKey(); // 构造函数中生成密钥 public SecureConfig() { validateKey(key); } // 密钥校验逻辑 // 若通过clone()复制,密钥可能未经验证 }
多线程环境下状态不一致风险
- 若多个线程同时克隆一个可变对象,且克隆过程未同步,可能导致克隆后的对象处于中间状态(如部分字段已更新、部分未更新),破坏数据一致性
深拷贝实现的复杂性
- 递归深拷贝的遗漏风险:手动实现
clone()
方法时,需递归处理所有引用类型字段。若遗漏某一层级,可能导致深拷贝不彻底,残留共享引用。
推荐:构造函数逐层复制
场景一:在需要保护用户隐私数据的系统中,若直接使用
clone()
方法,可能因浅拷贝导致地址信息被篡改。通过构造函数逐层复制可确保数据独立性:// 地址类(包含敏感地理位置信息) class SecureAddress { private final String city; // 使用final增强不可变性 private final String gpsCoordinate; // 原始构造函数(含数据校验) public SecureAddress(String city, String gps) { validateGPS(gps); // 高安全场景中的校验逻辑 this.city = city; this.gpsCoordinate = gps; } // 复制构造函数(逐层深拷贝) public SecureAddress(SecureAddress other) { this(other.city, other.gpsCoordinate); // 调用原始构造函数执行校验 } private void validateGPS(String gps) { if (!gps.matches("^\\d+°\\d+'\\d+\" [NS] \\d+°\\d+'\\d+\" [EW]$")) throw new SecurityException("非法GPS格式"); } } // 用户类(包含敏感地址信息) class SecureUser { private final String id; private final SecureAddress address; // 原始构造函数(含身份验证) public SecureUser(String id, SecureAddress address) { validateID(id); // 身份ID格式校验 this.id = id; this.address = new SecureAddress(address); // 防御性拷贝 } // 复制构造函数(逐层调用) public SecureUser(SecureUser other) { this(other.id, new SecureAddress(other.address)); // 递归调用SecureAddress的复制构造 } private void validateID(String id) { if (!id.matches("^[A-Z]\\d{9}$")) throw new SecurityException("非法用户ID"); } }
安全性优势:
- 绕过
clone()
校验漏洞:直接调用构造函数时,会触发validateGPS()
和validateID()
校验逻辑,而clone()可能跳过这些校验。 - 防御拷贝:在原始构造函数中对address进行拷贝,防止外部引用篡改(如通过
setAddress()
注入非法数据)。 - 不可变设计:字段使用
final
修饰,防止对象创建后被修改。
场景二:在密钥管理系统(KMS)中,加密配置需要确保密钥和算法参数的绝对隔离:
// 加密算法参数(含敏感密钥) class CryptoParams { private final byte[] secretKey; private final String algorithm; // 原始构造函数(密钥加密存储) public CryptoParams(byte[] key, String algo) { this.secretKey = encryptKey(key); // 内存加密处理 this.algorithm = algo; } // 复制构造函数(深拷贝字节数组) public CryptoParams(CryptoParams other) { this.secretKey = Arrays.copyOf(other.secretKey, other.secretKey.length); this.algorithm = other.algorithm; } private byte[] encryptKey(byte[] rawKey) { // 使用硬件安全模块(HSM)加密密钥 return HSM.encrypt(rawKey); } } // 系统安全配置(嵌套多层对象) class SecurityConfig { private final CryptoParams params; private final List<String> accessIPs; // 原始构造函数(IP白名单过滤) public SecurityConfig(CryptoParams params, List<String> ips) { this.params = new CryptoParams(params); // 深拷贝CryptoParams this.accessIPs = new ArrayList<>(filterIPs(ips)); // 防御性拷贝+过滤 } // 复制构造函数(逐层复制) public SecurityConfig(SecurityConfig other) { this(new CryptoParams(other.params), new ArrayList<>(other.accessIPs)); } private List<String> filterIPs(List<String> ips) { return ips.stream().filter(ip -> isValidIP(ip)).collect(Collectors.toList()); } }
安全性优势:
- 内存安全:通过
Arrays.copyOf()
复制密钥字节数组,避免clone()
可能遗留的数组引用。 - 数据过滤:构造函数中调用
filterIPs()
实现输入校验,而clone()
无法自动执行此类逻辑。 - 防御集合拷贝:对
accessIPs
进行new ArrayList<>()
复制,防止外部列表修改影响内部状态。
用户点评