设计模式学习笔记 - 开源实战一(下):通过剖析JDK源码学习灵活应用设计模式

概述

上篇文章我们讲解了工厂模式、建造者模式、适配器模式适配器模式在 JDK 中的应用,其中 Calendar 类用到了工厂模式和建造者模式, Collections 类用到了装饰器模式和适配器模式。学习的重点是让你了解,在真实的项目中模式的实现和应用更加灵活、多变,会根据具体的场景做实现或者设计上的调整。

本章,再重点讲一下模板模式、观察者模式这两个模式在 JDK 中的应用。此外,还会在对理论部分已经经过的一些模式在 JDK 中的应用做一个汇总,带你一起回忆复习下。


模版模式在 Collections 类中的应用

策略、模板、职责链这三个模式常用在框架的设计中,提供框架的扩展点,让框架使用者在不修改框架源码的情况下,基于扩展点定制化框架的功能。Java 中的 Collections 类的 sort() 函数就是利用了模板模式的这个扩展特性。

首先,我们看下 Collections.sort() 是如何使用的。下面写了一个实例代码,如下所示。这个代码实现了按照不同的排序方式(按照年龄从小到大、按照字母序从小到大、按照成绩从大到小)对 students 数组进行排序。

public class Demo {public static void main(String[] args) {List<Student> students = new ArrayList<>();students.add(new Student("Jack", 19, 91));students.add(new Student("Lucky", 21, 92));students.add(new Student("Yaml", 20, 91));Collections.sort(students, new AgeAscComparator());}private static class AgeAscComparator implements Comparator<Student> {@Overridepublic int compare(Student o1, Student o2) {return o1.getAge() - o2.getAge();}}private static class NameAscComparator implements Comparator<Student> {@Overridepublic int compare(Student o1, Student o2) {return o1.getName().compareTo(o2.getName());}}private static class ScoreDescComparator implements Comparator<Student> {@Overridepublic int compare(Student o1, Student o2) {if (Math.abs(o1.getScore() - o2.getScore()) < 0.1f) {return 0;} else if (o1.getScore() < o2.getScore()) {return 1;} else {return -1;}}}
}

结合刚刚这个例子,再来看下,为什么说 Collections.sort() 函数用到了模板模式?

Collections.sort() 实现了对集合的排序。为了扩展性,它将其中 “比较大小” 这部分逻辑,委派给用户来实现。如果我们把比较大小这部分逻辑看做整个排序逻辑中的其中一个步骤,那我们就可以把它看做模板模式。不过,从代码实现角度来看,它看起来有点类似 JdbcTemplate,并不是模板模式的经典代码实现,而是基于 Callback 回调机制来实现。

不过,有的资料中把 Collections.sort() 看做策略模式。这样的说法也不是没有道理。如果我们并不把 “比较大小” 看做排序逻辑中的一个步骤,而是看作一种算法或者策略,那我们就可以把它看作是策略模式的一种应用。

不过,这也不是经典的策略模式,前面讲过,在经典的策略模式中,策略模式分为策略的定义、创建、使用这三部分。策略通过工厂模式来创建,并且在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。而在 Collections.sort() 函数中,策略的创建并非通过工厂模式,策略的使用也非动态确定。

观察者模式在 JDK 中的应用

在讲观察者模式时,我们重点讲解了 Google Guava 的 EventBus 框架,它提供了观察者模式的骨架代码。使用 EventBus,我们不需要从零开始开发观察者模式。实际上,Java JDK 也提供了观察者模式的简单框架实现。在平时的开发中,如果我们不希望引入 Google Guava 开发库,可以直接使用 Java 语音本身提供的这个框架类。

不过,它比 EventBus 简单多了,只包含两个类: java.util.Observablejava.util.Observer。前者是被观察者,后者是观察者。它们的代码实现也非常简单,为了方便你查看,我直接复制到这里。

public interface Observer {void update(Observable o, Object arg);
}public class Observable {private boolean changed = false;private Vector<Observer> obs;public Observable() {obs = new Vector<>();}public synchronized void addObserver(Observer o) {if (o == null)throw new NullPointerException();if (!obs.contains(o)) {obs.addElement(o);}}public synchronized void deleteObserver(Observer o) {obs.removeElement(o);}public void notifyObservers() {notifyObservers(null);}public void notifyObservers(Object arg) {Object[] arrLocal;synchronized (this) {if (!changed)return;arrLocal = obs.toArray();clearChanged();}for (int i = arrLocal.length-1; i>=0; i--)((Observer)arrLocal[i]).update(this, arg);}public synchronized void deleteObservers() {obs.removeAllElements();}protected synchronized void setChanged() {changed = true;}protected synchronized void clearChanged() {changed = false;}public synchronized boolean hasChanged() {return changed;}public synchronized int countObservers() {return obs.size();}
}

对于 ObservableObserver 的代码实现,大部分都很好理解,我们重点来看其中的两个地方。一个是 changed 成员变量,另一个是 notifyObservers() 函数。

先看 changed 成员变量

它用来表面被观察者(Observable)有没有状态更新。当有状态更新时,我们需要手动调用 setChange() 函数,将 changed 变量设置为 true,这样才能在调用 notifyObservers() 函数时,真正触发观察者(Observer)执行 update() 函数。否则,即使你调用了 notifyObservers() 函数,观察者的 update() 函数也不会被执行。

也就是说,当通知观察者被观察者状态更新的时候,我们需要依次调用 setChange()notifyObservers() 两个函数,单独调用 notifyObservers() 是不起作用的。

再看 notifyObservers() 函数

为了保证多线程环境下,添加、移除、通知观察者三个操作之间不发生冲突,Observable 类中的大部分函数都通过 synchronized 加了锁,不过,也有特例,notifyObservers() 函数就没有加 synchronized 锁。这是为什么呢?在 JDK 的代码实现中,notifyObservers() 函数是如何保证跟其他函数操作不冲突的呢?这种加锁方法是否存在问题?又存在什么问题?

notifyObservers() 之所以没有在函数上加一把大锁,主要还是处于性能的考虑。

notifyObservers() 函数依次执行每个观察者的 update() 函数,每个 update() 函数执行的逻辑未知,有可能会耗时。如果在 notifyObservers() 函数上加 synchronized 锁,notifyObservers() 函数持有锁的事件可能会很长,这就会导致其他线程迟迟获取不到锁,影响整个 Observable 类的并发性能。

我们知道,Vector 类不是线程安全地,在多线程环境下,同时添加、删除、遍历 Vector 对象中的元素,会出现不可预期的结果。所以,在 JDK 的代码实现中,为了避免直接给 notifyObservers() 加锁而出现性能问题,JDK 采用了一种折中的方案。这个方案有点类似于我们之前讲过的让迭代器支持 “快照” 的解决方案。

notifyObservers() 函数中,我们先拷贝一份观察者列表,赋值给函数的局部变量,局部变量是线程私有的,并不在线程间共享。这个拷贝出来的线程私有的观察者列表就相当于一个快照。我们变量快照,逐一执行每个观察者的 update() 函数。而这个遍历执行的过程是在快照这个局部变量上操作的,不存在线程安全问题,不需要加锁。所以,我们只需要拷贝创建快照的过程加锁,加锁的范围减少了很多,并发性能提高了。

为什么说这是一种折中的方案呢?这是因为,这种加锁方法实际上是存在一些问题的。**在创建快照之后,添加、删除观察者并不会更新快照,新加入的观察者就不会被通知到,新删除的观察者仍然会被通知到。**这种权衡是否能接受完全看你的业务场景。实际上,这种处理方式也是多线程中减少锁粒度、提高并发性能的常用方法。

单例模式在 Runtime 类中的应用

JDK 中 java.lang.Runtime 类就是一个单例类。在我们之前讲到 Callback 回调的时候,添加 shutdown hook 就是通过这个类实现的。

每个 Java 应用在运行时会启动一个 JVM 进程,每个 JVM 进程都只对应一个 Runtime 实例,用于查看 JVM 状态以及控制 JVM 行为。进程内唯一,所以比较适合设计为单例。在编程时,我们不能自己去实例化一个 Runtime 对象,只能通过 getRuntime() 静态方法来获得。

Runtime 类的代码实现如下所示。这里只包含部分相关代码,其他代码做了省略。从代码中,可以看出,它使用了最简单的饿汉式单例实现方式。

public class Runtime {private static Runtime currentRuntime = new Runtime();public static Runtime getRuntime() {return currentRuntime;}private Runtime() {}// ...public void addShutdownHook(Thread hook) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("shutdownHooks"));}ApplicationShutdownHooks.add(hook);}// ...
}

其他模式在 JDK 中的应用汇总

在讲解理论部分时,已经讲过很多模式在 JDK 中的应用了。这里,在一起回顾下。

在讲到模板模式时,我们结合 Java Servlet、JUnit TestCase、Java InputStream、Java AbstractList 四个例子,来具体讲解了它的两个作用:扩展性和复用性。

在讲到享元模式时,我们讲到 Integer 类中的 -128~127 之间的整型对象是可以复用的,还讲到 String 类型中的常量字符串也是可以复用的。这些都是享元模式的经典应用。

在讲到职责链模式时,我们讲到 Java Servlet 中的 Filter 就是通过职责链来实现的,同时还对比了 Spring 中的 interceptor。实际上,拦截器、过滤器这些功能绝大部分都是采用职责链模式来实现的。

在讲到迭代器模式时,我们重点剖析了 Java 中 Iterator 迭代器的实现,手把手带你实现了一个针对线性数据结构的迭代器。

总结

上篇文章和本章主要剖析了 JDK 中应用到的几个设计模式,其中重点剖析的有:工厂模式、建造者模式、装饰器模式、适配器模式、模板模式、观察者模式,此外,我们还汇总了其他模式在 JDK 中的应用,比如:单例模式、享元模式、职责链模式、迭代器模式。

实际上,源码都很简单,理解起来不难,都没有逃出我们之前讲解的理论知识范畴。学习的重点也不是表面上去理解、记忆某某类用了哪种设计模式,而是让你了解,在真是的项目开发中,如何灵活应用设计模式,做到活学活用,能够根据具体的场景、需求,做灵活的设计和实现上的调整。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/323.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在 Linux 终端中创建目录

目录 ⛳️推荐 前言 在 Linux 中创建一个新目录 创建多个新目录 创建多个嵌套的子目录 测试你的知识 ⛳️推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站 前言 在本系列的这一部…

Maven的dependencyManagement与dependencies区别

先说结论&#xff1a;Maven 使用dependencyManagement 元素来提供了一种管理依赖版本号的方式。 在maven多模块项目的pom文件中&#xff0c;有的小伙伴会发现最外层的pom文件和里面的pom文件有个地方不一样 如下图 父pom 子pom 一般来说是在maven的最外父工程pom文件里&…

npm内部机制与核心原理

npm 的核心目标&#xff1a; Bring the best of open source to you, your team and your company. npm 最重要的任务是安装和维护开源库。 npm 安装机制与背后思想 npm 的安装机制非常值得探究。Ruby 的 Gem&#xff0c;Python的pip都是全局安装机制&#xff0c;但是npm的安装…

界面组件Telerik UI for WPF 2024 Q1新版亮点 - 全新DateRangePicker组件

Telerik UI for WPF拥有超过100个控件来创建美观、高性能的桌面应用程序&#xff0c;同时还能快速构建企业级办公WPF应用程序。UI for WPF支持MVVM、触摸等&#xff0c;创建的应用程序可靠且结构良好&#xff0c;非常容易维护&#xff0c;其直观的API将无缝地集成Visual Studio…

spring事务处理

spring事务 事务介绍 一个事务要么同时成功&#xff0c;要么同时失败 特性 Atomic原子性 事务是由一个或多个活动组成的一个工作单元。原子性确保事务中的所有操作全部发生或全部不发生 Consistent一致性 一旦事务完成&#xff0c;系统必须确保它所建模的业务处于一致的状态 Is…

Hive进阶Day06

目录 一、MapReduce的计算过程 二、Yarn的资源调度 1、yarn的资源调度策略 三、Hive的语法树 四、数据开发 五、数据仓库 六、数据仓库开发流程 七、数仓分层 八、ETL和ELT 一、MapReduce的计算过程 分布式计算框架 需要编写代码执行&#xff0c;执行时会数据所在服务器…

如何正确查看容器的CPU使用率

进入容器中top&#xff0c;虽然看到的PID是容器的&#xff0c;但是%Cpu的统计信息却是宿主机的。 如图 原理 进程的cpu使用率是如何计算出来的&#xff1f; 每个进程的状态是放在文件里的&#xff0c;在/proc目录下&#xff0c;每个进程有自己pid命名的文件夹&#xff0c; …

.NET 爬虫从入门到入狱

目录 前言 1.&#x1f4a1;使用HttpClient爬取数据 2.&#x1f680;模拟User-Agent 3.&#x1f935;使用HTML解析库 3.&#x1f44c;前端Price显示 4.&#x1f331;运行实例 获取金价Au 5.&#x1f9fe;使用正则表达式解析 6.&#x1f4ab;获取BTC价格 7.✨获取CSDN热点…

4.15报错记录

打开文件时出错a bytes-like object is required,notNoneType 确保E:/data/stdata/st- images-1208-json|ST-WT-1.json是一个有效的标签文件。 今天用X-anylabling更改标签目录时出现这个报错 解决方案&#xff1a;图片文件夹中创建同名的一个文件夹把json文件放进去就可以打…

QML语法基础一

import QtQuick QML类型系统 1.基本类型:int bool real double string url list var enum 2.Quick类型:color font matrix4*4 quaternion vector2d vector3d vector4d date point size rect 等 3.javascripte类型:qml支持标准的javascript类型 4.对象类型:用于QML对象实例化 对…

[Qt网络编程]之UDP通讯的简单编程实现

hello&#xff01;欢迎大家来到我的Qt学习系列之网络编程之UDP通讯的简单编程实现。希望这篇文章能对你有所帮助&#xff01;&#xff01;&#xff01; 本篇文章的相关知识请看我的上篇文章: http://t.csdnimg.cn/UKyeM 目录 UDP通讯 基于主窗口的实现 基于线程的实现 UDP通讯…

【YOLO系列PR、F1绘图】更改v5、v7、v8(附v8训练、验证方式),实现调用val.py或者test.py后生成pr.csv,然后再整合绘制到一张图上(使用matplotlib绘制)

目录 1. 前提 效果图2. 更改步骤2.1 得到PR_curve.csv和F1_curve.csv2.1.1 YOLOv7的更改2.1.1.1 得到PR_curve.csv2.2.1.2 得到F1_curve.csv 2.1.2 YOLOv5的更改&#xff08;v6.1版本&#xff09;2.1.3 YOLOv8的更改&#xff08;附训练、验证方式&#xff09; 2.2 绘制PR曲线 …

【创建型模式】抽象工厂模式

一、抽象工厂模式概述 抽象工厂模式定义&#xff1a;提供一个创建一系列相关或相互依赖对象的接口&#xff0c;而无须指定它们具体的类。 模式动机&#xff1a; 1.当系统提供的工厂生产的具体产品并不是一个简单的对象&#xff0c;而是多个位于不同产品等级结构、属于不同类型的…

HiveSql中的函数家族(一)

一.内置函数 1-1 日期类型操作 -- 获取当前日期 select current_date(); -- 获取当前日期时间 select current_timestamp(); -- 获取unix时间&#xff08;时间戳&#xff09; 从1970年1月1号0时0分0秒 到现在过去了多少秒 select unix_timestamp();-- unix时间 和日期时间的转…

41、二叉树-二叉树的层序遍历

思路&#xff1a; 层序遍历就是从左到右依次遍历。这个时候就可以使用队列的方式。例如先把头节点入队&#xff0c;然后遍历开始&#xff0c;首先计算队列长度&#xff0c;第一层&#xff0c;长度为了&#xff0c;遍历一次&#xff0c;依次出队&#xff0c;头结点出队&#xff…

Tomcat和Spring Boot配置https

生成测试证书 生成证书前&#xff0c;先验证本地是否正确配置jdk环境变量&#xff0c;如果jdk环境变量配置正确&#xff0c;在命令行程序输入生成证书的命令。 keytool -genkey -alias tomcat -keyalg RSA -keystore "F:\job\apache-tomcat-8.5.29\key\freeHttps.keysto…

微信小程序之图片上传并保存在服务器

先将图片上传到服务器&#xff0c;后端接口将保存好的图片地址返回给小程序&#xff0c;再将小程序中添加图像的图片的url替换为服务器中照片的存储地址&#xff08;使微信小程序中显示出上传的图片&#xff09;。 1、效果如下&#xff1a; 点击图像后选择图像&#xff1a; 结…

Kafka不仅是消息队列而是一个分布式消息处理平台

目录 1. kafka架构图 2.关键概念解析 2.1 producer 2.2 consumer: 2.3 brkoer 2.4 Topic 与 Partition 2.5 AR (Assigned Replicas) 2.6 ISR(In-Sync Replicas) 2.7 OSR (Out-of-Sync Replicas) 2.8 HW (High Water-mark) 2.9 LEO (Log End Offset)

搜维尔科技:【工业仿真】煤矿机械安全事故VR警示教育系统

产品概述 搜维尔科技 煤矿机械安全事故VR警示教育系统 系统内容&#xff1a; 系统采用虚拟现实技术模拟矿井井下机械安全技术及事故&#xff0c;展现井下常见机械伤害事故&#xff0c;表现伤害事故的隐患点&#xff0c;能够模拟事故发生和发展过程&#xff1b;营造井下灾害发…

如何使用 Node.js 发送电子邮件全解和相关工具推荐

大多数Web应用程序都需要发送电子邮件。它可能用于注册、密码重置、状态报告&#xff0c;甚至是完整的市场营销活动&#xff0c;如新闻和促销。本教程解释了如何在Node.js中发送电子邮件&#xff0c;但其概念和挑战适用于您正在使用的任何系统。 你会在 npm 上找到大量与电子邮…