引入需求背景
有一隻羊,名爲: 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();
}
}
}