从实际编程示例中看java中对象的浅拷贝和深拷贝
<h3 id="浅拷贝克隆与深拷贝克隆">浅拷贝(克隆)与深拷贝(克隆)</h3><p>先来看一个简单的例子,我们希望<code>复制一个set对象,在修改这个复制对象的时候,原有的set对象不应该改变</code></p>
<p>接下来举两种复制方法,我们应该选择哪一个呢?</p>
<pre><code class="language-java">Set<String> copiedSet = originalSet;
Set<String> copiedSet = new HashSet<>(originalSet);
</code></pre>
<p>显然我们应当选择第二种:</p>
<ul>
<li>
<p><code>Set<String> copiedSet = originalSet;</code>使 <code>copiedSet</code> 和 <code>originalSet</code> <strong>指向同一个内存地址</strong>,两者本质上是同一个对象的两个别名。因此可以说,我们操作 <code>copiedSet</code> 就是操作<code>originalSet</code> 。</p>
</li>
<li>
<p><code>Set<String> copiedSet = new HashSet<>(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<Person> original = new HashSet<>();
orginal.put(new Person("Alice"));
Set<Person> copied = new HashSet<>(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<String> 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<String> hobbies;
// 深拷贝方法
@Override
public Person clone() {
Person copy = new Person();
copy.name = this.name;//String类型,是不可变类,无需深拷贝
copy.hobbies = new ArrayList<>(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 <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);
</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<Map<String, Object>></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<String> hobbies;
public ImmutablePerson(List<String> hobbies) {
this.hobbies = new ArrayList<>(hobbies); // 深拷贝
}
public List<String> 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<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也被修改
</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<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());
}
}
</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<>()</code>复制,防止外部列表修改影响内部状态。</li>
</ul>
</li>
</ol><br><br>
来源:https://www.cnblogs.com/PJRAWA/p/18901080
頁:
[1]