26 - 原型模式與享元模式:提升系統性能的利器

原型模式和享元模式,前者是在創建多個實例時,對創建過程的性能進行調優;后者是用減少創建實例的方式,來調優系統性能。這么看,你會不會覺得兩個模式有點相互矛盾呢?

其實不然,它們的使用是分場景的。在有些場景下,我們需要重復創建多個實例,例如在循環體中賦值一個對象,此時我們就可以采用原型模式來優化對象的創建過程;而在有些場景下,我們則可以避免重復創建多個實例,在內存中共享對象就好了。

今天我們就來看看這兩種模式的適用場景,了解了這些你就可以更高效地使用它們提升系統性能了。

1、原型模式

我們先來了解下原型模式的實現。原型模式是通過給出一個原型對象來指明所創建的對象的類型,然后使用自身實現的克隆接口來復制這個原型對象,該模式就是用這種方式來創建出更多同類型的對象。

使用這種方式創建新的對象的話,就無需再通過 new 實例化來創建對象了。這是因為 Object 類的 clone 方法是一個本地方法,它可以直接操作內存中的二進制流,所以性能相對 new 實例化來說,更佳。

1.1、實現原型模式

我們現在通過一個簡單的例子來實現一個原型模式:

   // 實現 Cloneable 接口的原型抽象類 Prototype class Prototype implements Cloneable {// 重寫 clone 方法public Prototype clone(){Prototype prototype = null;try{prototype = (Prototype)super.clone();}catch(CloneNotSupportedException e){e.printStackTrace();}return prototype;}}// 實現原型類class ConcretePrototype extends Prototype{public void show(){System.out.println(" 原型模式實現類 ");}}public class Client {public static void main(String[] args){ConcretePrototype cp = new ConcretePrototype();for(int i=0; i< 10; i++){ConcretePrototype clonecp = (ConcretePrototype)cp.clone();clonecp.show();}}}

要實現一個原型類,需要具備三個條件:

  • 實現 Cloneable 接口:Cloneable 接口與序列化接口的作用類似,它只是告訴虛擬機可以安全地在實現了這個接口的類上使用 clone 方法。在 JVM 中,只有實現了 Cloneable 接口的類才可以被拷貝,否則會拋出 CloneNotSupportedException 異常。
  • 重寫 Object 類中的 clone 方法:在 Java 中,所有類的父類都是 Object 類,而 Object 類中有一個 clone 方法,作用是返回對象的一個拷貝。
  • 在重寫的 clone 方法中調用 super.clone():默認情況下,類不具備復制對象的能力,需要調用 super.clone() 來實現。

從上面我們可以看出,原型模式的主要特征就是使用 clone 方法復制一個對象。通常,有些人會誤以為 Object a=new Object();Object b=a; 這種形式就是一種對象復制的過程,然而這種復制只是對象引用的復制,也就是 a 和 b 對象指向了同一個內存地址,如果 b 修改了,a 的值也就跟著被修改了。

我們可以通過一個簡單的例子來看看普通的對象復制問題:

class Student {  private String name;  public String getName() {  return name;  }  public void setName(String name) {  this.name= name;  }  }  
public class Test {  public static void main(String args[]) {  Student stu1 = new Student();  stu1.setName("test1");  Student stu2 = stu1;  stu1.setName("test2");  System.out.println(" 學生 1:" + stu1.getName());  System.out.println(" 學生 2:" + stu2.getName());  }  
}

如果是復制對象,此時打印的日志應該為:

學生 1:test1
學生 2:test2

然而,實際上是:

學生 2:test2
學生 2:test2

通過 clone 方法復制的對象才是真正的對象復制,clone 方法賦值的對象完全是一個獨立的對象。剛剛講過了,Object 類的 clone 方法是一個本地方法,它直接操作內存中的二進制流,特別是復制大對象時,性能的差別非常明顯。我們可以用 clone 方法再實現一遍以上例子。

// 學生類實現 Cloneable 接口
class Student implements Cloneable{  private String name;  // 姓名public String getName() {  return name;  }  public void setName(String name) {  this.name= name;  } // 重寫 clone 方法public Student clone() { Student student = null; try { student = (Student) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return student; } }  
public class Test {  public static void main(String args[]) {  Student stu1 = new Student();  // 創建學生 1stu1.setName("test1");  Student stu2 = stu1.clone();  // 通過克隆創建學生 2stu2.setName("test2");  System.out.println(" 學生 1:" + stu1.getName());  System.out.println(" 學生 2:" + stu2.getName());  }  
}

運行結果:

學生 1:test1
學生 2:test2

1.2、深拷貝和淺拷貝

在調用 super.clone() 方法之后,首先會檢查當前對象所屬的類是否支持 clone,也就是看該類是否實現了 Cloneable 接口。

如果支持,則創建當前對象所屬類的一個新對象,并對該對象進行初始化,使得新對象的成員變量的值與當前對象的成員變量的值一模一樣,但對于其它對象的引用以及 List 等類型的成員屬性,則只能復制這些對象的引用了。所以簡單調用 super.clone() 這種克隆對象方式,就是一種淺拷貝。

所以,當我們在使用 clone() 方法實現對象的克隆時,就需要注意淺拷貝帶來的問題。我們再通過一個例子來看看淺拷貝。

// 定義學生類
class Student implements Cloneable{  private String name; // 學生姓名private Teacher teacher; // 定義老師類public String getName() {  return name;  }  public void setName(String name) {  this.name = name;  } public Teacher getTeacher() {  return teacher;  }  public void setName(Teacher teacher) {  this.teacher = teacher;  } // 重寫克隆方法public Student clone() { Student student = null; try { student = (Student) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return student; } }  // 定義老師類
class Teacher implements Cloneable{  private String name;  // 老師姓名public String getName() {  return name;  }  public void setName(String name) {  this.name= name;  } // 重寫克隆方法,堆老師類進行克隆public Teacher clone() { Teacher teacher= null; try { teacher= (Teacher) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return student; } }
public class Test {  public static void main(String args[]) {Teacher teacher = new Teacher (); // 定義老師 1teacher.setName(" 劉老師 ");Student stu1 = new Student();  // 定義學生 1stu1.setName("test1");           stu1.setTeacher(teacher);Student stu2 = stu1.clone(); // 定義學生 2stu2.setName("test2");  stu2.getTeacher().setName(" 王老師 ");// 修改老師System.out.println(" 學生 " + stu1.getName + " 的老師是:" + stu1.getTeacher().getName);  System.out.println(" 學生 " + stu1.getName + " 的老師是:" + stu2.getTeacher().getName);  }  
}

運行結果:

學生 test1 的老師是:王老師
學生 test2 的老師是:王老師

觀察以上運行結果,我們可以發現:在我們給學生 2 修改老師的時候,學生 1 的老師也跟著被修改了。這就是淺拷貝帶來的問題。

我們可以通過深拷貝來解決這種問題,其實深拷貝就是基于淺拷貝來遞歸實現具體的每個對象,代碼如下:

   public Student clone() { Student student = null; try { student = (Student) super.clone(); Teacher teacher = this.teacher.clone();// 克隆 teacher 對象student.setTeacher(teacher);} catch (CloneNotSupportedException e) { e.printStackTrace(); } return student; } 

1.3、適用場景

前面我詳講了原型模式的實現原理,那到底什么時候我們要用它呢?

在一些重復創建對象的場景下,我們就可以使用原型模式來提高對象的創建性能。例如,我在開頭提到的,循環體內創建對象時,我們就可以考慮用 clone 的方式來實現。

例如:

for(int i=0; i<list.size(); i++){Student stu = new Student(); ...
}

我們可以優化為:

Student stu = new Student(); 
for(int i=0; i<list.size(); i++){Student stu1 = (Student)stu.clone();...
}

?除此之外,原型模式在開源框架中的應用也非常廣泛。例如 Spring 中,@Service 默認都是單例的。用了私有全局變量,若不想影響下次注入或每次上下文獲取 bean,就需要用到原型模式,我們可以通過以下注解來實現,@Scope(“prototype”)。

2、享元模式

享元模式是運用共享技術有效地最大限度地復用細粒度對象的一種模式。該模式中,以對象的信息狀態劃分,可以分為內部數據和外部數據。內部數據是對象可以共享出來的信息,這些信息不會隨著系統的運行而改變;外部數據則是在不同運行時被標記了不同的值。

享元模式一般可以分為三個角色,分別為 Flyweight(抽象享元類)、ConcreteFlyweight(具體享元類)和 FlyweightFactory(享元工廠類)。抽象享元類通常是一個接口或抽象類,向外界提供享元對象的內部數據或外部數據;具體享元類是指具體實現內部數據共享的類;享元工廠類則是主要用于創建和管理享元對象的工廠類。

2.1、實現享元模式

我們還是通過一個簡單的例子來實現一個享元模式:

// 抽象享元類
interface Flyweight {// 對外狀態對象void operation(String name);// 對內對象String getType();
}
// 具體享元類
class ConcreteFlyweight implements Flyweight {private String type;public ConcreteFlyweight(String type) {this.type = type;}@Overridepublic void operation(String name) {System.out.printf("[類型 (內在狀態)] - [%s] - [名字 (外在狀態)] - [%s]\n", type, name);}@Overridepublic String getType() {return type;}
}
// 享元工廠類
class FlyweightFactory {private static final Map<String, Flyweight> FLYWEIGHT_MAP = new HashMap<>();// 享元池,用來存儲享元對象public static Flyweight getFlyweight(String type) {if (FLYWEIGHT_MAP.containsKey(type)) {// 如果在享元池中存在對象,則直接獲取return FLYWEIGHT_MAP.get(type);} else {// 在響應池不存在,則新創建對象,并放入到享元池ConcreteFlyweight flyweight = new ConcreteFlyweight(type);FLYWEIGHT_MAP.put(type, flyweight);return flyweight;}}
}
public class Client {public static void main(String[] args) {Flyweight fw0 = FlyweightFactory.getFlyweight("a");Flyweight fw1 = FlyweightFactory.getFlyweight("b");Flyweight fw2 = FlyweightFactory.getFlyweight("a");Flyweight fw3 = FlyweightFactory.getFlyweight("b");fw1.operation("abc");System.out.printf("[結果 (對象對比)] - [%s]\n", fw0 == fw2);System.out.printf("[結果 (內在狀態)] - [%s]\n", fw1.getType());}
}

輸出結果:

[類型 (內在狀態)] - [b] - [名字 (外在狀態)] - [abc]
[結果 (對象對比)] - [true]
[結果 (內在狀態)] - [b]

觀察以上代碼運行結果,我們可以發現:如果對象已經存在于享元池中,則不會再創建該對象了,而是共用享元池中內部數據一致的對象。這樣就減少了對象的創建,同時也節省了同樣內部數據的對象所占用的內存空間。

2.2、適用場景

享元模式在實際開發中的應用也非常廣泛。例如 Java 的 String 字符串,在一些字符串常量中,會共享常量池中字符串對象,從而減少重復創建相同值對象,占用內存空間。代碼如下:

 String s1 = "hello";String s2 = "hello";System.out.println(s1==s2);//true

還有,在日常開發中的應用。例如,線程池就是享元模式的一種實現;將商品存儲在應用服務的緩存中,那么每當用戶獲取商品信息時,則不需要每次都從 redis 緩存或者數據庫中獲取商品信息,并在內存中重復創建商品信息了。

3、總結

通過以上講解,相信你對原型模式和享元模式已經有了更清楚的了解了。兩種模式無論是在開源框架,還是在實際開發中,應用都十分廣泛。

在不得已需要重復創建大量同一對象時,我們可以使用原型模式,通過 clone 方法復制對象,這種方式比用 new 和序列化創建對象的效率要高;在創建對象時,如果我們可以共用對象的內部數據,那么通過享元模式共享相同的內部數據的對象,就可以減少對象的創建,實現系統調優。

4、思考題

上一講的單例模式和這一講的享元模式都是為了避免重復創建對象,你知道這兩者的區別在哪兒嗎?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/161974.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/161974.shtml
英文地址,請注明出處:http://en.pswp.cn/news/161974.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

TC397 EB MCAL開發從0開始系列 之 [15.1] Fee配置 - 雙扇區demo

一、Fee配置1、配置目標2、目標依賴2.1 硬件使用2.2 軟件使用2.3 新增模塊3、EB配置3.1 配置講解3.2 模塊配置3.2.1 MCU配置3.2.2 PORT配置3.2.3 Fls_17_Dmu配置3.2.4 Fee配置3.2.5 Irq配置3.2.6 ResourceM配置4、ADS代碼編寫及調試4.1 工程編譯4.2 測試結果4.3 測例源碼->

2023年學習Go語言是否值得?探索Go語言的魅力

關注公眾號【愛發白日夢的后端】分享技術干貨、讀書筆記、開源項目、實戰經驗、高效開發工具等&#xff0c;您的關注將是我的更新動力&#xff01; 作為一門流行且不斷增長的編程語言&#xff0c;Go語言在2023年是否值得學習呢&#xff1f;讓我們來看看學習Go語言的好處以及為何…

Java使用Maven打包jar包的全部方式

1. spring-boot-maven-plugin插件&#xff08;在springboot項目中使用&#xff09; <plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><executions><execution><goals>…

1410.HTML 實體解析器

??題目來源&#xff1a; leetcode題目&#xff0c;網址&#xff1a;1410. HTML 實體解析器 - 力扣&#xff08;LeetCode&#xff09; 解題思路&#xff1a; 使用map存放特殊字符串及其應被替換為的字符串。然后遍歷字符串替換 map 中的字符串即可。 解題代碼&#xff1a; …

ubuntu 手動清理內存cache

/proc是一個虛擬文件系統&#xff0c;我們可以通過對它的讀寫操作來做為與kernel實體間進行通信的一種手段。也就是說可以通過修改/proc中的文件&#xff0c;來對當前kernel的行為做出調整。 那么我們可以通過調整/proc/sys/vm/drop_caches來釋放內存。操作如下&#xff1a; …

富士康轉移產線和中國手機海外設廠,中國手機出口減少超5億部

富士康和蘋果轉移生產線對中國手機制造造成了巨大的影響&#xff0c;除此之外&#xff0c;中國手機企業紛紛在海外設廠也在減少中國手機的出口&#xff0c;2022年中國的手機出口較高峰期減少了5.2億部。 手機是中國的大宗出口商品&#xff0c;不過公開的數據顯示2022年中國的手…

每日OJ題_算法_雙指針_力扣202. 快樂數

力扣202. 快樂數 202. 快樂數 - 力扣&#xff08;LeetCode&#xff09; 難度 簡單 編寫一個算法來判斷一個數 n 是不是快樂數。 「快樂數」 定義為&#xff1a; 對于一個正整數&#xff0c;每一次將該數替換為它每個位置上的數字的平方和。然后重復這個過程直到這個數變為…

RT-Thread 線程間同步【信號量、互斥量、事件集】

線程間同步 一、信號量1. 創建信號量2. 獲取信號量3. 釋放信號量4. 刪除信號量5. 代碼示例 二、互斥量1. 創建互斥量2. 獲取互斥量3. 釋放互斥量4. 刪除互斥量5. 代碼示例 三、事件集1. 創建事件集2. 發送事件3. 接收事件4. 刪除事件集5. 代碼示例 簡單來說&#xff0c;同步就是…

PDF轉成圖片

使用開源庫Apache PDFBox將PDF轉換為圖片 依賴 <dependency><groupId>org.apache.pdfbox</groupId><artifactId>fontbox</artifactId><version>2.0.4</version> </dependency> <dependency><groupId>org.apache…

DockerHub 無法訪問 - 解決辦法

背景 DockerHub 鏡像倉庫地址 https://hub.docker.com/ 突然就無法訪問了,且截至今日(2023/11)還無法訪問。 這對我們來說,還是有一些影響的: ● 雖然 DockerHub 頁面無法訪問,但是還是可以下載鏡像的,只是比較慢而已 ● 沒法通過界面查詢相關鏡像,或者維護相關鏡像了…

JAVA 使用stream流將List中的對象某一屬性創建新的List

JAVA 使用stream流將List中的對象某一屬性創建新的List 1.stream流介紹 Java Stream是Java 8引入的一種新機制&#xff0c;它可以讓我們以聲明式方式操作集合數據&#xff0c;提供了更加簡潔、優雅的集合處理方式。Stream是一個來自數據源的元素隊列&#xff0c;并支持聚合操…

【Rxjava詳解】(二) 操作符的妙用

文章目錄 接口變化操作符mapflatmapdebouncethrottleFirst()takeconcat RxJava 是一個基于 觀察者模式的異步編程庫&#xff0c;它提供了豐富的操作符來處理和轉換數據流。 操作符是 RxJava 的核心組成部分&#xff0c;它們提供了一種靈活、可組合的方式來處理數據流&#xf…

C++二分算法:得到子序列的最少操作次數

本文涉及的基礎知識點 二分查找算法合集 題目 給你一個數組 target &#xff0c;包含若干 互不相同 的整數&#xff0c;以及另一個整數數組 arr &#xff0c;arr 可能 包含重復元素。 每一次操作中&#xff0c;你可以在 arr 的任意位置插入任一整數。比方說&#xff0c;如果…

【如何學習Python自動化測試】—— 多層窗口定位

6 、 多層窗口定位 多層窗口指的是在操作系統圖形界面中&#xff0c;一個窗口被另一個窗口覆蓋的情況。在多層窗口中&#xff0c;如何定位需要操作的窗口&#xff1f; 一種常見的方法是使用操作系統提供的AltTab快捷鍵&#xff0c;可以在打開的所有窗口中快速切換焦點。如果需要…

第十三章 控制值的轉換 - 處理UTC時區指示符

文章目錄 第十三章 控制值的轉換 - 處理UTC時區指示符 第十三章 控制值的轉換 - 處理UTC時區指示符 對于支持XML的類&#xff0c;可以指定在從XML文檔導入時是否使用UTC時區指示符。同樣&#xff0c;可以指定是否在導出時包含UTC時區指示符。 為此&#xff0c;指定XMLTIMEZON…

GEE生物量碳儲量——利用sens和MK檢驗方法計算1987-2022年森林地上生物量AGB和碳儲量的時空變化特征

簡介: 本文是將之前已經處理好的森林生物量和碳儲量數據保存到GEE Assets中,然后分別將單張影像導入到代碼編輯器中,構建一個時間序列集合,并且這里需要用到的是我們給影像添加指定的時間屬性,這樣方便進行下一步的時序分析和空間預測。 首先,需要收集1987年至2022年期…

C語言實現Linux下TCP Server測試工具

Linux TCP Server測試工具代碼 實現了接受數據并輸出文本和十六制字符串 #include <stdio.h> #include<string.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <signal.h> #include <arpa/inet.h> #incl…

STM32內存介紹

ROM是一種只讀存儲器&#xff0c;經歷了從NOR Flash到NAND Flash再到現在的eMMC的發展。為了便于使用和大批量生產&#xff0c;ROM進一步分為了4種類型&#xff1a;PROM、EPROM、EEPROM和Flash。PROM只能被編程一次&#xff0c;EPROM可擦寫可編程且可達1000次&#xff0c;EEPRO…

leetcode/hot100

文章目錄 一、哈希1.兩數之和2.字母異位詞分組3.最長連續序列 二、雙指針4. 移動零5.盛最多水的容器6.三數之和7.接雨水 三、滑動窗口8.無重復字符的最長子串9.找到字符串中所有字母異位詞 四、子串10.和為 K 的子數組 一、哈希 1.兩數之和 1. 兩數之和 class Solution { pu…