基础篇六 改了克隆对象的属性,原对象也跟着变了?你被浅克隆坑过吗

张开发
2026/4/21 3:43:28 15 分钟阅读

分享文章

基础篇六 改了克隆对象的属性,原对象也跟着变了?你被浅克隆坑过吗
文章目录一、先说结论一句话区分二、浅克隆看似复制实则共用怎么实现翻车现场为什么会这样三、深克隆真正的独立复制方式一手动 clone手动挡方式二序列化自动挡之一方式三第三方库自动挡之二四、三种方式怎么选五、一个容易被忽略的坑集合的克隆六、构造方法拷贝最容易被遗忘的方案七、回到全貌一张图搞定八、面试速答模板个人网站你花了一下午 clone 了一个对象美滋滋地改了改属性回头一看——原对象也变了那种感觉就像你复印了一份合同在复印件上涂涂改改结果原件上也跟着出现了涂改痕迹。离谱吗但在 Java 的浅克隆世界里这就是日常。今天我们就来把深克隆和浅克隆这件事彻底讲透保证你以后再也不会被坑。一、先说结论一句话区分浅克隆深克隆基本类型复制值 ✅复制值 ✅引用类型复制地址共用同一个对象⚠️复制对象各自独立✅浅克隆只复制门牌号深克隆才是搬家。用一个生活场景秒懂浅克隆 你抄了朋友家地址贴在自己门上。你俩门牌号一样打开门进的是同一间房。你把沙发搬走朋友回家也找不到沙发。深克隆 你照着朋友家的户型重新盖了一模一样的房子。你把沙发搬走朋友家沙发还在。二、浅克隆看似复制实则共用怎么实现实现Cloneable接口重写clone()方法classAddress{Stringcity;Address(Stringcity){this.citycity;}}classPersonimplementsCloneable{Stringname;Addressaddress;Person(Stringname,Addressaddress){this.namename;this.addressaddress;}OverrideprotectedObjectclone()throwsCloneNotSupportedException{returnsuper.clone();// 浅克隆}}翻车现场AddressaddrnewAddress(北京);Personp1newPerson(张三,addr);Personp2(Person)p1.clone();// 改克隆对象的引用类型属性p2.address.city上海;// 原对象也被改了System.out.println(p1.address.city);// 上海不是北京System.out.println(p2.address.city);// 上海// 但是基本类型没问题p2.name李四;System.out.println(p1.name);// 张三没变System.out.println(p2.name);// 李四为什么会这样内存长这样浅克隆后 栈 堆 ┌─────┐ ┌──────────────┐ │ p1 │────→ │ Person │ └─────┘ │ name张三 │ │ address ─────┼──→ ┌──────────────┐ ┌─────┐ └──────────────┘ │ Address │ │ p2 │────→ │ Person │ │ city北京 │ └─────┘ │ name张三 │ └──────────────┘ │ address ─────┼──→ 同一个 Address └──────────────┘两个 Person 的address字段指向堆上同一个 Address 对象。改了一个另一个当然跟着变。而基本类型name是直接存在 Person 对象里的clone 时复制了值所以互不影响。三、深克隆真正的独立复制深克隆要让引用类型也各自独立。实现方式有三种从手动挡到自动挡逐级升级。方式一手动 clone手动挡每层引用类型都实现Cloneable并重写clone()classAddressimplementsCloneable{Stringcity;Address(Stringcity){this.citycity;}OverrideprotectedObjectclone()throwsCloneNotSupportedException{returnsuper.clone();// Address 没有引用类型字段浅克隆就行}}classPersonimplementsCloneable{Stringname;Addressaddress;OverrideprotectedObjectclone()throwsCloneNotSupportedException{Personp(Person)super.clone();p.address(Address)address.clone();// 手动克隆引用类型returnp;}}AddressaddrnewAddress(北京);Personp1newPerson(张三,addr);Personp2(Person)p1.clone();p2.address.city上海;System.out.println(p1.address.city);// 北京 ✅ 原对象没变System.out.println(p2.address.city);// 上海缺点对象嵌套深的时候你要一层一层手动 clone写到手抽筋。引用类型多一个字段就要多写一行 clone 代码漏了就出 Bug。方式二序列化自动挡之一把对象冻成字节流再解冻成新对象天然实现深克隆importjava.io.*;classAddressimplementsSerializable{Stringcity;Address(Stringcity){this.citycity;}}classPersonimplementsSerializable{Stringname;Addressaddress;// 通用深克隆方法publicPersondeepClone()throwsException{ByteArrayOutputStreambosnewByteArrayOutputStream();ObjectOutputStreamoosnewObjectOutputStream(bos);oos.writeObject(this);// 把自己写入字节流ByteArrayInputStreambisnewByteArrayInputStream(bos.toByteArray());ObjectInputStreamoisnewObjectInputStream(bis);return(Person)ois.readObject();// 从字节流读出新对象}}原理对象 → 字节流完整快照 → 新对象中间没有任何引用共享的可能。缺点所有涉及的类都要实现Serializable且序列化/反序列化有性能开销。方式三第三方库自动挡之二用 JSON 库或工具库一行搞定// GsonPersonp2newGson().fromJson(newGson().toJson(p1),Person.class);// JacksonPersonp2newObjectMapper().readValue(newObjectMapper().writeValueAsString(p1),Person.class);// Apache Commons LangPersonp2SerializationUtils.clone(p1);// 需要实现 Serializable// Spring BeanUtils注意这是浅拷贝// BeanUtils.copyProperties(p1, p2); // ❌ 不是深克隆注意BeanUtils.copyProperties()是浅拷贝别用错了四、三种方式怎么选方式优点缺点适用场景手动 clone精准控制性能最好嵌套深时代码量大容易漏字段结构简单的对象序列化通用不需要逐字段处理所有类要实现 Serializable性能一般嵌套较深、不追求极致性能第三方库一行搞定最省事引入额外依赖JSON 方式对特殊类型支持有限快速开发、原型验证口诀简单对象手 clone深层嵌套走序列化图省事上工具库。五、一个容易被忽略的坑集合的克隆ListPersonlist1newArrayList();list1.add(newPerson(张三));// 浅克隆ListPersonlist2newArrayList(list1);// 或者 list1.clone()list2.get(0).name李四;System.out.println(list1.get(0).name);// 李四 又被改了new ArrayList(list1)只是创建了一个新 List但里面的元素还是指向原来的对象。集合克隆是浅的元素克隆要自己处理。// ✅ 集合深克隆ListPersonlist2list1.stream().map(p-(Person)p.clone()).collect(Collectors.toList());六、构造方法拷贝最容易被遗忘的方案除了 clone其实还有一个朴实无华的方式——拷贝构造方法classAddress{Stringcity;Address(Stringcity){this.citycity;}// 拷贝构造方法Address(Addressother){this.cityother.city;}}classPerson{Stringname;Addressaddress;Person(Stringname,Addressaddress){this.namename;this.addressaddress;}// 拷贝构造方法Person(Personother){this.nameother.name;this.addressnewAddress(other.address);// 手动深拷贝}}Personp2newPerson(p1);// 深克隆简洁优雅优点不需要实现Cloneable不需要处理CloneNotSupportedException代码可读性强。很多 JDK 源码也用这种方式比如ArrayList(ArrayList? extends E c)、HashMap(Map? extends K, ? extends V m)。七、回到全貌一张图搞定克隆方式选择 你的对象嵌套深吗 │ ├─ 不深1~2层引用 │ ├─ 追求性能 → 手动 clone │ └─ 图省事 → 拷贝构造方法 │ └─ 深多层嵌套 ├─ 已有 Serializable → 序列化深克隆 └─ 没有 → 第三方 JSON 库 千万别直接 super.clone() 就完事 那是浅克隆引用类型字段会共用对象八、面试速答模板Q什么是深克隆和浅克隆区别是什么A浅克隆复制对象时基本类型复制值引用类型只复制地址——所以克隆对象和原对象的引用字段指向同一个实例改一个另一个也变。深克隆则让引用类型也各自独立修改互不影响。区别的本质就是引用类型字段是共用还是各有一份。Q怎么实现深克隆A三种方式——① 手动 clone每层引用类型都实现 Cloneable 并在 clone() 中递归调用② 序列化把对象写成字节流再读回来天然深克隆③ 第三方库用 Gson/Jackson 做序列化反序列化一行搞定。Q为什么浅克隆会出 BugA因为引用类型字段复制的是地址而非对象本身。克隆对象和原对象共享同一个引用实例通过克隆对象修改引用类型字段的属性原对象也会被影响。这是浅克隆最容易被忽视的陷阱。原文阅读内容有帮助点赞、收藏、关注三连评论区等你

更多文章