金典型造旺仔 發表於 2025-5-28 18:36:00

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

<h3 id="浅拷贝克隆与深拷贝克隆">浅拷贝(克隆)与深拷贝(克隆)</h3>
<p>先来看一个简单的例子,我们希望<code>复制一个set对象,在修改这个复制对象的时候,原有的set对象不应该改变</code></p>
<p>接下来举两种复制方法,我们应该选择哪一个呢?</p>
<pre><code class="language-java">Set&lt;String&gt; copiedSet = originalSet;
Set&lt;String&gt; copiedSet = new HashSet&lt;&gt;(originalSet);
</code></pre>
<p>显然我们应当选择第二种:</p>
<ul>
<li>
<p><code>Set&lt;String&gt; copiedSet = originalSet;</code>使 <code>copiedSet</code> 和 <code>originalSet</code> <strong>指向同一个内存地址</strong>,两者本质上是同一个对象的两个别名。因此可以说,我们操作 <code>copiedSet</code> 就是操作<code>originalSet</code> 。</p>
</li>
<li>
<p><code>Set&lt;String&gt; copiedSet = new HashSet&lt;&gt;(originalSet);</code>通过构造函数创建一个<strong>新的 <code>HashSet</code> 对象</strong>,我们操作 <code>copiedSet</code> 不会影响<code>originalSet</code> 。但这也仅仅只是<strong>浅拷贝</strong>。</p>
<hr>
</li>
</ul>
<p>那么什么是深拷贝呢?以上例子中的<code>Set</code>是java自带的封装类,如果<mark>我们设定的集合中元素是自定义类且未实现深拷贝逻辑,修改元素内部属性会导致原集合和新集合共享变更</mark></p>
<pre><code class="language-java">class Person {
    String name;
}

Set&lt;Person&gt; original = new HashSet&lt;&gt;();
orginal.put(new Person("Alice"));
Set&lt;Person&gt; copied = new HashSet&lt;&gt;(original);
for(Person p:conpied){
    p.name = "Bob"; // 原集合中的Person对象name也被修改
}
</code></pre>
<h4 id="浅拷贝">浅拷贝</h4>
<ul>
<li>
<p><strong>定义</strong>:仅复制<mark>对象及对象的顶层结构(如基本类型字段的值、引用类型字段的<strong>内存地址</strong>),不复制引用指向的实际对象</mark>,引用类型字段仍指向原对象的内存地址。修改其中一个对象的引用类型属性会影响另一个对象。</p>
</li>
<li>
<p>特点:</p>
<ul>
<li>实现简单,资源消耗低。</li>
<li>适用于<strong>只读场景</strong>或结构简单的对象。</li>
</ul>
</li>
<li>
<p>示例代码:</p>
<pre><code class="language-java">class Person implements Cloneable {
    String name;
    Address address; // Address为引用类型
    @Override
    public Person clone() throws CloneNotSupportedException {
      return (Person) super.clone(); // 默认浅拷贝
    }
}
</code></pre>
</li>
</ul>
<h4 id="深拷贝">深拷贝</h4>
<ul>
<li>
<p><strong>定义</strong>:递归复制<mark>对象及其所有引用类型字段</mark>,生成完全独立的副本。修改新对象不会影响原对象。</p>
</li>
<li>
<p>特点:</p>
<ul>
<li>实现复杂,资源消耗高。</li>
<li>适用于需要<strong>完全独立副本</strong>的场景(如多线程修改、数据隔离)。</li>
</ul>
</li>
<li>
<p>示例代码:</p>
<pre><code class="language-java">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;
    }
}
</code></pre>
<hr>
</li>
</ul>
<table>
<thead>
<tr>
<th style="text-align: center"><strong>方式</strong></th>
<th style="text-align: center"><strong>浅拷贝</strong></th>
<th style="text-align: center"><strong>深拷贝</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center"><strong>复制内容</strong></td>
<td style="text-align: center">仅复制对象本身及其字段的引用</td>
<td style="text-align: center">递归复制对象及其所有引用指向的子对象</td>
</tr>
<tr>
<td style="text-align: center"><strong>内存占用</strong></td>
<td style="text-align: center">低(共享子对象)</td>
<td style="text-align: center">高(独立副本)</td>
</tr>
<tr>
<td style="text-align: center"><strong>修改影响</strong></td>
<td style="text-align: center">原对象和拷贝对象相互影响</td>
<td style="text-align: center">完全独立</td>
</tr>
<tr>
<td style="text-align: center"><strong>典型实现</strong></td>
<td style="text-align: center"><code>Object.clone()</code>(默认未修改时)</td>
<td style="text-align: center">手动递归复制、序列化工具、第三方库</td>
</tr>
</tbody>
</table>
<hr>
<h4 id="实现方式">实现方式</h4>
<table>
<thead>
<tr>
<th><strong>方法</strong></th>
<th><strong>浅拷贝</strong></th>
<th><strong>深拷贝</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Object.clone()</code></td>
<td>✔️</td>
<td>❌</td>
<td>默认浅拷贝,需实现<code>Cloneable</code>接口。</td>
</tr>
<tr>
<td>手动递归拷贝</td>
<td>❌</td>
<td>✔️</td>
<td>需为每个引用类型字段显式调用拷贝方法(如<code>clone()</code>或构造函数)。</td>
</tr>
<tr>
<td>序列化与反序列化</td>
<td>❌</td>
<td>✔️</td>
<td>要求所有类实现<code>Serializable</code>接口,利用IO流生成新对象。</td>
</tr>
<tr>
<td>第三方库(如Gson/Jackson)</td>
<td>❌</td>
<td>✔️</td>
<td>通过JSON序列化实现深拷贝,无需修改原有类结构。</td>
</tr>
</tbody>
</table>
<h5 id="1-浅拷贝实现"><strong>1. 浅拷贝实现</strong></h5>
<ul>
<li>
<p>默认<code>clone()</code>方法:</p>
<pre><code class="language-Java">class Person implements Cloneable {
    String name;
    List&lt;String&gt; hobbies;

    @Override
    public Person clone() {
      try {
            return (Person) super.clone();// 浅拷贝
      } catch (CloneNotSupportedException e) {
            throw new AssertionError();
      }
    }
}
</code></pre>
</li>
</ul>
<h5 id="2-深拷贝实现"><strong>2. 深拷贝实现</strong></h5>
<h6 id="1-手动递归复制"><strong>(1) 手动递归复制</strong></h6>
<pre><code class="language-Java">class Person implements Cloneable {
    String name;
    List&lt;String&gt; hobbies;

    // 深拷贝方法
    @Override
    public Person clone() {
      Person copy = new Person();
      copy.name = this.name;//String类型,是不可变类,无需深拷贝
      copy.hobbies = new ArrayList&lt;&gt;(this.hobbies);// 复制List内容
      return copy;
    }
}
</code></pre>
<h6 id="2-序列化与反序列化"><strong>(2) 序列化与反序列化</strong></h6>
<p>需实现<code>Serializable</code>接口,借助IO流完成深拷贝:</p>
<pre><code class="language-Java">import java.io.*;

public static &lt;T&gt; 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);
</code></pre>
<h6 id="3-第三方工具库"><strong>(3) 第三方工具库</strong></h6>
<ul>
<li>
<p>Apache Commons Lang:</p>
<pre><code class="language-java">import org.apache.commons.lang3.SerializationUtils;
Person p2 = SerializationUtils.clone(p1);
</code></pre>
</li>
<li>
<p>JSON序列化(如Gson):</p>
<pre><code class="language-java">Gson gson = new Gson();
Person p2 = gson.fromJson(gson.toJson(p1), Person.class);
</code></pre>
</li>
</ul>
<hr>
<h4 id="应用场景分析">应用场景分析</h4>
<h5 id="按方式">按方式</h5>
<ol>
<li><strong>浅拷贝适用场景</strong>
<ul>
<li>对象仅包含基本类型或不可变类(如<code>String</code>)。</li>
<li>数据仅用于读取,无后续修改需求。</li>
<li>资源敏感场景(如高频调用的简单对象)。</li>
</ul>
</li>
<li><strong>深拷贝适用场景</strong>
<ul>
<li>对象包含多层嵌套引用类型(如<code>List&lt;Map&lt;String, Object&gt;&gt;</code>)。</li>
<li>需要副本与原对象完全隔离(如缓存数据副本、事务回滚)。</li>
<li>多线程环境下操作独立数据。</li>
</ul>
</li>
</ol>
<h5 id="按对象">按对象</h5>
<ol>
<li><strong>简单对象</strong><br>
使用<code>Cloneable</code>接口重写<code>clone()</code>方法(浅拷贝)或手动递归(深拷贝)。</li>
<li><strong>复杂对象</strong><br>
优先选择序列化(需<code>Serializable</code>接口)或第三方库(如Apache Commons Lang、Gson)。</li>
</ol>
<hr>
<h4 id="注意事项">注意事项</h4>
<ol>
<li>
<p><strong>引用类型递归问题</strong><br>
深拷贝需确保所有嵌套的引用类型均实现拷贝逻辑,否则可能残留浅拷贝链路。</p>
<pre><code class="language-java">// 错误示例: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;
    }
}
</code></pre>
<p>我们需要修改为:</p>
<pre><code class="language-java">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;
        }
}
</code></pre>
</li>
<li>
<p><strong>性能与复杂度</strong></p>
<ul>
<li>深拷贝的递归层级越深,性能开销越大。(上面的代码以可以看出)</li>
<li>序列化方式可能因对象复杂度导致效率低下。</li>
</ul>
</li>
<li>
<p>若引用类型为不可变类(如<code>String</code>、<code>Integer</code>),可直接赋值,无需深拷贝。</p>
<p><strong>知识补充:不可变类(Immutable Class)</strong></p>
<p>不可变类是指实例一旦创建后,<u>其状态(字段值)不能被修改的类</u>。Java中的不可变类具有线程安全、缓存优化等优势。</p>
<p><strong>不可变类的特点</strong></p>
<ul>
<li>状态不可变:所有字段在对象创建后不可修改。</li>
<li>线程安全:无需同步机制,多线程共享时无竞态条件。</li>
<li>哈希稳定:对象的<code>hashCode()</code>在生命周期内不变,适合作为<code>HashMap</code>的键。</li>
</ul>
<p><strong>识别方法</strong></p>
<ul>
<li>
<p>类声明为<code>final</code>:防止子类覆盖方法破坏不可变性。</p>
</li>
<li>
<p>字段为<code>private final</code>:确保字段只能在构造函数中初始化。</p>
</li>
<li>
<p>无<code>setter</code>方法:避免外部修改字段值。</p>
</li>
<li>
<p>防御拷贝:如果类包含字段是引用类型(如集合),那么这些引用也应该被封装为不可变对象或提供只读的访问方式。</p>
<pre><code class="language-java">public final class ImmutablePerson {
    private final List&lt;String&gt; hobbies;
    public ImmutablePerson(List&lt;String&gt; hobbies) {
      this.hobbies = new ArrayList&lt;&gt;(hobbies); // 深拷贝
    }
    public List&lt;String&gt; getHobbies() {
      return Collections.unmodifiableList(hobbies); // 返回不可修改集合
    }
}
</code></pre>
</li>
</ul>
<p><strong>常见不可变类示例</strong></p>
<ul>
<li>
<p><code>String</code>:当你对String对象进行修改时(如拼接操作),实际上Java会创建一个新的String对象,而不是修改原有的对象。</p>
</li>
<li>
<p>基本类型的包装类:</p>
<p>Java的八个基本数据类型(<code>byte</code>, <code>short</code>,<code> int</code>, <code>long</code>, <code>float</code>, <code>double</code>, <code>char</code>, <code>boolean</code>)的包装类(<code>Byte</code>,<code> Short</code>, <code>Integer</code>, <code>Long</code>, <code>Float</code>, <code>Double</code>, <code>Character</code>, <code>Boolean</code>)都是不可变的。这意味着当你创建一个这些类型的对象后,你不能改变其内部的值。</p>
</li>
<li>
<p>Java 8时间API:<code>LocalDate</code>、<code>ZonedDateTime</code>。</p>
</li>
<li>
<p>不可变的集合类</p>
<p>Java集合框架提供了一些不可变的集合实现,如<code>Collections.unmodifiableList()</code>、<code>Collections.unmodifiableSet()</code>等。这些方法返回的是原有集合的不可变视图,任何对它们的修改操作都会抛出<code>UnsupportedOperationException</code>异常。</p>
</li>
<li>
<p>枚举类</p>
<p>在Java中,大多数枚举类也是不可变的。枚举类型的实例在JVM中只有一个,且不能被修改。</p>
</li>
<li>
<p>其他</p>
<p>Java中还有其他一些常用的不可变类,如BigDecimal、BigInteger等。此外,<code>java.lang.StackTraceElement</code>用于构建异常的堆栈跟踪,也是不可变的。</p>
</li>
</ul>
</li>
<li>
<p><strong>高安全性场景</strong>:采用<mark>构造函数逐层复制</mark>,避免依赖<code>clone()</code>方法的潜在漏洞。</p>
<p><strong>浅拷贝导致数据污染风险</strong></p>
<pre><code class="language-java">class User implements Cloneable {
    private List&lt;String&gt; permissions = new ArrayList&lt;&gt;();
    public User clone() { return super.clone(); } // 浅拷贝
}
User user1 = new User(Arrays.asList("read"));
User user2 = user1.clone();
user2.getPermissions().add("delete"); // 原user1的permissions也被修改
</code></pre>
<p><strong>Cloneable接口的脆弱性​​</strong><br>
Cloneable 是一个空标记接口,未强制要求类实现 clone() 方法。若开发者未正确覆盖 clone() 方法,可能导致以下问题:</p>
<ul>
<li>未受控的克隆行为​​:攻击者可通过继承并覆写 clone() 方法,绕过安全校验直接复制对象。</li>
<li>异常处理漏洞​​:默认 clone() 可能抛出 CloneNotSupportedException,若未妥善处理,程序可能因异常中断或暴露内部状态。</li>
</ul>
<p><strong>绕过构造函数的安全校验</strong></p>
<p>对象初始化不可控:<code>clone()</code> 方法通过内存复制直接创建对象,<strong>不调用构造函数</strong>,可能绕过构造函数中的安全检查或初始化逻辑。例如:</p>
<ul>
<li>若构造函数包含权限验证、加密初始化等逻辑,克隆对象可能处于未经验证的状态</li>
</ul>
<pre><code class="language-java">class SecureConfig {
    private String key = generateEncryptedKey(); // 构造函数中生成密钥
    public SecureConfig() { validateKey(key); }// 密钥校验逻辑
    // 若通过clone()复制,密钥可能未经验证
}
</code></pre>
<p><strong>多线程环境下状态不一致风险</strong></p>
<ul>
<li>若多个线程同时克隆一个可变对象,且克隆过程未同步,可能导致克隆后的对象处于<strong>中间状态</strong>(如部分字段已更新、部分未更新),破坏数据一致性</li>
</ul>
<p><strong>深拷贝实现的复杂性</strong></p>
<ul>
<li><strong>递归深拷贝的遗漏风险</strong>:手动实现 <code>clone()</code> 方法时,需递归处理所有引用类型字段。若遗漏某一层级,可能导致深拷贝不彻底,残留共享引用。</li>
</ul>
<p><mark>推荐:构造函数逐层复制</mark></p>
<p>场景一:在需要保护用户隐私数据的系统中,若直接使用<code>clone()</code>方法,可能因浅拷贝导致地址信息被篡改。通过构造函数逐层复制可确保数据独立性:</p>
<pre><code class="language-java">// 地址类(包含敏感地理位置信息)
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+\" \\d+°\\d+'\\d+\" $"))
            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("^\\d{9}$"))
            throw new SecurityException("非法用户ID");
    }
}
</code></pre>
<p><strong>安全性优势</strong>:</p>
<ul>
<li>绕过<code>clone()</code>校验漏洞:直接调用构造函数时,会触发<code>validateGPS()</code>和<code>validateID()</code>校验逻辑,而clone()可能跳过这些校验。</li>
<li>防御拷贝:在原始构造函数中对address进行拷贝,防止外部引用篡改(如通过<code>setAddress()</code>注入非法数据)。</li>
<li>不可变设计:字段使用<code>final</code>修饰,防止对象创建后被修改。</li>
</ul>
<p>场景二:在密钥管理系统(KMS)中,加密配置需要确保密钥和算法参数的绝对隔离:</p>
<pre><code class="language-java">// 加密算法参数(含敏感密钥)
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&lt;String&gt; accessIPs;

    // 原始构造函数(IP白名单过滤)
    public SecurityConfig(CryptoParams params, List&lt;String&gt; ips) {
      this.params = new CryptoParams(params);// 深拷贝CryptoParams
      this.accessIPs = new ArrayList&lt;&gt;(filterIPs(ips));// 防御性拷贝+过滤
    }

    // 复制构造函数(逐层复制)
    public SecurityConfig(SecurityConfig other) {
      this(new CryptoParams(other.params), new ArrayList&lt;&gt;(other.accessIPs));
    }

    private List&lt;String&gt; filterIPs(List&lt;String&gt; ips) {
      return ips.stream().filter(ip -&gt;
                                     isValidIP(ip)).collect(Collectors.toList());
    }
}
</code></pre>
<p><strong>安全性优势</strong>:</p>
<ul>
<li>内存安全:通过<code>Arrays.copyOf()</code>复制密钥字节数组,避免<code>clone()</code>可能遗留的数组引用。</li>
<li>数据过滤:构造函数中调用<code>filterIPs()</code>实现输入校验,而<code>clone()</code>无法自动执行此类逻辑。</li>
<li>防御集合拷贝:对<code>accessIPs</code>进行<code>new ArrayList&lt;&gt;()</code>复制,防止外部列表修改影响内部状态。</li>
</ul>
</li>
</ol><br><br>
来源:https://www.cnblogs.com/PJRAWA/p/18901080
頁: [1]
查看完整版本: 从实际编程示例中看java中对象的浅拷贝和深拷贝