设计模式是在软件工程设计问题中总结出来的有用的经验,是某类问题的通用解决方案

设计模式的本质是提高软件的维护性,通用性和扩展性,并降低软件的复杂度

设计模式大体上分为三种类型,23种

  • 创建型模式
    对象该如何创建

    • 单例模式,抽象工厂模式,原型模式,建造者模式,工厂模式
  • 结构型模式
    结构型模式描述如何将类或对象按某种布局组成更大的结构

    • 适配器模式,桥接模式,装饰模式,组合模式,外观模式,享元模式,代理模式
  • 行为型模式
    描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法 单独完成的任务,它涉及算法与对象间职责的分配

    • 模板方法模式,命令模式,访问者模式,迭代器模式,观察者模式,中介者模式,备忘录模式,解释器模式 ( Interpreter 模式 ),状态模式,策略模式,责任链模式

单例模式

采取一定的方法,保证在整个软件系统中,某一个类只能存在一个对象实例,并且该类只提供一个取得其对象的方法

实现单例模式有八种方式:

  • 饿汉式 ( 静态常量 )
  • 饿汉式 ( 静态代码块 )
  • 懒汉式 ( 线程不安全 )
  • 懒汉式 ( 线程安全,同步方法 )
  • 懒汉式 ( 线程安全,同步代码块 )
  • 双重检查
  • 静态内部类
  • 枚举

饿汉式——————

饿汉式 ( 静态常量 )

饿汉式 ( 静态常量 ) 的步骤如下

  • 构造器私有化
  • 类的内部创建对象
  • 向外暴露一个静态的公共方法,getInstance(),通过调用该方法来获得单例对象
  1. public class SingletonHungry {
  2. public static void main(String[] args) {
  3. Person instance = Person.getInstance();
  4. Person instance1 = Person.getInstance();
  5. System.out.println(instance == instance1);
  6. }
  7. }
  8. class Person{
  9. /**
  10. * 1.
  11. * 私有化构造器,防止 new
  12. */
  13. private Person(){
  14. }
  15. /**
  16. * 2.在本类内部创建对象实例
  17. */
  18. private final static Person INSTANCE = new Person();
  19. /**
  20. *
  21. * @return: 提供公有的静态方法,返回实例对象
  22. */
  23. public static Person getInstance(){
  24. return INSTANCE;
  25. }
  26. }

设计模式 (下) - 图1

INSTACE 作为静态变量是随着类的加载而创建,并且由于类只会被加载一次,因此内存中只会存在一份 INSTACE 实例

优缺点

优点:写法简单,在类加载时完成实例化,避免了线程同步问题

缺点:在类加载时完成实例化,没有达到 lazy loading ( 懒加载,用到了才去加载 ) 的效果 ( 引起类加载的方式有很多,无法保证一定是通过调用获取单例对象的方法才引起的类加载 )如果从未使用过这个实例,会造成内存的浪费

结论:这种单例模式可能会造成内存浪费

饿汉式 ( 静态代码块 )

不过是把初始化方法了静态代码块中鹅以

  1. public class SingletonHungry {
  2. public static void main(String[] args) {
  3. Person instance = Person.getInstance();
  4. Person instance1 = Person.getInstance();
  5. System.out.println(instance == instance1);
  6. }
  7. }
  8. class Person{
  9. private static Person instance;
  10. private Person(){}
  11. /**
  12. * 在静态代码块中初始化单例对象
  13. */
  14. static{
  15. instance = new Person();
  16. }
  17. public static Person getInstance(){
  18. return instance;
  19. }
  20. }

优缺点

和使用静态常量没啥区别

懒汉式——————

懒汉式 ( 线程不安全 )

懒汉式即懒加载,用到时才去初始化单例对象

  1. public class LazyManThreadNoSecurity {
  2. public static void main(String[] args) {
  3. Singleton instance = Singleton.getInstance();
  4. Singleton instance1 = Singleton.getInstance();
  5. System.out.println(instance == instance1);
  6. }
  7. }
  8. class Singleton{
  9. private static Singleton instance;
  10. private Singleton(){}
  11. /**
  12. * 提供一个静态的公有方法,当使用到该方法
  13. * 并且单例对象没有被创建时才去创建单例对象
  14. * @return:单例对象
  15. */
  16. public static Singleton getInstance(){
  17. if(instance == null)
  18. return instance = new Singleton();
  19. return instance;
  20. }
  21. }

但是这种方法在多线程情况下是有问题的:如果一个线程判真并进入后执行权被抢夺 (这时还没有创建单例对象),另一个线程进入也会判真,然后创建出单例对象,执行完成后执行权回到原来的线程,又会再创建出一个单例对象。就不再保证了单例对象的唯一性

多线程情况下

  1. public class LazyManThreadNoSecurity {
  2. public static void main(String[] args) {
  3. SingletonThread singletonThread = new SingletonThread();
  4. SingletonThread singletonThread2 = new SingletonThread();
  5. new Thread(singletonThread).start();
  6. new Thread(singletonThread2).start();
  7. }
  8. }
  9. class Singleton{
  10. private static Singleton instance;
  11. private Singleton(){}
  12. /**
  13. * 提供一个静态的公有方法,当使用到该方法
  14. * 并且单例对象没有被创建时才去创建单例对象
  15. * 但这种方法在多线程下是不安全的
  16. * @return:单例对象
  17. */
  18. public static Singleton getInstance(){
  19. if(instance == null){
  20. System.out.println("执行了初始化单例对象的方法");
  21. return instance = new Singleton();
  22. }
  23. return instance;
  24. }
  25. }
  26. class SingletonThread implements Runnable{
  27. //存储单例对象
  28. Set<Singleton> list= new HashSet<>();
  29. @Override
  30. public void run() {
  31. Singleton instance = Singleton.getInstance();
  32. list.add(instance);
  33. System.out.println(list.size());
  34. }
  35. }

设计模式 (下) - 图2

可以看见多次初始化了单例对象,没有保证单例对象的唯一性

优缺点

懒汉式起到了懒加载的效果,但是只能在单线程下使用

在多线程情况下,一个线程进入了 if( singleton==null ) 语句块中,还未来得及创建单例对象,就被另一个线程抢夺了执行权,进入到这个判断语句块中创建了一个单例对象并完成执行后,原来的线程获得执行权,继续向下执行,又创建了一个单例对象。这时就产生了对个单例对象

结论:在实际开发中,不要使用这种方式

懒汉式 ( 同步方法 )

就是在初始化单例对象的方法上加个 synchronized 即可

  1. public class LazyManThreadSecurity {
  2. public static void main(String[] args) {
  3. SingletonThread singletonThread = new SingletonThread();
  4. SingletonThread singletonThread2 = new SingletonThread();
  5. new Thread(singletonThread).start();
  6. new Thread(singletonThread2).start();
  7. }
  8. }
  9. class Singleton{
  10. private static Singleton instance;
  11. private Singleton(){}
  12. /**
  13. * 提供一个静态的公有方法,当使用到该方法
  14. * 并且单例对象没有被创建时才去创建单例对象
  15. * 但这种方法在多线程下是不安全的
  16. * @return:单例对象
  17. */
  18. public static synchronized Singleton getInstance(){
  19. if(instance == null){
  20. System.out.println("执行了初始化单例对象的方法");
  21. return instance = new Singleton();
  22. }
  23. return instance;
  24. }
  25. }
  26. class SingletonThread implements Runnable{
  27. //存储单例对象
  28. Set<Singleton> list= new HashSet<>();
  29. @Override
  30. public void run() {
  31. Singleton instance = Singleton.getInstance();
  32. list.add(instance);
  33. System.out.println(list.size());
  34. }
  35. }

设计模式 (下) - 图3

优缺点

解决了线程不安全问题

效率太低,每个线程如果想要获得类的实例,都需要在执行 getInstance() 方法时对整个方法要进行上锁和释放锁

在实际开发中,不推荐这种方式

懒汉式 ( 同步代码块 )

可以用同步代码块取代对整个方法的同步,仅对方法中操作共享变量的代码加同步锁,这样就能使没有获取到锁的线程能执行同步代码块之前的代码,进而提高程序性能

  1. public class LazyManThreadSecurity {
  2. public static void main(String[] args) {
  3. SingletonThread singletonThread = new SingletonThread();
  4. SingletonThread singletonThread2 = new SingletonThread();
  5. new Thread(singletonThread).start();
  6. new Thread(singletonThread2).start();
  7. }
  8. }
  9. class Singleton{
  10. private static Singleton instance;
  11. private Singleton(){}
  12. /**
  13. * 提供一个静态的公有方法,当使用到该方法
  14. * 并且单例对象没有被创建时才去创建单例对象
  15. * 但这种方法在多线程下是不安全的
  16. * @return:单例对象
  17. */
  18. public static Singleton getInstance(){
  19. synchronized (Singleton.class){
  20. if(instance == null){
  21. System.out.println("执行了初始化单例对象的方法");
  22. return instance = new Singleton();
  23. }
  24. }
  25. return instance;
  26. }
  27. }
  28. class SingletonThread implements Runnable{
  29. //存储单例对象
  30. Set<Singleton> list= new HashSet<>();
  31. @Override
  32. public void run() {
  33. Singleton instance = Singleton.getInstance();
  34. list.add(instance);
  35. System.out.println(list.size());
  36. }
  37. }

双重检查———

双重检查解决了线程安全问题的同时,解决了懒加载的问题

  1. public class DoubleCheck {
  2. public static void main(String[] args) {
  3. SingletonThread singletonThread = new SingletonThread();
  4. new Thread(singletonThread).start();
  5. new Thread(singletonThread).start();
  6. new Thread(singletonThread).start();
  7. new Thread(singletonThread).start();
  8. new Thread(singletonThread).start();
  9. }
  10. }
  11. class Singleton{
  12. //volatile 可以认为是轻量级的同步锁
  13. private static volatile Singleton instance;
  14. private Singleton(){}
  15. /**
  16. * 提供一个静态的公有方法,当使用到该方法
  17. * 并且单例对象没有被创建时才去创建单例对象
  18. * 双重检查
  19. * @return:单例对象
  20. */
  21. public static Singleton getInstance(){
  22. if( instance == null ){
  23. synchronized (Singleton.class){
  24. if(instance == null){
  25. System.out.println("执行了初始化单例对象的方法");
  26. instance = new Singleton();
  27. }
  28. }
  29. }
  30. return instance;
  31. }
  32. }
  33. class SingletonThread implements Runnable{
  34. //存储单例对象
  35. Set<Singleton> list= new HashSet<>();
  36. @Override
  37. public void run() {
  38. Singleton instance = Singleton.getInstance();
  39. list.add(instance);
  40. System.out.println(list.size());
  41. }
  42. }

假设,A 和 B 两个线程,都进入到了第一个 if( instance == null ) 代码块中,A 先拿到了同步锁,进入同步代码块,这时 B 就进不来了。然后 A 执行完成后创建了一个单例对象,这时执行权回到 B,B 拿到同步锁进入同步代码块中后进行判断发现已经有了单例对象,所以不再创建,而是直接返回创建了的单例对象

为什么使用 volatile?

在介绍为什么使用 volatile 时,先介绍一下什么是 volatile

volatile 变量可以被看作是一种轻量级的 synchronized

锁提供了两种主要特性:互斥(mutual exclusion)可见性(visibility)

  • 互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。
  • 可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题

volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性

同时,volatile 具有禁止指令重排的功能

指令重排

推荐博文:https://www.cnblogs.com/tuhooo/p/7921651.html

指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序

指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果

然而,指令重排是一把双刃剑,虽然优化了程序的执行效率,但是在某些情况下,会影响到多线程的执行结果

由于 volatile 禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性

接下来分析一下如果不禁止指令重排优化的后果

从字节码看一个对象的创建,分为一几步

  1. 分配对象内存
  2. 调用构造器方法,执行初始化
  3. 将对象引用赋值给变量

JVM 实际运行时,2,3 可能发生重排序,但 1 不会

如果不禁止指令重排优化,线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序 ( 先赋值引用,而不是先初始化 )。当线程 1 赋值引用后,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。但是由于该对象还未初始化,所以线程 2 访问时将会发生异常

所以在使用双重检查时使用 volatile 主要是用到了其以下两个功能

  • 保证可见性。使用 volatile 修饰的变量,将会保证对所有线程的可见性
  • 禁止指令重排优化

静态内部类——

静态内部类的特点

  • 外部类加载时不会被加载。保证了懒加载
  • 在单例模式下,调用静态内部类中的单例对象导致静态内部类加载时,线程是安全的 ( JVM 在进行类加载时只有一个线程能够获取到 Class 对象的初始化锁 )。保证了线程安全
  1. public class StaticSingleton {
  2. public static void main(String[] args) {
  3. Singleton instance = Singleton.getInstance();
  4. Singleton instance1 = Singleton.getInstance();
  5. System.out.println(instance == instance1);
  6. }
  7. }
  8. class Singleton{
  9. private Singleton(){}
  10. /**
  11. * 静态内部类
  12. */
  13. private static class GetSingleton{
  14. private static final Singleton INSTANCE = new Singleton();
  15. }
  16. public static Singleton getInstance(){
  17. return GetSingleton.INSTANCE;
  18. }
  19. }

优缺点

采用了类加载机制来保证初始化单例对象时只有一个线程,同时由于静态属性只有在类第一次加载时才会初始化,所以 JVM 保证了线程的安全性。

基于静态内部类在外部类加载时不会被加载的特性,我们只用在需要时调用 getInstance() 方法,在其中加载静态内部类,从而实例化单例对象并将其返回即可。保证了懒加载

序列化破坏单例

在看过那么多单例模式的写法后,那单例模式有没有漏洞呢?

来看看下面这段代码

  1. public class Test {
  2. public static void main(String[] args) throws IOException, ClassNotFoundException {
  3. Singleton instance = Singleton.getInstance();
  4. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
  5. oos.writeObject(instance);
  6. File file = new File("singleton_file");
  7. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
  8. Singleton newInstance = (Singleton) ois.readObject();
  9. System.out.println(instance == newInstance);
  10. }
  11. }
  12. class Singleton implements Serializable{
  13. /**
  14. * 1.
  15. * 私有化构造器,防止 new
  16. */
  17. private Singleton(){
  18. }
  19. /**
  20. * 2.在本类内部创建对象实例
  21. */
  22. private final static Singleton INSTANCE = new Singleton();
  23. /**
  24. *
  25. * @return: 提供公有的静态方法,返回实例对象
  26. */
  27. public static Singleton getInstance(){
  28. return INSTANCE;
  29. }
  30. }

这段代码中我们先将 instance 这个单例对象序列化,然后又将其读取出来,再次比较,发现不再是同一对象

设计模式 (下) - 图4

接下来,尝试改进这个会被破坏的单例模式

  1. public class Test {
  2. public static void main(String[] args) throws IOException, ClassNotFoundException {
  3. Singleton instance = Singleton.getInstance();
  4. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
  5. oos.writeObject(instance);
  6. File file = new File("singleton_file");
  7. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
  8. Singleton newInstance = (Singleton) ois.readObject();
  9. System.out.println(instance == newInstance);
  10. }
  11. }
  12. class Singleton implements Serializable{
  13. /**
  14. * 1.
  15. * 私有化构造器,防止 new
  16. */
  17. private Singleton(){
  18. }
  19. /**
  20. * 2.在本类内部创建对象实例
  21. */
  22. private final static Singleton INSTANCE = new Singleton();
  23. /**
  24. *
  25. * @return: 提供公有的静态方法,返回实例对象
  26. */
  27. public static Singleton getInstance(){
  28. return INSTANCE;
  29. }
  30. /**
  31. * 只需加上这个方法,就可以防止序列化破坏单例模式
  32. * @return
  33. */
  34. private Object readResolve(){
  35. return INSTANCE;
  36. }
  37. }

设计模式 (下) - 图5

那究竟是为什么呢会出现 false,而加上一个方法后又为 true 了呢?

源码分析

进入 ois 对象的 readObject() 方法中

设计模式 (下) - 图6

进入 readObject0() 这个方法中,找到这一串 switch 中的明显的关于 Object 的选项,这里有两层方法,先进入里面那层

设计模式 (下) - 图7

继续深入

设计模式 (下) - 图8

可以看到这个方法中最终返回的是 obj,并且会通过 isInstantibale() 方法进行三目判断后赋值,进入 isInstantibale() 方法,看一下这个方法做何用

设计模式 (下) - 图9

显然。。从代码并不能看出什么,但好在是有注释的

这个注释的大概意思是:如果实现了 serializable/externalizable 接口 (后者用于序列化定制),就会返回 true,返回 true,返回到 readOrdinaryObject() 方法中的

设计模式 (下) - 图10

这里就会 newInstance(),通过反射创建出对象,最后返回 obj,因此这样得到的对象肯定和一开始得到的那个对象就不是同一个了,所以一开始的结果是 false

这样,我们的第一个疑惑就解答了

接下来,我们继续探索为啥加上一个 readResolve() 方法后就可以让两个对象成为同一个的原因

设计模式 (下) - 图11

从 hasReadResolveMethod() 这个方法名来看,就是判断有没有这个方法存在的,我们进入这个方法

设计模式 (下) - 图12

查看其注释:如果一个类实现了 serializable or externalizable 接口,并且有 readResolve 这个方法,就返回 true;否则返回 false

回到 readOrdinaryObject() 方法中

设计模式 (下) - 图13

进入这个反射方法中,这个方法应该就是在通过反射调用 obj 中的 readResolve 这个方法了

设计模式 (下) - 图14

那 readResolveMethod 又是在哪被指定为 obj 中的 readResolve 方法呢。搜索关键字 readResolve

设计模式 (下) - 图15

可以发现 readResolveMethod 的目标方法名被指定为了 readResolve

所以 invokeReadResolve 这个方法会调用 obj 中的 readResolve 方法,在这个案例中,readResolve 会返回单例对象

返回后回到上层方法 readOrdinaryObject()

设计模式 (下) - 图16

这里的 rep 就是 readResolve 方法返回的单例对象了,而 obj 就是上面的 newInstance 方法反射新建的对象,所以并不是相同的,所以会将 obj 设置为我们的返回的单例对象,最后返回 obj

所以就解决了第二个疑问,为什么加上一个 readResolve 方法后,就避免了序列化破坏单例模式

单例模式反射攻击

  1. public class Test {
  2. public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
  3. Class<Singleton> singletonClass = Singleton.class;
  4. //通过反射获得构造器对象
  5. Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
  6. //修改访问权限
  7. constructor.setAccessible(true);
  8. //获取一个单例对象
  9. Singleton instance = Singleton.getInstance();
  10. Singleton newInstance = constructor.newInstance();
  11. System.out.println(newInstance == instance);
  12. }
  13. }
  14. class Singleton{
  15. private Singleton(){
  16. }
  17. private final static Singleton INSTANCE = new Singleton();
  18. public static Singleton getInstance(){
  19. return INSTANCE;
  20. }
  21. }

设计模式 (下) - 图17

由于反射可以创建出对象,因此我们可以在构造器中写一些防御反射的代码

  1. class Singleton{
  2. private Singleton(){
  3. /*这种防御反射攻击的方法,对饿汉式是有效的
  4. * 因为饿汉式的单例对象在类加载时就已经初始化了
  5. **/
  6. if(INSTANCE != null){
  7. throw new RuntimeException("单例构造器禁止反射调用");
  8. }
  9. }
  10. private final static Singleton INSTANCE = new Singleton();
  11. public static Singleton getInstance(){
  12. return INSTANCE;
  13. }
  14. }

设计模式 (下) - 图18

对于 INSTANCE 属性而言,由于其实静态的,所以在类初始化时就完成了加载,并且由于 JVM 保证了类初始化的线程安全性,所以也不会有多线程的问题存在,所以只要 INSTANCE 不为空就说明单例对象已经存在了,便不允许再创建

静态内部类单例模式下的代码

  1. public class Test {
  2. public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
  3. //静态内部类获取一个单例对象
  4. StaticSingleton staticInstance = StaticSingleton.getInstance();
  5. //通过反射获得构造器对象
  6. Class<StaticSingleton> staticSingletonClass = StaticSingleton.class;
  7. Constructor<StaticSingleton> constructor2 = staticSingletonClass.getDeclaredConstructor();
  8. constructor2.setAccessible(true);
  9. StaticSingleton newInstance2 = constructor2.newInstance();
  10. System.out.println(newInstance2 == staticInstance);
  11. }
  12. }
  13. class StaticSingleton{
  14. private StaticSingleton(){
  15. /*这种防御反射攻击的方法,对饿汉式和静态内部类是有效的
  16. * 因为对象在类加载时就已经初始化了
  17. **/
  18. if(GetSingleton.INSTANCE != null){
  19. throw new RuntimeException("单例构造器禁止反射调用");
  20. }
  21. }
  22. /**
  23. * 静态内部类
  24. */
  25. private static class GetSingleton{
  26. private static final StaticSingleton INSTANCE = new StaticSingleton();
  27. }
  28. public static StaticSingleton getInstance(){
  29. return GetSingleton.INSTANCE;
  30. }
  31. }

设计模式 (下) - 图19

只要我们调用了构造单例对象的方法,就会初始化内部类及其静态属性中的单例对象,并将其返回,在获取单例对象后如果还想用反射来创建,则直接报错;如果我们先利用反射来创建其对象,那也会触发私有构造器中的判断条件,从而初始化静态内部类及其单例对象属性,此时 GetSingleton.INSTANCE 也就不为 null 了,所以在这个例子中私有构造器中的判断条件是永真的

对于不是在类加载时创建单例对象的单例模式中,又该如何做呢?

这里以懒汉式举例

  1. public class Test {
  2. public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
  3. //通过反射获得对象
  4. Class<LazySingleton> lazySingleton = LazySingleton.class;
  5. Constructor<LazySingleton> constructor = lazySingleton.getDeclaredConstructor();
  6. constructor.setAccessible(true);
  7. LazySingleton singleton = constructor.newInstance();
  8. //获得单例对象
  9. LazySingleton instance = LazySingleton.getInstance();
  10. System.out.println(singleton == instance);
  11. }
  12. }
  13. class LazySingleton{
  14. private static LazySingleton instance;
  15. private LazySingleton(){
  16. if(instance != null){
  17. throw new RuntimeException("不允许构造器创建单例对象");
  18. }
  19. }
  20. public static synchronized LazySingleton getInstance(){
  21. if(instance == null){
  22. return instance = new LazySingleton();
  23. }
  24. return instance;
  25. }
  26. }

很明显,饿汉式和静态内部类的防御反射攻击的方法显然无法用到这里。因为懒汉式只有在调用getInstance() 时才会去初始化单例对象,因此在使用反射时如果没有在之前没有调用初始化单例对象的方法,显然 instance 就是为 null,是不会抛出异常的。

设计模式 (下) - 图20

我们来调换一下

  1. LazySingleton singleton = constructor.newInstance();
  2. //获得单例对象
  3. LazySingleton instance = LazySingleton.getInstance();

这两句话的顺序,再次执行

设计模式 (下) - 图21

因此,对于懒汉式来说,反射攻击是无法避免的 (存疑)

百度了好几圈,找到个可能的解决方案,思路是:获取当前调用栈的方法名,判断是不是和单例模式中的全局访问点方法同名

  1. public class Test {
  2. public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
  3. //通过反射获得对象
  4. Class<LazySingleton> lazySingleton = LazySingleton.class;
  5. Constructor<LazySingleton> constructor = lazySingleton.getDeclaredConstructor();
  6. constructor.setAccessible(true);
  7. LazySingleton singleton = constructor.newInstance();
  8. //获得单例对象
  9. LazySingleton instance = LazySingleton.getInstance();
  10. System.out.println(instance);
  11. }
  12. }
  13. class LazySingleton{
  14. private static LazySingleton instance;
  15. private LazySingleton(){
  16. //懒加载下防止反射攻击的可能方案
  17. if (!Thread.currentThread().getStackTrace()[2].getMethodName().equals("getInstance")) {
  18. throw new RuntimeException("不允许创建单例对象");
  19. }
  20. }
  21. public static synchronized LazySingleton getInstance(){
  22. if(instance == null){
  23. return instance = new LazySingleton();
  24. }
  25. return instance;
  26. }
  27. }

枚举(推荐)————

单例模式的唯一推荐方案,可以天然防止序列化破坏和反射攻击,简直是无敌的象征

  1. public class EnumSingleton {
  2. public static void main(String[] args) throws IOException, ClassNotFoundException {
  3. Singleton instance = Singleton.INSTANCE;
  4. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
  5. oos.writeObject(instance);
  6. File file = new File("singleton_file");
  7. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
  8. Singleton newInstance = (Singleton) ois.readObject();
  9. System.out.println(instance == newInstance);
  10. }
  11. }
  12. /**
  13. * 使用枚举实现单例模式,不仅能避免多线程的同步问题,
  14. * 而且能防止反序列化重新创建新的对象
  15. */
  16. enum Singleton{
  17. INSTANCE;
  18. }

设计模式 (下) - 图22

那为什么 Enum 能够防止序列化破坏呢?让我们进入源码一探究竟

进入设计模式 (下) - 图23

ObjectInputStream 类中,搜索 readEnum

设计模式 (下) - 图24

可以看到就在我们探究序列化破坏单例的原理时所找到的那个方法的上面,进入 readEnum 方法

设计模式 (下) - 图25

en 通过枚举名和 Class 对象获取到枚举常量,由于枚举中的枚举名是唯一的,并且只对应一个枚举常量,因此 en 获得的一定是唯一的对象,维持了单例对象的唯一性

同样,我们来看下反射能否破坏枚举形式的单例

  1. public class EnumSingleton {
  2. public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
  3. Class<Singleton> singletonClass = Singleton.class;
  4. //通过反射获得构造器对象
  5. Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
  6. constructor.setAccessible(true);
  7. //获取一个单例对象
  8. Singleton instance = Singleton.INSTANCE;
  9. Singleton newInstance = constructor.newInstance();
  10. System.out.println(newInstance == instance);
  11. }
  12. }
  13. /**
  14. * 使用枚举实现单例模式,不仅能避免多线程的同步问题,
  15. * 而且能防止反序列化和反射重新创建新的对象
  16. */
  17. enum Singleton{
  18. INSTANCE;
  19. }

设计模式 (下) - 图26

这里说的很明显,没有构造器,那我们进入 Enum 类中

设计模式 (下) - 图27

找到了这唯一的一个构造器,这明显的不是无参构造器,所以报错了

另一个思路是,通过 jad 将编译出来的 class 文件反编译回去

设计模式 (下) - 图28

可以发现构造器也是有两个参数的

那我们修改下获得构造器的反射方法,通过传入对应参数对象的 Class 对象,来获得对应的构造器

  1. public class EnumSingleton {
  2. public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
  3. Class<Singleton> singletonClass = Singleton.class;
  4. //通过反射获得构造器对象,通过传入 Class 对象指定构造器
  5. Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor(String.class, int.class);
  6. constructor.setAccessible(true);
  7. //饿汉式获取一个单例对象
  8. Singleton instance = Singleton.INSTANCE;
  9. Singleton newInstance = constructor.newInstance("mhc", 666);
  10. System.out.println(newInstance == instance);
  11. }
  12. }
  13. /**
  14. * 使用枚举实现单例模式,不仅能避免多线程的同步问题,
  15. * 而且能防止反序列化重新创建新的对象
  16. */
  17. enum Singleton{
  18. INSTANCE;
  19. }

设计模式 (下) - 图29

这个报错就很明显了,不能通过反射去创建枚举对象

进入报错的 417 行源码中

设计模式 (下) - 图30

可以发现,如果想要使用反射创建的实例是枚举类型,就会抛出异常,天然的防止了反射攻击

容器单例

容器单例的原理时在 Map 中,key 只能存在一份,后面将对象 put 进 map 的线程会覆盖原来的

  1. public class Test {
  2. public static void main(String[] args) {
  3. Thread t1 = new Thread(new Th());
  4. Thread t2 = new Thread(new Th());
  5. t1.start();
  6. t2.start();
  7. }
  8. }
  9. class ContainerSingleton{
  10. private static Map<String, Object> singletonMap = new HashMap<>();
  11. private ContainerSingleton(){
  12. }
  13. public static void putInstance(String key, Object instance){
  14. if(StringUtils.isNotBlank(key) && instance != null){
  15. if(!singletonMap.containsKey(key)){
  16. singletonMap.put(key, instance);
  17. }
  18. }
  19. }
  20. public static Object getInstance(String key){
  21. return singletonMap.get(key);
  22. }
  23. }
  24. class Th implements Runnable{
  25. @Override
  26. public void run() {
  27. ContainerSingleton.putInstance("object", new Object());
  28. Object object = ContainerSingleton.getInstance("object");
  29. System.out.println(Thread.currentThread().getName()+" "+object);
  30. }
  31. }

HashMap 虽然不是线程安全的,但也不建议使用线程安全的 HashTable,因为同步锁会造成较大的性能损耗

总结

单例模式保证了系统内存中该类只存在一个对象,节省了系统资源。对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能

当想要实例化一个单例类的时候,必须要将构造器私有化,并使用相应的方法获取对象,而不是直接 new

单例模式的使用场景

  • 需要频繁的进行创建和销毁的对象
  • 创建时耗时过多或耗费资源过多 ( 重量级对象 ),但又经常用到的对象
  • 工具类对象,频繁访问数据库或文件的对象 ( 数据源,sessionFactory 等 )

工厂模式

简单工厂模式

先来看一个具体需求

有一个卖披萨的食品店,其管理系统要求如下

  • 要便于披萨种类的扩展,要便于维护
  • 披萨的种类很多
  • 披萨的制作流程有:prepare,bake,cut,box
  • 披萨店有订购功能

先尝试用已经学过的 OO 思想来画出这个需求的类图

设计模式 (下) - 图31

首先,通过抽取出共有的方法成为一个抽象类 Pizza,然后再根据情况由子类实现

  1. public class PizzaStore {
  2. public static void main(String[] args) {
  3. //发出披萨订购任务
  4. new PizzaStore().OrderPizza();
  5. }
  6. public void OrderPizza(){
  7. Pizza pizza = null;
  8. String orderType;
  9. do {
  10. System.out.print("输入订购的pizza种类:");
  11. orderType = new Scanner(System.in).next();
  12. if("greek".equals(orderType)){
  13. pizza = new GreekPizza();
  14. pizza.setName("希腊");
  15. }else if("cheese".equals(orderType)){
  16. pizza = new ChessPizza();
  17. pizza.setName("起司");
  18. }else{
  19. break;
  20. }
  21. pizza.prepare();
  22. pizza.bake();
  23. pizza.cut();
  24. pizza.box();
  25. }while(true);
  26. }
  27. }
  28. abstract class Pizza{
  29. protected String name;
  30. void prepare() {
  31. System.out.println("准备"+name+"披萨的原材料");
  32. }
  33. void bake() {
  34. System.out.println(name+"披萨进行烘焙");
  35. }
  36. void cut() {
  37. System.out.println(name+"披萨进行切分");
  38. }
  39. void box() {
  40. System.out.println(name+"披萨进行装箱");
  41. }
  42. public void setName(String name){
  43. this.name = name;
  44. }
  45. }
  46. class ChessPizza extends Pizza{
  47. }
  48. class GreekPizza extends Pizza{
  49. }

这种方式违背了 ocp 原则 ( 面向修改关闭,面向扩展开放 ),当增加一个新的 Pizza 类时,我们需要去修改使用处的代码

  1. if("greek".equals(orderType)){
  2. pizza = new GreekPizza();
  3. pizza.setName("希腊");
  4. }else if("cheese".equals(orderType)){
  5. pizza = new ChessPizza();
  6. pizza.setName("起司");
  7. }else{
  8. break;
  9. }

如果仅仅只有一处代码需要修改,那么也不是不能接受,但如果我们在其它的地方也用到了创建 Pizza 的代码,就意味着这些地方也需要进行修改,显然这样做是不合理的

因此,我们可以把创建 Pizza 对象的方法封装到一个类中,这样增加新的 Pizza 种类时,只需要修改该类即可,其它使用该类创建的 Pizza 对象的代码无需再修改,这就是简单工厂模式

介绍

简单工厂模式属于创建型模式,是工厂模式的一种。简单工厂模式由一个工厂对象决定创建出哪一种产品类的实例

简单工厂模式:定义一个创建对象的类,由这个类来封装实例化对象的代码

设计模式 (下) - 图32

简单工厂类

  1. public class SimplePizzaFactory {
  2. /**
  3. *
  4. * @param orderType: 订购的pizza类型
  5. * @return:通过pizza类型创建的pizza对象
  6. */
  7. public static Pizza createPizza(String orderType){
  8. Pizza pizza = null;
  9. if("greek".equals(orderType)){
  10. pizza = new GreekPizza();
  11. pizza.setName("希腊");
  12. }else if("cheese".equals(orderType)){
  13. pizza = new ChessPizza();
  14. pizza.setName("起司");
  15. }else if("prepper".equals(orderType)){
  16. pizza = new HeiHuJiao();
  17. pizza.setName("黑胡椒");
  18. }
  19. return pizza;
  20. }
  21. }

披萨店类

  1. public class PizzaStore {
  2. public static void main(String[] args) {
  3. new PizzaStore().orderPizza();
  4. }
  5. public void orderPizza(){
  6. Scanner scanner = new Scanner(System.in);
  7. do {
  8. System.out.print("请输入要什么pizza:");
  9. String orderType = scanner.next();
  10. Pizza pizza = SimplePizzaFactory.createPizza(orderType);
  11. if(pizza != null){
  12. pizza.prepare();
  13. pizza.bake();
  14. pizza.cut();
  15. pizza.box();
  16. }else{
  17. System.out.println("没这种pizza");
  18. break;
  19. }
  20. }while (true);
  21. }
  22. }
  23. abstract class Pizza{
  24. protected String name;
  25. void prepare() {
  26. System.out.println("准备"+name+"披萨的原材料");
  27. }
  28. void bake() {
  29. System.out.println(name+"披萨进行烘焙");
  30. }
  31. void cut() {
  32. System.out.println(name+"披萨进行切分");
  33. }
  34. void box() {
  35. System.out.println(name+"披萨进行装箱");
  36. }
  37. public void setName(String name){
  38. this.name = name;
  39. }
  40. }
  41. class ChessPizza extends Pizza {
  42. }
  43. class GreekPizza extends Pizza {
  44. }
  45. class HeiHuJiao extends Pizza{
  46. }

在简单工厂模式下,当我们增加披萨的种类时,我们只需要去修改工厂类即可,而无需大规模的改动需要创建 Pizza 对象的地方

工厂方法模式

定义一个创建对象的接口 / 抽象类,但让实现这个接口类来实现方法进而决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行

适用场景

  • 创建对象需要大量重复的代码
  • 客户端 (应用层) 不依赖于产品类实例如何被创建,实现等细节。客户端只需要知道产品在哪个工厂里
  • 一个类通过其子类来指定创建哪个对象

优点

  • 用户只需要关心所需产品对应的工厂,无需关系创建细节
  • 加入新的产品符合开闭原则,提高可扩展性

缺点

  • 类的个数容易过多,增加复杂度 (增加新的产品类需要增加新的对应的工厂类)
  • 增加了系统的抽象性和理解难度

继续用卖披萨的例子,胡椒披萨,起司披萨,希腊披萨都是相同的 “产品等级”

工厂方法就是为了解决同一产品等级的产品的业务抽象问题

我们可以将如何创建对象通过工厂方法模式分解成下面这样,其实就是每一个产品对应一个工厂

/**
 * 只定义规范
 */
abstract public class PizzaFactory {

    /**
     * 
     * @param orderType:披萨类型
     * @return
     */
    abstract public Pizza createPizza();

}

class ChessPizzaFactory extends PizzaFactory{

    @Override
    public Pizza createPizza() {
        ChessPizza chessPizza = new ChessPizza();
        chessPizza.setName("起司");
        return chessPizza;
    }
}

class GreekPizzaFactory extends PizzaFactory{


    @Override
    public Pizza createPizza() {
        GreekPizza greekPizza = new GreekPizza();
        greekPizza.setName("希腊");
        return greekPizza;
    }
}

class HeiHuJiaoPizzaFactory extends PizzaFactory{

    @Override
    public Pizza createPizza() {
        HeiHuJiao heiHuJiao = new HeiHuJiao();
        heiHuJiao.setName("黑胡椒");
        return heiHuJiao;
    }
}

同样,修改下客户端的代码

public class Test {
    public static void main(String[] args) {

        PizzaFactory pizzaFactory = new ChessPizzaFactory();
        Pizza pizza = pizzaFactory.createPizza();

        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
    }
}

这样在创建所需要的 Pizza 对象时,我们只需要修改子类工厂为对应的工厂类即可

来看下类图

设计模式 (下) - 图33

抽象工厂模式

抽象工厂模式提供一个创建一系列相关或相互依赖的对象的接口 / 抽象类

优点

  • 将一个系列的产品族统一到一起创建

缺点

  • 规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口
  • 增加了系统的抽象性和理解难度

产品等级和产品族

工厂方法解决的是产品等级的问题

抽象工厂解决的是产品族的问题

产品等级和产品族

  • 产品等级:工厂方法模式针对的是产品等级,产品等级结构是指不同工厂生产出的同一类产品。产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,这几个子类就是相同的产品等级。抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类

  • 产品族:抽象工厂针对的是产品族,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中,海尔电视机、海尔电冰箱构成了一个产品族

我们只要在美的的工厂里面取出空调,就一定是美的的空调,同理只要在美的的工厂里面取出冰箱就一定是美的的冰箱

设计模式 (下) - 图34

再修改一下披萨的案例。原来我们的披萨店卖的披萨没有配料,现在搞促销了,每份披萨会附赠一份对应的配料

胡椒披萨,希腊披萨,起司披萨 都是同一产品等级,都是披萨

胡椒配料,胡椒披萨 都是同一产品族,都是胡椒工厂生产的

修改上面的 pizza 案例后,结构如下

设计模式 (下) - 图35

Factory 就是抽象工厂的接口,其中规范了能够获得该工厂产品族的哪些产品

public interface Factory {

    PeiLiao getPeiLiao();
    Pizza getPizza();
}

对应的 ChessFactoey,GreekFactory,HeiHuJiaoFactory,就是其子类

比如:工厂接口规定了现在只能生产 pizza 及其配料 ( 我们可以称之为产品线 ),这二者就称为同一产品族,实现了工厂接口的子类就需要去生产这两个产品

在拓展性方面

  • 优点:如果想要新增一个产品 (用已有产品线可以生产的),比如奥尔良披萨,只需要创建对应的工厂类去实现工厂类接口 / 抽象类,然后新增的产品类去实现其对应的接口 / 抽象类即可
  • 缺点:如果想要新增一个产品线 (产品等级),比如辣椒酱,就需要在接口中新增一个返回产品线对应的产品类的方法,然后所有的子类都需要去实现这个方法,不符合 ocp 原则

工厂方法模式和抽象工厂模式区别

工厂方法模式:

  • 一个抽象产品类,可以派生出多个具体产品类
    一个抽象工厂类,可以派生出多个具体工厂类
    每个具体工厂类只能创建一个具体产品类的实例(产品等级)

抽象工厂模式:

  • 多个抽象产品类,每个抽象产品类可以派生出多个具体产品类

  • 一个抽象工厂类,可以派生出多个具体工厂类
    每个具体工厂类可以创建多个具体产品类的实例(产品族)

区别:

  • 工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个

  • 工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个

  • 抽象工厂针对的是产品族,工厂方法针对的是产品等级

建造者模式

讲一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。主要用于创建一些复杂的对象,这些对象内部构建间的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化

用户只需指定需要建造的类型,无需知道过程和实现细节

适用场景

  • 如果一个对象有非常复杂的内部结构 (很多属性)
  • 想把复杂对象的创建和使用分离

优点和缺点

  • 扩展性好,建造类之间独立,一定程度上解耦
  • 封装性好,创建和使用分离
  • 产生多余的 Builder 对象
  • 产品内部发生变化,所有的建造者都要修改,成本可能比较大

建造者模式和工厂模式的区别

  • 建造者模式更注重于方法的调用顺序;工厂模式更注重于对象的创建
  • 建造者模式可以创建更复杂的对象,由不同的组件组成;而工厂模式中一个工厂类创建出来的对象都是一样的
  • 工厂模式注重于结果 (对象的创建);建造者模式注重过程 (如何创建对象)

接下来模拟一个场景来讲解建造者模式,比如:我们需要 “建造” 一个视频课程

首先,我们需要一个课程类,它应该具有一些视频的通用属性

public class Course {
    private String courseName;
    private String coursePPT;
    private String courseVedio;
    private String courseArticle;
    private String courseQA;
}

这里省略 getter 和 setter 方法

其次,我们需要一个抽象的建造类,这个抽象类规范了创建一个课程所需要创建的组件 (属性)

abstract public class CourseBuilder {

    abstract public void buildCourseName(String courseName);
    abstract public void buildCoursePPT(String coursePPT);
    abstract public void buildCourseVeido(String courseVeido);
    abstract public void buildCourseArticle(String courseArticle);
    abstract public void buildCourseQA(String courseQA);

    abstract public Course makeCourse();

}

然后,我们再创建一个实际的建造类,这个类继承抽象建造类,实现创建一个课程所需要的组件及其创建逻辑

public class CourseActualBuilder extends CourseBuilder {

    private Course course = new Course();

    @Override
    public void buildCourseName(String courseName) {
        course.setCourseName(courseName);
    }

    @Override
    public void buildCoursePPT(String coursePPT) {
        course.setCoursePPT(coursePPT);
    }

    @Override
    public void buildCourseVeido(String courseVeido) {
        course.setCourseVedio(courseVeido);
    }

    @Override
    public void buildCourseArticle(String courseArticle) {
        course.setCourseArticle(courseArticle);
    }

    @Override
    public void buildCourseQA(String courseQA) {
        course.setCourseQA(courseQA);
    }

    @Override
    public Course makeCourse() {
        return course;
    }
}

最后,我们需要一个指挥者类,由这个类来指挥如何建造课程对象

我们将参数传递给指挥者类,再由指挥者类指挥 (调用) 建造者,最后将建造好的课程对象返回给客户端

public class Coach {
    private CourseBuilder courseBuilder;

    public void setCourseBuilder(CourseBuilder courseBuilder) {
        this.courseBuilder = courseBuilder;
    }

    public Course makeCourse(String courseName, String coursePPT,
                             String courseVedio, String courseArticle,
                             String courseQA){

        this.courseBuilder.buildCourseName(courseName);
        this.courseBuilder.buildCoursePPT(coursePPT);
        this.courseBuilder.buildCourseVeido(courseVedio);
        this.courseBuilder.buildCourseArticle(courseArticle);
        this.courseBuilder.buildCourseQA(courseQA);

        return this.courseBuilder.makeCourse();
    }
}

Q: 为什么需要指挥者类呢

A: 它用来控制建造过程,也用它来隔离用户与建造过程的关联。比如我们建造汽车,轮子是其中的一个组件,但是突然有了未来汽车,不需要轮子了,这样就只需要再定义一个指挥类来建造不需要轮子的汽车即可,建造过程只需要去掉建造轮子的步骤即可

最最后,我们创建测试类来进行测试

public class Test {
    public static void main(String[] args) {

        //创建建造者对象
        CourseBuilder courseBuilder = new CourseActualBuilder();

        //创建指挥者
        Coach coach = new Coach();

        //指挥者只会哪个建造者
        coach.setCourseBuilder(courseBuilder);

        //通过指挥者获得课程对象
        Course course = coach.makeCourse("Java", "JavaPPT",
                "JavaVedio", "Java笔记",
                "Java问答");

        System.out.println(course);
    }
}

设计模式 (下) - 图36

接下来我们写一个改进版本,我们将实体类和对应的建造者类放在一个类当中,建造者类拥有实体类的所有属性,这样就可以形成链式调用

public class Course {

    private String courseName;
    private String coursePPT;
    private String courseVedio;
    private String courseArticle;
    private String courseQA;

    public Course(CourseBuilder courseBuilder){
        this.courseName = courseBuilder.courseName;
        this.coursePPT = courseBuilder.coursePPT;
        this.courseArticle = courseBuilder.courseArticle;
        this.courseVedio = courseBuilder.courseVedio;
        this.courseQA = courseBuilder.courseQA;
    }

    @Override
    public String toString() {
        return "Course{" +
                "courseName='" + courseName + '\'' +
                ", coursePPT='" + coursePPT + '\'' +
                ", courseVedio='" + courseVedio + '\'' +
                ", courseArticle='" + courseArticle + '\'' +
                ", courseQA='" + courseQA + '\'' +
                '}';
    }

    public  static class CourseBuilder{

        private String courseName;
        private String coursePPT;
        private String courseVedio;
        private String courseArticle;
        private String courseQA;


        public CourseBuilder buildCourseName(String courseName) {
            this.courseName = courseName;
            return this;
        }

        public CourseBuilder buildCoursePPT(String coursePPT) {
            this.coursePPT = coursePPT;
            return this;
        }

        public CourseBuilder buildCourseVeido(String courseVeido) {
            this.courseVedio = courseVeido;
            return this;
        }

        public CourseBuilder buildCourseArticle(String courseArticle) {
            this.courseArticle = courseArticle;
            return this;
        }

        public CourseBuilder buildCourseQA(String courseQA) {
            this.courseQA = courseQA;
            return this;
        }

        public Course build(){
            return new Course(this);
        }

    }
}

再写一个测试类

public class Test {
    public static void main(String[] args) {

        //链式调用
        Course course = new Course.CourseBuilder().
                buildCourseName("Java").buildCourseVeido("Java视频")
                .buildCourseArticle("Java笔记").buildCoursePPT("JavaPPT")
                .buildCourseQA("Java问答").build();

        System.out.println(course);
    }
}

设计模式 (下) - 图37

在链式调用中,我们可以按需调用,并且是链式调用的,最后调用 build 方法建造出需要的对象即可

查看这个改进版本的类图

设计模式 (下) - 图38

可以看见,客户端 Test 只需要去指定使用哪个建造者即可,而无需和 Course 类发生过度的耦合

原型模式

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节,无需调用构造函数

适用场景

  • 类初始化消耗资源过多
  • new 产生的一个对象需要非常繁琐的过程
  • 构造函数比较复杂
  • 循环中生产大量对象

优缺点

优点

  • 原型模式在性能上比直接 new 一个对象性能高
  • 简化创建过程

缺点

  • 必须重写 Object 的克隆方法
  • 对克隆复杂对象或对克隆出的对象进行复杂改造时,容易引入风险
  • 深拷贝,浅拷贝要运用得当

    • 对于引用类型,如果需要他们指向不同的对象,就需要深拷贝

原型模式包含以下主要角色

  1. 抽象原型类:规定了具体原型对象必须实现的接口
  2. 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象
  3. 访问类:使用具体原型类中的 clone() 方法来复制新的对象

coding

首先,假设一个场景:现在有一只羊,叫肖恩,年龄为100,颜色为尼哥色,请编写程序创建和肖恩属性完全相同的10只羊

传统模式

我们先来看下传统模式下我们该如何解决这个问题

很简单,我们只需要一个 Sheep 类,然后用这个类创建 10 个对象即可

Sheep 类

public class Sheep {
    private String name;
    private Integer age;
    private String color;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "Sheep{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", color='" + color + '\'' +
                '}';
    }
}

Test 类 (客户端)

public class Test {
    public static void main(String[] args) {


        Sheep sheep = new Sheep("肖恩", 100, "尼哥色");
        Sheep sheep2 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor());
        Sheep sheep3 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor());
        //......

        System.out.println(sheep);
        System.out.println(sheep2);
        System.out.println(sheep3);
        //......
    }
}

在这个传统版本下

  • 在创建新对象时,总是要重新获取原始对象的属性来创建新的对象,如果创建的对象比较复杂时,效率较低
  • 总是需要重新初始化对象,而不是动态的获得对象运行时的状态,不够灵活

我们可以通过实现 Object 类提供的 clone() 方法,该方法可以将一个 Java 对象复制一份,但是想要能够使用这个方法的 Java 类必须实现 Cloneable 接口,该接口表示该类能够被复制。

改进模式

我们先来看下原型模式的类图

设计模式 (下) - 图39

Prototype:抽象原型类,声明一个克隆自己的接口

ConcretePrototype:具体原型类,实现抽象原型类的 clone() 方法,是可被复制的对象

client:即客户端,使用具体原型类中的 clone() 方法来复制新的对象

了解了这些过后,接下来我们就来修改下 Sheep 类

public class Sheep implements Cloneable{
    private String name;
    private Integer age;
    private String color;

    public Sheep(String name, Integer age, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }

    public Sheep(){}

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "Sheep{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", color='" + color + '\'' +
                '}';
    }

    /**
     * 克隆该实例,通过重写默认的clone方法完成
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() {

        Sheep sheep = null;
        try {
            sheep = (Sheep) super.clone();
        }catch (CloneNotSupportedException cse){
            System.out.println(cse.getMessage());
        }
        return sheep;
    }
}

首先,我们让 Sheep 类实现了一个 Cloneable 接口,对于这个 Sheep 类而言,这个接口就是抽象原型类。然后我们重写 clone() 方法即可

客户端的调用就变成了

public class Client {
    public static void main(String[] args) {

        Sheep sheep = new Sheep("肖恩", 100, "尼哥色");
        Sheep sheep2 = (Sheep) sheep.clone();
        Sheep sheep3 = (Sheep) sheep.clone();
        Sheep sheep4 = (Sheep) sheep.clone();
        //......

        System.out.println(sheep);
        System.out.println(sheep2);
        System.out.println(sheep3);
        System.out.println(sheep4);
    }
}

我们只需要调用具体原型类的 clone() 方法,然后强转一下即可

思考一下,修改过后的案例相比于传统模式有何优点?

  • 假如我们要新增一个属性,并且在构造克隆羊时进行赋值

    • 对于传统模式而言,我们就需要修改所有初始化了克隆羊的地方:在给原型对象的构造方法传入参数后,又要把这个新增的属性又增加入到其它克隆对象的初始化处。较大程度的违反了 ocp 原则 ```java Sheep sheep = new Sheep(“肖恩”, 100, “尼哥色”, “蒙古”);

Sheep sheep2 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor(), sheep.getAddres());

Sheep sheep3 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor(), sheep.getAddress());


   - 对于改进版的原型模式而言,我们只需要在 Sheep 类中增加这个属性,然后在原型对象的初始化处增加对于属性值即可,而无需改动其它的克隆对象
```java
Sheep sheep = new Sheep("肖恩", 100, "尼哥色", "蒙古");

Sheep sheep2 = (Sheep) sheep.clone();
Sheep sheep3 = (Sheep) sheep.clone();
Sheep sheep4 = (Sheep) sheep.clone();

探究深拷贝和浅拷贝

浅拷贝

对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象

对于数据类型是引用类型的成员变量,浅拷贝会进行引用传递,也就是说只会将该成员变量的引用值 (地址值) 复制一份给新的对象。最终的结果就导致两个对象的该成员变量都指向了同一个实例。在这种情况下,在一个对象中修改该成员变量就会影响到另一个对象的该成员变量值

在我们上面所做的原型模式中,所使用的 clone 方法就是浅拷贝

我们尝试输出

System.out.println(sheep.getName() == sheep2.getName());

设计模式 (下) - 图40

结果为 true,说明他们引用实际上是同一个地址上的字符串

我们再在 Sheep 类中新增一个 Sheep 类型的成员变量命名为 friend,增加其 getter 和 setter 方法,来测试下是否会如我们所说的改变

客户端修改为如下

public class Client {
    public static void main(String[] args) {

        Sheep sheep = new Sheep("肖恩", 100, "尼哥色", "蒙古");
        //一开始先将引用类型成员赋值为一个没有属性值的对象
        sheep.setFriend(new Sheep());
        Sheep sheep2 = (Sheep) sheep.clone();
        Sheep sheep3 = (Sheep) sheep.clone();
        Sheep sheep4 = (Sheep) sheep.clone();
        //......

        //获得原型对象的引用类型成员,并设置其内部属性
        sheep.getFriend().setAge(19);
        System.out.println(sheep2);
        System.out.println(sheep3);
        System.out.println(sheep4);
    }
}

设计模式 (下) - 图41

可以看见,所有克隆羊的朋友都被修改了

深拷贝

复制原型对象的所有基本数据类的成员变量值

为所有引用类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象。即深拷贝要对整个原型对象进行拷贝

深拷贝有两种常见的实现方式

  • 重写 clone 方法来实现深拷贝
  • 通过对象序列化来实现深拷贝

重写 clone 方法实现深拷贝

我们先建立一个我们所需要的被引用类

public class DeepCloneableTarget implements Cloneable {

    private String cloneName;
    private String cloneClass;

    public DeepCloneableTarget(String cloneName, String cloneClass) {
        this.cloneName = cloneName;
        this.cloneClass = cloneClass;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

这个类需要实现 Cloneable 这两个接口

再来建立一个具体原型类

public class DeepPrototype implements Cloneable {

    private String name;
    private DeepCloneableTarget deepCloneableTarget;

    //使用 clone 方法实现深拷贝

    @Override
    protected Object clone() throws CloneNotSupportedException {

        Object deep = null;

        //这里完成对基本数据类型的复制
        deep = super.clone();

        //对引用类型的属性进行单独处理;
        //引用类型把自己克隆下来后交给具体原型类中对象的属性
        DeepPrototype deepPrototype = (DeepPrototype)deep;
        deepPrototype.deepCloneableTarget = (DeepCloneableTarget) deepCloneableTarget.clone();

        return deepPrototype;
    }
}

在这个类中,我们只有两个属性,一个 String 类型,一个引用类型,并且在 clone 方法中实现二者的克隆

最后编写客户端类进行测试

public class Client {
    public static void main(String[] args) throws CloneNotSupportedException {

        DeepPrototype dp = new DeepPrototype();
        dp.setName("燕双鹰");
        dp.setDeepCloneableTarget(new DeepCloneableTarget("燕双鹰的自行车", "燕双鹰的枪"));

        //完成深拷贝
        DeepPrototype cloneDp = (DeepPrototype) dp.clone();
        //比较引用类型是否是同一个
        System.out.println(cloneDp.getDeepCloneableTarget() == dp.getDeepCloneableTarget());
    }
}

设计模式 (下) - 图42

最后发现引用类型不再是同一个

这种方式很大的一个弊端就是如果引用类型的属性很多的话,clone 方法会变得很臃肿

序列化实现深拷贝

还是根据上面的三个类来实现,不过现在需要实现的是 Serializable 接口

引用类

public class DeepCloneableTarget implements Serializable {

    private String cloneName;
    private String cloneClass;

    public DeepCloneableTarget(String cloneName, String cloneClass) {
        this.cloneName = cloneName;
        this.cloneClass = cloneClass;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public String getCloneName() {
        return cloneName;
    }

    public void setCloneName(String cloneName) {
        this.cloneName = cloneName;
    }

    public String getCloneClass() {
        return cloneClass;
    }

    public void setCloneClass(String cloneClass) {
        this.cloneClass = cloneClass;
    }
}

具体原型类

public class DeepPrototype implements Serializable {

    private String name;
    private DeepCloneableTarget deepCloneableTarget;

    public DeepPrototype(String name, DeepCloneableTarget deepCloneableTarget) {
        this.name = name;
        this.deepCloneableTarget = deepCloneableTarget;
    }

    public DeepPrototype(){}

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public DeepCloneableTarget getDeepCloneableTarget() {
        return deepCloneableTarget;
    }

    public void setDeepCloneableTarget(DeepCloneableTarget deepCloneableTarget) {
        this.deepCloneableTarget = deepCloneableTarget;
    }

    //通过序列化实现 clone
    public Object deepClone(){

        //需要返回的反序列化出来的对象
         Object deepCopy = null;
        //创建流对象
        ByteArrayOutputStream bos = null;
        ObjectOutputStream oos = null;

        ByteArrayInputStream bis = null;
        ObjectInputStream ois = null;

        try{
            //序列化操作
            bos = new ByteArrayOutputStream();
            //指定接下来的输出的对象输出到哪个流中
            oos = new ObjectOutputStream(bos);
            //把当前对象以对象流的方式输出到bos中
            oos.writeObject(this);

            //反序列化
            //获得bos字节数组内容
            bis = new ByteArrayInputStream(bos.toByteArray());
            //将字节输入流内容转到对象输入流
            ois = new ObjectInputStream(bis);
            //读取出流中对象
            deepCopy = ois.readObject();

        }catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }finally {
            try {
                assert bos != null;
                bos.close();
                assert oos != null;
                oos.close();
                assert bis != null;
                bis.close();
                assert ois != null;
                ois.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return deepCopy;
    }
}

客户端

public class Client {
    public static void main(String[] args) throws CloneNotSupportedException {

        DeepPrototype dp = new DeepPrototype();
        dp.setName("燕双鹰");
        dp.setDeepCloneableTarget(new DeepCloneableTarget("燕双鹰的自行车", "燕双鹰的枪"));

        //完成深拷贝
        DeepPrototype cloneDp = (DeepPrototype) dp.deepClone();
        System.out.println(dp.getName()+"  "+cloneDp.getName());
        //比较引用类型是否是同一个
        System.out.println(cloneDp.getDeepCloneableTarget() == dp.getDeepCloneableTarget());
    }
}

设计模式 (下) - 图43

所以使用序列化的方式进行 clone,引用类型成员也被克隆了,并且这种方式无须在意引用类型成员的数量,所以这种方式在大部分情况下优于基于重写 clone() 方法而实现的深拷贝

注意事项和细节

  • 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提升效率
  • 不用重新初始化对象,而是随着克隆原型对象的时机来动态的获得对象运行时状态
  • 如果原始对象发生变化,其它克隆对象也会发生相应的变化,无需修改代码

缺点:需要为每一个类配备一个克隆方法,如果想让已有的类支持克隆,则需要修改源码,违反了 OCP 原则

适配器模式

现实中的适配器例子

不同国家用的电源插座孔可能是不一样的,这时为了正常使用这些插座,我们就需要一个转换插头,这个转换插头就相当于适配器,即,将我们需要使用但是又不满足要求的东西转换成既满足我们要求,又能使用的类型。

基本介绍

  • 适配器模式将某个类的接口转换成客户端期望的另一个接口来表示,主要的目的是兼容。让原本不能一起工作的两个类可以协同工作
  • 适配器属于结构型模式
  • 适配器模式主要分为三类:类适配器,对象适配器,接口适配器

工作原理

  • 将一个类的接口转换成另一种接口,让原本不兼容的类可以兼容

  • 对客户端而言适配过程是透明的,看不到被适配者

  • 用户调用适配器转换出来的目标方法接口,适配器再调用被适配者的相关接口方法
    设计模式 (下) - 图44

在上面的插座案例中,不同国家的电源插座就是被适配者,我们的转换插头就是适配器,而最终输出的目标就是我们需要插在转接头上的电器

类适配器

生活中我们给手机充电时,不能直接接上电源,因为电源的电压是 220v,此时就需要用手机充电器来将电源电压转换成能够给我们手机充电的电压 (假设是 5v )。在这个例子中,充电器就是 Adapter (适配器) ,220v 的电压就是被适配者,5v电压就是我们最终需要的输出

设计模式 (下) - 图45

先建立被适配者类

public class Voltage220V {

    public int output(){
        int src = 220;
        System.out.println("电压:"+src+"V");
        return src;
    }
}

然后建立适配接口 (让适配器能够将 220V 转为 5V)

public interface IVoltage5V {

    int output5v();
}

建立适配器类

public class VoltageAdapter extends Voltage220V implements IVoltage5V {
    @Override
    public int output5v() {
        //获取到 220v 电压
        int src = output220V();
        //转为 5v
        int des = src / 44;
        System.out.println("电压转为了:" + des + "V");
        return des;
    }
}

再建一个手机类

public class Phone {

    public void chongDian(IVoltage5V iVoltage5V){
        if(iVoltage5V.output5v() == 5){
            System.out.println("开始充电");
        }else if(iVoltage5V.output5v() > 5){
            System.out.println("充爆了");
        }else{
            System.out.println("电压不足,充得很慢");
        }
    }
}

最后建立客户端进行测试

public class Client {
    public static void main(String[] args) {

        Phone phone = new Phone();
        //我们只需要调用适配器,而无需关心内部是如何实现充电的
        phone.chongDian(new VoltageAdapter());
    }
}

设计模式 (下) - 图46

类适配器模式注意事项

  • Java 是单继承,所以累适配器需要继承被适配者类,所以又必须要求适配方法是来自接口的
  • 被适配者所有的类都会在适配器中出现

对象适配器

基本思路与类适配器相同,只是需要将 Adapter 进行修改,不再是继承被适配者类,而是持有被适配者类的实例,去掉继承关系,进行一定程度的解耦

以上面的案例为例,我们先改进适配器类

ublic class VoltageAdapter implements IVoltage5V {

    //持有被适配类
    private Voltage220V voltage220V;

    public VoltageAdapter(Voltage220V voltage220V) {
        this.voltage220V = voltage220V;
    }
    public VoltageAdapter(){}

     @Override
    public int output5v() {

        int des = 0;

        //获取到 220v 电压
        if(voltage220V != null){
            int src = voltage220V.output220V();
            //转为 5v
            des = src / 44;
        }
        System.out.println("电压通过对象适配器转为了:" + des + "V");
        return des;
    }
}

所以此时对于客户端而言,我们就需要依赖三个类:适配器类,被适配者类,Phone 类, 其实这也更符合我们的逻辑,如果我们想要给手机充电,那肯定我们是需要电源 (被适配者),充电器 (适配者),和手机的

修改客户端后再进行测试

public class Client {
    public static void main(String[] args) {

        Phone phone = new Phone();
        //我们只需要调用适配器,而无需关系内部是如何给我们进行充电的
        phone.chongDian(new VoltageAdapter(new Voltage220V()));
    }
}

设计模式 (下) - 图47

注意事项

对象适配器和类适配器思想其实并无大的差别,只不过实现方式不同。在对象适配器中,我们使用了组合来代替继承,实现了一定程度上的解耦

接口适配器

当适配器类不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每一个方法提供一个默认实现 (空方法),那么该抽象类的子类就可以有选择的覆盖父类中的某些方法来实现需求

我们先创建一个接口

public interface TestInterface {

    void m1();
    void m2();
    void m3();
    void m4();
    void m5();
}

然后建立一个抽象类,实现这个接口的所有方法为默认实现

abstract public class TestAbstract implements TestInterface {

    @Override
    public void m1() {

    }

    @Override
    public void m2() {

    }

    @Override
    public void m3() {

    }

    @Override
    public void m4() {

    }

    @Override
    public void m5() {

    }
}

这时,在客户端中,我们只需要 new 这个抽象类作为匿名内部类,然后重写我们需要的方法即可

public class Client {
    public static void main(String[] args) {

        TestAbstract ts = new TestAbstract() {
                                //只需要复写需要使用的接口方法即可
                                public void m1() {
                                    System.out.println("重写m1方法");
                                    ;
                                }
                            };

        ts.m1();
    }
}

注意事项和细节

适配器模式的三种实现方式,是根据被适配者是以何种方式注入到适配器类中而命名的

  • 类适配器:以类的形式注入到适配器类中,适配器类继承被适配类
  • 对象适配器:以对象的形式注入到适配器类中,适配器类持有被适配类的引用
  • 接口适配器:以接口的实现注入给适配器类,适配器类实现被适配接口