`

java Cloneable—从原型模式说起

阅读更多

从原型模式说起

 

最近复习了一下23种设计模式,其中有一种模式叫“原型模式”,我更想称之为“克隆模式”。看到一遍讲的比较清楚的文章:

http://www.cnblogs.com/java-my-life/archive/2012/04/11/2439387.html

 

文中提到克隆,分为浅克隆和深克隆。看完之后我个人的理解是这样:

浅克隆:只负责克隆不可变类型的数据(比如基本数据类型、String类型),而不复制它所引用的对象,换言之,所有的对其他对象的引用都仍然指向原来的对象。具体做法是实现Cloneable接口,覆盖Object的clone()方法。

 

深克隆:利用序列化实现深度克隆。

 

要实现“深克隆”只能利用序列化来实现吗?这个成本也太高了点:每次将对象写入一个outputStream,再构造一个inputStream读出来。

答案显然不是,因为jdk源码中很多类都不是通过这种方式实现的深度克隆。

 

Cloneable接口

 

这里不讲解采用序列化实现深度克隆,感兴趣的同学可以看一下我上面提到博客,里面讲得很清楚。

Cloneable接口的目的:实现这个接口的类的对象允许被克隆。同时该类需要重写Object类的clone方法。先看下一个最简单的做法:

/**
 * Created by gantianxing on 2017/5/13.
 */
public class PhoneNumber implements Cloneable{
    private short areaCode;//区号
    private short lineNum;//号码

    public PhoneNumber(short areaCode,short lineNum) {
        this.areaCode = areaCode;
        this.lineNum = lineNum;
    }

    @Override
    public PhoneNumber clone(){
        try {
            return (PhoneNumber)super.clone();
        } catch (CloneNotSupportedException e){
            throw new AssertionError();
        }
    }
    // 其他方法
}

 

 

super.clone()实际上调用的是Object的clone方法,看下源码是个本地方法,应该是JVM实现的:

protected native Object clone() throws CloneNotSupportedException;

 

 

实例化一个PhoneNumber的对象,调用clone方法,可以完全得到一个新的对象。

实现clone方法需要注意:clone方法必须确保它不会伤害到原始对象。

由于short类型是不可变类型,假设克隆对象里的值改变了,也不会影响到原始对象。

假设PhoneNumber对象中添加一个 private Person person 的对象,采用上面的方法进行clone就是所谓的“浅克隆”,克隆对象和原始对象持有的是同一个person对象,克隆对象改变person对象,就会伤害到原始对象。这种做法是不建议使用的(除非你业务确实需要)。

 

那怎么来实现含有“可变对象”成员变量的“深克隆”呢?我们可以从jdk中学习到一些经典做法。

 

jdk中Vector的clone方法

 

我们先看一个简单的Vector类的clone方法。

public class Vector<E>
            extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
 
        protected Object[] elementData; //数组对象 可变
        protected int elementCount;//成员个数 不可变
        protected transient int modCount = 0;//修改次数
       
        //省略代码
        public synchronized Object clone() {
            try {
                @SuppressWarnings("unchecked")
                Vector<E> v = (Vector<E>) super.clone();
                //最终通过System.arraycopy生成一个新的数组
                v.elementData = Arrays.copyOf(elementData, elementCount);
                v.modCount = 0;
                return v;
            } catch (CloneNotSupportedException e) {
                // this shouldn't happen, since we are Cloneable
                throw new InternalError(e);
            }
        }
        //省略代码
    }
 

 

我们看到:

第一步,克隆“不可变对象”(elementCount等),调用的是super.clone(),生成一个新的对象v。

第二步,克隆“可变对象”(elementData),通过Arrays.copyOf方法生成一个新的数组对象。

第三步,调整“不可变对象”modCount,新克隆对象的修改次数肯定是从0开始。这一步根据具体业务需要进行调整。

 

其实对于数组还有一种方式:

public synchronized Object clone() {
            try {
                Vector<E> v = (Vector<E>) super.clone();
                v.elementData = elementData.clone();
                return v;
            } catch (CloneNotSupportedException e) {
                throw new InternalError(e);
            }
        }

 

这是采用递归的方式进行克隆。

 

其实这两种clone方式都不是严格意义上的“深克隆”,如果Vector中存放的是不可变类型对象(比如 string,Integer等)那属于“深克隆”,但如果存放的是其他对象,则还是是“浅克隆”。

测试代码如下:

 

 

	public static void main(String[] args) {
		Vector<User> vector = new Vector();
		User test = new User();
		test.setId(1);
		vector.add(test);

		//以vector对象clone出 vector2对象
		Vector <User> vector1 = (Vector) vector.clone();
		System.out.println(vector.get(0).getId());
		System.out.println(vector1.get(0).getId());

        //改版vector对象中存储的对象值,发现vector1中也跟着变了
		vector.get(0).setId(2);
		System.out.println(vector.get(0).getId());
		System.out.println(vector1.get(0).getId());
	}

        public class User {

              private int id;

              public int getId() {
                  return id;
              }

              public void setId(int id) {
                  this.id = id;
              }
         }
 

 

执行main方法 结果为:

 

1
1
2
2
 

 

说明Vector的clone方法本质上还是属于“浅克隆”。

 

另外还有其他很多工具类都是"浅克隆",比如散列表的clone方法(HashMap Hashtable),它的内部包含的是一个散列桶数组,每个散列桶指向一个单项链表的第一项键值对,如果为空,则为null。如果这时候采用数据copy或者递归克隆,都只是对桶数组进行克隆,其中的单项链表其实还是同一份。同样违背了“clone方法必须确保它不会伤害到原始对象”的原则。

 

这里提到的Vetor、HashMap、ArrayList、Hashtable的clone方法实现都是“浅克隆”。

 

jdk中HashTable的clone方法

 

我们来看看Hashtable的做法:

public class Hashtable<K,V>
            extends Dictionary<K,V>
            implements Map<K,V>, Cloneable, java.io.Serializable {
        //。。。省略代码。。。
        public synchronized Object clone() {
            try {
                Hashtable<K, V> t = (Hashtable<K, V>) super.clone();//调用object的clone方法
                t.table = new Entry[table.length];//新建一个桶数组
                for (int i = table.length; i-- > 0; ) {//克隆桶数组
                    t.table[i] = (table[i] != null)
                            ? (Entry<K, V>) table[i].clone() : null;//table[i].clone() 为
                }
                //。。。。省略代码
                return t;
            } catch (CloneNotSupportedException e) {
                // this shouldn't happen, since we are Cloneable
                throw new InternalError();
            }
        }
 
        private static class Entry<K, V> implements Map.Entry<K, V> {
            //。。。。省略代码
            protected Entry(int hash, K key, V value, Entry<K, V> next) { //Entry对象,组成单项链表
                this.hash = hash;
                this.key = key;
                this.value = value;
                this.next = next;
            }
 
            protected Object clone() { //单项链表的克隆,递归调用下一个对象的clone方法
                return new Entry<K, V>(hash, key, value,
                        (next == null ? null : (Entry<K, V>) next.clone()));
            }
            //省略代码
        }
        //。。。省略代码。。。
}
 

 

可以看到对单项链表的克隆采用的是对下一个Entry clone()方法的递归调用,如果这个单项链表太长,可能会引发 栈溢出:栈内存默认是128K,递归调用会不停的压栈,超过128k就会溢出。如果你的Hashtable(或HashMap)中存放有比较大的对象,恰巧某个hash桶里的单项链表又比较长。这个时候调用这个Hashtable的clone方法很有可能会出现StackOverflowError。当然你可以通过修改 jvm启动参数 –Xss 1024k,调大每个线程的栈内存;或者直接放弃clone。

个人愚见,这也相当于jdk的坑吧,在effective java中有一个用循环改递归的改进方法,不知道jdk以后会不会采纳(话说Hashtable的使用基本已经废弃,估计是不会改了):

Entry deepCopy(){
        Entry result = new Entry(key,vlue,next);
        for(Entry p = result;p.next !=null;p=p.next){
            p.next=new Entry(p.next.key,p.next.vlue,p.next.next);
        }
    }

 

成员变量是自定义对象的情况,就不说了,通过新建对象,并为其赋值即可。 当然更好的方式是让该自定义类也实现其clone方法,外部类直接调用即可。

 

虽然Hashtable的实现也不是“深克隆”(可以自己做类似上述Vector的测试),但我们已经找到我们想要的了,就是递归调用内部可变对象的clone方法。这里以HashMap为例进行讲解如何实现 对HashMap的“深克隆”,定义一个新的User类,多个Address信息以HashMap的形式作为User类的成员变量,为了能对User实现“深克隆”,我们必须重新User类、Address类的clone方法:

public class User implements Cloneable{

    private int id;

    private HashMap<String,Address> addressMap = new HashMap<>();

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public HashMap<String, Address> getAddressMap() {
        return addressMap;
    }

    public void setAddressMap(HashMap<String, Address> addressMap) {
        this.addressMap = addressMap;
    }

    @Override
    public Object clone(){
        try {
            User newUser = (User)super.clone();
            newUser.addressMap = new HashMap<>(addressMap.size());
            for (String key:addressMap.keySet()){
                //把clone对象放到新的hashmap
                newUser.addressMap.put(key,(Address)addressMap.get(key).clone());
            }
            return newUser;
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    }
}

User类的clone方法,主要是循环遍历老Hashmap,clone里面的Address对象,把clone的Address对象放入新HashMap里。

 

由于没有可变对象类型的成员变量,Address类的clone方法很简单,直接调用super.clone即可:

 

public class Address implements Cloneable{

    private String address;

    public Object clone(){
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

 

最后编写测试main方法:

public static void main(String[] args) {
		User user = new User();
		user.setId(1);
		Address home = new Address();
		home.setAddress("四川成都");
		user.getAddressMap().put("home",home);

		//以user对象为基础进行克隆
		User newUser = (User)user.clone();
		System.out.println(user.getAddressMap().get("home"));
		System.out.println(newUser.getAddressMap().get("home"));

		//改变老user对象hashmap中的地址信息,对比看newUser对象hashmap中的地址信息是否发生变化
		user.getAddressMap().get("home").setAddress("中国北京");
		System.out.println(user.getAddressMap().get("home").getAddress());
		System.out.println(newUser.getAddressMap().get("home").getAddress());
	}

 

执行mian方法,结果为:

com.jd.ejshop.domain.pub.render.Address@4617c264
com.jd.ejshop.domain.pub.render.Address@36baf30c
中国北京
四川成都

 测试结果说明,老User对象中的Address发生变化时,不会影响newUser对象中的Adress。说明newUser对象是“深克隆”。

 

 

至此如何实现自己的“深克隆”模式讲解完毕,简单的讲就是递归调用clone方法,关于Cloneable就说这么多吧。

1
1
分享到:
评论
2 楼 bart233 2018-02-28  
   
1 楼 bart233 2018-02-28  
引用

相关推荐

    java设计模式【之】原型模式、深拷贝与浅拷贝【源码】【场景:克隆羊】

    java设计模式【之】原型模式、深拷贝与浅拷贝【源码】【场景:克隆羊】 * 原型模式(Prototype) * 实现方式: * 需要被克隆的 class类, 重写Object中的clone()方法,并实现Cloneable接口(否则报错 ...

    24设计模式-原型模式1

    2. 为什么需要接口Cloneable 3. java什么时候会抛出CloneNotSupportedException异常 4. 流的概念 5. 什么是序列化

    GOF23之原型模式

    通过New产生一个对象需要非常繁琐的数据准备或者访问权限,则可以使用原型模式 二、优点: 1、效率高,避免了重新执行构造过程步骤 2、ProtoType类似New,但不同于New,克隆出的对象属性值与原型对象相同,克隆出的...

    leetcode分类-DesignPatternAndAlgorithm:常用设计模式和算法,练习用

    leetcode 分类 DesignPatternAndAlgorithm 常用设计模式和算法,练习用。...原型模式:与new一个新对象不同,原型模式是clone出一个新对象。已与java融为一体,实现Cloneable接口。分为深拷贝和浅拷贝。 抽

    java se和java ee基础功能点开发测试包

    该包围绕java se结合java ee的基础应用对目前最基础最常见的应用分别给以做了相关demo 具体包括有设计模式demo(设配模式、装饰模式、享元模式)、线程池设计方案、集合类容器demo、io流、Cloneable接口、自定义标签...

    Android代码-安卓设计模式

    AndroidDesign 安卓设计模式 抽象模式(abstract) builder模式 原型模式(cloneable) 责任链模式(iterator) 单例模式(singleton) 状态模式(state) 策略模式(strategy)

    Java2游戏编程.pdf

    第1篇 步入Java丛林:从Java2 API开始 第1章 Java2软件开发工具包 1.1 Java简史 1.2 为什么在游戏中使用Java 1.3 为Java准备系统 1.3.1 安装Java SDK 1.3.2 编译和运行Java程序 1.3.3 使用命令行 1.3.4 使用集成开发...

    详解Java中的克隆技术

    有关Java中的克隆技术的详细讲述,其中讲解了“浅复制”和“深复制”的用法和区别,继承了Java中的封装类Cloneable

    【java系列文章】java 基础知识

    2.Java为什么不直接实现lterator接口,而是实现lterable? 3.简述什么是值传递和引用传递? 4.概括的解释下Java线程的几种...1.为什么集合类没有实现Cloneable和Serializable接口? 2.Java中的HashMap的工作原理是什么?

    Java深浅clone

    Java深浅clone测试代码 流拷贝 Cloneable

    java初学者必看

    最近正在学习Java,也买了很多的有关Java方面的书籍,其中发现《跟我学Java》这本书,都的很不错啊,所以顺便拿电脑把这本书的目录敲了下来,与大家分享。尤其是那些和我一样初学Java的朋友们,看看哪一节对你有用,...

    Java数据库查询结果的输出

    implements List , Cloneable , Serializable{…} 类JTable:  JTable组件是Swing组件中比较复杂的小件,隶属于javax.swing包,它能以二维表的形式显示数据。类Jtable: 定义如下: public class JTable extends ...

    Test8.java

    该资源为Java源代码,设计了一个名为Complex的复数类,实现了复数运算的加减乘除和绝对值,覆盖了toString方法,实现了Cloneable接口。包含一个测试程序,提示用户输入两个复数,然后显示它们加减乘除之后的结果。

    java编程常见问题

    当Java虚拟机试图从读取某个类文件,但是发现该文件的主、次版本号不被当前Java虚拟机支持的时候,抛出该错误。 39.java.lang.VerifyError 验证错误。当验证器检测到某个类文件中存在内部不兼容或者安全问题时抛出该...

    二十三种设计模式【PDF版】

    之道 》,其中很多观点我看了很受启发,以前我也将"设计模式" 看成一个简单的解决方案,没有从一种高度来看待"设计模式"在软 件中地位,下面是我自己的一些想法: 建筑和软件某些地方是可以来比喻的 特别是中国传统建筑...

    新手入门写Java程序的三十个基本规则

    新手入门写Java程序的三十个基本规则 (1) 类名首字母应该大写。字段、方法以及对象(句柄)的首字母应小写。对于所有标识符,其中包含的所有单词都应紧靠在... clone()(implement Cloneable)  implement Serializable

    JAVA高级程序设计测试题含答案.docx

    JAVA高级程序设计测试题含答案 JAVA高级程序设计测试题含答案 160题_共320.00分_及格260.00分 您的姓名: [填空题] * _________________________________ 第1题 【单选题】【2.00分】【概念理解】 在程序读入字符...

    java常用类解析及示例及一些工具类源代码

    主要讲解了System类、Object类、Arrays类、Cloneable接口、IO系统输入输出类及装饰类、IO系统文本读写工具类、IO系统二进制读写工具类、对象序列化工具类、File类及文件搜索工具类、java异常机制及自定义异常类、...

    java克隆对象(两种方法)

    java的两种深度克隆方法,1cloneTest是用Cloneable接口的clone方法实现(对象必须要实现cloneable接口).2cloneSerialize.java是用对象流写对象到byte数组中,然后从byte数组中取得对象.(对象必须要实现serializble接口)

    Java程序设计复习题.docx.docx

    Java程序设计复习题 Java程序设计复习题全文共19页,当前为第1页。Java程序设计复习题全文共19页,当前为第1页。一、选择题: Java程序设计复习题全文共19页,当前为第1页。 Java程序设计复习题全文共19页,当前为第...

Global site tag (gtag.js) - Google Analytics