备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)

本人本科毕业,21届毕业生,一年工作经验,简历专业技能如下,现根据简历,并根据所学知识复习准备面试。

记录日期:2022.1.9

大部分知识点只做大致介绍,具体内容根据推荐博文链接进行详细复习。

文章目录

  • 设计模式-23种设计模式之结构型模式
    • 设计模式分类
      • 设计模式介绍
        • 创建型模式
        • 结构型模式
        • 行为型模式
      • 结构型模式
        • 适配器模式
          • 引入
          • 概念
          • 对象适配器
            • 模型结构图
            • 示例代码
          • 类适配器
            • 模型结构图
            • 示例代码
          • 缺省适配器
            • 引入
            • 概念
            • 示例代码
          • 模式优点
          • 模式缺点
          • 使用场景
          • 最佳示例
        • 装饰器模式
          • 概念
          • 模型结构图
          • 示例代码
          • 模式优点
          • 模式缺点
          • 使用场景
          • 最佳示例
            • JDK的IO流
            • Spring 的 TransactionAwareCacheDecorator
            • SpringMVC 的 HttpHeadResponseDecorator
            • MyBatis 的 Cache
        • 代理模式(重要)
          • 概念
          • 模型结构图
          • 静态代理
            • 示例代码
          • 动态代理
          • JDK 动态代理
            • 实现方式
            • Proxy
            • InvocationHandler
            • 使用方式
            • 示例代码
            • 原理分析
            • 总结
          • CGLIB 动态代理
            • 实现方式
            • 示例代码
          • JDK 动态代理 和 CGLIB 动态代理的区别?
          • JDK 动态代理 和 CGLIB 动态代理总结
          • 模式优点
          • 模式缺点
          • 使用场景
          • 最佳示例
        • 外观模式(门面模式)
          • 概念
          • 模型结构图
          • 示例代码
          • 模式优点
          • 模式缺点
          • 使用场景
          • 最佳示例
        • 桥接模式
          • 引入
          • 概念
          • 模型结构图
          • 示例代码
          • 模式优点
          • 模式缺点
          • 使用场景
          • 最佳示例
        • 组合模式
          • 概念
          • 模型结构图
          • 示例代码
          • 模式优点
          • 模式缺点
          • 使用场景
          • 最佳示例
        • 享元模式
          • 概念
          • 模型结构图
            • 单纯享元模式
            • 复合享元模式
          • 示例代码
          • 模式优点
          • 模式缺点
          • 使用场景
          • 最佳示例
            • Integer
            • Long
            • Tomcat连接池

设计模式-23种设计模式之结构型模式

这部分我之前的学习,是通过博客,以及《Spring 5核心原理与30个类手写实战》这本书学习的,这本书前面会介绍设计模式多一点。

当然比较推荐的是《Java设计模式》。

有一篇比较详细的博客链接参考:史上最全设计模式导学目录(完整版)

设计模式分类

设计模式介绍

设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。

设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。

正确使用设计模式具有以下优点。

  • 可以提高程序员的思维能力、编程能力和设计能力。
  • 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

一般主要讲一下23种设计模式。

创建型模式

创建型设计模式,是指在创建对象的同时隐藏创建逻辑,不使⽤ new 直接实例化对象,程序在判断需要创建哪些对象时更灵活。包括下面五种模式:

  • 工厂方法模式(Factory Method)
  • 抽象工厂模式(Abstract Factory)
  • 单例模式(Singleton)
  • 建造者模式(Builder)
  • 原型模式(Prototype)

一般说二四种设计模式的时候,是指23个GoF设计模式+简单工厂模式,简单工厂模式属于创建型模式,待会也可以提一下。

科普一下GOF:GOF是设计模式的经典名著Design Patterns: Elements of Reusable Object-Oriented Software(中译本名为《设计模式——可复用面向对象软件的基础》)的四位作者,他们分为是:Elich Gamma、Richard Helm、Ralph Johnson、以及John Vlissides。这四个人常被称为Gang of Four, 即四人组,简称Gof。 他们总结了23个设计模式。

结构型模式

结构型设计模式,是指通过类和接⼝间的继承和引⽤实现创建复杂结构的对象。包括下面七种模式:

  • 适配器模式(Adapter)

  • 装饰器模式(Decorator)

  • 代理模式(Proxy)

  • 外观模式(Facade)

  • 桥接模式(Bridge)

  • 组合模式(Composite)

  • 享元模式(Flyweight)

行为型模式

行为型设计模式,是指通过类之间不同通信方式实现不同行为。包括下面十一种模式:

  • 策略模式(Strategy)

  • 模板方法模式(Template Method)

  • 观察者模式(Observer)

  • 迭代器模式(Iterator)

  • 责任链模式(Chain of Responsibility)

  • 命令模式(Command)

  • 备忘录模式(Memento)

  • 状态模式(State)

  • 访问者模式(Visitor)

  • 中介者模式(Mediator)

  • 解释器模式(Interpreter)

上面二十三种中重点学习的设计模式我用加粗标出来,大部分是平时有用到的设计模式。

结构型模式

适配器模式

参考文章链接:设计模式(十)适配器模式

引入

在日本,日本的插座电压都110V,而我们国内的手机充电器和笔记本充电器都是220V,这个时候就无法充电了,因为电压不匹配,此时需要一个变压器来将电压升到220V,这样我们就可以在日本插座和我们的电器中间添加一个变压器来适配我们的设备了。

概念

适配器模式(Adapter Design Pattern),它是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。

适配器模式有两种实现方式:类适配器对象适配器
类适配器使用继承关系来实现;对象适配器使用组合关系来实现。

对象适配器

对象适配器是使用组合的方法,在Adapter中会保留一个原对象(Adaptee)的引用,适配器的实现就是讲Target中的方法委派给Adaptee对象来做,用Adaptee中的方法实现Target中的方法。

模型结构图

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第1张图片

从上图可以看出,对象适配器主要由目标接口(Target)需要适配的类(Adaptee)适配器(Adapter)三部分组成(除去调用接口的客户端Client):

  • 目标接口(Target):客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
  • 需要适配的类(Adaptee):需要适配的类或适配者类。
  • 适配器(Adapter):通过包装一个需要适配的对象,把原接口转换成目标接口。
示例代码

就以刚才日本电压 和 中国电压的区别来编写电源适配器,示例代码如下:

// 日本插座抽象类
public abstract class IJapanSocket {
	public abstract Integer japanCharge();
}

// 中国插座抽象类
public abstract class IChineseSocket {
	public abstract Integer chineseCharge();
}

// 中国220V插座
class ChineseSocket extends IChineseSocket {
    public Integer chineseCharge() {
        return 220;
    }
}
// 电压适配器
public class SocketAdapter extends IJapanSocket {
    
    ChineseSocket chineseSocket;
    
    public SocketAdapter(ChineseSocket chineseSocket) {
        this.chineseSocket = chineseSocket;
    }
    
    public Integer charge() {
        return chineseSocket.chineseCharge;
    }
}

那我们实际使用时,客户端代码如下:

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

        IChineseSocket socket = new ChineseSocket(); // 中国插座实现类
        IJapanSocket socketAdapter = new SocketAdapter(socket); // 电源适配器

        System.out.println("电源电压是" + socketAdapter.charge() + "V");
    }
   
}

日本插座和中国插座有相似之处,他们都可以作为供电源,区别就在于电压不同,现在我们有一个中国插座的实现类,有日本插座的抽象类也就是接口。我们的电压适配器继承自日本插座抽象类并且保留了中国插座实现类的引用,重写供电的方法,但是是使用的中国插座的供电方法来实现的。在客户端中,我们给适配器传递一个中国插座实现类对象,就可以把它当做日本插座来使用了。

类适配器

与对象适配器不同的是,类适配器是通过类的继承来实现的。Adpater直接继承了Target和Adaptee中的所有方法,并进行改写,从而实现了Target中的方法。

模型结构图

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第2张图片

类适配器的主要部分与对象适配器相似。

这种方式的缺点就是必须实现Target和Adaptee中的方法,由于Java不支持多继承,所以通常将Target设计成接口,Adapter继承自Adaptee然后实现Target接口。

示例代码
// 日本插座抽象类
interface IJapanSocket {
	Integer japanCharge();
}

// 中国插座抽象类
public abstract class IChineseSocket {
	public abstract Integer chineseCharge();
}

// 电压适配器
public class SocketAdapter extends IChineseSocket implements IJapanSocket {
    
    public Integer charge() {
        return chineseSocket.chineseCharge;
    }
    
    public Integer japanCharge() {
        return chineseCharge();
    }
    
    public Integer chineseCharge() {
        return 220;
    }
}

那我们实际使用时,客户端代码如下:

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

        IJapanSocket socketAdapter = new SocketAdapter(); // 电源适配器

        System.out.println("电源电压是" + socketAdapter.chineseCharge() + "V");
    }
   
}

这两种方法的效果是一样的,只是用的方法不一样。

Java不支持多继承,所以将Duck声明为接口,Adapter继承自中国插座类并且实现了日本插座的方法,但是实现日本插座的方法不再是委派给中国插座类的对象,而是直接调用中国插座类的方法,因为在Adapter中实现了中国插座类的方法,所以可以直接调用。

缺省适配器
引入

在实际开发中,一个类如果想要实现某一个接口,就必须要实现接口中的每一个方法,如果目标(Target)角色中的方法众多,而这个类需要的仅仅几个,但是根据接口的实现规则,其余的方法也必须实现,这就造成了很多的不便,代码会很累赘。这个时候就可以考虑使用缺省适配器模式了。

概念

缺省适配器模式,是为一个接口提供缺省实现,这样的类型可以从这个缺省实现进行扩展,而不必从原有接口进行扩展。当原接口中定义的方法太多,而其中大部分又不被需要时,这种模式非常实用。由缺省适配器类直接实现目标(Target)角色接口,并为所有方法提供缺省的空实现。用户类就只需要继承适配器类,只实现自己需要实现的方法就行了。

示例代码

先举一个例子:

在学校里,一般学生会做的事情有:吃饭、睡觉、学习、看书、提问、谈恋爱。

而在我们初一四班里的同学,张三是个学渣,他只会吃饭、睡觉。

那我们可以将学生作为接口,初一四班里的同学作为中间者,而张三属于班级的一员,让张三继承于初一四班里的同学。

示例代码如下:

// 学生接口
public interface Student {
    public void eat();
    public void sleep();
    public void study();
    public void read();
    public void question();
    public void love();
}
// 初一四班学生抽象类
public abstract class Class4Student implements Student {
    public void eat(){}
    public void sleep(){}
    public void study(){}
    public void read(){}
    public void question(){}
    public void love(){}
}
// 张三
public class ZhangSan extends Class4Student {

    public void eat(){
        System.out.println("张三爱吃东西");
    }

    public void sleep(){
        System.out.println("张三爱睡觉");
    }
}

我们看到通过初一四班的同学(缺省适配器),张三不需要再实现自己不需要的方法了。

模式优点
  • 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
  • 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
  • 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

类适配器模式还具有如下优点:
由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。

对象适配器模式还具有如下优点:
一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。

模式缺点

类适配器模式的缺点如下:

对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。

对象适配器模式的缺点如下:

与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

使用场景
  1. 系统需要使用现有的类,而这些类的接口不符合系统的需要。
  2. 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
最佳示例

SpringMVC在请求处理流程(见后面的SpringMVC详解,应该会写的)中涉及HandlerAdapter处理器适配器。

public interface HandlerAdapter {

	boolean supports(Object handler);

	@Nullable
	ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

	long getLastModified(HttpServletRequest request, Object handler);
}

HandlerAdapter的实现类有这几种:

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第3张图片

使用 HandlerAdapter的原因分析:如果处理器的类型不同,有多重实现方式,那么调用方式就不是确定的,如果直接调用 Controller 方法,就得不断使用 if else 来进行判断是哪一种子类然后执行。那么如果后面要扩展 Controller,就得修改原来的代码,这样违背了 OCP 原则。

HandlerAdapter使用说明:

  • Spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类;
  • 适配器代替 controller执行相应的方法;
  • 扩展Controller时,只需要增加一个适配器类就完成了SpringMVC的扩展;
装饰器模式
概念

装饰模式(Decorator Pattern) ,动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。

模型结构图

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第4张图片

从上图中可以看出,装饰模式一共有抽象组件角色(Component)具体组件角色(ConcreteComponent)抽象装饰器(Decorator)具体装饰器角色(ConcreteDecorator)四部分组成:

  • 抽象组件角色(Component):定义一个对象接口,以规范准备接受附加责任的对象,即可以给这些对象动态地添加职责。
  • 具体组件角色(ConcreteComponent):被装饰者,定义一个将要被装饰增加功能的类。可以给这个类的对象添加一些职责。
  • 抽象装饰器(Decorator):维持一个指向构件Component对象的实例,并定义一个与抽象组件角色Component接口一致的接口。
  • 具体装饰器角色(ConcreteDecorator):向组件添加职责。
示例代码

以做咖啡为例子,一杯咖啡,可以选择加牛奶、加糖、加水、加巧克力,不同的选择在最后拿出来的是一杯不同的咖啡,我们通过代码实现:

// 一种普通的咖啡 抽象类
public abstract class Coffee {
    // 咖啡的介绍
    protected String description = "genral Coffee";

    public String getDescription() {
        return description;
    }
    
    // 每个子类都有自己的实现方法
    public abstract double cost();
}

现在写一个抽象装饰类,规范好具体装饰类需要实现的代码:

// 抽象装饰类
public abstract class AbstractAddDecorator extends Coffee {
    public abstract String getDescription();
}

现在来具体实现装饰类:

// 具体的装饰者,加牛奶类
public class MilkCoffee extends AbstractAddDecorator {
    
    // 保留一个被装饰者的引用
    Coffee coffee;

    public Mocha(Coffee coffee) {
        this.coffee = coffee;
    }

    public String getDescription() {
        return coffee.getDescription() + ", Add milk";
    }

    public double cost() {
        return beverage.cost() + 20;
    }
}

被装饰者实现类,比如咖啡有分为摩卡、卡布奇诺等…

// 被装饰者实现类,摩卡咖啡
public class Moka extends Coffee {

    public Moka() {
        description = "Moka";
    }

    public double cost() {
        return 150;
    }
}

写一段测试测试代码:

public static void main(String[] args) {
    Coffee coffee = new Moka();
    System.out.println("咖啡:" + coffee.getDescription());
    System.out.println("消费了" + coffee.cost());

    Coffee milkMoka = new MilkCoffee(coffee);
    System.out.println("咖啡:" + milkMoka.getDescription());
    System.out.println("消费了" + milkMoka.cost());
    }
}
模式优点
  • 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
  • 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
  • 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
  • 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”。
模式缺点
  • 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
  • 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
使用场景
  1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  2. 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
  3. 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类)。
最佳示例
JDK的IO流

Java的IO流是践行装饰者模式的典型实践。

我们拿InputStream举例,来看一下它的类图:

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第5张图片

  1. InputStream是最上层的父类,表示字节流。
  2. FileInputStream是一个具体类,用来对文件进行读取。
  3. BufferedInputStream是字节缓冲流,主要是为了减少I/O操作的次数,提高效率。

我们来看一下FilterInputStream

class FilterInputStream extends InputStream {
    
	protected volatile InputStream in;
    
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }
}

为什么要拿字节流举例?Java还有一种字符流Reader。

因为字符流的体系里面还涉及到其他的设计模式,并不完全契合这小节的主题。

Spring 的 TransactionAwareCacheDecorator

在Spring 中的TransactionAwareCacheDecorator 类我们也可以来尝试理解一下,这个类主要是用来处理事务缓存的,来看一下代码:

public class TransactionAwareCacheDecorator implements Cache {
    
    private final Cache targetCache;
 
    public TransactionAwareCacheDecorator(Cache targetCache) {
        Assert.notNull(targetCache, "Target Cache must not be null");
        this.targetCache = targetCache;
    }
 
    public Cache getTargetCache() {
        return this.targetCache;
    }
}

TransactionAwareCacheDecorator 就是对Cache 的一个包装。

SpringMVC 的 HttpHeadResponseDecorator

MVC 中的装饰者模式HttpHeadResponseDecorator 类,看一下代码:

public class HttpHeadResponseDecorator extends ServerHttpResponseDecorator {
    public HttpHeadResponseDecorator(ServerHttpResponse delegate) {
        super(delegate);
    }
}
MyBatis 的 Cache

org.apache.ibatis.cache.Cache接口,他有很多实现类,比如FifoCache 先入先出算法的缓存;LruCache 最近最少使用的缓存;TransactionlCache 事务相关的缓存,都是采用装饰者模式。实现类有:

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第6张图片

我们拿一个举例子来看一下,比如FifoCache

public class FifoCache implements Cache {
    
    private final Cache delegate;
    private final Deque<Object> keyList;
    private int size;
    
	public FifoCache(Cache delegate) {
        this.delegate = delegate;
        this.keyList = new LinkedList();
        this.size = 1024;
    }
}
代理模式(重要)

参考文章链接:代理模式的使用总结

敖丙的文章:《设计模式系列》- 代理模式

概念

代理模式,为其他对象提供一种代理以控制对这个对象的访问

模型结构图

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第7张图片

从上图中可以看出,代理模式一共有抽象主题角色(Subject)具体主题角色(RealSubject)代理主题角色(Proxy)三部分组成:

  1. 抽象主题角色(Subject):声明了目标对象和代理对象的共同接口,这样一来在任何可以使用目标对象的地方都可以使用代理对象。
  2. 具体主题角色(RealSubject):也称为委托角色或者被代理角色。定义了代理对象所代表的目标对象。
  3. 代理主题角色(Proxy):也叫委托类、代理类。代理对象内部含有目标对象的引用,从而可以在任何时候操作目标对象;代理对象提供一个与目标对象相同的接口,以便可以在任何时候替代目标对象。代理对象通常在客户端调用传递给目标对象之前或之后,执行某个操作,而不是单纯地将调用传递给目标对象。

代理模式又分为静态代理动态代理

静态代理是由开发者创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。

动态代理是在程序运行时,通过运用反射机制动态的创建而成。

静态代理

静态代理,由程序员创建或特定工具自动生成源代码,也就是在编译时就已经将接口、被代理类、代理类等确定下来。在程序运行之前,代理类的.class文件就已经生成。

示例代码

举个例子,一个班的同学都要向老师交班费,但是都是通过班长把自己的钱转交给老师。这里,班长代理学生上交班费,班长就是学生的代理,示例代码如下:

// 创建Person接口
public interface Person {
    //上交班费
    void giveMoney();
}

编写Student实现类,示例代码如下:

public class Student implements Person {
    public String name;
    public Student(String name) {
        this.name = name;
    }
    
    public void giveMoney() {
       System.out.println(name + "上交班费50元");
    }
}

再编写StudentsProxy类,这个类也实现了Person接口,但是还另外持有一个学生类对象。由于实现了Peson接口,同时持有一个学生对象,那么他可以代理学生类对象执行上交班费行为,示例代码如下:

// 学生代理类,也实现了Person接口,保存一个学生实体,这样可以代理学生产生行为
public class StudentsProxy implements Person {
    //被代理的学生
    Student student;
    
    public StudentsProxy(Person student) {
        // 只代理学生对象
        if(student instanceof Student) {
            this.stu = (Student) stu;
        }
    }
    
    //代理上交班费,调用被代理学生的上交班费行为
    public void giveMoney() {
        stu.giveMoney();
    }
}

编写测试代码如下:

public static void main(String[] args) {
    //被代理的学生张三
    Person zhangsan = new Student("张三");
    //生成代理对象,并将张三传给代理对象
    Person banzhang = new StudentsProxy(zhangsan);
    //班长代理上交班费
    banzhang.giveMoney();
}

这里并没有直接通过张三(被代理对象)来执行上交班费的行为,而是通过班长(代理对象)来代理执行了,这就是代理模式。

代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。这里的间接性就是指不直接调用实际对象的方法,那么我们在代理过程中就可以加上一些其他用途。

就拿这个例子来说,假如班长在帮张三上交班费之前想要先反映一下张三最近学习有很大进步,通过代理模式实现:

public class StudentsProxy implements Person{
    //被代理的学生
    Student student;
    
    public StudentsProxy(Person student) {
        // 只代理学生对象
        if(student instanceof Student) {
            this.stu = (Student) stu;
        }
    }
    
    //代理上交班费,调用被代理学生的上交班费行为
    public void giveMoney() {
        System.out.println(student.name + "最近学习有进步!");
        student.giveMoney();
    }
}

只需要在代理类中帮张三上交班费之前,执行其他操作就可以了。这种操作,也是使用代理模式的一个很大的优点。

动态代理

动态代理,是指在运行时,动态生成代理类。即代理类的字节码将在运行时生成并载入当前的ClassLoader。

动态代理,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法

动态代理在程序运⾏时通过反射创建具体的代理类,代理类和被代理类的关系在运⾏前是不确定的。动态代理的适⽤性更强,主要分为 JDK 动态代理CGLib 动态代理

比如说如果我们想要给每一个代理方法都加上一个前置处理方法,如下:

public void giveMoney() {
    //调用被代理方法前加入处理方法
    beforeMethod();
    student.giveMoney();
}

这里只有一个giveMoney方法,就写一次beforeMethod方法,但是如果除了giveMonney还有很多其他的方法,那就需要写很多次beforeMethod方法,麻烦。所以建议使用动态代理实现。

JDK 动态代理
实现方式

Jdk的动态代理是基于接口的。现在想要为RealSubject这个类创建一个动态代理对象,Jdk主要会做一下工作:

  1. 获取RealSubject上的所有接口列表。
  2. 确定要生成的代理类的类名,默认为:com.sun.proxy.$ProxyXXXX。
  3. 根据需要实现的接口信息,在代码中动态创建该Proxy类的字节码。
  4. 将对应的字节码转换为对于的class对象。
  5. 创建InvocationHandler实例handler,用来处理Proxy所有方法的调用。
  6. Proxy的class对象以创建的handler对象为参数,实例化一个proxy对象。
Proxy

Jdk通过java.lang.reflect.Proxy包来支持动态代理,在Java中要创建一个代理对象,必须调用Proxy类的静态方法newProxyInstance(),该方法的原型如下:

Object Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler handler) throws IllegalArgumentException

其中对于上面3个参数做一下说明:

  1. loader:类加载器,对于不同来源(系统库或网络等)的类需要不同的类加载器来加载,这是Java安全模型的一部分。可以使用null来使用默认的加载器。
  2. interfaces:接口或对象的数组,它就是前述代理对象和真实对象都必须共有的父类或者接口。
  3. handler:调用处理器,它必须是实现了InvocationHandler接口的对象,其作用是定义代理对象中需要执行的具体操作。
InvocationHandler

InvocationHandler接口是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法。

InvocationHandlerProxy的关系,就如RunnableThread的关系。InvocationHandler接口中只有一个方法invoke,它的作用就跟Runnable中的run方法类似,定义了代理对象在执行真实对象的方法时所希望执行的动作。其主要代码如下:

Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

其中对于上面3个参数做一下说明:

  1. proxy:表示执行这个方法的代理对象。
  2. method:表示真实对象实际需要执行的方法(关于Method类参见Java的反射机制)。
  3. args:表示真实对象实际执行方法时所需的参数。
使用方式
  1. 在实际的编程中,需要优先定义一个实现InvocationHandler接口的调用处理器对象,然后将它作为创建代理类实例的参数。(抑或在调用newProxyInstance方法时使用匿名内部类。)这样就得到了代理对象。
  2. 真实对象本身的实例化在调用处理器对象内部完成,实例化时需要的参数也应该及时传入调用处理器对象中。这样一来就完成了代理对象对真实对象的包装,而代理对象需要执行的额外操作也在invoke方法中处理。
  3. 其后,在客户端中,如果需要使用真实对象时,就可以用代理对象来替代它了(有时需要类型强制转化)。
示例代码

还是以之前的例子来说明:

// 创建Person接口
public interface Person {
    //上交班费
    void giveMoney();
}

我们来实现学生类:

// 学生类 实现Person接口
public class Student implements Person {
    public String name;
    
    public Student(String name) {
        this.name = name;
    }
    
    @Override
    public void giveMoney() {
        try {
            //假设数钱花了一秒时间
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       System.out.println(name + "上交班费50元");
    }
}

我们通过定义一个检测方法执行时间的工具类,在任何方法执行前先调用start方法,执行后调用finsh方法,就可以计算出该方法的运行时间,这也是一个最简单的方法执行时间检测工具,代码如下:

// 线程运行时间监测工具类
public class MonitorUtil {
    
    private static ThreadLocal<Long> tl = new ThreadLocal<>();
    
    public static void start() {
        tl.set(System.currentTimeMillis());
    }
    
    //结束时打印耗时
    public static void finish(String methodName) {
        long finishTime = System.currentTimeMillis();
        System.out.println(methodName + "方法耗时" + (finishTime - tl.get()) + "ms");
    }
}

这个时候我们准备实现JDK动态代理的流程,一个代理类实现接口,需要完成委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。

我们来创建StuInvocationHandler类,实现InvocationHandler接口,这个类中持有一个被代理对象的实例targetInvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。在invoke方法中执行被代理对象target的相应方法。在代理过程中,我们在真正执行被代理对象的方法前加入自己其他处理。

这也是Spring中的AOP实现的主要原理,这里还涉及到一个很重要的关于java反射方面的基础知识。

public class StuInvocationHandler<T> implements InvocationHandler {
    //invocationHandler持有的被代理对象
    T target;
    
    public StuInvocationHandler(T target) {
       this.target = target;
    }
    
    /**
     * proxy:代表动态代理对象
     * method:代表正在执行的方法
     * args:代表调用目标方法时传入的实参
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理执行" +method.getName() + "方法");  
        //代理过程中插入监测方法,计算该方法耗时
        MonitorUtil.start();
        if (Objects.equals(method.getName(), "giveMoney")) { // 匹配一下对应方法
            Object result = method.invoke(target, args);
        }
        MonitorUtil.finish(method.getName());
        return result;
    }
}

我们测试一下代码:

public static void main(String[] args) {
    //创建一个实例对象,这个对象是被代理的对象
    Person zhangsan = new Student("张三");
    //创建一个与代理对象相关联的InvocationHandler
    InvocationHandler stuHandler = new StuInvocationHandler<Person>(zhangsan);
    //创建一个代理对象stuProxy来代理zhangsan,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
    Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler)//代理执行上交班费的方法
    stuProxy.giveMoney();
}

创建了一个需要被代理的学生张三,将zhangsan对象传给了stuHandler中,我们在创建代理对象stuProxy时,将stuHandler作为参数了的,上面也有说到所有执行代理对象的方法都会被替换成执行invoke方法,也就是说,最后执行的是StuInvocationHandler中的invoke方法。

原理分析

我们利用Proxy类的newProxyInstance方法创建了一个动态代理对象,查看该方法的源码,发现它只是封装了创建动态代理类的步骤:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,
                          InvocationHandler h) throws IllegalArgumentException {
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone(); // 对接口数组克隆
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    Class<?> cl = getProxyClass0(loader, intfs); // 这里产生了代理类

    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }

        // private static final Class[] constructorParams = { InvocationHandler.class };
        final Constructor<?> cons = cl.getConstructor(constructorParams); // 获取InvocationHandler构造器
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        return cons.newInstance(new Object[]{h}); // 使用构造器返回实例
    } catch (IllegalAccessException|InstantiationException e) {
        // throw ex
    } catch (InvocationTargetException e) {
        // throw ex
    } catch (NoSuchMethodException e) {
        // throw ex
    }
}

**Jdk为我们的生成了一个叫$Proxy0(这个名字后面的0是编号,有多个代理类会一次递增)的代理类,这个类文件时放在内存中的,我们在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。**通过对这个生成的代理类源码的查看,我们很容易能看出,动态代理实现的具体过程。

我们可以对InvocationHandler看做一个中介类,中介类持有一个被代理对象,在invoke方法中调用了被代理对象的相应方法。通过聚合方式持有被代理对象的引用,把外部对invoke的调用最终都转为对被代理对象的调用。

代理类调用自己方法时,通过自身持有的中介类对象来调用中介类对象的invoke方法,从而达到代理执行被代理对象的方法。也就是说,动态代理通过中介类实现了具体的代理功能。

总结

生成的代理类:$Proxy0 extends Proxy implements Person,我们看到代理类继承了Proxy类,所以也就决定了java动态代理只能对接口进行代理,Java的继承机制注定了这些动态代理类们无法实现对class的动态代理

CGLIB 动态代理

生成动态代理的方法很多,不止jdk自带的动态代理这一种,还有CGLIB,Javassist或者ASM。

Jdk的动态代理依靠接口实现,如果有些类并没有实现接口,则不能使用jdk代理,这就要用到CGLIB代理了。

CGLIB是针对类来实现的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理

实现方式
  1. 查找被代理类上的所有非final的public类型的方法定义。
  2. 将这些方法的定义转换成字节码。
  3. 将组成的字节码转换成相应的代理的class对象。
  4. 实现MethodInterceptor接口,用来处理对代理类上所有方法的请求(这个接口和Jdk动态代理InvocationHandler的功能和角色是一样的)。

需要两个jar包:cglib-nodep-2.2.jar、asm.jar

示例代码

写一个UserDao,有一个用来保存用户的方法,代码如下:

// 目标对象,没有实现任何接口
public class UserDao {
    public void save() {
        System.out.println("----已经保存数据!----");
    }
}

编写CGLIB代理工厂,第一种方式,代码如下:

/**
 * Cglib子类代理工厂
 * 对UserDao在内存中动态构建一个子类对象
 */
public class ProxyFactory implements MethodInterceptor {
    //维护目标对象
    private Object target;

    public ProxyFactory(Object target) {
        this.target = target;
    }

    //给目标对象创建一个代理对象
    public Object getProxyInstance(){
        //1.工具类
        Enhancer en = new Enhancer();
        //2.设置父类
        en.setSuperclass(target.getClass());
        //3.设置回调函数
        en.setCallback(this);
        //4.创建子类(代理对象)
        return en.create();

    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("开始事务...");
        //执行目标对象的方法
        Object returnValue = method.invoke(target, args);
        System.out.println("提交事务...");
        return returnValue;
    }
}

或者第二种方式,代码如下:

/**
 * Cglib子类代理工厂
 * 对UserDao在内存中动态构建一个子类对象
 */
public class ProxyFactory {
    //维护目标对象
    private Object target;

    public ProxyFactory(Object target) {
        this.target = target;
    }

    //给目标对象创建一个代理对象
    public Object getProxyInstance() {
        //1.工具类
        Enhancer en = new Enhancer();
        //2.设置父类
        en.setSuperclass(target.getClass());
        //3.设置回调函数
        en.setCallback(new MethodInterceptor() {
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
          System.out.println("开始事务...");
          //执行目标对象的方法
          Object returnValue = method.invoke(target, args);
          System.out.println("提交事务...");
          return returnValue;
        }
        });
        //4.创建子类(代理对象)
        return en.create();
    }
}

我们测试一下代码:

public class App {
    @Test
    public void test(){
        //目标对象
        UserDao target = new UserDao();
        //代理对象
        UserDao proxy = (UserDao)new ProxyFactory(target).getProxyInstance();
        //执行代理对象的方法
        proxy.save();
    }
}
JDK 动态代理 和 CGLIB 动态代理的区别?

**jdk动态代理 **是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

**cglib动态代理 **是利用ASM开源包,对被代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

ASM: 一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

JDK 动态代理 和 CGLIB 动态代理总结

JDK动态代理只能对实现了接口的类生成代理,而不能针对类 ,使用的是 Java反射技术实现,生成类的过程比较高效。

CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法 ,使用asm字节码框架实现,相关执行的过程比较高效,生成类的过程可以利用缓存弥补,因为是继承,所以该类或方法最好不要声明成final。

JDK代理是不需要第三方库支持,只需要JDK环境就可以进行代理,使用条件:实现InvocationHandler + 使用Proxy.newProxyInstance产生代理对象 + 被代理的对象必须要实现接口。

CGLib必须依赖于CGLib的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,覆盖其中的方法,是一种继承但是针对接口编程的环境下推荐使用JDK的代理。

简单来说,如果加入容器的目标对象有实现接口,用JDK代理;如果目标对象没有实现接口,用Cglib代理 ;如果目标对象实现了接口,且强制使用cglib代理,则会使用cglib代理。

模式优点
  • 代理模式能将代理对象与真实被调用的目标对象分离。
  • 一定程度上降低了系统的耦合度,扩展性好。
  • 可以起到保护目标对象的作用。
  • 可以对目标对象的功能增强。
模式缺点
  • 代理模式会造成系统设计中类的数量增加。
  • 在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢。
  • 增加了系统的复杂度。
使用场景
  1. 远程代理:即为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实;
  2. 虚拟代理:即根据需要创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象;
  3. 安全代理:用来控制真实对象访问时的权限;
  4. 智能指引:即当调用真实对象时,代理处理另外一些事。
最佳示例

参考博客链接:Spring5 AOP 默认使用Cglib? 从现象到源码深度分析

在 Spring 5.x 中默认使用的是JDK动态代理,在SpringBoot2.x 版本中通过AopAutoConfiguration来自动装配 AOP,会默认使用 Cglib 来实现。

外观模式(门面模式)

参考博客链接:设计模式(七)门面模式(Facade Pattern 外观模式)

概念

门面模式(Facade Pattern)又称为外观模式,外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用

模型结构图

img

从上图中我们可以看出门面模式一共有两种角色:

  1. 门面角色(Facade):客户端调用这个角色的方法。此角色知晓相关的子系统的功能和责任。正常情况下,本角色会将所有从客户端发来的请求委派到相应的子系统中去。
  2. 子系统角色:可以同时有一个或者多个子系统。每个子系统都不是一个单独的类,而是一个类的集合。每一个子系统都可以被客户端直接调用,或者被门面角色直接调用。子系统并不知道门面的存在,罪域子系统而言,门面仅仅是另一个客户端而已。
示例代码

假如我们每天上班回来很累,我们需要打开电视、打开电脑、打开热水器,我们需要一样一样打开它们,代码示例如下:

// 电视类
public class TV {
    public void open(){
        System.out.println("打开电视看电影!");
    }
}
// 电脑类
public class Computer {
    public void open(){
        System.out.println("打开电脑放音乐!");
    }
}
// 热水器类
public class Heater {
    public void open(){
        System.out.println("打开热水器烧水!");
    }
}

我们打开的时候就需要这样操作:

public static void main(String[] args){
	Computer computer = new Computer();
    Heater heater = new Heater();
    TV tv = new TV();

    computer.open();
    heater.open();
    tv.open();
}

但是如果我们把他们一个统一的开关,来调度所有开关,代码如下:

public class Facade {

    private Computer computer;
    private Heater heater;
    private TV tv;

    public Facade() {
        computer = new Computer();
        heater = new Heater();
        tv = new TV();
    }

    public void open() {
		computer.open();
        heater.open();
        tv.open();
    }
}

这样在主函数类只需要使用门面类就可以了。

我们打开的时候就需要这样操作:

public static void main(String[] args) {
    Facade facade = new Facade();
    facade.open();
}
模式优点
  • 对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易。通过引入门面模式,客户代码将变得很简单,与之关联的对象也很少。
  • 实现了子系统与客户之间的松耦合关系,这使得子系统的组件变化不会影响到调用它的客户类,只需要调整外观类即可。
  • 降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。
  • 只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类。
模式缺点
  • 不能很好地限制客户使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性。
  • 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
使用场景
  1. 当要为一个复杂子系统提供一个简单接口时可以使用外观模式。该接口可以满足大多数用户的需求,而且用户也可以越过外观类直接访问子系统。
  2. 客户程序与多个子系统之间存在很大的依赖性。引入外观类将子系统与客户以及其他子系统解耦,可以提高子系统的独立性和可移植性。
  3. 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
最佳示例

日志系统应该是最常见的门面模式的应用了,Slf4j 就是一个非常经典的门面。

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第8张图片

桥接模式

参考博客链接:设计模式(八)桥梁模式(Bridge)

引入

现需要提供大、中、小3种型号的画笔,能够绘制5种不同颜色,如果使用蜡笔,我们需要准备3 * 5 = 15支蜡笔,也就是说必须准备15个具体的蜡笔类。

而如果使用毛笔的话,只需要3种型号的毛笔,外加5个颜料盒,用3 + 5 = 8个类就可以实现15支蜡笔的功能。实际上,蜡笔和毛笔的关键一个区别就在于笔和颜色是否能够分离。

概念

桥梁模式的用意是“将抽象化与实现化脱耦,使得二者可以独立地变化。”

那么什么是脱耦呢?我们先来看一下什么是耦合。两个类之间的关系分为两种,一种是强关联一种是弱关联,强关联是在编译时期就已经确定的,无法在运行时期动态的改变的关联;弱关联是可以动态地确定并且可以在运行时期动态改变的关联。显然,Java中继承是强关联而聚合是弱关联。耦合就是两个实体的行为的某种强关联,脱耦就是指将他们之间的强关联解除,但是在桥梁模式中是指将它们之间的强关联改换成弱关联。所以桥梁模式的精髓就是尽量使用聚合/组合来实现弱关联。

模型结构图

img

这是具有一般性的桥梁模式的类图,我们可以看到桥梁模式一共有四部分组成:

  1. 抽象化角色(Abstraction):抽象化给出的定义,并保存一个对实现化对象的引用,就是图像类中的形状父类。
  2. 修正抽象化角色(RefinedAbstraction):扩展抽象化角色,改变和修正父类对抽象化的定义,比如形状下有正方形,圆形等图形。
  3. 实现化角色(Implementor):这个角色给出具体角色的接口,但是不给出具体的实现,这个接口不一定和抽象化角色的接口定义相同,实际上两者可以完全不一样,好比形状的颜色接口。
  4. 具体实现化角色(ConcereteImplementor):这个角色给出实现化角色接口的具体实现,好比各种具体的颜色。

如果将AbstractionImplementor看成两个岸边的话,那么聚合关系就像桥一样将他们连接起来,这就是这个模式为什么叫桥梁模式的原因。

示例代码

拿上面三个毛笔五个颜料盒举例子画出类图:

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第9张图片

首先编写画笔类代码举例:

// 抽象画笔类
public abstract class BrushPenAbstraction {

    // 保留对颜色的引用
    protected ImplementorColor imp;

    // 每种笔都有自己的实现
    public abstract void operationDraw();

    public void setImplementor(ImplementorColor imp) {
        this.imp = imp;
    }
}

// 粗毛笔的实现
public class BigBrushPenRefinedAbstraction extends BrushPenAbstraction{

    public void operationDraw() {
        System.out.println("Big and "+imp.bepaint()+" drawing!");
    }
}
// 细毛笔的实现
public class SmallBrushPenRefinedAbstraction extends BrushPenAbstraction{

    public void operationDraw() {
        System.out.println("Small and "+imp.bepaint()+" drawing!");
    }
}
// 中等粗细毛笔的实现
public class MiddleBrushPenRefinedAbstraction extends BrushPenAbstraction{

    public void operationDraw() {
        System.out.println("Middle and "+imp.bepaint()+" drawing!");
    }
}

再编写颜色类代码举例:

// 颜色的接口
public abstract class ImplementorColor {

    public abstract String bepaint();
}

// 红颜色
public class OncreteImplementorRed extends ImplementorColor {
    public String bepaint() {
        return "red";
    }
}
// 蓝颜色
public class OncreteImplementorBule extends ImplementorColor {
    public String bepaint() {
        return "bule";
    }
}
...

我们再来编写测试代码如下:

public static void main(String[] args) {
    BrushPenAbstraction brushPen = new BigBrushPenRefinedAbstraction();
    ImplementorColor col = new OncreteImplementorRed();
	// 设置颜色
    brushPen.setImplementor(col);
	// 画画
    brushPen.operationDraw();
}
模式优点
  • 分离抽象接口及其实现部分。
  • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
  • 实现细节对客户透明,可以对用户隐藏实现细节。
模式缺点
  • 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进象层进行开发。
  • 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
使用场景
  1. 不希望或不适用使用继承的场景:例如继承层次过渡、无法更细化设计颗粒等场景,需要考虑使用桥梁模式。
  2. 接口或抽象类不稳定的场景:明知道接口不稳定还想通过实现或继承来实现业务需求,那是得不偿失的,也是比较失败的做法。
  3. 重用性要求较高的场景:设计的颗粒度越细,则被重用的可能性就越大,而采用继承则受父类的限制,不可能出现太细的颗粒度。
最佳示例

java.sql.Driver是一个接口

public interface Driver {
    Connection connect(String url, java.util.Properties info) throws SQLException;
    boolean acceptsURL(String url) throws SQLException;
    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException;
    int getMajorVersion();
    int getMinorVersion();
    boolean jdbcCompliant();
    public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

下面可以有MySQL的Driver,Oracle的Driver,这些就可以当做实现接口类。那么我们现在来看看MySQL中的Driver类:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

特别简短的代码,其实只调用了DriverManager中的registerDriver()方法来注册驱动。当驱动注册完成后,我们就会开始调用DriverManager中的getConnection()方法了。

public class DriverManager {
    
    public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }

        return (getConnection(url, info, Reflection.getCallerClass()));
    }

    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {

        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }

        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }
    
}

可以看到需要返回的是Connection对象。在Java中通过Connection提供给各个数据库一样的操作接口,这里的Connection可以看作抽象类。

可以说我们用来操作不同数据库的方法都是相同的,不过MySQL有自己的ConnectionImpl类,同样Oracle也有对应的实现类。这里Driver和Connection之间是通过DriverManager类进行桥接的,不是像我们上面说的那样用组合关系来进行桥接。

组合模式
概念

组合模式(CompositePattern)又称为整体-部分模式,是指组合多个对象形成树形结构以表示“整体-部分”的关系的层次结构。组合模式对叶子节点和容器节点的处理具有一致性

模型结构图

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第10张图片

我们可以看到组合模式一共有三部分组成:

  1. 抽象构件角色(Component):叶子构件与容器构件共同继承的父类或者是共同实现的接口,该角色中包含所有子类共有方法的声明和实现,在抽象构件中定义了管理子构件的方法,新增构件、删除构件、获取构件。
  2. 叶子构件角色(Leaf):表示叶子节点,没有子节点,对于继承父类的管理子节点的方法以抛出异常的方式处理。
  3. 容器构件角色(Composite):表示容器节点,包含子节点,子节点可以是容器节点也可以是叶子节点,其提供一个集合来对子节点进行维护,以迭代的方式对子节点进行处理。

组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。

示例代码

比方说我们一个大型公司,公司里分为不同的部门,不同的部门下有不同的组,对于每一个团体我们都能抽象成一个团队抽象。

首先我们来编写团队的抽象类,示例代码如下:

// 抽象构建角色 - 团队抽象类
public interface Unit {
    void add(Unit unit);
    void remove(Unit unit);
    Unit getChild(int i);
    void sendMessage(String message);
}

再来编写公司的实现类,示例代码如下:

// 容器构件角色 - 公司
@Data
@NoArgsConstructor
public class Subsidiary implements Unit {
    private List<Unit> units = new ArrayList<>();
    private String name;
    public Subsidiary(String name) {
        this.name = name;
    }
    @Override
    public void add(Unit unit) {
        units.add(unit);
    }
    @Override
    public void remove(Unit unit) {
        units.remove(unit);
    }
    @Override
    public Unit getChild(int i) {
        return units.get(i);
    }
    @Override
    public void sendMessage(String message) {
        System.out.println(name + "收到通知");
        System.out.println(name + "下发通知");
        units.forEach(child -> child.sendMessage(message));
    }
}

再来实现以下不同的组,比如说开发组、产品组、测试组等,示例代码如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DevGroup implements Unit {
    private String name;
    @Override
    public void add(Unit unit) {
        System.out.println("开发组没有子节点");
    }
    @Override
    public void remove(Unit unit) {
        System.out.println("开发组没有子节点");
    }
    @Override
    public Unit getChild(int i) {
        System.out.println("开发组没有子节点");
        return null;
    }
    @Override
    public void sendMessage(String message) {
        System.out.println(name + "收到通知");
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TestGroup implements Unit {
    private String name;
    @Override
    public void add(Unit unit) {
        System.out.println("测试组没有子节点");
    }
    @Override
    public void remove(Unit unit) {
        System.out.println("测试组没有子节点");
    }
    @Override
    public Unit getChild(int i) {
        System.out.println("测试组没有子节点");
        return null;
    }
    @Override
    public void sendMessage(String message) {
        System.out.println(name + "收到通知");
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProductGroup implements Unit {
    private String name;
    @Override
    public void add(Unit unit) {
        System.out.println(name + "没有子节点");
    }
    @Override
    public void remove(Unit unit) {
        System.out.println(name + "没有子节点");
    }
    @Override
    public Unit getChild(int i) {
        System.out.println(name + "没有子节点");
        return null;
    }
    @Override
    public void sendMessage(String message) {
        System.out.println(name + "收到通知");
    }
}

最后,我们通过编写测试代码理解:

public class Client {
    public static void main(String[] args) {
        // 总公司
        Unit head = new Subsidiary("总公司");
        Unit headDev = new DevGroup("总公司开发组");
        Unit headTest = new TestGroup("总公司测试组");
        Unit headProduct = new ProductGroup("总公司产品组");
        head.add(headDev);
        head.add(headTest);
        head.add(headProduct);
        // 北京分公司
        Unit bj = new Subsidiary("北京分公司");
        Unit bjDev = new DevGroup("北京分公司开发组");
        Unit bjTest = new TestGroup("北京分公司测试组");
        Unit bjProduct = new ProductGroup("北京分公司产品组");
        bj.add(bjDev);
        bj.add(bjTest);
        bj.add(bjProduct);
        head.add(bj);
        // 北京朝阳部门
        Unit bjcy = new Subsidiary("北京分公司朝阳分部");
        Unit bjcyDev = new DevGroup("北京分公司朝阳分部开发组");
        Unit bjcyTest = new TestGroup("北京分公司朝阳分部测试组");
        Unit bjcyProduct = new ProductGroup("北京分公司朝阳分部产品组");
        bjcy.add(bjcyDev);
        bjcy.add(bjcyTest);
        bjcy.add(bjcyProduct);
        bj.add(bjcy);
        // 北京西城部门
        Unit bjxc = new Subsidiary("北京分公司西城分部");
        Unit bjxcDev = new DevGroup("北京分公司西城分部开发组");
        Unit bjxcTest = new TestGroup("北京分公司西城分部测试组");
        Unit bjxcProduct = new ProductGroup("北京分公司西城分部产品组");
        bjxc.add(bjxcDev);
        bjxc.add(bjxcTest);
        bjxc.add(bjxcProduct);
        bj.add(bjxc);
        // 上海分公司
        Unit sh = new Subsidiary("上海分公司");
        Unit shDev = new DevGroup("上海分公司开发组");
        Unit shTest = new TestGroup("上海分公司测试组");
        Unit shProduct = new ProductGroup("上海分公司产品组");
        sh.add(shDev);
        sh.add(shTest);
        sh.add(shProduct);
        head.add(sh);
        // 上海浦东部门
        Unit shpd = new Subsidiary("上海分公司浦东分部");
        Unit shpdDev = new DevGroup("上海分公司浦东分部开发组");
        Unit shpdTest = new TestGroup("上海分公司浦东分部测试组");
        Unit shpdProduct = new ProductGroup("上海分公司浦东分部产品组");
        shpd.add(shpdDev);
        shpd.add(shpdTest);
        shpd.add(shpdProduct);
        sh.add(shpd);
        // 上海徐汇部门
        Unit shxh = new Subsidiary("上海分公司徐汇分部");
        Unit shxhDev = new DevGroup("上海分公司徐汇分部开发组");
        Unit shxhTest = new TestGroup("上海分公司徐汇分部测试组");
        Unit shxhProduct = new ProductGroup("上海分公司徐汇分部产品组");
        shxh.add(shxhDev);
        shxh.add(shxhTest);
        shxh.add(shxhProduct);
        sh.add(shxh);
        head.sendMessage("五一放假");
    }
}
模式优点
  • 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
  • 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
  • 在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
  • 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
模式缺点
  • 组合模式不容易限制组合中的构件。
使用场景
  1. 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
  2. 在一个使用面向对象语言开发的系统中需要处理一个树形结构。
  3. 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。
最佳示例

Jdk中的组合模式

  1. java.util.Map#putAll(Map)
  2. java.util.List#addAll(Collection)
  3. java.util.Set#addAll(Collection)
享元模式
概念

享元模式(Flyweight Pattern) 也叫蝇量模式,又称为轻量级模式,是指运用共享技术有效地支持大量细粒度的对象。运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。

享元模式中存在以下两种状态

  1. 内部状态,指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变。
  2. 外部状态,指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。

举个例子来说明:围棋理论上有361个空位可以放棋子,棋子颜色就是棋子的内部状态,而各个棋子之间的差别就是位置的不同,所以棋子坐标就是棋子的外部状态。

也就是说,**享元模式的本质是分离与共享 : 分离变与不变,并且共享不变。**把一个对象的状态分成内部状态和外部状态,内部状态即是不变的,外部状态是变化的;然后通过共享不变的部分,达到减少对象数量并节约内存的目的。

在享元模式中通常会出现工厂模式,需要创建一个享元工厂来负责维护一个享元池(Flyweight Pool)(用于存储具有相同内部状态的享元对象)。在享元模式中,共享的是享元对象的内部状态,外部状态需要通过环境来设置。在实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种对象也称为 细粒度对象

享元模式的目的就是使用共享技术来实现大量细粒度对象的复用。

模型结构图

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第11张图片

享元模式的主要角色有如下。

  1. 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
  2. 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
  3. 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
  4. 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

在上面的图中看到,其结构图通常包含可以共享的部分和不可以共享的部分。在实际使用过程中,有时候会稍加改变,即存在两种特殊的享元模式:单纯享元模式复合享元模式,下面分别对它们进行简单介绍。

单纯享元模式

这种享元模式中的所有的具体享元类都是可以共享的,不存在非共享的具体享元类。

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第12张图片

复合享元模式

这种享元模式中的有些享元对象是由一些单纯享元对象组合而成的,它们就是复合享元对象。虽然复合享元对象本身不能共享,但它们可以分解成单纯享元对象再被共享。

备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)_第13张图片

示例代码

用户访问网站,网站只是一个抽象概念,它的具体实现是内部状态,用户是外部状态,示例代码如下:

// 用户类
@Data
public class User {
	private String name;
	public User(String name) {
		this.name = name;
	}		
}
// 网页抽象
public abstract class WebSite {
	public abstract void use(User user);
}
// 具体网页实现
public class ConcreteWebSite extends WebSite {
	private String type = "";

	public ConcreteWebSite(String type) {
		this.type = type;
	}
    
	@Override
	public void use(User user) {
		System.out.println("网站的发布形式为:" + type + ",使用者是" + user.getName());
	}
}

我们来创建享元工厂类:

// 享元工厂
public class WebSiteFactory {

	private HashMap<String, WebSite> pool = new HashMap<>();
	
	public WebSite getWebSiteCategory(String type) {
		if(!pool.containsKey(type)) {
			pool.put(type, new ConcreteWebSite(type));
		}
		return pool.get(type);
	}
	
	public int getWebSiteCount() {
		return pool.size();
	}
}

来进行测试:

public static void main(String[] args) {
    WebSiteFactory factory = new WebSiteFactory();

    WebSite webSite1 = factory.getWebSiteCategory("新闻");
    webSite1.use(new User("tom"));
    System.out.println("webSite1地址" + webSite1.hashCode());

    WebSite webSite2 = factory.getWebSiteCategory("博客");
    webSite2.use(new User("jack"));
    System.out.println("webSite2地址" + webSite2.hashCode());

    WebSite webSite3 = factory.getWebSiteCategory("博客");
    webSite3.use(new User("smith"));
    System.out.println("webSite3地址" + webSite3.hashCode());

    System.out.println("网站共" + factory.getWebSiteCount()+"类");
}
模式优点
  • 它可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。
  • 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
模式缺点
  • 享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
  • 为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
使用场景

享元模式是通过减少内存中对象的数量来节省内存空间的,是运用共享技术有效地支持大量 细粒度对象的复用,系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。所以以下几种情形适合采用享元模式:

  1. 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
  2. 大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
  3. 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。
最佳示例

池化技术,String常量池、数据库连接池、缓冲池、线程池、包装类中的缓存等等都是享元模式的应用。

Integer

举一个例子,就拿Integer的缓存来说:

// 首先我们看一下Integer这个类,在使用它的时候非常非常的频繁,那我们看一下Integer有一个方法,叫valueOf
public final class Integer extends Number implements Comparable<Integer> 
 
/**
    看一下里面的实现,首先一个if判断,如果大于IntegerCache的最小值,并且小于等于IntegerCache的最大值,
    我们直接从Cache里面获取,否则返回一个New出来的Integer对象,经常有一些Integer的判断,通过Integer的各种构造,
    然后把构造出来的数字做等等判断,让你们判断这个结果是true还是false。
    这里面就要对IntegerCache了解,
    这段逻辑也非常清晰,也就是说如果走到return IntegerCache.cache[i + (-IntegerCache.low)];这里,这里并不是
    new出来的IntegerCache对象,所以数字没有进入到if里面的时候,都是new出来的Integer对象,他们肯定不是同一个对象,
    所以这个数字如果不在这个范围,使用==的时候一定是false。
 */
public static Integer valueOf(int i) {
	if (i >= IntegerCache.low && i <= IntegerCache.high)
		return IntegerCache.cache[i + (-IntegerCache.low)];
	return new Integer(i);
}

/**
    IntegerCache这个类是一个private的静态内部类,最小值是-128,而high在静态块里边,声明了为127,也就是说如果我们
    小于等于-128,大于等于127的话,不会在Cache里边,而这个high的值也是可以修改的,
    String integerCacheHighPropValue =
        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    我们看一下这一行,这里面从JVM的参数里面获取IntegerCache的最大值,然后再进行一些判断,非常容易理解,然后同理Long
    类型里面也是有Cache的
*/
private static class IntegerCache {
	static final int low = -128;
	static final int high;
	static final Integer cache[];
 
	static {
		// high value may be configured by property
		int h = 127;
		String integerCacheHighPropValue =
			sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
		if (integerCacheHighPropValue != null) {
			try {
				int i = parseInt(integerCacheHighPropValue);
				i = Math.max(i, 127);
				// Maximum array size is Integer.MAX_VALUE
				h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
			} catch( NumberFormatException nfe) {
				// If the property cannot be parsed into an int, ignore it.
			}
		}
		high = h;
 
		cache = new Integer[(high - low) + 1];
		int j = low;
		for(int k = 0; k < cache.length; k++)
			cache[k] = new Integer(j++);
 
		// range [-128, 127] must be interned (JLS7 5.1.7)
		assert IntegerCache.high >= 127;
	}
 
	private IntegerCache() {}
}
Long

再来看一下Long里的缓存:

// 这里面也是有LongCache,我们再看一下他的valueOf方法
private static class LongCache {
	private LongCache(){}
 
	static final Long cache[] = new Long[-(-128) + 127 + 1];
 
	static {
		for(int i = 0; i < cache.length; i++)
			cache[i] = new Long(i - 128);
	}
}

public static Long valueOf(long l) {
	final int offset = 128;
	if (l >= -128 && l <= 127) { // will cache
		return LongCache.cache[(int)l + offset];
	}
	return new Long(l);
}
Tomcat连接池

看一下Tomcat提供的common连接池。

/**
    Tomcat提供的common连接池,我们打开这个类GenericObjectPool,
    这个很明显就是一个连接池,我们先看一下GenericObjectPoolConfig,连接池里默认的一些配置,也就是说如果这些数字
    如果我们不配置的话,也会有,那他的实现很简单,我们随便找一个,GenericKeyedObjectPool,有两个版本,一个是pool2,
    一个pool1,我们就看pool2的,那我们注意这个连接池,肯定有拿,肯定有放,我可以从这个连接池里界一个对象出来,
    借过来我使用,使用完之后我还要放回去,我们看方法borrowObject()
*/
@Override
public T borrowObject(final K key) throws Exception {
	return borrowObject(key, getMaxWaitMillis()); // 这里面又调用这个borrowObject(key, getMaxWaitMillis())方法
}
 
// 上面回调的 borrowObject(key, getMaxWaitMillis())方法,实现如下
/**
	这里面通过ObjectDeque对象,一个双端队列,让他来保持对象池的对象,在最上面把这个连接池声明为一个null,
    PooledObject p = null;我们看一下下面怎么用,然后在try里面对他实际的使用,通过Object的双端队列,
    来保存连接对象,当然这里还会调用一些factory的,factory.activateObject(key, p);存活着的对象,factory里面
    有很多的方法,public interface KeyedPooledObjectFactory,我们来看一下方法,有存活的对象activateObject,
    有销毁的对象destroyObject,创建对象destroyObject,钝化对象passivateObject,校验对象validateObject,那很简单,
    关注对象的几个状态,首先用这个池对象工厂来创建对象,那将不用的池对象进行钝化,对要使用的对象进行激活,
    并且还要对池对象进行激活,把有问题的池对象进行销毁,
*/
public T borrowObject(final K key, final long borrowMaxWaitMillis) throws Exception {
    assertOpen(); // 断言是否开启

    PooledObject<T> p = null;

    // Get local copy of current config so it is consistent for entire
    // method execution
    final boolean blockWhenExhausted = getBlockWhenExhausted();

    boolean create;
    final long waitTime = System.currentTimeMillis();
    final ObjectDeque<T> objectDeque = register(key);

    try {
        while (p == null) {
            create = false;
            p = objectDeque.getIdleObjects().pollFirst();
            if (p == null) {
                p = create(key);
                if (p != null) {
                    create = true;
                }
            }
            if (blockWhenExhausted) {
                if (p == null) {
                    if (borrowMaxWaitMillis < 0) {
                        p = objectDeque.getIdleObjects().takeFirst();
                    } else {
                        p = objectDeque.getIdleObjects().pollFirst(
                            borrowMaxWaitMillis, TimeUnit.MILLISECONDS);
                    }
                }
                if (p == null) {
                    throw new NoSuchElementException(
                        "Timeout waiting for idle object");
                }
            } else {
                if (p == null) {
                    throw new NoSuchElementException("Pool exhausted");
                }
            }
            if (!p.allocate()) {
                p = null;
            }

            if (p != null) {
                try {
                    factory.activateObject(key, p);
                } catch (final Exception e) {
                    try {
                        destroy(key, p, true);
                    } catch (final Exception e1) {
                        // Ignore - activation failure is more important
                    }
                    p = null;
                    if (create) {
                        final NoSuchElementException nsee = new NoSuchElementException(
                            "Unable to activate object");
                        nsee.initCause(e);
                        throw nsee;
                    }
                }
                if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
                    boolean validate = false;
                    Throwable validationThrowable = null;
                    try {
                        validate = factory.validateObject(key, p);
                    } catch (final Throwable t) {
                        PoolUtils.checkRethrow(t);
                        validationThrowable = t;
                    }
                    if (!validate) {
                        try {
                            destroy(key, p, true);
                            destroyedByBorrowValidationCount.incrementAndGet();
                        } catch (final Exception e) {
                            // Ignore - validation failure is more important
                        }
                        p = null;
                        if (create) {
                            final NoSuchElementException nsee = new NoSuchElementException(
                                "Unable to validate object");
                            nsee.initCause(validationThrowable);
                            throw nsee;
                        }
                    }
                }
            }
        }
    } finally {
        deregister(key);
    }

    updateStatsBorrow(p, System.currentTimeMillis() - waitTime);

    return p.getObject();
}
	
/**
	我们再来看一个方法returnObject,把一个对象返回。
*/
@Override
public void returnObject(final K key, final T obj) {

    final ObjectDeque<T> objectDeque = poolMap.get(key);

    final PooledObject<T> p = objectDeque.getAllObjects().get(new IdentityWrapper<>(obj));

    if (p == null) {
        throw new IllegalStateException(
            "Returned object not currently part of this pool");
    }

    synchronized(p) {
        final PooledObjectState state = p.getState();
        if (state != PooledObjectState.ALLOCATED) {
            throw new IllegalStateException(
                "Object has already been returned to this pool or is invalid");
        }
        p.markReturning(); // Keep from being marked abandoned (once GKOP does this)
    }

    final long activeTime = p.getActiveTimeMillis();

    try {
        if (getTestOnReturn()) {
            if (!factory.validateObject(key, p)) {
                try {
                    destroy(key, p, true);
                } catch (final Exception e) {
                    swallowException(e);
                }
                if (objectDeque.idleObjects.hasTakeWaiters()) {
                    try {
                        addObject(key);
                    } catch (final Exception e) {
                        swallowException(e);
                    }
                }
                return;
            }
        }

        try {
            factory.passivateObject(key, p);
        } catch (final Exception e1) {
            swallowException(e1);
            try {
                destroy(key, p, true);
            } catch (final Exception e) {
                swallowException(e);
            }
            if (objectDeque.idleObjects.hasTakeWaiters()) {
                try {
                    addObject(key);
                } catch (final Exception e) {
                    swallowException(e);
                }
            }
            return;
        }

        if (!p.deallocate()) {
            throw new IllegalStateException(
                "Object has already been returned to this pool");
        }

        final int maxIdle = getMaxIdlePerKey();
        final LinkedBlockingDeque<PooledObject<T>> idleObjects =
            objectDeque.getIdleObjects();

        if (isClosed() || maxIdle > -1 && maxIdle <= idleObjects.size()) {
            try {
                destroy(key, p, true);
            } catch (final Exception e) {
                swallowException(e);
            }
        } else {
            if (getLifo()) {
                idleObjects.addFirst(p);
            } else {
                idleObjects.addLast(p);
            }
            if (isClosed()) {
                // Pool closed while object was being added to idle objects.
                // Make sure the returned object is destroyed rather than left
                // in the idle object pool (which would effectively be a leak)
                clear(key);
            }
        }
    } finally {
        if (hasBorrowWaiters()) {
            reuseCapacity();
        }
        updateStatsReturn(activeTime);
    }
}
	
// 有一个poolMap,里面传了一个key,打开看一下
/**
	poolMap它是一个ConcurrentHashMap,也就是说他折中了HashMap和Hashtable,使用ConcurrentHashMap,
    来做这个连接池的Map,非常容易理解,也就是说在这个双端队列上一层,又包装了一层Map,也就是说呢poolMap,
    是连接池的对象池,那这些都可以认为是享元模式的一个应用,非常容易理解,希望通过这个过程,对以后有类似的
    场景,我们就要考虑是否可以使用享元模式
*/
private final Map<K,ObjectDeque<T>> poolMap =
		new ConcurrentHashMap<>(); 

你可能感兴趣的:(面试准备,Java设计模式,面试,java,职场和发展)