单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供全局访问点。
单例模式类图:
单例模式有多种实现方式,下面我们详细介绍几种常见的实现方式:
1. 懒汉式(Lazy Initialization)
懒汉式单例模式(Lazy Singleton)是一种延迟实例化的方式,即在首次使用该类时才会创建实例。
代码示例(线程不安全):
public class Singleton {
private static Singleton instance;
// 私有构造函数,避免外部实例化
private Singleton() {}
// 获取实例的方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:
- 延迟实例化,直到第一次使用时才创建实例。
- 节省内存,如果没有使用这个单例,实例不会被创建。
缺点:
- 在多线程环境下,可能会出现多个线程同时进入
if (instance == null)
语句块,导致创建多个实例。需要考虑线程安全问题。
2. 线程安全的懒汉式(双重检查锁定,Double-Checked Locking)
为了避免线程不安全问题,懒汉式可以通过双重检查锁定来保证线程安全。
代码示例:
public class Singleton {
private static volatile Singleton instance;
// 私有构造函数,避免外部实例化
private Singleton() {}
// 获取实例的方法
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
关键点:
- 双重检查:第一次检查
instance == null
用来减少不必要的同步开销;第二次检查是在同步块内,保证在实例创建时只有一个线程可以创建。 volatile
关键字:确保多线程环境下对instance
的访问是可见的,并且防止由于 JIT 编译器优化等原因造成的问题。
优点:
- 保证线程安全,并且提高了性能。只有第一次实例化时需要同步,后续调用不再需要加锁。
缺点:
- 代码比较复杂,理解起来需要更多的思考。
3. 饿汉式(Eager Initialization)
饿汉式单例模式在类加载时就创建实例,这种方式不需要考虑线程安全问题,因为实例在类加载时就已经被创建。
代码示例:
public class Singleton {
// 静态初始化时就创建实例
private static final Singleton instance = new Singleton();
// 私有构造函数,避免外部实例化
private Singleton() {}
// 获取实例的方法
public static Singleton getInstance() {
return instance;
}
}
优点:
- 简单,容易理解。
- 线程安全:类加载时已经完成初始化,且
instance
是静态常量,JVM 保证线程安全。
缺点:
- 浪费内存:即使实例可能永远不被使用,类加载时实例就会创建。
- 不支持延迟初始化:如果类加载时并不需要这个单例对象,就会造成不必要的内存开销。
4. 静态内部类式(Bill Pugh Singleton)
静态内部类式单例模式是一种懒加载的单例实现方式,它结合了懒汉式和饿汉式的优点。使用静态内部类时,实例化的过程是延迟的,但又能够避免线程安全问题。
代码示例:
public class Singleton {
// 静态内部类,它在第一次使用时被加载
private static class SingletonHelper {
private static final Singleton INSTANCE = new Singleton();
}
// 私有构造函数,避免外部实例化
private Singleton() {}
// 获取实例的方法
public static Singleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
关键点:
- 静态内部类:
SingletonHelper
是一个静态内部类,它只有在getInstance()
被调用时才会被加载,因此实现了懒加载。 - 线程安全:
SingletonHelper.INSTANCE
只会在类加载时初始化一次,而且类加载是线程安全的,因此不需要显式的同步控制。
优点:
- 线程安全。
- 延迟加载:实例会在第一次使用时才会被创建。
- 相对于双重检查锁定,代码更加简洁。
- 不会造成内存浪费,因为静态内部类只有在需要时才会被加载。
缺点:
- 无明显缺点,适用于绝大多数场景。
5. 枚举式(Enum Singleton)
枚举式单例模式是最简单、最安全的实现方式。Java 的枚举类型天然就是单例的,JVM 保证枚举实例的创建是线程安全的,而且枚举不允许被反射破坏单例。
代码示例:
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("Singleton with Enum");
}
}
优点:
- 简洁:单例实例是由
INSTANCE
常量代表的,代码非常简洁。 - 线程安全:Java 枚举类型在类加载时保证了线程安全。
- 防止反射攻击:枚举类型无法通过反射进行实例化,因此避免了反射破解单例的风险。
- 防止序列化破坏:枚举本身能够防止序列化导致的实例重复创建。
缺点:
- 不常见的实现方式,对于不了解枚举的开发者可能会产生困惑。
对比
特性 | 懒汉式(Lazy Initialization) | 线程安全的懒汉式(双重检查锁定) | 饿汉式(Eager Initialization) | 静态内部类式(Bill Pugh Singleton) | 枚举式(Enum Singleton) |
---|---|---|---|---|---|
实例化时机 | 延迟实例化,首次调用时才创建 | 延迟实例化,首次调用时才创建,但保证线程安全 | 类加载时就创建实例 | 延迟实例化,静态内部类加载时创建 | 类加载时创建(JVM保证线程安全和单例性) |
线程安全 | 否,可能出现并发问题(需要手动同步) | 是,使用双重检查锁定(synchronized )保证线程安全 | 是,由于类加载时创建,因此天然线程安全 | 是,JVM保证静态内部类加载时线程安全 | 是,JVM保证枚举类型线程安全和单例性 |
性能 | 较低(每次访问都需要判断 null ) | 较高(只有第一次需要同步) | 较高(不需要同步) | 较高(避免了每次判断 null ) | 最高,JVM直接管理,几乎无性能损耗 |
实现复杂度 | 简单,易于理解 | 稍复杂,需要额外的同步机制 | 简单,直接实现 | 稍复杂,涉及到静态内部类 | 非常简单,直接使用枚举实现 |
内存消耗 | 可能浪费内存(在未使用时实例仍然存在) | 不浪费内存,只在需要时实例化 | 可能浪费内存(实例化时就创建,不管是否使用) | 不浪费内存,实例化仅在第一次使用时创建 | 不浪费内存,枚举实例只在类加载时创建 |
防止反射攻击 | 否,反射可以创建多个实例 | 否,反射可以创建多个实例 | 否,反射可以创建多个实例 | 是,反射无法破坏单例 | 是,枚举类的反射破坏会抛出异常 |
防止序列化破坏 | 否,需要手动处理 | 否,需要手动处理 | 否,需要手动处理 | 是,JVM管理序列化,确保不会破坏单例 | 是,JVM保证序列化时保持唯一性 |
推荐场景 | 简单场景,单线程应用 | 线程安全需求较高,但性能要求不极端的场景 | 单线程应用或对性能要求较高的场景 | 需要延迟加载并且希望避免同步开销的场景 | 高度推荐,适用于大多数单例场景 |
常见问题 | 可能出现并发问题,需要同步 | 代码较复杂,性能相对稍差 | 不支持延迟加载,类加载时会立即初始化 | 稍复杂,理解需要一定的知识 | 极为简洁,但不适用于需要多个实例化参数的情况 |
总结
- 懒汉式适用于需要延迟实例化的场景,但需要注意线程安全问题。
- 双重检查锁定懒汉式是懒汉式的改进,能够保证线程安全并提高性能。
- 饿汉式不需要担心线程安全问题,但存在内存浪费的风险。
- 静态内部类式是较为推荐的懒加载方式,线程安全且高效。
- 枚举式是最简洁、最安全的实现方式,JVM保证线程安全和防止反射攻击,是最推荐的实现方式。
工厂模式
简单工厂模式
简单工厂模式(也称为静态工厂方法模式)是创建型设计模式之一,它提供了一个类来负责创建实例化对象的工作,客户端只需要传入相关的参数,而不需要关心对象的创建过程。
- Factory:工厂类,这是简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
- IProduct:抽象产品类,这是简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
- Product:具体产品类,这是简单工厂模式的创建目标。
简单实现
这里我们用生产计算机来举例,假设有一个计算机的代工生产商,它目前已经可以代工生产联想计算机了。随着业务的拓展,这个代工生产商还要生产惠普和华硕的计算机。这样我们就需要用一个单独的类来专门生产计算机,这就用到了简单工厂模式。下面我们来实现简单工厂模式。
(1)抽象产品类
public abstract class Computer {
public abstract void start();
}
(2)具体产品类
我们创建多个品牌的计算机,都继承自己父类Computer,并且实现父类的start()方法:
public class LenovoComputer extends Computer{
@Override
public void start() {
System.out.println("联想计算机");
}
}
public class HpComputer extends Computer{
@Override
public void start() {
System.out.println("惠普计算机");
}
}
public class AsusComputer extends Computer{
@Override
public void start() {
System.out.println("华硕计算机");
}
}
(3)工厂类
下来创建一个工厂类,提供一个静态方法createComputer去生产计算机:
public class ComputerFactory {
public static Computer createComputer(String type){
Computer mComputer = null;
switch (type) {
case "lenovo":
mComputer = new LenovoComputer();
break;
case "hp":
mComputer = new HpComputer();
break;
case "asus":
mComputer = new HpComputer();
break;
}
return mComputer;
}
}
(4)客户端调用工厂类
public class CreatComputer {
public static void main(String[] args) {
ComputerFactory.createComputer("hp").start();
}
}
优缺点
优点:
- 客户端与产品解耦:客户端代码不需要知道具体的产品类,只需要知道如何调用工厂来获取产品。
- 易于扩展:如果要添加新的产品类型,只需要在工厂类中增加新的
if-else
分支或修改创建产品的逻辑即可,客户端无需修改。 - 集中管理产品创建:产品的创建逻辑都集中在工厂类中,便于管理和修改。
缺点:
- 违反开闭原则:每当需要增加新的产品类型时,都需要修改工厂类的
createProduct()
方法,违反了“对扩展开放,对修改封闭”的设计原则。 - 工厂类职责过重:如果产品种类很多,工厂类的逻辑可能变得非常庞大,影响维护性。
- 不利于产品的复杂多样化:如果产品种类非常多且产品之间有较复杂的差异,简单的
if-else
分支可能使得工厂类显得臃肿和不可维护。
使用场景
简单工厂模式适用于以下场景:
- 产品种类较少且变化不大的情况:如果需要创建的产品类型固定且不经常变动,使用简单工厂模式非常适合。
- 客户端需要创建多个类型的对象:当客户端需要通过不同的输入来创建不同类型的对象时,工厂模式可以有效地隐藏对象的创建逻辑。
- 控制产品创建逻辑:当需要集中管理和控制产品的创建过程时,简单工厂模式提供了很好的解决方案。
工厂方法模式
工厂方法模式(Factory Method Pattern) 是一种创建型设计模式,用于定义一个创建对象的接口,让子类决定实例化哪一个类。它通过将对象的创建委托给子类,从而实现了代码的解耦,使得代码更加灵活和易于扩展。
工厂方法模式有如下角色:
- 产品接口(Product):定义产品的公共接口。
- 具体产品(ConcreteProduct):实现产品接口的具体类。
- 工厂方法(Creator):声明创建产品对象的工厂方法。
- 具体工厂(ConcreteCreator):实现工厂方法,创建并返回具体的产品对象。
简单实现
(1)创建抽象工厂
public abstract class ComputerFactory {
public abstract <T extends Computer> T createComputer(Class<T> clz);
}
(2)创建具体工厂
广达代工厂是一个具体的工厂,其继承自抽象工厂,通过反射来生产不同厂家的计算机:
public class GDComputerFactor extends ComputerFactory{
@Override
public <T extends Computer> T createComputer(Class<T> clz) {
Computer computer = null;
String classname = clz.getName();
try {
computer = (Computer) Class.forName(classname).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return (T) computer;
}
}
(3)客户端调用
客户端创建了GDComputerFactor,并分别生产了联想计算机、惠普计算机和华硕计算机:
public class Client {
public static void main(String[] args) {
ComputerFactory computerFactory = new GDComputerFactor();
LenovoComputer mLenovoComputer = computerFactory.createComputer(LenovoComputer.class);
mLenovoComputer.start();
HpComputer mHpComputer = computerFactory.createComputer(HpComputer.class);
mHpComputer.start();
AsusComputer mAsusComputer = computerFactory.createComputer(AsusComputer.class);
mAsusComputer.start();
}
}
比较
特点 | 简单工厂模式 | 工厂方法模式 |
---|---|---|
创建方式 | 使用静态方法通过条件判断创建不同的产品对象。 | 通过定义一个抽象工厂接口,具体工厂类实现该接口来创建产品。 |
工厂类数量 | 只有一个工厂类,负责所有产品的创建。 | 有多个工厂类,每个具体工厂类负责创建一种产品。 |
可扩展性 | 不符合开闭原则,增加新产品需要修改工厂类代码。 | 符合开闭原则,新增产品时只需添加新的具体工厂类,而无需修改现有代码。 |
耦合性 | 客户端代码直接依赖于工厂方法,需要知道产品的种类。 | 客户端代码依赖于抽象工厂接口,具体的工厂类是透明的。 |
复杂度 | 简单,适用于产品种类较少的情况。 | 相对复杂,适用于产品种类较多或者希望扩展的情况。 |
优点 | 实现简单,适合产品种类较少的情况。 | 符合开闭原则,容易扩展,灵活性更强。 |
缺点 | 不符合开闭原则,增加产品时需要修改工厂类代码。 | 增加了系统的复杂性,需要创建多个工厂类。 |
建造者模式
建造者模式(Builder Pattern) 是一种 创建型设计模式,它允许通过一步一步地构建复杂对象,而无需指定对象的具体构造过程。建造者模式关注的是对象的构建过程,将对象的构建和表示分离开来,使得同样的构建过程可以创建不同类型的对象。
例如,我们要“DIY”一台台式计算机。我们找到“DIY”商家。这时我们可以要求这台计算机的CPU、主板或者其他部件都是什么牌子的、什么配置的,这些部件可以是我们根据自己的需求来定制的。但是这些部件组装成计算机的过程是一样的,我们无须知道这些部件是怎样组装成计算机的,我们只需要提供相关部件的牌子和配置就可以了。
建造者模式通常包含以下几个角色:
- 产品(Product):即最终要创建的复杂对象。
- 抽象建造者(Builder):提供构建对象的抽象接口,定义如何构建产品的各个部分。
- 具体建造者(ConcreteBuilder):实现
Builder
接口,负责具体产品的构建。 - 指挥者(Director):负责安排建造的顺序和调用建造者的方法,指导建造者如何构建产品。
简单实现
(1)创建产品类
我要组装一台计算机,计算机被抽象为Computer类,(本例假设)它有3个部件:CPU主板和内存,并在里面提供了3个方法分别用来设置CPU、主板和内存:
public class Computer {
private String mCpu;
private String mMainboard;
private String mRam;
public void setmCpu(String mCpu) {
this.mCpu = mCpu;
}
public void setmMainboard(String mMainboard) {
this.mMainboard = mMainboard;
}
public void setmRam(String mRam) {
this.mRam = mRam;
}
}
(2)创建Builder类,规范产品的组建
商家组装计算机有一套组装方法的模板,就是一个抽象的Builder 类,其里面提供了安装CPU、主板和内存的方法,以及组装成计算机的create方法,如下所示:
public abstract class Builder {
public abstract void buildCpu(String cpu);
public abstract void buildMainboard(String mainboard);
public abstract void buildRam(String ram);
public abstract Computer create();
}
商家实现了抽象的Builder类,MoonComputerBuilder类用于组装计算机:
public class MoonComputerBuilder extends Builder{
private Computer mComputer = new Computer();
@Override
public void buildCpu(String cpu) {
mComputer.setmCpu(cpu);
}
@Override
public void buildMainboard(String mainboard) {
mComputer.setmMainboard(mainboard);
}
@Override
public void buildRam(String ram) {
mComputer.setmRam(ram);
}
@Override
public Computer create() {
return mComputer;
}
}
(3)用导演类来统一组装过程
商家的导演类用来规范组装计算机的流程:先安装主板,再安装CPU,最后安装内存并组装成计算机:
public class Diretor {
Builder mBuild = null;
public Diretor(Builder mBuild) {
this.mBuild = mBuild;
}
public Computer createComputer(String cpu, String mainboard, String ram) {
this.mBuild.buildMainboard(mainboard);
this.mBuild.buildCpu(cpu);
this.mBuild.buildRam(ram);
return this.mBuild.create();
}
}
(4)客户端调用导演类
最后商家用导演类组装计算机。我们只需要提供自己想要的CPU、主板和内存就可以了至于商家怎样组装计算机我们无须知道。具体代码如下所示:
public class CreateComputer {
public static void main(String[] args) {
Builder mBuilder = new MoonComputerBuilder();
Diretor mDiretor = new Diretor(mBuilder);
mDiretor.createComputer("i5","8G","1T");
}
}
优缺点
优点
- 解耦复杂对象的构建过程和表示:客户端不需要知道构建的细节,可以专注于产品的组装。
- 代码可读性和可维护性高:将复杂对象的构建过程拆分成多个步骤,使得代码结构更加清晰。
- 支持不同产品的变体:同样的建造过程可以用于构建不同的产品对象。
缺点
- 产生多余的 Build 对象以及导演类
使用场景
- 创建一个复杂对象时,其构建过程应该独立于该对象的组成部分,并且可以允许被构建成不同的表示。
- 当一个对象的构建过程独立于其组成部分,并且可以组合成不同的方式时,可以使用建造者模式。
已经到底啦!!