欢迎访问悦橙教程(wld5.com),关注java教程。悦橙教程  java问答|  每日更新
页面导航 : > > 文章正文

从实际编程示例中看java中对象的浅拷贝和深拷贝,因此可以说,我们操作

来源: javaer 分享于  点击 20493 次 点评:111

从实际编程示例中看java中对象的浅拷贝和深拷贝,因此可以说,我们操作


浅拷贝(克隆)与深拷贝(克隆)

先来看一个简单的例子,我们希望复制一个set对象,在修改这个复制对象的时候,原有的set对象不应该改变

接下来举两种复制方法,我们应该选择哪一个呢?

Set<String> copiedSet = originalSet;
Set<String> copiedSet = new HashSet<>(originalSet);

显然我们应当选择第二种:

  • Set<String> copiedSet = originalSet;使 copiedSetoriginalSet 指向同一个内存地址,两者本质上是同一个对象的两个别名。因此可以说,我们操作 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);
    

应用场景分析

按方式
  1. 浅拷贝适用场景
    • 对象仅包含基本类型或不可变类(如String)。
    • 数据仅用于读取,无后续修改需求。
    • 资源敏感场景(如高频调用的简单对象)。
  2. 深拷贝适用场景
    • 对象包含多层嵌套引用类型(如List<Map<String, Object>>)。
    • 需要副本与原对象完全隔离(如缓存数据副本、事务回滚)。
    • 多线程环境下操作独立数据。
按对象
  1. 简单对象
    使用Cloneable接口重写clone()方法(浅拷贝)或手动递归(深拷贝)。
  2. 复杂对象
    优先选择序列化(需Serializable接口)或第三方库(如Apache Commons Lang、Gson)。

注意事项

  1. 引用类型递归问题
    深拷贝需确保所有嵌套的引用类型均实现拷贝逻辑,否则可能残留浅拷贝链路。

    // 错误示例: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;
    	}
    }
    
  2. 性能与复杂度

    • 深拷贝的递归层级越深,性能开销越大。(上面的代码以可以看出)
    • 序列化方式可能因对象复杂度导致效率低下。
  3. 若引用类型为不可变类(如StringInteger),可直接赋值,无需深拷贝。

    知识补充:不可变类(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:LocalDateZonedDateTime

    • 不可变的集合类

      Java集合框架提供了一些不可变的集合实现,如Collections.unmodifiableList()Collections.unmodifiableSet()等。这些方法返回的是原有集合的不可变视图,任何对它们的修改操作都会抛出UnsupportedOperationException异常。

    • 枚举类

      在Java中,大多数枚举类也是不可变的。枚举类型的实例在JVM中只有一个,且不能被修改。

    • 其他

      Java中还有其他一些常用的不可变类,如BigDecimal、BigInteger等。此外,java.lang.StackTraceElement用于构建异常的堆栈跟踪,也是不可变的。

  4. 高安全性场景:采用构造函数逐层复制,避免依赖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<>()复制,防止外部列表修改影响内部状态。
相关栏目:

用户点评