下面總結(jié)設(shè)計模式中的創(chuàng)建型模式:
1.簡單工廠模式
簡單工廠不是設(shè)計模式,更像是一種編程習(xí)慣。它把實(shí)例化的操作單獨(dú)放到一個類中,這個類就成為簡單工廠類,讓簡單工廠類來決定應(yīng)該用哪個具體子類來實(shí)例化。
在簡單工廠模式中,我們在創(chuàng)建對象時不會對客戶端暴露創(chuàng)建邏輯,并且是通過使用一個共同的接口來指向新創(chuàng)建的對象,從而將客戶類和具體子類的實(shí)現(xiàn)解耦,客戶類不再需要知道有哪些子類以及應(yīng)當(dāng)實(shí)例化哪個子類。客戶類往往有多個,如果不使用簡單工廠,那么所有的客戶類都要知道所有子類的細(xì)節(jié)。而且一旦子類發(fā)生改變,例如增加子類,那么所有的客戶類都要進(jìn)行修改。
介紹
意圖:在創(chuàng)建一個對象時不向客戶暴露內(nèi)部細(xì)節(jié),并提供一個創(chuàng)建對象的通用接口。
主要解決:主要解決接口選擇的問題。
何時使用:我們明確地計劃不同條件下創(chuàng)建不同實(shí)例時。
如何解決:讓其子類實(shí)現(xiàn)工廠接口,返回的也是一個抽象的產(chǎn)品。
關(guān)鍵代碼:創(chuàng)建過程在其子類執(zhí)行。
應(yīng)用實(shí)例: 1、您需要一輛汽車,可以直接從工廠里面提貨,而不用去管這輛汽車是怎么做出來的,以及這個汽車?yán)锩娴木唧w實(shí)現(xiàn)。 2、Hibernate 換數(shù)據(jù)庫只需換方言和驅(qū)動就可以。
優(yōu)點(diǎn): 1、一個調(diào)用者想創(chuàng)建一個對象,只要知道其名稱就可以了。 2、擴(kuò)展性高,如果想增加一個產(chǎn)品,只要擴(kuò)展一個工廠類就可以。 3、屏蔽產(chǎn)品的具體實(shí)現(xiàn),調(diào)用者只關(guān)心產(chǎn)品的接口。
缺點(diǎn):每次增加一個產(chǎn)品時,都需要增加一個具體類和對象實(shí)現(xiàn)工廠,使得系統(tǒng)中類的個數(shù)成倍增加,在一定程度上增加了系統(tǒng)的復(fù)雜度,同時也增加了系統(tǒng)具體類的依賴。這并不是什么好事。
使用場景: 1、日志記錄器:記錄可能記錄到本地硬盤、系統(tǒng)事件、遠(yuǎn)程服務(wù)器等,用戶可以選擇記錄日志到什么地方。 2、數(shù)據(jù)庫訪問,當(dāng)用戶不知道最后系統(tǒng)采用哪一類數(shù)據(jù)庫,以及數(shù)據(jù)庫可能有變化時。 3、設(shè)計一個連接服務(wù)器的框架,需要三個協(xié)議,"POP3"、"IMAP"、"HTTP",可以把這三個作為產(chǎn)品類,共同實(shí)現(xiàn)一個接口。
注意事項:作為一種創(chuàng)建類模式,在任何需要生成復(fù)雜對象的地方,都可以使用工廠模式。有一點(diǎn)需要注意的地方就是復(fù)雜對象適合使用工廠模式,而簡單對象,特別是只需要通過 new 就可以完成創(chuàng)建的對象,無需使用工廠模式。如果使用工廠模式,就需要引入一個工廠類,會增加系統(tǒng)的復(fù)雜度。
實(shí)現(xiàn)
我們將創(chuàng)建一個 Shape 接口和實(shí)現(xiàn) Shape 接口的實(shí)體類。下一步是定義工廠類 ShapeFactory。
FactoryPatternDemo,我們的演示類使用 ShapeFactory 來獲取 Shape 對象。它將向 ShapeFactory 傳遞信息(CIRCLE / RECTANGLE / SQUARE),以便獲取它所需對象的類型。
步驟 1
創(chuàng)建一個接口。
Shape.java
public interface Shape {
void draw();
}
步驟 2
創(chuàng)建實(shí)現(xiàn)接口的實(shí)體類。
Rectangle.java
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
Square.java
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}
Circle.java
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
步驟 3
創(chuàng)建一個工廠,生成基于給定信息的實(shí)體類的對象。
ShapeFactory.java
public class ShapeFactory {
/*使用 getShape 方法獲取形狀類型的對象,
在實(shí)際操作中建議把CIRCLE等定義為常量*/
public static Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("SQUARE")){
return new Square();
}
return null;
}
}
步驟 4
使用該工廠,通過傳遞類型信息來獲取實(shí)體類的對象。
FactoryPatternDemo.java
public class FactoryPatternDemo {
public static void main(String[] args) {
//獲取 Circle 的對象,并調(diào)用它的 draw 方法
Shape shape1 = ShapeFactory.getShape("CIRCLE");
//調(diào)用 Circle 的 draw 方法
shape1.draw();
//獲取 Rectangle 的對象,并調(diào)用它的 draw 方法
Shape shape2 = ShapeFactory.getShape("RECTANGLE");
//調(diào)用 Rectangle 的 draw 方法
shape2.draw();
//獲取 Square 的對象,并調(diào)用它的 draw 方法
Shape shape3 = ShapeFactory.getShape("SQUARE");
//調(diào)用 Square 的 draw 方法
shape3.draw();
}
}
步驟 5
驗證輸出。
Inside Circle::draw() method.
Inside Rectangle::draw() method.
Inside Square::draw() method.
使用反射機(jī)制可以解決每次增加一個產(chǎn)品時,都需要增加一個對象實(shí)現(xiàn)工廠接口的缺點(diǎn)。注意要使用完整包名,也可以使用Properties文件做映射。
public class ShapeFactory {
public static Shape getShapeByClassName(String className) {
Shape obj = null;
try {
obj = (Shape)Class.forName(clazz.getName()).newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return obj;
}
}
2.工廠方法模式
介紹
意圖:定義了一個創(chuàng)建對象的接口,但由子類決定要實(shí)例化哪個類。工廠方法把實(shí)例化操作推遲到子類。
工廠方法和簡單工廠的區(qū)別:簡單工廠模式的實(shí)質(zhì)是由一個工廠類根據(jù)傳入的參數(shù),動態(tài)決定應(yīng)該創(chuàng)建哪一個產(chǎn)品類(這些產(chǎn)品類繼承自一個父類或接口)的實(shí)例。相當(dāng)于是一個工廠中有各種產(chǎn)品,這樣工廠的職責(zé)較重,而且當(dāng)產(chǎn)品類型過多時不利于系統(tǒng)的擴(kuò)展維護(hù)。如果增加一類產(chǎn)品,必須修改工廠類的方法。而工廠方法提供了一個工廠接口,定義了許多工廠實(shí)現(xiàn)類來負(fù)責(zé)生產(chǎn)不同的產(chǎn)品,這樣如果想要增加一類產(chǎn)品只需定義一個新的工廠實(shí)現(xiàn)類即可。且簡單工廠返回產(chǎn)品的方法為static的,因為簡單工廠不需要創(chuàng)建工廠實(shí)例,而工廠方法要創(chuàng)建工廠實(shí)例。
工廠模式的實(shí)際應(yīng)用:
JDBC中Connection的創(chuàng)建
特定的Driver創(chuàng)建對應(yīng)的connection。
Spring的Bean管理
實(shí)現(xiàn)
在簡單工廠中,創(chuàng)建對象的是另一個類,而在工廠方法中,是由子類來創(chuàng)建對象。
下圖中,F(xiàn)actory 有一個 doSomething() 方法,這個方法需要用到一個產(chǎn)品對象,這個產(chǎn)品對象由 factoryMethod() 方法創(chuàng)建。該方法是抽象的,需要由子類去實(shí)現(xiàn)。
Factory.java
public abstract class Factory {
abstract public Product factoryMethod();
public void doSomething() {
Product product = factoryMethod();
// do something with the product
}
}
ConcreteFactory.java
public class ConcreteFactory extends Factory {
public Product factoryMethod() {
return new ConcreteProduct();
}
}
BrickFactory.java
public class BrickFactory extends Factory {
public Product factoryMethod() {
return new Brick();
}
}
SteelFactory.java
public class SteelFactory extends Factory {
public Product factoryMethod() {
return new Steel();
}
}
3.抽象工廠模式
抽象工廠模式(Abstract Factory Pattern)是圍繞一個超級工廠創(chuàng)建其他工廠。該超級工廠又稱為其他工廠的工廠。這種類型的設(shè)計模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對象的最佳方式。
在抽象工廠模式中,接口是負(fù)責(zé)創(chuàng)建一個相關(guān)對象的工廠,不需要顯式指定它們的類。每個生成的工廠都能按照工廠模式提供對象。
抽象工廠模式是所有形態(tài)的工廠模式中最為抽象和最具一般性的一種形態(tài)。
抽象工廠模式創(chuàng)建的是對象家族,也就是很多對象而不是一個對象,并且這些對象是相關(guān)的,也就是說必須一起創(chuàng)建出來。而工廠方法模式只是用于創(chuàng)建一個對象,這和抽象工廠模式有很大不同。
為了方便引進(jìn)抽象工廠模式,引進(jìn)一個新概念:產(chǎn)品族(Product Family)。所謂產(chǎn)品族,是指位于不同產(chǎn)品等級結(jié)構(gòu),功能相關(guān)聯(lián)的產(chǎn)品組成的家族。如圖:
圖中一共有四個產(chǎn)品族,分布于三個不同的產(chǎn)品等級結(jié)構(gòu)中。同一個產(chǎn)品族是同一個工廠生產(chǎn)的,而不同等級結(jié)構(gòu)來自不同的工廠。只要指明一個產(chǎn)品所處的產(chǎn)品族以及它所屬的等級結(jié)構(gòu),就可以唯一的確定這個產(chǎn)品。
所謂的抽象工廠是指一個工廠等級結(jié)構(gòu)可以創(chuàng)建出分屬于不同產(chǎn)品等級結(jié)構(gòu)的一個產(chǎn)品族中的所有對象。如果用圖來描述的話,如下圖:
介紹
意圖:提供一個接口,用于創(chuàng)建相關(guān)的對象家族。
主要解決:主要解決接口選擇的問題。
何時使用:系統(tǒng)的產(chǎn)品有多于一個的產(chǎn)品族,而系統(tǒng)只消費(fèi)其中某一族的產(chǎn)品。
如何解決:在一個產(chǎn)品族里面,定義多個產(chǎn)品。
關(guān)鍵代碼:在一個工廠里聚合多個同類產(chǎn)品。
應(yīng)用實(shí)例:工作了,為了參加一些聚會,肯定有兩套或多套衣服吧,比如說有商務(wù)裝(成套,一系列具體產(chǎn)品)、時尚裝(成套,一系列具體產(chǎn)品),甚至對于一個家庭來說,可能有商務(wù)女裝、商務(wù)男裝、時尚女裝、時尚男裝,這些也都是成套的,即一系列具體產(chǎn)品。假設(shè)一種情況(現(xiàn)實(shí)中是不存在的,要不然,沒法進(jìn)入共產(chǎn)主義了,但有利于說明抽象工廠模式),在您的家中,某一個衣柜(具體工廠)只能存放某一種這樣的衣服(成套,一系列具體產(chǎn)品),每次拿這種成套的衣服時也自然要從這個衣柜中取出了。用 OO 的思想去理解,所有的衣柜(具體工廠)都是衣柜類的(抽象工廠)某一個,而每一件成套的衣服又包括具體的上衣(某一具體產(chǎn)品),褲子(某一具體產(chǎn)品),這些具體的上衣其實(shí)也都是上衣(抽象產(chǎn)品),具體的褲子也都是褲子(另一個抽象產(chǎn)品)。
優(yōu)點(diǎn):當(dāng)一個產(chǎn)品族中的多個對象被設(shè)計成一起工作時,它能保證客戶端始終只使用同一個產(chǎn)品族中的對象。
缺點(diǎn):產(chǎn)品族擴(kuò)展非常困難,要增加一個系列的某一產(chǎn)品,既要在抽象的 Creator 里加代碼,又要在具體的里面加代碼。
使用場景: 1、QQ 換皮膚,一整套一起換。 2、生成不同操作系統(tǒng)的程序。
注意事項:產(chǎn)品族難擴(kuò)展,產(chǎn)品等級易擴(kuò)展。
上圖的描述用產(chǎn)品族描述如下:
實(shí)現(xiàn)
我們來舉這樣一個例子,QQ秀有不同的裝扮,分為男孩和女孩,而男孩又分為圣誕男孩和新年男孩,女孩分為圣誕女孩和新年女孩。那么就可以有一個抽象工廠生產(chǎn)男孩和女孩。兩個具體的工廠分別生產(chǎn)圣誕系列的男孩和女孩、新年系列的男孩和女孩。同一系列的男孩和女孩是一個產(chǎn)品族,而不同系列構(gòu)成不同的產(chǎn)品等級。
步驟 1
為男孩創(chuàng)建一個接口。
Boy.java
public interface Boy {
public void drawMan();
}
步驟 2
創(chuàng)建實(shí)現(xiàn)接口的實(shí)體類。
MCBoy.java
public class MCBoy implements Boy {
@Override
public void drawMan() {
System.out.println("-----------------圣誕系列的男孩子--------------------");
}
}
HNBoy.java
public class HNBoy implements Boy {
@Override
public void drawMan() {
System.out.println("-----------------新年系列的男孩子--------------------");
}
}
步驟 3
為女孩創(chuàng)建一個接口
Girl.java
public interface Girl {
public void drawWomen();
}
步驟 4
創(chuàng)建實(shí)現(xiàn)接口的實(shí)體類。
MCGirl.java
public class MCGirl implements Girl {
@Override
public void drawWomen() {
System.out.println("-----------------圣誕系列的女孩子--------------------");
}
}
HNGirl.java
public class HNGirl implements Girl {
@Override
public void drawWomen() {
// TODO Auto-generated method stub
System.out.println("-----------------新年系列的女孩子--------------------");
}
}
步驟 5
創(chuàng)建生產(chǎn)男孩女孩的抽象工廠接口
PersonFactory.java
public interface PersonFactory {
//男孩接口
public Boy getBoy();
//女孩接口
public Girl getGirl();
}
步驟 6
創(chuàng)建生產(chǎn)圣誕和新年系列的具體工廠
MCFactory.java
public class MCFctory implements PersonFactory {
@Override
public Boy getBoy() {
return new MCBoy();
}
@Override
public Girl getGirl() {
return new MCGirl();
}
}
HNFactory.java
public class HNFactory implements PersonFactory {
@Override
public Boy getBoy() {
return new HNBoy();
}
@Override
public Girl getGirl() {
return new HNGirl();
}
}
步驟 7
使用工廠生產(chǎn)
AbstractFactoryPatternDemo.java
public class AbstractFactoryPatternDemo{
public static void main(String[] args){
MCFactory mcFactory = new MCFactory();
HNFactory hnFactory = new HNFactory();
Boy mcBoy = mcFactory.getBoy();
Girl mcGirl = mcFactory.getGirl();
Boy hnBoy = hnFactory.getBoy();
Girl hnGirl = hnFactory.getGirl();
mcBoy.drawMan();
mcGirl.drawWomen();
hnBoy.drawMan();
hnGirl.drawWomen();
}
}
步驟 8
驗證輸出
-----------------圣誕系列的男孩子--------------------
-----------------圣誕系列的女孩子--------------------
-----------------新年系列的男孩子--------------------
-----------------新年系列的女孩子--------------------
抽象工廠模式用到了工廠方法模式來創(chuàng)建單一對象,PersonFactory 中的 getBoy() 和 getGirl() 方法都是讓子類來實(shí)現(xiàn),這兩個方法單獨(dú)來看就是在創(chuàng)建一個對象,這符合工廠方法模式的定義。
至于創(chuàng)建對象的家族這一概念是在 AbstractFactoryPatternDemo 體現(xiàn), AbstractFactoryPatternDemo 要通過 PersonFactory 同時調(diào)用兩個方法來創(chuàng)建出兩個對象,在這里這兩個對象就有很大的相關(guān)性, AbstractFactoryPatternDemo 需要同時創(chuàng)建出這兩個對象。
從高層次來看,抽象工廠使用了組合,即 AbstractFactoryPatternDemo 組合了 PersonFactory,而工廠方法模式使用了繼承。
注意:抽象工廠也可以運(yùn)用反射,只不過反射的不再是產(chǎn)品類,而是不同的具體工廠類。
4.單例模式
單例模式(Singleton Pattern)是 Java 中最簡單的設(shè)計模式之一。這種類型的設(shè)計模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對象的最佳方式。
這種模式涉及到一個單一的類,該類負(fù)責(zé)創(chuàng)建自己的對象,同時確保只有單個對象被創(chuàng)建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實(shí)例化該類的對象。
注意:
1、單例類只能有一個實(shí)例。
2、單例類必須自己創(chuàng)建自己的唯一實(shí)例,而不是在外部隨意地new對象。
3、單例類必須給所有其他對象提供這一實(shí)例。
介紹
意圖:保證一個類僅有一個實(shí)例,并提供一個訪問它的全局訪問點(diǎn)。
主要解決:一個全局使用的類頻繁地創(chuàng)建與銷毀。
何時使用:當(dāng)想控制實(shí)例數(shù)目,節(jié)省系統(tǒng)資源的時候。
如何解決:判斷系統(tǒng)是否已經(jīng)有這個單例,如果有則返回,如果沒有則創(chuàng)建。
關(guān)鍵代碼:使用一個私有構(gòu)造函數(shù)、一個私有靜態(tài)變量以及一個公有靜態(tài)函數(shù)來實(shí)現(xiàn)。私有構(gòu)造函數(shù)保證了不能通過構(gòu)造函數(shù)來創(chuàng)建對象實(shí)例,只能通過公有靜態(tài)函數(shù)返回唯一的私有靜態(tài)變量。
應(yīng)用實(shí)例: 1、一個黨只能有一個主席。 2、Windows 是多進(jìn)程多線程的,在操作一個文件的時候,就不可避免地出現(xiàn)多個進(jìn)程或線程同時操作一個文件的現(xiàn)象,所以所有文件的處理必須通過唯一的實(shí)例來進(jìn)行。 3、一些設(shè)備管理器常常設(shè)計為單例模式,比如一個電腦有兩臺打印機(jī),在輸出的時候就要處理不能兩臺打印機(jī)打印同一個文件。
優(yōu)點(diǎn): 1、在內(nèi)存里只有一個實(shí)例,減少了內(nèi)存的開銷。2、避免頻繁的創(chuàng)建和銷毀實(shí)例,提高性能(比如管理學(xué)院首頁頁面緩存)。 2、避免對資源的多重占用(比如寫文件操作)。
缺點(diǎn):1、擴(kuò)展比較困難,沒有接口,不能繼承,與單一職責(zé)原則沖突,一個類應(yīng)該只關(guān)心內(nèi)部邏輯,而不關(guān)心外面怎么樣來實(shí)例化。2、如果實(shí)例化后的對象長期不利用,系統(tǒng)將默認(rèn)為垃圾進(jìn)行回收,造成對象狀態(tài)丟失。
使用場景: 1、當(dāng)多個實(shí)例存在可能引起程序邏輯錯誤,如要求生產(chǎn)唯一序列號。 2、對系統(tǒng)內(nèi)資源要求統(tǒng)一讀寫,如讀寫配置信息,又如WEB 中的計數(shù)器,不用每次刷新都在數(shù)據(jù)庫里加一次,用單例先緩存起來。 3、創(chuàng)建的一個對象需要消耗的資源過多,但同時又需要用到該對象,比如 I/O 與數(shù)據(jù)庫的連接等。
注意事項:getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多線程同時進(jìn)入造成 instance 被多次實(shí)例化。
實(shí)現(xiàn)
我們將創(chuàng)建一個 SingleObject 類。SingleObject 類有它的私有構(gòu)造函數(shù)和本身的一個靜態(tài)實(shí)例。
SingleObject 類提供了一個靜態(tài)方法,供外界獲取它的靜態(tài)實(shí)例。SingletonPatternDemo,我們的演示類使用 SingleObject 類來獲取 SingleObject 對象。
步驟 1
創(chuàng)建一個 Singleton 類。
SingleObject.java
public class SingleObject {
//創(chuàng)建 SingleObject 的一個對象
private static SingleObject instance = new SingleObject();
//讓構(gòu)造函數(shù)為 private,這樣該類就不會被實(shí)例化
private SingleObject(){}
//獲取唯一可用的對象
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
步驟 2
從 singleton 類獲取唯一的對象。
SingletonPatternDemo.java
public class SingletonPatternDemo {
public static void main(String[] args) {
//不合法的構(gòu)造函數(shù)
//編譯時錯誤:構(gòu)造函數(shù) SingleObject() 是不可見的
//SingleObject object = new SingleObject();
//獲取唯一可用的對象
SingleObject object = SingleObject.getInstance();
//顯示消息
object.showMessage();
}
}
步驟 3
驗證輸出。
Hello World!
單例模式的幾種實(shí)現(xiàn)方式
單例模式的實(shí)現(xiàn)有多種方式,如下所示:
1、懶漢式,線程不安全
是否 Lazy 初始化:是
是否多線程安全:否
實(shí)現(xiàn)難度:易
代碼實(shí)例:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) { /*懶漢式標(biāo)志:Lazy 初始化,
在外部第一次請求使用該類對象時才實(shí)例化,是時間換空間的模式*/
instance = new Singleton();
}
return instance;
}
}
描述:這種方式是最基本的實(shí)現(xiàn)方式,這種實(shí)現(xiàn)最大的問題就是不支持多線程。假設(shè)開始線程1進(jìn)入,判斷instance為空,在將要創(chuàng)建實(shí)例時,時間片切換,線程2又進(jìn)來了,同樣判斷instance為空,創(chuàng)建了實(shí)例,這是CPU調(diào)度回到線程1,繼續(xù)創(chuàng)建實(shí)例。因為沒有加鎖 synchronized,所以嚴(yán)格意義上它并不算單例模式。
這種方式 lazy loading 很明顯,不要求線程安全,在多線程不能正常工作。
接下來介紹的幾種實(shí)現(xiàn)方式都支持多線程,但是在性能上有所差異。
2、懶漢式,線程安全
是否 Lazy 初始化:是
是否多線程安全:是
實(shí)現(xiàn)難度:易
代碼實(shí)例:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
//加同步鎖
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
描述:這種方式具備很好的 lazy loading,能夠在多線程中很好的工作,但是,效率很低,99% 情況下不需要同步。
優(yōu)點(diǎn):第一次調(diào)用才初始化,避免內(nèi)存浪費(fèi)。
缺點(diǎn):必須加鎖 synchronized 才能保證單例,但加鎖會影響效率,下一個線程想要取得對象,必須等上一個線程釋放鎖之后,才可以繼續(xù)執(zhí)行。
getInstance() 的性能對應(yīng)用程序不是很關(guān)鍵(該方法使用不太頻繁)。
3、餓漢式
是否 Lazy 初始化:否
是否多線程安全:是
實(shí)現(xiàn)難度:易
代碼實(shí)例:
public class Singleton {
private static Singleton instance = new Singleton();
/*餓漢式標(biāo)志:在類加載時直接初始化,
是空間換時間的模式*/
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
描述:這種方式比較常用,但容易產(chǎn)生垃圾對象。
優(yōu)點(diǎn):沒有加鎖,執(zhí)行效率會提高。
缺點(diǎn):類加載時就初始化,浪費(fèi)內(nèi)存。
它基于 classloder 機(jī)制避免了多線程的同步問題,不過,instance 在類裝載時就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時候初始化 instance 顯然沒有達(dá)到 lazy loading 的效果。
4、靜態(tài)代碼塊
是否 Lazy 初始化:否
是否多線程安全:是
實(shí)現(xiàn)難度:易
代碼實(shí)例:
public class Singleton {
private static Singleton instance = null;
static{
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
描述:類似于餓漢式。
優(yōu)點(diǎn):沒有加鎖,執(zhí)行效率會提高。
缺點(diǎn):類加載時就初始化,浪費(fèi)內(nèi)存。
5、雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking)
JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多線程安全:是
實(shí)現(xiàn)難度:較復(fù)雜
代碼實(shí)例:
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
描述:這種方式采用雙鎖機(jī)制,安全且在多線程情況下能保持高性能。getInstance() 的性能對應(yīng)用程序很關(guān)鍵。這種方法既能保證線程安全又能提高了效率。
假設(shè)線程1進(jìn)入方法,instance為空,進(jìn)入同步代碼塊,時間片切換,線程2進(jìn)來,instance為空,在同步代碼塊外被阻塞,因為此時線程1正在里面。cup切換,線程1執(zhí)行創(chuàng)建實(shí)例,當(dāng)2再進(jìn)入代碼塊后,此時instace不為空,直接返回instance。當(dāng)再有線程進(jìn)來,instance不為空,不用執(zhí)行同步代碼塊,提高了效率。
外層 if 語句是為了保證 instance 不為空時不執(zhí)行同步代碼塊,直接返回對象,提高效率;里層 if 語句則是為了防止之前已經(jīng)進(jìn)入外層 if 語句的線程重復(fù)實(shí)例化對象,保證單例。
注意:singleton 采用 volatile 關(guān)鍵字修飾是很有必要的!
這里涉及到了JVM編譯器的指令重排。
簡單的一句 singleton = new Singleton(); 會被編譯器編譯成如下JVM指令:
memory =allocate(); //1:分配對象的內(nèi)存空間
ctorInstance(memory); //2:初始化對象
instance =memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
但是這些指令順序并非一成不變,有可能會經(jīng)過JVM和CPU的優(yōu)化,指令重排成下面的順序:
memory =allocate(); //1:分配對象的內(nèi)存空間
instance =memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
ctorInstance(memory); //2:初始化對象
當(dāng)線程A執(zhí)行完1,3,時,instance對象還未完成初始化,但已經(jīng)不再指向null。此時如果線程B搶占到CPU資源,執(zhí)行 if(instance == null)的結(jié)果會是false,從而返回一個沒有初始化完成的instance對象。如下圖所示:
使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環(huán)境下也能正常運(yùn)行。
6、登記式 / 靜態(tài)內(nèi)部類
是否 Lazy 初始化:是
是否多線程安全:是
實(shí)現(xiàn)難度:一般
代碼實(shí)例:
public class Singleton {
private Singleton (){}
private static class LazyHolder {
private static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
描述:這里有幾個需要注意的點(diǎn):
1、從外部無法訪問靜態(tài)內(nèi)部類LazyHolder,只有當(dāng)調(diào)用Singleton.getInstance()方法的時候,才能得到單例對象INSTANCE。
2、靜態(tài)內(nèi)部類LazyHolder是懶加載的,它不隨外部類Singleton的加載而加載。所以INSTANCE對象不會在單例類Singleton被加載的時候就初始化,而是在調(diào)用getInstance()方法時,靜態(tài)內(nèi)部類LazyHolder被加載的時候被初始化。因此這種實(shí)現(xiàn)方式是利用classloader的加載機(jī)制來實(shí)現(xiàn)懶加載,并保證構(gòu)建單例的線程安全。
7、序列化與反序列化
是否 Lazy 初始化:是
是否多線程安全:是
實(shí)現(xiàn)難度:一般
代碼實(shí)例:
MyObject.java
SaveAndRead.java
運(yùn)行結(jié)果
發(fā)現(xiàn)返回的不是同一個對象
去掉如下代碼的注釋
運(yùn)行結(jié)果
返回的是同一個對象
分析
靜態(tài)內(nèi)部類可以實(shí)現(xiàn)線程安全,但如果遇到序列化對象,使用默認(rèn)的方法運(yùn)行得到的結(jié)果還是多例的。如果既想要做到可序列化,又想要反序列化為同一對象,則必須實(shí)現(xiàn)readResolve方法。readResolve() 方法會緊挨著 readObject() 之后被調(diào)用,該方法的返回值將會代替原來反序列化的對象,而原來 readObject() 反序列化的對象將會被立即丟棄。
8、枚舉
JDK 版本:JDK1.5 起
是否 Lazy 初始化:否
是否多線程安全:是
實(shí)現(xiàn)難度:易
代碼實(shí)例:
public enum Singleton {
INSTANCE;
}
描述:單元素的枚舉類型是實(shí)現(xiàn)單例模式的最佳方法。它更簡潔,自動支持序列化機(jī)制,絕對防止多次實(shí)例化。
這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機(jī)制,該實(shí)現(xiàn)在多次序列化再進(jìn)行反序列化之后,不會得到多個實(shí)例。而其它實(shí)現(xiàn),為了保證不會出現(xiàn)反序列化之后出現(xiàn)多個實(shí)例,需要使用 transient 修飾所有字段,并且實(shí)現(xiàn)序列化和反序列化的方法,來防止反序列化重新創(chuàng)建新的對象。不過,由于 JDK1.5 之后才加入 enum 特性,用這種方式寫不免讓人感覺生疏,在實(shí)際工作中,也很少用。
該實(shí)現(xiàn)可以防止反射攻擊。在其它實(shí)現(xiàn)中,通過 setAccessible() 方法可以將私有構(gòu)造函數(shù)的訪問級別設(shè)置為 public,然后調(diào)用構(gòu)造函數(shù)從而實(shí)例化對象,如果要防止這種攻擊,需要在構(gòu)造函數(shù)中添加防止實(shí)例化第二個對象的代碼。但是該實(shí)現(xiàn)是由 JVM 保證只會實(shí)例化一次,因此不會出現(xiàn)上述的反射攻擊。
經(jīng)驗之談:一般情況下,不建議使用第 1 種和第 2 種懶漢方式,建議使用第 3 種餓漢方式。只有在要明確實(shí)現(xiàn) lazy loading 效果時,才會使用第 6 種登記方式。如果涉及到反序列化創(chuàng)建對象時,可以嘗試使用第 8 種枚舉方式。如果有其他特殊的需求,可以考慮使用第 5 種雙檢鎖方式。
5.建造者模式
建造者模式(Builder Pattern)使用多個簡單的對象一步一步構(gòu)建成一個復(fù)雜的對象。這種類型的設(shè)計模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對象的最佳方式。
一個 Builder 類會一步一步構(gòu)造最終的對象。該 Builder 類是獨(dú)立于其他對象的。
介紹
意圖:將一個復(fù)雜的構(gòu)建與其表示相分離,使得同樣的構(gòu)建過程可以創(chuàng)建不同的表示。
主要解決:主要解決在軟件系統(tǒng)中,有時候面臨著"一個復(fù)雜對象"的創(chuàng)建工作,其通常由各個部分的子對象用一定的算法構(gòu)成;由于需求的變化,這個復(fù)雜對象的各個部分經(jīng)常面臨著劇烈的變化,但是將它們組合在一起的算法卻相對穩(wěn)定。
何時使用:一些基本部件不會變,而其組合經(jīng)常變化的時候。
如何解決:將變與不變分離開。
關(guān)鍵代碼:建造者:創(chuàng)建和提供實(shí)例,導(dǎo)演:管理建造出來的實(shí)例的依賴關(guān)系。
應(yīng)用實(shí)例:
1、去肯德基,漢堡、可樂、薯條、炸雞翅等是不變的,而其組合是經(jīng)常變化的,生成出所謂的"套餐"。
2、JAVA 中的 StringBuilder。
優(yōu)點(diǎn):1、建造者獨(dú)立,易擴(kuò)展。 2、便于控制細(xì)節(jié)風(fēng)險。
缺點(diǎn): 1、產(chǎn)品必須有共同點(diǎn),范圍有限制。 2、如內(nèi)部變化復(fù)雜,會有很多的建造類。
使用場景: 1、需要生成的對象具有復(fù)雜的內(nèi)部結(jié)構(gòu)。 2、需要生成的對象內(nèi)部屬性本身相互依賴。
注意事項:與工廠模式的區(qū)別是:建造者模式更加關(guān)注與零件裝配的順序。
實(shí)現(xiàn)
我們假設(shè)一個快餐店的商業(yè)案例,其中,一個典型的套餐可以是一個漢堡(Burger)和一杯冷飲(Cold drink)。漢堡(Burger)可以是素食漢堡(Veg Burger)或雞肉漢堡(Chicken Burger),它們是包在紙盒中。冷飲(Cold drink)可以是可口可樂(coke)或百事可樂(pepsi),它們是裝在瓶子中。
我們將創(chuàng)建一個表示食物條目(比如漢堡和冷飲)的 Item 接口和實(shí)現(xiàn) Item 接口的實(shí)體類,以及一個表示食物包裝的 Packing 接口和實(shí)現(xiàn) Packing 接口的實(shí)體類,漢堡是包在紙盒中,冷飲是裝在瓶子中。
然后我們創(chuàng)建一個 Meal 類,帶有 Item 的 ArrayList 和一個通過結(jié)合 Item 來創(chuàng)建不同類型的 Meal 對象的 MealBuilder。BuilderPatternDemo,我們的演示類使用 MealBuilder 來創(chuàng)建一個 Meal。
步驟 1
創(chuàng)建一個表示食物條目和食物包裝的接口。
Item.java
public interface Item {
public String name();
public Packing packing();
public float price();
}
Packing.java
public interface Packing {
public String pack();
}
步驟 2
創(chuàng)建實(shí)現(xiàn) Packing 接口的實(shí)體類。
Wrapper.java
public class Wrapper implements Packing {
@Override
public String pack() {
return "Wrapper";
}
}
Bottle.java
public class Bottle implements Packing {
@Override
public String pack() {
return "Bottle";
}
}
步驟 3
創(chuàng)建實(shí)現(xiàn) Item 接口的抽象類,該類提供了默認(rèn)的功能。
Burger.java
public abstract class Burger implements Item {
@Override
public Packing packing() {
return new Wrapper();
}
@Override
public abstract float price();
}
ColdDrink.java
public abstract class ColdDrink implements Item {
@Override
public Packing packing() {
return new Bottle();
}
@Override
public abstract float price();
}
步驟 4
創(chuàng)建擴(kuò)展了 Burger 和 ColdDrink 的實(shí)體類。
VegBurger.java
public class VegBurger extends Burger {
@Override
public float price() {
return 25.0f;
}
@Override
public String name() {
return "Veg Burger";
}
}
ChickenBurger.java
public class ChickenBurger extends Burger {
@Override
public float price() {
return 50.5f;
}
@Override
public String name() {
return "Chicken Burger";
}
}
Coke.java
public class Coke extends ColdDrink {
@Override
public float price() {
return 30.0f;
}
@Override
public String name() {
return "Coke";
}
}
Pepsi.java
public class Pepsi extends ColdDrink {
@Override
public float price() {
return 35.0f;
}
@Override
public String name() {
return "Pepsi";
}
}
步驟 5
創(chuàng)建一個 Meal 類,帶有上面定義的 Item 對象。
Meal.java
import java.util.ArrayList;
import java.util.List;
public class Meal {
private List<Item> items = new ArrayList<Item>();
public void addItem(Item item){
items.add(item);
}
public float getCost(){
float cost = 0.0f;
for (Item item : items) {
cost += item.price();
}
return cost;
}
public void showItems(){
for (Item item : items) {
System.out.print("Item : "+item.name());
System.out.print(", Packing : "+item.packing().pack());
System.out.println(", Price : "+item.price());
}
}
}
步驟 6
創(chuàng)建一個 MealBuilder 類,實(shí)際的 builder 類負(fù)責(zé)創(chuàng)建 Meal 對象。
MealBuilder.java
public class MealBuilder {
public Meal prepareVegMeal (){
Meal meal = new Meal();
meal.addItem(new VegBurger());
meal.addItem(new Coke());
return meal;
}
public Meal prepareNonVegMeal (){
Meal meal = new Meal();
meal.addItem(new ChickenBurger());
meal.addItem(new Pepsi());
return meal;
}
}
步驟 7
BuiderPatternDemo 使用 MealBuider 來演示建造者模式(Builder Pattern)。
BuilderPatternDemo.java
public class BuilderPatternDemo {
public static void main(String[] args) {
MealBuilder mealBuilder = new MealBuilder();
Meal vegMeal = mealBuilder.prepareVegMeal();
System.out.println("Veg Meal");
vegMeal.showItems();
System.out.println("Total Cost: " +vegMeal.getCost());
Meal nonVegMeal = mealBuilder.prepareNonVegMeal();
System.out.println("\n\nNon-Veg Meal");
nonVegMeal.showItems();
System.out.println("Total Cost: " +nonVegMeal.getCost());
}
}
步驟 8
執(zhí)行程序,輸出結(jié)果:
Veg Meal
Item : Veg Burger, Packing : Wrapper, Price : 25.0
Item : Coke, Packing : Bottle, Price : 30.0
Total Cost: 55.0
Non-Veg Meal
Item : Chicken Burger, Packing : Wrapper, Price : 50.5
Item : Pepsi, Packing : Bottle, Price : 35.0
Total Cost: 85.5
6.原型模式
原型模式(Prototype Pattern)是用于創(chuàng)建重復(fù)的對象,同時又能保證性能。這種類型的設(shè)計模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對象的最佳方式。
這種模式是實(shí)現(xiàn)了一個原型接口,該接口用于創(chuàng)建當(dāng)前對象的克隆。當(dāng)直接創(chuàng)建對象的代價比較大時,則采用這種模式。例如,一個對象需要在一個高代價的數(shù)據(jù)庫操作之后被創(chuàng)建。我們可以緩存該對象,在下一個請求時返回它的克隆,在需要的時候更新數(shù)據(jù)庫,以此來減少數(shù)據(jù)庫調(diào)用。
介紹
意圖:用原型實(shí)例指定創(chuàng)建對象的種類,并且通過拷貝這些原型創(chuàng)建新的對象。
主要解決:在運(yùn)行期建立和刪除原型。
何時使用: 1、當(dāng)一個系統(tǒng)應(yīng)該獨(dú)立于它的產(chǎn)品創(chuàng)建,構(gòu)成和表示時。 2、當(dāng)要實(shí)例化的類是在運(yùn)行時刻指定時,例如,通過動態(tài)裝載。 3、為了避免創(chuàng)建一個與產(chǎn)品類層次平行的工廠類層次時。 4、當(dāng)一個類的實(shí)例只能有幾個不同狀態(tài)組合中的一種時。建立相應(yīng)數(shù)目的原型并克隆它們可能比每次用合適的狀態(tài)手工實(shí)例化該類更方便一些。
如何解決:利用已有的一個原型對象,快速地生成和原型對象一樣的實(shí)例。
關(guān)鍵代碼: 1、實(shí)現(xiàn)克隆操作,繼承 Cloneable,重寫 clone() 2、原型模式同樣用于隔離類對象的使用者和具體類型(易變類)之間的耦合關(guān)系,它同樣要求這些"易變類"擁有穩(wěn)定的接口。
應(yīng)用實(shí)例: 1、細(xì)胞分裂。 2、JAVA 中的 Object clone() 方法。
優(yōu)點(diǎn):1、性能提高。 2、逃避構(gòu)造函數(shù)的約束。
缺點(diǎn):1、配備克隆方法需要對類的功能進(jìn)行通盤考慮,這對于全新的類不是很難,但對于已有的類不一定很容易,特別當(dāng)一個類引用不支持串行化的間接對象,或者引用含有循環(huán)結(jié)構(gòu)的時候。 2、必須實(shí)現(xiàn) Cloneable 接口。
使用場景:1、資源優(yōu)化場景。 2、類初始化需要消化非常多的資源,這個資源包括數(shù)據(jù)、硬件資源等。 3、性能和安全要求的場景。 4、通過 new 產(chǎn)生一個對象需要非常繁瑣的數(shù)據(jù)準(zhǔn)備或訪問權(quán)限,則可以使用原型模式。 5、一個對象多個修改者的場景。 6、一個對象需要提供給其他對象訪問,而且各個調(diào)用者可能都需要修改其值時,可以考慮使用原型模式拷貝多個對象供調(diào)用者使用。 7、在實(shí)際項目中,原型模式很少單獨(dú)出現(xiàn),一般是和工廠方法模式一起出現(xiàn),通過 clone 的方法創(chuàng)建一個對象,然后由工廠方法提供給調(diào)用者。原型模式已經(jīng)與 Java 融為渾然一體,大家可以隨手拿來使用。
注意事項:與通過對一個類進(jìn)行實(shí)例化來構(gòu)造新對象不同的是,原型模式是通過拷貝一個現(xiàn)有對象生成新對象的。淺拷貝實(shí)現(xiàn) Cloneable,重寫,深拷貝是通過實(shí)現(xiàn) Serializable 讀取二進(jìn)制流。
實(shí)現(xiàn)
我們將創(chuàng)建一個抽象類 Shape 和擴(kuò)展了 Shape 類的實(shí)體類。下一步是定義類 ShapeCache,該類把 shape 對象存儲在一個 Hashtable 中,并在請求的時候返回它們的克隆。
PrototypePatternDemo,我們的演示類使用 ShapeCache 類來獲取 Shape 對象。
步驟 1
創(chuàng)建一個實(shí)現(xiàn)了 Clonable 接口的抽象類。
Shape.java
public abstract class Shape implements Cloneable {
private String id;
protected String type;
abstract void draw();
public String getType(){
return type;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
步驟 2
創(chuàng)建擴(kuò)展了上面抽象類的實(shí)體類。
Rectangle.java
public class Rectangle extends Shape {
public Rectangle(){
type = "Rectangle";
}
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
Square.java
public class Square extends Shape {
public Square(){
type = "Square";
}
@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}
Circle.java
public class Circle extends Shape {
public Circle(){
type = "Circle";
}
@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
步驟 3
創(chuàng)建一個類,從數(shù)據(jù)庫獲取實(shí)體類,并把它們存儲在一個 Hashtable 中。
ShapeCache.java
import java.util.Hashtable;
public class ShapeCache {
private static Hashtable<String, Shape> shapeMap
= new Hashtable<String, Shape>();
public static Shape getShape(String shapeId) {
Shape cachedShape = shapeMap.get(shapeId);
return (Shape) cachedShape.clone();
}
// 對每種形狀都運(yùn)行數(shù)據(jù)庫查詢,并創(chuàng)建該形狀
// shapeMap.put(shapeKey, shape);
// 例如,我們要添加三種形狀
public static void loadCache() {
Circle circle = new Circle();
circle.setId("1");
shapeMap.put(circle.getId(),circle);
Square square = new Square();
square.setId("2");
shapeMap.put(square.getId(),square);
Rectangle rectangle = new Rectangle();
rectangle.setId("3");
shapeMap.put(rectangle.getId(),rectangle);
}
}
步驟 4
PrototypePatternDemo 使用 ShapeCache 類來獲取存儲在 Hashtable 中的形狀的克隆。
PrototypePatternDemo.java
public class PrototypePatternDemo {
public static void main(String[] args) {
ShapeCache.loadCache();
Shape clonedShape = (Shape) ShapeCache.getShape("1");
System.out.println("Shape : " + clonedShape.getType());
Shape clonedShape2 = (Shape) ShapeCache.getShape("2");
System.out.println("Shape : " + clonedShape2.getType());
Shape clonedShape3 = (Shape) ShapeCache.getShape("3");
System.out.println("Shape : " + clonedShape3.getType());
}
}
步驟 5
執(zhí)行程序,輸出結(jié)果:
Shape : Circle
Shape : Square
Shape : Rectangle