
面试中常问的:单例(手写),简单工厂,工厂方法,抽象工厂,建造者(手写)

提供一个全局访问点
对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能;节约资源
单例类的职责过重,里面的代码可能会过于复杂,在一定程度上违背了“单一职责原则”。
如果实例化的对象长时间不被利用,会被系统认为是垃圾而被回收,这将导致对象状态的丢失。
会有内存泄漏和内存溢出的风险
1·饿汉式:初始化单例时就创建
public class Singleton {
//1. 创建私有变量 ourInstance(用以记录 Singleton 的唯一实例)
//2. 内部进行实例化
private static Singleton ourInstance = new Singleton();
//3. 把类的构造方法私有化,不让外部调用构造方法实例化
private Singleton() {
}
//4. 定义公有方法提供该类的全局唯一访问点
//5. 外部通过调用getInstance()方法来返回唯一的实例
public static Singleton newInstance() {
return ourInstance;
}
}
2·枚举:既可以避免多线程同步问题;还可以防止反射和序列化带来的重新创建新对象
因为Java虚拟机会保证枚举对象的唯一性,因此每一个枚举类型和定义的枚举变量在JVM中都是唯一的。
public enum Singleton{
//定义1个枚举的元素,即为单例类的1个实例
INSTANCE;
// 隐藏了1个空的、私有的 构造方法
// private Singleton () {}
public void method(){
System.out.println("我是一个单例");
}
}
例子:
public class User {
//私有化构造函数
private User(){ }
//定义一个静态枚举类
static enum SingletonEnum{
//创建一个枚举对象,该对象天生为单例
INSTANCE;
private User user;
//私有化枚举的构造函数
private SingletonEnum(){
user=new User();
}
public User getInstnce(){
return user;
}
}
//对外暴露一个获取User对象的静态方法
public static User getInstance(){
return SingletonEnum.INSTANCE.getInstnce();
}
}
public class Test {
public static void main(String [] args){
System.out.println(User.getInstance());
System.out.println(User.getInstance());
System.out.println(User.getInstance()==User.getInstance());
}
}
结果为true
3·懒汉式:基础实现的懒汉式是线程不安全的
class Singleton {
// 1. 类加载时,先不自动创建单例
// 即,将单例的引用先赋值为 Null
private static Singleton ourInstance = null;
// 2. 构造函数 设置为 私有权限
// 原因:禁止他人创建实例
private Singleton() {
}
// 3. 需要时才手动调用 newInstance() 创建 单例
public static Singleton newInstance() {
// 先判断单例是否为空,以避免重复创建
if( ourInstance == null){
ourInstance = new Singleton();
}
return ourInstance;
}
}
4·同步锁
每次访问都要进行线程同步(即 调用synchronized锁),造成过多的同步开销(加锁 = 耗时、耗能)
// 写法1
class Singleton {
// 1. 类加载时,先不自动创建单例
// 即,将单例的引用先赋值为 Null
private static Singleton ourInstance = null;
// 2. 构造函数 设置为 私有权限
// 原因:禁止他人创建实例
private Singleton() {
}
// 3. 加入同步锁
public static synchronized Singleton getInstance(){
// 先判断单例是否为空,以避免重复创建
if ( ourInstance == null )
ourInstance = new Singleton();
return ourInstance;
}
}
// 写法2
// 该写法的作用与上述写法作用相同,只是写法有所区别
class Singleton{
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
// 加入同步锁
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
return instance;
}
}
5·双重检验锁
class Singleton {
private static Singleton ourInstance = null;
private Singleton() {
}
public static Singleton newInstance() {
// 加入双重校验锁
// 校验锁1:第1个if
if( ourInstance == null){ // ①
synchronized (Singleton.class){ // ②
// 校验锁2:第2个 if
if( ourInstance == null){
ourInstance = new Singleton();
}
}
}
return ourInstance;
}
}
// 说明
// 校验锁1:第1个if
// 作用:若单例已创建,则直接返回已创建的单例,无需再执行加锁操作
// 即直接跳到执行 return ourInstance
// 校验锁2:第2个 if
// 作用:防止多次创建单例问题
// 原理
// 1. 线程A调用newInstance(),当运行到②位置时,此时线程B也调用了newInstance()
// 2. 因线程A并没有执行instance = new Singleton();,此时instance仍为空,因此线程B能突破第1层 if 判断,运行到①位置等待synchronized中的A线程执行完毕
// 3. 当线程A释放同步锁时,单例已创建,即instance已非空
// 4. 此时线程B 从①开始执行到位置②。此时第2层 if 判断 = 为空(单例已创建),因此也不会创建多余的实例
volatile 关键词作用:
正确的双重检查锁定模式需要需要使用 volatile。volatile主要包含两个功能。
保证可见性。使用 volatile 定义的变量,将会保证对所有线程的可见性。
禁止指令重排序优化。
由于 volatile 禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
6·静态内部类:根据 静态内部类 的特性,同时解决了按需加载、线程安全的问题,同时实现简洁
class Singleton {
// 1. 创建静态内部类
private static class Singleton2 {
// 在静态内部类里创建单例
private static Singleton ourInstance = new Singleton();
}
// 私有构造函数
private Singleton() {
}
// 延迟加载、按需创建
public static Singleton newInstance() {
return Singleton2.ourInstance;
}
}
// 调用过程说明:
// 1. 外部调用类的newInstance()
// 2. 自动调用Singleton2.ourInstance
// 2.1 此时单例类Singleton2得到初始化
// 2.2 而该类在装载 & 被初始化时,会初始化它的静态域,从而创建单例;
// 2.3 由于是静态域,因此只会JVM只会加载1遍,Java虚拟机保证了线程安全性
// 3. 最终只创建1个单例
模式组成

| 组成(角色) | 关系 | 作用 |
|---|---|---|
| 抽象产品(Product) | 具体产品的父类 | 描述产品的公共接口 |
| 具体产品(Concrete Product) | 抽象产品的子类;工厂类创建的目标类 | 描述生产的具体产品 |
| 工厂(Creator) | 被外界调用 | 根据传入不同参数从而创建不同具体产品类的实例 |
优点
缺点
应用场景
定义:工厂方法模式,又称工厂模式,多态工厂模式和虚拟构造器模式,通过定义工厂父类负责定义创建对象的公共接口,而子类则负责生成具体的对象。
将类的实例化(具体产品的创建)延迟到工厂类的子类(具体工厂)中完成,即由子类来决定应该实例化(创建)哪一个类。
解决的问题:
克服简单工厂模式的缺点,遵循了“开闭原则”,

| 组成(角色) | 关系 | 作用 |
|---|---|---|
| 抽象产品(Product) | 具体产品的父类 | 描述具体产品的公共接口 |
| 具体产品(Concrete Product) | 抽象产品的子类;工厂类创建的目标类 | 描述生产的具体产品 |
| 抽象工厂(Creator) | 具体工厂的父类 | 描述具体工厂的公共接口 |
| 具体工厂(Concrete Creator) | 抽象工厂的子类;被外界调用 | 描述具体工厂;实现FactoryMethod工厂方法创建产品的实例 |
优点
总结:工厂模式可以说是简单工厂模式的进一步抽象和拓展,在保留了简单工厂的封装优点的同时,让扩展变得简单,让继承变得可行,增加了多态性的体现。
缺点
有更多的类需要编译和运行,会给系统带来一些额外的开销;
抽象工厂模式,即Abstract Factory Pattern,提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类;具体的工厂负责实现具体的产品实例。一个工厂类生产一系列产品
允许使用抽象的接口来创建一组相关产品,而不需要知道或关心实际生产出的具体产品是什么,这样就可以从具体产品中被解耦。
UML图

优点:
隐藏创建对象的建造过程 & 细节,使得用户在不知对象的建造过程 & 细节的情况下,就可直接创建复杂的对象
UML:

优点
缺点
面试中常问的行为型设计模式:策略模式、观察者模式 和模板方法模式。

定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
观察者模式结构中通常包括观察目标和观察者两个继承层次结构:

观察者模式描述了如何建立对象与对象之间的依赖关系,以及如何构造满足这种需求的系统。观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。作为对这个通知的响应,每个观察者都将监视观察目标的状态以使其状态与目标状态同步,这种交互也称为发布-订阅(Publish-Subscribe)。观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。
根据上面的UML实现观察者模式
public interface Observer {
public void update(Subject subject);
}
import java.util.ArrayList;
public abstract class Subject {
public String subjectName;
public Subject(String subjectName) {
this.subjectName = subjectName;
}
//定义一个观察者集合用于存储所有观察者对象
protected ArrayList<Observer> observers = new ArrayList<Observer>();
//注册方法,用于向观察者集合中增加一个观察者
public void attach(Observer observer) {
observers.add(observer);
System.out.println(subjectName+"添加"+((ConcreteObserver)observer).observerName);
}
//注销方法,用于在观察者集合中删除一个观察者
public void detach(Observer observer) {
observers.remove(observer);
System.out.println(subjectName+"移除"+((ConcreteObserver)observer).observerName);
}
// 声明抽象通知方法
public abstract void notified();
}
public class ConcreteObserver implements Observer{
public String observerName;
public ConcreteObserver(String observerName) {
this.observerName = observerName;
}
// 具体的响应方法
@Override
public void update(Subject subject) {
String subjectName = ((ConcreteSubject) subject).subjectName;
System.out.println(observerName+"收到来自"+subjectName+"的通知");
}
}
import java.util.ArrayList;
public class ConcreteSubject extends Subject{
public ConcreteSubject(String subjectName) {
super(subjectName);
}
@Override
public void notified() {
//遍历观察者集合,调用每一个观察者的响应方法
for (Object obs:observers){
System.out.println(subjectName+"通知"+((ConcreteObserver)obs).observerName);
((Observer)obs).update(this);
}
}
}
public class Test {
public static void main(String[] args) {
ConcreteSubject concreteSubject = new ConcreteSubject("被观察者");
ConcreteObserver observer = new ConcreteObserver("观察者一");
ConcreteObserver observer2 = new ConcreteObserver("观察者二");
concreteSubject.attach(observer);
concreteSubject.attach(observer2);
concreteSubject.notified();
}
}

在JDK的java.util包中,提供了Observable类以及Observer接口,它们构成了JDK对观察者模式的支持

Observer接口充当抽象观察者 当观察目标的状态发生变化时,该方法将会被调用,在Observer的子类中将实现update()方法,即具体观察者可以根据需要具有不同的更新行为。当调用观察目标类Observable notifyObservers()方法时,将执行观察者类中的update()方法。
Observable类充当观察者目标类

我们可以直接使用Observer接口和Observable类来作为观察者模式的抽象层,再自定义具体观察者类和具体观察目标类,通过使用JDK中的Observer接口和Observable类,可以更加方便地在Java语言中应用观察者模式。
推荐阅读:Java设计模式之观察者模式
定义一系列算法,将每个算法封装到具有公共接口的一系列策略类中,从而使它们可以相互替换 & 让算法可在不影响客户端的情况下发生变化
策略模式仅仅封装算法(包括添加 & 删除),但策略模式并不决定在何时使用何种算法,算法的选择由客户端来决定
优点
缺点
定义一个模板结构,将具体内容延迟到子类里面去实现,在不改变模板结构的情况下,在子类中重新定义模板的内容,模板方法是基于继承的
解决问题:

优点
缺点
引入了抽象类,每一个不同的实现都需要一个子类来实现,导致类的个数增加,从而增加了系统实现的复杂度。
面试中常问的是:适配器模式,代理模式,外观模式

定义一个包装类,用于包装不兼容的接口对象,包装类=适配器Adapter,被包装的对象=适配者Adaptee=被适配的类
主要作用:把一个类的接口变换成客户端所期待的另一种接口,从而使原本借口不匹配而无法一起工作的两个类能够在一起工作,适配器分为:类适配器&对象适配器
类的适配器模式是把适配的类的API转换成为目标类的API。
UML类

为使Target能够使用Adaptee类里的SpecificRequest方法,故提供一个中间环节Adapter类(继承Adaptee & 实现Target接口),把Adaptee的API与Target的API衔接起来(适配)。
与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是使用委派关系连接到Adaptee类。

为使Target能够使用Adaptee类里的SpecificRequest方法,故提供一个中间环节Adapter类(包装了一个Adaptee的实例),把Adaptee的API与Target的API衔接起来(适配)。
优点
缺点
定义了一个高层、统一的接口,外部与通过这个统一的接口对子系统中的一群接口进行访问。
通过创建一个统一的外观类,用来包装子系统中一个 / 多个复杂的类,客户端可通过调用外观类的方法来调用内部子系统中所有方法

主要作用

优点
缺点
适配器模式是将一个对象包装起来以改变其接口,而外观是将一群对象 ”包装“起来以简化其接口。它们的意图是不一样的,适配器是将接口转换为不同接口,而外观模式是提供一个统一的接口来简化接口。
给目标对象提供一个代理对象,并由代理对象控制对目标对象的引用,通过引入代理对象的方式来间接访问目标对象,防止直接访问目标对象给系统带来不必要的复杂性

优点
缺点
应用场景

设计动态代理类(DynamicProxy)时,不需要显式实现与目标对象类(RealSubject)相同的接口,而是将这种实现推迟到程序运行时由 JVM来实现

推荐阅读:Carson带你学设计模式:动态代理模式(Proxy Pattern)
到这里基本面试常问的设计模式就全部结束了,主要问的就是单例,手写单例一定要会,里面有加锁机制的原理一定要懂,还有手写观察者模式要能手写