关于java克隆的探讨
在做项目的过程中踩了一个深克隆浅克隆的坑,利用闲暇时间将克隆相关的知识进行了一下总结。其中夹杂了一些个人的思考,部分结论可能存在问题,请大家指正。
什么是克隆
什么的克隆,克隆是否等于复制?
个人观点:
1、狭义的克隆指Object类中的clone()方法,创建并返回此对象的一个复制。基于此定义,基本类型的复制不属于克隆,不实现clone()方法来复制对象也不属于克隆。
2、广义的克隆等同于复制,创建并返回了对象的一个副本,这里的对象不仅仅指Object,还包括基本类型创建的对象。实现方式上也不局限于clone(),可以是new对象在塞属性值等方式。
本文主要讨论的是广义的克隆。
基本类型的克隆
例1:
思考:tt、m、n的地址是否相同? m的值改变后,m、n的地址是否发生变化?
public class Test {
public static final int tt = 1;
public void fun(){
int m = 1;
int n = m;
m=2;
}
}
结果:
m=1,m.hashCode=1846274136
n=1,n.hashCode=1846274136
tt=1,tt.hashCode=1846274136
new m=2,m.hashCode=1639705018
new n=1,n.hashCode=1846274136
借助javap -v Test.class 指令,我们可以观察到常量池中有tt、m、n这几个字符常量,有1这个Integer类型的常量,没有2。
我们得出以下结论:
1作为静态常量tt的字面值存在方法区中,tt指向1,当定义局部变量m和n时,先从常量池中寻找是否有此常量,若有此常量,m和n也直接指向1的地址。若无此常量,按照顺序在栈中开辟地址存放1,并将m和n指向1。2是在栈中的,因此不在常量池中。
例2:
思考:基本类型的变量超过一定范围后,是否还是指向同一内存地址呢?
int s = 128;
int w = 128;
执行结果:
s=128,s.hashCode=1627674070
w=128,w.hashCode=1360875712
s==w ? true
可见,超过一定范围后,相同值的基本类型变量指向不同的内存地址,int类型的范围是-127~127。
扩展一下,基本类型用 == 与equal都是比较值,非基本类型用==是比较内存地址,equal是比较值。
通过这两个小例子我们可以更深刻的明白基本类型能实现克隆的原因,与内存分配有根本的联系。
String的克隆
将String单独拿出来讲,是因为他不是基本类型,也区别于其他类,有着与基本类型相似的特性。
网上有一些说法,说String的clone特性是因为他是final的,String类由final修饰,并且其核心成员变量 char value[]也是final的,但这能保证他的clone特性吗?
看两个小例子,例1:
思考:mm、nn地址相同吗?mm值改变后,tt值变吗?
public void fun(){
String mm = "abc";
String nn = "abc";
String tt = mm;
mm = "dd";
}
结果:
mm=abc,mm.hashCode=1846274136
nn=abc,nn.hashCode=1846274136
mm=dd,mm.hashCode=1639705018
tt=abc,tt.hashCode=1846274136
观察class文件:
可见,常量池中有"abc"与"dd",长度小于两个字节的String类型会以Utf8的格式存在常量池中,因此mm与nn指向相同的内存地址。
当mm指向新值时,实际是mm指向新的地址,String虽然不是基本数据类型,但其clone的表现与基本类型一致。
例2:
针对final的实验,仿造String,写final的TestString,用反射破坏final,观察结果。
TestString testString = new TestString("aaa".toCharArray());
System.out.println("testString=" + testString + ",testString.hashCode="+ System.identityHashCode(testString));
testString.changeValue();
System.out.println("testString=" + testString + ",testString.hashCode="+ System.identityHashCode(testString));
try {
Field field = nn.getClass().getDeclaredField("value");
field.setAccessible(true);
field.set(nn, "aa".toCharArray());
System.out.println("nn=" + nn + ",nn.hashCode="+ System.identityHashCode(nn));
System.out.println("tt=" + tt + ",tt.hashCode="+ System.identityHashCode(tt));
}catch (Exception e){
System.out.println(e);
}
TestString部分代码:
public final class TestString {
private final char[] value;
public TestString(char[] value) {
this.value = value;
}
public char[] getValue() {
return value;
}
public void changeValue(){
this.value[0] = 'q';
}
}
执行结果:
testString={"value":[a, a, a]},testString.hashCode=1627674070
testString={"value":[q, a, a]},testString.hashCode=1627674070
nn=aa,nn.hashCode=1846274136
tt=aa,tt.hashCode=1846274136
通过这个小例子可以看出,当nn发生变化时,tt也发生了变化,用final并不是保证String的clone特性的充分条件。
真正原因是JVM对于String采用与基本类型类似的内存处理方式。
对象的克隆
介绍完基本类型与String的克隆,我们再来分析下对象的克隆,分析常用的一些克隆方式。
深克隆与浅克隆
深浅克隆的区别在于,能否支持引用类型(包括类、接口、数组等)的成员变量的复制。
浅克隆中,对象只复制了它本身和其中包含的值类型的成员变量,引用类型的成员对象并没有复制。
深克隆中,对象本身以及包含的所有成员变量都会被复制。
下面两个图表示了这两种克隆方式。
图1:
图2:
Cloneable实现浅克隆
被克隆类要实现Cloneable接口,重写clone()方法。请看下面两个小例子,例1:
/**
* 测试不实现Cloneable接口
*/
private static void testNoCloneable(){
StudentNoCloneable studentNoCloneable = new StudentNoCloneable();
studentNoCloneable.setName("张三");
studentNoCloneable.setNumber(1);
StudentNoCloneable studentNoCloneableClone = studentNoCloneable;
//这种写法会报错,
//StudentNoCloneable studentNoCloneableClone = (StudentNoCloneable)studentNoCloneable.clone();
System.out.println("studentNoCloneable=" + studentNoCloneable + ",studentNoCloneable.hashCode="+ System.identityHashCode(studentNoCloneable));
System.out.println("studentNoCloneableClone=" + studentNoCloneableClone + ",studentNoCloneableClone.hashCode="+ System.identityHashCode(studentNoCloneableClone));
studentNoCloneable.setName("李四");
studentNoCloneable.setNumber(2);
System.out.println("studentNoCloneable=" + studentNoCloneable + ",studentNoCloneable.hashCode="+ System.identityHashCode(studentNoCloneable));
System.out.println("studentNoCloneableClone=" + studentNoCloneableClone + ",studentNoCloneableClone.hashCode="+ System.identityHashCode(studentNoCloneableClone));
System.out.println("不实现Cloneable接口,被克隆的对象发生了变化");
System.out.println();
}
例2:
/**
* 测试实现Cloneable接口
* 测试Cloneable接口能否实现深拷贝
*/
private static void testCloneable(){
StudentCloneable studentCloneable = new StudentCloneable();
studentCloneable.setName("张三");
studentCloneable.setNumber(1);
School school = new School();
school.setName("大一");
school.setNum(1);
studentCloneable.setSchool(school);
StudentCloneable studentCloneableClone = (StudentCloneable)studentCloneable.clone();
System.out.println("studentCloneable=" + studentCloneable + ",studentCloneable.hashCode="+ System.identityHashCode(studentCloneable));
System.out.println("studentCloneableClone=" + studentCloneableClone + ",studentCloneableClone.hashCode="+ System.identityHashCode(studentCloneableClone));
studentCloneable.setName("李四");
studentCloneable.setNumber(2);
studentCloneable.getSchool().setName("大二");
studentCloneable.getSchool().setNum(2);
System.out.println("studentCloneable=" + studentCloneable + ",studentCloneable.hashCode="+ System.identityHashCode(studentCloneable));
System.out.println("studentCloneableClone=" + studentCloneableClone + ",studentCloneableClone.hashCode="+ System.identityHashCode(studentCloneableClone));
System.out.println("实现Cloneable接口,被克隆的对象未发生变化,但是引用类型发生了变化");
System.out.println();
}
StudentCloneable:
@Getter
@Setter
@ToString
public class StudentCloneable implements Cloneable{
private int number;
private String name;
private School school;
public StudentCloneable() {
}
public StudentCloneable(int number, String name, School school) {
this.number = number;
this.name = name;
this.school = school;
}
@Override
public Object clone() {
StudentCloneable stu = null;
try{
stu = (StudentCloneable)super.clone();
}catch (Exception e){
System.out.println(e);
}
return stu;
}
}
我们可以得出以下结论:
1、通过实现Cloneable接口可以实现浅克隆。
2、如果不使用clone,对象直接等于赋值实际是引用指向同一个地址,更改其中一个会影响另外一个。
序列化实现深克隆
把对象写到流里的过程是序列化的过程,把对象从流里读出来的过程是反序列化。写在流里的是对象的一个拷贝,与原对象的地址并不相同。
以下为通过序列化实现深克隆的一个例子:
School school = new School("大一",1);
Student student = new Student("张三",1,school);
Student studentClone = student.clone();
System.out.println("student=" + student + ",student.hashCode="+ System.identityHashCode(student));
System.out.println("studentClone=" + studentClone + ",studentClone.hashCode="+ System.identityHashCode(studentClone));
System.out.println();
student.setName("李四");
student.setNum(2);
student.getSchool().setName("大二");
student.getSchool().setNum(2);
System.out.println("student=" + student + ",student.hashCode="+ System.identityHashCode(student));
System.out.println("studentClone=" + studentClone + ",studentClone.hashCode="+ System.identityHashCode(studentClone));
@Getter
@Setter
@ToString
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer num;
private School school;
public Student() {
}
public Student(String name, Integer num, School school) {
this.name = name;
this.num = num;
this.school = school;
}
@Override
public Student clone(){
Student student = null;
try{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
// 将流序列化成对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
student = (Student) ois.readObject();
}catch (Exception e){
System.out.println(e.toString());
}
return student;
}
}
由此我们可以得出结论:
1、通过序列化与反序列化可以实现深拷贝。
2、对象以及对象中的引用类型成员变量都需要实现Serializable接口。
BeanUtils.copyproperties实现原理
BeanUtils.copyproperties是我们非常常用的一个方法,关于其用法大家也都很熟悉,网上也有不少介绍,篇幅有限,这里不再过多介绍。
这里主要分析下原理。
通过测试以及阅读源码,我们得出以下结论:
1、BeanUtils.copyproperties本质上是通过new对象,然后通过get、set的方式来复制对象的。
2、实现的是浅克隆。
3、应用了java内省机制与反射机制。
4、对PropertyDescriptors的解析应用了成员变量作为缓存,提高了效率。
5、如果源对象的属性没有get方法、目标对象的属性没有set方法,则此属性不能完成clone。
6、属性的类型不需要完全一致,满足JDK的isAssignableFrom,就能复制。
BeanCopier实现原理
BeanCopier也是使用较多的一种克隆对象的工具类,主要通过create方法动态生成类,调用生成对象的copy方法类复制对象。
两个小例子:
/**
* 测试包装类,无converter
*/
public static void testOne(){
StudentOne studentOne = new StudentOne(1,"张三");
StudentTwo studentTwo = new StudentTwo();
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\testDebug\\target\\classes\\clone\\beanCopy");
BeanCopier beanCopier = BeanCopier.create(StudentOne.class, StudentTwo.class, false);
beanCopier.copy(studentOne,studentTwo,null);
System.out.println(studentTwo);
System.out.println();
}
/**
* 测试包装类,有converter
*/
public static void testTwo(){
StudentOne studentOne = new StudentOne(1,"张三");
StudentTwo studentTwo = new StudentTwo();
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\testDebug\\target\\classes\\clone\\beanCopy");
BeanCopier beanCopier = BeanCopier.create(StudentOne.class, StudentTwo.class, true);
Converter converter = new Converter() {
@Override
public Object convert(Object value, Class target, Object context) {
if (value instanceof Integer) {
return (Integer) value;
}
return null;
}
};
beanCopier.copy(studentOne,studentTwo,converter);
System.out.println(studentTwo);
System.out.println();
}
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\testDebug\\target\\classes\\clone\\beanCopy");
这一句的作用是设置动态生成的类的路径。
BeanCopier默认类型强校验,不同类型的属性不能转换。但是可以通过设置Converter来实现类型的转换。通过create()方法动态生成类,调用copy()方法完成复制。
我们分析下create方法。
通过测试以及阅读源码,我们得出以下结论:
1、BeanCopier本质上是通过new对象,然后通过get、set的方式来复制对象的。
2、实现是浅克隆。
3、是通过动态生成字节码来生成类,调用copy方法实际是调用了get、set方法。
4、相比于BeanUtils.copyproperties,逻辑复杂在create部分,copy没有多余的逻辑判断。因此性能要高。
5、如果源对象的属性没有get方法、目标对象的属性没有set方法,则此属性不能完成clone。
6、属性的类型要求完全一致,如果不一致,可以通过写converter的方式进行转换。
几种不同克隆方式的比较
通过以上一些分析,我们可以看出各种克隆方式在实现上、性能上各有优缺点,这里简单总结下:
通过Cloneable与序列化实现克隆最高效,且代码简洁。但是对被克隆对象有要求,也不好对不同类型的对象做操作。
通过get set方式代码不够简洁,但是非常高效。建议大量复制时采用这种方式。
使用BeanCopier较为高效,性能主要花在create上(可以通过自写工具类,利用缓存优化),copy方法基本等同于get set方法,因此较为高效,建议大量复制时使用。
使用BeanUtils.copyproperties也较为高效,但是逻辑处理中仍然有比较多的校验影响性能。
另外还有Apache等提供的工具类,这里没有做分析,请感兴趣的小伙伴继续研究。
未尽的探讨
如何不重写clone()方法,实现深克隆?用json方式可以实现。
public static<T> T convert(Object src, Class<T> clazz) {
String json = JSONObject.toJSONString(src);
T object = JSONObject.parseObject(json, clazz);
return object;
}
其原理是什么?有没有更好的方案?关于克隆的探讨可以继续挖掘,也欢迎小伙伴们一起来讨论。
上一篇: Mysql
下一篇: 荐 EasyPoi实现动态模板导入导出