设计模式,面试级别的详解(持续更新中)
设计模式,面试级别的详解(持续更新中)
软件的设计原则
常⽤的⾯向对象设计原则包括7个,这些原则并不是孤⽴存在的,它们相互依赖,相互补充。
- 开闭原则(Open Closed Principle,OCP)
- 单⼀职责原则(Single Responsibility Principle, SRP)
- ⾥⽒替换原则(Liskov Substitution Principle,LSP)
- 依赖倒置原则(Dependency Inversion Principle,DIP)
- 接⼝隔离原则(Interface Segregation Principle,ISP)
- 合成/聚合复⽤原则(Composite/Aggregate Reuse Principle, C/ARP)
- 最少知识原则(Least Knowledge Principle,LKP)或者迪⽶特法则 (Law of Demeter,LOD)
设计模式的分类
- 创建型: 在创建对象的同时隐藏创建逻辑,不使用 new 直接实例化对象,程序在判断需要创建哪些对象时更灵活。包括工厂/抽象工厂/单例/建造者/原型模式。
- 结构型: 通过类和接口间的继承和引用实现创建复杂结构的对象。包括适配器/桥接模式/过滤器/组合/装饰器/外观/享元/代理模式。
- 行为型: 通过类之间不同通信方式实现不同行为。包括责任链/命名/解释器/迭代器/中介者/备忘录/观察者/状态/策略/模板/访问者模式。
单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
构造方法必须是私有的、由自己创建一个静态变量存储实例,对外提供一个静态公有方法获取实例
- 优点
- 内存中只有一个实例,减少了开销,尤其是频繁创建和销毁实例的情况下并且可以避免对资源的多重占用
- 缺点
- 没有抽象层,难以扩展,与单一职责原则冲突。
单例模式的常见写法
饿汉式,线程安全
饿汉式单例模式,顾名思义,类一加载就创建对象,这种方式比较常用,但容易产生垃圾对象,浪费内存空间。
/**
* 单例模式。饿汉式
* 比较常用,但是容易产生垃圾对象
* 优点:不用加锁,执行效率会很高
* 缺点:类加载时就初始化,浪费内存
*/
public class SingletonEHan {
private static SingletonEHan instance = new SingletonEHan();
private SingletonEHan(){}
public static SingletonEHan getInstance(){
return instance;
}
}
- 优点:线程安全,没有加锁,执行效率较高
- 缺点:不是懒加载,类加载时就初始化,浪费内存空间
- 懒加载 (lazy loading):使用的时候再创建对象
饿汉式单例是如何保证线程安全的呢
它是基于类加载机制避免了多线程的同步问题,但是如果类被不同的类加载器加载就会创建不同的实例,比如使用反射来破坏单例
懒汉式,线程安全
通过 synchronized 关键字加锁保证线程安全, synchronized 可以添加在方法上面,也可以添加在代码块上面,这里演示添加在方法上面,存在的问题是 每一次调用getInstance
获取实例时都需要加锁和释放锁,这样是非常影响性能的。
/**
* 单例模式
* 懒汉式,线程安全,但是效率很低
* 优点:第一次调用才初始化,避免内存浪费
* 缺点:必须枷锁synchronized才能保证单例,枷锁会影响效率
*/
public class SingletonLanHan{
private static SingletonLanHan instance;
private SingletonLanHan(){}
public static synchronized SingletonLanHan getInstance(){
if(instance == null){
instance = new SingletonLanHan();
}
return instance;
}
}
- 优点:懒加载,线程安全
- 缺点:必须枷锁synchronized才能保证单例,枷锁会影响效率
双重检查锁(DCL, 即 double-checked locking)
采用双锁机制,安全且能在多线程的情况下保持高性能
public class SingletonDCL{
private static volatile SingletonDCL instance;
private SingletonDCL(){}
publica static SingletonDCL getInstance(){
if(instance == null){
synchronized(SingletonDCL.class){
if(instance == null){
instance == new SingletonDCL();
}
}
}
return instance;
}
}
- 优点:懒加载,线程安全,效率较高
- 缺点:实现较复杂
这里的双重检查是指两次非空判断,锁指的是 synchronized 加锁。
为什么要进行双重判断
其实很简单,第一重判断,如果实例已经存在,那么就不再需要进行同步操作,而是直接返回这个实例,如果没有创建,才会进入同步块,同步块的目的与之前相同,目的是为了防止有多个线程同时调用时,导致生成多个实例,有了同步块,每次只能有一个线程调用访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建,之后的所有调用都不会进入同步块,直接在第一重判断就返回了单例。
关于内部的第二重空判断的作用,当多个线程一起到达锁位置时,进行锁竞争,其中一个线程获取锁,如果是第一次进入则为 null,会进行单例对象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返回已创建的单例对象。
这里为什么要使用 volatile
?
volatile
的两个特性:可⻅性、禁止指令重排序
这是因为 new 关键字创建对象不是原子操作,创建一个对象会经历下面的步骤:
- 在堆内存开辟内存空间
- 调用构造方法,初始化对象
- 引用变量指向堆内存空间
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序,排序后的顺序可能为1、2、3或者1、3、2,因此当某个线程在乱序运行 1、3、2 指令的时候,引用变量指向堆内存空间,这个对象不为 null,但是没有初始化,其他线程有可能这个时候进入了 getInstance 的第一个 if(instance == null) 判断不为 nulll ,导致错误使用了没有初始化的非 null 实例,这样的话就会出现异常,这个就是著名的DCL 失效问题。
,其他线程有可能这个时候进入了 getInstance 的第一个 if(instance == null) 判断不为 nulll ,导致错误使用了没有初始化的非 null 实例,这样的话就会出现异常,这个就是著名的DCL 失效问题**。
⼯⼚模式
简单工厂模式
简单⼯⼚模式指由⼀个⼯⼚对象来创建实例,客户端不需要关注创建逻 辑,只需提供传⼊⼯⼚的参数。
UML类图如下:
适⽤于⼯⼚类负责创建对象较少的情况,缺点是如果要增加新产品,就需要修改⼯⼚类的判断逻辑,违背开闭原则,且产品多的话会使⼯⼚类⽐较复杂。
例子
Calendar
抽象类的 getInstance
⽅法,调⽤createCalendar
⽅法根据不同 的地区参数创建不同的⽇历对象。
Spring 中的 BeanFactory
使⽤简单⼯⼚模式,根据传⼊⼀个唯⼀的标识来 获得 Bean 对象.
⼯⼚⽅法模式
和简单⼯⼚模式中⼯⼚负责⽣产所有产品相⽐,⼯⼚⽅法模式将⽣成具体 产品的任务分发给具体的产品⼯⼚。
UML类图如下:
也就是定义⼀个抽象⼯⼚,其定义了产品的⽣产接⼝,但不负责具体的产品,将⽣产任务交给不同的派⽣类⼯⼚。这样不⽤通过指定类型来创建对象了。
抽象⼯⼚模式
简单⼯⼚模式和⼯⼚⽅法模式不管⼯⼚怎么拆分抽象,都只是针对⼀类产 品,如果要⽣成另⼀种产品,就⽐较难办了!
抽象⼯⼚模式通过在 AbstarctFactory
中增加创建产品的接⼝,并在具体⼦⼯⼚中实现新加产品的创建,当然前提是⼦⼯⼚⽀持⽣产该产品。否则继承的这个接⼝可以什么也不⼲。
UML类图如下:
从上⾯类图结构中可以清楚的看到如何在⼯⼚⽅法模式中通过增加新产品 接⼝来实现产品的增加的。