零基础入门学习java第十四节: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.0Title:GoodsA Price:20.0After Change:Title:GoodsB Price:50.0Title: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.0Title:GoodsA Price:20.0After Change:Title:GoodsA Price:20.0Title: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.0Cart内的预算信息:5000.0 商品信息:Title:GoodsA Price:20.0After Change:Cart内的预算信息:5000.0 商品信息:Title:NewTitle Price:20.0Cart内的预算信息: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.0Cart内的预算信息:5000.0 商品信息:Title:GoodsA Price:20.0After Change:Cart内的预算信息:5000.0 商品信息:Title:NewTitle Price:20.0Cart内的预算信息: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()方法进行对象拷贝,将对象中的复杂类型同样进行了拷贝,这样两个对象就再无瓜葛,井水不犯河水了。

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

原文地址:https://www.cnblogs.com/java008/p/10715376.html

时间: 2024-10-27 13:53:51

零基础入门学习java第十四节:Java对象的克隆的相关文章

鱼C《零基础入门学习Python》1—9节课时知识点总结

第一节:我和python的第一次亲密接触 0. Python 是什么类型的语言? 答:脚本语言(Scripting language)是电脑编程语言,因此也能让开发者藉以编写出让电脑听命行事的程序.以简单的方式快速完成某些复杂的事情通常是创造脚本语言的重要原则,基于这项原则,使得脚本语言通常比 C语言.C++语言 或 Java 之类的系统编程语言要简单容易.也让脚本语言另有一些属于脚本语言的特性: 语法和结构通常比较简单 学习和使用通常比较简单 通常以容易修改程序的“解释”作为运行方式,而不需要

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

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

鱼C《零基础入门学习Python》10-17节课时知识点总结

第10讲:列表:一个打了激素的数组 1. 列表都可以存放一些什么东西?  我们说 Python 的列表是一个打了激素的数组,如果把数组比喻成集装箱,那么 Python 的列表就是一个大仓库,Ta 可以存放我们已经学习过的任何数据类型. 2. 向列表增加元素有哪些方法?  三种方法想列表增加元素,分别是:append().extend() 和 insert().    3. append() 方法和 extend() 方法都是向列表的末尾增加元素,请问他们有什么区别?  append() 方法是将

汇编入门学习笔记 (十四)—— 直接定址表

疯狂的暑假学习之  汇编入门学习笔记 (十四)-- 直接定址表 参考: <汇编语言> 王爽 第16章 1. 描述单元长度的标号 普通的标号:a,b assume cs:code code segment a:db 1,2,3,4,5,6,7,8 b:dw 0 start: mov si,offset a mov di,offset b mov ah,0 mov cx,8 s: mov al,cs:[si] add cs:[di],ax inc si loop s mov ax,4c00h in

大牛整理最全Python零基础入门学习资料

大牛整理最全Python零基础入门学习资料 0 发布时间:『 2017-11-12 11:56 』     帖子类别:『人工智能』  阅读次数:3504 (本文『大牛整理最全Python零基础入门学习资料』的责任编辑:老王) 摘要:大牛整理最全Python零基础入门学习资料 Python数据类型--数字 Python Number 数据类型用于存储数值. 数据类型是不允许改变的,这就意味着如果改变 Number 数据类型的值,将重新分配内存空间. var1 = 1 var2 = 10 您也可以使

【Python教程】《零基础入门学习Python》(小甲鱼)

[Python教程]<零基础入门学习Python>(小甲鱼) 讲解通俗易懂,诙谐. 哈哈哈. https://www.bilibili.com/video/av27789609 原文地址:https://www.cnblogs.com/F4NNIU/p/9765629.html

Python3零基础入门学习视频+源码+课件+习题-小甲鱼

目录 1. 介绍 2. 目录 3. 下载地址 1. 介绍 适用人群 完全零基础入门,不需要任何前置知识. 课程概述 本系列教程面向零基础的同学,是一个深入浅出,通俗易懂的Python3视频教程. 前半部分主要讲解Python3的语法特性,后半部分着重讲解Python3在爬虫.Tkinter.Pygame游戏开发等实例上的应用.整个系列共16个章节,前边13个章节从一个小游戏引入Python,逐步介绍Python的语法以及语言特色.最后3个章节为案例的演示,是前边内容的总结和提高. 其他介绍 2.

Egret入门学习日记 --- 第二十四篇(书中 9.12~9.15 节 内容)

第二十四篇(书中 9.12~9.15 节 内容) 开始 9.12节 内容. 重点: 1.TextInput的使用,以及如何设置加密属性. 操作: 1.TextInput的使用,以及如何设置加密属性. 创建exml文件,拖入组件,设置好id. 这是显示密码星号处理的属性. 创建绑定类. 实例化,并运行. 但是焦点在密码输入框时,密码是显示的. 暂时不知道怎么设置 “焦点在密码框上时,还是显示为 * 号” 的方法. 至此,9.12节 内容结束. 开始 9.13节 . 这个,和TextInput的使用

Scala入门到精通——第二十四节 高级类型 (三)

作者:摆摆少年梦 视频地址:http://blog.csdn.net/wsscy2004/article/details/38440247 本节主要内容 Type Specialization Manifest.TypeTag.ClassTag Scala类型系统总结 在scala中,类(class)与类型(type)是两个不一样的概念.我们知道类是对同一类型数据的抽象,而类型则更详细. 比方定义class List[T] {}, 能够有List[Int] 和 List[String]等详细类型