Java中Cloneable接口

原文地址:Java中Cloneable接口作者:我有一个梦

目录

预备知识
为什么要clone
Object的clone以及为什么如此实现
如何clone
对clone的态度
其他的选择
和Serializable的比较
性能

预备知识

为了理解java的clone,有必要先温习以下的知识。
java的类型,java的类型分为两大类,一类为primitive,如int,另一类为引用类型,如String,Object等等。
java引用类型的存储,java的引用类型都是存储在堆上的。

Java代码

 

  public class B {
    int a;
    Stringb;

    public B(inta, String b) {
      super();
       this.a =a;
       this.b =b;
    }
}
对这样一个引用类型的实例,我们可以推测,在堆上它的内存存储形式(除去指向class的引用,锁的管理等等内务事务所占内存),应该有一个int值表示a,以及一个引用,该引用指向b在堆上的存储空间。

为什么要clone

恩,因为需要。废话。
有名的GoF设计模式里有一个模式为原型模式,用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象.
简单的说就是clone一个对象实例。使得clone出来的copy和原有的对象一模一样。

插一个简单使用clone的例子,如果一个对象内部有可变对象实例的话,publicAPI不应该直接返回该对象的引用,以防调用方的code改变该对象的内部状态。这个时候可以返回该对象的clone。

问题来了,什么叫一模一样。
一般来说,有
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)
但是这些都不是强制的。
我们需要什么样的clone就搞出什么样的clone好了。
一般而言,我们要的clone应该是这样的。copy和原型的内容一样,但是又是彼此隔离的。即在clone之后,改变其中一个不影响另外一个。

Object的clone以及为什么如此实现

Object的clone的行为是最简单的。以堆上的内存存储解释的话(不计内务内存),对一个对象a的clone就是在堆上分配一个和a在堆上所占存储空间一样大的一块地方,然后把a的堆上内存的内容复制到这个新分配的内存空间上。
看例子。

Java代码

class User {
    Stringname;
    intage;
}

class Account implements Cloneable {
    Useruser;
    longbalance;

   @Override
    publicObject clone() throws CloneNotSupportedException {
       returnsuper.clone();
    }
}

Java代码

//user
 User user = new User(); 
 user.name = "user"; 
 user.age = 20;
 // account. 
 Account account = newAccount(); 
 account.user = user; 
 account.balance = 10000; 
 // copy. 
 Account copy = (Account)account.clone(); 
  
 //balance因为是primitive,所以copy和原型是相等且独立的。 
 Assert.assertEquals(copy.balance,account.balance); 
 copy.balance =20000;
// 改变copy不影响原型。 
 Assert.assertTrue(copy.balance !=account.balance); 
  
 //user因为是引用类型,所以copy和原型的引用是同一的。 
 Assert.assertTrue(copy.user ==account.user); 
 copy.user.name ="newName"; 
 // 改变的是同一个东西。 
 Assert.assertEquals("newName",account.user.name); 

恩,默认实现是帮了我们一些忙,但是不是全部。
primitive的确做到了相等且隔离。
引用类型仅仅是复制了一下引用,copy和原型引用的东西是一样的。
这个就是所谓的浅copy了。
要实现深copy,即复制原型中对象的内存copy,而不仅仅是一个引用。只有自己动手了。
等等,是不是所有的引用类型都需要深copy呢?
不是!
我们之所以要深copy,是因为默认的实现提供的浅copy不是隔离的,换言之,改变copy的东西,会影响到原型的内部。比如例子中,改变copy的user的name,影响了原型。
如果我们要copy的类是不可变的呢,如String,没有方法可以改变它的内部状态呢。

Java代码

  class User implements Cloneable{ 
       String name; 
       int age; 
     
       @Override 
       public Object clone() throws CloneNotSupportedException{ 
           return super.clone(); 
       } 
   } 

Java代码

// user.  
User user = new User();  
user.name = "user";  
user.age = 20;  
   
 // copy 
 User copy = (User)user.clone(); 
  
// age因为是primitive,所以copy和原型是相等且独立的。 
 Assert.assertEquals(copy.age,user.age); 
 copy.age = 30; 
 // 改变copy不影响原型。 
 Assert.assertTrue(copy.age !=user.age); 
  
 //name因为是引用类型,所以copy和原型的引用是同一的。 
 Assert.assertTrue(copy.name ==user.name); 
 //String为不可变类。没有办法可以通过对copy.name的字符串的操作改变这个字符串。 
 // 改变引用新的对象不会影响原型。 
 copy.name = "newname"; 
 Assert.assertEquals("newname",copy.name); 
 Assert.assertEquals("user",user.name); 
 

可见,在考虑clone时,primitive和不可变对象类型是可以同等对待的。

java为什么如此实现clone呢?
也许有以下考虑。
1 效率和简单性,简单的copy一个对象在堆上的的内存比遍历一个对象网然后内存深copy明显效率高并且简单。
2不给别的类强加意义。如果A实现了Cloneable,同时有一个引用指向B,如果直接复制内存进行深copy的话,意味着B在意义上也是支持Clone的,但是这个是在使用B的A中做的,B甚至都不知道。破坏了B原有的接口。
3有可能破坏语义。如果A实现了Cloneable,同时有一个引用指向B,该B实现为单例模式,如果直接复制内存进行深copy的话,破坏了B的单例模式。
4 方便且更灵活,如果A引用一个不可变对象,则内存deep copy是一种浪费。Shadowcopy给了程序员更好的灵活性。

如何clone
clone三部曲。
1 声明实现Cloneable接口。
2调用super.clone拿到一个对象,如果父类的clone实现没有问题的话,在该对象的内存存储中,所有父类定义的field都已经clone好了,该类中的primitive和不可变类型引用也克隆好了,可变类型引用都是浅copy。
3 把浅copy的引用指向原型对象新的克隆体。
给个例子。

Java代码

class User implements Cloneable {
          String name;
          int age;

          @Override
          public User clone() throws CloneNotSupportedException {
                  return (User) super.clone();
          }
    }

    classAccount implements Cloneable {
          User user;
          long balance;

          @Override
          public Account clone() throws CloneNotSupportedException {
                  Account account = null;

                  account = (Account) super.clone();
                  if (user != null) {
                          account.user = user.clone();
                  }

                  return account;
          }
    }

对clone的态度

clone嘛,我觉得是个好东西,毕竟系统默认实现已经帮我们做了很多事情了。
但是它也是有缺点的。
1 手工维护clone的调用链。这个问题不大,程序员有责任做好。
2 如果class的field是个final的可变类,就不行了。三部曲的第三步没有办法做了。

考虑一个类对clone的态度,有如下几种。
1 公开支持:好吧,按照clone三部曲实现吧。前提是父类支持(公开或者默默)。
2默默支持:不实现Cloneable接口,但是在类里面有正确的protected的clone实现,这样,该类不支持clone,但是它的子类如果想支持的话也不妨碍。
3 不支持:好吧,为了明确该目的,提供一个抛CloneNotSupportedException异常的protected的clone实现。
4 看情况支持:该类内部可以保存其他类的实例,如果其他类支持则该类支持,如果其他类不支持,该类没有办法,只有不支持。

其他的选择

可以用原型构造函数,或者静态copy方法来手工制作一个对象的copy。
好处是即使class的field为final,也不会影响该方法的使用。不好的地方是所有的primitive赋值都得自己维护。

和Serializable的比较

使用Serializable同样可以做到对象的clone。但是:
Cloneable本身就是为clone设计的,虽然有一些缺点,但是如果它可以clone的话无疑用它来做clone比较合适。如果不行的话用原型构造函数,或者静态copy方法也可以。

Serializable制作clone的话,添加了太多其它的东西,增加了复杂性。
1 所有的相关的类都得支持Serializable。这个相比支持Cloneable只会工作量更大
2Serializable添加了更多的意义,除了提供一个方法用Serializable制作Clone,该类等于也添加了其它的publicAPI,如果一个类实现了Serializable,等于它的2进制形式就已经是其API的一部分了,不便于该类以后内部的改动。
3当类用Serializable来实现clone时,用户如果保存了一个老版本的对象2进制,该类升级,用户用新版本的类反系列化该对象,再调用该对象用Serializable实现的clone。这里为了一个clone的方法又引入了类版本兼容性的问题。不划算。

性能

不可否认,JVM越来越快了。
但是系统默认的native实现还是挺快的。
clone一个有100个元素的int数组,用系统默认的clone比静态copy方法快2倍左右。

原文地址:http://www.javaeye.com/topic/483469
可以对考的文章:http://blog.sina.com.cn/s/blog_62c1c11f0100fpht.html
        http://student.csdn.net/space.php?uid=111253&do=blog&id=8905

时间: 2024-09-13 10:25:52

Java中Cloneable接口的相关文章

有关java中的 接口 没有new 就可以赋值?

问题描述 有关java中的 接口 没有new 就可以赋值? 就是private Ishape=shape://Ishape为接口public shapeProc(Ishape shape){this.shape=shape:}//shapeProc 为构造函数 这为什么可以编译通过呢?小白,求指导 解决方案 shape是传进来的参数,自然是调用它的代码负责初始化.肯定有这样的代码:某实现了ISharp的类 shape = new 某实现了ISharp的类();xxx.shapeProc(shar

java序列化接口-java中序列化接口和parclable接口有什么区别和联系

问题描述 java中序列化接口和parclable接口有什么区别和联系 java中序列化接口和parclable接口有什么区别和联系 数据是存储到手机本地还是网络服务器啊 解决方案 Android中实现序列化有两个选择:一是实现Serializable接口(是JavaSE本身就支持的),一是实现Parcelable接口(是Android特有功能,效率比实现Serializable接口高效,可用于Intent数据传递,也可以用于进程间通信(IPC)).实现Serializable接口非常简单,声明

Java中Executor接口用法总结_java

本文实例讲述了Java中Executor接口用法.分享给大家供大家参考.具体如下: 1.Java中Executor接口的定义 public interface Executor { void execute(Runnable command); } 2.Executors以下静态工厂方法创建一个线程池: a) newFixedThreadPool:创建一个定长的线程池.达到最大线程数后,线程数不再增长. 如果一个线程由于非预期Exception而结束,线程池会补充一个新的线程. b) newCa

Java中利用接口实现回调

在 Java 支持方法指针之前,Java 接口不能提供一种实现回调的好方法.如果您习惯于传递在事件驱动编 程模型中调用的函数指针,则您会喜欢本技巧. 熟悉 MS-Windows 和 X Window System 事件驱动编程 模型的开发人员,习惯于传递在某种事件发生时调用(即"回调")的函数指针.Java 的面向对象模型目前 并不支持方法指针,这样似乎就不可能使用这种很好的机制.但我们并不是一点办法都没有! Java 的接 口支持提供了一种获得回调的等价功能的机制.其技巧就是:定义一

java中的接口与实现

我们通常认为访问控制是"隐藏实施细节"的一种方式.将数据和方法封装到类内后,可生成一种数据类型,它具有自己的特征与行为.但由于两方面重要的原因,访问为那个数据类型加上了自己的边界.第一个原因是规定客户程序员哪些能够使用,哪些不能.我们可在结构里构建自己的内部机制,不用担心客户程序员将其当作接口的一部分,从而自由地使用或者"滥用". 这个原因直接导致了第二个原因:我们需要将接口同实施细节分离开.若结构在一系列程序中使用,但用户除了将消息发给public接口之外,不能做

实例讲解Java中的接口的作用

接口的作用 接口的作用简单一点就是:接口是用来标记类的,不同的类属于不同的接口(通过向上转型),管理接口比管理各种各样的类方便多了,接口体现了抽象的观点,什么是抽象?抽象就是"抽去像的部分". 使用接口解决问题 问题:现在我们要写个连接数据库的类给用户使用,有两个函数:一个返回Connection对象,另一个是关闭数据库,close(),一般的解决方法是:给每个数据库写一个类,再根据用户使用的数据库决定使用具体的类. 好的,我们看看这样有什么不好之处: (1).首先每个类都要有重复的代

Java中的接口知识汇总_java

一.为什么要使用接口  假如有一个需求:要求实现防盗门的功能.门有"开"和"关"的功能,锁有"上锁"和"开锁"的功能. 分析:首先防盗门是一个门,门有开门和关门的功能,还有一把锁,锁有开锁和上锁,按照面向对象的编程的思想,我们会将门和锁都作为一个类而单独存在,但是,不能让防盗门继承自门的同时又继承自锁,防盗门不是锁,不符合继承中is a的关系,在java中支持单继承.那么我们如何来解决这一问题,这时就要用到接口.     二.

对于Java中的接口实现多继承的疑问

问题描述 目前的理解接口:Interface   的作用就是包含一些抽象方法   然后再其他类中implement接口并override这些抽象方法        感觉纯粹只为了达到统一的方法名访问而作这么个抽象方法,跟直接在类中创建这么一个方法没啥区别      如果要做到像extend那样不重写方法就能使用父类的方法,Interface还是不行啊   实现的多继承也就变成了重写多个接口中的抽象方法,没有那层继承的意义啊 解决方案 嘿嘿,看看大师们是怎么用interface的.多看看设计模式,

java中set接口使用方法详解_java

java中的set接口有如下的特点: 不允许出现重复元素: 集合中的元素位置无顺序: 有且只有一个值为null的元素. 因为java中的set接口模仿了数学上的set抽象,所以,对应的数学上set的特性为: 互异性:一个集合中,任何两个元素都认为是不相同的,即每个元素只能出现一次.无序性:一个集合中,每个元素的地位都是相同的,元素之间是无序的.集合上可以定义序关系,定义了序关系后,元素之间就可以按照序关系排序.但就集合本身的特性而言,元素之间没有必然的序.空集的性质:空集是一切集合的子集