学习设计模式不光要学习设计模式的思想,还要去深入理解,为什么要用这个设计模式。
如何深入理解?读优秀的框架代码,看别人代码,了解它们的使用场景。 - - - 博主老师(感谢他)
本文先介绍了原型模式的概念及简单实现。也简单讨论了浅拷贝和深拷贝,再贴了jdk中对原型模式的实现。最后总结了一点点思考。
概念
定义:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象
在Java中的表现就是clone
意思就是说,不是通过new 来构造对象,而是通过调用已构造对象的clone方法去复制一个对象。什么时候要这么做?适用于构造方法非常耗时的类。
clone并不一定比new操作速度块,所以一定是构造方法非常耗时,才这么做。
这里还涉及到浅拷贝和深拷贝的问题,我们结合下面的代码来讲。
实现:
public class CloneTest implements Cloneable {
private List<String> list;
private int a;
public CloneTest() {
System.out.println("CloneTest 构造方法");
this.list = new ArrayList<>();
this.a = 1;
list.add("test1");
}
@Override
protected Object clone() throws CloneNotSupportedException {
CloneTest obj = (CloneTest) super.clone();
obj.list = this.list;
obj.a = this.a;
return obj;
}
@Override
public String toString() {
return "CloneTest{" +
"list=" + list +
", a=" + a +
'}';
}
public List<String> getList() {
return list;
}
public void setList(List<String> list) {
this.list = list;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
测试类
public static void main(String[] args) throws CloneNotSupportedException {
CloneTest test = new CloneTest();
CloneTest clone = (CloneTest) test.clone();
System.out.println(test.toString());
System.out.println(clone.toString());
System.out.println("修改clone");
clone.setA(2);
clone.getList().add("test2");
System.out.println(test.toString());
System.out.println(clone.toString());
}
输出
CloneTest 构造方法
CloneTest{list=[test1], a=1}
CloneTest{list=[test1], a=1}
修改clone
CloneTest{list=[test1, test2], a=1}
CloneTest{list=[test1, test2], a=2}
观察结果我们可以得出如下结论:
- clone不是通过构造方法去创建一个对象,他不会调用该对象的构造方法。
- clone只是浅拷贝:克隆后的对象虽然是新的一个,但是对象里面的引用类型字段还是指向了原始对象的对应引用类型同一个地址。(正如我们看到,我们修改了拷贝后的list,拷贝前对象的list也跟着改了,而int虽然改了,但由于他是基本数据类型,所有不会影响)
怎么做深拷贝?改下我们的clone方法,对list赋值的时候,不要直接等于,可以new ArrayList(),然后再addAll,或者直接调用list的clone方法。
@Override
protected Object clone() throws CloneNotSupportedException {
CloneTest obj = (CloneTest) super.clone();
obj.list = (List<String>) ((ArrayList)this.list).clone();
obj.a = this.a;
return obj;
}
结果
CloneTest 构造方法
CloneTest{list=[test1], a=1}
CloneTest{list=[test1], a=1}
修改clone
CloneTest{list=[test1], a=1}
CloneTest{list=[test1, test2], a=2}
这时候,就是修改克隆后的list就不会影响原来的list。
这里其实还有个问题:如果我们不是List,而是List这个XXX是我们自己定义的类,如果我们修改了克隆后的list中的XXX对象的某个属性,其实克隆前的list中这个也存在的XXX对象的属性也会发生修改。
意思就是说:你每次clone,只能clone这个对象本身,而对象的引用类型还不是完全的与原对象隔离。
JDK中的原型模式
JDK源码里其实有很多原型模式的实现。我们观察到,刚才的ArrayList其实是实现了Cloneable接口,重写了clone方法。所以我们能那么用。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
}
我们再看个调用clone方法的例子
java.util.EnumMap
为什么选这个类?因为博主以前没见过~哈哈
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
implements java.io.Serializable, Cloneable
{
private transient Object[] vals;
public EnumMap(EnumMap<K, ? extends V> m) {
keyType = m.keyType;
keyUniverse = m.keyUniverse;
vals = m.vals.clone();
size = m.size;
}
}
在构造方法里,调用了m.vals.clone(); vals是个数组,为什么这里不是用new而是用clone,答案显而易见。
- 首先要获取的一个vals的副本,clone方法能直接拿到,方便高效
- 如果不用clone,new个新数组呢?通过for循环一个个填… 这个其实效率会低。
我们可以看下for和clone的效率
public static void main(String[] args) {
String[] strs = new String[100000000];
for (int i = 0; i < strs.length; i++) {
strs[i] = "aaaaaaaa";
}
clone(strs);
forr(strs);
}
public static void clone(String[] strs) {
long cost = System.currentTimeMillis();
String[] cl = new String[strs.length];
cl.clone();
System.out.println("clone:" + (System.currentTimeMillis() - cost));
}
public static void forr(String[] strs) {
long cost = System.currentTimeMillis();
String[] cl = new String[strs.length];
int i = 0;
for (String s: strs) {
cl[i] = strs[i];
i ++;
}
System.out.println("for:" + (System.currentTimeMillis() - cost));
}
输出:
clone:1082
for:7960
clone比for块
思考
其实原型模式比较简单,但其实我一开始也想不到这个能有什么使用场景,网上找了些资料,大家都在说,构造方法耗时、或者做备份可以用。直到我这篇文章写到"JDK中原型模式的使用“,看了源码,才算有所领悟:正如我们上面看到的EnumMap的构造方法,这里用clone应该算是理所当然,不用clone我们用什么呢?总不能真的去for循环创建吧!
正如一开始引用博主老师所说:“如何深入理解?读优秀的框架代码,看别人代码,了解它们的使用场景。” ,共勉!