单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供全局访问点。
单例模式类图:
单例模式有多种实现方式,下面我们详细介绍几种常见的实现方式:
懒汉式单例模式(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)
语句块,导致创建多个实例。需要考虑线程安全问题。为了避免线程不安全问题,懒汉式可以通过双重检查锁定来保证线程安全。
代码示例:
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 编译器优化等原因造成的问题。优点:
缺点:
饿汉式单例模式在类加载时就创建实例,这种方式不需要考虑线程安全问题,因为实例在类加载时就已经被创建。
代码示例:
public class Singleton {
// 静态初始化时就创建实例
private static final Singleton instance = new Singleton();
// 私有构造函数,避免外部实例化
private Singleton() {}
// 获取实例的方法
public static Singleton getInstance() {
return instance;
}
}
优点:
instance
是静态常量,JVM 保证线程安全。缺点:
静态内部类式单例模式是一种懒加载的单例实现方式,它结合了懒汉式和饿汉式的优点。使用静态内部类时,实例化的过程是延迟的,但又能够避免线程安全问题。
代码示例:
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
只会在类加载时初始化一次,而且类加载是线程安全的,因此不需要显式的同步控制。优点:
缺点:
枚举式单例模式是最简单、最安全的实现方式。Java 的枚举类型天然就是单例的,JVM 保证枚举实例的创建是线程安全的,而且枚举不允许被反射破坏单例。
代码示例:
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("Singleton with Enum");
}
}
优点:
INSTANCE
常量代表的,代码非常简洁。缺点:
特性 | 懒汉式(Lazy Initialization) | 线程安全的懒汉式(双重检查锁定) | 饿汉式(Eager Initialization) | 静态内部类式(Bill Pugh Singleton) | 枚举式(Enum Singleton) |
---|---|---|---|---|---|
实例化时机 | 延迟实例化,首次调用时才创建 | 延迟实例化,首次调用时才创建,但保证线程安全 | 类加载时就创建实例 | 延迟实例化,静态内部类加载时创建 | 类加载时创建(JVM保证线程安全和单例性) |
线程安全 | 否,可能出现并发问题(需要手动同步) | 是,使用双重检查锁定(synchronized )保证线程安全 |
是,由于类加载时创建,因此天然线程安全 | 是,JVM保证静态内部类加载时线程安全 | 是,JVM保证枚举类型线程安全和单例性 |
性能 | 较低(每次访问都需要判断 null ) |
较高(只有第一次需要同步) | 较高(不需要同步) | 较高(避免了每次判断 null ) |
最高,JVM直接管理,几乎无性能损耗 |
实现复杂度 | 简单,易于理解 | 稍复杂,需要额外的同步机制 | 简单,直接实现 | 稍复杂,涉及到静态内部类 | 非常简单,直接使用枚举实现 |
内存消耗 | 可能浪费内存(在未使用时实例仍然存在) | 不浪费内存,只在需要时实例化 | 可能浪费内存(实例化时就创建,不管是否使用) | 不浪费内存,实例化仅在第一次使用时创建 | 不浪费内存,枚举实例只在类加载时创建 |
防止反射攻击 | 否,反射可以创建多个实例 | 否,反射可以创建多个实例 | 否,反射可以创建多个实例 | 是,反射无法破坏单例 | 是,枚举类的反射破坏会抛出异常 |
防止序列化破坏 | 否,需要手动处理 | 否,需要手动处理 | 否,需要手动处理 | 是,JVM管理序列化,确保不会破坏单例 | 是,JVM保证序列化时保持唯一性 |
推荐场景 | 简单场景,单线程应用 | 线程安全需求较高,但性能要求不极端的场景 | 单线程应用或对性能要求较高的场景 | 需要延迟加载并且希望避免同步开销的场景 | 高度推荐,适用于大多数单例场景 |
常见问题 | 可能出现并发问题,需要同步 | 代码较复杂,性能相对稍差 | 不支持延迟加载,类加载时会立即初始化 | 稍复杂,理解需要一定的知识 | 极为简洁,但不适用于需要多个实例化参数的情况 |
简单工厂模式(也称为静态工厂方法模式)是创建型设计模式之一,它提供了一个类来负责创建实例化对象的工作,客户端只需要传入相关的参数,而不需要关心对象的创建过程。
这里我们用生产计算机来举例,假设有一个计算机的代工生产商,它目前已经可以代工生产联想计算机了。随着业务的拓展,这个代工生产商还要生产惠普和华硕的计算机。这样我们就需要用一个单独的类来专门生产计算机,这就用到了简单工厂模式。下面我们来实现简单工厂模式。
(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) 是一种创建型设计模式,用于定义一个创建对象的接口,让子类决定实例化哪一个类。它通过将对象的创建委托给子类,从而实现了代码的解耦,使得代码更加灵活和易于扩展。
工厂方法模式有如下角色:
(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、主板或者其他部件都是什么牌子的、什么配置的,这些部件可以是我们根据自己的需求来定制的。但是这些部件组装成计算机的过程是一样的,我们无须知道这些部件是怎样组装成计算机的,我们只需要提供相关部件的牌子和配置就可以了。
建造者模式通常包含以下几个角色:
Builder
接口,负责具体产品的构建。(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");
}
}
优点
缺点
已经到底啦!!