java的clone()的使用
clone()方法的约定
首先明确的是clone()是object的方法。Cloneable接口没有任何方法,它只起到标识的作用。(java的原型模式有用到)
Cloneable接口的目的是作为对象的一个混合接口,表明这样的对象允许克隆(clone),但是这个接口却没有定义clone(),这是它的缺陷:无法约束子类实现clone()方法。
Object定义了一个受保护的clone()方法。Cloneable虽然没有定义clone()方法,但是却影响了Object.clone()方法的行为:如果一个类实现了Cloneable,调用Object的clone()就会返回该对象的逐域拷贝,否则抛出CloneNotSupportedException。这真是一种非常规的用法,Cloneable接口没有规定实现类的视图,却改变了父类受保护方法的行为。调用clone()会创建并返回对象的拷贝,看看JDK文档中对clone()方法的约定:
(1)x.clone() != x; 克隆对象与原对象不是同一个对象
(2)x.clone().getClass() == x.getClass(); 克隆的是同一类型的对象
(3)x.clone().equals(x) == true,如果x.equals()方法定义恰当的话
注意,上面的三条规则要求不是绝对的,一般来说前两条是必需的,第三个也应该尽量遵守。
实现Cloneable接口的类和其所有超类都必需遵守一个复杂、不可实施、且没有文档说明的协议,由此得到一种语言之外的机制:无需调用构造器就可以创建对象。然而,“不调用构造器”的规定有些僵硬,行为良好的clone()方法可以调用构造器创建对象,比如final类,它不会有子类,所以在它的clone()方法中调用构造器创建对象是一种合理的选择。
使用clone()的规则
“如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone()而得到的对象”,这是使用clone()方法的规则,如果不遵守这条规则,在clone()方法中调用了构造器,那么就会得到错误的类。如代码所示:
- class A implements Cloneable
- {<p style="margin:0in;font-size:10.5pt"><span style="font-family:Calibri" lang="en-US"> //</span><span style="font-family:SimSun" lang="zh-CN">类</span><span style="font-family:Calibri" lang="en-US">A</span><span style="font-family:SimSun" lang="zh-CN">的</span><span style="font-family:Calibri" lang="en-US">clone()</span><span style="font-family:SimSun" lang="zh-CN">直接调用构造器</span></p> public A clone() {
- return new A();
- }
- }
- class B extends A
- {
- public B clone() {
- try
- {
- return (B) super.clone();
- }
- catch (CloneNotSupportedException e)
- {
- throw new AssertionError();
- }
- }
- }
类B的clone()方法就不会得到正确的对象,因为super.clone()返回的是使用A的构造器创建的类A的对象。如果类B的clone方法想得到正确的对象,那么A的clone方法应该这样写:
- public A clone() {
- try{
- return (A) super.clone();
- }catch (CloneNotSupportedException e){
- throw new AssertionError();
- }
- }
由此,我们可以看出调用super.clone()最终会调用Object类的clone方法,前提是子类的所有超类都遵循了上面的规则,否则无法实施。注意,A和B的clone方法的返回值不必是Object,Java1.5引入了协变返回类型作为泛型,覆盖方法的返回值可以是被覆盖方法返回值的子类。
Cloneable实在是一个失败的接口,它并没有指明实现它的类需要承担哪些责任,通常情况下,实现Cloneable的类应当提供一个功能适当的公有的clone()方法。
浅克隆
克隆出来的对象的所有变量含有与原来的对象相同的值,而对其他对象的引用都指向原来的对象。也就是说,浅克隆仅仅克隆所考虑的对象。Object的clone就是"shallow copy"。如果类的每个域都是基本类型的值,或者是指向不可变对象的引用,那么调用Object.clone()就能得到正确的对象。
- /**
- *如果每个域都是基本类型,或者指向不可变对象的引用
- *那么这个类只需要声明实现Cloneable接口,提供公有的clone()方法
- */
- class ShallowCopy implements Cloneable
- {
- private String name;
- private int no;
- public ShallowCopy(String name,int no) {
- this.name = name;
- this.no = no;
- }
- /*只需调用super.clone()就能得到正确的行为*/
- public ShallowCopy clone() {
- try
- {
- return (ShallowCopy)super.clone();
- }
- catch (CloneNotSupportedException e)
- {
- throw new AssertionError();
- }
- }
- }
通常情况下,我们已经得到了正确的对象,但是如果类里面包含代表序列号或者唯一ID的域,或者创建时间的域,还需要对这些域进行修正。
深克隆
深克隆把引用域所指向的对象也克隆一遍。考虑下面这样一个类:
- class Person
- {
- private Dog friend;
- public Person(Dog dog) {
- friend = dog;
- }
- }
- class Dog
- {
- private String name;
- public Dog(String name) {
- this.name = name;
- }
- }
Person类的friend域不是基本类型,而是指向了可变的对象,这个时候如果调用Object.clone()进行浅克隆,那么克隆出来的对象的friend指向的还是原来的dog,就是说:
Person p = new Person(new Dog("金毛"));
p.clone().friend== p.friend;//true
p.clone().friend.name = "狼狗";
p.friend.name.equals("狼狗");//true,改变克隆对象,却同时更改了原对象
实际上,clone方法是另一种构造器:你必须确保不会伤害到原来的对象。为了使Person的clone方法正确工作,也要对friend进行克隆,最简单的做法就是调用friend.clone():
- public Person clone() {
- try{
- Person result = (Person) super.clone();
- result.friend = friend.clone();
- }
- catch (CloneNotSupportedException e){
- throw new AssertionError();
- }
- }
可是,如果friend域是final的,那么上面的clone()也无法正常工作,因为super.clone()时已经给friend赋一次值了,不能再去修正克隆对象的friend域了。这是个根本问题:clone架构与引用可变对象的final域的正常用法是不相兼容的!
抛去final域的问题不谈,递归的调用clone()方法就解决问题了吗?问题在于,深克隆要深入到哪一层,是一个不易确定的问题。考虑下面的类:
- import java.util.Arrays;
- /**
- *内部实现了单向链表
- *buckets里的每个元素保存一个单向链表
- *
- */
- class NMap implements Cloneable
- {
- private Entry[] buckets;
- public NMap(int size) {
- buckets = new Entry[size];
- for(int i = 0; i < size; i++)
- buckets[i] = new Entry("M10","Messi",new Entry("X6", "Xavi", null));
- }
- public Entry[] getBuckets() {
- return buckets;
- }
- static class Entry
- {
- final Object key;
- Object value;
- Entry next;
- Entry(Object key,Object value,Entry next) {
- this.key = key;
- this.value = value;
- this.next = next;
- }
- public void setNext(Entry next) {
- this.next = next;
- }
- public String toString() {
- String result = key + ":" + value + " ";
- if(next != null)
- result += next.toString();
- return result;
- }
- }
- public NMap clone(){
- try
- {
- NMap result = (NMap)super.clone();
- //数组被视为实现了Cloneable接口
- result.buckets = buckets.clone();
- return result;
- }catch (CloneNotSupportedException e){
- throw new AssertionError();
- }
- }
- public static void main(String[] args) {
- NMap map = new NMap(5);
- System.out.println(Arrays.toString(map.getBuckets()));
- NMap clone = map.clone();
- Entry entry = new Entry("G4","Guadiorla",new Entry("R9","Ronaldo",null));
- for(Entry ent : clone.getBuckets())
- ent.setNext(entry);
- System.out.println(Arrays.toString(map.getBuckets()));
- }
- }
运行这段代码之后会发现,虽然克隆对象有自己的数组buckets,但是数组中引用的链表与原始对象是一样的,修改克隆对象数组中的链表,原始对象中数组保存的对象也会随之而修改。解决这种问题的方法是在Entry类中增加一个“深度拷贝(deep copy)”方法。
- public Entry deepEntry() {
- Entry result = new Entry(key,value,next);
- for(Entry p = result; p.next != null; p = p.next)
- p.next = new Entry(p.next.key,p.next.value,p.next.next);
- return result;
- }
- NMap的clone()方法如下:
- public NMap clone(){
- try
- {
- NMap result = (NMap)super.clone();
- //数组被视为实现了Cloneable接口
- result.buckets = buckets.clone();
- for(int i = 0; i < buckets.length; i++)
- if(buckets[i] != null)
- result.buckets[i] = buckets[i].deepEntry();
- return result;
- }catch (CloneNotSupportedException e){
- throw new AssertionError();
- }
- }
克隆复杂对象还有一种方法,先调用super.clone()得到类型正确的对象,然后把所有域都设置成空白状态,然后调用高层的方法重新产生对象的状态。这种做法会产生一个简单、合理且相当优美的clone方法,运行速度稍慢。
总结
1.Cloneable接口是一个失败的接口,它没有提供clone()方法,却影响了Object.clone()克隆的行为:如果类没有实现Cloneable接口,调用super.clone()方法会得到CloneNotSupportedException。
2.所有实现了Cloneable接口的类都应该提供一个公有的方法覆盖clone(),此公有方法首先调用super.clone(),然后修正域,此公有方法一般不应该声明抛出CloneNotSupportedException。
3.如果为了继承而设计的类不应该实现Cloneable接口,这样可以使子类具有实现或者不实现Cloneable接口的自由,就仿佛它们直接扩展了Object一样。父类没有实现Cloneable接口,也没有覆盖clone(),子类如果实现了Cloneable,在覆盖的clone()中调用super.clone()是可以得到正确对象的。
据说很多专家级程序猿从来都不使用clone()方法。
更好的方法
等等,为了实现clone()方法的功能,有必要这么复杂吗?很少有这种必要。为了实现对象拷贝的更好的方法是提供一个拷贝构造器或者拷贝工厂,它们接受这类的一个对象作为参数。
public Yum( Yum yum);//拷贝构造器
public static YumcopyInstance(Yum yum);//拷贝工厂
转载请注明出处:喻红叶《Java中的clone()方法》
java的clone()的使用的更多相关文章
- Java基础——clone()方法浅析
一.clone的概念 clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象.所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象.那 ...
- 分析java中clone()方法 (转载+修改)
Java中的clone() 方法 java所有的类都是从java.lang.Object类继承而来的,而Object类提供下面的方法对对象进行复制. protected native Object c ...
- java中clone的深入理解
Java中Clone的概念大家应该都很熟悉了,它可以让我们很方便的“制造”出一个对象的副本来,下面来具体看看java中的Clone机制是如何工作的? 1. Clone和Copy 假 ...
- Java中clone方法的使用
什么是clone 在实际编程过程中,我们常常要遇到这种情况:有一个对象object1,在某一时刻object1中已经包含了一些有效值,此时可能会需要一个和object1完全相同新对象object2,并 ...
- Java Object Clone
Java Object Clone User user = new User(); user.setName("tom"); User user1 = new User(); us ...
- Java的clone方法效率问题
在Java中,经常会需要新建一个对象,很多情况下,需要这个新建的对象和现有的某个对象保持属性一致. 那么,就有两种方式来实现这个对象的构造: ①通过新建一个对象,为这个对象的属性根据原有对象的属性来进 ...
- 关于Java深clone 的例子学习
之前http://www.cnblogs.com/lhppom/p/4857702.html里有提到关于Java的深克隆的学习,深浅区别就是在于仅复制对象引用和复制对象引用所指向的对象,最近在看< ...
- java的 clone方法
1.java语言中没有明确提供指针的概念与用法,而实质上每个new语句返回的都是一个指针的引用,只不过在大部分情况下开发人员不需要关心如果取操作这个指针而已. 2.在java中处理基本数据类型时,都是 ...
- java的clone
做项目时有时可能会遇到需要克隆对象的时候,因为有时候对象是直接从别的类get到的,那样引用的是一个对象,修改的话会将原先的对象也修改了. java的浅克隆,十分简单.但是只会克隆基本的数据类型,当涉及 ...
- java的clone()方法
什么是"clone"? 在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能 会需要一个和A完全相同新对象B,并且此后对B任何改动都 ...
随机推荐
- PowerBuilder -- 数据窗口
获取数据窗口列数 ls_colnum= integer(this.Describe("DataWindow.Column.Count")) 获取数据窗口列名 ls_colName ...
- Process Stats:了解你的APP怎样使用内存
原文地址:http://android-developers.blogspot.com/2014/01/process-stats-understanding-how-your.html?m=1 原作 ...
- mongodb.py
from chatterbot.storage import StorageAdapter class Query(object): def __init__(self, query={}): sel ...
- POJ2594 Treasure Exploration[DAG的最小可相交路径覆盖]
Treasure Exploration Time Limit: 6000MS Memory Limit: 65536K Total Submissions: 8301 Accepted: 3 ...
- EasyPlayer RTSP 安卓Android播放器显示模式设置方法
一般对于一个播放器,应该支持如下几种显示模式: 等比例,最大化区域显示,不裁剪 等比例,最大区域显示,裁剪 拉伸显示,铺满全屏 要实现这几种显示模式,其实只要对播放控件的布局进行些许调整即可.那Eas ...
- 一起来学linux:压缩与解压缩
Linux场景下一般存在如下的压缩文件格式: 1 .Z compress程序压缩的文件 2 *.gz gzip程序压缩的文件 3 *.bz2 bzip2程序压缩的文件 4 *.tar tar程序打包的 ...
- ABAP 程序运行时间记录表
自建表记录程序运行时间,测试程序效率,可作为系统优化工具.
- ansible3
一.setup模块 ansible的setup模块主要用来收集信息,查看参数: [root@localhost ~]# ansible-doc -s setup # 查看参数,部分参数如下: filt ...
- swift和oc的混编
一.Swift工程中加入oc代码 1.在将oc代码加入到Swift工程的时候Xcode会自动创建一个桥接文件“yourProgectName-Bridging-Header.h”,如果没有创建或者删除 ...
- 经典数学问题<手电过河问题>的动态解法--问题规模扩展至任意大小
非常有趣的一件事是今天在TopCoder的1000分题里面发现了这道经典数学问题. Notes - In an optimal solution ...