克隆羊多利之原型模式
今天就不想开头了,直接来吧。如果想要创建几个和某对象一模一样的新对象,我们很容易想到new对象,在构造器里面进行复制即可。但是今天就看一个新模式,原型模式。
创建对象的正常方法(蠢方法)
背景
新建一个Sheep类,并在客户端Client里面创建一个sheep对象,那我们还想要多添加几个克隆羊,即和sheep对象一模一样的几个对象。我们很容易想到的是,直接new方法,再通过构造方法,将sheep里面的几个参数进行赋值,具体代码如下。
代码
Sheep类:
public class Sheep { private String name; private int age; private String color; public Sheep(String name, int age, String color) { this.name = name; this.age = age; this.color = color; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } @Override public String toString() { return "Sheep{" + "name='" + name + '\'' + ", age=" + age + ", color='" + color + '\'' + '}'; } }
Client类:
public class Client { public static void main(String[] args) throws CloneNotSupportedException { Sheep sheep = new Sheep("tom", 1, "白色"); Sheep sheep1=new Sheep(sheep.getName(),sheep.getAge(),sheep.getColor()); Sheep sheep2=new Sheep(sheep.getName(),sheep.getAge(),sheep.getColor()); System.out.println(sheep1.toString()); System.out.println(sheep2.toString()); System.out.println(sheep.hashCode()+","+sheep1.hashCode()+","+sheep2.hashCode()); } }
运行结果:
优点
比较好理解,简单易操作,傻白甜操作。
缺点
1.在创建新的对象时,总是需要获取原始对象的值,如果创建的对象比较复杂,效率较低。
2.总是需要重新初始化对象,而不是动态的获取对象运行时的状态,不够灵活
引入原型模式
官方概念
用原型实例(原来的对象)来指定创建对象的种类,并且通过拷贝这些原型,创建新的对象。
白话文
就是通过一个旧对象,创建一个新对象,即克隆羊多利。打个比方,咱能单个复制多利身上的部位,然后拼起来,但是这种方法麻烦且低效,部位的增加可能导致越来越低能。我们也能通过某种方法,直接复制所有。某种方法映射到设计模式里面就是原型模式。
创建对象的新方法(clone)
方法概念描述
原型类需要具备以下两个条件
:
- 实现Cloneable接口。在java语言有一个Cloneable接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。
- 重写Object类中的clone方法。Java中,所有类的父类都是Object类,Object类中有一个clone方法,作用是返回对象的一个拷贝,但是其作用域protected类型的,一般的类无法调用,因此,原型类需要将clone方法的作用域修改为public类型。
代码
Sheep类:(与之前的Sheep区别就是实现了Cloneable接口及重写了clone方法)
public class Sheep implements Cloneable { private String name; private int age; private String color; public Sheep(String name, int age, String color) { this.name = name; this.age = age; this.color = color; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } @Override public String toString() { return "Sheep{" + "name='" + name + '\'' + ", age=" + age + ", color='" + color + '\'' + '}'; } @Override protected Object clone() throws CloneNotSupportedException { Sheep otherSheep=(Sheep)super.clone(); return otherSheep; } }
Client类:
public class Client { public static void main(String[] args) throws CloneNotSupportedException { Sheep sheep = new Sheep("tom", 1, "白色"); Sheep sheep1 = (Sheep) sheep.clone(); System.out.println(sheep.toString()); System.out.println(sheep1.toString()); System.out.println(sheep.hashCode()+","+sheep1.hashCode()); } }
运行结果:
插曲(为什么要实现Cloneable接口,可不可以不实现?)
在上面的方法描述中我们可以知道了要实现Cloneable接口,但是我们看看如果不实现,有什么问题?
Sheep类:
public class Sheep{ private String name; private int age; private String color; //构造器,setter/getter,toString就先略去了,看重点 @Override protected Object clone() { Sheep otherSheep=null; try{ otherSheep=(Sheep)super.clone(); return otherSheep; }catch(Exception e){ e.printStackTrace(); } return otherSheep; } }
test类一样,我就不贴了,直接来看运行结果:
我们可以看到直接抛出了异常,这是因为如果对象的类不支持Cloneable接口,重写clone方法的子类也可以引发此异常以指示无法克隆实例。所以啊,都要写,不要偷懒。
浅拷贝
举个例子,如果羊对象里面有个house这种的复杂对象,我们看能不能拷贝成功。
Sheep类:
public class Sheep implements Cloneable{ private String name; private int age; private String color; private House house; public Sheep(String name, int age, String color,House house) { this.name = name; this.age = age; this.color = color; this.house=house; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } public House getHouse() { return house; } public void setHouse(House house) { this.house = house; } @Override public String toString() { return "Sheep{" + "name='" + name + '\'' + ", age=" + age + ", color='" + color + '\'' + ", house=" + house + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
House类:
public class House implements Cloneable { private String name; private String address; public House(String name, String address) { this.name = name; this.address = address; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
Client类:
public class Client { public static void main(String[] args) throws CloneNotSupportedException { House house=new House("羊羊家","地球上"); Sheep sheep = new Sheep("tom", 1, "白色",house); Sheep sheep1 = (Sheep) sheep.clone(); System.out.println(sheep.getHouse().hashCode()+","+sheep1.getHouse().hashCode()); } }
运行结果:
我们可以看到两个Sheep对象中house的hashCode并没有变化,说明他们实际上指向的是同一片空间,即为浅拷贝,那我们之前的例子都是浅拷贝,因为Sheep对象中的各个属性的hashCode是一样的,他们指向的是同一片内存空间。那我们应该如何让里面对象的值也不一样呢?那就是下面的深拷贝。
深拷贝(复杂对象)
方法一(使用自己子对象的clone方法)
Sheep类:(与之前的区别只是方法上的区别,所以就不贴重复代码啦)
@Override protected Object clone() throws CloneNotSupportedException { Sheep otherSheep=(Sheep) super.clone(); otherSheep.setHouse((House)house.clone()); return otherSheep; }
方法二(序列化和反序列化)
public class Sheep implements Serializable, Cloneable { private String name; private int age; private String color; private House house; //setter/getter方法,构造器,toString方法都略去了 public Object clone2() { Sheep sheep = null; ByteArrayOutputStream bos = null; ObjectOutputStream oos = null; ByteArrayInputStream bis = null; ObjectInputStream ois = null; try { //序列化 bos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(bos); oos.writeObject(this); bis = new ByteArrayInputStream(bos.toByteArray()); ois = new ObjectInputStream(bis); sheep = (Sheep) ois.readObject(); } catch (Exception e) { e.printStackTrace(); } finally { //关闭流 try { bos.close(); oos.close(); bis.close(); ois.close(); } catch (Exception e) { e.printStackTrace(); } } return sheep; } }
public class House implements Serializable,Cloneable { private String name; private String address; //setter/getter方法,构造器,toString方法都略去了 }
优点
在内存中二进制流的拷贝,比直接new一个对象的性能要好,毕竟人家是native方法,可以直接操作内存,是亲儿子。
缺点
没错,亲儿子也有缺点。当处理简单类的时候,他的性能还不如new,但当处理复杂类的时候,性能就比new高出一大截啦。
咱来试试,普通的House类,有两个基本类型的参数,分别是name和address,我们看new和利用原型模式分别用了多少毫秒?
public class House implements Serializable,Cloneable { private String name; private String address; public House(String name, String address) { this.name = name; this.address = address; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
public class Client { public static void main(String[] args) throws CloneNotSupportedException { long startTime=System.currentTimeMillis(); for(int i=0;i<100000000;i++){ new House("羊羊家","地球上"); } System.out.println(System.currentTimeMillis()-startTime); startTime=System.currentTimeMillis(); House house=new House("羊羊家","地球上"); for(int i=0;i<100000000;i++){ house.clone(); } System.out.println(System.currentTimeMillis()- startTime); } }
运行结果:
从上图我们可以看到采用new方法,只用了13毫秒,而采用原型模式,却花了接近900毫秒,但是将House类的构造方法做一点点改变,我们就能发现差别,如下:
public class House implements Serializable,Cloneable { private String name; private String address; public House(String name, String address) { this.name = name.substring(1); this.address = address; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
运行结果:
从上述代码可以看到,采用原型模式的时间比new方法的时间少了一半左右。
结语
原型模式属于创建型模式,能产生新的对象。
其对复制功能进行优化,采用Java提供的native方法clone,前提是实现cloneable接口。
使用默认的clone方法,是浅拷贝,只是复制值,实际上地址并没有变化。如果想要深拷贝,可以重写clone方法或采用序列化和反序列化。