【Java】Object 的 clone 方法分析
学习视频:https://study.163.com/course/introduction/1006177009.htm
学习目标
- 能够理解clone方法的由来
- 能够使用clone方法创建对象
- 能够理解克隆对象和原对象的关系
- 能够理解clone方法创建对象与new关键字和反射创建对象的不同
- 能够理解浅表复制和深层复制的含义
- 能够探寻对象的复制必须实现Cloneable接口的底层源码
1. 克隆方法的由来
问题一:什么是克隆(clone)方法?
答:创建并返回此对象的一个副本--按照原对象,创建一个新对象(复制原对象的内容)。
问题二:已经存在new关键字和反射技术都可以创建对象,为什么还需要一个Object的clone方法呢?
答:必然是new关键字和 反射技术,存在一些弊端。看下面的例子体会弊端在哪。
1.1 new关键字和反射创建对象的弊端
我们来看一个需求:使用new关键字和反射创建内容一模一样的对象,并且打印它们的哈希值。
演示素材--Person:
package com.yynm.pojo;
/**
* @Description: Person实体类
* @Author: yeyulemon
* @Date: 2021-12-16 21:06
**/
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
演示Person类使用new关键字和反射创建内容一模一样的对象,并且打印它们的哈希值
/**
* @Description: 使用new关键字和反射创建内容一模一样的对象,并且打印它们的哈希值
* @Param: []
* @return: void
* @Author: yeyulemon
* @Date: 2021/12/16 21:12
*/
@Test
public void test01() throws Exception {
Person person1 = new Person();
person1.setName("张三");
person1.setAge(18);
Person person2 = new Person();
person2.setName("张三");
person2.setAge(18);
System.out.println(person1 + ":" + person1.hashCode());
System.out.println(person2 + ":" + person2.hashCode());
Class clazz = Class.forName("com.yynm.pojo.Person");
Person person3 = (Person) clazz.getConstructor().newInstance();
person3.setName("张三");
person3.setAge(18);
Person person4 = (Person) clazz.getConstructor().newInstance();
person4.setName("张三");
person4.setAge(18);
System.out.println(person3 + ":" + person3.hashCode());
System.out.println(person4 + ":" + person4.hashCode());
}
效果
com.yynm.pojo.Person@dc24521:230835489
com.yynm.pojo.Person@10bdf5e5:280884709
com.yynm.pojo.Person@6e1ec318:1847509784
com.yynm.pojo.Person@7e0b0338:2114650936
总结:通过new和反射可以创建内容一模一样的对象。但是,创建对象之后,通过setter方法设置一模一样的内容,如果需要创建更多内容一致的对象,那么就需要调用非常多的setter方法。
接下来,使用Object的clone方法演示,更加简便快捷,复制对象的操作!
1.2 使用clone方法创建对象
1.2.1 使用步骤
- 在需要clone方法的类上实现Cloneable接口
- 重写clone方法,在自己的clone方法中调用父类的clone方法,将返回值类型强转成本类类型,将当前clone方法修饰符改为public
- 在测试中调用对象的clone方法
1.2.2 代码演示
- 在需要clone方法的类上实现Cloneable接口
- 重写clone方法,在自己的clone方法中调用父类的clone方法,将返回值类型强转成本类类型,将当前clone方法修饰符改为public
package com.yynm.pojo;
/**
* @Description: Person实体类
* @Author: yeyulemon
* @Date: 2021-12-16 21:06
**/
public class Person implements Cloneable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
- 在测试中调用对象的clone方法
/**
* @Description: 在测试中调用对象的clone方法
* @Param: []
* @return: void
* @Author: yeyulemon
* @Date: 2021/12/16 21:33
*/
@Test
public void test02() throws Exception {
Person person1 = new Person();
person1.setName("张三");
person1.setAge(18);
Person person2 = person1.clone();
System.out.println(person1 + ":" + person1.hashCode());
System.out.println(person2 + ":" + person2.hashCode());
}
效果
com.yynm.pojo.Person@dc24521:230835489
com.yynm.pojo.Person@10bdf5e5:280884709
总结:通过使用clone方法,我们发现大大的减少了创建重复对象代码。这也就是clone方法存在的意义。
2. 克隆出来的对象和原来的对象有什么关系
通过上面的测试,我们已经知道了,克隆出来的对象内容一致,但是对象哈希值不一样,所以是不同对象。那么两个对象的内容之间有什么关联呢?两个对象的内容是彼此独立,还是,两个对象底层使用的同一个内容呢?
素材(新Person):
package com.yynm.pojo;
/**
* @Description: Person实体类
* @Author: yeyulemon
* @Date: 2021-12-16 21:06
**/
public class Person implements Cloneable {
private String name;
private Integer age;
private Children children;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Children getChildren() {
return children;
}
public void setChildren(Children children) {
this.children = children;
}
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
素材(新Children):
package com.yynm.pojo;
/**
* @Description: Children实体类
* @Author: yeyulemon
* @Date: 2021-12-16 21:49
**/
public class Children {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
测试代码:
/**
* @Description: 测试克隆对象和原对象之间成员变量的联系
* @Param: []
* @return: void
* @Author: yeyulemon
* @Date: 2021/12/16 21:51
*/
@Test
public void test03() throws Exception {
Person person1 = new Person();
person1.setName("张三");
person1.setAge(18);
Children children = new Children();
children.setName("李四");
children.setAge(18);
person1.setChildren(children);
Person person2 = person1.clone();
System.out.println(person1 + ":" + person1.hashCode() + ";" + children + ":" +person1.getChildren().hashCode());
System.out.println(person2 + ":" + person2.hashCode() + ";" + children + ":" +person2.getChildren().hashCode());
}
效果:
com.yynm.pojo.Person@10bdf5e5:280884709;com.yynm.pojo.Children@6e1ec318:1847509784
com.yynm.pojo.Person@7e0b0338:2114650936;com.yynm.pojo.Children@6e1ec318:1847509784
结论:通过测试发现克隆出来的对象虽然不一致,但是底层的成员变量的哈希值是一致的。
这种复制我们称之为:浅表复制。
浅表复制内存结构:
3. 能不能让克隆出来的对象其中成员变量也变成新对象
3.1 浅表复制的弊端
由于浅表复制导致克隆的对象中成员变量的底层哈希值一致,如果我们操作其中一个对象的成员变量内容,就会导致所有的克隆对象的成员变量内容发生改变。
测试代码:
@Test
public void test04() throws Exception {
Person person1 = new Person();
Children children = new Children();
children.setName("张三");
children.setAge(18);
person1.setChildren(children);
Person person2 = person1.clone();
System.out.println(person1 + ":" + person1.hashCode() + ";" + person1.getChildren().getName());
System.out.println(person2 + ":" + person2.hashCode() + ";" + person2.getChildren().getName());
children.setName("李四");
children.setAge(18);
System.out.println(person1 + ":" + person1.hashCode() + ";" + person1.getChildren().getName());
System.out.println(person2 + ":" + person2.hashCode() + ";" + person2.getChildren().getName());
}
效果:
com.yynm.pojo.Person@573fd745:1463801669;张三
com.yynm.pojo.Person@15327b79:355629945;张三
com.yynm.pojo.Person@573fd745:1463801669;李四
com.yynm.pojo.Person@15327b79:355629945;李四
结论:clone方法默认的赋值操作是浅表复制,浅表复制存在弊端--仅仅创建新的对象,对象的成员内容底层哈希值是一致的,因此,不管是原对象还是克隆对象,只有其中一个修改了成员的数据,就会影响所有的原对象和克隆对象。
要解决浅表复制的问题:进行深层的复制。
3.2 深层复制
目的:不仅在执行克隆的时候,克隆对象是一个新对象,而且,克隆对象中的成员变量,也要求是一个新的对象。
3.2.1 开发步骤
- 修改Children类实现Cloneable接口
- 修改Children类重写clone方法
- 修改Person类重写clone方法,在clone方法中调用children的clone方法
3.2.2 代码实现
- 修改Children类实现Cloneable接口
- 修改Children类实现clone方法
Children类:
package com.yynm.pojo;
/**
* @Description: Children实体类
* @Author: yeyulemon
* @Date: 2021-12-16 21:49
**/
public class Children implements Cloneable {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public Children clone() throws CloneNotSupportedException {
return (Children) super.clone();
}
}
Person类:
package com.yynm.pojo;
/**
* @Description: Person实体类
* @Author: yeyulemon
* @Date: 2021-12-16 21:06
**/
public class Person implements Cloneable {
private String name;
private Integer age;
private Children children;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Children getChildren() {
return children;
}
public void setChildren(Children children) {
this.children = children;
}
@Override
public Person clone() throws CloneNotSupportedException {
Person personClone = (Person) super.clone();
personClone.setChildren(this.children.clone());
return personClone;
}
}
测试代码:
@Test
public void test05() throws Exception {
Person person1 = new Person();
Children children = new Children();
children.setName("张三");
children.setAge(18);
person1.setChildren(children);
Person person2 = person1.clone();
System.out.println(person1 + ":" + person1.hashCode() + ";" + person1.getChildren().hashCode());
System.out.println(person2 + ":" + person2.hashCode() + ";" + person2.getChildren().hashCode());
}
效果:
com.yynm.pojo.Person@573fd745:1463801669;355629945
com.yynm.pojo.Person@4f2410ac:1327763628;1915503092
深层复制内存结构:
4. 使用clone接口实现深层复制的弊端
4.1 使用clone接口实现深层复制的弊端
以上的方法虽然完成了深层复制,但是修改类中成员变量对应的源码,如果成员变量特别多,那么就需要修改多个类的源码。
例如以下代码,我们就需要修改两个成员变量对应类的源码(Children,Grandson):
package com.yynm.pojo;
/**
* @Description: Person实体类
* @Author: yeyulemon
* @Date: 2021-12-16 21:06
**/
public class Person implements Cloneable {
private String name;
private Integer age;
private Children children;
private Grandson grandson;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Children getChildren() {
return children;
}
public void setChildren(Children children) {
this.children = children;
}
public Grandson getGrandson() {
return grandson;
}
public void setGrandson(Grandson grandson) {
this.grandson = grandson;
}
@Override
public Person clone() throws CloneNotSupportedException {
Person personClone = (Person) super.clone();
personClone.setChildren(this.children.clone());
personClone.setGrandson(this.grandson.clone());
return personClone;
}
}
结论:使用克隆接口完成深度复制的弊端:
1. 重复实现Cloneable接口
2. 重复实现clone方法
3. 重复改写Person类的clone方法
可以使用IO流的方式进行复制操作(深度复制),可以解决重复修改源代码的问题。
4.2 使用IO进行克隆复制(深度复制)
4.2.1 使用IO复制相关的API介绍
1、ByteArrayOutputStream
构造方法:
2、ByteArrayInputStream
构造方法:
3、ObjectOutputStream
构造方法:
将对象写入流的方法:
4、ObjectInputStream
构造方法:
要调用的方法:
简单演示:一个对象的复制。
开发步骤:
- 创建ByteArrayOutputStream,将数据可以转换成字节
- 创建ObjectOutputStream,关联ByteArrayOutputStream
- 使用ObjectOutputStream的writeObject方法,读取要复制的对象
- 使用ByteArrayInputStream读取ByteArrayOutputStream的转化的对象字节数据
- 创建ObjectInputStream并用readObject读取对象字节数据返回新对象
素材User类:
package com.yynm.pojo;
import java.io.Serializable;
/**
* @Description:
* @Author: yeyulemon
* @Date: 2021-12-19 20:09
**/
public class User implements Serializable {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
测试:
@Test
public void test06() throws Exception {
User user1 = new User();
user1.setUsername("张三");
user1.setPassword("123456");
// 1. 创建ByteArrayOutputStream,将数据可以转换成字节
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// 2. 创建ObjectOutputStream,关联ByteArrayOutputStream
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
// 3. 使用ObjectOutputStream的writeObject方法,读取要复制的对象
objectOutputStream.writeObject(user1);
// 4. 使用ByteArrayInputStream读取ByteArrayOutputStream的转化的对象字节数据
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
// 5. 创建ObjectInputStream并用readObject读取对象字节数据返回新对象
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
User user2 = (User) objectInputStream.readObject();
System.out.println(user1 + ":" + user1.hashCode());
System.out.println(user2 + ":" + user2.hashCode());
}
效果:
com.yynm.pojo.User@2b05039f:721748895
com.yynm.pojo.User@77468bd9:2001112025
4.3 使用IO改写Person的clone方法
4.3.1 开发步骤
- 克隆涉及的所有的类实现Serializable
- 修改Person类的clone方法,使用IO复制对象
- 测试演示
4.3.2 代码实现
- 克隆涉及的所有的类实现Serializable
- 修改Person类的clone方法,使用IO复制对象
package com.yynm.pojo;
import java.io.*;
/**
* @Description: Person实体类
* @Author: yeyulemon
* @Date: 2021-12-16 21:06
**/
public class Person implements Cloneable, Serializable {
private String name;
private Integer age;
private Children children;
private Grandson grandson;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Children getChildren() {
return children;
}
public void setChildren(Children children) {
this.children = children;
}
public Grandson getGrandson() {
return grandson;
}
public void setGrandson(Grandson grandson) {
this.grandson = grandson;
}
@Override
public Person clone() throws CloneNotSupportedException {
try {
// 1. 创建ByteArrayOutputStream,将数据可以转换成字节
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 2. 创建ObjectOutputStream,关联ByteArrayOutputStream
ObjectOutputStream oos = new ObjectOutputStream(baos);
// 3. 使用ObjectOutputStream的writeObject方法,读取要复制的对象
oos.writeObject(this);
// 4. 使用ByteArrayInputStream读取ByteArrayOutputStream的转化的对象字节数据
ByteArrayInputStream bAIS = new ByteArrayInputStream(baos.toByteArray());
// 5. 创建ObjectInputStream并用readObject读取对象字节数据返回新对象
ObjectInputStream oIS = new ObjectInputStream(bAIS);
Person personClone = (Person) oIS.readObject();
return personClone;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
测试:
@Test
public void test07() throws Exception {
Person person1 = new Person();
Person person2 = person1.clone();
System.out.println(person1 + ":" + person1.hashCode());
System.out.println(person2 + ":" + person2.hashCode());
}
效果:
com.yynm.pojo.Person@4dcbadb4:1305193908
com.yynm.pojo.Person@77468bd9:2001112025
5. 为什么使用clone方法需要实现Cloneable接口
答:源代码就是这么设定的,实现接口仅仅是一个可以使用clone方法的标记。
那么源代码在哪里设定的呢?
查看jdk源码我们发现:
因此,我们需要查看native修饰的背后的源码,这个一直要追溯到jdk底层C,C++源码。
5.1 下载完整jdk源码
下载地址:http://jdk.java.net/java-se-ri/7
查看步骤
源码展示:
JVM_ENTRY(jobject, JVM_Clone(JNIEnv* env, jobject handle))
JVMWrapper("JVM_Clone");
Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
const KlassHandle klass (THREAD, obj->klass());
JvmtiVMObjectAllocEventCollector oam;
#ifdef ASSERT
// Just checking that the cloneable flag is set correct
if (obj->is_javaArray()) {
guarantee(klass->is_cloneable(), "all arrays are cloneable");
} else {
guarantee(obj->is_instance(), "should be instanceOop");
bool cloneable = klass->is_subtype_of(SystemDictionary::Cloneable_klass());
guarantee(cloneable == klass->is_cloneable(), "incorrect cloneable flag");
}
#endif
// Check if class of obj supports the Cloneable interface.
// All arrays are considered to be cloneable (See JLS 20.1.5)
if (!klass->is_cloneable()) {
ResourceMark rm(THREAD);
THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
}
// Make shallow object copy
const int size = obj->size();
oop new_obj = NULL;
if (obj->is_javaArray()) {
const int length = ((arrayOop)obj())->length();
new_obj = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
} else {
new_obj = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
}
// 4839641 (4840070): We must do an oop-atomic copy, because if another thread
// is modifying a reference field in the clonee, a non-oop-atomic copy might
// be suspended in the middle of copying the pointer and end up with parts
// of two different pointers in the field. Subsequent dereferences will crash.
// 4846409: an oop-copy of objects with long or double fields or arrays of same
// won't copy the longs/doubles atomically in 32-bit vm's, so we copy jlongs instead
// of oops. We know objects are aligned on a minimum of an jlong boundary.
// The same is true of StubRoutines::object_copy and the various oop_copy
// variants, and of the code generated by the inline_native_clone intrinsic.
assert(MinObjAlignmentInBytes >= BytesPerLong, "objects misaligned");
Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj,
(size_t)align_object_size(size) / HeapWordsPerLong);
// Clear the header
new_obj->init_mark();
// Store check (mark entire object and let gc sort it out)
BarrierSet* bs = Universe::heap()->barrier_set();
assert(bs->has_write_region_opt(), "Barrier set does not have write_region");
bs->write_region(MemRegion((HeapWord*)new_obj, size));
// Caution: this involves a java upcall, so the clone should be
// "gc-robust" by this stage.
if (klass->has_finalizer()) {
assert(obj->is_instance(), "should be instanceOop");
new_obj = instanceKlass::register_finalizer(instanceOop(new_obj), CHECK_NULL);
}
return JNIHandles::make_local(env, oop(new_obj));
JVM_END
校验当前类是否实现克隆接口的代码:
// Check if class of obj supports the Cloneable interface.
// All arrays are considered to be cloneable (See JLS 20.1.5)
if (!klass->is_cloneable()) {
ResourceMark rm(THREAD);
THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
}
注释翻译:
数组类型默认可以直接克隆,而其它对象实现clone需要先实现Cloneable接口,否则抛出:
CloneNotSupportedException异常
结论,对象使用clone方法必须实现Cloneable接口。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~