学习目标
1. 能够理解clone方法的由来
2. 能够使用clone方法创建对象
3. 能够理解克隆对象和原对象的关系
4. 能够理解clone方法创建对象与反射和new关键字创建对象的不同
5. 能够理解浅表复制和深层复制的含义
6. 能够探寻对象的复制必须实现Cloneable接口的底层源码
1. 克隆方法的由来
问题一:什么是克隆(clone)方法
答: 创建并返回此对象的一个副本——按照原对象,创建一个新的对象(复制原对象的内容)。
问题二:已经存在new关键字和反射技术都可以创建对象,为什么还需要一个Object的clone方法呢?
答:必然是new关键字和反射技术,存在一些弊端。
1.1 new关键字和反射创建对象的弊端
我们来看一个需求:使用new关键字和反射创建内容一模一样的对象,并且打印他们哈希值。
演示素材——Person对象:
package cn.itcast.domain;
public class Person {
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;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
演示new关键字和反射创建对象:
@Test
public void test1() throws Exception {
Person p1 = new Person();
p1.setName("张三");
p1.setAge(18);
Person p2 = new Person();
p2.setName("张三");
p2.setAge(18);
System.out.println(p1+":"+p1.hashCode());
System.out.println(p2+":"+p2.hashCode());
Class clazz = Class.forName("cn.itcast.domain.Person");
Person p3 = (Person) clazz.getConstructor().newInstance();
p3.setName("李四");
p3.setAge(28);
Person p4 = (Person) clazz.getConstructor().newInstance();
p4.setName("李四");
p4.setAge(28);
System.out.println(p3+":"+p3.hashCode());
System.out.println(p4+":"+p4.hashCode());
}
效果:
Person{name='张三', age=18}:103536485
Person{name='张三', age=18}:1279309678
Person{name='李四', age=17}:48914743
Person{name='李四', age=17}:1106131243
总结:通过new和反射可以创建内容一模一样的对象。但是,创建对象之后,通过setter方法,完成设置一不一样的内容,如果需要创建更多的内容一致的对象,那么setter方法调用就不断在重复。
接下来,使用Object的clone方法演示,更加简便方式,复制对象的操作!
1.2 使用clone方法创建对象
1.2.1 使用步骤
1. 在需要调用clone方法的对象上添加实现Cloneable接口
2. 复写clone方法,在自己的clone方法中调用父类的clone方法,将返回值类型强转成本类类型,将当前clone方法修饰符改成public
3. 在测试中调用对象的clone方法
1.2.2 代码演示
1. 在需要调用clone方法的对象上添加实现Cloneable接口
2. 复写clone方法,在自己的clone方法中调用父类的clone方法,将返回值类型强转成本类类型
package cn.itcast.domain;
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 int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
3. 在测试中调用对象的clone方法
@Test
public void test2() throws Exception {
Person p1 = new Person();
p1.setName("张三");
p1.setAge(18);
Person p2 = p1.clone();
System.out.println(p1+":"+p1.hashCode());
System.out.println(p2+":"+p2.hashCode());
}
效果:
Person{name='张三', age=18}:2104028992
Person{name='张三', age=18}:1790421142
通过使用clone方法,我们发现大大的减少了创建重复对象代码。这也就是clone方法存在的意义。
2. 克隆出来的对象和原来的对象有什么关系
通过上面的测试,我们已经知道了,克隆出来的对象内容一致,但是对象哈希值不同,所以是不同对象。
那么两个对象的内容之间有什么关联呢——两个对象的内容是彼此独立,还是,两个对象底层使用的同一个内容呢?
素材(新Person):
package cn.itcast.domain;
public class Person implements Cloneable{
private String name;
private Integer age;
private Children child;
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 getChild() {
return child;
}
public void setChild(Children child) {
this.child = child;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", child=" + child +
'}';
}
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
素材(Person内部的Children):
package cn.itcast.domain;
public class Children {
private String name;
private Integer 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 String toString() {
return "Children{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试代码:
@Test
public void test3() throws Exception {
Person p1 = new Person();
p1.setName("张三");
p1.setAge(28);
Children children1 = new Children();
children1.setName("张伟");
children1.setAge(5);
p1.setChild(children1);
Person p2 = p1.clone();
System.out.println(p1+":对象的哈希值:"+p1.hashCode()+":child成员变量的哈希值:"+p1.getChild().hashCode());
System.out.println(p2+":对象的哈希值:"+p2.hashCode()+":child成员变量的哈希值:"+p2.getChild().hashCode());
}
效果:
Person{name='张三', age=28, child=Children{name='张伟', age=5}}:对象的哈希值:1527430292:child成员变量的哈希值:1975546571
Person{name='张三', age=28, child=Children{name='张伟', age=5}}:对象的哈希值:1978869058:child成员变量的哈希值:1975546571
结论:通过测试发现克隆出来的对象虽然不一致,但是底层的成员变量的哈希值是一致的。
这种复制我们称之为:浅表复制。
浅表复制的内存结构:
3.能不能让克隆对象其中成员变量也变成新的对象
3.1 浅表复制的弊端
由于浅表复制导致克隆的对象中成员变量的底层哈希值一致,如果我们操作其中一个对象的成员变量内容,就会导致,所有的克隆对象的成员内容发送改变。
测试代码:
/**
* 需求:测试浅表复制的弊端
* */
@Test
public void test4() throws Exception {
Person p1 = new Person();
p1.setName("张三");
p1.setAge(28);
Children children1 = new Children();
children1.setName("张伟");
children1.setAge(5);
p1.setChild(children1);
Person p2 = p1.clone();
System.out.println(p1.getChild());
System.out.println(p2.getChild());
children1.setName("张三丰");
System.out.println(p1.getChild());
System.out.println(p2.getChild());
Children children2 = p2.getChild();
children2.setName("张无忌");
System.out.println(p1.getChild());
System.out.println(p2.getChild());
}
效果:
Children{name='张伟', age=5}
Children{name='张伟', age=5}
Children{name='张三丰', age=5}
Children{name='张三丰', age=5}
Children{name='张无忌', age=5}
Children{name='张无忌', age=5}
结论:clone方法默认的复制操作是浅表复制,浅表复制存在弊端——仅仅创建新的对象,对象的成员内容底层哈希值是一致的,因此,不管是原对象还是克隆对象,只有其中一个修改了成员的数据,就会影响所有的原对象和克隆对象。
要解决浅表复制的问题:进行深层的复制。
3.2 深层复制
目的:不仅在执行克隆的时候,克隆对象是一个新对象,而且,克隆对象中的成员变量,也要求是一个新的对象。
3.2.1 开发步骤
1. 修改children类实现Cloneable接口
2. 修改children类重写clone方法
3. 修改Person类重写clone方法,在clone方法中调用children的clone方法
3.2.2 代码实现
1. 修改child类实现Cloneable接口
2. 修改child类重写clone方法
package cn.itcast.domain;
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 Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Children{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public Children clone() throws CloneNotSupportedException {
return (Children) super.clone();
}
}
3. 修改Person类重写clone方法,在clone方法中调用child的clone方法
package cn.itcast.domain;
public class Person implements Cloneable{
private String name;
private Integer age;
private Children child;
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 getChild() {
return child;
}
public void setChild(Children child) {
this.child = child;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", child=" + child +
'}';
}
@Override
public Person clone() throws CloneNotSupportedException {
Person clone = (Person) super.clone();
clone.setChild(child.clone());
return clone;
}
}
4. 测试代码
/**
* 需求:测试深层复制
* */
@Test
public void test5() throws Exception {
Person p1 = new Person();
p1.setName("张三");
p1.setAge(28);
Children children1 = new Children();
children1.setName("张伟");
children1.setAge(5);
p1.setChild(children1);
Person p2 = p1.clone();
System.out.println(p1.getChild());
System.out.println(p2.getChild());
children1.setName("张三丰");
System.out.println(p1.getChild());
System.out.println(p2.getChild());
Children children2 = p2.getChild();
children2.setName("张无忌");
System.out.println(p1.getChild());
System.out.println(p2.getChild());
System.out.println(p1.getChild().hashCode());
System.out.println(p2.getChild().hashCode());
}
5. 效果:
Children{name='张伟', age=5}
Children{name='张伟', age=5}
Children{name='张三丰', age=5}
Children{name='张伟', age=5}
Children{name='张三丰', age=5}
Children{name='张无忌', age=5}
1131040331
254749889
深层复制内存结构:
4. 成员变量不实现clone接口的情况下进行深度复制
4.1 使用clone接口实现深层复制的弊端
以上的方法虽然完成了深度复制,但是修改类中成员变量对应的源码,如果成员变量特别多,那么就需要修改多个类的源码。
例如一下代码,我们就需要修改两个成员变量对应类的源码(Children,Grandson):
package cn.itcast.domain;
public class Person implements Cloneable{
private String name;
private Integer age;
private Children child;
private Grandson gdson;
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 getChild() {
return child;
}
public void setChild(Children child) {
this.child = child;
}
public Grandson getGdson() {
return gdson;
}
public void setGdson(Grandson gdson) {
this.gdson = gdson;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", child=" + child +
", gdson=" + gdson +
'}';
}
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
素材:Grandson类
package cn.itcast.domain;
public class Grandson {
private String name;
private Integer 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 String toString() {
return "Grandson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
结论:使用克隆接口完成深度复制的弊端:
1 重复实现cloneable接口
2 重复实现clone方法
3 重复改写Person类的clone方法
可以使用IO流的方式进行复制操作(深层复制),可以解决重复修改源代码的问题。
4.2 使用IO进行克隆复制(深层复制)
4.2.1 使用IO复制相关的API介绍
1、ByteArrayOutputStream
构造方法:
2、ByteArrayInputStream
构造方法:
3、ObjectOutputStream
构造方法:
将对象写入流的方法:
4、O
构造函数:
要调用的方法:
简单演示:一个对象的复制。
开发步骤:
1. 创建ByteArrayOutputStream,将数据可以转换成字节
2. 创建ObjectOutputStream,关联ByteArrayOutputStream
3. 使用ObjectOutputStream的writeObject,读取要复制的对象
4. 使用ByteArrayInputStream读取ByteArrayOutputStream的转换的对象字节数据
5. 创建ObjectInputStream读取对象字节数据,创建新的对象
素材:
package cn.itcast.domain;
import java.io.Serializable;
public class User implements Serializable {
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;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试代码:
@Test
public void test7() throws Exception {
User user = new User();
user.setName("李四");
user.setAge(18);
//1. 创建ByteArrayOutputStream,将数据可以转换成字节
ByteArrayOutputStream bout = new ByteArrayOutputStream();
//2. 创建ObjectOutputStream,关联ByteArrayOutputStream
ObjectOutputStream out = new ObjectOutputStream(bout);
//3. 使用ObjectOutputStream的writeObject,读取要复制的对象
out.writeObject(user);
//4. 使用ByteArrayInputStream读取ByteArrayOutputStream的转换的对象字节数据
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
//5. 创建ObjectInputStream读取对象字节数据,创建新的对象
ObjectInputStream in = new ObjectInputStream(bin);
User obj = (User) in.readObject();
System.out.println(user+":"+user.hashCode());
System.out.println(obj+":"+obj.hashCode());
}
效果:
User{name='李四', age=18}:556529265
User{name='李四', age=18}:667447085
4.3 使用IO改写Person的clone方法
4.3.1 开发步骤
1. 克隆涉及的所有的类实现Serializable接口
2. 修改Person类的clone方法,使用IO复制对象
3. 测试演示
4.3.2 代码实现
1. 克隆涉及的所有的类实现Serializable
2. 修改Person类的clone方法,使用IO复制对象
@Override
public Person clone() {
try {
//1. 创建ByteArrayOutputStream,将数据可以转换成字节
ByteArrayOutputStream bout = new ByteArrayOutputStream();
//2. 创建ObjectOutputStream,关联ByteArrayOutputStream
ObjectOutputStream out = new ObjectOutputStream(bout);
//3. 使用ObjectOutputStream的writeObject,读取要复制的对象
out.writeObject(this);
//4. 使用ByteArrayInputStream读取ByteArrayOutputStream的转换的对象字节数据
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
//5. 创建ObjectInputStream读取对象字节数据,创建新的对象
ObjectInputStream in = new ObjectInputStream(bin);
Person clone = (Person) in.readObject();
return clone;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
3. 测试演示
@Test
public void test8() throws Exception {
Person p1 = new Person();
p1.setName("张三");
p1.setAge(58);
Children children1 = new Children();
children1.setName("张伟");
children1.setAge(25);
p1.setChild(children1);
Grandson grandson = new Grandson();
grandson.setAge(2);
grandson.setName("张无忌");
p1.setGdson(grandson);
Person p2 = p1.clone();
System.out.println(p1.getChild()+":"+p1.getChild().hashCode());
System.out.println(p2.getChild()+":"+p2.getChild().hashCode());
System.out.println(p1.getGdson()+":"+p1.getGdson().hashCode());
System.out.println(p2.getGdson()+":"+p2.getGdson().hashCode());
}
效果:
Children{name='张伟', age=25}:370869802
Children{name='张伟', age=25}:1139700454
Grandson{name='张无忌', age=2}:1213349904
Grandson{name='张无忌', age=2}:592617454
5. 为什么使用clone方法需要实现Cloneable接口
答:源代码就是这么设定的,实现接口仅仅是一个可以使用clone方法的标记。
那么源代码是在哪里设定的呢?
查看jdk源码我们发现:
因此,我们需要查看native修饰的背后的源码,这个一直要追溯到jdk底层c,c++源码。
5.1 下载完整jdk源码
下载地址:http://jdk.java.net/java-se-ri/7
下载效果:
解压后:
由于Object必须在虚拟机启动的时候加载,想要查看Object底层的源码一定涉及虚拟机文件中。
这个clone native方法 位于openjdk\hotspot\src\share\vm\prims\jvm.cpp文件中。
关键代码在:JVM_Clone方法中
源码展示:
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异常
代码演示:
package cn.itcast.domain;
public class ExampleA {
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
测试:
@Test
public void test4() throws Exception{
ExampleA a = new ExampleA();
Object clone = a.clone();
System.out.println(clone);
}
效果:
java.lang.CloneNotSupportedException: cn.itcast.domain.ExampleA
at java.lang.Object.clone(Native Method)
at cn.itcast.domain.ExampleA.clone(ExampleA.java:7)
at cn.itcast.listener.TestUtils.test4(TestUtils.java:62)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
|
|