谈谈Java常用类库中的设计模式 - Part Ⅰ
背景
最近一口气看完了Joshua Bloch大神的Effective Java(下文简称EJ)。书中以tips的形式罗列了Java开发中的最佳实践,每个tip都将其意图和要点压缩在了标题里,这种做法我很喜欢:一来比较亲切,比起难啃的系统书,EJ就像是一本Java的《俚语指南》;二来记忆起来十分方便,整本书过一遍就能望标题生义。
在通读这本书时,我发现作者多次列举现有类库中的实现的设计模式,我有意将其收集起来,这些实现相当经典,我觉得有必要落成一篇文章。随着以后对类库的理解越来越深,我也会持续追加上自己发现的Pattern。
概述
由于篇幅限制,本主题会做成一个系列,每个系列介绍3-4个模式。
本文介绍的设计模式(可跳转):
Here We Go
建造者 (Builder)
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
场景:创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式时;对象内部结构复杂;对象内部属性相互依赖。
类型:创建型
建造者模式在Java中最广泛的用途就是复杂对象创建。比起类构造器或Getter/Setter,它同时保证了创建过程的可读性(和属性名一致的设参方法)和安全性(未创建完毕的对象不会逸出),同时它还有:参数可选、可在类继承层次中复用、对集合类字段更加友好等等优点 。对于复杂的对象都可使用建造者模式,代价是一定的性能开销与编写工作量,好在后者可以用Lombok这样的代码生成插件来解决。
借助Lombok生成类的建造者:
import lombok.*;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Foo {
private String name;
private Integer height;
private Integer length;
public static void main(String[] args) {
Foo f = Foo.builder().name("SQUARE").height(10),length(10).build();
}
}
除了使用建造者创建普通Java Bean之外,许多类库中配置类对象也照葫芦画瓢。比如SpringBoot中对Swagger2的简单配置,使其在生产环境下关闭。
@Configuration
public class SwaggerConfig {
@Value("${spring.profiles.active}")
private String prop;
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.enable(!prop.contains("prod"))
.select()
.apis(RequestHandlerSelectors.any()).build();
}
}
工厂方法 (Factory Method)
定义:定义一个创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
场景:明确计划不同条件下创建不同实例时。
类型:创建型
若要找工厂方法在JAVA原生类库中最贴切的对照物,非 Supplier 莫属。这个来自JAVA 8 Function包的函数式接口,将工厂方法模式的编写成本降到极低。
package java.util.function;
/**
* Represents a supplier of results.
*
* <p>There is no requirement that a new or distinct result be returned each
* time the supplier is invoked.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #get()}.
*
* @param <T> the type of results supplied by this supplier
*
* @since 1.8
*/
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
如果将工厂类作为方法入参,将保证对象会在最迫切的时机才会创建:
public class StreamTest {
public static void main(String[] args) {
Stream.generate(()->{
System.out.println("creating a new object.");
return new Object();
}).limit(3);
}
}
==============================
输出:
Stream.generate(Supplier<T> s)
创建了一个流,并声明了这个流的元素来源:一个由Lambda表达式编写的工厂。编译器将其推导为一个Supplier实例。在对象创建时将打印日志,然而结果表明没有任何对象创建成功。因为在未声明终结函数时,赋予Stream的任何中间函数都不会执行。
使用工厂方法,生产数据的时机将由消费者把握,这是一种懒汉思想。
再回到 Supplier<T> 的定义,它是一个泛型,按照EJ的建议,当使用泛型作为方法入参和返回值时,最好遵循 PECS 规则。
Producer-Extends Consumer-Super
Supplier通常是放在方法入参的生产者,所以应该这么声明:
public void generate(Supplier<T extends Shape> supplier) {}
这样Shape的所有子类工厂都能传入到此方法中,增强其拓展性。对应了工厂方法定义当中 让子类决定实例化哪一个类 的部分。
享元 (Flyweight)
定义:运用共享技术有效地支持大量细粒度的对象。
场景:应用使用大量对象,造成庞大的存储开销;对象中的大多数状态可以移至外部,剩下的部分可以共享。
类型:结构型
JDK类库中使用了大量的静态工厂(泛指创建对象的静态类/静态方法),这些静态工厂有一个重要的作用:为重复的调用返回相同的对象。使类成为实例受控的类(instance-controlled),这实际上就是享元的思想。
举个例子,下面是 Boolean.valueOf(boolean b) 的代码片段。
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code true}.
*/
public static final Boolean TRUE = new Boolean(true);
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code false}.
*/
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
Boolean 作为布尔类型的包装类,进行了实例控制。 因为布尔类型的值只有 True 和 False ,除此之外没有其他状态字段,所以类库设计者选择在类加载时初始化两个不可变实例,在静态工厂中不创建对象。
这也表明 Boolean.valueOf 返回的实例在执行==和equals时结果一致。
其他包装类型如Byte,Short,Integer也有类似的设计:使用名为XCache(X表示类型名)的私有内部类中存储值在 -128 ~ 127 之间共256个实例,并在静态工厂中使用。在面试中经常碰见的数值包装类“==”问题,考点就在这里。
桥接(Bridge)
定义:将抽象部分与他的实现部分分离,使它们都可以独立地变化。
场景:实现系统可能有多个角度分类,每一种角度都可能变化;在构件的抽象化和具体化之间增加更多的灵活性,避免两个层次之间的静态继承关系;控制系统中继承层次过多过深。
类型:结构型
首先了解一个概念:服务提供者框架(Service Provider Framework)(下文简称SPF)。
服务提供者框架指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来。
它由4种组件组成:服务接口:需被实现的接口或抽象类
提供者注册API:实现类用来注册自己到SPF中的
服务访问API:客户端用来获取实现的
服务提供者接口:实现类的工厂对象,用来创建实现类的实例,是可选的
SPF模式的应用是如此广泛,其实现的变体也有很多。如Java 6提供的标准实现ServiceLoader,还有Spring、Guice这样的依赖注入框架。但我选择举一个大家更为熟悉的例子:JDBC。
使用JDBC与数据库交互是每个Java程序员的必经之路,而它的设计实际上也是SPF模式:
服务接口 -> Connection
提供者注册API:DriverManager.registerDriver
服务访问API:DriverManager.getConnection
服务提供者接口:Driver
想要使用不同的数据库连接实现,只需通过服务访问API切换即可。这体现了桥接中将抽象与实现分离的精神。
(实际上如果加载多个数据库驱动,DriverManager会逐个尝试连接,并返回连接成功的实例。并不能人为选择提供者,但可以通过更改提供者注册代码来实现。)
Druid、Hikari等现代连接池的实现往往比JDBC定义的服务接口更加丰富,如监控、插件链、SQL日志等等,这体现了桥接当中的独立变化。
参考:
[1] Play With Java ServiceLoader And Forget About Dependency Injection Frameworks - (2016/10/02)
https://dzone.com/articles/play-with-java-serviceloader-forget-about-dependen
[2] Effective Java - 机械工业出版社 - Joshua Bloch (2017/11)
[3] 《大话设计模式》 - 清华大学出版社 - 陈杰 (2007/12)
谈谈Java常用类库中的设计模式 - Part Ⅰ的更多相关文章
- 谈谈Java常用类库中的设计模式 - Part Ⅱ
概述 本系列上一篇:建造者.工厂方法.享元.桥接 本文介绍的设计模式(建议按顺序阅读): 适配器 模板方法 装饰器 相关缩写:EJ - Effective Java Here We Go 适配器 (A ...
- 访何红辉:谈谈Android源码中的设计模式
最近Android 6.0版本的源代码开放下载,刚好分析Android源码的技术书籍<Android源码设计模式解析与实战>上市,我们邀请到它的作者何红辉,来谈谈Android源码中的设计 ...
- JAVA(三)JAVA常用类库/JAVA IO
成鹏致远 | lcw.cnblog.com |2014-02-01 JAVA常用类库 1.StringBuffer StringBuffer是使用缓冲区的,本身也是操作字符串的,但是与String类不 ...
- Java 常用类库与技巧【笔记】
Java 常用类库与技巧[笔记] Java异常体系 Java异常相关知识 Java在其创立的时候就设置了比较有效的处理机制,其异常处理机制主要回答了三个问题:what,where,why what表示 ...
- Google的Java常用类库 Guava资料
java的人应该都知道Apache commons的java常用类库吧,这个Guava和commons一样,封装出一套比jdk本身提供的常用类库强大.既然有了这个这么强大的类库,我们就没必要重复造轮子 ...
- Java并发编程中的设计模式解析(二)一个单例的七种写法
Java单例模式是最常见的设计模式之一,广泛应用于各种框架.中间件和应用开发中.单例模式实现起来比较简单,基本是每个Java工程师都能信手拈来的,本文将结合多线程.类的加载等知识,系统地介绍一下单例模 ...
- java类库中的设计模式
原帖:http://stackoverflow.com/questions/1673841/examples-of-gof-design-patterns 提问:我正在学习GoF的<设计模式&g ...
- Java常用类库——观察者设计模式
观察者设计模式 现在很多的购房者都在关注着房子的价格变化,每当房子价格变化的时候,所有的购房者都可以观察得到.实际上以上的购房者都属于观察者,他们都关注着房子的价格. 如果要想实现观察者模式,则必须依 ...
- Java常用类库--观察者设计模式( Observable类Observer接口)
如果要想实现观察者模式,则必须依靠java.util包中提供的Observable类和Observer接口. import java.util.* ; class House extends Obse ...
随机推荐
- 一个老牌程序员推荐的JavaScript的书籍,看了真的不后悔!
很多人问我怎么学前端?我的回答是:读书吧!相对于在网上学习,在项目中学习和跟着有经验的同事学习,书中有着相对完整的知识体系,每读一本好书都会带来一次全面的提高.而如果深一脚浅一脚的学习,写出代码的质量 ...
- [Inno Setup] 字符串列表,当要处理一长串文件时很有用
https://wiki.freepascal.org/TStringList-TStrings_Tutorial TStringList-TStrings Tutorial │ Deutsch (d ...
- Spring Boot的exit code
文章目录 Spring Boot的exit code 自定义Exit Codes ExitCodeGenerator ExitCodeExceptionMapper ExitCodeEvent Spr ...
- 【集群实战】NFS服务常见故障排查和解决方法
NFS,全名叫Network File System,中文叫网络文件系统,是Linux.UNIX系统的分布式文件系统的一个组成部分,可实现在不同网络上共享远程文件系统. NFS由Sun公司开发,目前已 ...
- 【shell】Shell变量基础及深入
1. 什么是变量 变量就是用一个固定的字符串(也可能是字符数字等的组合),替代更多更复杂的内容,这个内容里可能还会包含变量和路径,字符串等其他内容. 变量的定义是存在内存中. x=1 y=2 2. 变 ...
- 【Linux网络基础】网络子网划分基础知识(IP地址,子网)
一. IP地址分类与子网划分基础 1. 什么是IP地址? 常见的ip地址版本为ipv4, ipv6 32位 4 * 8=32位. 32位二进制数字序列组成的数字序列 点分十进制 采用点将32位数字 ...
- iOS9.2.1 App从AppStore上下载闪退问题
首先这是小编的第一篇文章,我是一名做iOS开发的小白,出于爱好会更新发表些相关的技术文章,偶尔也会发些视频.恳请大家不要去嘲笑一个努力的人,要是做的不好请多多评论,反正我也不改. 好了!敲黑板!!说正 ...
- Axios 拦截器中添加headers 属性
描述: 已在网上查过怎么在 interceptors 中对header进行处理,// http request 拦截器 axios.interceptors.request.use( config = ...
- HDOJ 4699 Editor 对顶栈模拟
Editor Time Limit: 3000/2000 MS (Java/Others) Memory Limit: 131072/131072 K (Java/Others) Total Subm ...
- 洛谷P5018 对称二叉树
不多扯题目 直接题解= = 1.递归 由题目可以得知,子树既可以是根节点和叶节点组成,也可以是一个节点,题意中的对称二叉子树是必须由一个根节点一直到树的最底部所组成的树. 这样一来就简单了,我们很容易 ...