设计模式之原型模式
那有没有什么办法解决这种问题呢?当然有,原型模式就可以解决这个痛点。
原型模式非常好理解,就是类的实例对象可以克隆自身,产生新的实例对象,这样就无需用new来创建。想一下,齐天大圣孙悟空是不是拔一根汗毛,就复制出了很多个一模一样的孙悟空,道理是一样的。(新的对象和原对象,内容相同,但是内存地址不同,因为是不同的对象。)
那在Java中,我们怎么实现原型模式呢?非常简单,只需要原型对象实现Cloneable接口就可以了,看代码:(学生对象的复制,学生对象中包含他所学的学科类的对象)
//学科类 public class Subject { private String name; private String content; public String getName() { return name; } public Subject setName(String name) { this.name = name; return this; } public String getContent() { return content; } public Subject setContent(String content) { this.content = content; return this; } public Subject(String name, String content) { this.name = name; this.content = content; } public Subject() { } @Override public String toString() { return "Subject{" + "name='" + name + '\'' + ", content='" + content + '\'' + '}'; } } //学生类 public class Student implements Cloneable { private int age; private String name; private Subject subject; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Subject getSubject() { return subject; } public void setSubject(Subject subject) { this.subject = subject; } public Student(int age, String name, Subject subject) { this.age = age; this.name = name; this.subject = subject; } public Student() { } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + ", subject=" + subject + '}'; } @Override protected Object clone() throws CloneNotSupportedException { //实现Cloneable接口,调用父类Object的clone方法来实现对象的拷贝 return super.clone(); } } public class ProTest { public static void main(String[] args) throws Exception { Student s1 = new Student(18,"张三",new Subject("语文","这是语文书")); //通过调用s1对象的clone方法,即可创建一个新的对象s2 Student s2 = (Student)s1.clone(); System.out.println(s1); System.out.println(s2); s2.setAge(20); s2.setName("李四"); s2.getSubject().setName("数学").setContent("这是数学书"); System.out.println("======="); System.out.println(s1); System.out.println(s2); } }
打印结果如下:
Student{age=18, name='张三', subject=Subject{name='语文', content='这是语文书'}} Student{age=18, name='张三', subject=Subject{name='语文', content='这是语文书'}} ======== Student{age=18, name='张三', subject=Subject{name='数学', content='这是数学书'}} Student{age=20, name='李四', subject=Subject{name='数学', content='这是数学书'}}
可以发现,s2对象修改的年龄和姓名对原对象s1没有任何影响,但是subject对象修改之后,原对象s1中的subject对象内容也被更改了,这是怎么回事呢?其实,这是因为我们使用的是浅拷贝。(Object对象的clone方法本身就是浅拷贝)
浅拷贝:对值类型的成员变量进行值的复制,对引用类型的成员变量只进行引用的复制,而不复制引用所指向的对象。(嗯?这句话怎么听起来这么熟悉,看下这个: 为什么大家都说Java中只有值传递
)此时,新对象里边的引用类型变量相当于原对象的引用类型变量的副本,他们指向的是同一个对象。
因此,修改了s2对象的subject对象的内容,原对象s1的subject对象内容也会跟着改变。那如果,我不想让原对象的引用类型变量内容发生改变,应该怎么做呢?
这就要用到深拷贝了,即把引用类型变量的内容也拷贝一份,这样他们就互不影响了。一般有两种方式来实现深拷贝:一种是让需要拷贝的引用类型也实现Cloneable接口,然后重写clone方法;另一种是利用序列化。
clone方式:
还是以上边的例子来说。首先需要修改Subject类让它实现Cloneable接口,重写clone方法。然后修改Student类的clone方法,把所有引用类型的变量手动拷贝一下。
public class Subject implements Cloneable{ private String name; private String content; public String getName() { return name; } public Subject setName(String name) { this.name = name; return this; } public String getContent() { return content; } public Subject setContent(String content) { this.content = content; return this; } public Subject(String name, String content) { this.name = name; this.content = content; } public Subject() { } @Override public String toString() { return "Subject{" + "name='" + name + '\'' + ", content='" + content + '\'' + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } public class Student implements Cloneable { private int age; private String name; private Subject subject; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Subject getSubject() { return subject; } public void setSubject(Subject subject) { this.subject = subject; } public Student(int age, String name, Subject subject) { this.age = age; this.name = name; this.subject = subject; } public Student() { } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + ", subject=" + subject + '}'; } @Override protected Object clone() throws CloneNotSupportedException { Student cloneStu = (Student)super.clone(); //手动拷贝subject对象,然后赋值给student克隆的新对象 cloneStu.setSubject((Subject) this.subject.clone()); return cloneStu; } }
再次运行测试类,会发现修改新对象的引用类型变量已经无法影响原对象的引用类型变量了。
Student{age=18, name='张三', subject=Subject{name='语文', content='这是语文书'}} Student{age=18, name='张三', subject=Subject{name='语文', content='这是语文书'}} ======== Student{age=18, name='张三', subject=Subject{name='语文', content='这是语文书'}} Student{age=20, name='李四', subject=Subject{name='数学', content='这是数学书'}}
序列化方式
可以发现,用clone的方式实现起来是非常简单的。但是,考虑如果对象中的引用类型变量很多的时候,这种方式就不太方便 了,因为你要把所有的引用类型都手动clone一遍。另外,如果引用类型又嵌套了多层引用类型,那将是一场灾难。这时,就可以考虑用序列化方式。
系列化方式是通过把对象序列化成二进制流,放到内存中,然后再反序列化成为新的Java对象,这样就可以保证新对象和原对象的互相独立了。
这种方式,需要所有类都实现Serializable接口。Student类只需要添加一个系列化和反序列化的方法就可以了。
public class Subject implements Serializable { private String name; private String content; public String getName() { return name; } public Subject setName(String name) { this.name = name; return this; } public String getContent() { return content; } public Subject setContent(String content) { this.content = content; return this; } public Subject(String name, String content) { this.name = name; this.content = content; } public Subject() { } @Override public String toString() { return "Subject{" + "name='" + name + '\'' + ", content='" + content + '\'' + '}'; } } public class Student implements Serializable { private int age; private String name; private Subject subject; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Subject getSubject() { return subject; } public void setSubject(Subject subject) { this.subject = subject; } public Student(int age, String name, Subject subject) { this.age = age; this.name = name; this.subject = subject; } public Student() { } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + ", subject=" + subject + '}'; } //深克隆 public Object deepClone() throws IOException, ClassNotFoundException{ //把对象写入到流中 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); //从流中读取 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } } public class ProTest { public static void main(String[] args) throws Exception { Student s1 = new Student(18,"张三",new Subject("语文","这是语文书")); Student s2 = (Student)s1.deepClone(); System.out.println(s1); System.out.println(s2); s2.setAge(20); s2.setName("李四"); s2.getSubject().setName("数学").setContent("这是数学书"); System.out.println("========"); System.out.println(s1); System.out.println(s2); } }
可以看到,在测试类中通过调用deepClone自定义的深克隆方法即可。这种方式适合引用类型或者子嵌套特别多的情况,每个类只需要实现Serializable接口即可,Student的deepClone方法也不需要再改动。
需要注意,这种方式的深拷贝,类中引用类型变量不能用 transient 修饰。(不懂的,自行搜索transient关键字,简单说就是用transient修饰的变量默认不会被序列化)