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()
:根据名称加载不同的资源文件。
并不是所有工厂方法都需要返回不同类型的实例。有些只是为了封装内部细节。例如:
Calendar.getInstance()
NumberFormat.getInstance()
这两个方法每次都返回相同实例,但客户端并不关心具体细节。
3. 抽象工厂(Abstract Factory)
抽象工厂 模式更进一步,它不仅将对象的创建抽象化,还把工厂本身也抽象出来。我们可以通过抽象工厂接口来编写代码,并在运行时选择具体的工厂实现。
首先定义接口和具体实现:
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()
就是一个工厂方法,返回抽象工厂,再由该工厂创建实际对象。
3.1. JVM 中的应用示例
JVM 中大量使用了这种设计模式。最常见的例子是 XML 处理相关的类:
DocumentBuilderFactory
TransformerFactory
XPathFactory
这些类都有一个特殊的 newInstance()
方法用于获取抽象工厂实例。
这个方法内部会尝试多种机制来决定使用哪个具体实现:
- 系统属性
- JVM 配置文件
- SPI(服务提供者接口)
这使得我们可以替换默认的 XML 实现库,而对使用方透明。
比如,默认情况下你会得到 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);
}
}
这种方式允许客户端逐个设置属性,最终通过 build()
方法生成完整的对象。
4.1. JVM 中的应用示例
JVM 中有很多建造者模式的经典应用:
✅ StringBuilder
和 StringBuffer
:用来构建长字符串。
✅ 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. 懒加载(Lazy Initialization)
懒加载模式用于延迟计算某些值,直到真正需要时才执行。它可以应用于单个数据项,也可以用于整个对象。
常见于以下场景:
❌ 构造对象需要访问数据库或网络,但可能永远用不到。 ❌ 计算大量可能不会被使用的结果,浪费内存。
典型实现方式是使用一个包装对象,在首次调用 getter 时才计算并缓存结果:
class LazyPi {
private Supplier<Double> calculator;
private Double value;
public synchronized Double getValue() {
if (value == null) {
value = calculator.get();
}
return value;
}
}
在这个例子中,计算 π 是一个昂贵的操作,只在第一次调用 getValue()
时才会发生。
5.1. JVM 中的应用示例
虽然 JVM 中直接使用懒加载的例子不多,但 Java 8 引入的 Streams API 是个很好的例子。
✅ 所有流操作都是懒加载的,只有在终端操作触发时才会真正执行。
例如:
Stream.generate(new BaeldungArticlesLoader())
.filter(article -> article.getTags().contains("java-streams"))
.map(article -> article.getTitle())
.findFirst();
这段代码中,generate()
只会在需要下一个元素时才会调用 Supplier
,从而避免不必要的网络请求。
6. 对象池(Object Pool)
当我们频繁创建代价高昂的对象时,可以考虑使用对象池模式。与其每次都新建对象,不如预先创建一批对象放入池中,按需复用。
对象池的作用就是管理这些共享对象,确保同一时间每个对象只被一个地方使用。
6.1. JVM 中的应用示例
✅ 最典型的例子就是线程池。
使用 ExecutorService
可以管理一组线程,避免每次异步任务都要新建线程带来的开销:
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.execute(new SomeTask()); // 使用池中的线程
pool.execute(new AnotherTask()); // 再次使用池中的线程
两个任务可能会使用相同的线程,也可能使用不同的线程,但客户端无需关心。
7. 原型模式(Prototype)
原型模式用于创建与现有对象完全相同的新实例。原始对象作为“原型”,通过克隆生成新的副本。
Java 提供了部分支持:实现 Cloneable
接口并重写 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;
}
}
⚠️ 注意:Object.clone()
默认是浅拷贝,如果字段是引用类型,则两个对象会共享同一个引用。必要时需手动深拷贝。
7.1. JVM 中的应用示例
JVM 中有不少类实现了 Cloneable
接口,如:
PKIXCertPathBuilderResult
PKIXBuilderParameters
PKIXParameters
PKIXCertPathBuilderResult
PKIXCertPathValidatorResult
还有一个经典例子是 java.util.Date
类,它重写了 clone()
方法,连同 transient 字段一起复制。
8. 单例模式(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.getRuntime()
Desktop.getDesktop()
SecurityManager.getSecurityManager()
另外,Java 反射 API 中的 Class
实例也是单例。无论你通过 Class.forName()
、String.class
还是其他方式获取,同一个类始终返回相同的 Class
实例。
类似地,当前线程的 Thread.currentThread()
返回的也是当前线程唯一的 Thread
实例。
9. 总结
本文介绍了多种创建型设计模式,并展示了它们在 JVM 核心库中的实际应用。理解这些模式不仅能提升代码质量,还能让我们更好地利用已有框架和工具。