核心Java中的創建設計模式
- java
1.簡介
設計模式是我們在編寫軟件時使用的常見模式。它們代表了隨著時間的發展而建立的最佳實踐。這些可以幫助我們確保代碼的設計和構建良好。
創建模式是專注於我們如何獲得對象實例的設計模式。通常,這意味著我們如何構造一個類的新實例,但是在某些情況下,這意味著獲得一個已經構造好的實例供我們使用。
在本文中,我們將重新介紹一些常見的創建設計模式。我們將看到它們的外觀以及在JVM或其他核心庫中的位置。
2.工廠方法
Factory Method模式是我們將實例的構造與正在構造的類分開的一種方式。這樣一來,我們就可以抽像出確切的類型,從而使我們的客戶端代碼可以改用接口或抽像類來工作:
class SomeImplementation implements SomeInterface {
// ...
}
public class SomeInterfaceFactory {
public SomeInterface newInstance() {
return new SomeImplementation();
}
}
在這裡,我們的客戶代碼永遠不需要了解SomeImplementation
,而是根據SomeInterface
。儘管如此,我們甚至可以更改從工廠返回的類型,而無需更改客戶端代碼。這甚至可以包括在運行時動態選擇類型。
2.1 JVM中的示例
JVM的這種模式中最著名的示例可能是Collections
類上的集合構建方法,例如singleton()
, singletonList()
和singletonMap().
所有這些都返回適當集合( Set
, List
或Map –
實例,但是確切的類型無關緊要。此外, Stream.of()
方法和新的Set.of()
, List.of()
和Map.ofEntries()
方法使我們可以對較大的集合進行相同的處理。
還有很多其他示例,包括Charset.forName()
ResourceBundle.getBundle()
,分別根據請求的名稱返回不同的Charset
類實例,ResourceBundle.getBundle()會根據所需的名稱加載其他資源在提供的名稱上。
並非所有這些都需要提供不同的實例。有些只是用來隱藏內部工作的抽象。例如, Calendar.getInstance()
和NumberFormat.getInstance()
始終返回相同的實例,但是確切的詳細信息與客戶端代碼無關。
3.抽象工廠
抽象工廠模式是超越此步驟的一步,此處使用的工廠也具有抽象基類型。然後,我們可以根據這些抽像類型編寫代碼,並在運行時以某種方式選擇具體的工廠實例。
首先,我們有一個接口和一些實際要使用的功能的具體實現:
interface FileSystem {
// ...
}
class LocalFileSystem implements FileSystem {
// ...
}
class NetworkFileSystem implements FileSystem {
// ...
}
接下來,我們為工廠提供了一個接口和一些具體實現以獲取上述信息:
interface FileSystemFactory {
FileSystem newInstance();
}
class LocalFileSystemFactory implements FileSystemFactory {
// ...
}
class NetworkFileSystemFactory implements FileSystemFactory {
// ...
}
然後,我們還有另一種工廠方法來獲取抽象工廠,通過該方法我們可以獲取實際實例:
class Example {
static FileSystemFactory getFactory(String fs) {
FileSystemFactory factory;
if ("local".equals(fs)) {
factory = new LocalFileSystemFactory();
else if ("network".equals(fs)) {
factory = new NetworkFileSystemFactory();
}
return factory;
}
}
在這裡,我們有一個FileSystemFactory
接口,它具有兩個具體的實現。我們在運行時選擇了確切的實現,但是使用該實現的代碼無需關心實際使用了哪個實例。然後,它們每個都返回FileSystem
接口的一個不同的具體實例,但是同樣,我們的代碼不需要精確地關心我們擁有該實例的哪個實例。
通常,如上所述,我們使用另一種工廠方法來獲得工廠本身。在這裡的示例中, getFactory()
方法本身就是一個工廠方法,該方法返回一個抽象FileSystemFactory
,然後將其用於構造FileSystem
。
3.1 JVM中的示例
整個JVM中都有很多使用這種設計模式的示例。最常見的是XML包周圍的DocumentBuilderFactory
, TransformerFactory,
和XPathFactory
。這些都具有特殊的newInstance()
工廠方法,以使我們的代碼可以獲得抽象工廠的實例。
在內部,此方法使用多種不同的機制(系統屬性,JVM中的配置文件以及服務提供者接口)來嘗試並準確確定要使用的具體實例。然後,這允許我們根據需要在應用程序中安裝替代XML庫,但這對於實際使用它們的任何代碼都是透明的。
一旦我們的代碼調用了newInstance()
方法,它將從相應的XML庫中獲得一個工廠實例。然後,該工廠從同一庫構造我們要使用的實際類。
例如,如果我們使用JVM默認的Xerces實現, com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl
的實例,但是如果我們想使用其他實現,則調用newInstance()
將透明地返回它。
4.Builder模式
當我們想以更靈活的方式構造複雜的對象時,Builder模式非常有用。它通過擁有一個單獨的類來工作,該類用於構建複雜的對象,並允許客戶端使用更簡單的接口來創建它:
class CarBuilder {
private String make = "Ford";
private String model = "Fiesta";
private int doors = 4;
private String color = "White";
public Car build() {
return new Car(make, model, doors, color);
}
}
這使我們可以分別提供make
, model
, doors
和color
,然後在構建Car
,所有構造函數參數都將解析為存儲的值。
4.1 JVM中的示例
JVM中有一些非常關鍵的示例。 **StringBuilder
和StringBuffer
類是允許我們通過提供許多小部分String
**的構建器。最新的Stream.Builder
類使我們可以完全相同地構造一個Stream
:
Stream.Builder<Integer> builder = Stream.builder<Integer>();
builder.add(1);
builder.add(2);
if (condition) {
builder.add(3);
builder.add(4);
}
builder.add(5);
Stream<Integer> stream = builder.build();
5.延遲初始化
我們使用惰性初始化模式將某些值的計算推遲到需要時才進行。有時,這可能涉及單個數據,有時,這可能意味著整個對象。
在許多情況下這很有用。例如,如果完全構造一個對象需要數據庫或網絡訪問權限,而我們可能永遠不需要使用它,那麼執行這些調用可能會導致我們的應用程序運行不佳。另外,如果我們正在計算大量可能不需要的值,那麼這可能會導致不必要的內存使用。
通常,通過使一個對象成為所需數據的惰性包裝,並通過getter方法訪問數據進行計算,可以實現此目的:
class LazyPi {
private Supplier<Double> calculator;
private Double value;
public synchronized Double getValue() {
if (value == null) {
value = calculator.get();
}
return value;
}
}
計算pi是一項昂貴的操作,我們可能不需要執行。上面將在我們第一次調用getValue()
而不是在此之前。
5.1。 JVM中的示例
JVM中的此類示例相對較少。但是,Java 8中引入的Streams API是一個很好的例子。在流上執行的所有操作都是延遲的,因此我們可以在此處執行昂貴的計算,並且知道僅在需要時才調用它們。
但是,流本身的實際生成也可能是延遲的。 Stream.generate()
會在需要下一個值時調用一個函數,並且僅在需要時才調用。我們可以使用它來加載昂貴的值(例如,通過進行HTTP API調用),並且僅在實際需要新元素時才支付費用:
Stream.generate(new BaeldungArticlesLoader())
.filter(article -> article.getTags().contains("java-streams"))
.map(article -> article.getTitle())
.findFirst();
在這裡,我們有一個Supplier
,它將進行HTTP調用以加載文章,根據關聯的標籤對其進行過濾,然後返回第一個匹配的標題。如果加載的第一篇文章與該過濾器匹配,則無論實際存在多少篇文章,都只需要進行一次網絡調用。
6.對像池
在構造對象的新實例(創建起來可能會很昂貴)時,我們將使用對像池模式,但是重新使用現有實例是可以接受的選擇。不必每次都構造一個新實例,而是可以預先構造一組這些實例,然後根據需要使用它們。
存在實際的對像池來管理這些共享對象。它還會跟踪它們,以便每個人只能同時在一個地方使用。在某些情況下,整個對象集僅在開始時構造。在其他情況下,必要時,池可以按需創建新實例
6.1。 JVM中的示例
JVM中這種模式的主要示例是線程池的使用。 ExecutorService
將管理一組線程,並允許我們在需要在一個線程上執行任務時使用它們。使用此方法意味著我們在需要產生異步任務時就無需創建新線程,也無需花費所有的成本:
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.execute(new SomeTask()); // Runs on a thread from the pool
pool.execute(new AnotherTask()); // Runs on a thread from the pool
這兩個任務從線程池中分配了一個要在其上運行的線程。它可能是同一線程,也可能是完全不同的線程,使用哪個線程與我們的代碼無關緊要。
7.原型
當我們需要創建與原始對象相同的對象的新實例時,可以使用Prototype模式。原始實例充當我們的原型,並被用來構造新的實例,然後這些實例完全獨立於原始實例。然後我們可以使用這些,但是這是必要的。
Cloneable
標記接口,然後使用Object.clone()
來對此提供一定程度的支持。這將產生對象的淺表克隆,創建一個新實例,然後直接複製字段。
這比較便宜,但缺點是對象內部結構化的任何字段都將是同一實例。因此,這意味著對這些字段的更改也會在所有實例中發生。但是,如有必要,我們總是可以自己重寫此方法:
public class Prototype implements Cloneable {
private Map<String, String> contents = new HashMap<>();
public void setValue(String key, String value) {
// ...
}
public String getValue(String key) {
// ...
}
@Override
public Prototype clone() {
Prototype result = new Prototype();
this.contents.entrySet().forEach(entry -> result.setValue(entry.getKey(), entry.getValue()));
return result;
}
}
7.1 JVM中的示例
JVM有一些這樣的示例。 Cloneable
接口的類,我們可以看到這些內容。例如, PKIXCertPathBuilderResult
, PKIXBuilderParameters
, PKIXParameters
, PKIXCertPathBuilderResult
和PKIXCertPathValidatorResult
都是可Cloneable.
另一個示例是java.util.Date
類。值得注意的是,這將覆蓋Object.
clone()
方法也可以跨其他瞬態字段進行複制。
8.Singleton模式
當我們擁有一個只應有一個實例的類,並且應該在整個應用程序中都可以訪問該實例時,通常使用Singleton模式。通常,我們通過一個靜態實例來管理此問題,該實例可以通過靜態方法訪問:
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
根據確切的需求,此方法有多種變體-例如,實例是在啟動時創建還是在首次使用時創建,訪問該實例是否必須是線程安全的,以及每個線程是否需要一個不同的實例。
8.1 JVM中的示例
JVM帶有一些這樣的示例,這些類具有代表JVM自身核心部分的類Runtime, Desktop,
和SecurityManager
。這些都具有訪問器方法,這些方法返回各自類的單個實例。
此外,許多Java Reflection API都可用於singleton實例。無論使用Class.forName()
, String.class
還是通過其他反射方法Class,
相同的實際類始終會返回Class的相同實例。
以類似的方式,我們可以將代表當前線程Thread
通常會有很多這樣的實例,但是根據定義,每個線程只有一個實例。從在同一線程中執行的任何地方調用Thread.currentThread()
9.總結
在本文中,我們研究了用於創建和獲取對象實例的各種不同的設計模式。我們還查看了核心JVM中使用的這些模式的示例,因此我們可以看到它們已經以許多應用程序已經從中受益的方式使用。