手機網(wǎng)站營銷的含義建設(shè)網(wǎng)站的網(wǎng)絡(luò)公司
個人主頁:金鱗踏雨
個人簡介:大家好,我是金鱗,一個初出茅廬的Java小白
目前狀況:22屆普通本科畢業(yè)生,幾經(jīng)波折了,現(xiàn)在任職于一家國內(nèi)大型知名日化公司,從事Java開發(fā)工作
我的博客:這里是CSDN,是我學(xué)習(xí)技術(shù),總結(jié)知識的地方。希望和各位大佬交流,共同進步 ~
把相同、相似的一些對象和屬性拿來復(fù)用,以至于節(jié)省內(nèi)存;由于這些對象將會被共享,所以它們最好是不可變的(不要又set() 方法)!
主要是通過工廠模式,在工廠類中,通過一個 Map 來緩存已經(jīng)創(chuàng)建過的享元對象,來達到復(fù)用的目的。
本篇博客來自IT楠老師的視頻教程,結(jié)合個人理解后輸出~
一、享元模式原理與實現(xiàn)
所謂“享元”,顧名思義就是被共享的單元。他也是一個不怎么常用的設(shè)計模式,享元模式的意圖是復(fù)用對象,節(jié)省內(nèi)存,前提是享元對象是不可變對象。
具體來講,當(dāng)一個系統(tǒng)中存在大量重復(fù)對象的時候,如果這些重復(fù)的對象是不可變對象,我們就可以利用享元模式將對象設(shè)計成享元,在內(nèi)存中只保留一份實例,供多處代碼引用。這樣可以減少內(nèi)存中對象的數(shù)量,起到節(jié)省內(nèi)存的目的。實際上,不僅僅相同對象可以設(shè)計成享元,對于相似對象,我們也可以將這些對象中相同的部分(字段)提取出來,設(shè)計成享元,讓這些大量相似對象引用這些享元。
這里定義中的“不可變對象”指的是,一旦通過構(gòu)造函數(shù)初始化完成之后,它的狀態(tài)(對象的成員變量或者屬性)就不會再被修改了。所以,不可變對象不能暴露任何 set() 等修改內(nèi)部狀態(tài)的方法。之所以要求享元是不可變對象,那是因為它會被多處代碼共享使用,避免一處代碼對享元進行了修改,影響到其他使用它的代碼。
接下來,我們通過一個簡單的例子解釋一下享元模式。
假設(shè)我們在開發(fā)一個棋牌游戲(比如象棋)。一個游戲廳中有成千上萬個“房間”,每個房間對應(yīng)一個棋局。棋局要保存每個棋子的數(shù)據(jù),比如:棋子類型(將、相、士、炮等)、棋子顏色(紅方、黑方)、棋子在棋局中的位置。利用這些數(shù)據(jù),我們就能顯示一個完整的棋盤給玩家。具體的代碼如下所示。其中,ChessPiece 類表示棋子,ChessBoard 類表示一個棋局,里面保存了象棋中 30 個棋子的信息。
public class ChessPiece {//棋子private int id;private String text;private Color color;public ChessPiece(int id, String text, Color color, int positionX, int positionY) {this.id = id;this.text = text;this.color = color;this.positionX = positionX;this.positionY = positionX;}public static enum Color {RED, BLACK}// ...省略其他屬性和getter/setter方法...
}public class ChessBoard {//棋局private Map<Integer, ChessPiece> chessPieces = new HashMap<>();public ChessBoard() {init();}private void init() {chessPieces.put(1, new ChessPiece(1, "車", ChessPiece.Color.BLACK, 0, 0));chessPieces.put(2, new ChessPiece(2,"馬", ChessPiece.Color.BLACK, 0, 1));//...省略擺放其他棋子的代碼...}public void move(int chessPieceId, int toPositionX, int toPositionY) {//...省略...}
}
為了記錄每個房間當(dāng)前的棋局情況,我們需要給每個房間都創(chuàng)建一個 ChessBoard 棋局對象。因為游戲大廳中有成千上萬的房間(實際上,百萬人同時在線的游戲大廳也有很多),那保存這么多棋局對象就會消耗大量的內(nèi)存。有沒有什么辦法來節(jié)省內(nèi)存呢?
這個時候,享元模式就可以派上用場了。
上述案例,在內(nèi)存中會有大量的相似對象。這些相似對象的 id、text、color 都是相同的,唯獨 positionX、positionY 不同。實際上,我們可以將棋子的 id、text、color 屬性拆分出來,設(shè)計成獨立的類,并且作為享元供多個棋盤復(fù)用。這樣,棋盤只需要記錄每個棋子的位置信息就可以了。具體的代碼實現(xiàn)如下所示:
// 享元類
@ToString
public class ChessUnit {private Long id;private String text;private Color Color;public ChessUnit(Long id, String text, ChessUnit.Color color) {this.id = id;this.text = text;Color = color;}// 枚舉(紅、黑)public enum Color{RED,BLACK}
}// 享元工廠
public class ChessUnitFactory {private static Map<Long,ChessUnit> chessUnitMap = new HashMap<>(64);static {chessUnitMap.put(1L,new ChessUnit(1L,"兵",ChessUnit.Color.RED));chessUnitMap.put(2L,new ChessUnit(2L,"馬",ChessUnit.Color.RED));chessUnitMap.put(3L,new ChessUnit(3L,"炮",ChessUnit.Color.RED));chessUnitMap.put(4L,new ChessUnit(4L,"將",ChessUnit.Color.RED));chessUnitMap.put(5L,new ChessUnit(5L,"將",ChessUnit.Color.BLACK));}/*** 暴露一個工廠方法,用來獲取棋子* @param id 棋子的id* @return 棋子*/public static ChessUnit getChessUnit(Long id){return chessUnitMap.get(id);}
}// 注意一定需要將其hashCode重寫,否則它不能作為key使用(@EqualsAndHashCode)
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class Position {private int positionX;private int positionY;
}// 棋子
@Data
@AllArgsConstructor
public class ChessPiece {private ChessUnit chessUnit;private Position position;
}// 棋盤
public class ChessBoard {// 應(yīng)該持有一個套棋子(有具體的坐標(biāo))private Map<Position, ChessPiece> chessPieceMap;public ChessBoard() {// 構(gòu)造棋牌this.chessPieceMap = new HashMap<>(64);// 初始化棋牌Position position1 = new Position(1, 2);chessPieceMap.put(position1,new ChessPiece(ChessUnitFactory.getChessUnit(1L),position1));Position position2 = new Position(1, 4);chessPieceMap.put(position2,new ChessPiece(ChessUnitFactory.getChessUnit(1L),position2));Position position3 = new Position(1, 5);chessPieceMap.put(position3,new ChessPiece(ChessUnitFactory.getChessUnit(3L),position3));}public void display(){for (Map.Entry<Position,ChessPiece> entry : chessPieceMap.entrySet()){System.out.println(entry.getKey() + "-->" + entry.getValue());}}public static void main(String[] args) {ChessBoard chessBoard = new ChessBoard();chessBoard.display();}
}
在上面的代碼實現(xiàn)中,我們利用工廠類來緩存 ChessPieceUnit 信息(也就是 id、text、color)。通過工廠類獲取到的 ChessPieceUnit 就是享元。所有的 ChessBoard 對象共享這 30 個 ChessPieceUnit 對象(因為象棋中只有 30 個棋子)。在使用享元模式之前,記錄 1 萬個棋局,我們要創(chuàng)建 30 萬(30*1 萬)個棋子的 ChessPieceUnit 對象。利用享元模式,我們只需要創(chuàng)建 30 個享元對象供所有棋局共享使用即可,大大節(jié)省了內(nèi)存。
二、源碼應(yīng)用
1、享元模式在 Java Integer 中的應(yīng)用
如何判定兩個 Java 對象是否相等(也就代碼中的“==”操作符的含義)?
什么是自動裝箱(Autoboxing)和自動拆箱(Unboxing)?
Java 為基本數(shù)據(jù)類型提供了對應(yīng)的包裝器類型。
Integer i = 56; //自動裝箱
int j = i; //自動拆箱
自動裝箱:就是自動將基本數(shù)據(jù)類型轉(zhuǎn)換為包裝器類型。
數(shù)值 56 是基本數(shù)據(jù)類型 int,當(dāng)賦值給包裝器類型(Integer)變量的時候,觸發(fā)自動裝箱操作,創(chuàng)建一個 Integer 類型的對象,并且賦值給變量 i。其底層相當(dāng)于執(zhí)行了下面這條語句
Integer i = 56;//底層執(zhí)行了:Integer i = Integer.valueOf(56);
自動拆箱:也就是自動將包裝器類型轉(zhuǎn)化為基本數(shù)據(jù)類型。
當(dāng)把包裝器類型的變量 i,賦值給基本數(shù)據(jù)類型變量 j 的時候,觸發(fā)自動拆箱操作,將 i 中的數(shù)據(jù)取出,賦值給 j。其底層相當(dāng)于執(zhí)行了下面這條語句
int j = i; //底層執(zhí)行了:int j = i.intValue();
“==”來判定兩個對象是否相等的時候,實際上是在判斷兩個局部變量存儲的地址是否相同,換句話說,是在判斷兩個局部變量是否指向相同的對象。
下面的這段"神奇"的代碼會輸出什么???
Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2); // true
System.out.println(i3 == i4); // false
一個 true,一個 false
實際上,這正是因為 Integer 用到了享元模式來復(fù)用對象,才導(dǎo)致了這樣的運行結(jié)果。當(dāng)我們通過自動裝箱,也就是調(diào)用 valueOf() 來創(chuàng)建 Integer 對象的時候,如果要創(chuàng)建的 Integer 對象的值在 -128 到 127 之間,會從 IntegerCache 類中直接返回,否則才調(diào)用 new 方法創(chuàng)建。
public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];return new Integer(i);
}
實際上,這里的 IntegerCache 相當(dāng)于,生成享元對象的工廠類,只不過名字不叫 xxxFactory 而已。這個類是 Integer 的內(nèi)部類。
private static class IntegerCache {// 下限static final int low = -128;// 上限(根據(jù)配置或默認值來確定)static final int high;// 存儲整數(shù)對象的數(shù)組static final Integer cache[];static {// high value may be configured by propertyint h = 127;String integerCacheHighPropValue =sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");if (integerCacheHighPropValue != null) {try {int i = parseInt(integerCacheHighPropValue);i = Math.max(i, 127);// Maximum array size is Integer.MAX_VALUEh = Math.min(i, Integer.MAX_VALUE - (-low) -1);} catch( NumberFormatException nfe) {// If the property cannot be parsed into an int, ignore it.}}high = h;cache = new Integer[(high - low) + 1];int j = low;for(int k = 0; k < cache.length; k++)cache[k] = new Integer(j++);// range [-128, 127] must be interned (JLS7 5.1.7)assert IntegerCache.high >= 127;}private IntegerCache() {}
}
為什么 IntegerCache 只緩存 -128 到 127 之間的整型值呢?
在 IntegerCache 的代碼實現(xiàn)中,當(dāng)這個類被加載的時候,緩存的享元對象會被集中一次性創(chuàng)建好。畢竟整型值太多了,我們不可能在 IntegerCache 類中預(yù)先創(chuàng)建好所有的整型值,這樣既占用太多內(nèi)存,也使得加載 IntegerCache 類的時間過長。所以,我們只能選擇緩存對于大部分應(yīng)用來說最常用的整型值,也就是一個字節(jié)的大小(-128 到 127 之間的數(shù)據(jù))。
實際上,JDK 也提供了方法來讓我們可以自定義緩存的最大值,有下面兩種方式。如果你通過分析應(yīng)用的 JVM 內(nèi)存占用情況,發(fā)現(xiàn) -128 到 255 之間的數(shù)據(jù)占用的內(nèi)存比較多,你就可以用如下方式,將緩存的最大值從 127 調(diào)整到 255。不過,這里注意一下,JDK 并沒有提供設(shè)置最小值的方法。
//方法一:
-Djava.lang.Integer.IntegerCache.high=255
//方法二:
-XX:AutoBoxCacheMax=255
現(xiàn)在,讓我們再回到最開始的問題,因為 56 處于 -128 和 127 之間,i1 和 i2 會指向相同的享元對象,所以 i1i2 返回 true。而 129 大于 127,并不會被緩存,每次都會創(chuàng)建一個全新的對象,也就是說,i3 和 i4 指向不同的 Integer 對象,所以 i3i4 返回 false。
實際上,除了 Integer 類型之外,其他包裝器類型,比如 Long、Short、Byte 等,也都利用了享元模式來緩存 -128 到 127 之間的數(shù)據(jù)。比如,Long 類型對應(yīng)的 LongCache 享元工廠類及 valueOf() 函數(shù)代碼如下所示:
private static class LongCache {private LongCache(){}static final Long cache[] = new Long[-(-128) + 127 + 1];static {for(int i = 0; i < cache.length; i++)cache[i] = new Long(i - 128);}
}
public static Long valueOf(long l) {final int offset = 128;if (l >= -128 && l <= 127) { // will cachereturn LongCache.cache[(int)l + offset];}return new Long(l);
}
在我們平時的開發(fā)中,對于下面這樣三種創(chuàng)建整型對象的方式,我們優(yōu)先使用后兩種。
Integer a = new Integer(123);
Integer a = 123;
Integer a = Integer.valueOf(123);
第一種創(chuàng)建方式并不會使用到 IntegerCache,而后面兩種創(chuàng)建方法可以利用 IntegerCache 緩存,返回共享的對象,以達到節(jié)省內(nèi)存的目的。
舉一個極端一點的例子,假設(shè)程序需要創(chuàng)建 1 萬個 -128 ~ 127 之間的 Integer 對象。使用第一種創(chuàng)建方式,我們需要分配 1 萬個 Integer 對象的內(nèi)存空間;使用后兩種創(chuàng)建方式,我們最多只需要分配 256 個 Integer 對象的內(nèi)存空間。
2、享元模式在 Java String 中的應(yīng)用
剛剛我們講了享元模式在 Java Integer 類中的應(yīng)用,現(xiàn)在,我們再來看下,享元模式在 Java String 類中的應(yīng)用。同樣,我們還是先來看一段代碼,你覺得這段代碼輸出的結(jié)果是什么呢?
String s1 = "楠老師";
String s2 = "楠老師";
String s3 = new String("楠老師");System.out.println(s1 == s2);
System.out.println(s1 == s3);
上面代碼的運行結(jié)果是:一個 true,一個 false。跟 Integer 類的設(shè)計思路相似,String 類利用享元模式來復(fù)用相同的字符串常量(也就是代碼中的“小爭哥”)。JVM 會專門開辟一塊存儲區(qū)來存儲字符串常量,這塊存儲區(qū)叫作“字符串常量池”。上面代碼對應(yīng)的內(nèi)存存儲結(jié)構(gòu)如下所示:
不過,String 類的享元模式的設(shè)計,跟 Integer 類稍微有些不同。Integer 類中要共享的對象,是在類加載的時候,就集中一次性創(chuàng)建好的。但是,對于字符串來說,我們沒法事先知道要共享哪些字符串常量,所以沒辦法事先創(chuàng)建好,只能在某個字符串常量第一次被用到的時候,存儲到常量池中,當(dāng)之后再用到的時候,直接引用常量池中已經(jīng)存在的即可,就不需要再重新創(chuàng)建了。
三、享元模式和單例、緩存、池化的區(qū)別
在上面的講解中,我們多次提到“共享”“緩存”“復(fù)用”這些字眼,那它跟單例、緩存、對象池這些概念有什么區(qū)別呢?
享元模式跟單例的區(qū)別
在單例模式中,一個類只能創(chuàng)建一個對象,而在享元模式中,一個類可以創(chuàng)建多個對象,每個對象被多處代碼引用共享。
我們前面也多次提到,區(qū)別兩種設(shè)計模式,不能光看代碼實現(xiàn),而是要看設(shè)計意圖,也就是要解決的問題。盡管從代碼實現(xiàn)上來看,享元模式和多例有很多相似之處,但從設(shè)計意圖上來看,它們是完全不同的。應(yīng)用享元模式是為了對象復(fù)用,節(jié)省內(nèi)存,而應(yīng)用多例模式是為了限制對象的個數(shù)。
享元模式跟緩存的區(qū)別
在享元模式的實現(xiàn)中,我們通過工廠類來“緩存”已經(jīng)創(chuàng)建好的對象。
這里的“緩存”實際上是“存儲”的意思,跟我們平時所說的“數(shù)據(jù)庫緩存”“CPU 緩存”“MemCache 緩存”是兩回事。我們平時所講的緩存,主要是為了提高訪問效率,而非復(fù)用。
享元模式跟對象池的區(qū)別
對象池、連接池(比如數(shù)據(jù)庫連接池)、線程池等也是為了復(fù)用,那它們跟享元模式有什么區(qū)別呢?
雖然對象池、連接池、線程池、享元模式都是為了復(fù)用,但是,如果我們再細致地摳一摳“復(fù)用”這個字眼的話,對象池、連接池、線程池等池化技術(shù)中的“復(fù)用”和享元模式中的“復(fù)用”實際上是不同的概念。
- 池化技術(shù)中的“復(fù)用”可以理解為“重復(fù)使用”,主要目的是節(jié)省時間(比如從數(shù)據(jù)庫池中取一個連接,不需要重新創(chuàng)建)。在任意時刻,每一個對象、連接、線程,并不會被多處使用,而是被一個使用者獨占,當(dāng)使用完成之后,放回到池中,再由其他使用者重復(fù)利用。
- 享元模式中的“復(fù)用”可以理解為“共享使用”,在整個生命周期中,都是被所有使用者共享的,主要目的是節(jié)省空間。
文章到這里就結(jié)束了,如果有什么疑問的地方,可以在評論區(qū)指出~
希望能和大佬們一起努力,諸君頂峰相見
再次感謝各位小伙伴兒們的支持!!!