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中使用的这些模式的示例,因此我们可以看到它们已经以许多应用程序已经从中受益的方式使用。
0 评论