【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 = null;
      ObjectOutputStream oo = null;
      ObjectInputStream oi = null;
      ByteArrayInputStream bi = null;
      try{
        bo = new ByteArrayOutputStream();
         oo = new ObjectOutputStream(bo);
oo.writeObject(this);
// 反序列化
   bi = new ByteArrayInputStream(bo.toByteArray());
    oi = new ObjectInputStream(bi);
        Object obj = oi.readObject();
      }
      catch(Exception e){
        e.printStackTrace();
      }
      finally {
        // 关闭流
        try{
          bo = null;
          bi = null;
          if(oo != null){
            oo.close();
          }
          if(oi != null){
            oi.close();
          }
        }catch(Eception e){
          e.printStackTrace();
        }
      }
return obj;
}
}

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

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

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

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

  1. 【JAVA零基础入门系列】Day15 对象的比较

    最近一直有事,博客也停笔了一段时间,十分抱歉. 这一篇主要讲讲对象的比较,什么是对象的比较,我们知道两个数值类型只需要用"=="符号即可进行相等判断,但如果是两个Goods对象呢? ...

  2. 【JAVA零基础入门系列】Day11 Java中的类和对象

    今天要说的是Java中两个非常重要的概念--类和对象. 什么是类,什么又是对象呢?类是对特定集合的概括描述,比如,人,这个类,外观特征上,有名字,有年龄,能说话,能吃饭等等,这是我们作为人类的相同特征 ...

  3. 【JAVA零基础入门系列】Day2 Java集成开发环境IDEA

    开发环境搭建好之后,还需要一个集成开发环境也就是IDE来进行编程.这里推荐的IDE是IDEA,那个老掉牙的Eclipse还是先放一边吧,(手动滑稽). IDEA的下载地址:http://www.jet ...

  4. 【JAVA零基础入门系列】Day4 变量与常量

    这一篇主要讲解Java中的变量,什么是变量,变量的作用以及如何声明,使用变量. 那么什么是变量?对于初学者而言,可以将变量理解为盒子,这些盒子可以用来存放数据,不同类型的数据需要放在对应类型的盒子里. ...

  5. 【JAVA零基础入门系列】Day5 Java中的运算符

    运算符,顾名思义就是用于运算的符号,比如最简单的+-*/,这些运算符可以用来进行数学运算,举个最简单的栗子: 已知长方形的长为3cm,高为4cm,求长方形的面积. 好,我们先新建一个项目,命名为Rec ...

  6. 【JAVA零基础入门系列】Day6 Java字符串

    字符串,是我们最常用的类型,每个用双引号来表示的串都是一个字符串.Java中的字符串是一个预定义的类,跟C++ 一样叫String,而不是Char数组.至于什么叫做类,暂时不做过多介绍,在之后的篇章中 ...

  7. 【JAVA零基础入门系列】Day8 Java的控制流程

    什么是控制流程?简单来说就是控制程序运行逻辑的,因为程序一般而言不会直接一步运行到底,而是需要加上一些判断,一些循环等等.举个栗子,就好比你准备出门买个苹果,把这个过程当成程序的话,可能需要先判断一下 ...

  8. 【JAVA零基础入门系列】Day9 Java中的那个大数值

    什么是大数值?用脚趾头想也知道,当然是"大"的数值(233).Java中有两个用于表示大数值的类,BigInteger和BigDecimal,那到底能表示多大的数值呢?理论上,可以 ...

  9. 【JAVA零基础入门系列】Day10 Java中的数组

    什么是数组?顾名思义,就是数据的组合,把一些相同类型的数放到一组里去. 那为什么要用数组呢?比如需要统计全班同学的成绩的时候,如果给班上50个同学的成绩信息都命名一个变量进行存储,显然不方便,而且在做 ...

随机推荐

  1. 初识Hibernate之环境搭建

         相信所有做后端的程序员同行们,没有不知道Hibernate大名的.这是一个经典的轻量级Java EE持久层的解决方案,它使得我们程序员能以面向对象的思维操作传统的关系型数据库,这也是其存在的 ...

  2. C语言编译过程及数据类型

    写在前面 C语言可以称得上是高级语言中的低级语言,接下来一段时间,我会写一下文章关于c语言,把它的神秘面纱一 一揭开.下面主要是c语言的C语言编译过程及数据类型 源文件编译过程 为了使计算机能执行高级 ...

  3. 不可不知的socket和TCP连接过程

    html { font-family: sans-serif } body { margin: 0 } article,aside,details,figcaption,figure,footer,h ...

  4. Gaussian and Truncated Gaussian

    Everybody knows about Gaussian distribution, and Gaussian is very popular in Bayesian world and even ...

  5. SQL Server 后悔药 delete drop update

    国庆假期终于有时间做点事情 因为平常工作会做些数据库操作 可能会有所操作失误  参考一下 方法一 ApexSql 2016一个软件 http://www.cnblogs.com/gsyifan/p/A ...

  6. pgpool-II主备流复制的架设

    1.环境 OS: CentOS release 6.4 (Final) DB: postgresql 9.3.6 pgpool服务器: pgpool 172.16.0.240 数据库主服务器:mast ...

  7. Request.QueryString("id")与Request("id")区别

    Request从几个集合取数据是有顺序的,从前到后的顺序依次是 QueryString,Form,最后是ServerVariables.Request对象按照这样的顺序依次搜索这几个集合中的变量,如果 ...

  8. JQuery上传插件Uploadify详解及其中文按钮解决方案 .

    Uploadify有一个参数是 buttonText 这个无论你怎么改都不支持中文,因为插件在js里用了一个转码方法把这个参数的值转过码了,解码的地方在那个swf文件里,看不到代码,所以这条路不行. ...

  9. phpstudy升级mysql数据库

    因为MySQL支持全文索引的只有5.6以上,而我下的phpstudy只有5.5的版本,在导入数据库的时候因为该数据库的表内有使用全文索引,因此必须升级phpstudy的mysql版本,这里就把自己当升 ...

  10. pm2部署多个nodejs项目配置教程

    实际项目部署中,我们服务器在启动的时候需要自动启动node服务.以前是通过liunx自带的命令启动.但是随着后台微服务越来越多.每次发布新程序.修改脚本太麻烦了.于是换成PM2来做. 1.首先安装pm ...