Java原型模式以及引用拷贝与对象拷贝问题

news2024/11/15 13:30:11

目录

    • 基本数据类型,引用数据类型,String
    • 引用拷贝
    • 对象拷贝
      • 浅拷贝
      • 深拷贝
    • 原型模式

基本数据类型,引用数据类型,String

这里为了更好的理解栈,堆的指向关系,Java传值,传引用问题,我找来一个题可以练习一下:

public class Application {

    public static void main(String[] args) throws CloneNotSupportedException {
        ReferenceOrValue referenceOrValue = new ReferenceOrValue();
		// 测试基本数据类型
        int a=0;
        referenceOrValue.changeValue1(a);
        System.out.println(a);
        
        // 测试引用类型
        Person person = new Person();
        person.setNum(0);
        referenceOrValue.changeValue2(person);
        System.out.println(person);

		//测试String类型
        String str="abc";
        referenceOrValue.changeValue3(str);
        System.out.println(str);
    }
}
class ReferenceOrValue{
    public void  changeValue1(int num){
        num=100;

    }
    public void changeValue2(Person person){
        person.num=100;

    }
    public void changeValue3(String str){
         str="xxx";

    }
}
@Data
class Person{
    public int num;
}

结果:
在这里插入图片描述

思考:为什么是这样的结果?
首先,要明白基本数据类型有哪些? Java8大基本数据类型:byte,short,int,long,float,double,char,boolean,其他都为引用类型。
基本数据类型在作为参数时传递的是值,引用参数类型在参数传递时传递的是地址。
回到题目中

  • 向changeValue1()函数中传递的是基本数据类型,将0作为值向函数中传递(相当于int的一个副本),但在函数中将变量设置为100,此100只在函数中有效所以不会影响到外面。
  • 向changeValue2()函数中传递的是引用类型,将对象的地址向函数中传递(相当于本身),在函数中已经将此地址的值修改,虽然没有将此变量向外暴露出去,但内存值已经发生改变
  • 向changeValue3()函数中传递的是String类型,String类型也是引用类型,之所以单拿出来是因为它比较特殊。传递的是引用,在方法内此引用的内存的地址被改为"xxx"按理说,最后输出变量的值应该为"xxx",但结果却不是这样,这就是String类型特殊的地方,这是为什么呢?
    String在JVM的方法区的元空间有一个常量池,在对String变量赋值时,原则就是在常量池去寻找相同的值,有则引用,没有则创建。在方法中,“xxx”在元空间并不存在,(元空间中只存在"abc")所以,就会在常量池重新开辟一个地址,然后在str指向新的地址,但函数内的str只在函数内有效,原str指向的内容并未被破坏,因此值依然为abc。
    在这里插入图片描述
    思考:如果改成这样,那么底层的引用关系是怎样的呢?
    在这里插入图片描述
    在这里插入图片描述

引用拷贝

引用拷贝是直接复制栈中指向堆内存的地址,例如:Object o1=new Object(),左边的变量在栈空间的地址为0x001,右边对象在堆空间的地址为 0x100,这时进行引用复制,Object o2=o1,这时没有进行new,在栈空间开辟了一个新地址 0x002,但堆内存还是之前的0x100。这时,如果我操作o2,堆内存的数据就会发生变化,这时,同样指向此堆空间的还有o1对象,因此o1的数据也会发生变化。
在这里插入图片描述

代码演示:(这里使用lombok只为简化代码,对结果没有影响)
在这里插入图片描述
结果:
在这里插入图片描述
可以证明:在复制了原对象进行引用拷贝后,修改副本但原对象却发生变化,可以证明只是栈空间进行了复制,引用同一个堆空间。

对象拷贝

对象拷贝包括深拷贝与浅拷贝。都是重新在堆空间又开辟了一个空间,但拷贝的深度不同。概括来说:深拷贝就是将对象中包含的其他对象也开辟堆空间进行拷贝,浅拷贝只开辟空间重新存储此对象的中的信息,至于此对象中的其他对象采用的是引用拷贝。
注意:无论是神拷贝还是浅拷贝 类都需要实现 Cloneable 接口,重写Clone()方法

浅拷贝

浅拷贝只需要要考虑要复制的对象,而不会关心对象中的其他对象。
如图:这是在对象中不含有其他对象的浅拷贝,拷贝后,原型与副本是两个独立的空间,对副本的修改不会影响到原型对象
在这里插入图片描述
代码演示:

public class Application {

    public static void main(String[] args) throws CloneNotSupportedException {
        Teacher t1 = new Teacher().setName("老师1").setAge(100);
        Teacher t2 = (Teacher) t1.clone();
        System.out.println("复制后的副本"+t2);
        t1.setName("测试");
        System.out.println("修改后的原型"+t1);
        System.out.println("原型修改后的副本"+t2);
    }
}

@Data
@Accessors(chain = true)
@ToString
class Teacher implements Cloneable{
    private String name;
    private int age;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

结果:
在这里插入图片描述

思考:如果对象中不含有其他对象,那浅拷贝与深拷贝的效果岂不是一样?
思考:其他类的对象作为此类的属性,那么其他类的对象发生修改,此类会不会发生修改?或者或,属性的值是使用的是原对象堆空间还是一个新堆空间?
是原对象的堆空间,对原对象发生改动,会影响到类中作为属性的对象
代码演示:
在这里插入图片描述

如图:浅拷贝对象中含有其他对象的情况
在这里插入图片描述
代码演示:

public class Application {

    public static void main(String[] args) throws CloneNotSupportedException {
        Teacher teacher = new Teacher().setName("老师1").setAge(100);
        Student s1 = new Student().setName("学生1").setAge(20).setTeacher(teacher);

        Student s2=s1.clone();
      

        s1.getTeacher().setName("原型中属性对象发生修改");

        System.out.println("复制后 原型修改为"+s1);
        System.out.println("复制 并原型修改后 副本"+s2);
    }
}

@Data
@Accessors(chain = true)
@ToString
class Teacher{
    private String name;
    private int age;
}
@Data
@Accessors(chain = true)
@ToString
class Student implements Cloneable{
    private String name;
    private int age;
    private Teacher teacher;
    @Override
    protected Student clone() throws CloneNotSupportedException {
        return (Student)super.clone();
    }
}

结果:复制了原型后,对副本发生改动
在这里插入图片描述

  • 思考:如果对原型对象的name属性发生修改,副本的name属性会不会发生修改?或者说,clone()后的String类型数据,是深拷贝还是浅拷贝?
    因为是浅拷贝,不是引用拷贝,对象中基本数据数据类型会被重新复制到一个独立的堆空间中,由于name是String类型,虽然不是基本数据类型,但String在方法区有常量池,由属性指向它的地址。那按照引用对象的常理说,原型与副本都是保存的name的地址空间,如果在原型中将name的值改变,由于指向同一块地址,副本的name值也应该改变。但String特别不同的是,如果将原型将name值修改,并不会将在原地址修改,而是又重新开辟一个地址存放修改的值,然后再重新指向这个新地址。这样,原型和副本的String类型指向的地址就会不相同,任何一方的String值修改都不会影响到另一方。
  • 思考:在这个例子中,如果我直接去修改Teacher对象的name,原型与副本中Teacher属性会不会发生改变?
    由于原型和副本指向的是同一个Teacher对象地址,原型修改,Teacher对象自己修改,副本修改,任何一方修改值都会影响到其他两方。

深拷贝

递归拷贝,将这个对象中所有其他对象也拷贝一份在堆空间中独立存储。深拷贝相比于浅拷贝速度较慢并且花销较大。

如图:
在这里插入图片描述
代码演示:深拷贝具体怎样拷贝,需要在clone()方法中定义,并不是一味的将所有引用对象都复制一份,也可以自己选择性的复制。

public class Application {

    public static void main(String[] args) throws CloneNotSupportedException {
        Teacher teacher = new Teacher().setName("老师1").setAge(100);
        Student s1 = new Student().setName("学生1").setAge(20).setTeacher(teacher);

        Student s2=s1.clone();
        
        teacher.setName("测试");
        System.out.println(s1);
        System.out.println(s2);

    }
}

@Data
@Accessors(chain = true)
@ToString
class Teacher implements Cloneable{
    private String name;
    private int age;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
@Data
@Accessors(chain = true)
@ToString
class Student implements Cloneable{
    private String name;
    private int age;
    private Teacher teacher;
    @Override
    protected Student clone() throws CloneNotSupportedException {
        Teacher teacher = (Teacher) this.teacher.clone(); //注意:这里指定了对Teacher属性的引用进行复制 ,要求teacher属性也要实现Cloneable接口
        Student student = (Student) super.clone(); // 再重新将复制出的,设置给副本对象student
        student.setTeacher(teacher);
        return student;
    }
}

结果:对Teacher修改没有影响到深拷贝的对象。
在这里插入图片描述

思考:如果这时这样互换会发生什么?为什么这样?
在这里插入图片描述
结果:
在这里插入图片描述
因为在拷贝原型作为副本时,是以原型当前状态作为拷贝的,在原型拷贝前其引用的对象name属性已经发生了改变,这时在进行拷贝,副本中拷贝来的也是改变后的数据

原型模式

原型模式第一件事想到的就是克隆,就在原来的基础上直接复制出和它一模一样的出来。假设这里有一份原型文件,从原型A复制出文件a,文件b,文件c。对这a,b,c三个文件任意的修改都不会影响到原型文件。
原型模式的好处有哪些呢?
如果原型对象是从数据库中查取出来的,那么如果再想去在获取这个相同的对象,就需要再次查询一次数据库。那如果有1次,100次呢?因此可以从原对象拷贝。至于是深拷贝还是浅拷贝要根据实际情况,上面已经介绍,这里不再赘述。
克隆出一个对象虽然也占据了堆内存的空间(相互不会干扰的原因),但相比于重新new再恢复至原型状态少了很多步骤,效率更高。
思考:为什么要从原型对象中拷贝,而不是直接拿原型来使用?这样不是更加节省资源吗 ?
如果全局中的对象不是进行拷贝获取的,而是从栈复制一份原对象在堆空间的地址。如果在一个地方,对原型对象发生了修改,全部的对象都将发生修改。
原型模式创建模式的一种,它的核心就是在多处使用同一个对象时,为了防止对原对象的修改使得全局对象发生变化,将原型保护起来,按照原型克隆出一个个副本去使用。就像博物馆为了防止原物的损坏,通常会一比一做出多个模型去展示,这样既能通过模型知道到原型的样子,而且即使模型坏了也不会影响到原型和其他模型。那为什么不直接再造一个相同的原物呢?肯定是创建一个原物的难度要远远大于克隆一个原物啊。因此,原型模式有以下几个使用场景:

  • 资源优化场景:
    类的初始化相较于克隆一个对象要花费更多的资源。
  • 安全问题:
    创建一个新的对象需要更多的权限,或者说想要恢复至原对象数据有权限问题
  • 一个对象被多个修改者修改
    一个对象可能被多处使用,而在使用时可能会对对象发生修改,而如果多个对象间是引用指向的是堆空间中同一块地址,那么全局使用的此对象都将发生变化。如果不想发生变化,创建新对象再赋值由耗费资源。这时就可以使用原型模式

原型模式通常要配合工厂模式使用,原型模式生产出克隆对象,再由工厂模式交给使用者。

下面使用代码演示在原型类上进行修改产生的问题:
例如:我要从数据库中查询一条数据封装到一个pojo对象中,为了减少系统压力,只查询一次数据库,这个对象会被多个地方访问,修改。

public class Application {

    public static void main(String[] args) throws CloneNotSupportedException {
        MyMyBatis myMyBatis = new MyMyBatis();
        User u1 = myMyBatis.getByDB("张三");
        u1.setAge(0);
        User u2 = myMyBatis.getByDB("张三");
        System.out.println(u1 == u2);
        System.out.println(u2);
    }
}
class MyMyBatis {
   HashMap<String,User>userHashMap=new HashMap<String, User>();
    User getByDB(String name) {
    // 模拟这里查询库,为了减少数据库的开销,同一个name只创建一个对象,然后放在map中,向外提供方法供到处使用
        User user = userHashMap.get(name);
        if(user==null){
            User u1 = new User().setUsername(name).setAge(100);
            user=u1;
            userHashMap.put(name,user);
        }
        return user;
    }
}
@Data
@ToString
@Accessors(chain = true)
class User {
    private String username;
    private int age;
}

结果:
在这里插入图片描述
有一个地方修改了此对象,另一个地方获取的还是此对象,一个对象到处共用,这就使得修改对象使得在另一个地方是可见的。
思考:为什么两次调用方法从userHashMap中获取的对象是同一个?
userHashMap中存放的是对象的地址,是在第一次new开启堆空间后将name作为key,值为对象地址存放在hashMap中,每次获取对象都是同一个对象即同一个堆地址。在一个地方对该地址的数据进行修改,这就使得其他地方通过该地址获取的数据是被更改后的。

使用单例模式对上面进行改装:

public class Application {

    public static void main(String[] args) throws CloneNotSupportedException {
        MyMyBatis myMyBatis = new MyMyBatis();
        User u1 = myMyBatis.getByDB("张三");
        u1.setAge(0);
        User u2 = myMyBatis.getByDB("张三");
        System.out.println(u1 == u2);
        System.out.println(u2);
    }
}

class MyMyBatis {
    private HashMap<String,User> hashMap=new HashMap<>();
    User getByDB(String name) throws CloneNotSupportedException {
        User user=hashMap.get(name);
        User user1=null;

        if(user==null){

            user = new User().setUsername(name).setAge(100);
            hashMap.put(name,user);
            hashMap.put(name,user);
        }
        user1= (User) user.clone();
        return user1;
    }
}
@Data
@ToString
@Accessors(chain = true)
class User implements Cloneable{
    private String username;
    private int age;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

结果:
在这里插入图片描述
将原型保护起来,克隆出多个对象向外提供, 一个克隆对象的修改不会影响到其他其他对象和原型。
思考:为什么克隆对象间不会相互影响?
因为每一个克隆对象与原型在堆内存中都是一个个独立的空间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/196650.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

全网最详细地介绍mybatis-plus框架

文章目录1. 简介2. 特性3. 支持数据库4. 框架结构5. 开始使用5.1 数据源5.2 初始化工程6. 总结之前使用mybatis框架时&#xff0c;需要写大量的xml配置文件&#xff0c;维护起来比较繁琐。现在使用mybatis-plus&#xff0c;若是简单的curd操作&#xff0c;可以不用写xml文件&am…

maxwell解析mysql的binlog数据并保存到kafka使用

通过maxwell来实现binlog的实时解析&#xff0c;实现数据的实时同步 1、mysql创建一个maxwell用户 为mysql添加一个普通用户maxwell&#xff0c;因为maxwell这个软件默认用户使用的是maxwell这个用户&#xff0c; 进入mysql客户端&#xff0c;然后执行以下命令&#xff0c;进…

IDEA操作git commit后(push项目失败:Access token is expired),撤销commit,恢复到提交前的状态

1. 在IDEA操作push代码报错 remote: [session-e6423190] Oauth: Access token is expired 原因&#xff1a;这个问题其实就是因为你的本地电脑上安全中心存储Gitee密码过期导致的。 解决此问题可以参考以下链接&#xff1a;本以为修改下IDEA的settings下的Gitee账号密码就可以了…

若依框架文档开发手册----开发中常用功能模块

目录 前端 add.html 时间框 大文本框 Ajax校验 自定义校验 回显选中图片 JS对添加下拉列元素 edit.html 下拉列 回显时间 list.html 搜索栏 时间框 mapper.xml Table表格 格式化时间 前端 表格匹配字典值 表格增加.减少功能项 全局 其他 关闭标签页 输入框…

前端使用vue-pdf、pdf-lib、canvas 给PDF文件添加水印,并预览与下载

前端使用vue-pdf、pdf-lib 给pdf添加水印&#xff0c;并预览与下载效果预览使用第三方插件安装依赖插件import 导入依赖预览添加水印的pdf下载添加水印的pdf预览及下载总结完整代码效果预览 使用第三方插件 安装依赖插件 npm i vue-pdf --save npm i pdf-lib --save npm inst…

java之面向对象基础

1.类和对象1.1什么是对象万物皆对象&#xff0c;只要是客观存在的事物都是对象1.2什么是面向对象1.3什么是类类是对现实生活中一类具有共同属性和行为的事物的抽象类的特点&#xff1a;类是对象的数据类型类是具有相同属性和行为的一组对象的集合1.4什么是对象的属性属性&#…

微信小程序——使用npm包,安装 Vant weapp 组件库安装教程及使用vant组件

一.小程序对 npm 的支持与限制目前&#xff0c;小程序中已经支持使用 npm 安装第三方包&#xff0c;从而来提高小程序的开发效率。但是&#xff0c;在小程序中使用 npm 包有如下3个限制&#xff1a;&#x1f4dc;不支持依赖于 Node . js 内置库的包&#x1f4dc;不支持依赖于浏…

【软件测试】2023年的软件测试咋样?见鬼,我到底该如何进阶?

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 一谈到进阶&#xf…

Sitecore本地安装详细介绍

一、简介 Sitecore 是一种 CMS(内容管理系统,位于 Web 前端和后端办公系统或流程之间的软件系统),本文已当前最新的 10.2.0 版本为例,介绍如何安装部署。 二、环境准备 Sitecore 依赖于 IIS、SQL Server,在后续 Sitecore 安装之前,这两依赖需要提前安装完成 2.1 II…

【CTF】ctf中用到的php伪协议总结及例题(持续更)

目录 前言 关于文件包含漏洞 php伪协议总结 关于php://协议 参考自&#xff1a; 前言 本篇文章使用的靶场是buuctf上的web题目&#xff1a;[BSidesCF 2020]Had a bad day 进行点击选项得到一个这样的url 这里猜测存在sql注入&#xff0c;没测出来。或者可能有php伪协议读…

excel函数应用:如何写出IF函数多级嵌套公式

说到函数就不得不提起函数中最受欢迎的三大家族&#xff1a;求和家族、查找引用家族、逻辑家族&#xff01;&#xff01;&#xff01;没错&#xff01;今天我们要介绍的就是三大家族之一逻辑函数家族的领头人&#xff1a;IF函数——很多人难以理解IF函数的多级嵌套使用。其实&a…

shell 函数详解

目录 函数 一&#xff0c;什么是函数 二&#xff0c; 函数的返回值 三&#xff0c;函数语法 示例1&#xff1a; 示例2&#xff1a; 四&#xff0c;函数的调用 示例1&#xff1a; 示例2&#xff1a; 五&#xff0c;函数库文件 六&#xff0c; 递归函数 示例1&#xf…

Node.js 全局对象介绍

在学习 Javascript 之初&#xff0c;会接触一个概念&#xff1a;JS 由三部分组成&#xff0c;DOM BOM ECMAScript。其中前两者是宿主环境&#xff0c;也就是浏览器所提供的能力。后者才是 JS 语言本身的标准。 在上篇文章《Node.js入门&#xff08;1&#xff09;&#xff1a…

SpringMVC之响应

目录 一&#xff1a;环境准备 二&#xff1a;响应页面[了解] 三&#xff1a;返回文本数据[了解] 四&#xff1a;响应JSON数据 SpringMVC接收到请求和数据后&#xff0c;进行一些了的处理&#xff0c;当然这个处理可以是转发给Service&#xff0c;Service层再调用Dao层完成的…

1月更新!EasyOps® 28+新功能“狂飙”上线~

2023节后&#xff0c;我们就要“搞事情”&#xff01; 一波新功能已上线&#xff0c;快看是不是你需要的&#xff01; 持续升级优化全平台产品&#xff0c; 只为成为你数字化变革最值得信赖的合作伙伴&#xff01; 优维EasyOps全平台28新功能来了&#xff01; ↓↓↓ 1、H…

Spring的后处理器之BeanFactoryPostProcessor

Spring的后处理器 Spring的后处理器是Spring对外开放的重要扩展点&#xff0c;允许我们介入到Bean的整个实例化流程中来&#xff0c;以达到动态注册BeanDefinition&#xff0c;动态修改BeanDefinition&#xff0c;以及动态修改Bean的作用。Spring主要有两种后处理器&#xff1…

【车载开发系列】StatusOfDTC的解析

【车载开发系列】StatusOfDTC的解析 StatusOfDTC的解析【车载开发系列】StatusOfDTC的解析StatusOfDTC概念StatusOfDTC列表StatusOfDTC状态掩码Bit 0: TestFailedBit 1: testFailedThisOperationCycleBit 2: pendingDTCBit 3: confirmedDTCBit 4: testNotCompletedSinceLastCle…

【操作系统】3、内存管理

文章目录三、内存管理3.1 内存基础3.1.1 内存管理概念3.1.2 程序装入与链接3.1.3 内存保护3.2 内存空间的分配与回收3.2.1 连续分配管理方式3.2.1.1 单一连续分配3.2.1.2 固定分区分配3.2.1.3 动态分区分配3.2.2 动态分区分配算法3.2.2.1 首次适应算法3.2.2.2 最佳适用算法3.2.…

【数据库原理与SQL Server应用】Part05——表和表数据操作

【数据库原理与SQL Server应用】Part05——表和表数据操作一、表概念1.1 表结构1.2 表类型1.3 数据类型二、创建表2.1 管理工具界面方式创建表2.2 命令行方式创建表三、修改表3.1 管理工具界面方式修改表3.2 命令行方式修改表四、删除表五、表数据操作5.1 管理工具界面方式操作…

怎么快速选择出色的香港服务器

相信一些大规模企业或站长都不满足于普通的香港VPS&#xff0c;虽然它也拥有很不错的性能与速度&#xff0c;但远远比不上香港服务器。但是&#xff0c;对于初次使用香港服务器的用户来说&#xff0c;选择起来肯定是要经过一番考虑的&#xff0c;那么&#xff0c;有没有什么简单…