【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. win7旗舰版最新激活密钥

    Win7旗舰.企业.专业版的激活密钥(32位.64位均可用).FJGCP-4DFJD-GJY49-VJBQ7-HYRR2 AcerVQ3PY-VRX6D-CBG4J-8C6R2-TCVBD Alien ...

  2. 关于 String 自我理解

    String 的一些认识: String对象是不可变,所以使用 final 修饰 字符串拼接,合理利用 StringBuilder(线程非安全),StringBuffer 线程安全 常用方法就不详细介 ...

  3. ThreadLocal的理解与应用场景分析

    对于Java ThreadLocal的理解与应用场景分析 一.对ThreadLocal理解 ThreadLocal提供一个方便的方式,可以根据不同的线程存放一些不同的特征属性,可以方便的在线程中进行存 ...

  4. 分享基于分布式Http长连接框架--设计模型

    追求简单的设计. 也许你的设计功能很强大,但能够在满足你需求的前提下尽量简单明了设计. 当你的设计过于复杂的时候想想是不是有其它路可以走,你站在别人的角度想下,如果别人看了你的设计会不会心领神会,还是 ...

  5. httpd日志和日志轮替工具

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

  6. Ubuntu中MongoDB安装

    在Ubuntu中MongoDB有时候启动不起来,可以参考以下方法从新安装: 1.导入包管理系统使用的公钥 Ubuntu 的软件包管理工具(即dpkg和APT)要求软件包的发布者通过GPG密钥签名来确保 ...

  7. JAVAWEB复习资料-01

    CSS中@import和link两种插入样式表方式有什么不同? 1.link属于HTML标签,除了引入css文件之外还能定义RSS等,而@import只能用于加载CSS. 2.link在引用CSS时, ...

  8. 教你ASP.NET中如何防止注入攻击

    你应该在程序中验证所有的不信任输入.你应该假定所有的用户输入都是非法的.用户可以在应用程序中提供表单字段,查询字串,客户端cookies和浏览器环境值比如用户代理字串和IP地址等. 弱输入校验通常为注 ...

  9. MyBatis Generator配置示例

    (一).MBG介绍 MyBatis Generator(MBG)是一个Mybatis的代码生成器,它可以用来生成可以访问(多个)表的基础对象.MBG解决了对数据库操作有最大影响的一些简单的CRUD(插 ...

  10. 用于Mysql操作的MySqlHelper类

    using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Te ...