关于java克隆的探讨

《科研伦理与学术规范》 全部 习题_答案 2020年秋 科研伦理与学术规范 答案 文章目录科研伦理与学术规范 答案1. 第一章课后习题(必做) 答案1.1 第一章Demo2. 第二章课后习题(必做) 答案3. 第三章课后习题(必做) 答案4. 第四章课后习题(必做) 答案5. 第五章课后习题(必做) 答案6. 第六章课后习题(必做) 答案 # 每行5题 #写于2020.11.17 侵删 1. 第一章课后习题(必做) 答案 ABC, A, B ,D ,ABCD ABC, D, A, BCD, C D, D, 1.1 第一章Demo 阅读详情

在做项目的过程中踩了一个深克隆浅克隆的坑,利用闲暇时间将克隆相关的知识进行了一下总结。其中夹杂了一些个人的思考,部分结论可能存在问题,请大家指正。

什么是克隆

什么的克隆,克隆是否等于复制?

个人观点:

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;
}

其原理是什么?有没有更好的方案?关于克隆的探讨可以继续挖掘,也欢迎小伙伴们一起来讨论。

android 如何分析应用的内存(十六)——使用AS查看Android堆 在前面,先介绍了如何使用jdb和VS code查看应用栈相关内容。本文将介绍,如何查看堆中的内容。 阅读详情

相关推荐

Mac和VScode配置fortran

macOS系统 vscode fortran gfortran 终端

weixin_43047707的博客 2410

BeanUtils.copyProperties和clone()方法的区别

最近撸代码的时候发现有人将一个对象的值赋给另一个对象的时候,并没有使用常规的set/get方法去给对象赋值,而是采用BeanUtils.copyProperties(A,B)这个方法去赋值,但是有的还是有局限性,比如Date类型的值无法赋值,只能赋值为null,所以我大致百度了一下,作为记录。 首先,BeanUtils有两种: org.springframework.beans和org.apach...

qxf1374268的博客 3867

用ST25DV芯片+KiCad快速设计NFC标签天线:参数计算/脚本生成/实测优化全流程

本文详细介绍了使用ST25DV芯片和KiCad工具设计NFC标签天线的全流程,包括参数计算、脚本生成和实测优化。通过解析芯片等效工作原理和设计思路,帮助开发者避开常见陷阱,实现高效、可量产的天线设计。文章还提供了Python计算工具和KiCad自动化脚本,大幅提升设计效率和成功率。

jj890的博客 758

Java Bean对象复制的三种方式详解

前言 一般Java对象的复制包含**浅拷贝、深度复制、BeanUtils.copyProperties()**等三种方式。 对象的克隆是指创建一个新的对象,且新的对象的状态与原始对象的状态相同且我们对新对象修改时,不影响原对象的状态。 原理如下 clone()是object类的protected 方法,只有类的对象自己可以克隆自己 因此,必须实现cloneable接口才可以使用obj.clone(...

纵横的博客 1万+

AI智能体协作:构建更精准的公司破产预测模型

公司破产预测对于金融机构、投资者和监管部门等具有至关重要的意义。准确的破产预测可以帮助金融机构提前做好风险防范,避免贷款损失;投资者可以根据预测结果调整投资组合,降低投资风险;监管部门可以及时发现潜在的金融风险,采取相应的监管措施。然而,传统的公司破产预测模型往往存在精度不高的问题,无法满足实际需求。本研究的目的是利用AI智能体协作的方式,构建一个更精准的公司破产预测模型。通过将多个AI智能体的优势进行整合,充分挖掘公司财务数据、市场数据等多源数据中的信息,提高预测的准确性和可靠性。

2501_92132293的博客 1081

龙头复盘神器/复盘于导出工具最新版5.6

龙头复盘神器,复盘于导出工具

初入python踩过的坑 8239

克隆表达式(用于任务调度)(转)

字段   允许值   允许的特殊字符 秒  0-59   , - * / 分   0-59   , - * / 小时   0-23   , - * / 日期   1-31   , - * ? / L W C 月份   1-12 或者 JAN-DEC   , - * / 星期   1-7 或者 SUN-SAT   , - * ? / L C # 年(可选)   留空, 1970-209

gaoyongqiang的专栏 2068

SAP Script表单开发中ABAP preform方法的实战应用与调试技巧

本文深入探讨了在SAP Script表单开发中调用ABAP程序(preform方法)的实战应用与调试技巧。通过一个获取供应商版本名称的实例,详细讲解了如何在SE71中嵌入PERFORM指令、编写ABAP子程序进行数据查询与处理,并重点介绍了使用SAP Script Debugger进行高效调试的方法,帮助开发者灵活扩展表单功能,实现复杂业务逻辑。

o0p1q2r3的博客 551

在线Cron表达式生成器

http://cron.qqe2.com/

hbxf_xs的博客 2189

11、频率响应与伯德图详解

本文详细介绍了电子电路分析中的频率响应与伯德图,涵盖分贝的定义与计算、带宽与3dB点、放大器反馈机制、倍频程与十倍频程概念,并系统讲解了基于实数和复数零极点的伯德图构建方法。通过具体示例分析了传递函数的渐近近似、幅度与相位响应,并结合MATLAB工具展示了伯德图的绘制过程。文章旨在帮助读者深入理解电路在不同频率下的行为特性,为电路设计与优化提供理论支持。

white的博客 172

实时系统vxWorks - 虚拟机环境搭建

概述VxWorks 操作系统是美国WindRiver公司于1983年设计开发的一种嵌入式实时操作系统(RTOS),是嵌入式开发环境的关键组成部分。良好的持续发展能力、高性能的内核以及友好的...

不只会拍照的程序猿 7853

cron表达式解析生成网站

https://crontab.guru/ https://cron.qqe2.com/

nansen0324的博客 2318

vmvare kali如何配置桥接模式进行上网

注意点:虚拟机可以PING通物理机,但是PING不通其他的网站。经过收集资料,得知由于是校园网连接,所以DHCP只能分配一个授权的IP地址给连接的主机,由于KALI是桥接物理机,物理机已经获得了这个授权的IP,所以导致桥接的虚拟机无法上网。所以不是因为配置的有问题,而是网络的安全设置导致了桥接失败。如果你是使用校园网,可以换个网络试试,使用校园网的时候,可能会遇到桥接失败。

猿小白的博客 1409

Java对象克隆(Clone)及Cloneable接口、Serializable接口的深入探讨

Java对象克隆(Clone)及Cloneable接口、Serializable接口的深入探讨 Part I 没啥好说的,直接开始Part II吧。 Part II 谈到了对象的克隆,就不得不说为什么要对对象进行克隆。Java中所有的对...

198

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

上一篇: JVM知识整理
下一篇: 关于java NIO的探讨
wu_yangchun
博客等级 码龄14年 10粉丝 57原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值