找回密码
 立即注册
首页 资源区 代码 从实际编程示例中看java中对象的浅拷贝和深拷贝 ...

从实际编程示例中看java中对象的浅拷贝和深拷贝

旺财 2025-5-28 22:06:47
浅拷贝(克隆)与深拷贝(克隆)

先来看一个简单的例子,我们希望复制一个set对象,在修改这个复制对象的时候,原有的set对象不应该改变
接下来举两种复制方法,我们应该选择哪一个呢?
  1. Set<String> copiedSet = originalSet;
  2. Set<String> copiedSet = new HashSet<>(originalSet);
复制代码
显然我们应当选择第二种:

  • Set copiedSet = originalSet;使 copiedSet 和 originalSet 指向同一个内存地址,两者本质上是同一个对象的两个别名。因此可以说,我们操作 copiedSet 就是操作originalSet 。
  • Set copiedSet = new HashSet(originalSet);通过构造函数创建一个新的 HashSet 对象,我们操作 copiedSet 不会影响originalSet 。但这也仅仅只是浅拷贝
那么什么是深拷贝呢?以上例子中的Set是java自带的封装类,如果我们设定的集合中元素是自定义类且未实现深拷贝逻辑,修改元素内部属性会导致原集合和新集合共享变更
  1. class Person {
  2.     String name;
  3. }
  4. Set<Person> original = new HashSet<>();
  5. orginal.put(new Person("Alice"));
  6. Set<Person> copied = new HashSet<>(original);
  7. for(Person p:conpied){
  8.     p.name = "Bob"; // 原集合中的Person对象name也被修改
  9. }
复制代码
浅拷贝


  • 定义:仅复制对象及对象的顶层结构(如基本类型字段的值、引用类型字段的内存地址),不复制引用指向的实际对象,引用类型字段仍指向原对象的内存地址。修改其中一个对象的引用类型属性会影响另一个对象。
  • 特点:

    • 实现简单,资源消耗低。
    • 适用于只读场景或结构简单的对象。

  • 示例代码:
    1. class Person implements Cloneable {
    2.     String name;
    3.     Address address; // Address为引用类型
    4.     @Override
    5.     public Person clone() throws CloneNotSupportedException {
    6.         return (Person) super.clone(); // 默认浅拷贝
    7.     }
    8. }
    复制代码
深拷贝


  • 定义:递归复制对象及其所有引用类型字段,生成完全独立的副本。修改新对象不会影响原对象。
  • 特点:

    • 实现复杂,资源消耗高。
    • 适用于需要完全独立副本的场景(如多线程修改、数据隔离)。

  • 示例代码:
    1. class Person implements Cloneable {
    2.     String name;
    3.     Address address;
    4.     @Override
    5.     public Person clone() throws CloneNotSupportedException {
    6.         Person cloned = (Person) super.clone();
    7.         cloned.address = this.address.clone(); // 递归拷贝引用类型
    8.         return cloned;
    9.     }
    10. }
    复制代码
方式浅拷贝深拷贝复制内容仅复制对象本身及其字段的引用递归复制对象及其所有引用指向的子对象内存占用低(共享子对象)高(独立副本)修改影响原对象和拷贝对象相互影响完全独立典型实现Object.clone()(默认未修改时)手动递归复制、序列化工具、第三方库实现方式

方法浅拷贝深拷贝说明Object.clone()✔️❌默认浅拷贝,需实现Cloneable接口。手动递归拷贝❌✔️需为每个引用类型字段显式调用拷贝方法(如clone()或构造函数)。序列化与反序列化❌✔️要求所有类实现Serializable接口,利用IO流生成新对象。第三方库(如Gson/Jackson)❌✔️通过JSON序列化实现深拷贝,无需修改原有类结构。1. 浅拷贝实现


  • 默认clone()方法:
    1. class Person implements Cloneable {
    2.     String name;
    3.     List<String> hobbies;
    4.     @Override
    5.     public Person clone() {
    6.         try {
    7.             return (Person) super.clone();  // 浅拷贝
    8.         } catch (CloneNotSupportedException e) {
    9.             throw new AssertionError();
    10.         }
    11.     }
    12. }
    复制代码
2. 深拷贝实现

(1) 手动递归复制
  1. class Person implements Cloneable {
  2.     String name;
  3.     List<String> hobbies;
  4.     // 深拷贝方法
  5.     @Override
  6.     public Person clone() {
  7.         Person copy = new Person();
  8.         copy.name = this.name;//String类型,是不可变类,无需深拷贝
  9.         copy.hobbies = new ArrayList<>(this.hobbies);  // 复制List内容
  10.         return copy;
  11.     }
  12. }
复制代码
(2) 序列化与反序列化

需实现Serializable接口,借助IO流完成深拷贝:
  1. import java.io.*;
  2. public static <T> T deepCopy(T obj) throws IOException, ClassNotFoundException {
  3.     ByteArrayOutputStream bos = new ByteArrayOutputStream();
  4.     ObjectOutputStream oos = new ObjectOutputStream(bos);
  5.     oos.writeObject(obj);
  6.     ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
  7.     ObjectInputStream ois = new ObjectInputStream(bis);
  8.     return (T) ois.readObject();
  9. }
  10. // 使用
  11. Person p2 = deepCopy(p1);
复制代码
(3) 第三方工具库


  • Apache Commons Lang:
    1. import org.apache.commons.lang3.SerializationUtils;
    2. Person p2 = SerializationUtils.clone(p1);
    复制代码
  • JSON序列化(如Gson):
    1. Gson gson = new Gson();
    2. Person p2 = gson.fromJson(gson.toJson(p1), Person.class);
    复制代码
应用场景分析

按方式


  • 浅拷贝适用场景

    • 对象仅包含基本类型或不可变类(如String)。
    • 数据仅用于读取,无后续修改需求。
    • 资源敏感场景(如高频调用的简单对象)。

  • 深拷贝适用场景

    • 对象包含多层嵌套引用类型(如List)。
    • 需要副本与原对象完全隔离(如缓存数据副本、事务回滚)。
    • 多线程环境下操作独立数据。

按对象


  • 简单对象
    使用Cloneable接口重写clone()方法(浅拷贝)或手动递归(深拷贝)。
  • 复杂对象
    优先选择序列化(需Serializable接口)或第三方库(如Apache Commons Lang、Gson)。
注意事项


  • 引用类型递归问题
    深拷贝需确保所有嵌套的引用类型均实现拷贝逻辑,否则可能残留浅拷贝链路。
    1. // 错误示例:Address未实现深拷贝
    2. class Person {
    3.     Address address;
    4.     public Person clone() {
    5.         Person cloned = new Person();
    6.         cloned.address = this.address; // address不是不可变类,仍然是浅拷贝
    7.         //这行代码直接将原对象this.address的引用赋值给了新对象cloned.address,导致两个Person对象的address字段指向同一个Address实例。因此,修改任一Person对象的address属性时,另一个对象的address也会被同步修改
    8.         return cloned;
    9.     }
    10. }
    复制代码
    我们需要修改为:
    1. class Address implements Cloneable {//首先把Address的拷贝逻辑修改正确
    2.     private String city;//不可变对象
    3.     @Override
    4.     public Address clone() {
    5.         try {
    6.             return (Address) super.clone();  // 若字段均为基本类型或不可变对象,则已足够,如果还有引用还需要继续嵌套
    7.         } catch (CloneNotSupportedException e) {
    8.             throw new AssertionError();
    9.         }
    10.     }
    11. }
    12. class Person {
    13.     Address address;
    14.     public Person clone() {
    15.         Person cloned = new Person();
    16.         cloned.address = this.address.clone();  // 调用Address的深拷贝方法
    17.         return cloned;
    18.         }
    19. }
    复制代码
  • 性能与复杂度

    • 深拷贝的递归层级越深,性能开销越大。(上面的代码以可以看出)
    • 序列化方式可能因对象复杂度导致效率低下。

  • 若引用类型为不可变类(如String、Integer),可直接赋值,无需深拷贝。
    知识补充:不可变类(Immutable Class)
    不可变类是指实例一旦创建后,其状态(字段值)不能被修改的类。Java中的不可变类具有线程安全、缓存优化等优势。
    不可变类的特点

    • 状态不可变:所有字段在对象创建后不可修改。
    • 线程安全:无需同步机制,多线程共享时无竞态条件。
    • 哈希稳定:对象的hashCode()在生命周期内不变,适合作为HashMap的键。
    识别方法

    • 类声明为final:防止子类覆盖方法破坏不可变性。
    • 字段为private final:确保字段只能在构造函数中初始化。
    • 无setter方法:避免外部修改字段值。
    • 防御拷贝:如果类包含字段是引用类型(如集合),那么这些引用也应该被封装为不可变对象或提供只读的访问方式。
      1. public final class ImmutablePerson {
      2.     private final List<String> hobbies;
      3.     public ImmutablePerson(List<String> hobbies) {
      4.         this.hobbies = new ArrayList<>(hobbies); // 深拷贝
      5.     }
      6.     public List<String> getHobbies() {
      7.         return Collections.unmodifiableList(hobbies); // 返回不可修改集合
      8.     }
      9. }
      复制代码
    常见不可变类示例

    • 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()方法的潜在漏洞。
    浅拷贝导致数据污染风险
    1. class User implements Cloneable {
    2.     private List<String> permissions = new ArrayList<>();
    3.     public User clone() { return super.clone(); } // 浅拷贝
    4. }
    5. User user1 = new User(Arrays.asList("read"));
    6. User user2 = user1.clone();
    7. user2.getPermissions().add("delete"); // 原user1的permissions也被修改
    复制代码
    Cloneable接口的脆弱性​​
    Cloneable 是一个空标记接口,未强制要求类实现 clone() 方法。若开发者未正确覆盖 clone() 方法,可能导致以下问题:

    • 未受控的克隆行为​​:攻击者可通过继承并覆写 clone() 方法,绕过安全校验直接复制对象。
    • 异常处理漏洞​​:默认 clone() 可能抛出 CloneNotSupportedException,若未妥善处理,程序可能因异常中断或暴露内部状态。
    绕过构造函数的安全校验
    对象初始化不可控:clone() 方法通过内存复制直接创建对象,不调用构造函数,可能绕过构造函数中的安全检查或初始化逻辑。例如:

    • 若构造函数包含权限验证、加密初始化等逻辑,克隆对象可能处于未经验证的状态
    1. class SecureConfig {
    2.     private String key = generateEncryptedKey(); // 构造函数中生成密钥
    3.     public SecureConfig() { validateKey(key); }  // 密钥校验逻辑
    4.     // 若通过clone()复制,密钥可能未经验证
    5. }
    复制代码
    多线程环境下状态不一致风险

    • 若多个线程同时克隆一个可变对象,且克隆过程未同步,可能导致克隆后的对象处于中间状态(如部分字段已更新、部分未更新),破坏数据一致性
    深拷贝实现的复杂性

    • 递归深拷贝的遗漏风险:手动实现 clone() 方法时,需递归处理所有引用类型字段。若遗漏某一层级,可能导致深拷贝不彻底,残留共享引用。
    推荐:构造函数逐层复制
    场景一:在需要保护用户隐私数据的系统中,若直接使用clone()方法,可能因浅拷贝导致地址信息被篡改。通过构造函数逐层复制可确保数据独立性:
    1. // 地址类(包含敏感地理位置信息)
    2. class SecureAddress {
    3.     private final String city;  // 使用final增强不可变性
    4.     private final String gpsCoordinate;
    5.     // 原始构造函数(含数据校验)
    6.     public SecureAddress(String city, String gps) {
    7.         validateGPS(gps);  // 高安全场景中的校验逻辑
    8.         this.city = city;
    9.         this.gpsCoordinate = gps;
    10.     }
    11.     // 复制构造函数(逐层深拷贝)
    12.     public SecureAddress(SecureAddress other) {
    13.         this(other.city, other.gpsCoordinate);  // 调用原始构造函数执行校验
    14.     }
    15.     private void validateGPS(String gps) {
    16.         if (!gps.matches("^\\d+°\\d+'\\d+" [NS] \\d+°\\d+'\\d+" [EW]$"))
    17.             throw new SecurityException("非法GPS格式");
    18.     }
    19. }
    20. // 用户类(包含敏感地址信息)
    21. class SecureUser {
    22.     private final String id;
    23.     private final SecureAddress address;
    24.     // 原始构造函数(含身份验证)
    25.     public SecureUser(String id, SecureAddress address) {
    26.         validateID(id);  // 身份ID格式校验
    27.         this.id = id;
    28.         this.address = new SecureAddress(address);  // 防御性拷贝
    29.     }
    30.     // 复制构造函数(逐层调用)
    31.     public SecureUser(SecureUser other) {
    32.         this(other.id, new SecureAddress(other.address));  // 递归调用SecureAddress的复制构造
    33.     }
    34.     private void validateID(String id) {
    35.         if (!id.matches("^[A-Z]\\d{9}$"))
    36.             throw new SecurityException("非法用户ID");
    37.     }
    38. }
    复制代码
    安全性优势

    • 绕过clone()校验漏洞:直接调用构造函数时,会触发validateGPS()和validateID()校验逻辑,而clone()可能跳过这些校验。
    • 防御拷贝:在原始构造函数中对address进行拷贝,防止外部引用篡改(如通过setAddress()注入非法数据)。
    • 不可变设计:字段使用final修饰,防止对象创建后被修改。
    场景二:在密钥管理系统(KMS)中,加密配置需要确保密钥和算法参数的绝对隔离:
    1. // 加密算法参数(含敏感密钥)
    2. class CryptoParams {
    3.     private final byte[] secretKey;
    4.     private final String algorithm;
    5.     // 原始构造函数(密钥加密存储)
    6.     public CryptoParams(byte[] key, String algo) {
    7.         this.secretKey = encryptKey(key);  // 内存加密处理
    8.         this.algorithm = algo;
    9.     }
    10.     // 复制构造函数(深拷贝字节数组)
    11.     public CryptoParams(CryptoParams other) {
    12.         this.secretKey = Arrays.copyOf(other.secretKey, other.secretKey.length);
    13.         this.algorithm = other.algorithm;
    14.     }
    15.     private byte[] encryptKey(byte[] rawKey) {
    16.         // 使用硬件安全模块(HSM)加密密钥
    17.         return HSM.encrypt(rawKey);
    18.     }
    19. }
    20. // 系统安全配置(嵌套多层对象)
    21. class SecurityConfig {
    22.     private final CryptoParams params;
    23.     private final List<String> accessIPs;
    24.     // 原始构造函数(IP白名单过滤)
    25.     public SecurityConfig(CryptoParams params, List<String> ips) {
    26.         this.params = new CryptoParams(params);  // 深拷贝CryptoParams
    27.         this.accessIPs = new ArrayList<>(filterIPs(ips));  // 防御性拷贝+过滤
    28.     }
    29.     // 复制构造函数(逐层复制)
    30.     public SecurityConfig(SecurityConfig other) {
    31.         this(new CryptoParams(other.params), new ArrayList<>(other.accessIPs));
    32.     }
    33.     private List<String> filterIPs(List<String> ips) {
    34.         return ips.stream().filter(ip ->
    35.                                      isValidIP(ip)).collect(Collectors.toList());
    36.     }
    37. }
    复制代码
    安全性优势

    • 内存安全:通过Arrays.copyOf()复制密钥字节数组,避免clone()可能遗留的数组引用。
    • 数据过滤:构造函数中调用filterIPs()实现输入校验,而clone()无法自动执行此类逻辑。
    • 防御集合拷贝:对accessIPs进行new ArrayList()复制,防止外部列表修改影响内部状态。


来源:新程序网络收集,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册