【JAVA零基础入门系列】Day14 Java对象的克隆

  今天要介绍一个概念,对象的克隆。本篇有一定难度,请先做好心理准备。看不懂的话可以多看两遍,还是不懂的话,可以在下方留言,我会看情况进行修改和补充。

  克隆,自然就是将对象重新复制一份,那为什么要用克隆呢?什么时候需要使用呢?先来看一个小栗子:

  简单起见,我们这里用的是Goods类的简单版本。

public class Goods {
    private String title;
    private double price;

    public Goods(String aTitle, double aPrice){
        title = aTitle;
        price = aPrice;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public void setTitle(String title) {
        this.title = title;
    }
  //用于打印输出商品信息
    public void print(){
        System.out.println("Title:"+title+" Price:"+price);
    }
}

  然后我们来使用这个类。

public class GoodsTest {
    public static void main(String[] args){
        Goods goodsA = new Goods("GoodsA",20);
        Goods goodsB = goodsA;
        System.out.println("Before Change:");
        goodsA.print();
        goodsB.print();

        goodsB.setTitle("GoodsB");
        goodsB.setPrice(50);
        System.out.println("After Change:");
        goodsA.print();
        goodsB.print();
    }
}

  我们创建了一个Goods对象赋值给变量goodsA,然后又创建了一个Goods变量,并把goodsA赋值给它,先调用Goods的print方法输出这两个变量中的信息,然后调用Goods类中的setTitle和setPrice方法来修改goodsB中的对象内容,再输出两个变量中的信息,下面是输出:

Before Change:
Title:GoodsA Price:20.0
Title:GoodsA Price:20.0
After Change:
Title:GoodsB Price:50.0
Title:GoodsB Price:50.0

  这里我们发现了灵异事,我们明明修改的是goodsB的内容,可是goodsA的内容也同样发生了改变,这究竟是为什么呢?别心急,且听我慢慢道来。

  在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。使用等号赋值都是进行值传递的,如将一个整数型变量赋值给另一个整数型变量,那么后者将存储前者的值,也就是变量中的整数值,对于基本类型如int,double,char等是没有问题的,但是对于对象,则又是另一回事了,这里的goodsA和goodsB都是Goods类对象的变量,但是它们并没有存储Goods类对象的内容,而是存储了它的地址,也就相当于C++中的指针,如果对于指针不了解,那我就再举个栗子好了。我们之前举过一个栗子,把计算机比作是仓库管理员,内存比作是仓库,你要使用什么类型的变量,就需要先登记,然后管理员才会把东西给你,但如果是给你分配一座房子呢?这时候不是把房子搬起来放到登记簿粒,而是登记下房子的地址,这里的地址就是我们的类对象变量里记录的内容,所以,当我们把一个类对象变量赋值给另一个类对象变量,如goodsB = goodsA时,实际上只是把A指向的对象地址赋值给了B,这样B也同样指向这个地址,所以这时候,goodsA和goodsB操作的是同一个对象。

  所以,如果只是简单的赋值的话,之后对于goodsA和goodsB的操作都将影响同一个对象,这显然不是我们的本意。也许你还会问,直接再new一个对象不就好了,确实如此,但有时候,如果我们需要保存一个goodsA的副本,那就不仅仅要new一个对象,还需要进行一系列赋值操作才能将我们的新对象设置成跟goodsA对象一样,而且Goods类越复杂,这个操作将会越繁琐,另外使用clone方法还进行本地优化,效率上也会快很多,总而言之,就是简单粗暴。

  那如何使用克隆呢?这里我们就要介绍我们牛逼哄哄的Object类了,所有的类都是Object类的子类,虽然我们并没有显式声明继承关系,但所有类都难逃它的魔掌,它有两个protected方法,其中一个就是clone方法。

  下面我来展示一波正确的骚操作:

//要使用克隆方法需要实现Cloneable接口
public class Goods implements Cloneable{
    private String title;
    private double price;

    public Goods(String aTitle, double aPrice){
        title = aTitle;
        price = aPrice;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void print(){
        System.out.println("Title:"+title+" Price:"+price);
    }

    //这里重载了接口的clone方法
    @Override
    protected Object clone(){
        Goods g = null;    //这里是异常处理的语句块,可以先不用了解,只要知道是这样使用就好,之后的文章中会有详细的介绍
        try{
            g = (Goods)super.clone();
        }catch (CloneNotSupportedException e){
            System.out.println(e.toString());
        }
        return g;
    }
}

  其实修改的地方只有两个,一个是定义类的时候实现了Cloneable接口,关于接口的知识在之后会有详细说明,这里只要简单理解为是一种规范就行了,然后我们重载了clone方法,并在里面调用了父类也就是(Object)的clone方法。可以看到我们并没有new一个新的对象,而是使用父类的clone方法进行克隆,关于try catch的知识这里不做过多介绍,之后会有文章做详细说明,这里只需要理解为try语句块里是一个可能发生错误的代码,catch会捕获这种错误并进行处理。

  接下来我们再使用这个类的克隆方法:

public class GoodsTest {
    public static void main(String[] args){
        Goods goodsA = new Goods("GoodsA",20);
        Goods goodsB = (Goods)goodsA.clone();
        System.out.println("Before Change:");
        goodsA.print();
        goodsB.print();

        goodsB.setTitle("GoodsB");
        goodsB.setPrice(50);
        System.out.println("After Change:");
        goodsA.print();
        goodsB.print();
    }
}

  我们仅仅是把赋值改成了调用goodsA的clone方法并进行类型转换。输出如下:

Before Change:
Title:GoodsA Price:20.0
Title:GoodsA Price:20.0
After Change:
Title:GoodsA Price:20.0
Title:GoodsB Price:50.0

  看,这样不就达到我们目的了吗?是不是很简单?

  但是别高兴的太早,关于克隆,还有一点内容需要介绍。

  克隆分为浅克隆和深克隆。我们上面使用的只是浅克隆,那两者有什么区别呢?这里再举一个栗子,使用的是简化版的Cart类:

public class Cart implements Cloneable{
    //实例域
    Goods goodsList = new Goods("",0);//简单起见,这里只放了一个商品
    double budget = 0.0;//预算

    //构造函数
    public Cart(double aBudget){
        budget = aBudget;
    }

    //获取预算
    public double getBudget() {
        return budget;
    }

    //修改预算
    public void setBudget(double aBudget) {
        budget = aBudget;
    }

    //这里只是简单的将商品进行了赋值
    public void addGoods(Goods goods){
        goodsList = (Goods) goods.clone();
    }

    //这是为了演示加上的代码,仅仅将商品标题修改成新标题
    public void changeGoodsTitle(String title){
        goodsList.setTitle(title);
    }

    //打印商品信息
    public void print(){
        System.out.print("Cart内的预算信息:"+budget+" 商品信息:");
        goodsList.print();
    }

    //重载clone方法
    @Override
    protected Object clone(){
        Cart c = null;
        try{
            c = (Cart)super.clone();
        }catch (CloneNotSupportedException e ){
            e.printStackTrace();
        }
        return c;
    }
}

  这里将goodsList由数组改成了单个对象变量,仅仅用于演示方便,还增加了一个changeGoodsTitle方法,用于将商品的标题修改成另一个标题,接下来修改一下GoodsTest类:

public class GoodsTest {
    public static void main(String[] args){
        Goods goodsA = new Goods("GoodsA",20);//新建一个商品对象
        Cart cartA = new Cart(5000);//新建一个购物车对象
        cartA.addGoods(goodsA);//添加商品
        Cart cartB = (Cart) cartA.clone();//使用浅克隆
     //输出修改前信息
        System.out.println("Before Change:");
        cartA.print();
        cartB.print();
     //修改购物车A中的商品标题
        cartA.changeGoodsTitle("NewTitle");     //重新输出修改后的信息
        System.out.println("After Change:");
        cartA.print();
        cartB.print();
    }
}

  输出信息:

Before Change:
Cart内的预算信息:5000.0 商品信息:Title:GoodsA Price:20.0
Cart内的预算信息:5000.0 商品信息:Title:GoodsA Price:20.0
After Change:
Cart内的预算信息:5000.0 商品信息:Title:NewTitle Price:20.0
Cart内的预算信息:5000.0 商品信息:Title:NewTitle Price:20.0

  我们发现,虽然我们调用的是cartA中的方法修改购物车A中的商品信息,但购物车B中的信息同样被修改了,这是因为使用浅克隆模式的时候,成员变量如果是对象等复杂类型时,仅仅使用的是值拷贝,就跟我们之前介绍的那样,所以cartB虽然是cartA的一个拷贝,但是它们的成员变量goodsList却共用一个对象,这样就藕断丝连了,显然不是我们想要的效果,这时候就需要使用深拷贝了,只需要将Cart类的clone方法修改一下即可:

    @Override
    protected Object clone(){
        Cart c = null;
        try{
            c = (Cart)super.clone();
            c.goodsList = (Goods) goodsList.clone();//仅仅添加了这段代码,将商品对象也进行了克隆
        }catch (CloneNotSupportedException e ){
            e.printStackTrace();
        }
        return c;
    }

  现在再来运行一下:

Before Change:
Cart内的预算信息:5000.0 商品信息:Title:GoodsA Price:20.0
Cart内的预算信息:5000.0 商品信息:Title:GoodsA Price:20.0
After Change:
Cart内的预算信息:5000.0 商品信息:Title:NewTitle Price:20.0
Cart内的预算信息:5000.0 商品信息:Title:GoodsA Price:20.0

  这样就得到了我们想要的结果了。

  这样,对象的拷贝就讲完了。

  吗?

  哈哈哈哈,不要崩溃,并没有,还有一种更复杂的情况,那就是当你的成员变量里也包含引用类型的时候,比如Cart类中有一个CartB类的成员变量,CartB类中同样存在引用类型的成员变量,这时候,就存在多层克隆的问题了。这里再介绍一个骚操作,只需要了解即可,那就是序列化对象。操作如下:

import java.io.*;

public class Cart implements Serializable{
    //实例域
    Goods goodsList = new Goods("",0);//简单起见,这里只放了一个商品
    double budget = 0.0;//预算

    //构造函数
    public Cart(double aBudget){
        budget = aBudget;
    }

    //获取预算
    public double getBudget() {
        return budget;
    }

    //修改预算
    public void setBudget(double aBudget) {
        budget = aBudget;
    }

    //这里只是简单的将商品进行了赋值
    public void addGoods(Goods goods){
        goodsList = (Goods) goods.clone();
    }

    //这是为了演示加上的代码,仅仅将商品标题修改成新标题
    public void changeGoodsTitle(String title){
        goodsList.setTitle(title);
    }

    //打印商品信息
    public void print(){
        System.out.print("Cart内的预算信息:"+budget+" 商品信息:");
        goodsList.print();
    }
  //这里是主要是骚操作
    public Object deepClone() throws IOException, OptionalDataException,ClassNotFoundException {
        // 将对象写到流里
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        ObjectOutputStream oo = new ObjectOutputStream(bo);
        oo.writeObject(this);
        // 从流里读出来
        ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi = new ObjectInputStream(bi);
        return (oi.readObject());
    }
}

  关于这种方法我就不多做介绍了,大家只需要知道有这样一种方法就行了,以后如果遇到了需要使用这种情况,就知道该怎样处理了。

  这里总结一下,对象的克隆就是把一个对象的当前状态重新拷贝一份到另一个新对象中,两个对象变量指向不同的对象,浅克隆仅仅调用super.clone()方法,对成员变量也只是简单的值拷贝,所以当成员变量中有数组,对象等复杂类型的时候,就会存在藕断丝连的混乱关系,深拷贝不仅仅调用super.clone()方法进行对象拷贝,将对象中的复杂类型同样进行了拷贝,这样两个对象就再无瓜葛,井水不犯河水了。

  至此,对象的克隆就真正的结束了,欢迎大家继续关注!如有不懂的问题可以留言。也欢迎各位大佬来批评指正。喜欢我的教程的话记得动动小手点下推荐,也欢迎关注我的博客。

时间: 2024-11-03 22:15:06

【JAVA零基础入门系列】Day14 Java对象的克隆的相关文章

【JAVA零基础入门系列】Day12 Java类的简单应用

俗话说的好,实践出真知,所以除了理论知识掌握扎实以外,更重要的是要多加操练,这样才能掌握核心科技. 今天我们就用刚学会的类来实践一下,目标便是完成上一篇中的剁手任务. 我们的商品类已经准备好了,代码重新罗列一次,这里增加了一个重载方法,关于重载这里不过多介绍,以后会专门文章介绍,这里把它当成一个普通方法就好了,主要用来比较两个商品是否相等,如果对代码不熟悉的话可以再返回去看一下上一篇文章. 这里顺便介绍一下Java注释的正确使用姿势.注释是插在源代码中间用于对代码进行说明的文字,不会被编译和执行

【JAVA零基础入门系列】Day13 Java类的继承与多态

继承是类的一个很重要的特性,什么?你连继承都不知道?你是想气死爸爸好继承爸爸的遗产吗?(滑稽) 开个玩笑,这里的继承跟我们现实生活的中继承还是有很大区别的,一个类可以继承另一个类,继承的内容包括属性跟方法,被继承的类被称为父类或者基类,继承的类称为子类或者导出类,在子类中可以调用父类的方法和变量.在java中,只允许单继承,也就是说 一个类最多只能显示地继承于一个父类.但是一个类却可以被多个类继承,也就是说一个类可以拥有多个子类.这就相当于一个人不能有多个父亲一样(滑稽,老王表示不服). 话不多

2019最新版Java零基础入门视频教程(全套)

为了解决Java学习初学者在网上找视频难的事情,本人整理了一份2019年度最新版的Java学习视频教程.希望看到这份视频的你们都能找到一份称心的工作,技术上都能得到进一步的提升,好东西就要分享给你们,加油! 目录结构如下: 01.JavaSE基础 02.html 03.css 04.javascript 05.mysql 06.jdbc 07.xml 08.tomcat 09.servlet和jsp 10.ajax和json 11.记录日志 12.数据库连接池 13.maven 14.开发工具I

开始系列教程:Java零基础七天入门

本教程雄心勃勃,保证七天之内从零基础开始,学会java,达到什么程度呢? 很简单:达到之后可以自学android和J2EE开发的程度. 不一样的思路,敬请期待...

零基础入门学习java,应该如何快速打好Java基础?

零基础入门学习java,应该如何快速打好Java基础?从大学到现在,我使用Java已经将近20年,日常也带实习生,还在公司内部做training,所以可以分享下我的经验,希望对你有用. 创一个小群,供大家学习交流聊天如果有对学java方面有什么疑惑问题的,或者有什么想说的想聊的大家可以一起交流学习一起进步呀.也希望大家对学java能够持之以恒java爱好群,如果你想要学好java最好加入一个组织,这样大家学习的话就比较方便,还能够共同交流和分享资料,给你推荐一个学习的组织:快乐学习java组织

Java Script基础(九) 下拉列表对象

Java Script基础(九) 下拉列表对象 Select下拉列表 属性: options[]:返回包含下拉列表中的所有选项的一个数组. selectedIndex:设置返回下拉列表中被选项目的索引号. length:返回下拉列表框中选项的数目.如果设置为0,表示清空列表框. 方法: add(new,old):添加一个选项.如果old为空则添加到末尾,old有值则添加到old之前. 事件: onchange:当改变选项时调用的事件. Option对象 属性: text:设置或返回某个选项的文本

零基础如何系统学习Java Web

零基础如何系统学习Java Web? 我来给你说一说 你要下决心,我要转行做开发,这样你才能学成. 你要会打字,我公司原来有一个程序员,打字都是两个手一指禅,身为程序员你一指禅怎么写出的代码,半个月后被辞退了,当然我们还是朋友. 前两个条件都符合了你就可以学了,首先要了解web是什么,一般呢,java web开发无外乎就这么两大类,第一,互联网公司,第二,软件公司.对于互联网公司和软件公司还有一些差别,互联公司是面向广大网民的,会有专门的ui设计,前台开发,后台代码开发,ios开发,androi

Java 零基础跑起第一个程序

Java 零基础跑起第一个程序 一 概述 1  java代码编译 编译后才能在计算机中运行,编译就是把人能看懂的代码转换成机器能看懂的形式 2 java的优点 一次编译,到处运行.因为java代码是在虚拟机中运行,虚拟机消除了不同操作系统间的差异.Java Virtual Machine---简称JVM 3  java的版本 SUN公司发布Java1.2以后,叫把Java叫做java2了,所以现在都叫J2XX. 功能上分为三个版本: J2SE 标准版----现在大部分开发都用它 J2EE 企业版

零基础如何迅速学好Java

对于零基础的朋友来说,能够迅速的学好Java,那将是一件特别有成就感,也特别有快感的事情,但是零基础如何迅速学好Java?这就是零基础朋友特别苦恼的问题,你在学Java的时候有什么秘招可以分享吗?今天长沙尚学堂小编将为大家带来迅速学好Java的秘招,希望对你有所帮助. 零基础如何迅速学好Java? 1. 对于零基础的朋友来说,选择一个人关门造车的学Java,那么Java学成之日在何时不知,Java学完能找到工作吗也不知,最关键的是这种学习方式一点都不迅速,很多时候都是在做无用功,特别浪费时间,并