时间:2025-05-28 18:36
人气:
作者:admin
先来看一个简单的例子,我们希望复制一个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序列化实现深拷贝,无需修改原有类结构。 |
默认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();
}
}
}
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;
}
}
需实现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);
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() 方法通过内存复制直接创建对象,不调用构造函数,可能绕过构造函数中的安全检查或初始化逻辑。例如:
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()可能跳过这些校验。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<>()复制,防止外部列表修改影响内部状态。