目录
前言
每一种设计模式都是基于七大设计原则而存在的。需掌握设计模式七大原则(迪米特法则、里氏替换原则原则、依赖倒置原则、开闭原则、接口隔离原则、单一职责原则、合成复用原则)后再看下面的内容。
一、设计模式介绍
① 软件设计模式(Design pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
② 设计模式的本质提高软件的维护性,通用性和扩展性,并降低软件的复杂度。
③ 1994 年,Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著出版了一本名为 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 的书。该书首次提到了软件开发中的设计模式的概念。
④ 设计模式并不局限于某种语言。Java、php、Python、C++ 等各种编程语言都有设计模式的概念。
二、设计模式有 23 种
上图分类罗列了设计模式的类型和 23 种设计模式的名称。为了板式好看,有些设计模式是横着排版的。
三、单例模式
作用:实现在整个软件系统运行过程中,某个类始终永远只能存在一个对象实例。
Demo:
① SqlSessionFactory 是一个可以从中获得 SqlSession 的实例的工厂类。从 SqlSessionFactory 中获取的 SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。SqlSessionFactory 在整个应用运行期间应该一直存在。下图是 MyBatis 官方文档对 SqlSessionFactory 的描述。
② Hibernate 框架的 SessionFactory 充当数据存储源的代理,并负责创建 Session 对象。SessionFactory 并不是轻量级的,一个项目只需要一个 SessionFactory。此时就可使用单例模式。
(1) 单例模式有 8 种
① 饿汉式(静态常量)
② 饿汉式(静态代码块)
③ 懒汉式(线程不安全)
④ 懒汉式(线程安全,同步方法)
⑤ 懒汉式(线程安全,同步代码块)
⑥ 双重检查
⑦ 静态内部类
⑧ 枚举
(2) 饿汉式(静态常量)
/**
* 饿汉式(静态常量)
*/
public class Singleton01 {
// 所谓饿汉式,Singleton01 一被加载 Singleton01 实例就被创建(有点饥渴)
private static final Singleton01 INSTANCE = new Singleton01();
// 构造方法私有化
private Singleton01() {
}
/**
* 方法作用:返回唯一的 Singleton01 实例
*
* @return 唯一的 Singleton01 实例
*/
public static Singleton01 getInstance() {
return INSTANCE;
}
}
总结:① 构造方法私有化(单例模式的作用是:让某个类的实例在整个应用运行过程中永远只有一个。因此不能提供公共的构造方法来实例化对象)
② 在类的内部创建(new)唯一的该类的实例
③ 向外提供一个静态的公共的方法返回该类唯一的实例
所谓饿汉式,就是该类比较饥渴、比较饿。只要 Singleton01 被加载,它的实例就被创建。
饿汉式(静态常量)单例模式的优缺点:
① 优点: 类装载,唯一的对象实例就被创建。避免了线程同步问题。
② 缺点:在类装载的时候就完成实例化,没有实现懒加载(Lazy Load)。如果从始至终从未使用过该实例,则会造成内存浪费。
解释:该方式基于类加载(ClassLoader)机制避免了多线程的同步问题。但 INSTANCE 在类装载时就实例化可能造成内存浪费。在单例模式中,一般是调用 getInstance 方法导致类装载,进而创建唯一的对象实例。但导致类装载的方式不止一种,因此不能确定是否有其他的方式导致类装载。如果有,会导致内存方法。
触发类加载的方式:
① 调用静态成员时(静态常量、静态方法)
② 第一次 new 对象的时候
③ 子类加载会导致父类先被加载
(3) 饿汉式(静态代码块)
/**
* 饿汉式(静态代码块)
*/
public class Singleton02 {
private static Singleton02 instance;
static { // 在静态代码块中实例化 Singleton02 对象
instance = new Singleton02();
}
// 构造方法私有化
private Singleton02() {
}
/**
* 方法作用:返回唯一的 Singleton02 实例
*
* @return 唯一的 Singleton02 实例
*/
public static Singleton02 getInstance() {
return instance;
}
}
说明:该方式和静态常量饿汉式类似。区别是:将类实例化的过程放在了静态代码块中。由于也是在类装载的时候执行静态代码块中的代码,进行实例的创建。所以,可避免线程同步问题(优点),可能造成内存浪费(缺点)。
(4) 懒汉式(线程不安全)
/**
* 懒汉式(线程不安全)
*/
public class LazySingleton01 {
private static LazySingleton01 instance;
private LazySingleton01() {
}
/**
* 说明:当在程序中调用了该类的 getInstance 方法的时候,才创
* 键该类的唯一实例
*/
public static LazySingleton01 getInstance() {
if (instance == null) {
instance = new LazySingleton01();
}
return instance;
}
}
优缺点:
① 优点:相比饿汉式,该方式有懒加载(Lazy Load)的效果,可以避免内存浪费。
② 缺点:可能有线程安全问题。如果在多线程环境下,一个线程进入了 if (instance== null) 判断语句,且还未来得及往下执行。此时另一个线程也进入了 if (instance== null) 判断语句,次时便会产生多个实例,因此也就没有实现单例模式的功能了。
结论:该写法在多线程环境下不可使用,开发中也不这样写。
(5) 懒汉式(线程安全 - 同步方法)
/**
* 懒汉式(线程安全 - 同步方法)
*/
public class LazySingleton02 {
private static LazySingleton02 instance;
private LazySingleton02() {
}
/**
* 说明:【同步方法】实现线程同步
*/
public static synchronized LazySingleton02 getInstance() {
if (instance == null) {
instance = new LazySingleton02();
}
return instance;
}
}
优缺点:
① 线程安全、和饿汉式比不浪费内存(优点)
② 同步方法效率太低。 每个线程需要获得类实例的时候都得执行 getInstance() 方法,都要执行线程同步操作。线程同步操作(有 synchronized 效率是会降低的)。实际上,实例化对象的代码只需要执行一次即可,之后若想获得该类实例直接 return 即可。
(6) 懒汉式(线程安全 - 同步代码块)
该方式并未解决线程同步问题。
/**
* 懒汉式(线程安全 - 同步代码块)
*/
public class LazySingleton03 {
private static LazySingleton03 instance;
private LazySingleton03() {
}
/**
* 说明:并未解决线程同步问题
*/
public static LazySingleton03 getInstance() {
if (instance == null) {
// 同步代码块
synchronized (LazySingleton03.class) {
instance = new LazySingleton03();
}
}
return instance;
}
}
【说明】该方式本来是想对【懒汉式(线程安全 - 同步方法)】的代码进行改进,提高其效率。但该写法并不能起到线程同步的作用。假如一个线程进入了 if (instance== null) 判断语句,还未来得及往下执行。此时,另一个线程也进行了 if (instance== null) 判断语句,此时便会产生多个实例(哪就不是单例模式了)
(7) 双重检查(推荐使用)
volatile 是 Java 提供的一种轻量级的同步机制。
当多个线程同时访问同一个变量时,若某个线程修改了该变量的值,其他线程能够立即看到该变量被修改的值。
在多线程环境下,某个线程对共享变量的操作(修改)对其他线程是不可见的。Java 提供了 volatile 关键字来保证可见性(当一个变量被 volatile 修饰后,表示线程本地内存无效。当一个线程修改共享变量后,共享变量的值会被立即更新到主内存中。其他线程读取共享变量时,会直接从主内存读取。)
/**
* 双重检查
*/
public class DoubleCheckSingleton {
// volatile:假如某个线程修改了 instance 的值,其他线程能够立刻知道
private static volatile DoubleCheckSingleton instance;
private DoubleCheckSingleton() {
}
public static DoubleCheckSingleton getInstance() {
if (instance == null) { // 第一层检查
synchronized (DoubleCheckSingleton.class) {
if (instance == null) { // 第二层检查
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
【优点】
① 线程安全
② 懒加载
③ 实例化代码只执行一次,之后再调用 getInstance 方法的时候,依然会判断 instance 是否为 null。如果不为 null,直接 return 即可
④ 避免反复进行线程同步操作
实际开发和编程中,推荐双重检查的方式实现单例模式。
(8) 静态内部类【极度推荐】
public class Test {
/**
* 静态内部类
*/
private static class StaticInner {
private static String name = "顾清兒";
}
public static String getName() {
return StaticInner.name;
}
}
静态内部类特点:以上面的代码为例。① 当 Test 类被装载的时候,StaticInner 类不会被装载。
② 调用 Test 类的 getName 方法(getName 内部使用 StaticInner 静态内部类)会导致 StaticInner 类被装载(只装载一次),类被装载的时候线程是安全的。
下面代码的作用:证明静态内部类的特点
public class Test {
// 当 Test 类被装载的时候,静态代码块的代码会被执行
static {
System.out.println("Test - 静态代码块");
}
/**
* 静态内部类
*/
static class StaticInner {
static String name = "顾清兒";
static { // 当 StaticInner 被加载的时候,静态代码块的代码会被执行
System.out.println("StaticInner - 静态代码块");
}
}
public static void getName() {
// 使用 StaticInner 里面的 静态属性会导致 StaticInner 类的装载
String goodName = StaticInner.name;
}
}
使用静态内部类的特点编写单例模式。
/**
* 根据静态内部类的特点,实现单例模式的功能
*/
public class SingletonStaticInner {
private SingletonStaticInner() {
}
/**
* ① SingletonStaticInner 装载的时候不会导致 StaticInnerClass 的装载,进
* 而 INSTANCE 不会有值(懒加载、避免内存浪费)。
* ② 当调用 SingletonStaticInner 的 getInstance 方法的时候,方法内部
* 会使用到静态内部类 StaticInnerClass 里面的 INSTANCE,此操作会导致
* StaticInnerClass 的装载,进而创建 SingletonStaticInner 实例,和给 INSTANCE
* 赋值。
* ③ 静态内部类 StaticInnerClass 装载的时候是线程安全的(实现线程安全)。
*/
private static class StaticInnerClass {
private static final SingletonStaticInner INSTANCE =
new SingletonStaticInner();
}
public static SingletonStaticInner getInstance() {
return StaticInnerClass.INSTANCE;
}
}
【说明】
① 该方式通过类装载机制保证初始化实例时只有一个线程。
② 静态内部类在 SingletonStaticInner 类被装载时不会被装载(静态内部类里面的代码不会执行)
③ 当调用 SingletonStaticInner 类里面的 getInstance 方法后,getInstance 方法内部会使用到静态内部类 StaticInnerClass 里面的静态属性。使用静态内部类里面的静态属性会导致静态内部类的装载,进而导致静态内部类里面的代码被执行,以至于 SingletonStaticInner 被实例化,并赋值给了 INSTANCE 。
④ 类的静态属性只会在第一次装载类的时候初始化(效率高)。此处巧妙地利用 JVM 保证了线程安全,类装载的时候别的线程是无法进入的。
优点:避免线程不安全;利用静态内部类的特点实现延迟加载、避免内存浪费【推荐使用】
(9) 枚举【推荐使用】
/**
* 枚举实现单例模式
*/
public enum SingletonEnum {
INSTANCE; // 默认就是单例的
public void getInstance() {
System.out.println(INSTANCE.hashCode());
}
}
【说明】
① 在此借助 JDK1.5 中添加的枚举类型来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象的情况。
② Effective Java (书名,见下图)的作者 Josh Bloch 提倡该方式实现单例模式。
(10) JDK 中 的 Runtime 类就是单例模式
(11) 单例模式使用在…
① 被设计为单例模式的类的对象在整个应用运行过程中只有一个实例,节省了系统资源。对于一些需要频繁创建和销毁的对象,使用单例模式可以提高性能
② 单例模式使用的场景:
若对象需要被频繁地创建和销毁;
若创建某个对象时需要耗费很多时间和资源(重量级对象),但该对象又经常被使用到;
工具类对象;
频繁访问数据库或文件的对象(eg:数据源、session 工厂等)。
四、工厂模式
工厂模式涉及到的原则:
① 开闭原则:一个软件实体应对扩展开放,对修改关闭
② 依赖倒置原则:面向接口编程
③ 迪米特法则:只与直接朋友通信,避免与陌生朋友通信
工厂模式目的
① 若想使用某个类的实例:不使用 new 关键字创建该类实例,而使用工厂类的方法获取该类实例
② 实现对象创建者和对象使用者分离
工厂模式分类:
① 简单工厂模式:生产同一等级结构中的任意产品(若需增加新的产品,需修改原有代码)
② 工厂方法模式:生产同一等级结构中的固定产品(支持增加任意产品)
③ 抽象工厂模式:围绕一个超级工厂创建其他工厂。超级工厂是其他工厂的工厂。
(1) 简单(静态)工厂模式
/**
* 文具(如尺子、钢笔、橡皮擦)
*/
public interface Stationery {
void name();
}
public class Ruler implements Stationery {
@Override
public void name() {
System.out.println("❀ 尺子");
}
}
public class Pen implements Stationery {
@Override
public void name() {
System.out.println("❀ 钢笔");
}
}
/**
* 文具工厂类(专门用于生产各种文具)
* 不符合开闭原则:当增加了新的文具类型(如橡皮擦),需要修改 obtainStationery 方法的代码
* 开闭原则:对扩展开放,对修改关闭
*/
public class StationeryFactory {
public static Stationery obtainStationery(String stationeryName) {
Stationery stationery = null;
if (stationeryName.equals("钢笔")) {
stationery = new Pen();
} else if (stationeryName.equals("尺子")) {
stationery = new Ruler();
}
return stationery;
}
}
public class Main {
public static void main(String[] args) {
// 若想使用某个类的对象,不通过 new 关键字创建,而用工厂类的方法获得
Stationery pen = StationeryFactory.obtainStationery("钢笔");
Stationery ruler = StationeryFactory.obtainStationery("尺子");
pen.name();
ruler.name();
}
}
不符合开闭原则:若想扩展功能,需要修改工程类的方法。
客户端调用工厂的方法传入不同文具的名字获得不同文具的对象实例。
简单工厂模式不符合开闭原则,但使用得很多。
(2) 工厂方法模式
工厂方法模式:不修改原有代码的前提下,通过增加新的工厂类实现扩展。符合开闭原则,但结构比较复杂。和简单工厂模式比,不太常用。
/**
* 全部的工厂类都要实现该接口(制定工厂类的规范)
*/
public interface StationeryFactory {
Stationery obtainStationery();
}
/**
* 钢笔工厂类(用于创建和返回 Pen 实例)
*/
public class PenFactory implements StationeryFactory {
@Override
public Stationery obtainStationery() {
return new Pen();
}
}
/**
* 尺子工厂类(用于返回 Ruler 实例)
*/
public class RulerFactory implements StationeryFactory {
@Override
public Stationery obtainStationery() {
return new Ruler();
}
}
/**
* 文具接口(如尺子、钢笔、橡皮擦)
* 作用:制定文具符合的规范
*/
public interface Stationery {
void name();
}
public class Pen implements Stationery {
@Override
public void name() {
System.out.println("❀ 钢笔");
}
}
public class Ruler implements Stationery {
@Override
public void name() {
System.out.println("❀ 尺子");
}
}
public class Main {
public static void main(String[] args) {
Stationery pen = new PenFactory().obtainStationery();
Stationery ruler = new RulerFactory().obtainStationery();
pen.name();
ruler.name();
}
}
客户端需要什么文具就调用什么工厂类。