引入需求背景
有一只羊,名为: tom, 年龄为:1,颜色为:白色,请编写程序创建和 tom 羊 属性完全相同的 10只羊。
最简单的写法
public class Test {
public static void main(String[] args) {
Sheep sheep = new Sheep("tom",1,"白色");
Sheep sheep2 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor());
Sheep sheep3 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor());
Sheep sheep4 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor());
System.out.println(sheep);
System.out.println(sheep2);
System.out.println(sheep3);
System.out.println(sheep4);
}
}
上述写法太死板了,不够灵活,我们可以做一些改进。
改进思路:Java 中 Object 类是所有类的根类,Object 类提供了一个 clone()方法,该方法可以将一个 Java 对象复制 一份,但是需要实现clone的Java类必须要实现一个接口Cloneable,该接口表示该类能够复制且具有复制的能力 => 原型模式
原型模式
- 原型模式(Prototype模式)是指:用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象
- 原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节
- 工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建,即 对象.clone()
- 形象的理解:孙大圣拔出猴毛,变出其它孙大圣
所以,原型模式的核心就是一个clone()方法的操作。
通过原型模式改写克隆羊代码
步骤:
- 克隆羊(克隆的对象)实现Cloneable接口。
- 重写clone()方法。
- 客户端调用clone()方法,获得一个克隆对象。
Sheep.java
/**
* 克隆羊
* 实现Cloneable接口
*/
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 + '\'' +
", hashCode='" + this.hashCode() + '\'' +
'}';
}
//重写clone()
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); //通过super.clone()返回该对象的克隆实例,以Object的形式
}
}
Test.java
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Sheep sheep = new Sheep("tom",1,"白色");
Sheep sheep2 = (Sheep) sheep.clone(); //通过调用clone方法获取克隆对象
Sheep sheep3 = (Sheep) sheep.clone();
Sheep sheep4 = (Sheep) sheep.clone();
System.out.println(sheep);
System.out.println(sheep2);
System.out.println(sheep3);
System.out.println(sheep4);
// 输出结果:
// Sheep{name='tom', age=1, color='白色', hashCode='1625635731'}
// Sheep{name='tom', age=1, color='白色', hashCode='1580066828'}
// Sheep{name='tom', age=1, color='白色', hashCode='491044090'}
// Sheep{name='tom', age=1, color='白色', hashCode='644117698'}
}
}
深拷贝和浅拷贝
接下来,我们来思考一个问题。
刚刚我们克隆对象的时候,对象的属性都是基本数据类型,那么如果是一个引用类型的话?会是一个什么样的效果??
我们做一个简单的示范,步骤:
- 在之前的sheep羊里加一个属性,
private Sheep friend;
- 在客户端对这个属性进行赋值,同时clone一个新的羊出来。
- 同时打印原来的羊的friend和新克隆羊的friend 的 hashcode,看结果。
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Sheep friend = new Sheep("Bob", 1, "灰色"); //朋友羊,作为引用
Sheep sheep = new Sheep("tom", 1, "白色", friend);
Sheep sheep2 = (Sheep) sheep.clone(); //通过调用clone方法获取克隆对象
System.out.println(sheep.getFriend().hashCode()); // 1625635731
System.out.println(sheep2.getFriend().hashCode()); // 1625635731
}
}
根据结果,我们可以看到两只羊的朋友属性引用的是同一只羊。
浅拷贝介绍
- 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。
- 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行 引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成 员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
- 前面我们克隆羊就是浅拷贝
- 浅拷贝是使用默认的 clone()方法来实现
深拷贝介绍
-
复制对象的所有基本数据类型的成员变量值
-
为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝
-
深拷贝实现方式1:重写clone方法来实现深拷贝
-
深拷贝实现方式2:通过对象序列化实现深拷贝(推荐)
通过clone() 方法实现深拷贝
需求,sheep羊有一个家,克隆羊不要引用sheep的家,要一个属于自己的家。
步骤:
- 创建一个huose类,实现Cloneable接口。
- 重写sheep的clone方法。注意,clone只能克隆基本类型,不能克隆引用类型,所以,我们要把house对象做一个单独的克隆,并进行赋值。(克隆套克隆)
huose类
public class House implements Cloneable {
private String name;
public House(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
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;
}
//重写clone()
@Override
protected Object clone() throws CloneNotSupportedException {
//这里的克隆羊的house引用的是原先的地址
Sheep sheep = (Sheep) super.clone();
//把house单独拿出来克隆一份
House house = (House) sheep.getHouse().clone();
//再对其赋值新house的地址
sheep.setHouse(house);
return sheep;
}
}
上面的重写clone方法对应的图示。
先clone()一个sheep,但是此时的sheep的引用地址是原先的。
所以我们要继续开辟一个house的克隆,再将引用替换。
测试类
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
House house = new House("新疆"); //房子在新疆
Sheep sheep = new Sheep("tom", 1, "白色", house);
Sheep sheep2 = (Sheep) sheep.clone(); //通过调用clone方法获取克隆对象
System.out.println(sheep.getHouse().hashCode()); // 1625635731
System.out.println(sheep2.getHouse().hashCode()); // 1580066828
//两个对象的hashcode不一样,所以引用的是两个对象
}
}
通过对象序列化实现深拷贝
原理:
就是将对象通过序列化,以流的方式输出(这时会将引用类型拷贝一个新的地址出来),再用输入流去接收。
步骤:
1、sheep类以及house类要实现Serializable接口,才能做一个序列化的操作。
2、在sheep类里编写deepClone()方法。
3、客户端调用deepClone()方法返回新的深拷贝对象。
public Sheep deepClone() {
//创建流对象
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);
return (Sheep) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
//关闭流
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (Exception e2) {
e2.printStackTrace();
}
}
}