commit 018831037aaaccbcd7a7586c862fb4717e9a8318 Author: io2020 Date: Mon Jul 21 01:06:10 2025 +0800 init: 初始化提交--源码学习笔记 diff --git a/Dubbo/01-Dubbo 面试题.md b/Dubbo/01-Dubbo 面试题.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/02-Dubbo 学习指南.md b/Dubbo/02-Dubbo 学习指南.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/03-Dubbo 源码分析-调试环境搭建.md b/Dubbo/03-Dubbo 源码分析-调试环境搭建.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/04-Dubbo 源码分析-项目结构一览.md b/Dubbo/04-Dubbo 源码分析-项目结构一览.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/05-Dubbo 源码分析-API 配置(一)之应用.md b/Dubbo/05-Dubbo 源码分析-API 配置(一)之应用.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/06-Dubbo 源码分析-API 配置(二)之服务提供者.md b/Dubbo/06-Dubbo 源码分析-API 配置(二)之服务提供者.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/07-Dubbo 源码分析-API 配置(三)之服务消费者.md b/Dubbo/07-Dubbo 源码分析-API 配置(三)之服务消费者.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/08-Dubbo 源码分析-属性配置.md b/Dubbo/08-Dubbo 源码分析-属性配置.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/09-Dubbo 源码分析-XML 配置.md b/Dubbo/09-Dubbo 源码分析-XML 配置.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/10-Dubbo 源码分析-注解配置.md b/Dubbo/10-Dubbo 源码分析-注解配置.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/11-Dubbo 源码分析-外部化配置.md b/Dubbo/11-Dubbo 源码分析-外部化配置.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/12-Dubbo 源码分析-核心流程一览.md b/Dubbo/12-Dubbo 源码分析-核心流程一览.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/13-Dubbo 源码分析-拓展机制 SPI.md b/Dubbo/13-Dubbo 源码分析-拓展机制 SPI.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/14-Dubbo 源码分析-线程池.md b/Dubbo/14-Dubbo 源码分析-线程池.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/15-Dubbo 源码分析-服务暴露(一)之本地暴露(Injvm).md b/Dubbo/15-Dubbo 源码分析-服务暴露(一)之本地暴露(Injvm).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/16-Dubbo 源码分析-服务暴露(二)之远程暴露(Dubbo).md b/Dubbo/16-Dubbo 源码分析-服务暴露(二)之远程暴露(Dubbo).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/17-Dubbo 源码分析-服务引用(一)之本地引用(Injvm).md b/Dubbo/17-Dubbo 源码分析-服务引用(一)之本地引用(Injvm).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/18-Dubbo 源码分析-服务引用(二)之远程引用(Dubbo).md b/Dubbo/18-Dubbo 源码分析-服务引用(二)之远程引用(Dubbo).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/19-Dubbo 源码分析-Zookeeper 客户端.md b/Dubbo/19-Dubbo 源码分析-Zookeeper 客户端.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/20-Dubbo 源码分析-注册中心(一)之抽象 API.md b/Dubbo/20-Dubbo 源码分析-注册中心(一)之抽象 API.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/21-Dubbo 源码分析-注册中心(二)之 Zookeeper.md b/Dubbo/21-Dubbo 源码分析-注册中心(二)之 Zookeeper.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/22-Dubbo 源码分析-注册中心(三)之 Redis.md b/Dubbo/22-Dubbo 源码分析-注册中心(三)之 Redis.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/23-Dubbo 源码分析-动态编译(一)之 Javassist.md b/Dubbo/23-Dubbo 源码分析-动态编译(一)之 Javassist.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/24-Dubbo 源码分析-动态代理(一)之 Javassist.md b/Dubbo/24-Dubbo 源码分析-动态代理(一)之 Javassist.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/25-Dubbo 源码分析-动态代理(二)之 JDK.md b/Dubbo/25-Dubbo 源码分析-动态代理(二)之 JDK.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/26-Dubbo 源码分析-动态代理(三)之本地存根 Stub.md b/Dubbo/26-Dubbo 源码分析-动态代理(三)之本地存根 Stub.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/27-Dubbo 源码分析-服务调用(一)之本地调用(Injvm).md b/Dubbo/27-Dubbo 源码分析-服务调用(一)之本地调用(Injvm).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/28-Dubbo 源码分析-服务调用(二)之远程调用(Dubbo)【1】通信实现.md b/Dubbo/28-Dubbo 源码分析-服务调用(二)之远程调用(Dubbo)【1】通信实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/29-Dubbo 源码分析-服务调用(二)之远程调用(Dubbo)【2】同步调用.md b/Dubbo/29-Dubbo 源码分析-服务调用(二)之远程调用(Dubbo)【2】同步调用.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/30-Dubbo 源码分析-服务调用(三)之远程调用(Dubbo)【3】异步调用.md b/Dubbo/30-Dubbo 源码分析-服务调用(三)之远程调用(Dubbo)【3】异步调用.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/31-Dubbo 源码分析-服务调用(三)之远程调用(HTTP).md b/Dubbo/31-Dubbo 源码分析-服务调用(三)之远程调用(HTTP).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/32-Dubbo 源码分析-服务调用(四)之远程调用(Hessian).md b/Dubbo/32-Dubbo 源码分析-服务调用(四)之远程调用(Hessian).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/33-Dubbo 源码分析-服务调用(五)之远程调用(WebService).md b/Dubbo/33-Dubbo 源码分析-服务调用(五)之远程调用(WebService).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/34-Dubbo 源码分析-服务调用(六)之远程调用(REST).md b/Dubbo/34-Dubbo 源码分析-服务调用(六)之远程调用(REST).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/35-Dubbo 源码分析-服务调用(七)之远程调用(WebService).md b/Dubbo/35-Dubbo 源码分析-服务调用(七)之远程调用(WebService).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/36-Dubbo 源码分析-服务调用(八)之远程调用(Redis).md b/Dubbo/36-Dubbo 源码分析-服务调用(八)之远程调用(Redis).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/37-Dubbo 源码分析-服务调用(九)之远程调用(Memcached).md b/Dubbo/37-Dubbo 源码分析-服务调用(九)之远程调用(Memcached).md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/38-Dubbo 源码分析-调用特性(一)之回声测试.md b/Dubbo/38-Dubbo 源码分析-调用特性(一)之回声测试.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/39-Dubbo 源码分析-调用特性(二)之泛化引用.md b/Dubbo/39-Dubbo 源码分析-调用特性(二)之泛化引用.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/40-Dubbo 源码分析-调用特性(三)之泛化实现.md b/Dubbo/40-Dubbo 源码分析-调用特性(三)之泛化实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/41-Dubbo 源码分析-过滤器(一)之 ClassLoaderFilter.md b/Dubbo/41-Dubbo 源码分析-过滤器(一)之 ClassLoaderFilter.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/42-Dubbo 源码分析-过滤器(二)之 ContextFilter.md b/Dubbo/42-Dubbo 源码分析-过滤器(二)之 ContextFilter.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/43-Dubbo 源码分析-过滤器(三)之 AccessLogFilter.md b/Dubbo/43-Dubbo 源码分析-过滤器(三)之 AccessLogFilter.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/44-Dubbo 源码分析-过滤器(四)之 ActiveLimitFilter && ExecuteLimitFilter.md b/Dubbo/44-Dubbo 源码分析-过滤器(四)之 ActiveLimitFilter && ExecuteLimitFilter.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/45-Dubbo 源码分析-过滤器(五)之 TimeoutFilter.md b/Dubbo/45-Dubbo 源码分析-过滤器(五)之 TimeoutFilter.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/46-Dubbo 源码分析-过滤器(六)之 DeprecatedFilter.md b/Dubbo/46-Dubbo 源码分析-过滤器(六)之 DeprecatedFilter.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/47-Dubbo 源码分析-过滤器(七)之 ExceptionFilter.md b/Dubbo/47-Dubbo 源码分析-过滤器(七)之 ExceptionFilter.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/48-Dubbo 源码分析-过滤器(八)之 TokenFilter.md b/Dubbo/48-Dubbo 源码分析-过滤器(八)之 TokenFilter.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/49-Dubbo 源码分析-过滤器(九)之 TpsLimitFilter.md b/Dubbo/49-Dubbo 源码分析-过滤器(九)之 TpsLimitFilter.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/50-Dubbo 源码分析-过滤器(十)之 CacheFilter.md b/Dubbo/50-Dubbo 源码分析-过滤器(十)之 CacheFilter.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/51-Dubbo 源码分析-过滤器(十一)之 ValidationFilter.md b/Dubbo/51-Dubbo 源码分析-过滤器(十一)之 ValidationFilter.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/52-Dubbo 源码分析-NIO 服务器(一)之抽象 API.md b/Dubbo/52-Dubbo 源码分析-NIO 服务器(一)之抽象 API.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/53-Dubbo 源码分析-NIO 服务器(二)之 Transport 层.md b/Dubbo/53-Dubbo 源码分析-NIO 服务器(二)之 Transport 层.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/54-Dubbo 源码分析-NIO 服务器(三)之 Telnet 层.md b/Dubbo/54-Dubbo 源码分析-NIO 服务器(三)之 Telnet 层.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/55-Dubbo 源码分析-NIO 服务器(四)之 Exchange 层.md b/Dubbo/55-Dubbo 源码分析-NIO 服务器(四)之 Exchange 层.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/56-Dubbo 源码分析-NIO 服务器(五)之 Buffer 层.md b/Dubbo/56-Dubbo 源码分析-NIO 服务器(五)之 Buffer 层.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/57-Dubbo 源码分析-NIO 服务器(六)之 Netty4 实现.md b/Dubbo/57-Dubbo 源码分析-NIO 服务器(六)之 Netty4 实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/58-Dubbo 源码分析-NIO 服务器(七)之 Netty3 实现.md b/Dubbo/58-Dubbo 源码分析-NIO 服务器(七)之 Netty3 实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/59-Dubbo 源码分析-HTTP 服务器.md b/Dubbo/59-Dubbo 源码分析-HTTP 服务器.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/60-Dubbo 源码分析-序列化(一)之总体实现.md b/Dubbo/60-Dubbo 源码分析-序列化(一)之总体实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/61-Dubbo 源码分析-序列化(二)之 Dubbo 实现.md b/Dubbo/61-Dubbo 源码分析-序列化(二)之 Dubbo 实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/62-Dubbo 源码分析-序列化(三)之 Kryo 实现.md b/Dubbo/62-Dubbo 源码分析-序列化(三)之 Kryo 实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/63-Dubbo 源码分析-服务容器.md b/Dubbo/63-Dubbo 源码分析-服务容器.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/64-Dubbo 源码解析-集群容错(一)之抽象 API.md b/Dubbo/64-Dubbo 源码解析-集群容错(一)之抽象 API.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/65-Dubbo 源码解析-集群容错(二)之 Cluster 实现.md b/Dubbo/65-Dubbo 源码解析-集群容错(二)之 Cluster 实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/66-Dubbo 源码解析-集群容错(三)之 Directory 实现.md b/Dubbo/66-Dubbo 源码解析-集群容错(三)之 Directory 实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/67-Dubbo 源码解析-集群容错(四)之 LoadBalance 实现.md b/Dubbo/67-Dubbo 源码解析-集群容错(四)之 LoadBalance 实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/68-Dubbo 源码解析-集群容错(五)之 Merger 实现.md b/Dubbo/68-Dubbo 源码解析-集群容错(五)之 Merger 实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/69-Dubbo 源码解析-集群容错(六)之 Configurator 实现.md b/Dubbo/69-Dubbo 源码解析-集群容错(六)之 Configurator 实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/70-Dubbo 源码解析-集群容错(七)之 Router 实现.md b/Dubbo/70-Dubbo 源码解析-集群容错(七)之 Router 实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/71-Dubbo 源码解析-集群容错(八)之 Mock 实现.md b/Dubbo/71-Dubbo 源码解析-集群容错(八)之 Mock 实现.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/72-Dubbo 源码解析-优雅停机.md b/Dubbo/72-Dubbo 源码解析-优雅停机.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/73-Dubbo 源码解析-日志适配.md b/Dubbo/73-Dubbo 源码解析-日志适配.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/74-Dubbo 源码分析-集成 Spring Cloud.md b/Dubbo/74-Dubbo 源码分析-集成 Spring Cloud.md new file mode 100644 index 0000000..e69de29 diff --git a/Dubbo/file-create.py b/Dubbo/file-create.py new file mode 100644 index 0000000..a412975 --- /dev/null +++ b/Dubbo/file-create.py @@ -0,0 +1,27 @@ +import os +import pyperclip + +# 获取当前脚本的路径 +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# 从剪贴板获取文件名列表,假设每行一个文件名 +file_names = pyperclip.paste().strip().split('\n') + +# 遍历文件名列表,为每个文件名添加排序前缀并创建文件 +for index, file_name in enumerate(file_names, start=1): + # 去除文件名两端的空白字符 + file_name = file_name.strip() + + # 添加排序前缀和.md后缀 + file_name_with_prefix = f"{index:02d}-{file_name}.md" # 这里使用3位数的排序前缀,可以根据需要调整 + + # 构建文件的完整路径 + file_path = os.path.join(script_dir, file_name_with_prefix) + + # 创建文件 + with open(file_path, 'w') as file: + # 如果需要写入初始内容,可以在这里添加 + # 例如:file.write("This is the content of the file.") + pass # 如果不需要写入内容,则使用pass + +print(f"Files created successfully in {script_dir}") \ No newline at end of file diff --git a/Dubbo/tmp.txt b/Dubbo/tmp.txt new file mode 100644 index 0000000..790ae94 --- /dev/null +++ b/Dubbo/tmp.txt @@ -0,0 +1,74 @@ +Dubbo 面试题 +Dubbo 学习指南 +Dubbo 源码分析-调试环境搭建 +Dubbo 源码分析-项目结构一览 +Dubbo 源码分析-API 配置(一)之应用 +Dubbo 源码分析-API 配置(二)之服务提供者 +Dubbo 源码分析-API 配置(三)之服务消费者 +Dubbo 源码分析-属性配置 +Dubbo 源码分析-XML 配置 +Dubbo 源码分析-注解配置 +Dubbo 源码分析-外部化配置 +Dubbo 源码分析-核心流程一览 +Dubbo 源码分析-拓展机制 SPI +Dubbo 源码分析-线程池 +Dubbo 源码分析-服务暴露(一)之本地暴露(Injvm) +Dubbo 源码分析-服务暴露(二)之远程暴露(Dubbo) +Dubbo 源码分析-服务引用(一)之本地引用(Injvm) +Dubbo 源码分析-服务引用(二)之远程引用(Dubbo) +Dubbo 源码分析-Zookeeper 客户端 +Dubbo 源码分析-注册中心(一)之抽象 API +Dubbo 源码分析-注册中心(二)之 Zookeeper +Dubbo 源码分析-注册中心(三)之 Redis +Dubbo 源码分析-动态编译(一)之 Javassist +Dubbo 源码分析-动态代理(一)之 Javassist +Dubbo 源码分析-动态代理(二)之 JDK +Dubbo 源码分析-动态代理(三)之本地存根 Stub +Dubbo 源码分析-服务调用(一)之本地调用(Injvm) +Dubbo 源码分析-服务调用(二)之远程调用(Dubbo)【1】通信实现 +Dubbo 源码分析-服务调用(二)之远程调用(Dubbo)【2】同步调用 +Dubbo 源码分析-服务调用(三)之远程调用(Dubbo)【3】异步调用 +Dubbo 源码分析-服务调用(三)之远程调用(HTTP) +Dubbo 源码分析-服务调用(四)之远程调用(Hessian) +Dubbo 源码分析-服务调用(五)之远程调用(WebService) +Dubbo 源码分析-服务调用(六)之远程调用(REST) +Dubbo 源码分析-服务调用(七)之远程调用(WebService) +Dubbo 源码分析-服务调用(八)之远程调用(Redis) +Dubbo 源码分析-服务调用(九)之远程调用(Memcached) +Dubbo 源码分析-调用特性(一)之回声测试 +Dubbo 源码分析-调用特性(二)之泛化引用 +Dubbo 源码分析-调用特性(三)之泛化实现 +Dubbo 源码分析-过滤器(一)之 ClassLoaderFilter +Dubbo 源码分析-过滤器(二)之 ContextFilter +Dubbo 源码分析-过滤器(三)之 AccessLogFilter +Dubbo 源码分析-过滤器(四)之 ActiveLimitFilter && ExecuteLimitFilter +Dubbo 源码分析-过滤器(五)之 TimeoutFilter +Dubbo 源码分析-过滤器(六)之 DeprecatedFilter +Dubbo 源码分析-过滤器(七)之 ExceptionFilter +Dubbo 源码分析-过滤器(八)之 TokenFilter +Dubbo 源码分析-过滤器(九)之 TpsLimitFilter +Dubbo 源码分析-过滤器(十)之 CacheFilter +Dubbo 源码分析-过滤器(十一)之 ValidationFilter +Dubbo 源码分析-NIO 服务器(一)之抽象 API +Dubbo 源码分析-NIO 服务器(二)之 Transport 层 +Dubbo 源码分析-NIO 服务器(三)之 Telnet 层 +Dubbo 源码分析-NIO 服务器(四)之 Exchange 层 +Dubbo 源码分析-NIO 服务器(五)之 Buffer 层 +Dubbo 源码分析-NIO 服务器(六)之 Netty4 实现 +Dubbo 源码分析-NIO 服务器(七)之 Netty3 实现 +Dubbo 源码分析-HTTP 服务器 +Dubbo 源码分析-序列化(一)之总体实现 +Dubbo 源码分析-序列化(二)之 Dubbo 实现 +Dubbo 源码分析-序列化(三)之 Kryo 实现 +Dubbo 源码分析-服务容器 +Dubbo 源码解析-集群容错(一)之抽象 API +Dubbo 源码解析-集群容错(二)之 Cluster 实现 +Dubbo 源码解析-集群容错(三)之 Directory 实现 +Dubbo 源码解析-集群容错(四)之 LoadBalance 实现 +Dubbo 源码解析-集群容错(五)之 Merger 实现 +Dubbo 源码解析-集群容错(六)之 Configurator 实现 +Dubbo 源码解析-集群容错(七)之 Router 实现 +Dubbo 源码解析-集群容错(八)之 Mock 实现 +Dubbo 源码解析-优雅停机 +Dubbo 源码解析-日志适配 +Dubbo 源码分析-集成 Spring Cloud \ No newline at end of file diff --git a/jdk/01-JDK 源码解析-调试环境搭建(一)入门.md b/jdk/01-JDK 源码解析-调试环境搭建(一)入门.md new file mode 100644 index 0000000..9bafc45 --- /dev/null +++ b/jdk/01-JDK 源码解析-调试环境搭建(一)入门.md @@ -0,0 +1,65 @@ +# 精尽 JDK 源码解析 —— 调试环境搭建(一)入门 + +> 本文,我们在原来文章 [《JDK 源码解析 —— 调试环境搭建》](http://vip.iocoder.cn/JDK/build-debugging-environment/?self) 的基础上,从 JDK11 调整成 JDK13 ,并使用 Git 取代 Mercurial 获取 OpenJDK 的源码。 +> +> 在艿艿写完 [《精尽 JDK 源码解析 —— 调试环境搭建(二)进阶》](http://svip.iocoder.cn/JDK/build-debugging-environment-more?self) 之后,艿艿突然发现,从 Git 克隆的是 OpenJDK14 的 EA(Early Access抢 先体验版)。暂时不就纠正,反正胖友要记得,整个系列是基于 **OpenJDK14 的 EA** 写的。 + +# 0. 友情提示 + +JDK 源码的调试环境,实际上暂时没有特别好的方案。 + +因为,我们程序运行以来 JDK ,但是我们如果在 JDK 上增加源码相关的注释,就会导致代码行数的错乱。所以,艿艿目前的想法是,如下两个步骤: + +- 1、从官方的 Mercurial 获取 OpenJDK 源码,在上面添加源码注释。 + + > 获取 OpenJDK 源码的方式,一共有三种 + > + > - 1、从 http://jdk.java.net/ 网站上,直接下载源码。 + > - 2、使用 Mercurial ,从 http://hg.openjdk.java.net/jdk/ 克隆对应版本的源码。例如说,我们在 [《JDK 源码解析 —— 调试环境搭建》](http://vip.iocoder.cn/JDK/build-debugging-environment/?self) 文章中,就是这么干的。 + > - 3、使用 Git ,从 https://github.com/openjdk/jdk克隆对应版本的源码。该仓库就是 http://hg.openjdk.java.net/jdk/ 的镜像,方便我们使用我们所熟悉的 Git 而不是 Mercurial 。在本文,我们就会从该仓库克隆。 + +- 2、自己搭建一个项目,使用 JDK13 ,然后进行调试。 + +# 1. 获取 OpenJDK 源码 + +从官方仓库 https://github.com/openjdk/jdk Fork 出属于自己的仓库。为什么要 Fork ?既然开始阅读、调试源码,我们可能会写一些注释,有了自己的仓库,可以进行自由的提交。😈 + +使用 IntelliJ IDEA 从 Fork 出来的仓库拉取代码。因为 JDK 的源码比较大,所以拉取会花费漫长时间,耐心等待下。 + +> 如果拉取比较慢,也可以考虑使用 https://gitee.com/zhijiantianya/jdk 地址。这个是艿艿使用 Gitee 对 https://github.com/openjdk/jdk 做的镜像。毕竟,Github 对国内的网络,不是非常友好。 + +拉取完成后,我们可以搜索想看的类。例如说,HashMap 如下图所示:[![HashMap](http://static.iocoder.cn/images/JDK/2019_01_01/01.jpg)](http://static.iocoder.cn/images/JDK/2019_01_01/01.jpg)HashMap + +如此,我们就可以愉快的在其上添加源码注释。 + +> 可能胖友导入在 IDEA 看到的项目结构比较奇怪。可以先 IDEA 关闭 OpenJDK 项目,然后删除项目本地根目录下面 IDEA 相关的配置,最后再使用 IDEA 打开 OpenJDK 项目。 + +# 2. 搭建项目调试 + +① 下载 JDK + +从 https://www.oracle.com/technetwork/java/javase/downloads/jdk13-downloads-5672538.html 上,下载 Oracle JDK13 。 + +虽然说,我们在 [「1. 获取 OpenJDK 源码」](https://svip.iocoder.cn/JDK/build-debugging-environment/#) 中下载的是 OpenJDK 的源码,但是我们使用 Oracle JDK13 作为调试环境,也是没啥问题的。因为,Oracle JDK 和 OpenJDK 绝大部分源码是相同的。 + +> 如果想要纯粹以 OpenJDK 作为调试环境,可以参考 [《Mac 上的 JDK 多版本管理》](https://www.jishuwen.com/d/pm5V/zh-hk) 文章,使用 brew 安装对应版本的 OpenJDK 。 + +② 搭建项目 + +这个比较简单,就不重复赘述了。**需要注意的是,设置 Project SDK 要是 Oracle JDK13 噢**。 + +还有一个注意点,IDEA 需要升级到 2019.2 或以上版本,因为 2019.2 才支持 JDK13 。 + +> 友情提示:当然,胖友如果自己有精力,可以将**克隆的** OpenJDK13 **编译**出来,作为项目使用的 Project SDK 。 + +③ 调试 + +直接调试,这个无需多说列。 + +例如说,艿艿就在 https://github.com/YunaiV/openjdk 项目下,创建了一个叫 `yunai` 的 Maven 项目,各种创建测试类,各种调试。 + +# 666. 彩蛋 + +2019 年的下半年,艿艿准备写写大家比较常用的 JDK 类的源码解析,希望对胖友有一些帮助。共勉,2019 ,加油! + +当然,可能会有胖友会有疑惑,目前 JDK8 版本才是主流,而艿艿为什么使用 JDK13 呢?虽然说,Oracle JDK8 某个版本(具体不太记得了)之后,需要更新,但是我们最终还是会不断更新版本。Java 的强大,不是因为 Oracle ,而是完善的整个社区体系。 \ No newline at end of file diff --git a/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.assets/01.png b/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.assets/01.png new file mode 100644 index 0000000..05ac5ce Binary files /dev/null and b/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.assets/01.png differ diff --git a/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.assets/02.png b/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.assets/02.png new file mode 100644 index 0000000..6f2a4e6 Binary files /dev/null and b/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.assets/02.png differ diff --git a/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.assets/03.png b/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.assets/03.png new file mode 100644 index 0000000..c93d1b6 Binary files /dev/null and b/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.assets/03.png differ diff --git a/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.md b/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.md new file mode 100644 index 0000000..67f0acf --- /dev/null +++ b/jdk/02-JDK 源码解析-调试环境搭建(二)进阶.md @@ -0,0 +1,200 @@ +# 精尽 JDK 源码解析 —— 调试环境搭建(二)进阶 + +# 0. 概述 + +在 [《精尽 JDK 源码解析 —— 调试环境搭建(一)入门》](http://svip.iocoder.cn/JDK/build-debugging-environment?self) 中,我们已经能够简单的调试。但是,有些跟我一样“杠”的胖友,我要调试 JDK 源码,还要使用自己**编译**出来的 OpenJDK 。 + +对,所以本文,**我们就来自己编译 OpenJDK13 ,并使用它来调试**。 + +另外,因为每个人编译 OpenJDK 碰到的问题都不同,所以艿艿尽量多贴一些艿艿自己碰到的每个问题时,参考的文章。 + +还有一个,本文是基于 MacOS 系统。嘿嘿,因为手头暂时没有 Windows 的开发环境。 + +# 1. 依赖工具 + +## 1.1 编译源码 + +在 [《精尽 JDK 源码解析 —— 调试环境搭建(一)入门》](http://svip.iocoder.cn/JDK/build-debugging-environment?self) 中,我们已经使用 Git 从 https://github.com/YunaiV/openjdk 克隆了 OpenJDK**13** 的源码,直接使用它。 + +## 1.2 Xcode + +编译需要用到 Xcode 工具,一般情况下,我们都已经安装的。如果没有安装的胖友,记得安装下。T T 艿艿之前不小心卸载了,一脸懵逼。 + +建议的话,打开 https://apps.apple.com/cn/app/xcode/id497799835?mt=12 地址,去 App Store 更新到最新版本。 + +## 1.3 引导 JDK + +> 艿艿:如果嫌弃哔哔的有点长,可以看本小节最后一端。 + +编译 OpenJDK 时,需要一个引导 JDK(Boot JDK)。具体原因,艿艿看了下 [《Ubuntu 下编译 openjdk8》](https://www.jianshu.com/p/8a53708dd08a) 文章: + +> 安装引导 jdk ,openjdk 的源码编译是需要引导 jdk 的,引导 jdk 需要比要编译的 openjdk 低一个版本,比如我们要编译 openjdk8 ,就需要 jdk7 来做引导 jdk ,可以是 openjdk7 ,也可以是 oraclejdk7 。 +> +> 那么为什么需要引导 jdk 呢?我们知道 jdk 版本都是在前一个版本的基础上开发出来的,我们要编译的 openjdk8 在开发的时候,是需要用 jdk7 做环境的,在 jdk7 上做开发测试,是在 jdk7 的基础上开发出来的。 + +但是艿艿自己在编译 OpenJDK13 本地时,并未安装 JDK12 在本地。在 configure 成功,看了下自动使用了本地的 Oracle JDK13 (`java version "13-ea" 2019-09-17 Java(TM) SE Runtime Environment (build 13-ea+33) Java HotSpot(TM) 64-Bit Server VM (build 13-ea+33, mixed mode, sharing) (at /Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home)`) 。所以艿艿就搜了下,在 [《编译 OPENJDK 小记》](https://juejin.im/entry/5b3cbfaee51d451914629415) 文章: + +> 还有一个比较坑的地方。以前编译过虚拟机的同学应该都知道,编译虚拟机需要一个 `boot jdk` ,即为引导jdk。这个 `boot jdk` 是如果要编译 1.8 版本的jdk,需要 1.7 版本的jdk。 +> +> 但是笔者编译 `openjdk1.9` 的时候,总是报错 `boot jdk` 版本不对,非常蛋疼(笔者机器上装的1.8)。一开始我以为是路径不对,改了几次发现,其实编译会自动扫描 `JAVA_HOME` 下的jdk版本并且找一个合适的,在此提醒各位,如果发现有类似的问题,赶紧按照提示用 `brew cask install java` 下载一个最新版本的 jdk 就好了。 + +所以呢,**引导 JDK 这步的安装,胖友可以先忽略**。在出现问题的时候,可以直接使用 `brew cask install java` 安装一个**最新版本**的 JDK ,也是可以引导成功的。 + +## 1.4 autoconf + +``` +brew install autoconf +``` + +如果不安装 autoconf 的话,待会在 make 时会找不到配置文件。 + +# 2. 开始编译 + +## 2.1 configure + +在 OpenJDK 根目录下,执行命令如下: + +``` +# 这一句,仅仅是为了告诉胖友,一定要在根目录下 +$ pwd +/Users/yunai/Java/openjdk13 + +# 如下命令,才是正觉 +$ bash configure --enable-debug --with-jvm-variants=server --enable-dtrace +``` + +接着等待一会,如果出现如下日志,说明 configure 成功了: + +``` +Configuration summary: +* Debug level: fastdebug +* HS debug level: fastdebug +* JVM variants: server +* JVM features: server: 'aot cds cmsgc compiler1 compiler2 dtrace epsilongc g1gc graal jfr jni-check jvmci jvmti management nmt parallelgc serialgc services shenandoahgc vm-structs' +* OpenJDK target: OS: macosx, CPU architecture: x86, address length: 64 +* Version string: 14-internal+0-adhoc.yunai.openjdk13 (14-internal) + +Tools summary: +* Boot JDK: java version "13-ea" 2019-09-17 Java(TM) SE Runtime Environment (build 13-ea+33) Java HotSpot(TM) 64-Bit Server VM (build 13-ea+33, mixed mode, sharing) (at /Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home) +* Toolchain: clang (clang/LLVM from Xcode 11.0) +* C Compiler: Version 11.0.0 (at /usr/bin/clang) +* C++ Compiler: Version 11.0.0 (at /usr/bin/clang++) + +Build performance summary: +* Cores to use: 12 +* Memory limit: 32768 MB +``` + +不过往往,都是会碰到一些报错。艿艿在如下列举了部分,胖友逐个对照排查。如果没有,可以在星球里留言提问。 + +**报错 1** + +报错如下: + +``` +configure: error: No xcodebuild tool and no system framework headers found, use --with-sysroot or --with-sdk-name to provide a path to a valid SDK +``` + +> 参考文章 [《如何在 macOS 中编译 OpenJDK10 源代码》](https://blog.csdn.net/asahinokawa/article/details/82226093) 。 + +输入如下命令: + +``` +sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer +``` + +**报错 2** + +报错如下: + +``` +configure: error: Unable to determine SYSROOT and no headers found in /System/Library/Frameworks. Check Xcode configuration, --with-sysroot or --with-sdk-name arguments. +``` + +> 参考文章 [《build fails with OSX Mojave》](https://github.com/neovim/neovim/issues/9050) 。 + +输入如下命令: + +``` +open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg +``` + +## 2.2 make + +在 OpenJDK 根目录下,执行命令如下: + +``` +$ export LANG=C +$ make all +``` + +接着好久一会,如果出现如下日志,说明 make 成功了: + +``` +Creating jdk image +Creating CDS archive for jdk image +Stopping sjavac server +Finished building target 'all' in configuration 'macosx-x86_64-server-fastdebug' +``` + +到 `build` 目录下,我们来看看我们编译的 `java` 的版本: + +``` +$ build/macosx-x86_64-server-fastdebug/jdk/ +$ bin/java -version + +openjdk version "14-internal" 2020-03-17 +OpenJDK Runtime Environment (fastdebug build 14-internal+0-adhoc.yunai.openjdk13) +OpenJDK 64-Bit Server VM (fastdebug build 14-internal+0-adhoc.yunai.openjdk13, mixed mode) +``` + +- **T T 有一点很尴尬,实际我们使用的是 OpenJDK14 的 EA(Early Access抢 先体验版)。** + +一般情况下,这步是不太会碰到报错,但是也可能会碰到一些报错。艿艿在如下列举了部分,胖友逐个对照排查。如果没有,可以在星球里留言提问。 + +**报错 1** + +报错如下: + +``` +Undefined symbols for architecture x86_64: + "_objc_loadClassref", referenced from: + __ARCLite__load() in libarclite_macosx.a(arclite.o) +ld: symbol(s) not found for architecture x86_64 +``` + +> 参考文章 https://bugs.openjdk.java.net/browse/JDK-8231572 + +删除 `Lib-java.base.gmk` 文件中的 `-fobjc-link-runtime` 代码。可以看看艿艿 Git 的[461da7026034fdd63df8a9776d0b11895f361523](https://github.com/YunaiV/openjdk/commit/461da7026034fdd63df8a9776d0b11895f361523) 提交。 + +# 3. 使用调试 + +**SDKs** + +在 IDEA 中,创建我们编译出来的 OpenJDK 的 SDKs ,如下图所示:[![SDKs](02-JDK 源码解析-调试环境搭建(二)进阶.assets/01.png)](http://static.iocoder.cn/images/JDK/2019_01_04/01.png)SDKs + +**Project SDK** + +然后,记得将 Project SDK 修改成我们编译出来的 OpenJDK 的 SDK ,如下图所示: + +[![Project SDK](02-JDK 源码解析-调试环境搭建(二)进阶.assets/02.png)](http://static.iocoder.cn/images/JDK/2019_01_04/02.png)Project SDK + +**Module SDK** + +最后,还要将 Module SDK 修改成我们编译出来的 OpenJDK 的 SDK ,如下图所示:[![Module SDK](02-JDK 源码解析-调试环境搭建(二)进阶.assets/03.png)](http://static.iocoder.cn/images/JDK/2019_01_04/03.png)Module SDK + +之后,我们就可以愉快的调试了。 + +# 666. 彩蛋 + +编译的过程中,可能每个人碰到的问题不同,如下是艿艿在编译过程中,参考过的文章: + +- [《Ubuntu 下编译 openjdk8》](https://www.jianshu.com/p/8a53708dd08a) +- [《Ubuntu 下编译 openjdk11》](https://www.jianshu.com/p/53472697f2ef) +- [《在 MAC 上编译 JDK》](https://www.jianshu.com/p/b8177780c939) +- [《JVM(一):java 技术体系与编译 openjdk》](https://www.jianshu.com/p/8523d36d1d6e) +- [《编译 openJdk10》](https://thurstonzk2008.com/2019/08/26/编译openjdk10/) + +虽然 2013 年的时候,在 Ubuntu 上成功编译过一次 OpenJDK 。但是时隔 6 年之后,整个过程还是磕磕碰碰的。 + +国庆的最后一天假期的 21 点 55 分,准备休息,放空自己。 \ No newline at end of file diff --git a/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/01.png b/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/01.png new file mode 100644 index 0000000..c9b4a4d Binary files /dev/null and b/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/01.png differ diff --git a/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/02.png b/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/02.png new file mode 100644 index 0000000..9314f15 Binary files /dev/null and b/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/02.png differ diff --git a/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/03.png b/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/03.png new file mode 100644 index 0000000..cb05feb Binary files /dev/null and b/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/03.png differ diff --git a/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/04.png b/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/04.png new file mode 100644 index 0000000..f279bdf Binary files /dev/null and b/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.assets/04.png differ diff --git a/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.md b/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.md new file mode 100644 index 0000000..b3a73cd --- /dev/null +++ b/jdk/03-JDK 源码解析-集合(一)数组 ArrayList.md @@ -0,0 +1,1471 @@ +# 精尽 JDK 源码解析 —— 集合(一)数组 ArrayList + +# 1. 概述 + +ArrayList ,基于 `[]` 数组实现的,支持**自动扩容**的动态数组。相比数组来说,因为其支持**自动扩容**的特性,成为我们日常开发中,最常用的集合类,没有之一。 + +在前些年,实习或初级工程师的面试,可能最爱问的就是 ArrayList 和 LinkedList 的区别与使用场景。不过貌似,现在问的已经不多了,因为现在信息非常发达,这种常规面试题已经无法区分能力了。当然即使如此,也不妨碍我们拿它开刀,毕竟是咱的“老朋友”。 + +# 2. 类图 + +ArrayList 实现的接口、继承的抽象类,如下图所示:[![类图](03-JDK 源码解析-集合(一)数组 ArrayList.assets/01.png)](http://static.iocoder.cn/images/JDK/2019_12_01/01.png)类图 + +实现了 4 个接口,分别是: + +- [`java.util.List`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/List.java) 接口,提供数组的添加、删除、修改、迭代遍历等操作。 + +- [`java.util.RandomAccess`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/RandomAccess.java) 接口,表示 ArrayList 支持**快速**的随机访问。 + + > 关于 RandomAccess **标记**接口,我们这里先不展开,胖友可以自行阅读 [《RandomAccess 这个空架子有何用?》](https://juejin.im/post/5a26134af265da43085de060) 文章。 + +- [`java.io.Serializable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/io/Serializable.java) 接口,表示 ArrayList 支持序列化的功能。 + + > 关于 Serializable **标记**接口,我们这里先不展开,胖友可以自行阅读 [《Java 序列化与反序列化》](https://www.jianshu.com/p/e554c787c286) 文章。 + +- [`java.lang.Cloneable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/lang/Cloneable.java) 接口,表示 ArrayList 支持克隆。 + + > 关于 Cloneable **标记**接口,我们这里先不展开,胖友可以自行阅读 [《在 Java 中为什么实现了 Cloneable 接口,就能调用 Object 的 clone 方法?》](https://www.zhihu.com/question/52490586/answer/130786763) 讨论,特别是 R 大的回答。 + +继承了 [`java.util.AbstractList`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractList.java) 抽象类,而 AbstractList 提供了 List 接口的骨架实现,大幅度的减少了实现**迭代遍历**相关操作的代码。可能这样表述有点抽象,胖友点到 [`java.util.AbstractList`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractList.java) 抽象类中看看,例如说 `#iterator()`、`#indexOf(Object o)` 等方法。 + +😈 不过实际上,在下面中我们会看到,ArrayList 大量重写了 AbstractList 提供的方法实现。所以,AbstractList 对于 ArrayList 意义不大,更多的是 AbstractList 其它子类享受了这个福利。 + +# 3. 属性 + +ArrayList 的属性很少,仅仅 **2** 个。如下图所示: + +[![ArrayList](03-JDK 源码解析-集合(一)数组 ArrayList.assets/02.png)](http://static.iocoder.cn/images/JDK/2019_12_01/02.png)ArrayList + +- `elementData` 属性:元素数组。其中,图中红色空格代表我们已经添加元素,白色空格代表我们并未使用。 +- `size` 属性:数组大小。注意,`size` 代表的是 ArrayList 已使用 `elementData` 的元素的数量,对于开发者看到的 `#size()` 也是该大小。并且,当我们添加新的元素时,恰好其就是元素添加到 `elementData` 的位置(下标)。当然,我们知道 ArrayList **真正**的大小是 `elementData` 的大小。 + +对应代码如下: + +``` +// ArrayList.java + +/** + * 元素数组。 + * + * 当添加新的元素时,如果该数组不够,会创建新数组,并将原数组的元素拷贝到新数组。之后,将该变量指向新数组。 + * + * The array buffer into which the elements of the ArrayList are stored. + * The capacity of the ArrayList is the length of this array buffer. Any + * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA + * will be expanded to DEFAULT_CAPACITY when the first element is added. + */ +transient Object[] elementData; // non-private to simplify nested class access 不使用 private 修复,方便内嵌类的访问。 + +/** + * 已使用的数组大小 + * + * The size of the ArrayList (the number of elements it contains). + * + * @serial + */ +private int size; +``` + +# 4. 构造方法 + +ArrayList 一共有三个构造方法,我们分别来看看。 + +**① `#ArrayList(int initialCapacity)`** + +`#ArrayList(int initialCapacity)` 构造方法,根据传入的初始化容量,创建 ArrayList 数组。如果我们在使用时,如果预先指到数组大小,一定要使用该构造方法,可以避免数组扩容提升性能,同时也是合理使用内存。代码如下: + +``` +// ArrayList.java + +/** + * 共享的空数组对象。 + * + * 在 {@link #ArrayList(int)} 或 {@link #ArrayList(Collection)} 构造方法中, + * 如果传入的初始化大小或者集合大小为 0 时,将 {@link #elementData} 指向它。 + * + * Shared empty array instance used for empty instances. + */ +private static final Object[] EMPTY_ELEMENTDATA = {}; + +public ArrayList(int initialCapacity) { + // 初始化容量大于 0 时,创建 Object 数组 + if (initialCapacity > 0) { + this.elementData = new Object[initialCapacity]; + // 初始化容量等于 0 时,使用 EMPTY_ELEMENTDATA 对象 + } else if (initialCapacity == 0) { + this.elementData = EMPTY_ELEMENTDATA; + // 初始化容量小于 0 时,抛出 IllegalArgumentException 异常 + } else { + throw new IllegalArgumentException("Illegal Capacity: "+ + initialCapacity); + } +} +``` + +- 比较特殊的是,如果初始化容量为 0 时,使用 `EMPTY_ELEMENTDATA` 空数组。在添加元素的时候,会进行扩容创建需要的数组。 + +**② `#ArrayList(Collection c)`** + +`#ArrayList(Collection c)` 构造方法,使用传入的 `c` 集合,作为 ArrayList 的 `elementData` 。代码如下: + +``` +// ArrayList.java + +public ArrayList(Collection c) { + // 将 c 转换成 Object 数组 + elementData = c.toArray(); + // 如果数组大小大于 0 + if ((size = elementData.length) != 0) { + // defend against c.toArray (incorrectly) not returning Object[] + // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652) + // 如果集合元素不是 Object[] 类型,则会创建新的 Object[] 数组,并将 elementData 赋值到其中,最后赋值给 elementData 。 + if (elementData.getClass() != Object[].class) + elementData = Arrays.copyOf(elementData, size, Object[].class); + // 如果数组大小等于 0 ,则使用 EMPTY_ELEMENTDATA 。 + } else { + // replace with empty array. + this.elementData = EMPTY_ELEMENTDATA; + } +} +``` + +- 比较让人费解的是,在 `` 处的代码。它是用于解决 [JDK-6260652](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652) 的 Bug 。它在 JDK9 中被解决,😈 也就是说,JDK8 还会存在该问题。 + +我们来看一段能够触发 [JDK-6260652](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652) 的测试代码,然后分别在 JDK8 和 JDK13 下执行。代码如下: + +``` +// ArrayListTest.java + +public static void test02() { + List list = Arrays.asList(1, 2, 3); + Object[] array = list.toArray(); // JDK8 返回 Integer[] 数组,JDK9+ 返回 Object[] 数组。 + System.out.println("array className :" + array.getClass().getSimpleName()); + + // 此处,在 JDK8 和 JDK9+ 表现不同,前者会报 ArrayStoreException 异常,后者不会。 + array[0] = new Object(); +} +``` + +- JDK8 执行如下图所示:[![img](03-JDK 源码解析-集合(一)数组 ArrayList.assets/03.png)](http://static.iocoder.cn/images/JDK/2019_12_01/03.png) +- JDK13 执行如下图所示:[![img](03-JDK 源码解析-集合(一)数组 ArrayList.assets/04.png)](http://static.iocoder.cn/images/JDK/2019_12_01/04.png) +- 在 JDK8 中,返回的实际是 `Integer []` 数组,那么我们将 Object 对象设置到其中,肯定是会报错的。具体怎么修复的,看 [JDK-6260652](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652) 的最末尾一段。 + +**③ `#ArrayList()`** + +无参数构造方法 `#ArrayList()` 构造方法,也是我们使用最多的构造方法。代码如下: + +``` +// ArrayList.java + +/** + * 默认初始化容量 + * + * Default initial capacity. + */ +private static final int DEFAULT_CAPACITY = 10; + +/** + * 共享的空数组对象,用于 {@link #ArrayList()} 构造方法。 + * + * 通过使用该静态变量,和 {@link #EMPTY_ELEMENTDATA} 区分开来,在第一次添加元素时。 + * + * Shared empty array instance used for default sized empty instances. We + * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when + * first element is added. + */ +private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; + +/** + * Constructs an empty list with an initial capacity of ten. + */ +public ArrayList() { + this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; +} +``` + +- 在我们学习 ArrayList 的时候,一直被灌输了一个概念,在未设置初始化容量时,ArrayList 默认大小为 10 。但是此处,我们可以看到初始化为 `DEFAULTCAPACITY_EMPTY_ELEMENTDATA` 这个空数组。这是为什么呢?ArrayList 考虑到节省内存,一些使用场景下仅仅是创建了 ArrayList 对象,实际并未使用。所以,ArrayList 优化成初始化是个空数组,在首次添加元素时,才真正初始化为容量为 10 的数组。 +- 那么为什么单独声明了 `DEFAULTCAPACITY_EMPTY_ELEMENTDATA` 空数组,而不直接使用 `EMPTY_ELEMENTDATA` 呢?在下文中,我们会看到 `DEFAULTCAPACITY_EMPTY_ELEMENTDATA` 首次扩容为 10 ,而 `EMPTY_ELEMENTDATA` 按照 **1.5 倍**扩容从 0 开始而不是 10 。😈 两者的起点不同,嘿嘿。 + +# 5. 添加单个元素 + +`#add(E e)` 方法,**顺序**添加单个元素到数组。代码如下: + +``` +// ArrayList.java + +@Override +public boolean add(E e) { + // <1> 增加数组修改次数 + modCount++; + // 添加元素 + add(e, elementData, size); + // 返回添加成功 + return true; +} + +private void add(E e, Object[] elementData, int s) { + // <2> 如果容量不够,进行扩容 + if (s == elementData.length) + elementData = grow(); + // <3> 设置到末尾 + elementData[s] = e; + // <4> 数量大小加一 + size = s + 1; +} +``` + +- `<1>` 处,增加数组修改次数 `modCount` 。在父类 AbstractList 上,定义了 `modCount` 属性,用于记录数组修改次数。 +- `<2>` 处,如果元素添加的位置就超过末尾(数组下标是从 0 开始,而数组大小比最大下标大 1),说明数组容量不够,需要进行扩容,那么就需要调用 `#grow()` 方法,进行扩容。稍后我们在 [「6. 数组扩容」](https://svip.iocoder.cn/JDK/Collection-ArrayList/#) 小节来讲。 +- `<3>` 处,设置到末尾。 +- `<4>` 处,数量大小加一。 + +总体流程上来说,抛开扩容功能,和我们日常往 `[]` 数组里添加元素是一样的。 + +看懂这个方法后,胖友自己来看看 `#add(int index, E element)` 方法,**插入**单个元素到指定位置。代码如下: + +``` +// ArrayList.java + +public void add(int index, E element) { + // 校验位置是否在数组范围内 + rangeCheckForAdd(index); + // 增加数组修改次数 + modCount++; + // 如果数组大小不够,进行扩容 + final int s; + Object[] elementData; + if ((s = size) == (elementData = this.elementData).length) + elementData = grow(); + // 将 index + 1 位置开始的元素,进行往后挪 + System.arraycopy(elementData, index, + elementData, index + 1, + s - index); + // 设置到指定位置 + elementData[index] = element; + // 数组大小加一 + size = s + 1; +} + +private void rangeCheckForAdd(int index) { + if (index > size || index < 0) + throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); +} +``` + +# 6. 数组扩容 + +`#grow()` 方法,扩容数组,并返回它。整个的扩容过程,首先创建一个新的更大的数组,一般是 **1.5 倍**大小(为什么说是一般呢,稍后我们会看到,会有一些小细节),然后将原数组复制到新数组中,最后返回新数组。代码如下: + +``` +// ArrayList.java + +private Object[] grow() { + // <1> + return grow(size + 1); +} + +private Object[] grow(int minCapacity) { + int oldCapacity = elementData.length; + // <2> 如果原容量大于 0 ,或者数组不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,计算新的数组大小,并创建扩容 + if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + int newCapacity = ArraysSupport.newLength(oldCapacity, + minCapacity - oldCapacity, /* minimum growth */ + oldCapacity >> 1 /* preferred growth */); + return elementData = Arrays.copyOf(elementData, newCapacity); + // <3> 如果是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组,直接创建新的数组即可。 + } else { + return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; + } +} +``` + +- `<1>` 处,调用 `#grow(int minCapacity)` 方法,要求扩容后**至少**比原有大 1 。因为是最小扩容的要求,实际是允许比它大。 + +- ``` + <2> + ``` + + + + 处,如果原容量大于 0 时,又或者数组不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,则计算新的数组大小,并创建扩容。 + + - `ArraysSupport#newLength(int oldLength, int minGrowth, int prefGrowth)` 方法,计算新的数组大小。简单来说,结果就是 `Math.max(minGrowth, prefGrowth) + oldLength` ,按照 `minGrowth` 和 `prefGrowth` 取大的。 + - 一般情况下,从 `oldCapacity >> 1` 可以看处,是 **1.5 倍**扩容。但是会有两个特殊情况:1)初始化数组要求大小为 0 的时候,`0 >> 1` 时(`>> 1 为右移操作,相当于除以 2`)还是 0 ,此时使用 `minCapacity` 传入的 1 。2)在下文中,我们会看到添加多个元素,此时传入的 `minCapacity` 不再仅仅加 1 ,而是扩容到 `elementData` 数组恰好可以添加下多个元素,而该数量可能会超过当前 ArrayList **0.5** 倍的容量。 + +- `<3>` 处,如果是 `DEFAULTCAPACITY_EMPTY_ELEMENTDATA` 数组,直接创建新的数组即可。思考下,如果无参构造方法使用 `EMPTY_ELEMENTDATA` 的话,无法实现该效果了。 + +既然有数组扩容方法,那么是否有缩容方法呢?在 `#trimToSize()` 方法中,会创建大小恰好够用的新数组,并将原数组复制到其中。代码如下: + +``` +// ArrayList.java + +public void trimToSize() { + // 增加修改次数 + modCount++; + // 如果有多余的空间,则进行缩容 + if (size < elementData.length) { + elementData = (size == 0) + ? EMPTY_ELEMENTDATA // 大小为 0 时,直接使用 EMPTY_ELEMENTDATA + : Arrays.copyOf(elementData, size); // 大小大于 0 ,则创建大小为 size 的新数组,将原数组复制到其中。 + } +} +``` + +同时,提供 `#ensureCapacity(int minCapacity)` 方法,保证 `elementData` 数组容量至少有 `minCapacity` 。代码如下: + +``` +// ArrayList.java + +public void ensureCapacity(int minCapacity) { + if (minCapacity > elementData.length // 如果 minCapacity 大于数组的容量 + && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA + && minCapacity <= DEFAULT_CAPACITY)) { // 如果 elementData 是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的时候, + // 需要最低 minCapacity 容量大于 DEFAULT_CAPACITY ,因为实际上容量是 DEFAULT_CAPACITY 。 + // 数组修改次数加一 + modCount++; + // 扩容 + grow(minCapacity); + } +} +``` + +- 比较简单,我们可以将这个方法理解成主动扩容。 + +# 7. 添加多个元素 + +`#addAll(Collection c)` 方法,批量添加多个元素。在我们明确知道会添加多个元素时,推荐使用该该方法而不是添加单个元素,避免可能多次扩容。代码如下: + +``` +// ArrayList.java + +public boolean addAll(Collection c) { + // 转成 a 数组 + Object[] a = c.toArray(); + // 增加修改次数 + modCount++; + // 如果 a 数组大小为 0 ,返回 ArrayList 数组无变化 + int numNew = a.length; + if (numNew == 0) + return false; + // <1> 如果 elementData 剩余的空间不够,则进行扩容。要求扩容的大小,至于能够装下 a 数组。 + Object[] elementData; + final int s; + if (numNew > (elementData = this.elementData).length - (s = size)) + elementData = grow(s + numNew); + // <2> 将 a 复制到 elementData 从 s 开始位置 + System.arraycopy(a, 0, elementData, s, numNew); + // 数组大小加 numNew + size = s + numNew; + return true; +} +``` + +- `<1>` 处,如果 `elementData` 剩余的空间不足,则进行扩容。要求扩容的大小,至于能够装下 `a` 数组。当然,在 [「6. 数组扩容」](https://svip.iocoder.cn/JDK/Collection-ArrayList/#) 的小节,我们已经看到,如果要求扩容的空间太小,则扩容 **1.5 倍**。 +- `<2>` 处,将 `a` 复制到 `elementData` 从 `s` 开始位置。 + +总的看下来,就是 `#add(E e)` 方法的批量版本,优势就正如我们在本节开头说的,避免可能多次扩容。 + +看懂这个方法后,胖友自己来看看 `#addAll(int index, Collection c)` 方法,从指定位置开始插入多个元素。代码如下: + +``` +// ArrayList.java + +public boolean addAll(int index, Collection c) { + // 校验位置是否在数组范围内 + rangeCheckForAdd(index); + + // 转成 a 数组 + Object[] a = c.toArray(); + // 增加数组修改次数 + modCount++; + // 如果 a 数组大小为 0 ,返回 ArrayList 数组无变化 + int numNew = a.length; + if (numNew == 0) + return false; + // 如果 elementData 剩余的空间不够,则进行扩容。要求扩容的大小,至于能够装下 a 数组。 + Object[] elementData; + final int s; + if (numNew > (elementData = this.elementData).length - (s = size)) + elementData = grow(s + numNew); + + // 【差异点】如果 index 开始的位置已经被占用,将它们后移 + int numMoved = s - index; + if (numMoved > 0) + System.arraycopy(elementData, index, + elementData, index + numNew, + numMoved); + + // 将 a 复制到 elementData 从 s 开始位置 + System.arraycopy(a, 0, elementData, index, numNew); + // 数组大小加 numNew + size = s + numNew; + return true; +} +``` + +- 重点看【差异点】部分。 + +# 8. 移除单个元素 + +`#remove(int index)` 方法,移除指定位置的元素,并返回该位置的原元素。代码如下: + +``` +// ArrayList.java + +public E remove(int index) { + // 校验 index 不要超过 size + Objects.checkIndex(index, size); + final Object[] es = elementData; + + // 记录该位置的原值 + @SuppressWarnings("unchecked") E oldValue = (E) es[index]; + // 快速移除 + fastRemove(es, index); + + // 返回该位置的原值 + return oldValue; +} +``` + +- 重点是 `` 处,调用 `#fastRemove(Object[] es, int i)` 方法,快速移除。代码如下 + + ``` + // ArrayList.java + + private void fastRemove(Object[] es, int i) { + // 增加数组修改次数 + modCount++; + // 如果 i 不是移除最末尾的元素,则将 i + 1 位置的数组往前挪 + final int newSize; + if ((newSize = size - 1) > i) // -1 的原因是,size 是从 1 开始,而数组下标是从 0 开始。 + System.arraycopy(es, i + 1, es, i, newSize - i); + // 将新的末尾置为 null ,帮助 GC + es[size = newSize] = null; + } + ``` + + - `` 处,看起来比较复杂,胖友按照“如果 i 不是移除最末尾的元素,则将 i + 1 位置的数组往前挪”来理解,就很好懂了。 + +`#remove(Object o)` 方法,移除首个为 `o` 的元素,并返回是否移除到。代码如下: + +``` +// ArrayList.java + +public boolean remove(Object o) { + final Object[] es = elementData; + final int size = this.size; + // 寻找首个为 o 的位置 + int i = 0; + found: { + if (o == null) { // o 为 null 的情况 + for (; i < size; i++) + if (es[i] == null) + break found; + } else { // o 非 null 的情况 + for (; i < size; i++) + if (o.equals(es[i])) + break found; + } + // 如果没找到,返回 false + return false; + } + // 快速移除 + fastRemove(es, i); + // 找到了,返回 true + return true; +} +``` + +- 和 `#remove(int index)` 差不多,就是在 `` 处,改成获得首个为 `o` 的位置,之后就调用 `#fastRemove(Object[] es, int i)` 方法,快速移除即可。 + +# 9. 移除多个元素 + +我们先来看 `#removeRange(int fromIndex, int toIndex)` 方法,批量移除 `[fromIndex, toIndex)` 的多个元素,注意不包括 `toIndex` 的元素噢。代码如下: + +``` +// ArrayList.java + +protected void removeRange(int fromIndex, int toIndex) { + // 范围不正确,抛出 IndexOutOfBoundsException 异常 + if (fromIndex > toIndex) { + throw new IndexOutOfBoundsException( + outOfBoundsMsg(fromIndex, toIndex)); + } + // 增加数组修改次数 + modCount++; + // 移除 [fromIndex, toIndex) 的多个元素 + shiftTailOverGap(elementData, fromIndex, toIndex); +} + +private static String outOfBoundsMsg(int fromIndex, int toIndex) { + return "From Index: " + fromIndex + " > To Index: " + toIndex; +} +``` + +- `` 处,调用 `#shiftTailOverGap(Object[] es, int lo, int hi)` 方法,移除 `[fromIndex, toIndex)` 的多个元素。代码如下: + + ``` + // ArrayList.java + + private void shiftTailOverGap(Object[] es, int lo, int hi) { + // 将 es 从 hi 位置开始的元素,移到 lo 位置开始。 + System.arraycopy(es, hi, es, lo, size - hi); + // 将从 [size - hi + lo, size) 的元素置空,因为已经被挪到前面了。 + for (int to = size, i = (size -= hi - lo); i < to; i++) + es[i] = null; + } + ``` + + - 和 `#fastRemove(Object[] es, int i)` 方法一样的套路,先挪后置 `null` 。 + - 有一点要注意,ArrayList 特别喜欢把多行代码写成一行。所以,可能会有胖又会有疑惑,貌似这里没有修改数组的大小 `size` 啊?答案在 `i = (size -= hi - lo)` ,简直到精简到难懂。 + +`#removeAll(Collection c)` 方法,批量移除指定的多个元素。实现逻辑比较简单,但是看起来会比较绕。简单来说,通过两个变量 `w`(写入位置)和 `r`(读取位置),按照 `r` 顺序遍历数组(`elementData`),如果不存在于指定的多个元素中,则写入到 `elementData` 的 `w` 位置,然后 `w` 位置 + 1 ,跳到下一个写入位置。通过这样的方式,实现将不存在 `elementData` 覆盖写到 `w` 位置。可能理解起来有点绕,当然看代码也会有点绕绕,嘿嘿。代码如下: + +``` +// ArrayList.java + +public boolean removeAll(Collection c) { + return batchRemove(c, false, 0, size); +} +``` + +- 调用 `#batchRemove(Collection c, boolean complement, final int from, final int end)` 方法,批量移除指定的多个元素。代码如下: + + ``` + // ArrayList.java + + boolean batchRemove(Collection c, boolean complement, final int from, final int end) { + // 校验 c 非 null 。 + Objects.requireNonNull(c); + final Object[] es = elementData; + int r; + // Optimize for initial run of survivors + // <1> 优化,顺序遍历 elementData 数组,找到第一个不符合 complement ,然后结束遍历。 + for (r = from;; r++) { + // <1.1> 遍历到尾,都没不符合条件的,直接返回 false 。 + if (r == end) + return false; + // <1.2> 如果包含结果不符合 complement 时,结束 + if (c.contains(es[r]) != complement) + break; + } + // <2> 设置开始写入 w 为 r ,注意不是 r++ 。 + // r++ 后,用于读取下一个位置的元素。因为通过上的优化循环,我们已经 es[r] 是不符合条件的。 + int w = r++; + try { + // <3> 继续遍历 elementData 数组,如何符合条件,则进行移除 + for (Object e; r < end; r++) + if (c.contains(e = es[r]) == complement) // 判断符合条件 + es[w++] = e; // 移除的方式,通过将当前值 e 写入到 w 位置,然后 w 跳到下一个位置。 + } catch (Throwable ex) { + // Preserve behavioral compatibility with AbstractCollection, + // even if c.contains() throws. + // <4> 如果 contains 方法发生异常,则将 es 从 r 位置的数据写入到 es 从 w 开始的位置 + System.arraycopy(es, r, es, w, end - r); + w += end - r; + // 继续抛出异常 + throw ex; + } finally { // <5> + // 增加数组修改次数 + modCount += end - w; + // 将数组 [w, end) 位置赋值为 null 。 + shiftTailOverGap(es, w, end); + } + return true; + } + ``` + + - 不要慌,我们先一起看下每一小块的逻辑。然后,胖友自己调试下,妥妥的就明白了。 + + - ``` + complement + ``` + + + + 参数,翻译过来是“补足”的意思。怎么理解呢?表示如果 + + + + ``` + elementData + ``` + + + + 元素在 + + + + ``` + c + ``` + + + + 集合中时,是否保留。 + + - 如果 `complement` 为 `false` 时,表示在集合中,就不保留,这显然符合 `#removeAll(Collection c)` 方法要移除的意图。 + - 如果 `complement` 为 `true` 时,表示在集合中,就暴露,这符合我们后面会看到的 `#retainAll(Collection c)` 方法要求交集的意图。 + + - ``` + <1> + ``` + + + + 处,首先我们要知道这是一个基于 Optimize 优化的目的。我们是希望先判断是否 + + + + ``` + elementData + ``` + + + + 没有任何一个符合 + + + + ``` + c + ``` + + + + 的,这样就无需进行执行对应的移除逻辑。但是,我们又希望能够避免重复遍历,于是就有了这样一块的逻辑。总的来说,这块逻辑的目的是,优化,顺序遍历 + + + + ``` + elementData + ``` + + + + 数组,找到第一个不符合 + + + + ``` + complement + ``` + + + + ,然后结束遍历。 + + - `<1.1>` 处,遍历到尾,都没不符合条件的,直接返回 `false` 。也就是说,丫根就不需要进行移除的逻辑。 + - `<1.2>` 处,如果包含结果不符合 `complement` 时,结束循环。可能有点难理解,我们来举个例子。假设 `elementData` 是 `[1, 2, 3, 1]` 时,`c` 是 `[2]` 时,那么在遍历第 0 个元素 `1` 时,则 `c.contains(es[r]) != complement => false != false` 不符合,所以继续缓存;然后,在遍历第 1 个元素 `2` 时,`c.contains(es[r]) != complement => true != false` 符合,所以结束循环。此时,我们便找到了第一个需要移除的元素的位置。当然,移除不是在这里执行,我们继续往下看。😈 淡定~ + + - `<2>` 处,设置开始写入 `w` 为 `r` ,注意不是 `r++` 。这样,我们后续在循环 `elementData` 数组,就会从 `w` 开始写入。并且此时,`r` 也跳到了下一个位置,这样间接我们可以发现,`w` 位置的元素已经被“跳过”了。 + + - `<3>` 处,继续遍历 `elementData` 数组,如何符合条件,则进行移除。可能有点难理解,我们继续上述例子。遍历第 2 个元素 `3` 时候,`c.contains(es[r]) == complement => false == false` 符合,所以将 `3` 写入到 `w` 位置,同时 `w` 指向下一个位置;遍历第三个元素 `1` 时候,`c.contains(es[r]) == complement => true == false` 不符合,所以不进行任何操作。 + + - `<4>` 处,如果 contains 方法发生异常,则将 `es` 从 `r` 位置的数据写入到 `es` 从 `w` 开始的位置。这样,保证我们剩余未遍历到的元素,能够挪到从从 `w` 开始的位置,避免多出来一些元素。 + + - `<5>` 处,是不是很熟悉,将数组 `[w, end)` 位置赋值为 `null` 。 + + - 还是那句话,如果觉得绕,多调试,可以手绘点图,辅助理解下哈。 + +`#retainAll(Collection c)` 方法,求 `elementData` 数组和指定多个元素的交集。简单来说,恰好和 `#removeAll(Collection c)` 相反,移除不在 `c` 中的元素。代码如下: + +``` +// ArrayList.java + +public boolean retainAll(Collection c) { + return batchRemove(c, true, 0, size); +} +``` + +- 试着按照艿艿上面解释的,自己走一波。 + +# 10. 查找单个元素 + +`#indexOf(Object o)` 方法,查找首个为指定元素的位置。代码如下: + +``` +// ArrayList.java + +public int indexOf(Object o) { + return indexOfRange(o, 0, size); +} + +int indexOfRange(Object o, int start, int end) { + Object[] es = elementData; + // o 为 null 的情况 + if (o == null) { + for (int i = start; i < end; i++) { + if (es[i] == null) { + return i; + } + } + // o 非 null 的情况 + } else { + for (int i = start; i < end; i++) { + if (o.equals(es[i])) { + return i; + } + } + } + // 找不到,返回 -1 + return -1; +} +``` + +而 `#contains(Object o)` 方法,就是基于该方法实现。代码如下: + +``` +// ArrayList.java + +public boolean contains(Object o) { + return indexOf(o) >= 0; +} +``` + +有时我们需要查找最后一个为指定元素的位置,所以会使用到 `#lastIndexOf(Object o)` 方法。代码如下: + +``` +// ArrayList.java + +public int lastIndexOf(Object o) { + return lastIndexOfRange(o, 0, size); +} + +int lastIndexOfRange(Object o, int start, int end) { + Object[] es = elementData; + // o 为 null 的情况 + if (o == null) { + for (int i = end - 1; i >= start; i--) { // 倒序 + if (es[i] == null) { + return i; + } + } + // o 非 null 的情况 + } else { + for (int i = end - 1; i >= start; i--) { // 倒序 + if (o.equals(es[i])) { + return i; + } + } + } + + // 找不到,返回 -1 + return -1; +} +``` + +# 11. 获得指定位置的元素 + +`#get(int index)` 方法,获得指定位置的元素。代码如下: + +``` +// ArrayList.java + +public E get(int index) { + // 校验 index 不要超过 size + Objects.checkIndex(index, size); + // 获得 index 位置的元素 + return elementData(index); +} + +E elementData(int index) { + return (E) elementData[index]; +} +``` + +- 随机访问 `index` 位置的元素,时间复杂度为 O(1) 。 + +# 12. 设置指定位置的元素 + +`#set(int index, E element)` 方法,设置指定位置的元素。代码如下: + +``` +// ArrayList.java + +public E set(int index, E element) { + // 校验 index 不要超过 size + Objects.checkIndex(index, size); + // 获得 index 位置的原元素 + E oldValue = elementData(index); + // 修改 index 位置为新元素 + elementData[index] = element; + // 返回 index 位置的原元素 + return oldValue; +} +``` + +# 13. 转换成数组 + +`#toArray()` 方法,将 ArrayList 转换成 `[]` 数组。代码如下: + +``` +// ArrayList.java + +public Object[] toArray() { + return Arrays.copyOf(elementData, size); +} + +// Arrays.java + +public static T[] copyOf(T[] original, int newLength) { + return (T[]) copyOf(original, newLength, original.getClass()); +} +``` + +- 注意,返回的是 `Object[]` 类型噢。 + +实际场景下,我们可能想要指定 `T` 泛型的数组,那么我们就需要使用到 `#toArray(T[] a)` 方法。代码如下: + +``` +// ArrayList.java + +public T[] toArray(T[] a) { + // <1> 如果传入的数组小于 size 大小,则直接复制一个新数组返回 + if (a.length < size) + // Make a new array of a's runtime type, but my contents: + return (T[]) Arrays.copyOf(elementData, size, a.getClass()); + // <2> 将 elementData 复制到 a 中 + System.arraycopy(elementData, 0, a, 0, size); + // <2.1> 如果传入的数组大于 size 大小,则将 size 赋值为 null + if (a.length > size) + a[size] = null; + // <2.2> 返回 a + return a; +} +``` + +- 分成 2 个情况,根据传入的 `a` 数组是否足够大。 + +- `<1>` 处,如果传入的数组小于 `size` 大小,则直接复制一个新数组返回。一般情况下,我们不会这么干。 + +- ``` + <2> + ``` + + + + 处,将 + + + + ``` + elementData + ``` + + + + 复制到 + + + + ``` + a + ``` + + + + 中。 + + - `<2.1>` 处,如果传入的数组大于 `size` 大小,则将 `size` 位置赋值为 `null` 。额,有点没搞懂这个有啥目的。 + - `<2.2>` 处,返回传入的 `a` 。很稳。 + +- 考虑到 `<1>` 处,可能会返回一个新数组,所以即使 `<2>` 返回的就是 `a` 数组,最好使用还是按照 `a = list.toArray(a)` 。 + +# 14. 求哈希值 + +`#hashCode()` 方法,求 ArrayList 的哈希值。代码如下: + +``` +// ArrayList.java + +public int hashCode() { + // 获得当前的数组修改次数 + int expectedModCount = modCount; + // 计算哈希值 + int hash = hashCodeRange(0, size); + // 如果修改次数发生改变,则抛出 ConcurrentModificationException 异常 + checkForComodification(expectedModCount); + return hash; +} + +int hashCodeRange(int from, int to) { + final Object[] es = elementData; + // 如果 to 超过大小,则抛出 ConcurrentModificationException 异常 + if (to > es.length) { + throw new ConcurrentModificationException(); + } + // 遍历每个元素,* 31 求哈希。 + int hashCode = 1; + for (int i = from; i < to; i++) { + Object e = es[i]; + hashCode = 31 * hashCode + (e == null ? 0 : e.hashCode()); + } + return hashCode; +} +``` + +- 可能胖友会好奇,为什么使用 31 作为乘子呢?可以看看 [《科普:为什么 String hashCode 方法选择数字 31 作为乘子》](http://www.iocoder.cn/Fight/Why-did-the-String-hashCode-method-select-the-number-31-as-a-multiplier/?vip&self) 。 + +# 15. 判断相等 + +`#equals(Object o)` 方法,判断是否相等。代码如下: + +``` +// ArrayList.java + +public boolean equals(Object o) { + // 如果是自己,直接返回相等 + if (o == this) { + return true; + } + + // 如果不为 List 类型,直接不相等 + if (!(o instanceof List)) { + return false; + } + + // 获得当前的数组修改次数 + final int expectedModCount = modCount; + // ArrayList can be subclassed and given arbitrary behavior, but we can + // still deal with the common case where o is ArrayList precisely + // 根据不同类型,调用不同比对的方法。主要考虑 ArrayList 可以直接使用其 elementData 属性,性能更优。 + boolean equal = (o.getClass() == ArrayList.class) + ? equalsArrayList((ArrayList) o) + : equalsRange((List) o, 0, size); + + // 如果修改次数发生改变,则抛出 ConcurrentModificationException 异常 + checkForComodification(expectedModCount); + return equal; +} +``` + +- 可能第一眼让胖友比较费解的是,为什么根据类型是否为 ArrayList ,调用了两个不同的方法去比对呢?因为普通的 List ,我们只能使用 Iterator 进行迭代,相比 ArrayList 的 `elementData` 属性遍历,性能会略低一些。😈 处处是细节哈。 + +- 这两个方法的代码如下,已经添加详细注释。代码如下: + + ``` + // ArrayList.java + + boolean equalsRange(List other, int from, int to) { + // 如果 to 大于 es 大小,说明说明发生改变,抛出 ConcurrentModificationException 异常 + final Object[] es = elementData; + if (to > es.length) { + throw new ConcurrentModificationException(); + } + // 通过迭代器遍历 other ,然后逐个元素对比 + var oit = other.iterator(); + for (; from < to; from++) { + // 如果 oit 没有下一个,或者元素不相等,返回 false 不匹配 + if (!oit.hasNext() || !Objects.equals(es[from], oit.next())) { + return false; + } + } + // 通过 oit 是否遍历完。实现大小是否相等的效果。 + return !oit.hasNext(); + } + + private boolean equalsArrayList(ArrayList other) { + // 获得 other 数组修改次数 + final int otherModCount = other.modCount; + final int s = size; + boolean equal; + // 判断数组大小是否相等 + if (equal = (s == other.size)) { + final Object[] otherEs = other.elementData; + final Object[] es = elementData; + // 如果 s 大于 es 或者 otherEs 的长度,说明发生改变,抛出 ConcurrentModificationException 异常 + if (s > es.length || s > otherEs.length) { + throw new ConcurrentModificationException(); + } + // 遍历,逐个比较每个元素是否相等 + for (int i = 0; i < s; i++) { + if (!Objects.equals(es[i], otherEs[i])) { + equal = false; + break; // 如果不相等,则 break + } + } + } + // 如果 other 修改次数发生改变,则抛出 ConcurrentModificationException 异常 + other.checkForComodification(otherModCount); + return equal; + } + ``` + +# 16. 清空数组 + +`#clear()` 方法,清空数组。代码如下: + +``` +// ArrayList.java + +public void clear() { + // 获得当前的数组修改次数 + modCount++; + // 遍历数组,倒序设置为 null + final Object[] es = elementData; + for (int to = size, i = size = 0; i < to; i++) + es[i] = null; +} +``` + +# 17. 序列化数组 + +`#writeObject(java.io.ObjectOutputStream s)` 方法,实现 ArrayList 的序列化。代码如下: + +``` +// ArrayList.java + +@java.io.Serial +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out element count, and any hidden stuff + // 获得当前的数组修改次数 + int expectedModCount = modCount; + + // <1> 写入非静态属性、非 transient 属性 + s.defaultWriteObject(); + + // Write out size as capacity for behavioral compatibility with clone() + // <2> 写入 size ,主要为了与 clone 方法的兼容 + s.writeInt(size); + + // Write out all elements in the proper order. + // <3> 逐个写入 elementData 数组的元素 + for (int i = 0; i < size; i++) { + s.writeObject(elementData[i]); + } + + // 如果 other 修改次数发生改变,则抛出 ConcurrentModificationException 异常 + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } +} +``` + +- `<1>` 处,调用 `ObjectOutputStream#defaultWriteObject()` 方法,写入非静态属性、非 `transient` 属性。可能有些胖友不了解 Java 的序列化相关的知识,可以看看 [《Serializable 原理》](https://juejin.im/entry/5bf622436fb9a04a0b21cbe7) 文章。 +- `<2>` 处,写入 `size` ,主要为了与 clone 方法的兼容。不过艿艿也觉得挺奇怪的,明明在 `<1>` 处,已经写入了 `size` ,这里怎么还来这么一出呢?各种翻查资料,暂时只看到 [《源码分析:ArrayList 的 writeobject 方法中的实现是否多此一举?》](https://www.zhihu.com/question/41512382) 有个讨论。 +- `<3>` 吹,逐个写入 `elementData` 元素的数组。我们回过来看下 `elementData` 的定义,它是一个 `transient` 修饰的属性。为什么呢?因为 `elementData` 数组,并不一定是全满的,而可能是扩容的时候有一定的预留,如果直接序列化,会有很多空间的浪费,所以只序列化从 `[0, size)` 的元素,减少空间的占用。 + +# 18. 反序列化数组 + +`#readObject(java.io.ObjectInputStream s)` 方法,反序列化数组。代码如下: + +``` +// ArrayList.java + +@java.io.Serial +private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + + // Read in size, and any hidden stuff + // 读取非静态属性、非 transient 属性 + s.defaultReadObject(); + + // Read in capacity + // 读取 size ,不过忽略不用 + s.readInt(); // ignored + + if (size > 0) { + // like clone(), allocate array based upon size not capacity + SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size); // 不知道作甚,哈哈哈。 + // 创建 elements 数组 + Object[] elements = new Object[size]; + + // Read in all elements in the proper order. + // 逐个读取 + for (int i = 0; i < size; i++) { + elements[i] = s.readObject(); + } + + // 赋值给 elementData + elementData = elements; + } else if (size == 0) { + // 如果 size 是 0 ,则直接使用空数组 + elementData = EMPTY_ELEMENTDATA; + } else { + throw new java.io.InvalidObjectException("Invalid size: " + size); + } +} +``` + +- 和序列化的过程,恰好相反(哈哈哈,不然还想咋样),一眼就看的明白。 + +# 19. 克隆 + +`#clone()` 方法,克隆 ArrayList 对象。代码如下: + +``` +// ArrayList.java + +public Object clone() { + try { + // 调用父类,进行克隆 + ArrayList v = (ArrayList) super.clone(); + // 拷贝一个新的数组 + v.elementData = Arrays.copyOf(elementData, size); + // 设置数组修改次数为 0 + v.modCount = 0; + return v; + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError(e); + } +} +``` + +- 注意,`elementData` 是重新拷贝出来的新的数组,避免和原数组共享。 + +# 20. 创建子数组 + +`#subList(int fromIndex, int toIndex)` 方法,创建 ArrayList 的子数组。代码如下: + +``` +// ArrayList.java + +public List subList(int fromIndex, int toIndex) { + subListRangeCheck(fromIndex, toIndex, size); + return new SubList<>(this, fromIndex, toIndex); +} + +private static class SubList extends AbstractList implements RandomAccess { + + /** + * 根 ArrayList + */ + private final ArrayList root; + /** + * 父 SubList + */ + private final SubList parent; + /** + * 起始位置 + */ + private final int offset; + /** + * 大小 + */ + private int size; + + // ... 省略代码 +} +``` + +- 实际使用时,一定要注意,SubList 不是一个**只读**数组,而是和根数组 `root` 共享相同的 `elementData` 数组,只是说限制了 `[fromIndex, toIndex)` 的范围。 +- 这块的源码,并不复杂,所以这里也就不展开了。一般情况下,我们也不需要了解它的源码,嘿嘿。 + +# 21. 创建 Iterator 迭代器 + +`#iterator()` 方法,创建迭代器。一般情况下,我们使用迭代器遍历 ArrayList、LinkedList 等等 List 的实现类。代码如下: + +``` +// ArrayList.java + +public Iterator iterator() { + return new Itr(); +} +``` + +- 创建 Itr 迭代器。Itr 实现 [`java.util.Iterator`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/List.java) 接口,是 ArrayList 的内部类。虽然说 AbstractList 也提供了一个 Itr 的实现,但是 ArrayList 为了更好的性能,所以自己实现了,在其类上也有注释“An optimized version of AbstractList.Itr”。 + +Itr 一共有 3 个属性,如下: + +``` +// ArrayList.java#Itr + +/** + * 下一个访问元素的位置,从下标 0 开始。 + */ +int cursor; // index of next element to return +/** + * 上一次访问元素的位置。 + * + * 1. 初始化为 -1 ,表示无上一个访问的元素 + * 2. 遍历到下一个元素时,lastRet 会指向当前元素,而 cursor 会指向下一个元素。这样,如果我们要实现 remove 方法,移除当前元素,就可以实现了。 + * 3. 移除元素时,设置为 -1 ,表示最后访问的元素不存在了,都被移除咧。 + */ +int lastRet = -1; // index of last element returned; -1 if no such +/** + * 创建迭代器时,数组修改次数。 + * + * 在迭代过程中,如果数组发生了变化,会抛出 ConcurrentModificationException 异常。 + */ +int expectedModCount = modCount; + +// prevent creating a synthetic constructor +Itr() {} +``` + +- 每个属性,胖友自己看看注释噢。 + +下面,让我们来看看 Itr 对 Iterator 的 4 个实现方法。 + +`#hasNext()` 方法,判断是否还可以继续迭代。代码如下: + +``` +// ArrayList.java#Itr + +public boolean hasNext() { + return cursor != size; +} +``` + +- `cursor` 如果等于 `size` ,说明已经到数组末尾,无法继续迭代了。 + +`#next()` 方法,下一个元素。代码如下: + +``` +// ArrayList.java#Itr + +public E next() { + // 校验是否数组发生了变化 + checkForComodification(); + // 判断如果超过 size 范围,抛出 NoSuchElementException 异常 + int i = cursor; // <1> i 记录当前 cursor 的位置 + if (i >= size) + throw new NoSuchElementException(); + // 判断如果超过 elementData 大小,说明可能被修改了,抛出 ConcurrentModificationException 异常 + Object[] elementData = ArrayList.this.elementData; + if (i >= elementData.length) + throw new ConcurrentModificationException(); + // <2> cursor 指向下一个位置 + cursor = i + 1; + // <3> 返回当前位置的元素 + return (E) elementData[lastRet = i]; // <4> 此处,会将 lastRet 指向当前位置 +} + +final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); +} +``` + +- `<1>` 处,记录当前 `cursor` 的位置。因为我们当前返回的就是要求 `cursor` 位置的元素。 +- `<2>` 处,`cursor` 指向下一个位置。 +- `<3>` 处,返回当前位置的元素。同时在 `<4>` 处,会将 `lastRet` 指向当前位置。 + +`#remove()` 方法,移除当前元素。代码如下: + +``` +// ArrayList.java#Itr + +public void remove() { + // 如果 lastRet 小于 0 ,说明没有指向任何元素,抛出 IllegalStateException 异常 + if (lastRet < 0) + throw new IllegalStateException(); + // 校验是否数组发生了变化 + checkForComodification(); + + try { + // <1> 移除 lastRet 位置的元素 + ArrayList.this.remove(lastRet); + // <2> cursor 指向 lastRet 位置,因为被移了,所以需要后退下 + cursor = lastRet; + // <3> lastRet 标记为 -1 ,因为当前元素被移除了 + lastRet = -1; + // <4> 记录新的数组的修改次数 + expectedModCount = modCount; + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } +} +``` + +- `<1>` 处,调用 `#remove(int index)` 方法,移除 `lastRet` 位置的元素。所以,如果要注意,如果移除元素比较前面,会将后面位置的往前挪,即复制,可能比较消耗性能。 +- `<2>` 处,`cursor` 指向 `lastRet` 位置,因为被移了,所以需要后退下。 +- `<3>` 处,`lastRet` 标记为 `-1` ,因为当前元素被移除了。 +- `<4>` 处,记录新的数组的修改次数。因为此处修改了数组,如果不修改下,后续迭代肯定会报错。 + +`#forEachRemaining(Consumer action)` 方法,消费剩余未迭代的元素。代码如下: + +``` +// ArrayList.java#Itr + +@Override +public void forEachRemaining(Consumer action) { + // 要求 action 非空 + Objects.requireNonNull(action); + // 获得当前数组大小 + final int size = ArrayList.this.size; + // 记录 i 指向 cursor + int i = cursor; + if (i < size) { + // 判断如果超过 elementData 大小,说明可能被修改了,抛出 ConcurrentModificationException 异常 + final Object[] es = elementData; + if (i >= es.length) + throw new ConcurrentModificationException(); + // 逐个处理 + for (; i < size && modCount == expectedModCount; i++) + action.accept(elementAt(es, i)); + // update once at end to reduce heap write traffic + // 更新 cursor 和 lastRet 的指向 + cursor = i; + lastRet = i - 1; + // 校验是否数组发生了变化 + checkForComodification(); + } +} +``` + +- 比较简单,胖友自己瞅瞅。貌似平时这个方法用的不是很多。 + +# 22. 创建 ListIterator 迭代器 + +> 艿艿:可能一些胖友不了解 ListIterator 迭代器,因为平时使用不多。可以先去看看 [《Java 集合框架之 Iterator 和 ListIterator》](https://my.oschina.net/jackieyeah/blog/220134) 。简单来说,ListIterator 是为 List 设计的,功能更强大的 Iterator 迭代器。 + +`#listIterator(...)` 方法,创建 ListIterator 迭代器。代码如下: + +``` +// ArrayList.java + +public ListIterator listIterator(int index) { + rangeCheckForAdd(index); + return new ListItr(index); +} + +public ListIterator listIterator() { + return new ListItr(0); +} +``` + +- 创建 ListItr 迭代器。ListItr 实现 [`java.util.ListIterator`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/ListIterator.java) 接口,是 ArrayList 的内部类。虽然说 AbstractList 也提供了一个 ListItr 的实现,但是 ArrayList 为了更好的性能,所以自己实现了,在其类上也有注释“An optimized version of AbstractList.ListItr”。 + +ListItr 直接继承 Itr 类,无自定义的属性。代码如下: + +``` +// ArrayList.java#ListItr + +ListItr(int index) { + super(); + cursor = index; +} +``` + +- 可以手动设置指定的位置开始迭代。 + +因为 ListItr 的实现代码比较简单,我们就不逐个来看了,直接贴加了注释的代码。代码如下: + +``` +// ArrayList.java#ListItr + +/** + * @return 是否有前一个 + */ +public boolean hasPrevious() { + return cursor != 0; +} + +/** + * @return 下一个位置 + */ +public int nextIndex() { + return cursor; +} + +/** + * @return 前一个位置 + */ +public int previousIndex() { + return cursor - 1; +} + +/** + * @return 前一个元素 + */ +@SuppressWarnings("unchecked") +public E previous() { + // 校验是否数组发生了变化 + checkForComodification(); + // 判断如果小于 0 ,抛出 NoSuchElementException 异常 + int i = cursor - 1; + if (i < 0) + throw new NoSuchElementException(); + // 判断如果超过 elementData 大小,说明可能被修改了,抛出 ConcurrentModificationException 异常 + Object[] elementData = ArrayList.this.elementData; + if (i >= elementData.length) + throw new ConcurrentModificationException(); + // cursor 指向上一个位置 + cursor = i; + // 返回当前位置的元素 + return (E) elementData[lastRet = i]; // 此处,会将 lastRet 指向当前位置 +} + +/** + * 设置当前元素 + * + * @param e 设置的元素 + */ +public void set(E e) { + // 如果 lastRet 无指向,抛出 IllegalStateException 异常 + if (lastRet < 0) + throw new IllegalStateException(); + // 校验是否数组发生了变化 + checkForComodification(); + + try { + // 设置 + ArrayList.this.set(lastRet, e); + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } +} + +/** + * 添加元素当当前位置 + * + * @param e 添加的元素 + */ +public void add(E e) { + // 校验是否数组发生了变化 + checkForComodification(); + + try { + // 添加元素到当前位置 + int i = cursor; + ArrayList.this.add(i, e); + // cursor 指向下一个位置,因为当前位置添加了新的元素,所以需要后挪 + cursor = i + 1; + // lastRet 标记为 -1 ,因为当前元素并未访问 + lastRet = -1; + // 记录新的数组的修改次数 + expectedModCount = modCount; + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } +} +``` + +# 666. 彩蛋 + +咳咳咳,比想象中的长的多的一篇文章。并且实际上,我们还有几个 ArrayList 的方法的解析没有写,如下: + +- `#spliterator()` +- `#removeIf(Predicate filter)` +- `#replaceAll(UnaryOperator operator)` +- `#sort(Comparator c)` +- `#forEach(Consumer action)` + +哈哈,也是比较简单的方法,胖友自己可以解决一波的哈。就当,课后作业?!嘿嘿。 + +下面,我们来对 ArrayList 做一个简单的小结: + +- ArrayList 是基于 `[]` 数组实现的 List 实现类,支持在数组容量不够时,一般按照 **1.5** 倍**自动**扩容。同时,它支持**手动**扩容、**手动**缩容。 + +- ArrayList 随机访问时间复杂度是 O(1) ,查找指定元素的**平均**时间复杂度是 O(n) 。 + + > 可能胖友对时间复杂度的计算方式不是很了解,可以看看 [《算法复杂度分析(上):分析算法运行时,时间资源及空间资源的消耗》](https://www.cnblogs.com/jonins/p/9950799.html) 和 [《算法复杂度分析(下):最好、最坏、平均、均摊等时间复杂度概述》](https://www.cnblogs.com/jonins/p/9956752.html) 两文。 + +- ArrayList 移除指定位置的元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。 + + > 最好时间复杂度发生在末尾移除的情况。 + +- ArrayList 移除指定元素的时间复杂度是 O(n) 。 + + > 因为首先需要进行查询,然后在使用移除指定位置的元素,无论怎么计算,都需要 O(n) 的时间复杂度。 + +- ArrayList 添加元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。 + + > 最好时间复杂度发生在末尾添加的情况。 + +结尾在抛个拓展,在 Redis String 的数据结构,实现方式是类似 Java ArrayList 的方式,感兴趣的胖友可以自己去瞅瞅。 \ No newline at end of file diff --git a/jdk/04-JDK 源码解析-集合(二)链表 LinkedList.assets/01.jpg b/jdk/04-JDK 源码解析-集合(二)链表 LinkedList.assets/01.jpg new file mode 100644 index 0000000..fdc17c1 Binary files /dev/null and b/jdk/04-JDK 源码解析-集合(二)链表 LinkedList.assets/01.jpg differ diff --git a/jdk/04-JDK 源码解析-集合(二)链表 LinkedList.assets/02.jpg b/jdk/04-JDK 源码解析-集合(二)链表 LinkedList.assets/02.jpg new file mode 100644 index 0000000..ae2f18f Binary files /dev/null and b/jdk/04-JDK 源码解析-集合(二)链表 LinkedList.assets/02.jpg differ diff --git a/jdk/04-JDK 源码解析-集合(二)链表 LinkedList.md b/jdk/04-JDK 源码解析-集合(二)链表 LinkedList.md new file mode 100644 index 0000000..c2355a9 --- /dev/null +++ b/jdk/04-JDK 源码解析-集合(二)链表 LinkedList.md @@ -0,0 +1,1279 @@ +# 精尽 JDK 源码解析 —— 集合(二)链表 LinkedList + +> 考虑到 LinkedList 和 ArrayList 是 List 绝代双骄,所以本文在编写的时候,尽量保持标题一致,方便胖友对比。 +> +> 相比来说,LinkedList 会简单蛮多。看完本文后,胖友可以试着做下 [设计链表](https://leetcode-cn.com/problems/design-linked-list/) 题目。 + +# 1. 概述 + +LinkedList ,基于节点实现的**双向**链表的 List ,每个节点都指向前一个和后一个节点从而形成链表。 + +相比 ArrayList 来说,我们日常开发使用 LinkedList 相对比较少。如果胖友打开 IDEA ,搜下项目中 LinkedList 后,会发现使用的少之又少。 + +# 2. 类图 + +LinkedList 实现的接口、继承的抽象类,如下图所示:[![类图](04-JDK 源码解析-集合(二)链表 LinkedList.assets/01.jpg)](http://static.iocoder.cn/images/JDK/2019_12_04/01.jpg)类图 + +如下 3 个接口是 ArrayList 一致的: + +- [`java.util.List`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/List.java) 接口 +- [`java.io.Serializable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/io/Serializable.java) 接口 +- [`java.lang.Cloneable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/lang/Cloneable.java) 接口 + +如下 1 个接口是少于 ArrayList 的: + +- [`java.util.RandomAccess`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/RandomAccess.java) 接口,LinkedList 不同于 ArrayList 的很大一点,不支持随机访问。 + +如下 1 个接口是多于 ArrayList 的: + +- [`java.util.Deque`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/Deque.java) 接口,提供**双端**队列的功能,LinkedList 支持快速的在头尾添加元素和读取元素,所以很容易实现该特性。 + + > 注意,以为 LinkedList 实现了 Deque 接口,所以我们在 [「5. 添加单个元素」](https://svip.iocoder.cn/JDK/Collection-LinkedList/#) 和 [「7. 移除单个元素」](https://svip.iocoder.cn/JDK/Collection-LinkedList/#) 中,会看到多种方法,胖友可以快速看过去即可。😈 因为确实蛮多的。 + > + > 也因为实现 Deque 即可以作为队列使用,也可以作为栈使用。当然,作为双端队列,也是可以的。 + +继承了 [`java.util.AbstractSequentialList`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractSequentialList.java) 抽象类,它是 AbstractList 的子类,实现了只能**连续**访问“数据存储”(例如说链表)的 `#get(int index)`、`#add(int index, E element)` 等等**随机**操作的方法。可能这样表述有点抽象,胖友点到 [`java.util.AbstractSequentialList`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractSequentialList.java) 抽象类中看看这几个方法,基于迭代器顺序遍历后,从而实现后续的操作。 + +- 但是呢,LinkedList 和 ArrayList 多是一个有点“脾气”的小伙子,都为了结合自身的特性,更加高效的实现,多选择了重写了 AbstractSequentialList 的方法,嘿嘿。 +- 不过一般情况下,对于支持随机访问数据的继承 AbstractList 抽象类,不支持的继承 AbstractSequentialList 抽象类。 + +# 3. 属性 + +LinkedList 一共有 **3** 个属性。如下图所示: + +[![LinkedList](04-JDK 源码解析-集合(二)链表 LinkedList.assets/02.jpg)](http://static.iocoder.cn/images/JDK/2019_12_04/02.jpg)LinkedList + +> 艿艿:发现自己真是画图鬼才,画的真丑,哈哈哈哈。 + +- 通过 Node 节点指向前后节点,从而形成双向链表。 + +- ``` + first + ``` + + + + 和 + + + + ``` + last + ``` + + + + 属性:链表的头尾指针。 + + - 在初始时候,`first` 和 `last` 指向 `null` ,因为此时暂时没有 Node 节点。 + - 在添加完首个节点后,创建对应的 Node 节点 `node1` ,前后指向 `null` 。此时,`first` 和 `last` 指向该 Node 节点。 + - 继续添加一个节点后,创建对应的 Node 节点 `node2` ,其 `prev = node1` 和 `next = null` ,而 `node1` 的 `prev = null` 和 `next = node2` 。此时,`first` 保持不变,指向 `node1` ,`last` 发生改变,指向 `node2` 。 + +- `size` 属性:链表的节点数量。通过它进行计数,避免每次需要 List 大小时,需要从头到尾的遍历。 + +对应代码如下: + +``` +// LinkedList.java + +/** + * 链表大小 + */ +transient int size = 0; + +/** + * 头节点 + * + * Pointer to first node. + */ +transient Node first; + +/** + * 尾节点 + * + * Pointer to last node. + */ +transient Node last; + +/** + * 节点 + * + * @param 元素泛型 + */ +private static class Node { + + /** + * 元素 + */ + E item; + /** + * 前一个节点 + */ + Node next; + /** + * 后一个节点 + */ + Node prev; + + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } + +} +``` + +# 4. 构造方法 + +ArrayList 一共有两个构造方法,我们分别来看看。代码如下: + +``` +public LinkedList() { +} + +public LinkedList(Collection c) { + this(); + // 添加 c 到链表中 + addAll(c); +} +``` + +相比 ArrayList 来说,因为没有容量一说,所以不需要提供 `#ArrayList(int initialCapacity)` 这样的构造方法。 + +# 5. 添加单个元素 + +`#add(E e)` 方法,**顺序**添加单个元素到链表。代码如下: + +``` +// LinkedList.java + +public boolean add(E e) { + // 添加末尾 + linkLast(e); + return true; +} + +void linkLast(E e) { + // <1> 记录原 last 节点 + final Node l = last; + // <2> 创建新节点 + // 第一个参数表示,newNode 的前一个节点为 l 。 + // 第二个参数表示,e 为元素。 + // 第三个参数表示,newNode 的后一个节点为 null 。 + final Node newNode = new Node<>(l, e, null); + // <3> last 指向新节点 + last = newNode; + // <4.1> 如果原 last 为 null ,说明 first 也为空,则 first 也指向新节点 + if (l == null) + first = newNode; + // <4.2> 如果原 last 非 null ,说明 first 也非空,则原 last 的 next 指向新节点。 + else + l.next = newNode; + // <5> 增加链表大小 + size++; + // <6> 增加数组修改次数 + modCount++; +} +``` + +- `` 处,调用 `#linkLast(E e)` 方法,将新元素添加到链表的尾巴。所以,`#add(E e)` 方法,实际就是 `#linkLast(E e)` 方法。 +- 总体来说,代码实现比较简单。重点就是对 `last` 的处理。 +- 相比 ArrayList 来说,无需考虑容量不够时的扩容。 + +看懂这个方法后,我们来看看 `#add(int index, E element)` 方法,**插入**单个元素到指定位置。代码如下: + +``` +// LinkedList.java + +public void add(int index, E element) { + // 校验不要超过范围 + checkPositionIndex(index); + + // <1> 如果刚好等于链表大小,直接添加到尾部即可 + if (index == size) + linkLast(element); + // <2> 添加到第 index 的节点的前面 + else + linkBefore(element, node(index)); +} +``` + +- `<1>` 处,如果刚好等于链表大小,直接调用 `#linkLast(E element)` 方法,添加到尾部即可。 + +- `<2>` 处,先调用 `#node(int index)` 方法,获得第 `index` 位置的 Node 节点 `node` 。然后,调用 `#linkBefore(E element, Node node)` 方法,将新节点添加到 `node` 的前面。相当于说,`node` 的前一个节点的 `next` 指向新节点,`node` 的 `prev` 指向新节点。 + +- `#node(int index)` 方法,获得第 `index` 个 Node 节点。代码如下: + + ``` + // LinkedList.java + + Node node(int index) { + // assert isElementIndex(index); + + // 如果 index 小于 size 的一半,就正序遍历,获得第 index 个节点 + if (index < (size >> 1)) { + Node x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + // 如果 index 大于 size 的一半,就倒序遍历,获得第 index 个节点 + } else { + Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } + } + ``` + + - 这里 LinkedList 做的一个小骚操作,根据 `index` 是否超过链表的一半大小,选择是否使用倒序遍历替代正序遍历,从而减少遍历次数。 + +- `#linkBefore(E e, Node succ)` 方法,添加元素 `e` 到 `succ` 节点的前面。代码如下: + + ``` + // LinkedList.java + + void linkBefore(E e, Node succ) { + // assert succ != null; + // 获得 succ 的前一个节点 + final Node pred = succ.prev; + // 创建新的节点 newNode + final Node newNode = new Node<>(pred, e, succ); + // 设置 succ 的前一个节点为新节点 + succ.prev = newNode; + // 如果 pred 为 null ,说明 first 也为空,则 first 也指向新节点 + if (pred == null) + first = newNode; + // 如果 pred 非 null ,说明 first 也为空,则 pred 也指向新节点 + else + pred.next = newNode; + // 增加链表大小 + size++; + // 增加数组修改次数 + modCount++; + } + ``` + + - 逻辑上,和 `#linkLast(E e)` 方法差不多。差别在于 `` 处,设置 `succ` 的前一个节点为新节点。 + +因为 LinkedList 实现了 Deque 接口,所以它实现了 `#addFirst(E e)` 和 `#addLast(E e)` 方法,分别添加元素到链表的头尾。代码如下: + +``` +// LinkedList.java 实现 Deque 接口 + +public void addFirst(E e) { + linkFirst(e); +} +public boolean offerFirst(E e) { + addFirst(e); // 调用上面的方法 + return true; +} + +public void addLast(E e) { + linkLast(e); +} +public boolean offerLast(E e) { + addLast(e); // 调用上面的方法 + return true; +} +``` + +- `#linkLast(E e)` 方法,和 `#add(E e)` 方法是一致的,就不哔哔了。 + +- `#addFirst(E e)` 方法,调用 `#linkFirst(E e)` 方法,添加元素到队头。代码如下: + + ``` + // LinkedList.java + + private void linkFirst(E e) { + // 记录原 first 节点 + final Node f = first; + // 创建新节点 + final Node newNode = new Node<>(null, e, f); + // first 指向新节点 + first = newNode; + // 如果原 first 为空,说明 last 也为空,则 last 也指向新节点 + if (f == null) + last = newNode; + // 如果原 first 非空,说明 last 也非空,则原 first 的 next 指向新节点。 + else + f.prev = newNode; + // 增加链表大小 + size++; + // 增加数组修改次数 + modCount++; + } + ``` + + - 逻辑上,和 `#linkLast(E e)` 方法差不多。就不重复哔哔了。 + +因为 LinkedList 实现了 Queue 接口,所以它实现了 `#push(E e)` 和 `#offer(E e)` 方法,添加元素到链表的头尾。代码如下: + +``` +// LinkedList.java 实现 Queue 接口 + +public void push(E e) { + addFirst(e); +} + +public boolean offer(E e) { + return add(e); +} +``` + +总的来说,添加单个元素,分成三个情况: + +- 添加元素到队头 +- 添加元素到队尾 +- 添加元素到中间 + +对于链表的操作,代码会比较简洁,胖友如果不太理解,可以在草稿纸上手绘下整个过程。 + +# 6. 链表扩容 + +LinkedList 不存在扩容的需求,因为通过 Node 的前后指向即可。 + +# 7. 添加多个元素 + +`#addAll(Collection c)` 方法,批量添加多个元素。代码如下: + +``` +// LinkedList.java + +public boolean addAll(Collection c) { + return addAll(size, c); +} + +public boolean addAll(int index, Collection c) { + checkPositionIndex(index); + + // <1> 将 c 转成 a 数组 + Object[] a = c.toArray(); + int numNew = a.length; + if (numNew == 0) // 如果无添加元素,直接返回 false 数组未变更 + return false; + + // <2> 获得第 index 位置的节点 succ ,和其前一个节点 pred + Node pred, succ; + if (index == size) { // 如果 index 就是链表大小,那说明插入队尾,所以 succ 为 null ,pred 为 last 。 + succ = null; + pred = last; + } else { // 如果 index 小于链表大小,则 succ 是第 index 个节点,prev 是 succ 的前一个二节点。 + succ = node(index); + pred = succ.prev; + } + + // <3> 遍历 a 数组,添加到 pred 的后面 + for (Object o : a) { + // 创建新节点 + @SuppressWarnings("unchecked") E e = (E) o; + Node newNode = new Node<>(pred, e, null); + // 如果 pred 为 null ,说明 first 也为 null ,则直接将 first 指向新节点 + if (pred == null) + first = newNode; + // pred 下一个指向新节点 + else + pred.next = newNode; + // 修改 pred 指向新节点 + pred = newNode; + } + + // <4> 修改 succ 和 pred 的指向 + if (succ == null) { // 如果 succ 为 null ,说明插入队尾,则直接修改 last 指向最后一个 pred + last = pred; + } else { // 如果 succ 非 null ,说明插入到 succ 的前面 + pred.next = succ; // prev 下一个指向 succ + succ.prev = pred; // succes 前一个指向 pred + } + + // <5> 增加链表大小 + size += numNew; + // <6> 增加数组修改次数 + modCount++; + // 返回 true 数组有变更 + return true; +} +``` + +- `#addAll(Collection c)` 方法,其内部调用的是 `#addAll(int index, Collection c)` 方法,表示在队列之后,继续添加 `c` 集合。 +- `<2>` 处,获得第 `index` 位置的节点 `succ` ,和其前一个节点 `pred` 。分成两种情况,胖友自己看注释。实际上,ArrayList 在添加 `c` 集合的时候,也是分成跟 LinkedList 一样的两种情况,只是说 LinkedList 在一个方法统一实现了。 +- `<3>` 处,遍历 `a` 数组,添加到 `pred` 的后面。其实,我们可以把 `pred` 理解成“尾巴”,然后不断的指向新节点,而新节点又称为新的 `pred` 尾巴。如此反复插入~ +- `<4>` 处,修改 `succ` 和 `pred` 的指向。根据 `<2>` 处分的两种情况,进行处理。 +- 😈 虽然很长,但是还是很简单的。 + +# 8. 移除单个元素 + +`#remove(int index)` 方法,移除指定位置的元素,并返回该位置的原元素。代码如下: + +``` +// LinkedList.java + +public E remove(int index) { + checkElementIndex(index); + // 获得第 index 的 Node 节点,然后进行移除。 + return unlink(node(index)); +} +``` + +- 首先,调用 `#node(int index)` 方法,获得第 `index` 的 Node 节点。然后偶,调用 `#unlink(Node x)` 方法,移除该节点。 + +- `#unlink(Node x)` 方法,代码如下: + + ``` + // LinkedList.java + + E unlink(Node x) { + // assert x != null; + // <1> 获得 x 的前后节点 prev、next + final E element = x.item; + final Node next = x.next; + final Node prev = x.prev; + + // <2> 将 prev 的 next 指向下一个节点 + if (prev == null) { // <2.1> 如果 prev 为空,说明 first 被移除,则直接将 first 指向 next + first = next; + } else { // <2.2> 如果 prev 非空 + prev.next = next; // prev 的 next 指向 next + x.prev = null; // x 的 pre 指向 null + } + + // <3> 将 next 的 prev 指向上一个节点 + if (next == null) { // <3.1> 如果 next 为空,说明 last 被移除,则直接将 last 指向 prev + last = prev; + } else { // <3.2> 如果 next 非空 + next.prev = prev; // next 的 prev 指向 prev + x.next = null; // x 的 next 指向 null + } + + // <4> 将 x 的 item 设置为 null ,帮助 GC + x.item = null; + // <5> 减少链表大小 + size--; + // <6> 增加数组的修改次数 + modCount++; + return element; + } + ``` + + - `<2>` 处,将 `prev` 的 `next` 指向下一个节点。其中,`<2.1>` 处,是移除队头 `first` 的情况。 + - `<3>` 处,将 `next` 的 `prev` 指向上一个节点。其中,`<3.1>` 处,如果 `next` 为空,说明队尾 `last` 被移除的情况。 + - 其它步骤,胖友自己看看代码注释。 + +`#remove(Object o)` 方法,移除首个为 `o` 的元素,并返回是否移除到。代码如下: + +``` +// LinkedList.java + +public boolean remove(Object o) { + if (o == null) { // o 为 null 的情况 + // 顺序遍历,找到 null 的元素后,进行移除 + for (Node x = first; x != null; x = x.next) { + if (x.item == null) { + unlink(x); + return true; + } + } + } else { + // 顺序遍历,找到等于 o 的元素后,进行移除 + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) { + unlink(x); + return true; + } + } + } + return false; +} +``` + +- 相比 `#remove(int index)` 方法来说,需要去寻找首个等于 `o` 的节点进行移除。当然,最终还是调用 `#unlink(Node x)` 方法,移除该节点。 + +`#removeFirstOccurrence(Object o)` 和 `#removeLastOccurrence(Object o)` 方法,分别实现移除链表首个节点和最后节点。代码如下: + +``` +// LinkedList.java 实现 Deque 接口 + +public boolean removeFirstOccurrence(Object o) { // 移除首个 + return remove(o); +} + +public boolean removeLastOccurrence(Object o) { + if (o == null) { // o 为 null 的情况 + // 倒序遍历,找到 null 的元素后,进行移除 + for (Node x = last; x != null; x = x.prev) { + if (x.item == null) { + unlink(x); + return true; + } + } + } else { + // 倒序遍历,找到等于 o 的元素后,进行移除 + for (Node x = last; x != null; x = x.prev) { + if (o.equals(x.item)) { + unlink(x); + return true; + } + } + } + return false; +} +``` + +`#remove()` 方法,移除链表首个节点。代码如下: + +``` +// LinkedList.java 实现 Queue 接口 + +public E remove() { + return removeFirst(); +} + +public E removeFirst() { + final Node f = first; + // <1> 如果链表为空,抛出 NoSuchElementException 异常 + if (f == null) + throw new NoSuchElementException(); + // <2> 移除链表时首个元素 + return unlinkFirst(f); +} + +private E unlinkFirst(Node f) { + // assert f == first && f != null; + final E element = f.item; + // 获得 f 的下一个节点 + final Node next = f.next; + // 设置 f 的 item 为 null ,帮助 GC + f.item = null; + // 设置 f 的 next 为 null ,帮助 GC + f.next = null; // help GC + // 修改 fisrt 指向 next + first = next; + // 修改 next 节点的 prev 指向 null + if (next == null) // 如果链表只有一个元素,说明被移除后,队列就是空的,则 last 设置为 null + last = null; + else + next.prev = null; + // 链表大小减一 + size--; + // 增加数组修改次数 + modCount++; + return element; +} +``` + +- `<1>` 处,如果链表为空,抛出 NoSuchElementException 异常。 +- `<2>` 处,移除链表时首个元素。比较简单,胖友自己看看。😈 因为 LinkedList 有 `first` 和 `last` 头尾节点,所以添加和删除操作,都可能需要小心处理。 + +`#removeLast()` 方法,移除链表最后一个节点。代码如下: + +``` +// LinkedList.java 实现 Deque 接口 + +public E removeLast() { + final Node l = last; + // 如果链表为空,则抛出 NoSuchElementException 移除 + if (l == null) + throw new NoSuchElementException(); + // 移除链表的最后一个元素 + return unlinkLast(l); +} + +private E unlinkLast(Node l) { + // assert l == last && l != null; + final E element = l.item; + // 获得 f 的上一个节点 + final Node prev = l.prev; + // 设置 l 的 item 为 null ,帮助 GC + l.item = null; + // 设置 l 的 prev 为 null ,帮助 GC + l.prev = null; // help GC + // 修改 last 指向 prev + last = prev; + // 修改 prev 节点的 next 指向 null + if (prev == null) // 如果链表只有一个元素,说明被移除后,队列就是空的,则 first 设置为 null + first = null; + else + prev.next = null; + // 链表大小减一 + size--; + // 增加数组修改次数 + modCount++; + return element; +} +``` + +- 和 `#removeFirst()` 方法**相反**,当然实现上是差不多。 + +`#poll()` 和 `#` 方法,移除链表的头或尾,差异点在于链表为空时候,不会抛出 NoSuchElementException 异常。代码如下: + +``` +// LinkedList.java 实现 Queue 接口 + +public E poll() { // 移除头 + final Node f = first; + return (f == null) ? null : unlinkFirst(f); +} + +public E pop() { + return removeFirst(); // 这个方法,如果队列为空,还是会抛出 NoSuchElementException 异常。😈 不知道放在哪里哈。这里来凑凑~ +} + +// LinkedList.java 实现 Deque 接口 + +public E pollFirst() { // 移除头 + final Node f = first; + return (f == null) ? null : unlinkFirst(f); +} + +public E pollLast() { // 移除尾 + final Node l = last; + return (l == null) ? null : unlinkLast(l); +} +``` + +# 9. 移除多个元素 + +`#removeAll(Collection c)` 方法,批量移除指定的多个元素。代码如下: + +``` +// AbstractCollection.java + +public boolean removeAll(Collection c) { + Objects.requireNonNull(c); + boolean modified = false; + // 获得迭代器 + Iterator it = iterator(); + // 通过迭代器遍历 + while (it.hasNext()) { + // 如果 c 中存在该元素,则进行移除 + if (c.contains(it.next())) { + it.remove(); + modified = true; // 标记修改 + } + } + return modified; +} +``` + +- 该方法,是通过父类 AbstractCollection 来实现的,通过迭代器来遍历 LinkedList ,然后判断 `c` 中如果包含,则进行移除。 + +`#retainAll(Collection c)` 方法,求 LinkedList 和指定多个元素的交集。简单来说,恰好和 `#removeAll(Collection c)` 相反,移除不在 `c` 中的元素。代码如下: + +``` +// AbstractCollection.java + +public boolean retainAll(Collection c) { + Objects.requireNonNull(c); + boolean modified = false; + // 获得迭代器 + Iterator it = iterator(); + // 通过迭代器遍历 + while (it.hasNext()) { + // 如果 c 中不存在该元素,则进行移除 + if (!c.contains(it.next())) { + it.remove(); + modified = true; + } + } + return modified; +} +``` + +- 逻辑比较简单,`` 处的判断条件进行了调整。 + +# 10. 查找单个元素 + +`#indexOf(Object o)` 方法,查找首个为指定元素的位置。代码如下: + +``` +// LinkedList.java + +public int indexOf(Object o) { + int index = 0; + if (o == null) { // 如果 o 为 null 的情况 + // 顺序遍历,如果 item 为 null 的节点,进行返回 + for (Node x = first; x != null; x = x.next) { + if (x.item == null) + return index; // 找到 + index++; + } + } else { // 如果 o 非 null 的情况 + // 顺序遍历,如果 item 为 o 的节点,进行返回 + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) + return index; // 找到 + index++; + } + } + // 未找到 + return -1; +} +``` + +而 `#contains(Object o)` 方法,就是基于该方法实现。代码如下: + +``` +// LinkedList.java + +public boolean contains(Object o) { + return indexOf(o) >= 0; +} +``` + +有时我们需要查找最后一个为指定元素的位置,所以会使用到 `#lastIndexOf(Object o)` 方法。代码如下: + +``` +// LinkedList.java + +public int lastIndexOf(Object o) { + int index = size; + if (o == null) { // 如果 o 为 null 的情况 + // 倒序遍历,如果 item 为 null 的节点,进行返回 + for (Node x = last; x != null; x = x.prev) { + index--; + if (x.item == null) + return index; // 找到 + } + } else { // 如果 o 非 null 的情况 + // 倒序遍历,如果 item 为 o 的节点,进行返回 + for (Node x = last; x != null; x = x.prev) { + index--; + if (o.equals(x.item)) + return index; // 找到 + } + } + // 未找到 + return -1; +} +``` + +# 11. 获得指定位置的元素 + +`#get(int index)` 方法,获得指定位置的元素。代码如下: + +``` +// LinkedList.java + +public E get(int index) { + checkElementIndex(index); + // 基于 node(int index) 方法实现 + return node(index).item; +} +``` + +- 随机访问 `index` 位置的元素,时间复杂度为 O(n) 。 + +因为 LinkedList 实现了 Deque 接口,所以它实现了 `#peekFirst()` 和 `#peekLast()` 方法,分别获得元素到链表的头尾。代码如下: + +``` +// LinkedList.java 实现 Deque 接口 + +public E peekFirst() { + final Node f = first; + return (f == null) ? null : f.item; +} + +public E peekLast() { + final Node l = last; + return (l == null) ? null : l.item; +} +``` + +因为 LinkedList 实现了 Queue 接口,所以它实现了 `#peek()` 和 `#peek()` 和 `#element()` 方法,分别获得元素到链表的头。代码如下: + +``` +// LinkedList.java 实现 Queue 接口 + +public E peek() { + final Node f = first; + return (f == null) ? null : f.item; +} + +public E element() { // 如果链表为空识,抛出 NoSuchElementException 异常 + return getFirst(); +} +public E getFirst() { + final Node f = first; + if (f == null) // 如果链表为空识,抛出 NoSuchElementException 异常 + throw new NoSuchElementException(); + return f.item; +} +``` + +# 12. 设置指定位置的元素 + +`#set(int index, E element)` 方法,设置指定位置的元素。代码如下: + +``` +// LinkedList.java + +public E set(int index, E element) { + checkElementIndex(index); + // 获得第 index 位置的节点 + Node x = node(index); + E oldVal = x.item; + // 修改对应的值 + x.item = element; + return oldVal; +} +``` + +# 13. 转换成数组 + +`#toArray()` 方法,将 ArrayList 转换成 `[]` 数组。代码如下: + +``` +// LinkedList.java + +public Object[] toArray() { + // 创建 Object 数组 + Object[] result = new Object[size]; + // 顺序遍历节点,设置到 Object 数组中 + int i = 0; + for (Node x = first; x != null; x = x.next) + result[i++] = x.item; + return result; +} +``` + +实际场景下,我们可能想要指定 `T` 泛型的数组,那么我们就需要使用到 `#toArray(T[] a)` 方法。代码如下: + +``` +// LinkedList.java + +public T[] toArray(T[] a) { + // <1> 如果传入的数组小于 size 大小,则直接复制一个新数组返回 + if (a.length < size) + a = (T[])java.lang.reflect.Array.newInstance( + a.getClass().getComponentType(), size); + // <2> 顺序遍历链表,复制到 a 中 + int i = 0; + Object[] result = a; + for (Node x = first; x != null; x = x.next) + result[i++] = x.item; + + // <2.1> 如果传入的数组大于 size 大小,则将 size 赋值为 null + if (a.length > size) + a[size] = null; + + // <2.2> 返回 a + return a; +} +``` + +# 14. 求哈希值 + +`#hashCode()` 方法,求 LinkedList 的哈希值。代码如下: + +``` +// AbstractList.java + +public int hashCode() { + int hashCode = 1; + // 遍历,求哈希 + for (E e : this) + hashCode = 31*hashCode + (e==null ? 0 : e.hashCode()); + return hashCode; +} +``` + +- 该方法,是通过父类 AbstractList 来实现的,通过 `for` 来遍历 LinkedList ,然后进行求哈希。可能有胖友不了解 `for( : )` 语法糖,它最终会编译转换成 Iterator 迭代器。 + +# 15. 判断相等 + +`#equals(Object o)` 方法,判断是否相等。代码如下: + +``` +// AbstractList.java + +public boolean equals(Object o) { + // 如果 o 就是自己,直接返回 true + if (o == this) + return true; + // 如果不为 List 类型,直接返回 false + if (!(o instanceof List)) + return false; + + // 创建迭代器,顺序遍历比对 + ListIterator e1 = listIterator(); + ListIterator e2 = ((List) o).listIterator(); + while (e1.hasNext() && e2.hasNext()) { + E o1 = e1.next(); + Object o2 = e2.next(); + if (!(o1==null ? o2==null : o1.equals(o2))) // 如果不相等,返回 false + return false; + } + // 如果有迭代器没有遍历完,说明两者长度不等,所以就不相等;否则,就相等了 + return !(e1.hasNext() || e2.hasNext()); +} +``` + +- 该方法,是通过父类 AbstractList 来实现的,通过迭代器,实现遍历比对。 + +# 16. 清空链表 + +`#clear()` 方法,清空链表。代码如下: + +``` +// LinkedList.java + +public void clear() { + // Clearing all of the links between nodes is "unnecessary", but: + // - helps a generational GC if the discarded nodes inhabit + // more than one generation + // - is sure to free memory even if there is a reachable Iterator + // 顺序遍历链表,设置每个节点前后指向为 null + // 通过这样的方式,帮助 GC + for (Node x = first; x != null; ) { + // 获得下一个节点 + Node next = x.next; + // 设置 x 的 item、next、prev 为空。 + x.item = null; + x.next = null; + x.prev = null; + // 设置 x 为下一个节点 + x = next; + } + // 清空 first 和 last 指向 + first = last = null; + // 设置链表大小为 0 + size = 0; + // 增加数组修改次数 + modCount++; +} +``` + +# 17. 序列化链表 + +`#writeObject(java.io.ObjectOutputStream s)` 方法,实现 LinkedList 的序列化。代码如下: + +``` +// LinkedList.java + +@java.io.Serial +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out any hidden serialization magic + // 写入非静态属性、非 transient 属性 + s.defaultWriteObject(); + + // Write out size + // 写入链表大小 + s.writeInt(size); + + // Write out all elements in the proper order. + // 顺序遍历,逐个序列化 + for (Node x = first; x != null; x = x.next) + s.writeObject(x.item); +} +``` + +# 18. 反序列化链表 + +`#readObject(java.io.ObjectInputStream s)` 方法,反序列化数组。代码如下: + +``` +// LinkedList.java + +@java.io.Serial +private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + // Read in any hidden serialization magic + // 读取非静态属性、非 transient 属性 + s.defaultReadObject(); + + // Read in size + // 读取 size + int size = s.readInt(); + + // Read in all elements in the proper order. + // 顺序遍历,逐个反序列化 + for (int i = 0; i < size; i++) + linkLast((E)s.readObject()); // 添加到链表尾部 +} +``` + +# 19. 克隆 + +`#clone()` 方法,克隆 LinkedList 对象。代码如下: + +``` +// LinkedList.java + +public Object clone() { + // 调用父类,进行克隆 + LinkedList clone = superClone(); + + // Put clone into "virgin" state + // 重置 clone 为初始化状态 + clone.first = clone.last = null; + clone.size = 0; + clone.modCount = 0; + + // Initialize clone with our elements + // 遍历遍历,逐个添加到 clone 中 + for (Node x = first; x != null; x = x.next) + clone.add(x.item); + + return clone; +} +``` + +- 注意,`first`、`last` 等都是重新初始化进来,不与原 LinkedList 共享。 + +# 20. 创建子数组 + +`#subList(int fromIndex, int toIndex)` 方法,创建 ArrayList 的子数组。代码如下: + +``` +// AbstractList.java + +public List subList(int fromIndex, int toIndex) { + subListRangeCheck(fromIndex, toIndex, size()); + // 根据判断 RandomAccess 接口,判断是否支持随机访问 + return (this instanceof RandomAccess ? + new RandomAccessSubList<>(this, fromIndex, toIndex) : + new SubList<>(this, fromIndex, toIndex)); +} +``` + +- 该方法,是通过父类 AbstractList 来实现的。 +- 根据判断 RandomAccess 接口,判断是否支持随机访问,从而创建 RandomAccessSubList 或 SubList 对象。这里,我们就不拓展开解析这两个类,感兴趣的胖友自己去瞅瞅噢。 + +# 21. 创建 Iterator 迭代器 + +`#iterator()` 方法,创建迭代器。代码如下: + +``` +// AbstractSequentialList.java +public Iterator iterator() { + return listIterator(); +} + +// AbstractList.java +public ListIterator listIterator() { + return listIterator(0); +} + +// AbstractSequentialList.java +public abstract ListIterator listIterator(int index); +``` + +- 该方法,是通过父类 AbstractSequentialList 来实现的。 +- 整个调用过程是,`iterator() => listIterator() => listIterator(int index)` 的顺序,就是我们在代码里贴进去的顺序。最终呢,是调用 LinkedList 对 `#listIterator(int index)` 的实现,我们在 [「22. 创建 ListIterator 迭代器」](https://svip.iocoder.cn/JDK/Collection-LinkedList/#) 小节来看。 + +# 22. 创建 ListIterator 迭代器 + +`#listIterator(int index)` 方法,创建 ListIterator 迭代器。代码如下: + +``` +// LinkedList.java + +public ListIterator listIterator(int index) { + checkPositionIndex(index); + return new ListItr(index); +} +``` + +- 创建 ListItr 迭代器。 + +因为 ListItr 的实现代码比较简单,我们就不逐个来看了,直接贴加了注释的代码。代码如下: + +``` +// LinkedList.java + +private class ListItr implements ListIterator { + + /** + * 最后返回的节点 + */ + private Node lastReturned; + /** + * 下一个节点 + */ + private Node next; + /** + * 下一个访问元素的位置,从下标 0 开始。 + * + * 主要用于 {@link #nextIndex()} 中,判断是否遍历结束 + */ + private int nextIndex; + /** + * 创建迭代器时,数组修改次数。 + * + * 在迭代过程中,如果数组发生了变化,会抛出 ConcurrentModificationException 异常。 + */ + private int expectedModCount = modCount; + + ListItr(int index) { + // assert isPositionIndex(index); + // 获得下一个节点 + next = (index == size) ? null : node(index); + // 下一个节点的位置 + nextIndex = index; + } + + public boolean hasNext() { + return nextIndex < size; + } + + public E next() { + // 校验是否数组发生了变化 + checkForComodification(); + // 如果已经遍历到结尾,抛出 NoSuchElementException 异常 + if (!hasNext()) + throw new NoSuchElementException(); + + // lastReturned 指向,记录最后访问节点 + lastReturned = next; + // next 指向,下一个节点 + next = next.next; + // 下一个节点的位置 + 1 + nextIndex++; + // 返回 lastReturned + return lastReturned.item; + } + + public boolean hasPrevious() { + return nextIndex > 0; + } + + public E previous() { + // 校验是否数组发生了变化 + checkForComodification(); + // 如果已经遍历到结尾,抛出 NoSuchElementException 异常 + if (!hasPrevious()) + throw new NoSuchElementException(); + + // 修改 lastReturned 和 next 的指向。此时,lastReturned 和 next 是相等的。 + lastReturned = next = (next == null) ? last : next.prev; + // 下一个节点的位置 - 1 + nextIndex--; + // 返回 lastReturned + return lastReturned.item; + } + + public int nextIndex() { + return nextIndex; + } + + public int previousIndex() { + return nextIndex - 1; + } + + public void remove() { + // 校验是否数组发生了变化 + checkForComodification(); + // 如果 lastReturned 为空,抛出 IllegalStateException 异常,因为无法移除了。 + if (lastReturned == null) + throw new IllegalStateException(); + + // 获得 lastReturned 的下一个 + Node lastNext = lastReturned.next; + // 移除 lastReturned 节点 + unlink(lastReturned); + // 此处,会分成两种情况 + if (next == lastReturned) // 说明发生过调用 `#previous()` 方法的情况,next 指向下一个节点,而 nextIndex 是无需更改的 + next = lastNext; + else + nextIndex--; // nextIndex 减一。 + + // 设置 lastReturned 为空 + lastReturned = null; + // 增加数组修改次数 + expectedModCount++; + } + + public void set(E e) { + // 如果 lastReturned 为空,抛出 IllegalStateException 异常,因为无法修改了。 + if (lastReturned == null) + throw new IllegalStateException(); + // 校验是否数组发生了变化 + checkForComodification(); + // 修改 lastReturned 的 item 为 e + lastReturned.item = e; + } + + public void add(E e) { + // 校验是否数组发生了变化 + checkForComodification(); + // 设置 lastReturned 为空 + lastReturned = null; + // 此处,会分成两种情况 + if (next == null) // 如果 next 已经遍历到尾,则 e 作为新的尾节点,进行插入。算是性能优化 + linkLast(e); + else // 插入到 next 的前面 + linkBefore(e, next); + // nextIndex 加一。 + nextIndex++; + // 增加数组修改次数 + expectedModCount++; + } + + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + // 遍历剩余链表 + while (modCount == expectedModCount && nextIndex < size) { + // 执行 action 逻辑 + action.accept(next.item); + // lastReturned 指向 next + lastReturned = next; + // next 指向下一个节点 + next = next.next; + // nextIndex 加一。 + nextIndex++; + } + // 校验是否数组发生了变化 + checkForComodification(); + } + + final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + } + +} +``` + +- 虽然有点长,但是保持淡定哟。 + +# 666. 彩蛋 + +咳咳咳,总体还是有点长,不过相比 ArrayList 来说,LinkedList 确实简单蛮多。主要篇幅长的原因,还是因为 LinkedList 实现了 Deque 接口,需要多实现很多方法。 + +下面,我们来对 LinkedList 做一个简单的小结: + +- LinkedList 基于节点实现的**双向**链表的 List ,每个节点都指向前一个和后一个节点从而形成链表。 + +- LinkedList 提供队列、双端队列、栈的功能。 + + > 因为 `first` 节点,所以提供了队列的功能的实现的功能。 + > 因为 `last` 节点,所以提供了栈的功能的实现的功能。 + > 因为同时具有 `first` + `last` 节点,所以提供了双端队列的功能。 + +- LinkedList 随机访问**平均**时间复杂度是 O(n) ,查找指定元素的**平均**时间复杂度是 O(n) 。 + +- LinkedList 移除指定位置的元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。 + + > 最好时间复杂度发生在头部、或尾部移除的情况。 + +- LinkedList 移除指定位置的元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。 + + > 最好时间复杂度发生在头部移除的情况。 + +- LinkedList 添加元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。 + + > 最好时间复杂度发生在头部、或尾部添加的情况。 + +因为 LinkedList 提供了多种添加、删除、查找的方法,会根据是否能够找到对应的元素进行操作,抛出 NoSuchElementException 异常。我们整理了一个表格,避免胖友错误使用。 + +| | 返回结果 | 抛出异常 | +| :--- | :------------------------------------------------- | :---------- | +| 添加 | `#add(…)`、`#offset(...)` | | +| 删除 | `#remove(int index)`、`#remove(E e)`、`#poll(E E)` | `#remove()` | +| 查找 | `#get(int index)`、`#peek()` | `#poll()` | + +😈 这个表主要整理了 List 和 Queue 的操作,暂时没有整理 Deque 的操作。因为,Deque 相同前缀的方法,表现结果同 Queue 。 + +OK ,还是在结尾抛个拓展,在 Redis List 的数据结构,实现方式是类似 Java LinkedList 的方式,感兴趣的胖友可以自己去瞅瞅。 \ No newline at end of file diff --git a/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/01.jpg b/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/01.jpg new file mode 100644 index 0000000..c07ac81 Binary files /dev/null and b/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/01.jpg differ diff --git a/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/02.jpg b/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/02.jpg new file mode 100644 index 0000000..b12e2b5 Binary files /dev/null and b/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/02.jpg differ diff --git a/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/03.jpg b/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/03.jpg new file mode 100644 index 0000000..fc54959 Binary files /dev/null and b/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/03.jpg differ diff --git a/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/04.jpg b/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/04.jpg new file mode 100644 index 0000000..69cdd0c Binary files /dev/null and b/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.assets/04.jpg differ diff --git a/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.md b/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.md new file mode 100644 index 0000000..530deae --- /dev/null +++ b/jdk/05-JDK 源码解析-集合(三)哈希表 HashMap.md @@ -0,0 +1,1430 @@ +# 精尽 JDK 源码解析 —— 集合(三)哈希表 HashMap + +# 1. 简介 + +HashMap ,是一种[散列表](https://zh.wikipedia.org/wiki/哈希表),用于存储 key-value 键值对的数据结构,一般翻译为“哈希表”,提供**平均**时间复杂度为 O(1) 的、基于 key 级别的 get/put 等操作。 + +之前我们在分享 [《精尽 JDK 源码解析 —— 集合(一)数组 ArrayList》](http://svip.iocoder.cn/JDK/Collection-ArrayList) 中提到过,“在前些年,实习或初级工程师的面试,可能最爱问的就是 ArrayList 和 LinkedList 的区别与使用场景”。现在已经改变成,HashMap 的实现原理是什么。😈 相信令大多数胖友头疼不已,有木有噢。 + +在日常的业务开发中,HashMap 可以说是和 ArrayList 一样常用的集合类,特别是考虑到数据库的性能,又或者服务的拆分后,我们把关联数据的拼接,放到了内存中,这就需要使用到 HashMap 了。 + +# 2. 类图 + +HashMap 实现的接口、继承的抽象类,如下图所示:[![类图](05-JDK 源码解析-集合(三)哈希表 HashMap.assets/03.jpg)](http://static.iocoder.cn/images/JDK/2019_12_07/03.jpg)类图 + +- 实现 [`java.util.Map`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/Map.java) 接口,并继承 [`java.util.AbstractMap`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractMap.java) 抽像类。 +- 实现 [`java.io.Serializable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/io/Serializable.java) 接口。 +- 实现 [`java.lang.Cloneable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/lang/Cloneable.java) 接口。 + +# 3. 属性 + +在开始看 HashMap 的具体属性之前,我们先来简单说说 HashMap 的实现原理。 + +> 艿艿:实际上,我更加推荐大家去看 [《数据结构与算法》](http://www.iocoder.cn/images/jikeshijian/数据结构与算法之美.jpg) 的《散列表》章节。一方面是确实讲的有趣生动又系统,另一方面自己有几个知识盲点在里面解决了。 + +相信很多胖友,在初次看到 HashMap 时,都惊奇于其 **O(1)** 的 get 操作的时间复杂度。当时在我们已知的数据结构中,只有基于下标访问数组时,才能提供 **O(1)** get 操作的时间复杂度。 + +实际上,HashMap 所提供的 O(1) 是**平均**时间复杂度,大多数情况下保证 O(1) 。其实极端情况下,有可能退化为 O(N) 的时间复杂度噢,这又是为什么呢? + +HashMap 其实是在数组的基础上实现的,一个“加强版”的数组。如下图所示:[![数组](05-JDK 源码解析-集合(三)哈希表 HashMap.assets/01.jpg)](http://static.iocoder.cn/images/JDK/2019_12_07/01.jpg)数组 + +好像有点不对?!key 并不是一个整数,可以放入指向数组中的指定下标。咳咳咳,我们要 O(1) 的性能!!!所以,**hash** 就正式登场了,通过 `hash(key)` 的过程,我们可以将 key 成功的转成一个整数。但是,`hash(key)` 可能会超过数组的容量,所以我们需要 `hash(key) % size` 作为下标,放入数组的对应位置。至此,我们是不是已经可以通过 O(1) 的方式,快速的从 HashMap 中进行 get 读取操作了。 + +> 注意,一般每个数组的“位置”,比较专业的说法,叫做“槽位”(slot)或者“桶”。因为代码注释里,已经都使用了“位置”,所以我们就暂时不进行修正了。 + +😈 好像还是不对!?原因有两点: + +- 1、`hash(key)` 计算出来的哈希值,并不能保证唯一; +- 2、`hash(key) % size` 的操作后,即使不同的哈希值,也可能变成相同的结果。 + +这样,就导致我们常说的“哈希冲突”。那么怎么解决呢?方法有两种: + +- 1、开放寻址法 + + > 本文暂时不展开关于开放寻址法的内容,胖友可以看看 [《散列表的开放寻址法》](https://blog.csdn.net/zuoyigexingfude/article/details/40394859) 。等后面我们写到 ThreadLocalMap 的时候,我们在详细掰扯掰扯。 + +- 2、链表法 + +在 Java HashMap 中,采用了链表法。如果有看过 Redis Hash 数据结构的胖友,它也是采用了链表法。通过将数组的每个元素对应一个链表,我们将相同的 `hash(key) % size` 放到对应下标的链表中即可。 + +> 当然,put / get 操作需要做下是否等于指定 key 的判断,这个具体我们在源码中分享。 + +仿佛一切都很美好,但是我们试着来想,如果我们放入的 N 个 key-value 键值对到 HashMap 的情况: + +- 1、每个 key 经过 `hash(key) % size` 对应唯一下标,则 get 时间复杂度是 O(1) 。 +- 2、k 个 key 经过 `hash(key) % size` 对应唯一下标,那么在 get 这 k 个 key 的时间复杂度是 O(k) 。 +- 3、在情况 2 的极端情况下,k 恰好等于 N ,那么是不是就出现我们在上面说的 O(N) 的时间复杂度的情况。 + +所以,为了解决最差 O(N) 的时间复杂度的情况,我们可以将数组的每个元素对应成其它数据结构,例如说:1)红黑树;2)跳表。它们两者的时间复杂度是 O(logN) ,这样 O(N) 就可以缓解成 O(logN) 的时间复杂度。 + +> 😈 红黑树是相对复杂的数据结构,= = 反正艿艿没花时间去深究它,所以本文关于 HashMap 红黑树部分的源码,也并不会去分析。同时,也不是很建议胖友去看,因为看 HashMap 重点是搞懂 HashMap 本身。 +> +> 当然,对红黑树感兴趣的胖友,还是可以单独去看的。 +> +> 另外,跳表是我们一定要掌握甚至必须能够手写代码的数据结构,在 Redis Zset 数据结果,就采用了改造过的跳表。 + +- 在 JDK7 的版本中,HashMap 采用“数组 + 链表”的形式实现。 + +- 在 JDK8 开始的版本,HashMap 采用“数组 + 链表 + 红黑树”的形式实现,在空间和时间复杂度中做取舍。 + + > 这一点和 Redis 是相似的,即使是一个数据结构,可能内部采用多种数据结构,混合实现,为了平衡空间和时间复杂度。毕竟,时间不是唯一的因素,我们还需要考虑内存的情况。 + +如此,HashMap 的整体结构如下图:[![HashMap](05-JDK 源码解析-集合(三)哈希表 HashMap.assets/02.jpg)](http://static.iocoder.cn/images/JDK/2019_12_07/02.jpg)HashMap + +这样就结束了么?既然这么问,肯定还有故事,那就是“扩容”。我们是希望 HashMap 尽可能能够达到 O(1) 的时间复杂度,链表法只是我们解决哈希冲突的无奈之举。而在 O(1) 的时间复杂度,基本是“一个萝卜一个坑”,所以在 HashMap 的 key-value 键值对数量达到阀值后,就会进行**扩容**。 + +那么阀值是什么,又是怎么计算呢?此时就引入**负载因子**的概念。我们假设 HashMap 的数组容量为 `capacity` ,key-value 键值对数量为 `size` ,负载因子为 `loadFactor` 。那么,当 `capacity / size > loadFactor` 时,也就是使用的数组大小到达 `loadFactor` 比例时,我们就需要进行扩容。如此,我们便可以尽量达到“一个萝卜一个坑”的目的,从而尽可能的 O(1) 的时间复杂度。 + +> 🐱 貌似写了大 2000 字了。如果有不理解的地方,可以星球里给艿艿提问。 +> +> 当然,我们也可以结合下面的 HashMap 源码,更好的理解 HashMap 的实现原理。毕竟,源码之前,了无秘密。 +> +> 不过,还是再次推荐 [《数据结构与算法》](http://www.iocoder.cn/images/jikeshijian/数据结构与算法之美.jpg) ,写的真好,羡慕~ + +哔哔了这么多,重点就是几处: + +- 哈希 key +- 哈希冲突的解决 +- 扩容 + +------ + +下面,我们来看看 HashMap 的属性。代码如下: + +``` +// HashMap.java + +/* ---------------- Fields -------------- */ + +/** + * 底层存储的数组 + * + * The table, initialized on first use, and resized as + * necessary. When allocated, length is always a power of two. + * (We also tolerate length zero in some operations to allow + * bootstrapping mechanics that are currently not needed.) + */ +transient Node[] table; + +/** + * 调用 `#entrySet()` 方法后的缓存 + * + * Holds cached entrySet(). Note that AbstractMap fields are used + * for keySet() and values(). + */ +transient Set> entrySet; + +/** + * key-value 的键值对数量 + * + * The number of key-value mappings contained in this map. + */ +transient int size; + +/** + * HashMap 的修改次数 + * + * The number of times this HashMap has been structurally modified + * Structural modifications are those that change the number of mappings in + * the HashMap or otherwise modify its internal structure (e.g., + * rehash). This field is used to make iterators on Collection-views of + * the HashMap fail-fast. (See ConcurrentModificationException). + */ +transient int modCount; + +/** + * 阀值,当 {@link #size} 超过 {@link #threshold} 时,会进行扩容 + * + * The next size value at which to resize (capacity * load factor). + * + * @serial + */ +// (The javadoc description is true upon serialization. +// Additionally, if the table array has not been allocated, this +// field holds the initial array capacity, or zero signifying +// DEFAULT_INITIAL_CAPACITY.) +int threshold; + +/** + * 扩容因子 + * + * The load factor for the hash table. + * + * @serial + */ +final float loadFactor; +``` + +- 胖友重点看下 `table`、`size`、`threshold`、`loadFactor` 四个属性。 + +具体的解释,我们在「4. 构造方法」中来看。这里我们先来看看 `table` Node 数组。代码如下: + +``` +// HashMap.java#Node.java + +static class Node implements Map.Entry { + + /** + * 哈希值 + */ + final int hash; + /** + * KEY 键 + */ + final K key; + /** + * VALUE 值 + */ + V value; + /** + * 下一个节点 + */ + Node next; + + // ... 省略实现方法 + +} +``` + +- 实现了 Map.Entry 接口,该接口定义在 [`java.util.Map`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/Map.java) 接口中。相信这个接口,胖友已经很熟悉了,就不重复哔哔了。 +- `hash` + `key` + `value` 属性,定义了 Node 节点的 3 个重要属性。 +- `next` 属性,指向下一个节点。通过它可以实现 `table` 数组的每一个位置可以形成链表。 + +Node 子类如下图:[![Node 子类类图](05-JDK 源码解析-集合(三)哈希表 HashMap.assets/04.jpg)](http://static.iocoder.cn/images/JDK/2019_12_07/04.jpg)Node 子类类图 + +- TreeNode ,定义在 HashMap 中,红黑树节点。通过它可以实现 `table` 数组的每一个位置可以形成红黑树。因为本文不深入红黑树部分,所以我们也就不看 TreeNode 中的具体代码了。如果胖友自己对 HashMap 中的红黑树部分的实现,可以自己看看这块的代码。 + +# 4. 构造方法 + +HashMap 一共有四个构造方法,我们分别来看看。 + +**① `#HashMap()`** + +`#HashMap()` 构造方法,创建一个初始化容量为 16 的 HashMap 对象。代码如下: + +``` +// HashMap.java + +/** + * 默认的初始化容量 + * + * The default initial capacity - MUST be a power of two. + */ +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 + +/** + * 默认加载因子为 0.75 + * + * The load factor used when none specified in constructor. + */ +static final float DEFAULT_LOAD_FACTOR = 0.75f; + +/** + * Constructs an empty {@code HashMap} with the default initial capacity + * (16) and the default load factor (0.75). + */ +public HashMap() { + this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted +} +``` + +- 初始化 `loadFactor` 为 `DEFAULT_LOAD_FACTOR = 0.75` 。 +- 在该构造方法上,我们并没有看到 `table` 数组的初始化。它是**延迟**初始化,在我们开始往 HashMap 中添加 key-value 键值对时,在 `#resize()` 方法中才真正初始化。 + +**② `#HashMap(int initialCapacity)`** + +`#HashMap(int initialCapacity)` 方法,初始化容量为 `initialCapacity` 的 HashMap 对象。代码如下: + +``` +// HashMap.java + +public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); +} +``` + +- 内部调用 `#HashMap(int initialCapacity, float loadFactor)` 构造方法。 + +**③ `#HashMap(int initialCapacity, float loadFactor)`** + +`#HashMap(int initialCapacity, float loadFactor)` 构造方法,初始化容量为 `initialCapacity` 、加载因子为 `loadFactor` 的 HashMap 对象。代码如下: + +``` +// HashMap.java + +/** + * 最大的容量为 2^30 。 + * + * The maximum capacity, used if a higher value is implicitly specified + * by either of the constructors with arguments. + * MUST be a power of two <= 1<<30. + */ +static final int MAXIMUM_CAPACITY = 1 << 30; + +public HashMap(int initialCapacity, float loadFactor) { + // 校验 initialCapacity 参数 + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal initial capacity: " + + initialCapacity); + // 避免 initialCapacity 超过 MAXIMUM_CAPACITY + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + // 校验 loadFactor 参数 + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + + loadFactor); + // 设置 loadFactor 属性 + this.loadFactor = loadFactor; + // 计算 threshold 阀值 + this.threshold = tableSizeFor(initialCapacity); +} +``` + +- 我们重点来看 `` 处,调用 `#tableSizeFor(int cap)` 方法,返回大于 `cap` 的最小 2 的 N 次方。例如说,`cap = 10` 时返回 16 ,`cap = 28` 时返回 32 。代码如下: + + ``` + // HashMap.java + + static final int tableSizeFor(int cap) { + // 将 cap 从最高位(最左边)第一个为 1 开始的位开始,全部设置为 1 。 + int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); + // 因为 n 已经是 0..01..1 的情况,那么 n + 1 就能满足 cap 的最小 2 的 N 次方 + // 在 cap 为 0 和 1 的时候,n 会为 -1 ,则此时最小 2 的 N 次方为 2^0 = 1 。 + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; + } + ``` + + - 胖友先抛开里面的**位计算**,单纯看看这 2 行代码的注释。 + + - 理解之后,想要深究的就看看 [《Java8 —— HashMap 之tableSizeFor()》](https://www.jianshu.com/p/cbe3f22793be) 文章,不想的就继续跟着艿艿往下继续看 HashMap 的源码。 + + > 😈 看源码就是这样,需要先把重点给看完,不然就会陷入无限的调用栈的深入。当然,实在难受的,可以加一个“TODO 后续深入”之类的,回头在干。 + > + > 总之,先整体,后局部。 + + - 那么,为什么这里的 `threshold` 要返回大于等于 `initialCapacity` 的最小 2 的 N 次方呢? + + > 艿艿的理解,不一定正确,但是要哔哔下。 + > + > 在 put 方法中,计算 `table` 数组对应的位置,逻辑是 `(n - 1) & hash` ,这个和我们预想的 `hash % (n - 1)` 的有差别。这两者在 `n` 是 2 的 N 次方情况下是等价的。那么考虑到性能,我们会选择 `&` 位操作。这样,就要求数组容量 `n` 要尽可能是 2 的 N 次方。 + > + > 而在 `#resize()` 扩容方法中,我们会看到 HashMap 的容量,一直能够保证是 2 的 N 次方。 + > + > 如此,`#tableSizeFor(int cap)` 方法,也需要保证返回的是 2 的 N 次方。 + +**四 `#HashMap(Map m)`** + +`#HashMap(Map m)` 构造方法,创建 HashMap 对象,并将 `c` 集合添加到其中。代码如下: + +``` +// HashMap.java + +public HashMap(Map m) { + // 设置加载因子 + this.loadFactor = DEFAULT_LOAD_FACTOR; + // 批量添加到 table 中 + putMapEntries(m, false); +} +``` + +- `` 处,调用 `#putMapEntries(Map m, boolean evict)` 方法,批量添加到 `table` 中。代码如下: + + ``` + // HashMap.java + + final void putMapEntries(Map m, boolean evict) { + int s = m.size(); + // <1> + if (s > 0) { + // 如果 table 为空,说明还没初始化,适合在构造方法的情况 + if (table == null) { // pre-size + // 根据 s 的大小 + loadFactor 负载因子,计算需要最小的 tables 大小 + float ft = ((float)s / loadFactor) + 1.0F; // + 1.0F 的目的,是因为下面 (int) 直接取整,避免不够。 + int t = ((ft < (float)MAXIMUM_CAPACITY) ? + (int)ft : MAXIMUM_CAPACITY); + // 如果计算出来的 t 大于阀值,则计算新的阀值 + if (t > threshold) + threshold = tableSizeFor(t); + // 如果 table 非空,说明已经初始化,需要不断扩容到阀值超过 s 的数量,避免扩容 + } else { + // Because of linked-list bucket constraints, we cannot + // expand all at once, but can reduce total resize + // effort by repeated doubling now vs later + while (s > threshold && table.length < MAXIMUM_CAPACITY) + resize(); // 扩容 + } + + // <2> 遍历 m 集合,逐个添加到 HashMap 中。 + for (Map.Entry e : m.entrySet()) { + K key = e.getKey(); + V value = e.getValue(); + putVal(hash(key), key, value, false, evict); + } + } + } + ``` + + - 整个过程分成 `<1>` 和 `<2>` 的两个步骤。 + - `<1>` 处,保证 `table` 容量足够,分成了 `table` 是否为空有不同的处理。可能胖友比较疑惑的是,`table` 为空的情况的处理?因为此时 `table` 未初始化,我们只需要保证 `threshold` 大于数组大小即可,在 put key-value 键值的时候,在去真正的初始化 `table` 就好咧。 + - `<2>` 处,遍历 `m` 集合,逐个调用 `#putVal(hash, key, val, onlyIfAbsent, evict)` 方法,添加到 HashMap 中。关于这块的逻辑,我们本文的后面再来详细解析。 + +# 5. 哈希函数 + +对于哈希函数来说,有两个方面特别重要: + +- 性能足够高。因为基本 HashMap 所有的操作,都需要用到哈希函数。 +- 对于计算出来的哈希值足够离散,保证哈希冲突的概率更小。 + +在 HashMap 中,`#hash(Object key)` 静态方法,计算 key 的哈希值。代码如下: + +``` +// HashMap.java + +static final int hash(Object key) { + int h; + // h = key.hashCode() 计算哈希值 + // ^ (h >>> 16) 高 16 位与自身进行异或计算,保证计算出来的 hash 更加离散 + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +- 高效性:从整个计算过程上来说,`^ (h >>> 16)` 只有这一块的逻辑,两个位操作,性能肯定是有保障的。那么,如果想要保证哈希函数的高效性,就需要传入的 `key` 自身的 `Object#hashCode()` 方法的高效即可。 +- 离散型:和大多数胖友有一样的疑惑,为什么有 `^ (h >>> 16)` 一段代码呢,总结来说,就是保证“hash 更加离散”。关于这块的解释,直接来看 [《JDK 源码中 HashMap 的 hash 方法原理是什么?》](https://www.zhihu.com/question/20733617/answer/111577937) 的胖君的解答 ,好强! + +# 6. 添加单个元素 + +`#put(K key, V value)` 方法,添加单个元素。代码如下: + +``` +// HashMap.java + +public V put(K key, V value) { + // hash(key) 计算哈希值 + return putVal(hash(key), key, value, false, true); +} + +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; // tables 数组 + Node p; // 对应位置的 Node 节点 + int n; // 数组大小 + int i; // 对应的 table 的位置 + // <1> 如果 table 未初始化,或者容量为 0 ,则进行扩容 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize() /*扩容*/ ).length; + // <2> 如果对应位置的 Node 节点为空,则直接创建 Node 节点即可。 + if ((p = tab[i = (n - 1) & hash] /*获得对应位置的 Node 节点*/) == null) + tab[i] = newNode(hash, key, value, null); + // <3> 如果对应位置的 Node 节点非空,则可能存在哈希冲突 + else { + Node e; // key 在 HashMap 对应的老节点 + K k; + // <3.1> 如果找到的 p 节点,就是要找的,则则直接使用即可 + if (p.hash == hash && // 判断 hash 值相等 + ((k = p.key) == key || (key != null && key.equals(k)))) // 判断 key 真正相等 + e = p; + // <3.2> 如果找到的 p 节点,是红黑树 Node 节点,则直接添加到树中 + else if (p instanceof TreeNode) + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + // <3.3> 如果找到的 p 是 Node 节点,则说明是链表,需要遍历查找 + else { + // 顺序遍历链表 + for (int binCount = 0; ; ++binCount) { + // `(e = p.next)`:e 指向下一个节点,因为上面我们已经判断了最开始的 p 节点。 + // 如果已经遍历到链表的尾巴,则说明 key 在 HashMap 中不存在,则需要创建 + if ((e = p.next) == null) { + // 创建新的 Node 节点 + p.next = newNode(hash, key, value, null); + // 链表的长度如果数量达到 TREEIFY_THRESHOLD(8)时,则进行树化。 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; // 结束 + } + // 如果遍历的 e 节点,就是要找的,则则直接使用即可 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; // 结束 + // p 指向下一个节点 + p = e; + } + } + // <4.1> 如果找到了对应的节点 + if (e != null) { // existing mapping for key + V oldValue = e.value; + // 修改节点的 value ,如果允许修改 + if (!onlyIfAbsent || oldValue == null) + e.value = value; + // 节点被访问的回调 + afterNodeAccess(e); + // 返回老的值 + return oldValue; + } + } + // <4.2> + // 增加修改次数 + ++modCount; + // 如果超过阀值,则进行扩容 + if (++size > threshold) + resize(); + // 添加节点后的回调 + afterNodeInsertion(evict); + // 返回 null + return null; +} +``` + +- 有点长,不过逻辑上来说,简单的一笔噢。 + +- `<1>` 处,如果 `table` 未初始化,或者容量为 0 ,则调用 `#resize()` 方法,进行扩容。 + +- `<2>` 处,如果对应位置的 Node 节点为空,则直接创建 Node 节点即可。 + + - `i = (n - 1) & hash` 代码段,计算 `table` 所在对应位置的下标。😈 此处,结合我们在 `#tableSizeFor(int cap)` 方法,在理解一波。 + + - 调用 `#newNode(int hash, K key, V value, Node next)` 方法,创建 Node 节点即可。代码如下: + + ``` + // HashMap.java + + Node newNode(int hash, K key, V value, Node next) { + return new Node<>(hash, key, value, next); + } + ``` + + - 这样,一个新的链表就出现了。当然,此处的 `next` 肯定是 `null` 。 + +- `<3>` 处,如果对应位置的 Node 节点非空,则可能存在哈希冲突。需要分成 Node 节点是链表(`<3.3>`),还是红黑树(`<3.2>`)的情况。 + +- `<3.1>` 处,如果找到的 `p` 节点,就是要找的,则则直接使用即可。这是一个优化操作,无论 Node 节点是链表还是红黑树。 + +- `<3.2>` 处,如果找到的 `p` 节点,是红黑树 Node 节点,则调用 `TreeNode#putTreeVal(HashMap map, Node[] tab, int h, K k, V v)` 方法,直接添加到树中。这块,咱就先不深入了。 + +- `<3.3>` 处,如果找到的 `p` 是 Node 节点,则说明是链表,需要遍历查找。比较简单,胖友自己看下代码注释即可。其中,`binCount >= TREEIFY_THRESHOLD - 1` 代码段,在链表的长度超过 `TREEIFY_THRESHOLD = 8` 的时候,会调用 `#treeifyBin(Node[] tab, int hash)` 方法,将链表进行树化。当然,树化还有一个条件,具体在 [「TODO. 树化」](https://svip.iocoder.cn/JDK/Collection-HashMap/#) 中详细来看。 + +- `<4>` 处,根据是否在 HashMap 中已经存在 key 对应的节点,有不同的处理。 + +- ``` + <4.1> + ``` + + + + 处,如果存在的情况,会有如下处理: + + - 1)如果满足需要修改节点,则进行修改。 + - 2)如果节点被访问时,调用 `#afterNodeAccess((Node p)` 方法,节点被访问的回调。目前这是个一个空方法,用于 HashMap 的子类 LinkedHashMap 需要做的拓展逻辑。 + - 3)返回老的值。 + +- ``` + <4.2> + ``` + + + + 处,如果不存在的情况,会有如下处理: + + - 1)增加修改次数。 + - 2)增加 key-value 键值对 `size` 数。并且 `size` 如果超过阀值,则调用 `#resize()` 方法,进行扩容。 + - 3)调用 `#afterNodeInsertion(boolean evict)` 方法,添加节点后的回调。目前这是个一个空方法,用于 HashMap 的子类 LinkedHashMap 需要做的拓展逻辑。 + - 4)返回 `null` ,因为老值不存在。 + +> 艿艿:厚着脸皮来个互动。欢迎胖友在看完这块逻辑后,画个 HashMap 的 put 操作的流程图投稿给艿艿哟。 + +`#putIfAbsent(K key, V value)` 方法,当 `key` 不存在的时候,添加 key-value 键值对到其中。代码如下: + +``` +// HashMap.java + +@Override +public V putIfAbsent(K key, V value) { + return putVal(hash(key), key, value, true, true); +} +``` + +# 7. 扩容 + +`#resize()` 方法,**两倍扩容** HashMap 。实际上,我们在 [「4. 构造方法」](https://svip.iocoder.cn/JDK/Collection-HashMap/#) 中,看到 `table` 数组并未初始化,它是在 `#resize()` 方法中进行初始化,所以这是该方法的另外一个作用:**初始化数组**。代码如下: + +``` +// HashMap.java + +final Node[] resize() { + Node[] oldTab = table; + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + // <1> 开始: + // <1.1> oldCap 大于 0 ,说明 table 非空 + if (oldCap > 0) { + // <1.1.1> 超过最大容量,则直接设置 threshold 阀值为 Integer.MAX_VALUE ,不再允许扩容 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + // <1.1.2> newCap = oldCap << 1 ,目的是两倍扩容 + // 如果 oldCap >= DEFAULT_INITIAL_CAPACITY 满足,说明当前容量大于默认值(16),则 2 倍阀值。 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold + } + // <1.2.1>【非默认构造方法】oldThr 大于 0 ,则使用 oldThr 作为新的容量 + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + // <1.2.2>【默认构造方法】oldThr 等于 0 ,则使用 DEFAULT_INITIAL_CAPACITY 作为新的容量,使用 DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY 作为新的容量 + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + // 1.3 如果上述的逻辑,未计算新的阀值,则使用 newCap * loadFactor 作为新的阀值 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + // <2> 开始: + // 将 newThr 赋值给 threshold 属性 + threshold = newThr; + // 创建新的 Node 数组,赋值给 table 属性 + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap]; + table = newTab; + // 如果老的 table 数组非空,则需要进行一波搬运 + if (oldTab != null) { + for (int j = 0; j < oldCap; ++j) { + // 获得老的 table 数组第 j 位置的 Node 节点 e + Node e; + if ((e = oldTab[j]) != null) { + // 置空老的 table 数组第 j 位置 + oldTab[j] = null; + // <2.1> 如果 e 节点只有一个元素,直接赋值给新的 table 即可 + if (e.next == null) + newTab[e.hash & (newCap - 1)] = e; + // <2.2> 如果 e 节点是红黑树节点,则通过红黑树分裂处理 + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + // <2.3> 如果 e 节点是链表 + else { // preserve order + // HashMap 是成倍扩容,这样原来位置的链表的节点们,会被分散到新的 table 的两个位置中去 + // 通过 e.hash & oldCap 计算,根据结果分到高位、和低位的位置中。 + // 1. 如果结果为 0 时,则放置到低位 + // 2. 如果结果非 1 时,则放置到高位 + Node loHead = null, loTail = null; + Node hiHead = null, hiTail = null; + Node next; + // 这里 do while 的原因是,e 已经非空,所以减少一次判断。细节~ + do { + // next 指向下一个节点 + next = e.next; + // 满足低位 + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + // 满足高位 + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + // 设置低位到新的 newTab 的 j 位置上 + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + // 设置高位到新的 newTab 的 j + oldCap 位置上 + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; +} +``` + +- 不要怕,仅仅是代码长了点,逻辑很明确,就两步:1)计算新的容量和扩容阀值,并创建新的 `table` 数组;2)将老的 `table` 复制到新的 `table` 数组中。 + +下面开始,我们进入【第一步】。 + +- ``` + <1.1> + ``` + + + + 处, + + ``` + oldCap + ``` + + + + 大于 0 ,说明 + + + + ``` + table + ``` + + + + 非空,说明是 + + 两倍扩容 + + 的骚操作。 + + - `<1.1.1>` 处,超过最大容量,则直接设置 `threshold` 阀值为 `Integer.MAX_VALUE` ,不再允许扩容。 + - 【重要】`<1.1.2>` 处,**两倍扩容**,这个暗搓搓的 `newCap = oldCap << 1)` 代码段,😈 差点就看漏了。因为容量是两倍扩容,那么再 `newCap * loadFactor` 逻辑,相比直接 `oldThr << 1` 慢,所以直接使用 `oldThr << 1` 位运算的方案。 + +- ``` + <1.2.1> + ``` + + + + 和 + + + + ``` + <1.2.2> + ``` + + + + 处, + + ``` + oldCap + ``` + + + + 等于 0 ,说明 + + + + ``` + table + ``` + + + + 为空,说明是 + + 初始化 + + 的骚操作。 + + - `<1.2.1>` 处,`oldThr` 大于 0 ,说明使用的是【非默认构造方法】,则使用 `oldThr` 作为新的容量。这里,我们结合 `#tableSizeFor(int cap)` 方法,发现 HashMap 容量一定会是 2 的 N 次方。 + - `<1.2.2>` 处,`oldThr` 等于 0 ,说明使用的是【默认构造方法】,则使用 `DEFAULT_INITIAL_CAPACITY` 作为新的容量,然后计算新的 `newThr` 阀值。 + +- `<1.3>` 处,如果上述的逻辑,未计算新的阀值,则使用 `newCap * loadFactor` 作为新的阀值。满足该情况的,有 `<1.2.1>` 和 `<1.1.1>` 的部分情况(胖友自己看下那个判断条件)。 + +下面开始,我们进入【第二步】。 + +- 一共分成 `<2.1>`、`<2.2>`、`<2.3>` 的三种情况。😈 相信看懂了 `#put(K key, V value)` 也是分成三种情况,就很容易明白是为什么了。 + +- `<2.1>` 处,如果 `e` 节点只有一个元素,直接赋值给新的 `table` 即可。这是一个优化操作,无论 Node 节点是链表还是红黑树。 + +- `<2.2>` 处,如果 `e` 节点是红黑树节点,则通过红黑树分裂处理。 + +- `<2.3>` 处,如果 `e` 节点是链表,以为 HashMap 是成倍扩容,这样原来位置的链表的节点们,会被分散到新的 `table` 的两个位置中去。可能这里对于不熟悉位操作的胖友有点难理解,我们来一步一步看看: + + > 为了方便举例,`{}` 中的数字,胖友记得是二进制表示哈。 + + - 1)我们在选择 `hash & (cap - 1)` 方式,来获得到在 `table` 的位置。那么经过计算,`hash` 在 `cap` 最高位(最左边)的 **1** 自然就被抹去了。例如说,`11 & (4 - 1) = {1011 & 011} = {11} = 3` ,而 `15 & (4 - 1) = {1111 & 011} = {11}= 3` 。相当于 `15` 的 `1[1]11` 的 `[1]` 被**抹去**了。 + - 2)HashMap 成倍扩容之后,我们在来看看示例。`11 & (7 - 1) = {1011 & 0111} = {11} = 3` ,而 `15 & (8 - 1) = {1111 & 0111} = {111}= 7` 。相当于 `15` 的 `1[1]11` 的 `[1]` 被**保留**了。 + - 3)那么怎么判断这 `[1]` 是否能够在扩容的时候被保留呢,那就使用 `hash & oldCap` 是否等于 1 即可得到。既然 `[1]` 被保留下来,那么其位置就会 `j + oldCap` ,因为 `[1]` 的**价值**就是 `+ oldCap` 。 + - 🙂 如果不了解的胖友,可以在纸上画一画整个过程。 + +在 HashMap 中,暂时未提供**缩容**的操作。不过我们可以结合 `<2.3>` 处的逻辑,缩容可以理解将**高位**的位置的 Node 节点,放回其对应的**低位**的位置的 Node 节点中。😈 想要继续死磕的胖友,可以去研究下 Redis 的 Hash 数据结构在缩容的处理。 + +# 8. 树化 + +`#treeifyBin(Node[] tab, int hash)` 方法,将 `hash` 对应 `table` 位置的链表,转换成红黑树。代码如下: + +``` +// HashMap.java + +/** + * 每个位置链表树化成红黑树,需要的链表最小长度 + * + * The bin count threshold for using a tree rather than list for a + * bin. Bins are converted to trees when adding an element to a + * bin with at least this many nodes. The value must be greater + * than 2 and should be at least 8 to mesh with assumptions in + * tree removal about conversion back to plain bins upon + * shrinkage. + */ +static final int TREEIFY_THRESHOLD = 8; + +/** + * HashMap 允许树化最小 key-value 键值对数 + * + * The smallest table capacity for which bins may be treeified. + * (Otherwise the table is resized if too many nodes in a bin.) + * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts + * between resizing and treeification thresholds. + */ +static final int MIN_TREEIFY_CAPACITY = 64; + +final void treeifyBin(Node[] tab, int hash) { + int n, index; Node e; + // <1> 如果 table 容量小于 MIN_TREEIFY_CAPACITY(64) ,则选择扩容 + if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) + resize(); + // <2> 将 hash 对应位置进行树化 + else if ((e = tab[index = (n - 1) & hash]) != null) { + // 顺序遍历链表,逐个转换成 TreeNode 节点 + TreeNode hd = null, tl = null; + do { + TreeNode p = replacementTreeNode(e, null); + if (tl == null) + hd = p; + else { + p.prev = tl; + tl.next = p; + } + tl = p; + } while ((e = e.next) != null); + // 树化 + if ((tab[index] = hd) != null) + hd.treeify(tab); + } +} +``` + +- 在 [「6. 添加单个元素」](https://svip.iocoder.cn/JDK/Collection-HashMap/#) 中,我们已经看到,每个位置的链表想要树化成红黑树,想要链表长度大于等于 `TREEIFY_THRESHOLD = 8` 。那么可能胖友会疑惑,为什么是 8 呢?我们可以在 HashMap 代码上搜 `Implementation notes.` ,其中部分内容就解释了它。 + + ``` + // HashMap.java + + * Because TreeNodes are about twice the size of regular nodes, we + * use them only when bins contain enough nodes to warrant use + * (see TREEIFY_THRESHOLD). And when they become too small (due to + * removal or resizing) they are converted back to plain bins. In + * usages with well-distributed user hashCodes, tree bins are + * rarely used. Ideally, under random hashCodes, the frequency of + * nodes in bins follows a Poisson distribution + * (http://en.wikipedia.org/wiki/Poisson_distribution) with a + * parameter of about 0.5 on average for the default resizing + * threshold of 0.75, although with a large variance because of + * resizing granularity. Ignoring variance, the expected + * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / + * factorial(k)). The first values are: + * + * 0: 0.60653066 + * 1: 0.30326533 + * 2: 0.07581633 + * 3: 0.01263606 + * 4: 0.00157952 + * 5: 0.00015795 + * 6: 0.00001316 + * 7: 0.00000094 + * 8: 0.00000006 + * more: less than 1 in ten million + ``` + + - 首先,参考 [泊松概率函数(Poisson distribution)](http://en.wikipedia.org/wiki/Poisson_distribution) ,当链表长度到达 8 的概率是 0.00000006 ,不到千万分之一。所以绝大多数情况下,在 hash 算法正常的时,不太会出现链表转红黑树的情况。 + - 其次,TreeNode 相比普通的 Node 来说,会有**两倍**的空间占用。并且在长度比较小的情况下,红黑树的查找性能和链表是差别不大的。例如说,红黑树的 `O(logN) = log8 = 3` 和链表的 `O(N) = 8` 只相差 5 。 + - 毕竟 HashMap 是 JDK 提供的基础数据结构,必须在空间和时间做抉择。所以,选择链表是空间复杂度优先,选择红黑树是时间复杂度优化。在绝大多数情况下,不会出现需要红黑树的情况。 + +- `<1>` 处,如果 `table` 容量小于 `MIN_TREEIFY_CAPACITY = 64` 时,则调用 `#resize()` 方法,进行扩容。一般情况下,该链表可以分裂到两个位置上。😈 当然,极端情况下,解决不了,这时候一般是 hash 算法有问题。 + +- `<2>` 处,如果 `table` 容量大于等于 `MIN_TREEIFY_CAPACITY = 64` 时,则将 `hash` 对应位置进行树化。一共有两步,因为和红黑树相关,这里就不拓展开了。 + +有树化,必然有取消树化。当 HashMap 因为移除 key 时,导致对应 `table` 位置的红黑树的内部节点数小于等于 `UNTREEIFY_THRESHOLD = 6` 时,则将红黑树退化成链表。具体在 `HashMap.TreeNode#untreeify(HashMap map)` 中实现,整列就不拓展开了。代码如下: + +``` +// HashMap.java + +/** + * The bin count threshold for untreeifying a (split) bin during a + * resize operation. Should be less than TREEIFY_THRESHOLD, and at + * most 6 to mesh with shrinkage detection under removal. + */ +static final int UNTREEIFY_THRESHOLD = 6; +``` + +- 暂时没有行明白为什么使用 6 作为取消树化的阀值。暂时的想法,避免后续移除 key 时,红黑树如果内部节点数小于 7 就退化成链表,这样可能导致过于频繁的树化和取消树化。 + +# 9. 添加多个元素 + +`#putAll(Map m)` 方法,添加多个元素到 HashMap 中。代码如下: + +``` +// HashMap.java + +public void putAll(Map m) { + putMapEntries(m, true); +} +``` + +- 和 `#HashMap(Map m)` 构造方法一样,都调用 `#putMapEntries(Map m, boolean evict)` 方法。 + +# 10. 移除单个元素 + +`#remove(Object key)` 方法,移除 key 对应的 value ,并返回该 value 。代码如下: + +``` +// HashMap.java + +public V remove(Object key) { + Node e; + // hash(key) 求哈希值 + return (e = removeNode(hash(key), key, null, false, true)) == null ? + null : e.value; +} + +final Node removeNode(int hash, Object key, Object value, + boolean matchValue, boolean movable) { + Node[] tab; // table 数组 + Node p; // hash 对应 table 位置的 p 节点 + int n, index; + // <1> 查找 hash 对应 table 位置的 p 节点 + if ((tab = table) != null && (n = tab.length) > 0 && + (p = tab[index = (n - 1) & hash]) != null) { + Node node = null, // 如果找到 key 对应的节点,则赋值给 node + e; + K k; V v; + // <1.1> 如果找到的 p 节点,就是要找的,则则直接使用即可 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + node = p; + else if ((e = p.next) != null) { + // <1.2> 如果找到的 p 节点,是红黑树 Node 节点,则直接在红黑树中查找 + if (p instanceof TreeNode) + node = ((TreeNode)p).getTreeNode(hash, key); + // <1.3> 如果找到的 p 是 Node 节点,则说明是链表,需要遍历查找 + else { + do { + // 如果遍历的 e 节点,就是要找的,则则直接使用即可 + if (e.hash == hash && + ((k = e.key) == key || + (key != null && key.equals(k)))) { + node = e; + break; // 结束 + } + p = e; // 注意,这里 p 会保存找到节点的前一个节点 + } while ((e = e.next) != null); + } + } + // <2> 如果找到 node 节点,则进行移除 + // 如果有要求匹配 value 的条件,这里会进行一次判断先移除 + if (node != null && (!matchValue || (v = node.value) == value || + (value != null && value.equals(v)))) { + // <2.1> 如果找到的 node 节点,是红黑树 Node 节点,则直接在红黑树中删除 + if (node instanceof TreeNode) + ((TreeNode)node).removeTreeNode(this, tab, movable); + // <2.2.1> 如果查找到的是链表的头节点,则直接将 table 对应位置指向 node 的下一个节点,实现删除 + else if (node == p) + tab[index] = node.next; + // <2.2.2> 如果查找到的是链表的中间节点,则将 p 指向 node 的下一个节点,实现删除 + else + p.next = node.next; + // 增加修改次数 + ++modCount; + // 减少 HashMap 数量 + --size; + // 移除 Node 后的回调 + afterNodeRemoval(node); + // 返回 node + return node; + } + } + // 查找不到,则返回 null + return null; +} +``` + +- 在 HashMap 中,移除 和添加 key-value 键值对,整个流程是比较接近的。一共分成两步: + - `<1>` 处,查找到 key 对应的 Node 节点。 + - `<2>` 处,将查找到的 Node 节点进行移除。 +- 整体逻辑比较简单,这里就不哔哔,胖友可以顺着: + - 第一步,`<1.1>`、`<1.2>`、`<1.3>` 三种情况。 + - 第二步,`<2.1>`、`<2.2.1> + <2.2.2>` 两种情况。 + +`#remove(Object key, Object value)` 方法,移除指定 key-value 的键值对。代码如下: + +``` +// HashMap.java + +@Override +public boolean remove(Object key, Object value) { + return removeNode(hash(key), key, value, true, true) != null; +} +``` + +- 也是基于 `#removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)` 方法来实现的,差别在于传入了 `value` 和 `matchValue = true` 参数。 + +HashMap 暂时不提供批量移除多个元素的方法。 + +# 11. 查找单个元素 + +`#get(Object key)` 方法,查找单个元素。代码如下: + +``` +// HashMap.java + +public V get(Object key) { + Node e; + // hash(key) 哈希值 + return (e = getNode(hash(key), key)) == null ? null : e.value; +} + +final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + // 查找 hash 对应 table 位置的 p 节点 + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + // 如果找到的 first 节点,就是要找的,则则直接使用即可 + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + if ((e = first.next) != null) { + // 如果找到的 first 节点,是红黑树 Node 节点,则直接在红黑树中查找 + if (first instanceof TreeNode) + return ((TreeNode)first).getTreeNode(hash, key); + // 如果找到的 e 是 Node 节点,则说明是链表,需要遍历查找 + do { + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; +} +``` + +- 比较简单,`#removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)` 的 SE 版。 + + > 艿艿:这里 SE 指的是阉割版。咳咳咳。 + +`#containsKey(Object key)` 方法,就是基于该方法实现。代码如下: + +``` +// HashMap.java + +public boolean containsKey(Object key) { + return getNode(hash(key), key) != null; +} +``` + +`#containsValue(Object value)` 方法,查找指定 value 是否存在。代码如下: + +``` +// HashMap.java + +public boolean containsValue(Object value) { + Node[] tab; V v; + if ((tab = table) != null && size > 0) { + // 遍历 table 数组 + for (Node e : tab) { + // 处理链表或者红黑树节点 + for (; e != null; e = e.next) { + // 如果值相等,则返回 true + if ((v = e.value) == value || + (value != null && value.equals(v))) + return true; + } + } + } + // 找不到,返回 false + return false; +} +``` + +> 艿艿:看到这里,基本 HashMap 的源码解析已经结束,对后面方法不感兴趣的胖友,可以直接跳到 [666. 彩蛋](https://svip.iocoder.cn/JDK/Collection-HashMap/#) 中。 + +`#getOrDefault(Object key, V defaultValue)` 方法,获得 key 对应的 value 。如果不存在,则返回 `defaultValue` 默认值。代码如下: + +``` +// HashMap.java + +@Override +public V getOrDefault(Object key, V defaultValue) { + Node e; + return (e = getNode(hash(key), key)) == null ? defaultValue : e.value; +} +``` + +# 12. 转换成数组 + +`#keysToArray(T[] a)` 方法,转换出 key 数组返回。代码如下: + +``` +// HashMap.java + + T[] keysToArray(T[] a) { + Object[] r = a; + Node[] tab; + int idx = 0; + if (size > 0 && (tab = table) != null) { + // 遍历 table 数组 + for (Node e : tab) { + // 遍历链表或红黑树 + for (; e != null; e = e.next) { + // 逐个设置 key 到 r 数组中 + r[idx++] = e.key; + } + } + } + // 返回 + return a; +} +``` + +- 细心的胖友,可能已经意识到了,如果 `a` 数组的大小不够放下 HashMap 的所有 key 怎么办?答案是可以通过 `#prepareArray(T[] a)` 方法来保证。代码如下: + + ``` + // HashMap.java + + final T[] prepareArray(T[] a) { + int size = this.size; + // 如果 a 数组小于 HashMap 大小,则创建一个新的数组返回 + if (a.length < size) { + return (T[]) java.lang.reflect.Array + .newInstance(a.getClass().getComponentType(), size); + } + // 如果 a 数组大于 HashMap 大小,则将 size 位置设置为 null + if (a.length > size) { + a[size] = null; + } + return a; + } + ``` + + - 当 `a` 数组过小时,会创建一个新的数组返回。 + - 当然,一般情况下,我们肯定是不会使用到该方法。😈 至今貌似也没有使用过。 + +`#valuesToArray(T[] a)` 方法,转换出 value 数组返回。代码如下: + +``` +// HashMap.java + + T[] valuesToArray(T[] a) { + Object[] r = a; + Node[] tab; + int idx = 0; + if (size > 0 && (tab = table) != null) { + // 遍历 table 数组 + for (Node e : tab) { + // 遍历链表或红黑树 + for (; e != null; e = e.next) { + // 逐个设置 value 到 r 数组中 + r[idx++] = e.value; + } + } + } + // 返回 + return a; +} +``` + +# 13. 转换成 Set/Collection + +`#keySet()` 方法,获得 key Set 。代码如下: + +``` +// AbstractMap.java +transient Set keySet; + +// HashMap.java +public Set keySet() { + // 获得 keySet 缓存 + Set ks = keySet; + // 如果不存在,则进行创建 + if (ks == null) { + ks = new KeySet(); + keySet = ks; + } + return ks; +} +``` + +- 创建的 KeySet 类,实现了 [`java.util.AbstractSet`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractSet.java) 抽像类,是 HashMap 的内部类。比较简单,就不哔哔了。 + +`#values()` 方法,获得 value 集合。代码如下: + +``` +// AbstractMap.java +transient Collection values; + +// HashMap.java +public Collection values() { + // 获得 vs 缓存 + Collection vs = values; + // 如果不存在,则进行创建 + if (vs == null) { + vs = new Values(); + values = vs; + } + return vs; +} +``` + +- 创建的 Values 类,实现了 [`java.util.AbstractCollection`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractCollection.java) 抽像类,是 HashMap 的内部类。比较简单,就不哔哔了。 + +`#entrySet()` 方法,获得 key-value Set 。代码如下: + +``` +// HashMap.java + +transient Set> entrySet; + +public Set> entrySet() { + Set> es; + // 获得 entrySet 缓存 + // 如果不存在,则进行创建 + return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; +} +``` + +- 创建的 EntrySet 类,实现了 [`java.util.AbstractSet`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractSet.java) 抽像类,是 HashMap 的内部类。比较简单,就不哔哔了。 + +> 艿艿:感觉会被胖友锤死。嘿嘿。 + +# 14. 清空 + +`#clear()` 方法,清空 HashMap 。代码如下: + +``` +// HashMap.java + +public void clear() { + Node[] tab; + // 增加修改次数 + modCount++; + if ((tab = table) != null && size > 0) { + // 设置大小为 0 + size = 0; + // 设置每个位置为 null + for (int i = 0; i < tab.length; ++i) + tab[i] = null; + } +} +``` + +# 15. 序列化 + +`#writeObject(ObjectOutputStream s)` 方法,序列化 HashMap 对象。代码如下: + +``` +// HashMap.java + +@java.io.Serial +private void writeObject(java.io.ObjectOutputStream s) + throws IOException { + // 获得 HashMap table 数组大小 + int buckets = capacity(); + // Write out the threshold, loadfactor, and any hidden stuff + // 写入非静态属性、非 transient 属性 + s.defaultWriteObject(); + // 写入 table 数组大小 + s.writeInt(buckets); + // 写入 key-value 键值对数量 + s.writeInt(size); + // 写入具体的 key-value 键值对 + internalWriteEntries(s); +} + +final int capacity() { // table 数组大小。封装方法的原因,需要考虑 table 未初始化的情况。 + return (table != null) ? table.length : + (threshold > 0) ? threshold : + DEFAULT_INITIAL_CAPACITY; +} + +// Called only from writeObject, to ensure compatible ordering. +void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException { + Node[] tab; + if (size > 0 && (tab = table) != null) { + // 遍历 table 数组 + for (Node e : tab) { + // 遍历链表或红黑树 + for (; e != null; e = e.next) { + // 写入 key + s.writeObject(e.key); + // 写入 value + s.writeObject(e.value); + } + } + } +} +``` + +- 比较简单,胖友自己瞅瞅即可。 + +# 16. 反序列化 + +`#readObject(ObjectInputStream s)` 方法,反序列化成 HashMap 对象。代码如下: + +``` +// HashMap.java + +@java.io.Serial +private void readObject(java.io.ObjectInputStream s) + throws IOException, ClassNotFoundException { + // Read in the threshold (ignored), loadfactor, and any hidden stuff + // 读取非静态属性、非 transient 属性 + s.defaultReadObject(); + // 重新初始化 + reinitialize(); + // 校验 loadFactor 参数 + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new InvalidObjectException("Illegal load factor: " + + loadFactor); + // 读取 HashMap table 数组大小 + s.readInt(); // Read and ignore number of buckets + // 读取 key-value 键值对数量 size + int mappings = s.readInt(); // Read number of mappings (size) + // 校验 size 参数 + if (mappings < 0) + throw new InvalidObjectException("Illegal mappings count: " + + mappings); + else if (mappings > 0) { // (if zero, use defaults) + // Size the table using given load factor only if within + // range of 0.25...4.0 + float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f); + float fc = (float)mappings / lf + 1.0f; + // 计算容量 + int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ? + DEFAULT_INITIAL_CAPACITY : + (fc >= MAXIMUM_CAPACITY) ? + MAXIMUM_CAPACITY : + tableSizeFor((int)fc)); + // 计算 threshold 阀值 + float ft = (float)cap * lf; + threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? + (int)ft : Integer.MAX_VALUE); + + // Check Map.Entry[].class since it's the nearest public type to + // what we're actually creating. + SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap); // 不知道作甚,哈哈哈。 + // 创建 table 数组 + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] tab = (Node[])new Node[cap]; + table = tab; + + // Read the keys and values, and put the mappings in the HashMap + // 遍历读取 key-value 键值对 + for (int i = 0; i < mappings; i++) { + // 读取 key + @SuppressWarnings("unchecked") + K key = (K) s.readObject(); + // 读取 value + @SuppressWarnings("unchecked") + V value = (V) s.readObject(); + // 添加 key-value 键值对 + putVal(hash(key), key, value, false, false); + } + } +} + +/** + * Reset to initial default state. Called by clone and readObject. + */ +void reinitialize() { + table = null; + entrySet = null; + keySet = null; + values = null; + modCount = 0; + threshold = 0; + size = 0; +} +``` + +- 相比序列化的过程,复杂了一丢丢。跟着顺序往下看即可,嘿嘿。 + +# 17. 克隆 + +`#clone()` 方法,克隆 HashMap 对象。代码如下: + +``` +// HashMap.java + +@Override +public Object clone() { + // 克隆 HashMap 对象 + HashMap result; + try { + result = (HashMap)super.clone(); + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError(e); + } + // 重新初始化 + result.reinitialize(); + // 批量添加 key-value 键值对到其中 + result.putMapEntries(this, false); + // 返回 result + return result; +} +``` + +- 对于 key-value 键值对是浅拷贝,这点要注意哈。 + +# 666. 彩蛋 + +咳咳咳,在理解 HashMap 的实现原理之后,再去看 HashMap 的实现代码,其实会比想象中简单非常多。艿艿自己的卡壳点,主要还是 hash 函数的一些细节,😈 不知道胖友在哪些地方卡壳了? + +看完之后,有没觉得,面试的时候很稳,这里我们就不要吊打面试官了,毕竟万一让我们手写红黑树,我们不就可能 GG 了。 + +关于在 JDK8 新增的几个方法,艿艿暂时没有去写,主要如下: + +- `#replace(K key, V oldValue, V newValue)` +- `#replace(K key, V value)` +- `#computeIfAbsent(K key, Function mappingFunction)` +- `#computeIfPresent(K key, BiFunction remappingFunction)` +- `#compute(K key, BiFunction remappingFunction)` +- `#merge(K key, V value, BiFunction remappingFunction)` +- `#forEach(BiConsumer action)` +- `#replaceAll(BiFunction function)` + +哈哈,也是比较简单的方法,胖友自己可以解决一波的哈。就当,课后作业?!嘿嘿。 + +下面,我们来对 HashMap 做一个简单的小结: + +- HashMap 是一种散列表的数据结构,底层采用数组 + 链表 + 红黑树来实现存储。 + + > Redis Hash 数据结构,采用数组 + 链表实现。 + > + > Redis Zset 数据结构,采用跳表实现。 + > + > 因为红黑树实现起来相对复杂,我们自己在实现 HashMap 可以考虑采用数组 + 链表 + 跳表来实现存储。 + +- HashMap 默认容量为 16(`1 << 4`),每次超过阀值时,按照两倍大小进行自动扩容,所以容量总是 2^N 次方。并且,底层的 `table` 数组是延迟初始化,在首次添加 key-value 键值对才进行初始化。 + +- HashMap 默认加载因子是 0.75 ,如果我们已知 HashMap 的大小,需要正确设置容量和加载因子。 + +- HashMap 每个槽位在满足如下两个条件时,可以进行树化成红黑树,避免槽位是链表数据结构时,链表过长,导致查找性能过慢。 + + - 条件一,HashMap 的 `table` 数组大于等于 64 。 + - 条件二,槽位链表长度大于等于 8 时。选择 8 作为阀值的原因是,参考 [泊松概率函数(Poisson distribution)](http://en.wikipedia.org/wiki/Poisson_distribution) ,概率不足千万分之一。 + - 在槽位的红黑树的节点数量小于等于 6 时,会退化回链表。 + +- HashMap 的查找和添加 key-value 键值对的 + + 平均 + + 时间复杂度为 O(1) 。 + + - 对于槽位是链表的节点,**平均**时间复杂度为 O(k) 。其中 k 为链表长度。 + - 对于槽位是红黑树的节点,**平均**时间复杂度为 O(logk) 。其中 k 为红黑树节点数量。 + +OK ,还是在结尾抛个拓展,对于 Redis 的 Hash 和 ZSet 数据结构,胖友去研究下。 + +在故事的结尾,在推荐一篇美团技术团队的 [《Java 8 系列之重新认识HashMap》](https://zhuanlan.zhihu.com/p/21673805) 文章,写的更加生动细致。 \ No newline at end of file diff --git a/jdk/06-JDK 源码解析-集合(四)哈希表 LinkedHashMap.assets/01.png b/jdk/06-JDK 源码解析-集合(四)哈希表 LinkedHashMap.assets/01.png new file mode 100644 index 0000000..445955c Binary files /dev/null and b/jdk/06-JDK 源码解析-集合(四)哈希表 LinkedHashMap.assets/01.png differ diff --git a/jdk/06-JDK 源码解析-集合(四)哈希表 LinkedHashMap.assets/04.jpg b/jdk/06-JDK 源码解析-集合(四)哈希表 LinkedHashMap.assets/04.jpg new file mode 100644 index 0000000..69cdd0c Binary files /dev/null and b/jdk/06-JDK 源码解析-集合(四)哈希表 LinkedHashMap.assets/04.jpg differ diff --git a/jdk/06-JDK 源码解析-集合(四)哈希表 LinkedHashMap.md b/jdk/06-JDK 源码解析-集合(四)哈希表 LinkedHashMap.md new file mode 100644 index 0000000..08beae1 --- /dev/null +++ b/jdk/06-JDK 源码解析-集合(四)哈希表 LinkedHashMap.md @@ -0,0 +1,740 @@ +# 精尽 JDK 源码解析 —— 集合(四)哈希表 LinkedHashMap + +# 1. 概述 + +众所周知,HashMap 提供的访问,是**无序**的。而在一些业务场景下,我们希望能够提供**有序**访问的 HashMap 。那么此时,我们就有两种选择: + +- TreeMap :按照 key 的顺序。 +- LinkedHashMap :按照 key 的插入和访问的顺序。 + +LinkedHashMap ,在 HashMap 的基础之上,提供了**顺序**访问的特性。而这里的顺序,包括两种: + +- 按照 key-value 的**插入**顺序进行访问。关于这一点,相信大多数人都知道。 + + > 艿艿:如果不知道,那就赶紧知道。这不找抽么,哈哈哈。 + +- 按照 key-value 的**访问**顺序进行访问。通过这个特性,我们实现基于 LRU 算法的缓存。😈 相信这一点,可能还是有部分胖友不知道的噢,下文我们也会提供一个示例。 + + > 艿艿:面试中,有些面试官会喜欢问你,如何实现一个 LRU 的缓存。 + +实际上,LinkedHashMap 可以理解成是 LinkedList + HashMap 的组合。为什么这么说呢?让我们带着这样的疑问,一起往下看。 + +# 2. 类图 + +LinkedHashMap 实现的接口、继承的类,如下图所示:[![类图](06-JDK 源码解析-集合(四)哈希表 LinkedHashMap.assets/01.png)](http://static.iocoder.cn/images/JDK/2019_12_10/01.png)类图 + +- 实现 Map 接口。 +- 继承 HashMap 类。 + +😈 很简单,很粗暴。嘿嘿~ + +> 艿艿:因为 LinkedHashMap 继承自 HashMap 类,所以它的代码并不多,不到 500 行。 + +# 3. 属性 + +在开始看 LinkedHashMap 的属性之前,我们先来看在 [《精尽 JDK 源码解析 —— 集合(三)哈希表 HashMap》](http://svip.iocoder.cn/JDK/Collection-HashMap/) 看到的 HashMap 的 Node 子类图:[![Node 类图](06-JDK 源码解析-集合(四)哈希表 LinkedHashMap.assets/04.jpg)](http://static.iocoder.cn/images/JDK/2019_12_07/04.jpg)Node 类图 + +在图中,我们可以看到 LinkedHashMap 实现了自定义的节点 Entry ,一个支持指向前后节点的 Node 子类。代码如下: + +``` +// LinkedHashMap.java + +static class Entry extends HashMap.Node { + + Entry before, // 前一个节点 + after; // 后一个节点 + + Entry(int hash, K key, V value, Node next) { + super(hash, key, value, next); + } + +} +``` + +- `before` 属性,指向前一个节点。`after` 属性,指向后一个节点。 +- 通过 `before` + `after` 属性,我们就可以形成一个以 Entry 为节点的链表。😈 胖友,发挥下你的想象力。 + +既然 LinkedHashMap 是 LinkedList + HashMap 的组合,那么必然就会有头尾节点两兄弟。所以属性如下: + +``` +// LinkedHashMap.java + +/** + * 头节点。 + * + * 越老的节点,放在越前面。所以头节点,指向链表的开头 + * + * The head (eldest) of the doubly linked list. + */ +transient LinkedHashMap.Entry head; + +/** + * 尾节点 + * + * 越新的节点,放在越后面。所以尾节点,指向链表的结尾 + * + * The tail (youngest) of the doubly linked list. + */ +transient LinkedHashMap.Entry tail; + +/** + * 是否按照访问的顺序。 + * + * true :按照 key-value 的访问顺序进行访问。 + * false :按照 key-value 的插入顺序进行访问。 + * + * The iteration ordering method for this linked hash map: {@code true} + * for access-order, {@code false} for insertion-order. + * + * @serial + */ +final boolean accessOrder; +``` + +- 仔细看下每个属性的注释。 + +- `head` + `tail` 属性,形成 LinkedHashMap 的双向链表。而访问的顺序,就是 `head => tail` 的过程。 + +- ``` + accessOrder + ``` + + + + 属性,决定了 LinkedHashMap 的顺序。也就是说: + + - `true` 时,当 Entry 节点被访问时,放置到链表的结尾,被 `tail` 指向。 + - `false` 时,当 Entry 节点被添加时,放置到链表的结尾,被 `tail` 指向。如果插入的 key 对应的 Entry 节点已经存在,也会被放到结尾。 + +总结来说,就是如下一张图: + +> FROM [《Working of LinkedHashMap》](http://www.thejavageek.com/2016/06/05/working-linkedhashmap-java/) +> +> [![LinkedHashMap 结构图](http://www.thejavageek.com/wp-content/uploads/2016/06/SecondObjectInserted.png)](http://www.thejavageek.com/wp-content/uploads/2016/06/SecondObjectInserted.png)LinkedHashMap 结构图 + +# 4. 构造方法 + +LinkedHashMap 一共有 5 个构造方法,其中四个和 HashMap 相同,只是多初始化 `accessOrder = false` 。所以,默认使用**插入**顺序进行访问。 + +另外一个 `#LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)` 构造方法,允许自定义 `accessOrder` 属性。代码如下: + +``` +// LinkedHashMap.java + +public LinkedHashMap(int initialCapacity, + float loadFactor, + boolean accessOrder) { + super(initialCapacity, loadFactor); + this.accessOrder = accessOrder; +} +``` + +# 5. 创建节点 + +在插入 key-value 键值对时,例如说 `#put(K key, V value)` 方法,如果不存在对应的节点,则会调用 `#newNode(int hash, K key, V value, Node e)` 方法,创建节点。 + +因为 LinkedHashMap 自定义了 Entry 节点,所以必然需要重写该方法。代码如下: + +``` +// LinkedHashMap.java + +Node newNode(int hash, K key, V value, Node e) { + // <1> 创建 Entry 节点 + LinkedHashMap.Entry p = + new LinkedHashMap.Entry<>(hash, key, value, e); + // <2> 添加到结尾 + linkNodeLast(p); + // 返回 + return p; +} +``` + +- `<1>` 处,创建 Entry 节点。虽然此处传入 `e` 作为 `Entry.next` 属性,指向下一个节点。但是实际上,`#put(K key, V value)` 方法中,传入的 `e = null` 。 + +- `<2>` 处,调用 `#linkNodeLast(LinkedHashMap.Entry p)` 方法,添加到结尾。代码如下: + + ``` + // LinkedHashMap.java + + private void linkNodeLast(LinkedHashMap.Entry p) { + // 记录原尾节点到 last 中 + LinkedHashMap.Entry last = tail; + // 设置 tail 指向 p ,变更新的尾节点 + tail = p; + // 如果原尾节点 last 为空,说明 head 也为空,所以 head 也指向 p + if (last == null) + head = p; + // last <=> p ,相互指向 + else { + p.before = last; + last.after = p; + } + } + ``` + + - 这样,符合越新的节点,放到链表的越后面。 + +# 6. 节点操作回调 + +在 HashMap 的读取、添加、删除时,分别提供了 `#afterNodeAccess(Node e)`、`#afterNodeInsertion(boolean evict)`、`#afterNodeRemoval(Node e)` 回调方法。这样,LinkedHashMap 可以通过它们实现自定义拓展逻辑。 + +## 6.1 afterNodeAccess + +在 `accessOrder` 属性为 `true` 时,当 Entry 节点被访问时,放置到链表的结尾,被 `tail` 指向。所以 `#afterNodeAccess(Node e)` 方法的代码如下: + +``` +// LinkedHashMap.java + +void afterNodeAccess(Node e) { // move node to last + LinkedHashMap.Entry last; + // accessOrder 判断必须是满足按访问顺序。 + // (last = tail) != e 将 tail 赋值给 last ,并且判断是否 e 已经是队尾。如果是队尾,就不用处理了。 + if (accessOrder && (last = tail) != e) { + // 将 e 赋值给 p 【因为要 Node 类型转换成 Entry 类型】 + // 同时 b、a 分别是 e 的前后节点 + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + // 第一步,将 p 从链表中移除 + p.after = null; + // 处理 b 的下一个节点指向 a + if (b == null) + head = a; + else + b.after = a; + // 处理 a 的前一个节点指向 b + if (a != null) + a.before = b; + else + last = b; + // 第二步,将 p 添加到链表的尾巴。实际这里的代码,和 linkNodeLast 是一致的。 + if (last == null) + head = p; + else { + p.before = last; + last.after = p; + } + // tail 指向 p ,实际就是 e 。 + tail = p; + // 增加修改次数 + ++modCount; + } +} +``` + +- 代码已经添加详细的注释,胖友认真看看噢。 +- 链表的操作看起来比较繁琐,实际一共分成两步:1)第一步,将 `p` 从链表中移除;2)将 `p` 添加到链表的尾巴。 + +因为 HashMap 提供的 `#get(Object key)` 和 `#getOrDefault(Object key, V defaultValue)` 方法,并未调用 `#afterNodeAccess(Node e)` 方法,这在按照**读取**顺序访问显然不行,所以 LinkedHashMap 重写这两方法的代码,如下: + +``` +// LinkedHashMap.java + +public V get(Object key) { + // 获得 key 对应的 Node + Node e; + if ((e = getNode(hash(key), key)) == null) + return null; + // 如果访问到,回调节点被访问 + if (accessOrder) + afterNodeAccess(e); + return e.value; +} + +public V getOrDefault(Object key, V defaultValue) { + // 获得 key 对应的 Node + Node e; + if ((e = getNode(hash(key), key)) == null) + return defaultValue; + // 如果访问到,回调节点被访问 + if (accessOrder) + afterNodeAccess(e); + return e.value; +} +``` + +## 6.2 afterNodeInsertion + +在开始看 `#afterNodeInsertion(boolean evict)` 方法之前,我们先来看看如何基于 LinkedHashMap 实现 LRU 算法的缓存。代码如下: + +``` +class LRUCache extends LinkedHashMap { + + private final int CACHE_SIZE; + + /** + * 传递进来最多能缓存多少数据 + * + * @param cacheSize 缓存大小 + */ + public LRUCache(int cacheSize) { + // true 表示让 LinkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。 + super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); + CACHE_SIZE = cacheSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + // 当 map 中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。 + return size() > CACHE_SIZE; + } + +} +``` + +为什么能够这么实现呢?我们在 `#afterNodeInsertion(boolean evict)` 方法中来理解。代码如下: + +``` +// LinkedHashMap.java + +// evict 翻译为驱逐,表示是否允许移除元素 +void afterNodeInsertion(boolean evict) { // possibly remove eldest + LinkedHashMap.Entry first; + // first = head 记录当前头节点。因为移除从头开始,最老 + // <1> removeEldestEntry(first) 判断是否满足移除最老节点 + if (evict && (first = head) != null && removeEldestEntry(first)) { + // <2> 移除指定节点 + K key = first.key; + removeNode(hash(key), key, null, false, true); + } +} +``` + +- `<1>` 处,调用 `#removeEldestEntry(Map.Entry eldest)` 方法,判断是否移除最老节点。代码如下: + + ``` + // LinkedHashMap.java + + protected boolean removeEldestEntry(Map.Entry eldest) { + return false; + } + ``` + + - 默认情况下,**都不**移除最老的节点。所以在上述的 LRU 缓存的示例,重写了该方法,判断 LinkedHashMap 是否超过缓存最大大小。如果是,则移除最老的节点。 + +- `<2>` 处,如果满足条件,则调用 `#removeNode(...)` 方法,移除最老的节点。 + +😈 这样,是不是很容易理解基于 LinkedHashMap 实现 LRU 算法的缓存。 + +## 6.3 afterNodeRemoval + +在节点被移除时,LinkedHashMap 需要将节点也从链表中移除,所以重写 `#afterNodeRemoval(Node e)` 方法,实现该逻辑。代码如下: + +``` +// LinkedHashMap.java + +void afterNodeRemoval(Node e) { // unlink + // 将 e 赋值给 p 【因为要 Node 类型转换成 Entry 类型】 + // 同时 b、a 分别是 e 的前后节点 + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + // 将 p 从链表中移除 + p.before = p.after = null; + // 处理 b 的下一个节点指向 a + if (b == null) + head = a; + else + b.after = a; + // 处理 a 的前一个节点指向 b + if (a == null) + tail = b; + else + a.before = b; +} +``` + +# 7. 转换成数组 + +因为 LinkedHashMap 需要满足按**顺序**访问,所以需要重写 HashMap 提供的好多方法,例如说本小节我们看到的几个。 + +`#keysToArray(T[] a)` 方法,转换出 key 数组**顺序**返回。代码如下: + +``` +// LinkedHashMap.java + +@Override +final T[] keysToArray(T[] a) { + Object[] r = a; + int idx = 0; + // 通过 head 顺序遍历,从头到尾 + for (LinkedHashMap.Entry e = head; e != null; e = e.after) { + r[idx++] = e.key; + } + return a; +} +``` + +- 要小心噢,必须保证 `a` 放得下 LinkedHashMap 所有的元素。 + +`#valuesToArray(T[] a)` 方法,转换出 value 数组**顺序**返回。代码如下: + +``` +// LinkedHashMap.java + +@Override +final T[] valuesToArray(T[] a) { + Object[] r = a; + int idx = 0; + // 通过 head 顺序遍历,从头到尾 + for (LinkedHashMap.Entry e = head; e != null; e = e.after) { + r[idx++] = e.value; + } + return a; +} +``` + +> 艿艿:看到此处,胖友基本可以结束本文落。 + +# 8. 转换成 Set/Collection + +`#keySet()` 方法,获得 key Set 。代码如下: + +``` +// LinkedHashMap.java + +public Set keySet() { + // 获得 keySet 缓存 + Set ks = keySet; + // 如果不存在,则进行创建 + if (ks == null) { + ks = new LinkedKeySet(); // LinkedKeySet 是 LinkedHashMap 自定义的 + keySet = ks; + } + return ks; +} +``` + +- 其中, LinkedKeySet 是 LinkedHashMap 自定义的 Set 实现类。代码如下: + + ``` + // LinkedHashMap.java + + final class LinkedKeySet extends AbstractSet { + + public final int size() { return size; } + public final void clear() { LinkedHashMap.this.clear(); } + public final Iterator iterator() { + return new LinkedKeyIterator(); // + } + public final boolean contains(Object o) { return containsKey(o); } + public final boolean remove(Object key) { + return removeNode(hash(key), key, null, false, true) != null; + } + public final Spliterator spliterator() { + return Spliterators.spliterator(this, Spliterator.SIZED | + Spliterator.ORDERED | + Spliterator.DISTINCT); + } + + public Object[] toArray() { + return keysToArray(new Object[size]); + } + + public T[] toArray(T[] a) { + return keysToArray(prepareArray(a)); + } + + public final void forEach(Consumer action) { + if (action == null) + throw new NullPointerException(); + int mc = modCount; + for (LinkedHashMap.Entry e = head; e != null; e = e.after) + action.accept(e.key); + if (modCount != mc) + throw new ConcurrentModificationException(); + } + } + ``` + + - 其内部,调用的都是 LinkedHashMap 提供的方法。 + +`#values()` 方法,获得 value Collection 。代码如下: + +``` +// LinkedHashMap.java + +public Collection values() { + // 获得 values 缓存 + Collection vs = values; + // 如果不存在,则进行创建 + if (vs == null) { + vs = new LinkedValues(); // LinkedValues 是 LinkedHashMap 自定义的 + values = vs; + } + return vs; +} +``` + +- 其中, LinkedValues 是 LinkedHashMap 自定义的 Collection 实现类。代码如下: + + ``` + // LinkedHashMap.java + + final class LinkedValues extends AbstractCollection { + public final int size() { return size; } + public final void clear() { LinkedHashMap.this.clear(); } + public final Iterator iterator() { + return new LinkedValueIterator(); // + } + public final boolean contains(Object o) { return containsValue(o); } + public final Spliterator spliterator() { + return Spliterators.spliterator(this, Spliterator.SIZED | + Spliterator.ORDERED); + } + + public Object[] toArray() { + return valuesToArray(new Object[size]); + } + + public T[] toArray(T[] a) { + return valuesToArray(prepareArray(a)); + } + + public final void forEach(Consumer action) { + if (action == null) + throw new NullPointerException(); + int mc = modCount; + for (LinkedHashMap.Entry e = head; e != null; e = e.after) + action.accept(e.value); + if (modCount != mc) + throw new ConcurrentModificationException(); + } + } + ``` + + - 其内部,调用的都是 LinkedHashMap 提供的方法。 + +`#entrySet()` 方法,获得 key-value Set 。代码如下: + +``` +// LinkedHashMap.java + +public Set> entrySet() { + Set> es; + // LinkedEntrySet 是 LinkedHashMap 自定义的 + return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es; +} +``` + +- 其中, LinkedEntrySet 是 LinkedHashMap 自定义的 Set 实现类。代码如下: + + ``` + // LinkedHashMap.java + + final class LinkedEntrySet extends AbstractSet> { + public final int size() { return size; } + public final void clear() { LinkedHashMap.this.clear(); } + public final Iterator> iterator() { + return new LinkedEntryIterator(); // + } + public final boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry) o; + Object key = e.getKey(); + Node candidate = getNode(hash(key), key); + return candidate != null && candidate.equals(e); + } + public final boolean remove(Object o) { + if (o instanceof Map.Entry) { + Map.Entry e = (Map.Entry) o; + Object key = e.getKey(); + Object value = e.getValue(); + return removeNode(hash(key), key, value, true, true) != null; + } + return false; + } + public final Spliterator> spliterator() { + return Spliterators.spliterator(this, Spliterator.SIZED | + Spliterator.ORDERED | + Spliterator.DISTINCT); + } + public final void forEach(Consumer> action) { + if (action == null) + throw new NullPointerException(); + int mc = modCount; + for (LinkedHashMap.Entry e = head; e != null; e = e.after) + action.accept(e); + if (modCount != mc) + throw new ConcurrentModificationException(); + } + } + ``` + + - 其内部,调用的都是 LinkedHashMap 提供的方法。 + +在上面的代码中,艿艿实际标记了三处 `` 标记,分别是 LinkedKeyIterator、LinkedValueIterator、LinkedEntryIterator ,用于迭代遍历 key、value、Entry 。而它们都继承了 LinkedHashIterator 抽象类,代码如下: + +``` +// LinkedHashMap.java + +abstract class LinkedHashIterator { + + /** + * 下一个节点 + */ + LinkedHashMap.Entry next; + /** + * 当前节点 + */ + LinkedHashMap.Entry current; + /** + * 修改次数 + */ + int expectedModCount; + + LinkedHashIterator() { + next = head; + expectedModCount = modCount; + current = null; + } + + public final boolean hasNext() { + return next != null; + } + + final LinkedHashMap.Entry nextNode() { + LinkedHashMap.Entry e = next; + // 如果发生了修改,抛出 ConcurrentModificationException 异常 + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + // 如果 e 为空,说明没有下一个节点,则抛出 NoSuchElementException 异常 + if (e == null) + throw new NoSuchElementException(); + // 遍历到下一个节点 + current = e; + next = e.after; + return e; + } + + public final void remove() { + // 移除当前节点 + Node p = current; + if (p == null) + throw new IllegalStateException(); + // 如果发生了修改,抛出 ConcurrentModificationException 异常 + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + // 标记 current 为空,因为被移除了 + current = null; + // 移除节点 + removeNode(p.hash, p.key, null, false, false); + // 修改 expectedModCount 次数 + expectedModCount = modCount; + } + +} + +final class LinkedKeyIterator extends LinkedHashIterator + implements Iterator { + + // key + public final K next() { return nextNode().getKey(); } +} + +final class LinkedValueIterator extends LinkedHashIterator + implements Iterator { + + // value + public final V next() { return nextNode().value; } +} + +final class LinkedEntryIterator extends LinkedHashIterator + implements Iterator> { + + // Entry + public final Map.Entry next() { return nextNode(); } +} +``` + +# 9. 清空 + +`#clear()` 方法,清空 LinkedHashMap 。代码如下: + +``` +// LinkedHashMap.java + +public void clear() { + // 清空 + super.clear(); + // 标记 head 和 tail 为 null + head = tail = null; +} +``` + +- 需要额外清空 `head`、`tail` 。 + +# 10. 其它方法 + +> 本小节,我们会罗列下其他 LinkedHashMap 重写的方法。当然,可以选择不看。 + +在序列化时,会调用到 `#internalWriteEntries(java.io.ObjectOutputStream s)` 方法,重写代码如下: + +``` +// LinkedHashMap.java + +void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException { + // 通过 head 顺序遍历,从头到尾 + for (LinkedHashMap.Entry e = head; e != null; e = e.after) { + // 写入 key + s.writeObject(e.key); + // 写入 value + s.writeObject(e.value); + } +} +``` + +在反序列化时,会调用 `#reinitialize()` 方法,重写代码如下: + +``` +// LinkedHashMap.java + +void reinitialize() { + // 调用父方法,初始化 + super.reinitialize(); + // 标记 head 和 tail 为 null + head = tail = null; +} +``` + +查找值时,会调用 `#containsValue(Object value)` 方法,重写代码如下: + +``` +// LinkedHashMap.java + +public boolean containsValue(Object value) { + // 通过 head 顺序遍历,从头到尾 + for (LinkedHashMap.Entry e = head; e != null; e = e.after) { + V v = e.value; + // 判断是否相等 + if (v == value || (value != null && value.equals(v))) + return true; + } + return false; +} +``` + +# 666. 彩蛋 + +如下几个方法,是 LinkedHashMap 重写和红黑树相关的几个方法,胖友可以自己瞅瞅: + +- `#replacementNode(Node p, Node next)` +- `#newTreeNode(int hash, K key, V value, Node next)` +- `#replacementTreeNode(Node p, Node next)` +- `#transferLinks(LinkedHashMap.Entry src, LinkedHashMap.Entry dst)` + +下面,我们来对 LinkedHashMap 做一个简单的小结: + +- LinkedHashMap 是 HashMap 的子类,增加了 + + 顺序 + + 访问的特性。 + + - 【默认】当 `accessOrder = false` 时,按照 key-value 的**插入**顺序进行访问。 + - 当 `accessOrder = true` 时,按照 key-value 的**读取**顺序进行访问。 + +- LinkedHashMap 的**顺序**特性,通过内部的双向链表实现,所以我们把它看成是 LinkedList + LinkedHashMap 的组合。 + +- LinkedHashMap 通过重写 HashMap 提供的回调方法,从而实现其对**顺序**的特性的处理。同时,因为 LinkedHashMap 的**顺序**特性,需要重写 `#keysToArray(T[] a)` 等**遍历**相关的方法。 + +- LinkedHashMap 可以方便实现 LRU 算法的缓存, \ No newline at end of file diff --git a/jdk/07-JDK 源码解析-集合(五)哈希集合 HashSet.assets/01.png b/jdk/07-JDK 源码解析-集合(五)哈希集合 HashSet.assets/01.png new file mode 100644 index 0000000..d4c9659 Binary files /dev/null and b/jdk/07-JDK 源码解析-集合(五)哈希集合 HashSet.assets/01.png differ diff --git a/jdk/07-JDK 源码解析-集合(五)哈希集合 HashSet.md b/jdk/07-JDK 源码解析-集合(五)哈希集合 HashSet.md new file mode 100644 index 0000000..4d09607 --- /dev/null +++ b/jdk/07-JDK 源码解析-集合(五)哈希集合 HashSet.md @@ -0,0 +1,301 @@ +# 精尽 JDK 源码解析 —— 集合(五)哈希集合 HashSet + +# 1. 概述 + +> 艿艿:突然有点词穷,不知道怎么介绍,嘿嘿。 + +HashSet ,基于 HashMap 的 Set 实现类。在业务中,如果我们有排重的需求,一般会考虑使用 HashSet 。 + +在 Redis 提供的 Set 数据结构,不考虑编码的情况下,它是基于 Redis 自身的 Hash 数据结构实现的。这点,JDK 和 Redis 是相同的。😈 感兴趣的胖友,看完本文后,可以自己去撸一撸。 + +# 2. 类图 + +HashSet 实现的接口、继承的类,如下图所示:[![类图](07-JDK 源码解析-集合(五)哈希集合 HashSet.assets/01.png)](http://static.iocoder.cn/images/JDK/2019_12_13/01.png)类图 + +- 实现 [`java.util.Set`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/Set.java) 接口,并继承 [`java.util.AbstractSet`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractSet.java) 抽像类。 +- 实现 [`java.io.Serializable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/io/Serializable.java) 接口。 +- 实现 [`java.lang.Cloneable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/lang/Cloneable.java) 接口。 + +# 3. 属性 + +HashSet 只有一个属性,那就是 `map` 。代码如下: + +``` +// HashSet.java + +private transient HashMap map; +``` + +- `map` 的 key ,存储 HashSet 的每个 key 。 + +- `map` 的 value ,因为 HashSet 没有 value 的需要,所以使用一个统一的 `PRESENT` 即可。代码如下: + + ``` + // HashSet.java + + // Dummy value to associate with an Object in the backing Map + private static final Object PRESENT = new Object(); + ``` + +# 4. 构造方法 + +HashSet 一共有 5 个构造方法,代码如下: + +``` +// HashSet.java + +public HashSet() { + map = new HashMap<>(); +} + +public HashSet(Collection c) { + // 最小必须是 16 。 + // (int) (c.size()/.75f) + 1 避免扩容 + map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); + // 批量添加 + addAll(c); +} + +public HashSet(int initialCapacity, float loadFactor) { + map = new HashMap<>(initialCapacity, loadFactor); +} + +public HashSet(int initialCapacity) { + map = new HashMap<>(initialCapacity); +} + +HashSet(int initialCapacity, float loadFactor, boolean dummy) { + map = new LinkedHashMap<>(initialCapacity, loadFactor); // 注意,这种情况下的构造方法,创建的是 LinkedHashMap 对象 +} +``` + +- 在构造方法中,会创建 HashMap 或 LinkedHashMap 对象。 + +# 5. 添加单个元素 + +`#add(E e)` 方法,添加单个元素。代码如下: + +``` +// HashSet.java + +public boolean add(E e) { + return map.put(e, PRESENT)==null; +} +``` + +- `map` 的 value 值,就是我们看到的 `PRESENT` 。 + +而添加多个元素,继承自 AbstractCollection 抽象类,通过 `#addAll(Collection c)` 方法,代码如下: + +``` +// AbstractCollection.java + +public boolean addAll(Collection c) { + boolean modified = false; + // 遍历 c 集合,逐个添加 + for (E e : c) + if (add(e)) + modified = true; + return modified; +} +``` + +- 在方法内部,会逐个调用 `#add(E e)` 方法,逐个添加单个元素。 + +# 6. 移除单个元素 + +`#remove(Object key)` 方法,移除 key 对应的 value ,并返回该 value 。代码如下: + +``` +// HashSet.java + +public boolean remove(Object o) { + return map.remove(o) == PRESENT; +} +``` + +# 7. 查找单个元素 + +`#contains(Object key)` 方法,判断 key 是否存在。代码如下: + +``` +// HashSet.java + +public boolean contains(Object o) { + return map.containsKey(o); +} +``` + +> 艿艿:后面的内容,快速看即可。 + +# 8. 转换成数组 + +`#toArray(T[] a)` 方法,转换出 key 数组返回。代码如下: + +``` +// HashSet.java + +@Override +public Object[] toArray() { + return map.keysToArray(new Object[map.size()]); +} + +@Override +public T[] toArray(T[] a) { + return map.keysToArray(map.prepareArray(a)); +} +``` + +# 9. 清空 + +`#clear()` 方法,清空 HashSet 。代码如下: + +``` +// HashSet.java + +public void clear() { + map.clear(); +} +``` + +# 10. 序列化 + +`#writeObject(ObjectOutputStream s)` 方法,序列化 HashSet 对象。代码如下: + +``` +// HashSet.java + +@java.io.Serial +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out any hidden serialization magic + // 写入非静态属性、非 transient 属性 + s.defaultWriteObject(); + + // Write out HashMap capacity and load factor + // 写入 map table 数组大小 + s.writeInt(map.capacity()); + // 写入 map 加载因子 + s.writeFloat(map.loadFactor()); + + // Write out size + // 写入 map 大小 + s.writeInt(map.size()); + + // Write out all elements in the proper order. + // 遍历 map ,逐个 key 序列化 + for (E e : map.keySet()) + s.writeObject(e); +} +``` + +# 11. 反序列化 + +`#readObject(ObjectInputStream s)` 方法,反序列化成 HashSet 对象。代码如下: + +``` +// HashSet.java + +@java.io.Serial +private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + // Read in any hidden serialization magic + // 读取非静态属性、非 transient 属性 + s.defaultReadObject(); + + // Read capacity and verify non-negative. + // 读取 HashMap table 数组大小 + int capacity = s.readInt(); + // 校验 capacity 参数 + if (capacity < 0) { + throw new InvalidObjectException("Illegal capacity: " + + capacity); + } + + // Read load factor and verify positive and non NaN. + // 获得加载因子 loadFactor + float loadFactor = s.readFloat(); + // 校验 loadFactor 参数 + if (loadFactor <= 0 || Float.isNaN(loadFactor)) { + throw new InvalidObjectException("Illegal load factor: " + + loadFactor); + } + + // Read size and verify non-negative. + // 读取 key-value 键值对数量 size + int size = s.readInt(); + // 校验 size 参数 + if (size < 0) { + throw new InvalidObjectException("Illegal size: " + + size); + } + + // Set the capacity according to the size and load factor ensuring that + // the HashMap is at least 25% full but clamping to maximum capacity. + // 计算容量 + capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f), + HashMap.MAXIMUM_CAPACITY); + + // Constructing the backing map will lazily create an array when the first element is + // added, so check it before construction. Call HashMap.tableSizeFor to compute the + // actual allocation size. Check Map.Entry[].class since it's the nearest public type to + // what is actually created. + SharedSecrets.getJavaObjectInputStreamAccess() + .checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity)); // 不知道作甚,哈哈哈。 + + // Create backing HashMap + // 创建 LinkedHashMap 或 HashMap 对象 + map = (((HashSet)this) instanceof LinkedHashSet ? + new LinkedHashMap<>(capacity, loadFactor) : + new HashMap<>(capacity, loadFactor)); + + // Read in all elements in the proper order. + // 遍历读取 key 键,添加到 map 中 + for (int i=0; i newSet = (HashSet) super.clone(); + // 可控 mao 属性,赋值给 newSet + newSet.map = (HashMap) map.clone(); + // 返回 + return newSet; + } catch (CloneNotSupportedException e) { + throw new InternalError(e); + } +} +``` + +# 13. 获得迭代器 + +`#iterator()` 方法,获得迭代器。代码如下: + +``` +// HashSet.java + +public Iterator iterator() { + return map.keySet().iterator(); +} +``` + +# 666. 彩蛋 + +😈 总的来说,比较简单,相信胖友一会会时间就已经看完了。 + +可能偶尔我们会使用到 LinkedHashSet ,它是 HashSet 的子类,胖友自己去看。更加简单~ + +关于 HashSet 的总结,只有一句话:HashSet 是基于 HashMap 的 Set 实现类。 \ No newline at end of file diff --git a/jdk/08-JDK 源码解析-集合(六)TreeMap.assets/01.png b/jdk/08-JDK 源码解析-集合(六)TreeMap.assets/01.png new file mode 100644 index 0000000..5980550 Binary files /dev/null and b/jdk/08-JDK 源码解析-集合(六)TreeMap.assets/01.png differ diff --git a/jdk/08-JDK 源码解析-集合(六)TreeMap.assets/02.png b/jdk/08-JDK 源码解析-集合(六)TreeMap.assets/02.png new file mode 100644 index 0000000..78e7a28 Binary files /dev/null and b/jdk/08-JDK 源码解析-集合(六)TreeMap.assets/02.png differ diff --git a/jdk/08-JDK 源码解析-集合(六)TreeMap.assets/03.png b/jdk/08-JDK 源码解析-集合(六)TreeMap.assets/03.png new file mode 100644 index 0000000..4dfd23e Binary files /dev/null and b/jdk/08-JDK 源码解析-集合(六)TreeMap.assets/03.png differ diff --git a/jdk/08-JDK 源码解析-集合(六)TreeMap.md b/jdk/08-JDK 源码解析-集合(六)TreeMap.md new file mode 100644 index 0000000..8ecc825 --- /dev/null +++ b/jdk/08-JDK 源码解析-集合(六)TreeMap.md @@ -0,0 +1,2853 @@ +# 精尽 JDK 源码解析 —— 集合(六)TreeMap + +# 1. 概述 + +在 [《精尽 JDK 源码解析 —— 集合(四)哈希表 LinkedHashMap》](http://svip.iocodder.cn/JDK/Collection-LinkedHashMap) 中,我们提到了两种**有序** Map 的选择。一种是 LinkedHashMap ,以前在该文进行了详细解析,而本文,我们开始 TreeMap 之旅,**按照 key 的顺序**的 Map 实现类。 + +在开始之前,艿艿捉摸了下,什么业务场景下适合使用 TreeMap 呢?发现好像基本没有,嘿嘿。然后,我又翻了自己团队的几个项目,发现唯一在使用的,就是在 [签名生成算法](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3) 时,要求按照请求参数的 key 排序,然后拼接后加密。如果胖友有 TreeMap 的使用场景,请一定在星球给留言,艿艿可以补充到本文。 + +# 2. 类图 + +TreeMap 实现的接口、继承的类,如下图所示:[![类图](08-JDK 源码解析-集合(六)TreeMap.assets/01.png)](http://static.iocoder.cn/images/JDK/2019_12_13_02/01.png)类图 + +- 实现 [`java.util.Map`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/Map.java) 接口,并继承 [`java.util.AbstractMap`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractMap.java) 抽像类。 + +- 直接实现 + + + + `java.util.NavigableMap` + + + + 接口,间接实现 + + + + `java.util.NavigableMap` + + + + 接口。关于这两接口的定义的操作,已经添加注释,胖友可以直接点击查看。因为 SortedMap 有很多雷同的寻找最接近 key 的操作,这里简单总结下: + + - lower :小于 ;floor :小于等于 + - higher :大于;ceiling :大于等于 + +- 实现 [`java.io.Serializable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/io/Serializable.java) 接口。 + +- 实现 [`java.lang.Cloneable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/lang/Cloneable.java) 接口。 + +# 3. 属性 + +在开始看 TreeMap 的具体属性之前,我们先来简单说说 TreeMap 的实现原理。 + +在 [《精尽 JDK 源码解析 —— 集合(三)哈希表 HashMap》](http://svip.iocoder.cn/JDK/Collection-HashMap) 文章中,我们提到,HashMap 的数组,每个桶的链表在元素过多的情况下,会转换成**红黑树**。而 TreeMap 也是基于红黑树实现的,并且只是**一棵**红黑树。所以 TreeMap 可以理解成 HashMap 数组中的**一个**转换成**红黑树**的桶。 + +为什么 TreeMap 会采用红黑树实现呢?我们来看一段红黑树的定义: + +> FROM [《维基百科 —— 红黑树》](https://zh.wikipedia.org/wiki/红黑树) +> +> **红黑树**(英语:Red–black tree)是一种[自平衡二叉查找树](https://zh.wikipedia.org/wiki/自平衡二叉查找树),是在[计算机科学](https://zh.wikipedia.org/wiki/计算机科学)中用到的一种[数据结构](https://zh.wikipedia.org/wiki/数据结构),典型的用途是实现[关联数组](https://zh.wikipedia.org/wiki/关联数组)。 +> +> 它在 1972 年由[鲁道夫·贝尔](https://zh.wikipedia.org/wiki/鲁道夫·贝尔)发明,被称为”**对称二叉B树**“,它现代的名字源于 Leo J. Guibas 和 [Robert Sedgewick](https://zh.wikipedia.org/wiki/Robert_Sedgewick) 于 [1978年] (https://zh.wikipedia.org/wiki/1978年) 写的一篇论文。红黑树的结构复杂,但它的操作有着良好的最坏情况[运行时间](https://zh.wikipedia.org/wiki/算法分析),并且在实践中高效:它可以在 `logN` 时间内完成查找,插入和删除,这里的 `N` 是树中元素的数目。 + +- **有序性**:红黑树是一种**二叉查找树**,父节点的 key 小于左子节点的 key ,大于右子节点的 key 。这样,就完成了 TreeMap 的有序的特性。 +- **高性能**:红黑树会进行**自平衡**,避免树的高度过高,导致查找性能下滑。这样,红黑树能够提供 `logN` 的时间复杂度。 + +> 艿艿:绝大多数情况下,包括面试,我们无需了解红黑树是怎么实现的,甚至原理是什么。只要知道,红黑树的上述概念,和时间复杂度即可。 +> +> 所以,本文我们不会涉及红黑树的**自平衡**的内容。如果感兴趣的胖友,可以自己阅读如下的文章: +> +> - [《教你初步了解红黑树》](https://blog.csdn.net/v_july_v/article/details/6105630) +> - [《史上最清晰的红黑树讲解(上)》](https://www.cnblogs.com/CarpenterLee/p/5503882.html) +> - [《史上最清晰的红黑树讲解(下)》](https://www.cnblogs.com/CarpenterLee/p/5525688.html) + +下面,让我们来看看 TreeMap 的属性。代码如下: + +``` +// TreeMap.java + +/** + * key 排序器 + */ +private final Comparator comparator; + +/** + * 红黑树的根节点 + */ +private transient Entry root; + +/** + * key-value 键值对数量 + */ +private transient int size = 0; + +/** + * 修改次数 + */ +private transient int modCount = 0; +``` + +- `comparator` 属性,key 排序器。通过该属性,可以自定义 key 的排序规则。如果未设置,则使用 key 类型自己的排序。 + +- `root` 属性,红黑树的根节点。其中,Entry 是 TreeMap 的内部静态类,代码如下: + + ``` + // TreeMap.java + + /** + * 颜色 - 红色 + */ + private static final boolean RED = false; + /** + * 颜色 - 黑色 + */ + private static final boolean BLACK = true; + + static final class Entry implements Map.Entry { + + /** + * key 键 + */ + K key; + /** + * value 值 + */ + V value; + /** + * 左子节点 + */ + Entry left; + /** + * 右子节点 + */ + Entry right; + /** + * 父节点 + */ + Entry parent; + /** + * 颜色 + */ + boolean color = BLACK; + + // ... 省略一些 + + } + ``` + +# 4. 构造方法 + +TreeMap 一共有四个构造方法,我们分别来看看。 + +**① `#TreeMap()`** + +``` +// TreeMap.java + +public TreeMap() { + comparator = null; +} +``` + +- 默认构造方法,不使用自定义排序,所以此时 `comparator` 为空。 + +**② `#TreeMap(Comparator comparator)`** + +``` +// TreeMap.java + +public TreeMap(Comparator comparator) { + this.comparator = comparator; +} +``` + +- 可传入 `comparator` 参数,自定义 key 的排序规则。 + +**③ `#TreeMap(SortedMap m)`** + +``` +// TreeMap.java + +public TreeMap(SortedMap m) { + // <1> 设置 comparator 属性 + comparator = m.comparator(); + try { + // <2> 使用 m 构造红黑树 + buildFromSorted(m.size(), m.entrySet().iterator(), null, null); + } catch (java.io.IOException | ClassNotFoundException cannotHappen) { + } +} +``` + +- 传入已经排序的 `m` ,然后构建出 TreeMap 。 +- `<1>` 处,使用 `m` 的 key 排序器,设置到 `comparator` 属性。 +- `<2>` 处,调用 `#buildFromSorted(int size, Iterator it, ObjectInputStream str, V defaultVal)` 方法,使用 `m` 构造红黑树。因为 `m` 是 SortedMap 类型,所以天然**有序**,所以可以基于 `m` 的中间为红黑树的根节点,`m` 的左边为左子树,`m` 的右边为右子树。😈 胖友,发挥下自己的想象力。 + +`#buildFromSorted(int size, Iterator it, ObjectInputStream str, V defaultVal)` ,代码如下: + +``` +// TreeMap.java + +private void buildFromSorted(int size, Iterator it, + java.io.ObjectInputStream str, + V defaultVal) + throws java.io.IOException, ClassNotFoundException { + // <1> 设置 key-value 键值对的数量 + this.size = size; + // <2> computeRedLevel(size) 方法,计算红黑树的高度 + // <3> 使用 m 构造红黑树,返回根节点 + root = buildFromSorted(0, 0, size - 1, computeRedLevel(size), + it, str, defaultVal); +} +``` + +- `<1>` 处,设置 key-value 键值对的数量到 `size` 。 + +- `<2>` 处,调用 `#computeRedLevel(int size)` 方法,计算红黑树的高度。代码如下: + + ``` + // TreeMap.java + + private static int computeRedLevel(int size) { + return 31 - Integer.numberOfLeadingZeros(size + 1); + } + ``` + +- `<3>` 处,调用 `#buildFromSorted(int level, int lo, int hi, int redLevel, Iterator it, ObjectInputStream str, V defaultVal)` 方法,使用 `m` 构造红黑树,返回根节点。 + +`#buildFromSorted(int level, int lo, int hi, int redLevel, Iterator it, ObjectInputStream str, V defaultVal)` 方法,代码如下: + +``` +// TreeMap.java + +private final Entry buildFromSorted(int level, int lo, int hi, + int redLevel, + Iterator it, + java.io.ObjectInputStream str, + V defaultVal) + throws java.io.IOException, ClassNotFoundException { + /* + * Strategy: The root is the middlemost element. To get to it, we + * have to first recursively construct the entire left subtree, + * so as to grab all of its elements. We can then proceed with right + * subtree. + * + * The lo and hi arguments are the minimum and maximum + * indices to pull out of the iterator or stream for current subtree. + * They are not actually indexed, we just proceed sequentially, + * ensuring that items are extracted in corresponding order. + */ + // <1.1> 递归结束 + if (hi < lo) return null; + + // <1.2> 计算中间值 + int mid = (lo + hi) >>> 1; + + // <2.1> 创建左子树 + Entry left = null; + if (lo < mid) + // <2.2> 递归左子树 + left = buildFromSorted(level + 1, lo, mid - 1, redLevel, + it, str, defaultVal); + + // extract key and/or value from iterator or stream + // <3.1> 获得 key-value 键值对 + K key; + V value; + if (it != null) { // 使用 it 迭代器,获得下一个值 + if (defaultVal == null) { + Map.Entry entry = (Map.Entry)it.next(); // 从 it 获得下一个 Entry 节点 + key = (K) entry.getKey(); // 读取 key + value = (V) entry.getValue(); // 读取 value + } else { + key = (K)it.next(); // 读取 key + value = defaultVal; // 设置 default 为 value + } + } else { // use stream 处理 str 流的情况 + key = (K) str.readObject(); // 从 str 读取 key 值 + value = (defaultVal != null ? defaultVal : (V) str.readObject()); // 从 str 读取 value 值 + } + + + // <3.2> 创建中间节点 + Entry middle = new Entry<>(key, value, null); + + // color nodes in non-full bottommost level red + // <3.3> 如果到树的最大高度,则设置为红节点 + if (level == redLevel) + middle.color = RED; + + // <3.4> 如果左子树非空,则进行设置 + if (left != null) { + middle.left = left; // 当前节点,设置左子树 + left.parent = middle; // 左子树,设置父节点为当前节点 + } + + // <4.1> 创建右子树 + if (mid < hi) { + // <4.2> 递归右子树 + Entry right = buildFromSorted(level + 1, mid + 1, hi, redLevel, + it, str, defaultVal); + // <4.3> 当前节点,设置右子树 + middle.right = right; + // <4.3> 右子树,设置父节点为当前节点 + right.parent = middle; + } + + // 返回当前节点 + return middle; +} +``` + +- 基于**有序**的 `it` 迭代器或者 `str` 输入流,将其的中间点作为根节点,其左边作为左子树,其右边作为右子树。因为是基于递归实现,所以中间点是基于 `lo` 和 `hi` 作为 `it` 或 `str` 的“数组”范围。 + + > 如果胖友有学习过数据结构与算法,这里代码的实现,就是基于 [《五大常用算法之一:分治算法》](https://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741370.html) 。 + +- `<1.1>` 处,如果 `hi` 小于 `lo` ,说明已经超过范围,所以可以结束循环。 + +- `<1.2>` 处,计算中间位置。 + +- 左子树 + + - `<2.1>` 处,处理 `mid` 中间位置的左边,处理左子树。 + - `<2.2>` 处,调用 `#buildFromSorted(int level, int lo, int hi, int redLevel, Iterator it, ObjectInputStream str, V defaultVal)` 方法,递归处理 `it` 或 `str` 的 `[lo, mid - 1]` 范围,创建左子树,返回该子树的根节点,赋值给 `left` 。 + +- 当前节点(中间节点) + + - `<3.1>` 处,获得 key-value 键值对。分成使用 `it` 或 `str` 读取的两种情况。有一点要注意,在 `defaultVal` 非空的时候,使用它作为 value 。 + - `<3.2>` 处,创建当前节点。 + - `<3.3>` 处,如果到树的最大高度,则设置为红节点。 + - `<3.4>` 处,如果左子树非空,则进行设置。 + +- 右子树 + + - `<4.1>` 处,处理 `mid` 中间位置的右边,处理右子树。 + - `<4.2>` 处,调用 `#buildFromSorted(int level, int lo, int hi, int redLevel, Iterator it, ObjectInputStream str, V defaultVal)` 方法,递归处理 `it` 或 `str` 的 `[mid + 1, high]` 范围,创建右子树,返回该子树的根节点,赋值给 `right` 。 + - `<4.3>` 处,设置右子树。 + +- 返回当前节点。因为是递归,所以递归的第一层,是 TreeMap 红黑树的根节点。 + +**④ `#TreeMap(Map m)`** + +``` +// TreeMap.java + +public TreeMap(Map m) { + comparator = null; + // 添加所有元素 + putAll(m); +} +``` + +- 传入 `m` 的是 Map 类型,构建成初始的 TreeMap 。 +- 调用 `#putAll(Map map)` 方法,添加所有元素。 + +`#putAll(Map map)` 方法,代码如下: + +``` +// TreeMap.java + +public void putAll(Map map) { + // <1> 路径一,满足如下条件,调用 buildFromSorted 方法来优化处理 + int mapSize = map.size(); + if (size == 0 // 如果 TreeMap 的大小为 0 + && mapSize != 0 // map 的大小非 0 + && map instanceof SortedMap) { // 如果是 map 是 SortedMap 类型 + if (Objects.equals(comparator, ((SortedMap)map).comparator())) { // 排序规则相同 + // 增加修改次数 + ++modCount; + // 基于 SortedMap 顺序迭代插入即可 + try { + buildFromSorted(mapSize, map.entrySet().iterator(), + null, null); + } catch (java.io.IOException | ClassNotFoundException cannotHappen) { + } + return; + } + } + // <2> 路径二,直接遍历 map 来添加 + super.putAll(map); +} +``` + +- 分成 `<1>` 和 `<2>` 两种情况。其中,`<1>` 是作为优化的方式,处理在 TreeMap 为空时,并且 `map` 为 SortedMap 类型时,可以直接调用 `#buildFromSorted(int level, int lo, int hi, int redLevel, Iterator it, ObjectInputStream str, V defaultVal)` 方法,可以基于 SortedMap 顺序迭代插入即可,性能更优。 + +# 5. 添加单个元素 + +`#put(K key, V value)` 方法,添加单个元素。代码如下: + +``` +// TreeMap.java + +public V put(K key, V value) { + // 记录当前根节点 + Entry t = root; + // <1> 如果无根节点,则直接使用 key-value 键值对,创建根节点 + if (t == null) { + // <1.1> 校验 key 类型。 + compare(key, key); // type (and possibly null) check + + // <1.2> 创建 Entry 节点 + root = new Entry<>(key, value, null); + // <1.3> 设置 key-value 键值对的数量 + size = 1; + // <1.4> 增加修改次数 + modCount++; + return null; + } + // <2> 遍历红黑树 + int cmp; // key 比父节点小还是大 + Entry parent; // 父节点 + // split comparator and comparable paths + Comparator cpr = comparator; + if (cpr != null) { // 如果有自定义 comparator ,则使用它来比较 + do { + // <2.1> 记录新的父节点 + parent = t; + // <2.2> 比较 key + cmp = cpr.compare(key, t.key); + // <2.3> 比 key 小,说明要遍历左子树 + if (cmp < 0) + t = t.left; + // <2.4> 比 key 大,说明要遍历右子树 + else if (cmp > 0) + t = t.right; + // <2.5> 说明,相等,说明要找到的 t 就是 key 对应的节点,直接设置 value 即可。 + else + return t.setValue(value); + } while (t != null); // <2.6> + } else { // 如果没有自定义 comparator ,则使用 key 自身比较器来比较 + if (key == null) // 如果 key 为空,则抛出异常 + throw new NullPointerException(); + @SuppressWarnings("unchecked") + Comparable k = (Comparable) key; + do { + // <2.1> 记录新的父节点 + parent = t; + // <2.2> 比较 key + cmp = k.compareTo(t.key); + // <2.3> 比 key 小,说明要遍历左子树 + if (cmp < 0) + t = t.left; + // <2.4> 比 key 大,说明要遍历右子树 + else if (cmp > 0) + t = t.right; + // <2.5> 说明,相等,说明要找到的 t 就是 key 对应的节点,直接设置 value 即可。 + else + return t.setValue(value); + } while (t != null); // <2.6> + } + // <3> 创建 key-value 的 Entry 节点 + Entry e = new Entry<>(key, value, parent); + // 设置左右子树 + if (cmp < 0) // <3.1> + parent.left = e; + else // <3.2> + parent.right = e; + // <3.3> 插入后,进行自平衡 + fixAfterInsertion(e); + // <3.4> 设置 key-value 键值对的数量 + size++; + // <3.5> 增加修改次数 + modCount++; + return null; +} +``` + +- 虽然比较长,逻辑还是相对清晰的。因为红黑树是二叉查找树,所以我们可以使用二分查找的方式遍历红黑树。循环遍历红黑树的节点,根据不同的结果,进行处理: + + - 如果当前节点比 `key` 小,则遍历左子树。 + - 如果当前节点比 `key` 大,则遍历右子树。 + - 如果当前节点比 `key` 相等,则直接设置该节点的 `value` 即可。 + - 如果遍历到叶子节点,无法满足上述情况,则说明我们需要给 key-value 键值对,创建 Entry 节点。如果比叶子节点小,则作为左子树;如果比叶子节点大,则作为右子树。 + +- `<1>` 处,如果无根节点,则直接使用 key-value 键值对,创建根节点。 + + - `<1.1>` 处,调用 `#compare(Object k1, Object k2)` 方法, 比较 `key` 。代码如下: + + ``` + // TreeMap.java + + final int compare(Object k1, Object k2) { + return comparator == null ? + ((Comparable)k1).compareTo((K)k2) // 如果没有自定义 comparator 比较器,则使用 key 自身比较 + : comparator.compare((K)k1, (K)k2); // 如果有自定义 comparator 比较器,则使用它来比较。 + } + ``` + + - 根据是否有自定义的 `comparator` 比较器,进行 key 的比较。 + + - `<1.2>` 处,创建 key-value 键值对的 Entry 节点,并赋值给 `root` 节点。 + +- ``` + <2> + ``` + + + + 处,遍历红黑树。会分成是否有自定义的 + + + + ``` + comparator + ``` + + + + 作为遍历左右节点的比较器,逻辑是相同的。所以,我们只看 + + + + ``` + cpr != null + ``` + + + + 的部分先。 + + - `<2.1>` 处,记录新的父节点。目的是,如果遍历到叶子节点 `t` 时,无法继续遍历时,此时 `parent` 作为被插入的父节点。 + - `<2.2>` 处,比较 key 。 + - `<2.3>` 处, 比 `key` 小,说明要遍历左子树。 + - `<2.4>` 处,比 `key` 大,说明要遍历右子树。 + - `<2.5>` 处,相等,说明要找到的 `t` 就是 `key` 对应的节点,直接设置 value 即可。 + - `<2.6>` 处,通过 `while(t != null)` 来不断遍历,而 `t` 作为当前遍历到的节点。如果遍历到 `t` 为空时,说明二分查找不到 `key` 对应的节点,此时只能创建 key-value 的节点,根据 `key` 大小作为 `parent` 的左右节点。 + +- ``` + <3> + ``` + + + + 处,创建 key-value 的 Entry 节点。 + + - `<3.1>` 处,如果 `key` 比 `parent` 节点的 key 小,作为 `parent` 的左子节点。 + - `<3.2>` 处,如果 `key` 比 `parent` 节点的 key 大,作为 `parent` 的右子节点。 + - `<3.3>` 处,调用 `fixAfterInsertion(Entry x)` 方法,插入后,进行**自平衡**。关于这块,我们就先不进行深入了。 + +另外,因为 TreeMap 是基于树的结构实现,所以无需考虑扩容问题。 + +# 6. 获得单个元素 + +`#get(Object key)` 方法,获得 `key` 对应的 value 值。代码如下: + +``` +// TreeMap.java + +public V get(Object key) { + // 获得 key 对应的 Entry 节点 + Entry p = getEntry(key); + // 返回 value 值 + return (p == null ? null : p.value); +} + +final Entry getEntry(Object key) { // 不使用 comparator 查找 + // Offload comparator-based version for sake of performance + // 如果自定义了 comparator 比较器,则基于 comparator 比较来查找 + if (comparator != null) + return getEntryUsingComparator(key); + // 如果 key 为空,抛出异常 + if (key == null) + throw new NullPointerException(); + @SuppressWarnings("unchecked") + Comparable k = (Comparable) key; + // 遍历红黑树 + Entry p = root; + while (p != null) { + // 比较值 + int cmp = k.compareTo(p.key); + // 如果 key 小于当前节点,则遍历左子树 + if (cmp < 0) + p = p.left; + // 如果 key 大于当前节点,则遍历右子树 + else if (cmp > 0) + p = p.right; + // 如果 key 相等,则返回该节点 + else + return p; + } + // 查找不到,返回 null + return null; +} + +final Entry getEntryUsingComparator(Object key) { // 使用 comparator 查找 + @SuppressWarnings("unchecked") + K k = (K) key; + Comparator cpr = comparator; + if (cpr != null) { + // 遍历红黑树 + Entry p = root; + while (p != null) { + // 比较值 + int cmp = cpr.compare(k, p.key); + // 如果 key 小于当前节点,则遍历左子树 + if (cmp < 0) + p = p.left; + // 如果 key 大于当前节点,则遍历右子树 + else if (cmp > 0) + p = p.right; + // 如果 key 相等,则返回该节点 + else + return p; + } + } + // 查找不到,返回 null + return null; +} +``` + +- 和我们在 [「5. 添加单个元素」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 中看到的,也是基于红黑树进行二分查找,逻辑是一致的。 +- 如果未自定义 `comparator` 比较器,则调用 `#getEntry(Object key)` 方法,使用 key 自身的排序,进行比较二分查找。 +- 如果有自定义 `comparator` 比较器,则调用 `#getEntryUsingComparator(Object key)` 方法,使用 `comparator` 的排序,进行比较二分查找。 + +`#containsKey(Object key)` 方法,判断是否存在指定 key 。代码如下: + +``` +// TreeMap.java + +public boolean containsKey(Object key) { + return getEntry(key) != null; +} +``` + +- 基于 `#getEntry(key)` 方法来实现。 + +# 7. 删除单个元素 + +相比 [「5. 添加单个元素」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 来说,删除会更加复杂一些。所以呢,我们先看删除的四种情况。为了让案例更加复杂,我们会使用一颗二叉查找树来举例子。因为,在去掉自平衡的逻辑的情况下,红黑树的删除和二叉查查找树的删除逻辑是一致的。 + +对于二叉查找树的删除,需要保证删除节点后,能够继续满足**二叉**和**查找**的特性。 + +[![查找二叉树](08-JDK 源码解析-集合(六)TreeMap.assets/02.png)](http://static.iocoder.cn/images/JDK/2019_12_13_02/02.png)查找二叉树 + +> 该图通过 http://btv.melezinek.cz/binary-search-tree.html 绘制,胖友可以通过使用它,辅助理解这个过程。 + +情况一,无子节点。 +直接删除父节点对其的指向即可。 +例如说,叶子节点 5、11、14、18 。 + +情况二,**只**有左子节点。 +将删除节点的父节点,指向删除节点的左子节点。 +例如说,节点 20 。可以通过将节点 15 的右子节点指向节点 19 。 + +情况三,**只**有右子节点。 +和情况二的处理方式一致。将删除节点的父节点,指向删除节点的右子节点。 +图中暂无示例,胖友自己脑补下,嘿嘿。 + +情况四,有左子节点 + 右子节点。 +这种情况,相对会比较复杂,因为无法使用子节点替换掉删除的节点。所以此时有一个巧妙的思路。我们结合删除节点 15 来举例。 + +- 1、先查找节点 15 的右子树的**最小值**,找到是节点 17 。 +- 2、将节点 17 设置到节点 15 上。因为节点 17 是右子树的最小值,能够满足比节点 15 的左子树都大,右子树都小。这样,问题就可以变成删除节点 17 。 +- 3、删除节点 17 的过程,满足情况三。将节点 19 的左子节点指向节点 18 即可。 + +理解完这四种情况后,我们来看看代码。`#remove(Object key)` 方法,移除 `key` 对应的 Entry 节点。代码如下: + +``` +// TreeMap.java + +public V remove(Object key) { + // <1> 获得 key 对应的 Entry 节点 + Entry p = getEntry(key); + // <2> 如果不存在,则返回 null ,无需删除 + if (p == null) + return null; + + V oldValue = p.value; + // <3> 删除节点 + deleteEntry(p); + return oldValue; +} +``` + +- `<1>` 处,调用 `#getEntry(Object key)` 方法,获得 `key` 对应的 Entry 节点。 +- `<2>` 处,如果不存在,则返回 `null` ,无需删除。 +- `<3>` 处,调用 `#deleteEntry(Entry p)` 方法,删除该节点。 + +`#deleteEntry(Entry p)` 方法,代码如下: + +``` +// TreeMap.java + +private void deleteEntry(Entry p) { + // 增加修改次数 + modCount++; + // 减少 key-value 键值对数 + size--; + + // If strictly internal, copy successor's element to p and then make p + // point to successor. + // <1> 如果删除的节点 p 既有左子节点,又有右子节点, + if (p.left != null && p.right != null) { + // <1.1> 获得右子树的最小值 + Entry s = successor(p); + // <1.2> 修改 p 的 key-value 为 s 的 key-value 键值对 + p.key = s.key; + p.value = s.value; + // <1.3> 设置 p 指向 s 。此时,就变成删除 s 节点了。 + p = s; + } // p has 2 children + + // Start fixup at replacement node, if it exists. + // <2> 获得替换节点 + Entry replacement = (p.left != null ? p.left : p.right); + // <3> 有子节点的情况 + if (replacement != null) { + // Link replacement to parent + // <3.1> 替换节点的父节点,指向 p 的父节点 + replacement.parent = p.parent; + // <3.2.1> 如果 p 的父节点为空,则说明 p 是根节点,直接 root 设置为替换节点 + if (p.parent == null) + root = replacement; + // <3.2.2> 如果 p 是父节点的左子节点,则 p 的父子节的左子节指向替换节点 + else if (p == p.parent.left) + p.parent.left = replacement; + // <3.2.3> 如果 p 是父节点的右子节点,则 p 的父子节的右子节指向替换节点 + else + p.parent.right = replacement; + + // Null out links so they are OK to use by fixAfterDeletion. + // <3.3> 置空 p 的所有指向 + p.left = p.right = p.parent = null; + + // Fix replacement + // <3.4> 如果 p 的颜色是黑色,则执行自平衡 + if (p.color == BLACK) + fixAfterDeletion(replacement); + // <4> 如果 p 没有父节点,说明删除的是根节点,直接置空 root 即可 + } else if (p.parent == null) { // return if we are the only node. + root = null; + // <5> 如果删除的没有左子树,又没有右子树 + } else { // No children. Use self as phantom replacement and unlink. + // <5.1> 如果 p 的颜色是黑色,则执行自平衡 + if (p.color == BLACK) + fixAfterDeletion(p); + + // <5.2> 删除 p 和其父节点的相互指向 + if (p.parent != null) { + // 如果 p 是父节点的左子节点,则置空父节点的左子节点 + if (p == p.parent.left) + p.parent.left = null; + // 如果 p 是父节点的右子节点,则置空父节点的右子节点 + else if (p == p.parent.right) + p.parent.right = null; + // 置空 p 对父节点的指向 + p.parent = null; + } + } +} +``` + +- ``` + <1> + ``` + + + + 处,如果删除的节点 + + + + ``` + p + ``` + + + + 既有左子节点,又有右子节点,则符合我们提到的情况四。在这里,我们需要将其转换成情况三。 + + - `<1.1>` 处,调用 `#successor(Entry t)` 方法,获得右子树的最小值。这里,我们先不深究 `#successor(Entry t)` 方法的具体代码,知道在这里的用途即可。 + - `<1.2>` 处,修改 `p` 的 key-value 为 `s` 的 key-value 键值对。这样,我们就完成 `s` 对 `p` 的替换。 + - `<1.3>` 处,设置 `p` 指向 `s` 。此时,就变成删除 `s` 节点了。此时,情况四就转换成了情况三了。 + +- `<2>` 处,获得替换节点。此时对于 `p` 来说,至多有一个子节点,要么左子节点,要么右子节点,要么没有子节点。 + +- ``` + <3> + ``` + + + + 处,有左子节点,或者右子节点的情况: + + - `<3.1>` 处,替换节点的父节点,指向 `p` 的父节点。 + - `<3.2.1>` + `<3.2.2>` + `<3.2.3>` 处,将 `p` 的父节点的子节点,指向替换节点。 + - `<3.3>` 处, 置空 `p` 的所有指向。 + - `<3.4>` 处,如果 p 的颜色是黑色,则调用 `#fixAfterDeletion(Entry x)` 方法,执行自平衡。 + +- `<4>` 处,如果 `p` 没有父节点,说明删除的是根节点,直接置空 `root` 即可。 + +- ``` + <5> + ``` + + + + 处,既没有左子树,又没有右子树的情况: + + - `<5.1>` 处,如果 p 的颜色是黑色,则调用 `#fixAfterDeletion(Entry x)` 方法,执行自平衡。 + - `<5.2>` 处,删除 `p` 和其父节点的相互指向。 + +这样一看,其实删除节点的逻辑,也并不是怎么复杂噢。感兴趣的胖友,可以去 LeetCode 找树的题目刷一刷,哈哈。 + +在前面,我们漏了一个 `#successor(Entry t)` 静态方法,没有详细来看。获得 `t` 节点的后继节点,代码如下: + +``` +// TreeMap.java + +static TreeMap.Entry successor(Entry t) { + // <1> 如果 t 为空,则返回 null + if (t == null) + return null; + // <2> 如果 t 的右子树非空,则取右子树的最小值 + else if (t.right != null) { + // 先取右子树的根节点 + Entry p = t.right; + // 再取该根节点的做子树的最小值,即不断遍历左节点 + while (p.left != null) + p = p.left; + // 返回 + return p; + // <3> 如果 t 的右子树为空 + } else { + // 先获得 t 的父节点 + Entry p = t.parent; + // 不断向上遍历父节点,直到子节点 ch 不是父节点 p 的右子节点 + Entry ch = t; + while (p != null // 还有父节点 + && ch == p.right) { // 继续遍历的条件,必须是子节点 ch 是父节点 p 的右子节点 + ch = p; + p = p.parent; + } + return p; + } +} +``` + +- 对于树来说,会存在前序遍历,中序遍历,后续遍历。对于二叉查找树来说,中序遍历恰好满足 key **顺序递增**。所以,这个方法是基于中序遍历的方式,寻找传入 `t` 节点的后续节点,也是下一个比 `t` 大的节点。 + +- `<1>` 处,如果 `t` 为空,则返回 `null` 。 + +- `<2>` 处,如果 `t` 有右子树,则右子树的最小值,肯定是它的后继节点。胖友可以自己看下艿艿在代码里写的注释。在 `#deleteEntry(Entry p)` 方法的 `<1.1>` 处,就走了这块代码分支逻辑。 + +- ``` + <3> + ``` + + + + 处,如果 + + + + ``` + t + ``` + + + + 没有右子树,则需要向上遍历父节点。胖友可以自己看下艿艿在代码里写的注释,结合 + + + + 图 + + + + 来理解。 + + - 简单来说,**寻找第一个祖先节点 `p` 是其父节点的左子节点**。因为是中序遍历,该节点的左子树肯定已经遍历完,在没有右子节点的情况下,需要找到其所在的“大子树”,成为左子树的情况。 + - 例如说,节点 14 来说,需要按照 `14 -> 13 -> 15` 的路径,从而找到节点 15 是其后继节点。 + +# 8. 查找接近的元素 + +在 NavigableMap 中,定义了四个查找接近的元素: + +- `#lowerEntry(K key)` 方法,小于 `key` 的节点 +- `#floorEntry(K key)` 方法,小于等于 `key` 的节点 +- `#higherEntry(K key)` 方法,大于 `key` 的节点 +- `#ceilingEntry(K key)` 方法,大于等于 `key` 的节点 + +我们逐个来看看哈。 + +`#ceilingEntry(K key)` 方法,大于等于 `key` 的节点。代码如下: + +``` +// TreeMap.java + +public Map.Entry ceilingEntry(K key) { + // <1> + // <2> + return exportEntry(getCeilingEntry(key)); +} + +static Map.Entry exportEntry(TreeMap.Entry e) { + return (e == null) ? null : + new AbstractMap.SimpleImmutableEntry<>(e); +} + +final Entry getCeilingEntry(K key) { + Entry p = root; + // <3> 循环二叉查找遍历红黑树 + while (p != null) { + // <3.1> 比较 key + int cmp = compare(key, p.key); + // <3.2> 当前节点比 key 大,则遍历左子树,这样缩小节点的值 + if (cmp < 0) { + // <3.2.1> 如果有左子树,则遍历左子树 + if (p.left != null) + p = p.left; + // <3.2.2.> 如果没有,则直接返回该节点 + else + return p; + // <3.3> 当前节点比 key 小,则遍历右子树,这样放大节点的值 + } else if (cmp > 0) { + // <3.3.1> 如果有右子树,则遍历右子树 + if (p.right != null) { + p = p.right; + } else { + // <3.3.2> 找到当前的后继节点 + Entry parent = p.parent; + Entry ch = p; + while (parent != null && ch == parent.right) { + ch = parent; + parent = parent.parent; + } + return parent; + } + // <3.4> 如果相等,则返回该节点即可 + } else + return p; + } + // <3.5> + return null; +} +``` + +- `<1>` 处,调用 `#getCeilingEntry(K key)` 方法,查找满足大于等于 `key` 的 Entry 节点。 + +- `<2>` 处,调用 `#exportEntry(TreeMap.Entry e)` 方法,创建不可变的 SimpleImmutableEntry 节点。这样,避免使用者直接修改节点,例如说修改 `key` 导致破坏红黑树。 + +- 本质上,`#getCeilingEntry(K key)` 方法,是加强版的二叉树查找,在找不到 `key` 的情况下,找到比 `key` **大且最接近**的节点。 + +- `<3>` 处,循环二叉查找遍历红黑树,每一轮都会在 `<3.1>` 处,通过调用 `#compare(Object k1, Object k2)` 方法,比较当前节点的 key 与 `key` 的大小。 + +- `<3.4>` 处,当前节点和 `key` 相等,则返回该节点。此时,我们找到了和 `key` **相等**的节点。 + +- ``` + <3.2> + ``` + + + + 处,当前节点比 + + + + ``` + key + ``` + + + + 大,则遍历左子树,这样缩小节点的值。 + + - `<3.2.1>` 处,如果有左子树,则遍历左子树。 + - `<3.2.2>` 处,如果没有,则直接返回该节点。此时,我们找到的是比 `key` **大且最接近**的节点。 + +- ``` + <3.3> + ``` + + + + 处,当前节点比 + + + + ``` + key + ``` + + + + 小,则遍历右子树,这样放大节点的值。 + + - `<3.3.1>` 处,如果有右子树,则遍历右子树。 + - `<3.3.2>` 处,找到当前的后继节点。这小块的代码,和我们在 [「7. 删除单个元素」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 的 `#successor(Entry t)」` 方法的 `<3>` 处的代码是一致的。 + +- `<3.5>` 处,极端情况下,找不到,返回 `null` 。 + +对于 `<3.3.2>` 的逻辑,可能胖友理解起来会有一点懵逼。我们来看一个示例。如下图:[![查找二叉树](08-JDK 源码解析-集合(六)TreeMap.assets/03.png)](http://static.iocoder.cn/images/JDK/2019_12_13_02/03.png)查找二叉树 + +- 假设查找节点 60 时,遍历路径为 `20 -> 30 -> 40 -> 50` ,此时没有右子树,查找后继节点为不存在,返回 `null`。 + +- 假设查找节点 19 时,遍历路径为 `20 -> 10 -> 15 -> 18` ,此时没有右子树,查找后继节点为节点 20 ,返回节点 20 。 + + > 艿艿:有点不造怎么特别理论的描述,为什么这样 `<3.3.2>` 的逻辑是成立的。 + > + > 从直观感受上来说,对于没有右子树的节点,其后继节点一定大于它。 + > + > 并且,以节点 10 举例子。在我们因为 `key` 比节点 20 小时,遍历其左子树 `leftTree` 。在找不到匹配的节点时,此时 `leftTree` 的根节点 20 ,肯定是满足比 `key` **大且最接近**的节点。恰好,根节点 20 就是节点 18 的后继节点。 + > + > 等后面我在想想怎么能够描述的更清楚。如果胖友有更好的解释,可以星球给艿艿留言。 + > + > 目前的话,可以多画图理解。 + +`#higherEntry(K key)` 方法,大于 `key` 的节点。代码如下: + +``` +// TreeMap.java + +public Map.Entry higherEntry(K key) { + return exportEntry(getHigherEntry(key)); +} + +final Entry getHigherEntry(K key) { + Entry p = root; + // 循环二叉查找遍历红黑树 + while (p != null) { + // 比较 key + int cmp = compare(key, p.key); + // 当前节点比 key 大,则遍历左子树,这样缩小节点的值 + if (cmp < 0) { + // 如果有左子树,则遍历左子树 + if (p.left != null) + p = p.left; + // 如果没有,则直接返回该节点 + else + return p; + // 当前节点比 key 小,则遍历右子树,这样放大节点的值 + } else { + // 如果有右子树,则遍历右子树 + if (p.right != null) { + p = p.right; + } else { + // 找到当前的后继节点 + Entry parent = p.parent; + Entry ch = p; + while (parent != null && ch == parent.right) { + ch = parent; + parent = parent.parent; + } + return parent; + } + } + // 此处,相等的情况下,不返回 + } + // 查找不到,返回 null + return null; +} +``` + +- 和 `#ceilingEntry(K key)` 逻辑的差异,在于 `` 处,相等的情况下,不返回该节点。 + +`#ceilingEntry(K key)` 方法,小于等于 `key` 的节点。代码如下: + +``` +// TreeMap.java + +public Map.Entry floorEntry(K key) { + return exportEntry(getFloorEntry(key)); +} + +final Entry getFloorEntry(K key) { + Entry p = root; + // 循环二叉查找遍历红黑树 + while (p != null) { + // 比较 key + int cmp = compare(key, p.key); + if (cmp > 0) { + // 如果有右子树,则遍历右子树 + if (p.right != null) + p = p.right; + // 如果没有,则直接返回该节点 + else + return p; + // 当前节点比 key 小,则遍历右子树,这样放大节点的值 + } else if (cmp < 0) { + // 如果有左子树,则遍历左子树 + if (p.left != null) { + p = p.left; + } else { + // 找到当前节点的前继节点 + Entry parent = p.parent; + Entry ch = p; + while (parent != null && ch == parent.left) { + ch = parent; + parent = parent.parent; + } + return parent; + } + // 如果相等,则返回该节点即可 + } else + return p; + + } + // 查找不到,返回 null + return null; +} +``` + +- 思路是一致的,胖友自己看下注释噢。 + +`#getLowerEntry(K key)` 方法,小于 `key` 的节点。代码如下: + +``` +// TreeMap.java + +public Map.Entry lowerEntry(K key) { + return exportEntry(getLowerEntry(key)); +} + +final Entry getLowerEntry(K key) { + Entry p = root; + // 循环二叉查找遍历红黑树 + while (p != null) { + // 比较 key + int cmp = compare(key, p.key); + // 当前节点比 key 小,则遍历右子树,这样放大节点的值 + if (cmp > 0) { + // 如果有右子树,则遍历右子树 + if (p.right != null) + p = p.right; + // 如果没有,则直接返回该节点 + else + return p; + // 当前节点比 key 大,则遍历左子树,这样缩小节点的值 + } else { + // 如果有左子树,则遍历左子树 + if (p.left != null) { + p = p.left; + } else { + // 找到当前节点的前继节点 + Entry parent = p.parent; + Entry ch = p; + while (parent != null && ch == parent.left) { + ch = parent; + parent = parent.parent; + } + return parent; + } + } + // 此处,相等的情况下,不返回 + } + // 查找不到,返回 null + return null; +} +``` + +- 思路是一致的,胖友自己看下注释噢。 + +在一些场景下,我们并不需要返回 Entry 节点,只需要返回符合条件的 key 即可。所以有了对应的如下四个方法: + +``` +// TreeMap.java + +public K lowerKey(K key) { + return keyOrNull(getLowerEntry(key)); +} + +public K floorKey(K key) { + return keyOrNull(getFloorEntry(key)); +} + +public K ceilingKey(K key) { + return keyOrNull(getCeilingEntry(key)); +} + +public K higherKey(K key) { + return keyOrNull(getHigherEntry(key)); +} + +static K keyOrNull(TreeMap.Entry e) { + return (e == null) ? null : e.key; +} +``` + +# 9. 获得首尾的元素 + +`#firstEntry()` 方法,获得首个 Entry 节点。代码如下: + +``` +// TreeMap.java + +public Map.Entry firstEntry() { + return exportEntry(getFirstEntry()); +} + +final Entry getFirstEntry() { + Entry p = root; + if (p != null) + // 循环,不断遍历到左子节点,直到没有左子节点 + while (p.left != null) + p = p.left; + return p; +} +``` + +- 通过不断遍历到左子节点,直到没有左子节点。 + +- 在 `#getFirstEntry()` 方法的基础上,还提供了另外两个方法: + + ``` + // TreeMap.java + + public Map.Entry pollFirstEntry() { // 获得并移除首个 Entry 节点 + // 获得首个 Entry 节点 + Entry p = getFirstEntry(); + Map.Entry result = exportEntry(p); + // 如果存在,则进行删除。 + if (p != null) + deleteEntry(p); + return result; + } + + public K firstKey() { + return key(getFirstEntry()); + } + static K key(Entry e) { + if (e == null) // 如果不存在 e 元素,则抛出 NoSuchElementException 异常 + throw new NoSuchElementException(); + return e.key; + } + ``` + +`#lastEntry()` 方法,获得尾部 Entry 节点。代码如下: + +``` +// TreeMap.java + +public Map.Entry lastEntry() { + return exportEntry(getLastEntry()); +} + +final Entry getLastEntry() { + Entry p = root; + if (p != null) + // 循环,不断遍历到右子节点,直到没有右子节点 + while (p.right != null) + p = p.right; + return p; +} +``` + +- 通过不断遍历到右子节点,直到没有右子节点。 + +- 在 `#getLastEntry()` 方法的基础上,还提供了另外两个方法: + + ``` + // TreeMap.java + + public Map.Entry pollLastEntry() { // 获得并移除尾部 Entry 节点 + // 获得尾部 Entry 节点 + Entry p = getLastEntry(); + Map.Entry result = exportEntry(p); + // 如果存在,则进行删除。 + if (p != null) + deleteEntry(p); + return result; + } + + public K lastKey() { + return key(getLastEntry()); + } + ``` + +在这里,补充一个 `#containsValue(Object value)` 方法,通过中序遍历的方式,遍历查找值为 `value` 的节点是否存在。代码如下: + +``` +// TreeMap.java + +public boolean containsValue(Object value) { + for (Entry e = getFirstEntry(); // 获得首个 Entry 节点 + e != null; // 遍历到没有下一个节点 + e = successor(e)) { // 通过中序遍历,获得下一个节点 + if (valEquals(value, e.value)) // 判断值是否相等 + return true; + } + return false; +} + +static final boolean valEquals(Object o1, Object o2) { + return (o1==null ? o2==null : o1.equals(o2)); +} +``` + +# 10. 清空 + +`#clear()` 方法,清空。代码如下: + +``` +// TreeMap.java + +public void clear() { + // 增加修改次数 + modCount++; + // key-value 数量置为 0 + size = 0; + // 设置根节点为 null + root = null; +} +``` + +# 11. 克隆 + +`#clone()` 方法,克隆 TreeMap 。代码如下: + +``` +// TreeMap.java + +public Object clone() { + // 克隆创建 TreeMap 对象 + TreeMap clone; + try { + clone = (TreeMap) super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError(e); + } + + // Put clone into "virgin" state (except for comparator) + // 重置 clone 对象的属性 + clone.root = null; + clone.size = 0; + clone.modCount = 0; + clone.entrySet = null; + clone.navigableKeySet = null; + clone.descendingMap = null; + + // Initialize clone with our mappings + // 使用自己,构造 clone 对象的红黑树 + try { + clone.buildFromSorted(size, entrySet().iterator(), null, null); + } catch (java.io.IOException | ClassNotFoundException cannotHappen) { + } + + return clone; +} +``` + +# 12. 序列化 + +`#writeObject(ObjectOutputStream s)` 方法,序列化 TreeMap 对象。代码如下: + +``` +// TreeMap.java + +@java.io.Serial +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out the Comparator and any hidden stuff + // 写入非静态属性、非 transient 属性 + s.defaultWriteObject(); + + // Write out size (number of Mappings) + // 写入 key-value 键值对数量 + s.writeInt(size); + + // Write out keys and values (alternating) + // 写入具体的 key-value 键值对 + for (Map.Entry e : entrySet()) { + s.writeObject(e.getKey()); + s.writeObject(e.getValue()); + } +} +``` + +- 比较简单,胖友自己瞅瞅即可。 + +# 13. 反序列化 + +`#readObject(ObjectInputStream s)` 方法,反序列化成 TreeMap 对象。代码如下: + +``` +// TreeMap.java + +@java.io.Serial +private void readObject(final java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + // Read in the Comparator and any hidden stuff + // 读取非静态属性、非 transient 属性 + s.defaultReadObject(); + + // Read in size + // 读取 key-value 键值对数量 size + int size = s.readInt(); + + // 使用输入流,构建红黑树。 + // 因为序列化时,已经是顺序的,所以输入流也是顺序的 + buildFromSorted(size, null, s, null); // 注意,此时传入的是 s 参数,输入流 +} +``` + +# 14. 获得迭代器 + +> 艿艿:本小节,可以选择性看,或者不看。 + +`#keyIterator()` 方法,获得 key 的**正序**迭代器。代码如下: + +``` +// TreeMap.java + +Iterator keyIterator() { + return new KeyIterator(getFirstEntry()); // 获得的是首个元素 +} +``` + +- 创建的是 KeyIterator 迭代器。在 [「14.2 KeyIterator」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 详细解析。 + +`#descendingKeyIterator()` 方法,获得 key 的**倒序**迭代器。代码如下: + +``` +// TreeMap.java + +Iterator descendingKeyIterator() { + return new DescendingKeyIterator(getLastEntry()); // 获得的是尾部元素 +} +``` + +- 创建的是 DescendingKeyIterator 迭代器。在 [「14.3 DescendingKeyIterator」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 详细解析。 + +不过上述两个方法,都不是 `public` 方法,只提供给 TreeMap 内部使用。 + +## 14.1 PrivateEntryIterator + +PrivateEntryIterator ,实现 Iterator 接口,提供了 TreeMap 的通用实现 Iterator 的抽象类。代码如下: + +``` +// TreeMap.java + +abstract class PrivateEntryIterator implements Iterator { + + /** + * 下一个节点 + */ + Entry next; + /** + * 最后返回的节点 + */ + Entry lastReturned; + /** + * 当前的修改次数 + */ + int expectedModCount; + + PrivateEntryIterator(Entry first) { + expectedModCount = modCount; + lastReturned = null; + next = first; + } + + public final boolean hasNext() { + return next != null; + } + + final Entry nextEntry() { // 获得下一个 Entry 节点 + // 记录当前节点 + Entry e = next; + // 如果没有下一个,抛出 NoSuchElementException 异常 + if (e == null) + throw new NoSuchElementException(); + // 如果发生了修改,抛出 ConcurrentModificationException 异常 + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + // 获得 e 的后继节点,赋值给 next + next = successor(e); + // 记录最后返回的节点 + lastReturned = e; + // 返回当前节点 + return e; + } + + final Entry prevEntry() { // 获得前一个 Entry 节点 + // 记录当前节点 + Entry e = next; + // 如果没有下一个,抛出 NoSuchElementException 异常 + if (e == null) + throw new NoSuchElementException(); + // 如果发生了修改,抛出 ConcurrentModificationException 异常 + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + // 获得 e 的前继节点,赋值给 next + next = predecessor(e); + // 记录最后返回的节点 + lastReturned = e; + // 返回当前节点 + return e; + } + + public void remove() { // 删除节点 + // 如果当前返回的节点不存在,则抛出 IllegalStateException 异常 + if (lastReturned == null) + throw new IllegalStateException(); + // 如果发生了修改,抛出 ConcurrentModificationException 异常 + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + // deleted entries are replaced by their successors + // 在 lastReturned 左右节点都存在的时候,实际在 deleteEntry 方法中,是将后继节点替换到 lastReturned 中 + // 因此,next 需要指向 lastReturned + if (lastReturned.left != null && lastReturned.right != null) + next = lastReturned; + // 删除节点 + deleteEntry(lastReturned); + // 记录新的修改次数 + expectedModCount = modCount; + // 置空 lastReturned + lastReturned = null; + } + +} +``` + +- 整体代码比较简单,胖友自己看看艿艿写的注释噢。 + +在上述代码中,我们会看到 `#predecessor(Entry t)` 静态方法,我们来看看。获得 `t` 节点的前继节点,代码如下: + +``` +// TreeMap.java + +static Entry predecessor(Entry t) { + // 如果 t 为空,则返回 null + if (t == null) + return null; + // 如果 t 的左子树非空,则取左子树的最大值 + else if (t.left != null) { + Entry p = t.left; + while (p.right != null) + p = p.right; + return p; + // 如果 t 的左子树为空 + } else { + // 先获得 t 的父节点 + Entry p = t.parent; + // 不断向上遍历父节点,直到子节点 ch 不是父节点 p 的左子节点 + Entry ch = t; + while (p != null // 还有父节点 + && ch == p.left) { // 继续遍历的条件,必须是子节点 ch 是父节点 p 的左子节点 + ch = p; + p = p.parent; + } + return p; + } +} +``` + +- 和 `#successor(Entry t)` 方法,是一样的思路。所以,胖友跟着注释,自己再理解下。 + +## 14.2 KeyIterator + +KeyIterator ,继承 PrivateEntryIterator 抽象类,key 的**正序**迭代器。代码如下: + +``` +// TreeMap.java + +final class KeyIterator extends PrivateEntryIterator { + + KeyIterator(Entry first) { + super(first); + } + + // 实现 next 方法,实现正序 + public K next() { + return nextEntry().key; + } + +} +``` + +## 14.3 DescendingKeyIterator + +DescendingKeyIterator ,继承 PrivateEntryIterator 抽象类,key 的**倒序**迭代器。代码如下: + +``` +// TreeMap.java + +final class DescendingKeyIterator extends PrivateEntryIterator { + + DescendingKeyIterator(Entry first) { + super(first); + } + + // 实现 next 方法,实现倒序 + public K next() { + return prevEntry().key; + } + + // 重写 remove 方法,因为在 deleteEntry 方法中,在 lastReturned 左右节点都存在的时候,是将后继节点替换到 lastReturned 中。 + // 而这个逻辑,对于倒序遍历,没有影响。 + public void remove() { + // 如果当前返回的节点不存在,则抛出 IllegalStateException 异常 + if (lastReturned == null) + throw new IllegalStateException(); + // 如果发生了修改,抛出 ConcurrentModificationException 异常 + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + // 删除节点 + deleteEntry(lastReturned); + // 置空 lastReturned + lastReturned = null; + // 记录新的修改次数 + expectedModCount = modCount; + } + +} +``` + +## 14.4 EntryIterator + +EntryIterator ,继承 PrivateEntryIterator 抽象类,Entry 的**正序**迭代器。代码如下: + +``` +// TreeMap.java + +final class EntryIterator extends PrivateEntryIterator> { + + EntryIterator(Entry first) { + super(first); + } + + // 实现 next 方法,实现正序 + public Map.Entry next() { + return nextEntry(); + } + +} +``` + +## 14.5 ValueIterator + +ValueIterator ,继承 PrivateEntryIterator 抽象类,value 的**正序**迭代器。代码如下: + +``` +// TreeMap.java + +final class ValueIterator extends PrivateEntryIterator { + + ValueIterator(Entry first) { + super(first); + } + + // 实现 next 方法,实现正序 + public V next() { + return nextEntry().value; + } + +} +``` + +# 15. 转换成 Set/Collection + +> 艿艿:本小节,可以选择性看,或者不看。 + +## 15.1 keySet + +`#keySet()` 方法,获得**正序**的 key Set 。代码如下: + +``` +// TreeMap.java + +/** + * 正序的 KeySet 缓存对象 + */ +private transient KeySet navigableKeySet; + +public Set keySet() { + return navigableKeySet(); +} + +public NavigableSet navigableKeySet() { + KeySet nks = navigableKeySet; + return (nks != null) ? nks : (navigableKeySet = new KeySet<>(this)); +} +``` + +- 创建的 KeySet 类。它实现 NavigableSet 接口,继承了 [`java.util.AbstractSet`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractSet.java) 抽像类,是 TreeMap 的内部类。比较简单,就不哔哔了。 +- KeySet 使用的迭代器,就是 [「14.2 KeyIterator」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 。 + +## 15.2 descendingKeySet + +`#descendingKeySet()` 方法,获得**倒序**的 key Set 。代码如下: + +``` +// TreeMap.java + +/** + * 倒序的 NavigableMap 缓存对象 + */ +private transient NavigableMap descendingMap; + +public NavigableSet descendingKeySet() { + return descendingMap().navigableKeySet(); +} + +public NavigableMap descendingMap() { + NavigableMap km = descendingMap; + return (km != null) ? km : + (descendingMap = new DescendingSubMap<>(this, + true, null, true, + true, null, true)); +} +``` + +- 首先,调用 `#descendingMap()` 方法,返回**倒序**访问当前 TreeMap 的 DescendingSubMap 对象。 +- 然后,调用 `#navigableKeySet()` 方法,返回 DescendingSubMap 对象的**正序**的 key Set 。 +- 关于 DescendingSubMap 类,我们在 TODO 来详细解析。 + +## 15.3 values + +`#values()` 方法,获得 value 集合。代码如下: + +``` +// TreeMap.java + +public Collection values() { + Collection vs = values; + if (vs == null) { + vs = new Values(); + values = vs; // values 缓存,来自 AbstractMap 的属性 + } + return vs; +} +``` + +- 创建的 Values 类。它继承了 [`java.util.AbstractCollection`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractCollection.java) 抽像类,是 TreeMap 的内部类。比较简单,就不哔哔了。 +- Values 使用的迭代器,就是 [「14.4 ValueIterator」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 。 + +## 15.4 entrySet + +`#entrySet()` 方法,获得 Entry 集合。代码如下: + +``` +// TreeMap.java + +/** + * Entry 缓存集合 + */ +private transient EntrySet entrySet; + +public Set> entrySet() { + EntrySet es = entrySet; + return (es != null) ? es : (entrySet = new EntrySet()); +} +``` + +- 创建的 EntrySet 类。它继承了 [`java.util.AbstractSet`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractSet.java) 抽像类,是 TreeMap 的内部类。比较简单,就不哔哔了。 +- EntrySet 使用的迭代器,就是 [「14.3 EntryIterator」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 。 + +# 16. 查找范围的元素 + +> 艿艿:本小节,可以选择性看,或者不看。 +> +> 这部分,内容有点长。 + +在 SortedMap 接口中,定义了按照 key 查找范围,返回子 SortedMap 结果的方法: + +- `#subMap(K fromKey, K toKey)` +- `#headMap(K toKey)` +- `#tailMap(K fromKey)` + +在 NavigableMap 中,定义了按照 key 查找范围,返回子 NavigableMap 结果的方法: + +- `#subMap(K fromKey, K toKey)` +- `#subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive)` +- `#headMap(K toKey)` +- `#headMap(K toKey, boolean inclusive)` +- `#tailMap(K fromKey)` +- `#tailMap(K fromKey, boolean inclusive)` + +TreeMap 对上述接口,实现如下方法: + +``` +// TreeMap.java + +// subMap 组 +public SortedMap subMap(K fromKey, K toKey) { + return subMap(fromKey, true, toKey, false); +} +public NavigableMap subMap(K fromKey, boolean fromInclusive, + K toKey, boolean toInclusive) { + return new AscendingSubMap<>(this, + false, fromKey, fromInclusive, + false, toKey, toInclusive); +} + +// headMap 组 +public SortedMap headMap(K toKey) { + return headMap(toKey, false); +} +public NavigableMap headMap(K toKey, boolean inclusive) { + return new AscendingSubMap<>(this, + true, null, true, + false, toKey, inclusive); +} + +// tailMap 组 +public SortedMap tailMap(K fromKey) { + return tailMap(fromKey, true); +} +public NavigableMap tailMap(K fromKey, boolean inclusive) { + return new AscendingSubMap<>(this, + false, fromKey, inclusive, + true, null, true); +} +``` + +- 返回的都是 AscendingSubMap 对象。所以,我们在 [「XX. AscendingSubMap」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 来看。 + +## 16.1 NavigableSubMap + +NavigableSubMap ,实现 NavigableMap、Serializable 接口,继承 AbstractMap 抽象类,**子 NavigableMap 的抽象类**。 + +后续,我们会看到 NavigableSubMap 的两个子类: + +- AscendingSubMap ,**正序**的 子 NavigableMap 的实现类。 +- DescendingSubMap ,**倒序**的 子 NavigableMap 的实现类。 + +### 16.1.1 构造方法 + +NavigableSubMap 仅有一个构造方法,代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +/** + * The backing map. + */ +final TreeMap m; + +/** + * lo - 开始位置 + * hi - 结束位置 + */ +final K lo, hi; +/** + * fromStart - 是否从 TreeMap 开头开始。如果是的话,{@link #lo} 可以不传 + * toEnd - 是否从 TreeMap 结尾结束。如果是的话,{@link #hi} 可以不传 + */ +final boolean fromStart, toEnd; +/** + * loInclusive - 是否包含 key 为 {@link #lo} 的元素 + * hiInclusive - 是否包含 key 为 {@link #hi} 的元素 + */ +final boolean loInclusive, hiInclusive; + +NavigableSubMap(TreeMap m, + boolean fromStart, K lo, boolean loInclusive, + boolean toEnd, K hi, boolean hiInclusive) { + // 如果既不从开头开始,又不从结尾结束,那么就要校验 lo 小于 hi ,否则抛出 IllegalArgumentException 异常 + if (!fromStart && !toEnd) { + if (m.compare(lo, hi) > 0) + throw new IllegalArgumentException("fromKey > toKey"); + } else { + // 如果不从开头开始,则进行 lo 的类型校验 + if (!fromStart) // type check + m.compare(lo, lo); + // 如果不从结尾结束,则进行 hi 的类型校验 + if (!toEnd) + m.compare(hi, hi); + } + + // 赋值属性 + this.m = m; + this.fromStart = fromStart; + this.lo = lo; + this.loInclusive = loInclusive; + this.toEnd = toEnd; + this.hi = hi; + this.hiInclusive = hiInclusive; +} +``` + +- 每个属性,胖友自己看代码上的注释。 + +### 16.1.2 范围校验 + +因为 NavigableSubMap 是 TreeMap 的**子** NavigableMap ,所以其所有的操作,不能超过其**子范围**,既我们在创建 NavigableSubMap 时,锁设置的开始和结束的 key 位置。 + +`#inRange(Object key)` 方法,校验传入的 `key` 是否在子范围中。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +final boolean inRange(Object key) { + return !tooLow(key) + && !tooHigh(key); +} +``` + +- 调用 `#tooLow(Object key)` 方法,判断 `key` 是否小于 NavigableSubMap 的开始位置的 key 。代码如下: + + ``` + // TreeMap.java#NavigableSubMap.java + + final boolean tooLow(Object key) { + if (!fromStart) { + // 比较 key + int c = m.compare(key, lo); + if (c < 0 // 如果小于,则肯定过小 + || (c == 0 && !loInclusive)) // 如果相等,则进一步判断是否 !loInclusive ,不包含 lo 的情况 + return true; + } + return false; + } + ``` + +- 调用 `#tooHigh(Object key)` 方法,判断 `key` 是否大于 NavigableSubMap 的结束位置的 key 。代码如下: + + ``` + // TreeMap.java#NavigableSubMap.java + + final boolean tooHigh(Object key) { + if (!toEnd) { + // 比较 key + int c = m.compare(key, hi); + if (c > 0 // 如果大于,则肯定过大 + || (c == 0 && !hiInclusive)) // 如果相等,则进一步判断是否 !hiInclusive ,不包含 high 的情况 + return true; + } + return false; + } + ``` + +- 通过这样两个判断,不过大,且不过小,那么就在范围之内了。 + +`#inClosedRange(Object key)` 方法,判断是否在闭合的范围内。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +final boolean inClosedRange(Object key) { + return (fromStart || m.compare(key, lo) >= 0) + && (toEnd || m.compare(hi, key) >= 0); +} +``` + +- 也就是说,不包含包含边界的情况。 + +`#inRange(Object key, boolean inclusive)` 方法,根据传入的 `inclusive` 参数,调用上述的两种范围判断的方法。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +final boolean inRange(Object key, boolean inclusive) { + return inclusive ? inRange(key) : inClosedRange(key); +} +``` + +### 16.1.3 添加单个元素 + +`#put(K key, V value)` 方法,添加单个元素。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +public final V put(K key, V value) { + // 校验 key 的范围,如果不在,则抛出 IllegalArgumentException 异常 + if (!inRange(key)) + throw new IllegalArgumentException("key out of range"); + // 执行添加单个元素 + return m.put(key, value); +} +``` + +### 16.1.4 获得单个元素 + +`#get(Object key)` 方法,获得 `key` 对应的 value 值。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +public final V get(Object key) { + return !inRange(key) // 校验 key 的范围 + ? null : // 如果不在,则返回 null + m.get(key); // 执行获得单个元素 +} +``` + +`#containsKey(Object key)` 方法,判断是否存在指定 key 。代码如下: + +``` +// TreeMap.java + +public final boolean containsKey(Object key) { + return inRange(key) + && m.containsKey(key); +} +``` + +- 基于 `#getEntry(key)` 方法来实现。 + +### 16.1.5 删除单个元素 + +`#remove(Object key)` 方法,移除 `key` 对应的 Entry 节点。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +public final V remove(Object key) { + return !inRange(key) // 校验 key 的范围 + ? null : // 如果不在,则返回 null + m.remove(key); // 执行移除单个元素 +} +``` + +### 16.1.6 查找接近的元素 + +``` +// TreeMap.java#NavigableSubMap.java + +public final Map.Entry ceilingEntry(K key) { + return exportEntry(subCeiling(key)); +} +public final K ceilingKey(K key) { + return keyOrNull(subCeiling(key)); +} + +public final Map.Entry higherEntry(K key) { + return exportEntry(subHigher(key)); +} +public final K higherKey(K key) { + return keyOrNull(subHigher(key)); +} + +public final Map.Entry floorEntry(K key) { + return exportEntry(subFloor(key)); +} +public final K floorKey(K key) { + return keyOrNull(subFloor(key)); +} + +public final Map.Entry lowerEntry(K key) { + return exportEntry(subLower(key)); +} +public final K lowerKey(K key) { + return keyOrNull(subLower(key)); +} +``` + +因为子类的**排序规则不同**,所以 NavigableSubMap 定义了如下抽象方法,交给子类实现。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +abstract TreeMap.Entry subLowest(); +abstract TreeMap.Entry subHighest(); +abstract TreeMap.Entry subCeiling(K key); +abstract TreeMap.Entry subHigher(K key); +abstract TreeMap.Entry subFloor(K key); +abstract TreeMap.Entry subLower(K key); +``` + +当然,NavigableSubMap 为了子类实现更方便,提供了如下方法: + +``` +// TreeMap.java#NavigableSubMap.java + +final TreeMap.Entry absLowest() { // 获得 NavigableSubMap 开始位置的 Entry 节点 + TreeMap.Entry e = + (fromStart ? m.getFirstEntry() : // 如果从 TreeMap 开始,则获得 TreeMap 的首个 Entry 节点 + (loInclusive ? m.getCeilingEntry(lo) : // 如果 key 从 lo 开始(包含),则获得 TreeMap 从 lo 开始(>=)最接近的 Entry 节点 + m.getHigherEntry(lo))); // 如果 key 从 lo 开始(不包含),则获得 TreeMap 从 lo 开始(>)最接近的 Entry 节点 + return (e == null || tooHigh(e.key)) /** 超过 key 过大 **/ ? null : e; +} + +final TreeMap.Entry absHighest() { // 获得 NavigableSubMap 结束位置的 Entry 节点 + TreeMap.Entry e = + (toEnd ? m.getLastEntry() : // 如果从 TreeMap 开始,则获得 TreeMap 的尾部 Entry 节点 + (hiInclusive ? m.getFloorEntry(hi) : // 如果 key 从 hi 开始(包含),则获得 TreeMap 从 hi 开始(<=)最接近的 Entry 节点 + m.getLowerEntry(hi))); // 如果 key 从 lo 开始(不包含),则获得 TreeMap 从 lo 开始(<)最接近的 Entry 节点 + return (e == null || tooLow(e.key)) /** 超过 key 过小 **/ ? null : e; +} + +final TreeMap.Entry absCeiling(K key) { // 获得 NavigableSubMap >= key 最接近的 Entry 节点 + // 如果 key 过小,则只能通过 `#absLowest()` 方法,获得 NavigableSubMap 开始位置的 Entry 节点 + if (tooLow(key)) + return absLowest(); + // 获得 NavigableSubMap >= key 最接近的 Entry 节点 + TreeMap.Entry e = m.getCeilingEntry(key); + return (e == null || tooHigh(e.key)) /** 超过 key 过大 **/ ? null : e; +} + +final TreeMap.Entry absHigher(K key) { // 获得 NavigableSubMap > key 最接近的 Entry 节点 + // 如果 key 过小,则只能通过 `#absLowest()` 方法,获得 NavigableSubMap 开始位置的 Entry 节点 + if (tooLow(key)) + return absLowest(); + // 获得 NavigableSubMap > key 最接近的 Entry 节点 + TreeMap.Entry e = m.getHigherEntry(key); + return (e == null || tooHigh(e.key)) /** 超过 key 过大 **/ ? null : e; +} + +final TreeMap.Entry absFloor(K key) { // 获得 NavigableSubMap <= key 最接近的 Entry 节点 + // 如果 key 过大,则只能通过 `#absHighest()` 方法,获得 NavigableSubMap 结束位置的 Entry 节点 + if (tooHigh(key)) + return absHighest(); + // 获得 NavigableSubMap <= key 最接近的 Entry 节点 + TreeMap.Entry e = m.getFloorEntry(key); + return (e == null || tooLow(e.key)) /** 超过 key 过小 **/ ? null : e; +} + +final TreeMap.Entry absLower(K key) { // 获得 NavigableSubMap < key 最接近的 Entry 节点 + // 如果 key 过大,则只能通过 `#absHighest()` 方法,获得 NavigableSubMap 结束位置的 Entry 节点 + if (tooHigh(key)) + return absHighest(); + // 获得 NavigableSubMap < key 最接近的 Entry 节点 + TreeMap.Entry e = m.getLowerEntry(key); + return (e == null || tooLow(e.key)) /** 超过 key 过小 **/ ? null : e; +} + +/** Returns the absolute high fence for ascending traversal */ +final TreeMap.Entry absHighFence() { // 获得 TreeMap 最大 key 的 Entry 节点,用于升序遍历的时候。注意,是 TreeMap 。 + // toEnd 为真时,意味着无限大,所以返回 null + return (toEnd ? null : (hiInclusive ? + m.getHigherEntry(hi) : // 获得 TreeMap > hi 最接近的 Entry 节点。 + m.getCeilingEntry(hi))); // 获得 TreeMap => hi 最接近的 Entry 节点。 +} + +/** Return the absolute low fence for descending traversal */ +final TreeMap.Entry absLowFence() { // 获得 TreeMap 最小 key 的 Entry 节点,用于降序遍历的时候。注意,是 TreeMap 。 + return (fromStart ? null : (loInclusive ? + m.getLowerEntry(lo) : // 获得 TreeMap < lo 最接近的 Entry 节点。 + m.getFloorEntry(lo))); // 获得 TreeMap <= lo 最接近的 Entry 节点。 +} +``` + +- 方法有点点小多,不过基本是雷同的。耐心如我~ + +### 16.1.7 获得首尾的元素 + +`#firstEntry()` 方法,获得首个 Entry 节点。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +public final Map.Entry firstEntry() { + return exportEntry(subLowest()); +} + +public final K firstKey() { + return key(subLowest()); +} + +public final Map.Entry pollFirstEntry() { + // 获得 NavigableSubMap 的首个 Entry 节点 + TreeMap.Entry e = subLowest(); + Map.Entry result = exportEntry(e); + // 如果存在,则进行删除。 + if (e != null) + m.deleteEntry(e); + return result; +} +``` + +`#lastEntry()` 方法,获得尾部 Entry 节点。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +public final Map.Entry lastEntry() { + return exportEntry(subHighest()); +} + +public final K lastKey() { + return key(subHighest()); +} + +public final Map.Entry pollLastEntry() { + // 获得 NavigableSubMap 的尾部 Entry 节点 + TreeMap.Entry e = subHighest(); + Map.Entry result = exportEntry(e); + // 如果存在,则进行删除。 + if (e != null) + m.deleteEntry(e); + return result; +} +``` + +### 16.1.8 清空 + +直接使用继承自 AbstractMap 的 `#clear()` 方法,仅清空自己范围内的数据。代码如下: + +``` +// AbstractMap.java + +public void clear() { + entrySet().clear(); +} +``` + +- 而 `#entrySet()` 方法,NavigableSubMap 的子类在实现时,会重写该方法。这样,能够保证仅清空自己范围内的数据。 + +### 16.1.9 获得迭代器 + +SubMapIterator ,实现 Iterator 接口,提供了 NavigableSubMap 的通用实现 Iterator 的抽象类。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +abstract class SubMapIterator implements Iterator { + + /** + * 最后返回的节点 + */ + TreeMap.Entry lastReturned; + /** + * 下一个节点 + */ + TreeMap.Entry next; + /** + * 遍历的上限 key 。 + * + * 如果遍历到该 key ,说明已经超过范围了 + */ + final Object fenceKey; + /** + * 当前的修改次数 + */ + int expectedModCount; + + SubMapIterator(TreeMap.Entry first, + TreeMap.Entry fence) { + expectedModCount = m.modCount; + lastReturned = null; + next = first; + fenceKey = fence == null ? UNBOUNDED /** 无界限 **/ : fence.key; + } + + public final boolean hasNext() { // 是否还有下一个节点 + return next != null && next.key != fenceKey; + } + + final TreeMap.Entry nextEntry() { // 获得下一个 Entry 节点 + // 记录当前节点 + TreeMap.Entry e = next; + // 如果没有下一个,抛出 NoSuchElementException 异常 + if (e == null || e.key == fenceKey) + throw new NoSuchElementException(); + // 如果发生了修改,抛出 ConcurrentModificationException 异常 + if (m.modCount != expectedModCount) + throw new ConcurrentModificationException(); + // 获得 e 的后继节点,赋值给 next + next = successor(e); + // 记录最后返回的节点 + lastReturned = e; + // 返回当前节点 + return e; + } + + final TreeMap.Entry prevEntry() { // 获得前一个 Entry 节点 + // 记录当前节点 + TreeMap.Entry e = next; + // 如果没有下一个,抛出 NoSuchElementException 异常 + if (e == null || e.key == fenceKey) + throw new NoSuchElementException(); + // 如果发生了修改,抛出 ConcurrentModificationException 异常 + if (m.modCount != expectedModCount) + throw new ConcurrentModificationException(); + // 获得 e 的前继节点,赋值给 next + next = predecessor(e); + // 记录最后返回的节点 + lastReturned = e; + // 返回当前节点 + return e; + } + + final void removeAscending() { // 删除节点(顺序遍历的情况下) + // 如果当前返回的节点不存在,则抛出 IllegalStateException 异常 + if (lastReturned == null) + throw new IllegalStateException(); + // 如果发生了修改,抛出 ConcurrentModificationException 异常 + if (m.modCount != expectedModCount) + throw new ConcurrentModificationException(); + // deleted entries are replaced by their successors + // 在 lastReturned 左右节点都存在的时候,实际在 deleteEntry 方法中,是将后继节点替换到 lastReturned 中 + // 因此,next 需要指向 lastReturned + if (lastReturned.left != null && lastReturned.right != null) + next = lastReturned; + // 删除节点 + m.deleteEntry(lastReturned); + // 置空 lastReturned + lastReturned = null; + // 记录新的修改次数 + expectedModCount = m.modCount; + } + + final void removeDescending() { // 删除节点倒序遍历的情况下) + // 如果当前返回的节点不存在,则抛出 IllegalStateException 异常 + if (lastReturned == null) + throw new IllegalStateException(); + // 如果发生了修改,抛出 ConcurrentModificationException 异常 + if (m.modCount != expectedModCount) + throw new ConcurrentModificationException(); + // 删除节点 + m.deleteEntry(lastReturned); + // 置空 lastReturned + lastReturned = null; + // 记录新的修改次数 + expectedModCount = m.modCount; + } + +} +``` + +- 整体代码比较简单,胖友自己看看艿艿写的注释噢。 + +#### 16.1.9.1 SubMapKeyIterator + +SubMapKeyIterator ,继承 SubMapIterator 抽象类,key 的**正序**迭代器。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +final class SubMapKeyIterator extends SubMapIterator + implements Spliterator { + + SubMapKeyIterator(TreeMap.Entry first, + TreeMap.Entry fence) { + super(first, fence); + } + + // 实现 next 方法,实现正序 + public K next() { + return nextEntry().key; + } + + // 实现 remove 方法,实现正序的移除方法 + public void remove() { + removeAscending(); + } + + public Spliterator trySplit() { + return null; + } + public void forEachRemaining(Consumer action) { + while (hasNext()) + action.accept(next()); + } + public boolean tryAdvance(Consumer action) { + if (hasNext()) { + action.accept(next()); + return true; + } + return false; + } + public long estimateSize() { + return Long.MAX_VALUE; + } + public int characteristics() { + return Spliterator.DISTINCT | Spliterator.ORDERED | + Spliterator.SORTED; + } + public final Comparator getComparator() { + return NavigableSubMap.this.comparator(); + } +} +``` + +#### 16.1.9.2 DescendingSubMapKeyIterator + +DescendingSubMapKeyIterator ,继承 SubMapIterator 抽象类,key 的**倒序**迭代器。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +final class DescendingSubMapKeyIterator extends SubMapIterator + implements Spliterator { + + DescendingSubMapKeyIterator(TreeMap.Entry last, + TreeMap.Entry fence) { + super(last, fence); + } + + // 实现 next 方法,实现倒序 + public K next() { + return prevEntry().key; + } + + // 实现 remove 方法,实现倒序的移除方法 + public void remove() { + removeDescending(); + } + + public Spliterator trySplit() { + return null; + } + public void forEachRemaining(Consumer action) { + while (hasNext()) + action.accept(next()); + } + public boolean tryAdvance(Consumer action) { + if (hasNext()) { + action.accept(next()); + return true; + } + return false; + } + public long estimateSize() { + return Long.MAX_VALUE; + } + public int characteristics() { + return Spliterator.DISTINCT | Spliterator.ORDERED; + } +} +``` + +#### 16.1.9.3 SubMapEntryIterator + +SubMapEntryIterator ,继承 SubMapIterator 抽象类,Entry 的**正序**迭代器。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +final class SubMapEntryIterator extends SubMapIterator> { + + SubMapEntryIterator(TreeMap.Entry first, + TreeMap.Entry fence) { + super(first, fence); + } + + // 实现 next 方法,实现正序 + public Map.Entry next() { + return nextEntry(); + } + + // 实现 remove 方法,实现正序的移除方法 + public void remove() { + removeAscending(); + } + +} +``` + +#### 16.1.9.4 DescendingSubMapEntryIterator + +DescendingSubMapEntryIterator ,继承 SubMapIterator 抽象类,Entry 的**倒序**迭代器。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +final class DescendingSubMapEntryIterator extends SubMapIterator> { + + DescendingSubMapEntryIterator(TreeMap.Entry last, + TreeMap.Entry fence) { + super(last, fence); + } + + // 实现 next 方法,实现倒序 + public Map.Entry next() { + return prevEntry(); + } + + // 实现 remove 方法,实现倒序的移除方法 + public void remove() { + removeDescending(); + } +} +``` + +### 16.1.10 转换成 Set/Collection + +#### 16.1.10.1 keySet + +`#keySet()` 方法,获得**正序**的 key Set 。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +/** + * 正序的 KeySet 缓存对象 + */ +transient KeySet navigableKeySetView; + +public final Set keySet() { + return navigableKeySet(); +} + +public final NavigableSet navigableKeySet() { + KeySet nksv = navigableKeySetView; + return (nksv != null) ? nksv : + (navigableKeySetView = new TreeMap.KeySet<>(this)); +} +``` + +- KeySet 使用的迭代器,就是 [「16.1.9.1 SubMapKeyIterator」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 。 + +#### 16.1.10.2 navigableKeySet + +`#navigableKeySet()` 方法,获得**倒序**的 key Set 。代码如下: + +``` +// TreeMap.java#NavigableSubMap.java + +/** + * 倒序的 KeySet 缓存对象 + */ +transient NavigableMap descendingMapView; + +public NavigableSet descendingKeySet() { + return descendingMap().navigableKeySet(); +} +``` + +- 其中,`#descendingMap()` 方法,子类自己实现。 + +#### 16.1.10.3 values + +直接使用继承自 AbstractMap 的 `#values()` 方法,代码如下: + +``` +// AbstractMap.java + +transient Collection values; + +public Collection values() { + Collection vals = values; + if (vals == null) { + vals = new AbstractCollection() { + public Iterator iterator() { + return new Iterator() { + private Iterator> i = entrySet().iterator(); + + public boolean hasNext() { + return i.hasNext(); + } + + public V next() { + return i.next().getValue(); + } + + public void remove() { + i.remove(); + } + }; + } + + public int size() { + return AbstractMap.this.size(); + } + + public boolean isEmpty() { + return AbstractMap.this.isEmpty(); + } + + public void clear() { + AbstractMap.this.clear(); + } + + public boolean contains(Object v) { + return AbstractMap.this.containsValue(v); + } + }; + values = vals; + } + return vals; +} +``` + +- 也是基于 `#entrySet()` 方法,来实现。 + +#### 16.1.10.4 entrySet + +NavigableSubMap 未提供 `#entrySet()` 方法的实现,不过提供了 EntrySetView 抽象类,它实现了 [`java.util.AbstractSet`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractSet.java) 抽像类,是 NavigableSubMap 的内部类。比较简单,就不哔哔了。 + +### 16.1.11 查找范围的元素 + +``` +// TreeMap.java#NavigableSubMap.java + +public final SortedMap subMap(K fromKey, K toKey) { + return subMap(fromKey, true, toKey, false); +} + +public final SortedMap headMap(K toKey) { + return headMap(toKey, false); +} + +public final SortedMap tailMap(K fromKey) { + return tailMap(fromKey, true); +} +``` + +- 每个方法内部调用的方法,都是子类来实现。 + +## 16.2 AscendingSubMap + +AscendingSubMap ,继承 NavigableSubMap 抽象类,**正序**的 子 NavigableMap 的实现类。 + +### 16.2.1 查找接近的元素 + +``` +// TreeMap.java#AscendingSubMap.java + +TreeMap.Entry subLowest() { return absLowest(); } +TreeMap.Entry subHighest() { return absHighest(); } +TreeMap.Entry subCeiling(K key) { return absCeiling(key); } +TreeMap.Entry subHigher(K key) { return absHigher(key); } +TreeMap.Entry subFloor(K key) { return absFloor(key); } +TreeMap.Entry subLower(K key) { return absLower(key); } +``` + +### 16.2.2 获得迭代器 + +``` +// TreeMap.java#AscendingSubMap.java + +Iterator keyIterator() { + return new SubMapKeyIterator(absLowest(), absHighFence()); +} + +Iterator descendingKeyIterator() { + return new DescendingSubMapKeyIterator(absHighest(), absLowFence()); +} +``` + +### 16.2.3 转换成 Set/Collection + +#### 16.2.3.1 descendingMap + +`#descendingMap()` 方法,获得**倒序** descendingMap。代码如下: + +``` +// TreeMap.java#AscendingSubMap.java + +public NavigableMap descendingMap() { + NavigableMap mv = descendingMapView; + return (mv != null) ? mv : + (descendingMapView = + new DescendingSubMap<>(m, + fromStart, lo, loInclusive, + toEnd, hi, hiInclusive)); +} +``` + +- 该方法,会被 [「16.1.9.2 DescendingSubMapKeyIterator」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 调用。 + +#### 16.2.3.2 entrySet + +`#entrySet()` 方法,获得 Entry 集合。代码如下: + +``` +// TreeMap.java#AscendingSubMap.java + +public Set> entrySet() { + EntrySetView es = entrySetView; + return (es != null) ? es : (entrySetView = new AscendingEntrySetView()); +} + +final class AscendingEntrySetView extends EntrySetView { + public Iterator> iterator() { + return new SubMapEntryIterator(absLowest(), absHighFence()); + } +} +``` + +- AscendingEntrySetView 使用的迭代器,就是 [「16.1.9.3 SubMapEntryIterator」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 。 + +### 16.2.4 查找范围的元素 + +``` +// TreeMap.java#AscendingSubMap.java + +public NavigableMap subMap(K fromKey, boolean fromInclusive, + K toKey, boolean toInclusive) { + // 如果不在范围,抛出 IllegalArgumentException 异常 + if (!inRange(fromKey, fromInclusive)) + throw new IllegalArgumentException("fromKey out of range"); + // 如果不在范围,抛出 IllegalArgumentException 异常 + if (!inRange(toKey, toInclusive)) + throw new IllegalArgumentException("toKey out of range"); + // 创建 AscendingSubMap 对象 + return new AscendingSubMap<>(m, + false, fromKey, fromInclusive, + false, toKey, toInclusive); +} + +public NavigableMap headMap(K toKey, boolean inclusive) { + // 如果不在范围,抛出 IllegalArgumentException 异常 + if (!inRange(toKey, inclusive)) + throw new IllegalArgumentException("toKey out of range"); + // 创建 AscendingSubMap 对象 + return new AscendingSubMap<>(m, + fromStart, lo, loInclusive, + false, toKey, inclusive); +} + +public NavigableMap tailMap(K fromKey, boolean inclusive) { + // 如果不在范围,抛出 IllegalArgumentException 异常 + if (!inRange(fromKey, inclusive)) + throw new IllegalArgumentException("fromKey out of range"); + // 创建 AscendingSubMap 对象 + return new AscendingSubMap<>(m, + false, fromKey, inclusive, + toEnd, hi, hiInclusive); +} +``` + +### 16.2.5 获得排序器 + +`#comparator()` 方法,代码如下: + +``` +// TreeMap.java#AscendingSubMap.java + +// 排序器,使用传入的 TreeMap +public Comparator comparator() { + return m.comparator(); +} +``` + +## 16.3 DescendingSubMap + +DescendingSubMap ,继承 NavigableSubMap 抽象类,**倒序**的 子 NavigableMap 的实现类。 + +### 16.3.1 查找接近的元素 + +``` +// TreeMap.java#DescendingSubMap.java + +TreeMap.Entry subLowest() { return absHighest(); } +TreeMap.Entry subHighest() { return absLowest(); } +TreeMap.Entry subCeiling(K key) { return absFloor(key); } +TreeMap.Entry subHigher(K key) { return absLower(key); } +TreeMap.Entry subFloor(K key) { return absCeiling(key); } +TreeMap.Entry subLower(K key) { return absHigher(key); } +``` + +- 恰好是反过来。 + +### 16.3.2 获得迭代器 + +``` +// TreeMap.java#DescendingSubMap.java + +Iterator keyIterator() { + return new DescendingSubMapKeyIterator(absHighest(), absLowFence()); +} + +Iterator descendingKeyIterator() { + return new SubMapKeyIterator(absLowest(), absHighFence()); +} +``` + +- 恰好是反过来。 + +### 16.3.3 转换成 Set/Collection + +#### 16.3.3.1 descendingMap + +`#descendingMap()` 方法,获得**倒序** descendingMap。代码如下: + +> 负负得正,其实返回的是正序的。 + +``` +// TreeMap.java#DescendingSubMap.java + +public NavigableMap descendingMap() { + NavigableMap mv = descendingMapView; + return (mv != null) ? mv : + (descendingMapView = + new AscendingSubMap<>(m, + fromStart, lo, loInclusive, + toEnd, hi, hiInclusive)); +} +``` + +- 该方法,会被 [「16.1.9.2 DescendingSubMapKeyIterator」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 调用。 + +#### 16.3.3.2 entrySet + +`#entrySet()` 方法,获得 Entry 集合。代码如下: + +``` +// TreeMap.java#DescendingSubMap.java + +public Set> entrySet() { + EntrySetView es = entrySetView; + return (es != null) ? es : (entrySetView = new DescendingEntrySetView()); +} + +final class DescendingEntrySetView extends EntrySetView { + public Iterator> iterator() { + return new DescendingSubMapEntryIterator(absHighest(), absLowFence()); + } +} +``` + +- AscendingEntrySetView 使用的迭代器,就是 [「16.1.9.4 DescendingSubMapEntryIterator」](https://svip.iocoder.cn/JDK/Collection-TreeMap/#) 。 + +### 16.3.4 查找范围的元素 + +``` +// TreeMap.java#DescendingSubMap.java + +public NavigableMap subMap(K fromKey, boolean fromInclusive, + K toKey, boolean toInclusive) { + // 如果不在范围,抛出 IllegalArgumentException 异常 + if (!inRange(fromKey, fromInclusive)) + throw new IllegalArgumentException("fromKey out of range"); + // 如果不在范围,抛出 IllegalArgumentException 异常 + if (!inRange(toKey, toInclusive)) + throw new IllegalArgumentException("toKey out of range"); + // 创建 DescendingSubMap 对象 + return new DescendingSubMap<>(m, + false, toKey, toInclusive, + false, fromKey, fromInclusive); +} + +public NavigableMap headMap(K toKey, boolean inclusive) { + // 如果不在范围,抛出 IllegalArgumentException 异常 + if (!inRange(toKey, inclusive)) + throw new IllegalArgumentException("toKey out of range"); + // 创建 DescendingSubMap 对象 + return new DescendingSubMap<>(m, + false, toKey, inclusive, + toEnd, hi, hiInclusive); +} + +public NavigableMap tailMap(K fromKey, boolean inclusive) { + // 如果不在范围,抛出 IllegalArgumentException 异常 + if (!inRange(fromKey, inclusive)) + throw new IllegalArgumentException("fromKey out of range"); + // 创建 DescendingSubMap 对象 + return new DescendingSubMap<>(m, + fromStart, lo, loInclusive, + false, fromKey, inclusive); +} +``` + +### 16.3.5 获得排序器 + +`#comparator()` 方法,代码如下: + +``` +// TreeMap.java#DescendingSubMap.java + +/** + * 倒序排序器 + */ +private final Comparator reverseComparator = + Collections.reverseOrder(m.comparator); + +public Comparator comparator() { + return reverseComparator; +} +``` + +# 666. 彩蛋 + +抛开红黑树的自平衡的逻辑来说,TreeMap 的实现代码,实际是略简单于 HashMap 的。当然,因为 TreeMap 提供的方法较多,所以导致本文会比 HashMap 写的长一些。 + +下面,我们来对 TreeMap 做一个简单的小结: + +- TreeMap 按照 key 的**顺序**的 Map 实现类,底层采用**红黑树**来实现存储。 + +- TreeMap 因为采用树结构,所以无需初始考虑像 HashMap 考虑**容量**问题,也不存在扩容问题。 + +- TreeMap 的 **key** 不允许为空( `null` ),可能是因为红黑树是一颗二叉查找树,需要对 key 进行排序。 + + > 看了下 [《为什么 TreeMap 中不允许使用 null 键?》](https://codeday.me/bug/20190205/621458.html) 文章,也是这个观点。 + +- TreeMap 的查找、添加、删除 key-value 键值对的**平均**时间复杂度为 `O(logN)` 。原因是,TreeMap 采用红黑树,操作都需要经过二分查找,而二分查找的时间复杂度是 `O(logN)` 。 + +- 相比 HashMap 来说,TreeMap 不仅仅支持指定 key 的查找,也支持 key **范围**的查找。当然,这也得益于 TreeMap 数据结构能够提供的有序特性。 \ No newline at end of file diff --git a/jdk/09-JDK 源码解析-集合(七)TreeSet.assets/01.png b/jdk/09-JDK 源码解析-集合(七)TreeSet.assets/01.png new file mode 100644 index 0000000..2c05113 Binary files /dev/null and b/jdk/09-JDK 源码解析-集合(七)TreeSet.assets/01.png differ diff --git a/jdk/09-JDK 源码解析-集合(七)TreeSet.md b/jdk/09-JDK 源码解析-集合(七)TreeSet.md new file mode 100644 index 0000000..be84585 --- /dev/null +++ b/jdk/09-JDK 源码解析-集合(七)TreeSet.md @@ -0,0 +1,378 @@ +# 精尽 JDK 源码解析 —— 集合(七)TreeSet + +# 1. 概述 + +> 本文,和 [《精尽 JDK 源码解析 —— 集合(五)哈希集合 HashSet》](http://svip.iocoder.cn/JDK/Collection-HashSet/?self) 基本是一致的。 + +TreeSet ,基于 TreeSet 的 Set 实现类。在业务中,如果我们有排重+ **排序**的需求,一般会考虑使用 TreeSet 。不过,貌似很少会出现排重+ **排序**的双重需求。所以呢,TreeSet 反正艿艿是木有使用过。 + +# 2. 类图 + +TreeSet 实现的接口、继承的类,如下图所示:[![类图](09-JDK 源码解析-集合(七)TreeSet.assets/01.png)](http://static.iocoder.cn/images/JDK/2019_12_16/01.png)类图 + +- 实现 [`java.util.NavigableSet`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/NavigableSet.java) 接口,并继承 [`java.util.AbstractSet`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/AbstractSet.java) 抽像类。 +- 实现 [`java.io.Serializable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/io/Serializable.java) 接口。 +- 实现 [`java.lang.Cloneable`](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/lang/Cloneable.java) 接口。 + +对于 [NavigableSet](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/NavigableSet.java) 和 [SortedMap](https://github.com/YunaiV/openjdk/blob/master/src/java.base/share/classes/java/util/SortedMap.java) 接口,已经添加注释,胖友可以直接点击查看。 + +# 3. 属性 + +TreeSet 只有一个属性,那就是 `m` 。代码如下: + +``` +// TreeSet.java + +private transient NavigableMap m; +``` + +- `m` 的 key ,存储 HashSet 的每个 key 。 + +- `map` 的 value ,因为 TreeSet 没有 value 的需要,所以使用一个统一的 `PRESENT` 即可。代码如下: + + ``` + // TreeSet.java + + // Dummy value to associate with an Object in the backing Map + private static final Object PRESENT = new Object(); + ``` + +# 4. 构造方法 + +TreeSet 一共有 5 个构造方法,代码如下: + +``` +// TreeSet.java + +TreeSet(NavigableMap m) { + this.m = m; +} + +public TreeSet() { + this(new TreeMap<>()); +} + +public TreeSet(Comparator comparator) { + this(new TreeMap<>(comparator)); +} + +public TreeSet(Collection c) { + this(); + // 批量添加 + addAll(c); +} + +public TreeSet(SortedSet s) { + this(s.comparator()); + // 批量添加 + addAll(s); +} +``` + +- 在构造方法中,会创建 TreeMap 对象,赋予到 `m` 属性。 + +# 5. 添加单个元素 + +`#add(E e)` 方法,添加单个元素。代码如下: + +``` +// TreeSet.java + +public boolean add(E e) { + return m.put(e, PRESENT)==null; +} +``` + +- `m` 的 value 值,就是我们看到的 `PRESENT` 。 + +`#addAll(Collection c)` 方法,批量添加。代码如下: + +``` +// TreeSet.java + +public boolean addAll(Collection c) { + // Use linear-time version if applicable + // 情况一 + if (m.size()==0 && c.size() > 0 && + c instanceof SortedSet && + m instanceof TreeMap) { + SortedSet set = (SortedSet) c; + TreeMap map = (TreeMap) m; + if (Objects.equals(set.comparator(), map.comparator())) { + map.addAllForTreeSet(set, PRESENT); + return true; + } + } + // 情况二 + return super.addAll(c); +} +``` + +- 在实现上,和 TreeMap 的批量添加是一样的,对于情况一,会进行优化。 + +# 6. 移除单个元素 + +`#remove(Object o)` 方法,移除 `o` 对应的 value ,并返回是否成功。代码如下: + +``` +// TreeSet.java + +public boolean remove(Object o) { + return m.remove(o)==PRESENT; +} +``` + +# 7. 查找单个元素 + +`#contains(Object key)` 方法,判断 key 是否存在。代码如下: + +``` +// TreeSet.java + +public boolean contains(Object o) { + return m.containsKey(o); +} +``` + +> 艿艿:后面的内容,快速看即可。 + +# 8. 查找接近的元素 + +在 NavigableSet 中,定义了四个查找接近的元素: + +- `#lower(E e)` 方法,小于 `e` 的 key +- `#floor(E e)` 方法,小于等于 `e` 的 key +- `#higher(E e)` 方法,大于 `e` 的 key +- `#ceiling(E e)` 方法,大于等于 `e` 的 key + +我们一起来看看哈。 + +``` +// TreeSet.java + +public E lower(E e) { + return m.lowerKey(e); +} + +public E floor(E e) { + return m.floorKey(e); +} + +public E ceiling(E e) { + return m.ceilingKey(e); +} + +public E higher(E e) { + return m.higherKey(e); +} +``` + +# 9. 获得首尾的元素 + +`#first()` 方法,获得首个 key 。代码如下: + +``` +// TreeSet.java + +public E first() { + return m.firstKey(); +} +``` + +- `#pollFirst()` 方法,获得并移除首个 key 。代码如下: + + ``` + // TreeSet.java + + public E pollFirst() { + Map.Entry e = m.pollFirstEntry(); + return (e == null) ? null : e.getKey(); + } + ``` + +`#last()` 方法,获得尾部 key 。代码如下: + +``` +// TreeSet.java + +public E last() { + return m.lastKey(); +} +``` + +- `#pollLast()` 方法,获得并移除尾部 key 。代码如下: + + ``` + // TreeSet.java + + public E pollLast() { + Map.Entry e = m.pollLastEntry(); + return (e == null) ? null : e.getKey(); + } + ``` + +# 10. 清空 + +`#clear()` 方法,清空。代码如下: + +``` +// TreeSet.java + +public void clear() { + m.clear(); +} +``` + +# 11. 克隆 + +`#clone()` 方法,克隆 TreeSet 。代码如下: + +``` +// TreeSet.java + +public Object clone() { + // 克隆创建 TreeSet 对象 + TreeSet clone; + try { + clone = (TreeSet) super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError(e); + } + + // 创建 TreeMap 对象,赋值给 clone 的 m 属性 + clone.m = new TreeMap<>(m); + return clone; +} +``` + +# 12. 序列化 + +`#writeObject(ObjectOutputStream s)` 方法,序列化 TreeSet 对象。代码如下: + +``` +// TreeSet.java + +@java.io.Serial +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out any hidden stuff + // 写入非静态属性、非 transient 属性 + s.defaultWriteObject(); + + // Write out Comparator + // 写入比较器 + s.writeObject(m.comparator()); + + // Write out size + // 写入 key-value 键值对数量 + s.writeInt(m.size()); + + // Write out all elements in the proper order. + // 写入具体的 key-value 键值对 + for (E e : m.keySet()) + s.writeObject(e); +} +``` + +# 13. 反序列化 + +`#readObject(ObjectInputStream s)` 方法,反序列化成 TreeSet 对象。代码如下: + +``` +// TreeSet.java + +@java.io.Serial +private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + // Read in any hidden stuff + // 读取非静态属性、非 transient 属性 + s.defaultReadObject(); + + // Read in Comparator + // 读取比较器 + @SuppressWarnings("unchecked") + Comparator c = (Comparator) s.readObject(); + + // Create backing TreeMap + // 创建 TreeMap 对象 + TreeMap tm = new TreeMap<>(c); + m = tm; + + // Read in size + // 读取 key-value 键值对数量 + int size = s.readInt(); + + // 读取具体的 key-value 键值对 + tm.readTreeSet(size, s, PRESENT); +} + +// TreeMap.java + +void readTreeSet(int size, java.io.ObjectInputStream s, V defaultVal) + throws java.io.IOException, ClassNotFoundException { + buildFromSorted(size, null, s, defaultVal); +} +``` + +# 14. 获得迭代器 + +``` +// TreeSet.java + +public Iterator iterator() { // 正序 Iterator 迭代器 + return m.navigableKeySet().iterator(); +} + +public Iterator descendingIterator() { // 倒序 Iterator 迭代器 + return m.descendingKeySet().iterator(); +} +``` + +# 15. 转换成 Set/Collection + +``` +// TreeSet.java + +public NavigableSet descendingSet() { + return new TreeSet<>(m.descendingMap()); +} +``` + +# 16. 查找范围的元素 + +``` +// TreeSet.java + +// subSet 组 +public NavigableSet subSet(E fromElement, boolean fromInclusive, + E toElement, boolean toInclusive) { + return new TreeSet<>(m.subMap(fromElement, fromInclusive, + toElement, toInclusive)); +} +public SortedSet subSet(E fromElement, E toElement) { + return subSet(fromElement, true, toElement, false); +} + +// headSet 组 +public NavigableSet headSet(E toElement, boolean inclusive) { + return new TreeSet<>(m.headMap(toElement, inclusive)); +} +public SortedSet headSet(E toElement) { + return headSet(toElement, false); +} + +// tailSet 组 +public NavigableSet tailSet(E fromElement, boolean inclusive) { + return new TreeSet<>(m.tailMap(fromElement, inclusive)); +} + +public SortedSet tailSet(E fromElement) { + return tailSet(fromElement, true); +} +``` + +# 666. 彩蛋 + +😈 总的来说,比较简单,相信胖友一会会时间就已经看完了。 + +关于 TreeSet 的总结,只有一句话:TreeSet 是基于 TreeMap 的 Set 实现类。 \ No newline at end of file diff --git a/jdk/file-create.py b/jdk/file-create.py new file mode 100644 index 0000000..a412975 --- /dev/null +++ b/jdk/file-create.py @@ -0,0 +1,27 @@ +import os +import pyperclip + +# 获取当前脚本的路径 +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# 从剪贴板获取文件名列表,假设每行一个文件名 +file_names = pyperclip.paste().strip().split('\n') + +# 遍历文件名列表,为每个文件名添加排序前缀并创建文件 +for index, file_name in enumerate(file_names, start=1): + # 去除文件名两端的空白字符 + file_name = file_name.strip() + + # 添加排序前缀和.md后缀 + file_name_with_prefix = f"{index:02d}-{file_name}.md" # 这里使用3位数的排序前缀,可以根据需要调整 + + # 构建文件的完整路径 + file_path = os.path.join(script_dir, file_name_with_prefix) + + # 创建文件 + with open(file_path, 'w') as file: + # 如果需要写入初始内容,可以在这里添加 + # 例如:file.write("This is the content of the file.") + pass # 如果不需要写入内容,则使用pass + +print(f"Files created successfully in {script_dir}") \ No newline at end of file diff --git a/jdk/tmp.txt b/jdk/tmp.txt new file mode 100644 index 0000000..277cfa0 --- /dev/null +++ b/jdk/tmp.txt @@ -0,0 +1,9 @@ +JDK 源码解析-调试环境搭建(一)入门 +JDK 源码解析-调试环境搭建(二)进阶 +JDK 源码解析-集合(一)数组 ArrayList +JDK 源码解析-集合(二)链表 LinkedList +JDK 源码解析-集合(三)哈希表 HashMap +JDK 源码解析-集合(四)哈希表 LinkedHashMap +JDK 源码解析-集合(五)哈希集合 HashSet +JDK 源码解析-集合(六)TreeMap +JDK 源码解析-集合(七)TreeSet \ No newline at end of file diff --git a/mybatis/00-mybatis-面试.assets/02.png b/mybatis/00-mybatis-面试.assets/02.png new file mode 100644 index 0000000..94bf127 Binary files /dev/null and b/mybatis/00-mybatis-面试.assets/02.png differ diff --git a/mybatis/00-mybatis-面试.md b/mybatis/00-mybatis-面试.md new file mode 100644 index 0000000..2f0ec2d --- /dev/null +++ b/mybatis/00-mybatis-面试.md @@ -0,0 +1,637 @@ +# 精尽 MyBatis 面试题「最新更新时间:2024-02」 + +以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。 + +如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 MyBatis 面试题的大保健。 + +而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。 + +## MyBatis 编程步骤 + +1. 创建 SqlSessionFactory 对象。 +2. 通过 SqlSessionFactory 获取 SqlSession 对象。 +3. 通过 SqlSession 获得 Mapper 代理对象。 +4. 通过 Mapper 代理对象,执行数据库操作。 +5. 执行成功,则使用 SqlSession 提交事务。 +6. 执行失败,则使用 SqlSession 回滚事务。 +7. 最终,关闭会话。 + +## `#{}` 和 `${}` 的区别是什么? + +`${}` 是 Properties 文件中的变量占位符,它可以用于 XML 标签属性值和 SQL 内部,属于**字符串替换**。例如将 `${driver}` 会被静态替换为 `com.mysql.jdbc.Driver` : + +``` + + + + + +``` + +`${}` 也可以对传递进来的参数**原样拼接**在 SQL 中。代码如下: + +``` + +``` + +- 实际场景下,不推荐这么做。因为,可能有 SQL 注入的风险。 + +------ + +`#{}` 是 SQL 的参数占位符,Mybatis 会将 SQL 中的 `#{}` 替换为 `?` 号,在 SQL 执行前会使用 PreparedStatement 的参数设置方法,按序给 SQL 的 `?` 号占位符设置参数值,比如 `ps.setInt(0, parameterValue)` 。 所以,`#{}` 是**预编译处理**,可以有效防止 SQL 注入,提高系统安全性。 + +------ + +另外,`#{}` 和 `${}` 的取值方式非常方便。例如:`#{item.name}` 的取值方式,为使用反射从参数对象中,获取 `item` 对象的 `name` 属性值,相当于 `param.getItem().getName()` 。 + +## 当实体类中的属性名和表中的字段名不一样 ,怎么办? + +第一种, 通过在查询的 SQL 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。代码如下: + +``` + +``` + +- 这里,艿艿还有几点建议: + - 1、数据库的关键字,统一使用大写,例如:`SELECT`、`AS`、`FROM`、`WHERE` 。 + - 2、每 5 个查询字段换一行,保持整齐。 + - 3、`,` 的后面,和 `=` 的前后,需要有空格,更加清晰。 + - 4、`SELECT`、`FROM`、`WHERE` 等,单独一行,高端大气。 + +------ + +第二种,是第一种的特殊情况。大多数场景下,数据库字段名和实体类中的属性名差,主要是前者为**下划线风格**,后者为**驼峰风格**。在这种情况下,可以直接配置如下,实现自动的下划线转驼峰的功能。 + +``` + + + +``` + +😈 也就说,约定大于配置。非常推荐! + +------ + +第三种,通过 `` 来映射字段名和实体类属性名的一一对应的关系。代码如下: + +``` + + + + + + + + + +``` + +- 此处 `SELECT *` 仅仅作为示例只用,实际场景下,千万千万千万不要这么干。用多少字段,查询多少字段。 +- 相比第一种,第三种的**重用性**会一些。 + +## XML 映射文件中,除了常见的 select | insert | update | delete标 签之外,还有哪些标签? + +如下部分,可见 [《MyBatis 文档 —— Mapper XML 文件》](http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html) : + +- ``标签,给定命名空间的缓存配置。 + - `` 标签,其他命名空间缓存配置的引用。 +- `` 标签,是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。 +- ``标签,可被其他语句引用的可重用语句块。 + + - `` 标签,引用 `` 标签的语句。 +- `` 标签,不支持自增的主键生成策略标签。 + +如下部分,可见 [《MyBatis 文档 —— 动态 SQL》](http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html) : + +- `` +- ``、``、`` +- ``、``、`` +- `` +- `` + +## Mybatis 动态 SQL 是做什么的?都有哪些动态 SQL ?能简述一下动态 SQL 的执行原理吗? + +- Mybatis 动态 SQL ,可以让我们在 XML 映射文件内,以 XML 标签的形式编写动态 SQL ,完成逻辑判断和动态拼接 SQL 的功能。 +- Mybatis 提供了 9 种动态 SQL 标签:``、``、``、``、``、``、``、``、`` 。 +- 其执行原理为,使用 **OGNL** 的表达式,从 SQL 参数对象中计算表达式的值,根据表达式的值动态拼接 SQL ,以此来完成动态 SQL 的功能。 + +如上的内容,更加详细的话,请看 [《MyBatis 文档 —— 动态 SQL》](http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html) 文档。 + +## 最佳实践中,通常一个 XML 映射文件,都会写一个 Mapper 接口与之对应。请问,这个 Mapper 接口的工作原理是什么?Mapper 接口里的方法,参数不同时,方法能重载吗? + +Mapper 接口,对应的关系如下: + +- 接口的全限名,就是映射文件中的 `"namespace"` 的值。 +- 接口的方法名,就是映射文件中 MappedStatement 的 `"id"` 值。 +- 接口方法内的参数,就是传递给 SQL 的参数。 + +Mapper 接口是没有实现类的,当调用接口方法时,接口全限名 + 方法名拼接字符串作为 key 值,可唯一定位一个对应的 MappedStatement 。举例:`com.mybatis3.mappers.StudentDao.findStudentById` ,可以唯一找到 `"namespace"` 为 `com.mybatis3.mappers.StudentDao` 下面 `"id"` 为 `findStudentById` 的 MappedStatement 。 + +总结来说,在 Mybatis 中,每一个 ` + SELECT * + FROM students + LIMIT #{start}, #{end} + +``` + +- 显然,这不是一种优雅的方式。 + +------ + +第二种,保持传递多个参数,使用 `@Param` 注解。代码如下: + +``` +// 调用方法 +return studentMapper.selectStudents(0, 10); + +// Mapper 接口 +List selectStudents(@Param("start") Integer start, @Param("end") Integer end); + +// Mapper XML 代码 + +``` + +- 推荐使用这种方式。 + +------ + +第三种,保持传递多个参数,不使用 `@Param` 注解。代码如下: + +``` +// 调用方法 +return studentMapper.selectStudents(0, 10); + +// Mapper 接口 +List selectStudents(Integer start, Integer end); + +// Mapper XML 代码 + +``` + +- 其中,按照参数在方法方法中的位置,从 1 开始,逐个为 `#{param1}`、`#{param2}`、`#{param3}` 不断向下。 + +## Mybatis 是否可以映射 Enum 枚举类? + +Mybatis 可以映射枚举类,对应的实现类为 EnumTypeHandler 或 EnumOrdinalTypeHandler 。 + +- EnumTypeHandler ,基于 `Enum.name` 属性( String )。**默认**。 +- EnumOrdinalTypeHandler ,基于 `Enum.ordinal` 属性( `int` )。可通过 `` 来设置。 + +😈 当然,实际开发场景,我们很少使用 Enum 类型,更加的方式是,代码如下: + +``` +public class Dog { + + public static final int STATUS_GOOD = 1; + public static final int STATUS_BETTER = 2; + public static final int STATUS_BEST = 3; + + private int status; + +} +``` + +------ + +并且,不单可以映射枚举类,Mybatis 可以映射任何对象到表的一列上。映射方式为自定义一个 TypeHandler 类,实现 TypeHandler 的`#setParameter(...)` 和 `#getResult(...)` 接口方法。 + +TypeHandler 有两个作用: + +- 一是,完成从 javaType 至 jdbcType 的转换。 +- 二是,完成 jdbcType 至 javaType 的转换。 + +具体体现为 `#setParameter(...)` 和 `#getResult(..)` 两个方法,分别代表设置 SQL 问号占位符参数和获取列查询结果。 + +关于 TypeHandler 的**原理**,可以看看 [《精尽 MyBatis 源码分析 —— 类型模块》](http://svip.iocoder.cn/MyBatis/type-package/) 。 + +## Mybatis 都有哪些 Executor 执行器?它们之间的区别是什么? + +Mybatis 有四种 Executor 执行器,分别是 SimpleExecutor、ReuseExecutor、BatchExecutor、CachingExecutor 。 + +- SimpleExecutor :每执行一次 update 或 select 操作,就创建一个 Statement 对象,用完立刻关闭 Statement 对象。 +- ReuseExecutor :执行 update 或 select 操作,以 SQL 作为key 查找**缓存**的 Statement 对象,存在就使用,不存在就创建;用完后,不关闭 Statement 对象,而是放置于缓存 `Map` 内,供下一次使用。简言之,就是重复使用 Statement 对象。 +- BatchExecutor :执行 update 操作(没有 select 操作,因为 JDBC 批处理不支持 select 操作),将所有 SQL 都添加到批处理中(通过 addBatch 方法),等待统一执行(使用 executeBatch 方法)。它缓存了多个 Statement 对象,每个 Statement 对象都是调用 addBatch 方法完毕后,等待一次执行 executeBatch 批处理。**实际上,整个过程与 JDBC 批处理是相同**。 +- CachingExecutor :在上述的三个执行器之上,增加**二级缓存**的功能。 + +------ + +通过设置 `` 的 `"value"` 属性,可传入 SIMPLE、REUSE、BATCH 三个值,分别使用 SimpleExecutor、ReuseExecutor、BatchExecutor 执行器。 + +通过设置 ` + INSERT INTO users(name) + VALUES (#{value}) + +``` + +然后,然后在对应的 Mapper 接口中,编写映射的方法。代码如下: + +``` +public interface UserMapper { + + void insertUser(@Param("name") String name); + +} +``` + +最后,调用该 Mapper 接口方法。代码如下: + +``` +private static SqlSessionFactory sqlSessionFactory; + +@Test +public void testBatch() { + // 创建要插入的用户的名字的数组 + List names = new ArrayList<>(); + names.add("占小狼"); + names.add("朱小厮"); + names.add("徐妈"); + names.add("飞哥"); + + // 获得执行器类型为 Batch 的 SqlSession 对象,并且 autoCommit = false ,禁止事务自动提交 + try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) { + // 获得 Mapper 对象 + UserMapper mapper = session.getMapper(UserMapper.class); + // 循环插入 + for (String name : names) { + mapper.insertUser(name); + } + // 提交批量操作 + session.commit(); + } +} +``` + +代码比较简单,胖友仔细看看。当然,还有另一种方式,代码如下: + +``` +INSERT INTO [表名]([列名],[列名]) +VALUES +([列值],[列值])), +([列值],[列值])), +([列值],[列值])); +``` + +- 对于这种方式,需要保证单条 SQL 不超过语句的最大限制 `max_allowed_packet` 大小,默认为 1 M 。 + +这两种方式的性能对比,可以看看 [《[实验\]mybatis批量插入方式的比较》](https://www.jianshu.com/p/cce617be9f9e) 。 + +## 介绍 MyBatis 的一级缓存和二级缓存的概念和实现原理? + +内容有些长,直接参见 [《聊聊 MyBatis 缓存机制》](https://tech.meituan.com/mybatis_cache.html) 一文。 + +------ + +这块的源码解析,可见 [《精尽 MyBatis 源码分析 —— 缓存模块》](http://svip.iocoder.cn/MyBatis/cache-package) 。 + +## Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么? + +Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载。其中,association 指的就是**一对一**,collection 指的就是**一对多查询**。 + +在 Mybatis 配置文件中,可以配置 `` 来启用延迟加载的功能。默认情况下,延迟加载的功能是**关闭**的。 + +------ + +它的原理是,使用 CGLIB 或 Javassist( 默认 ) 创建目标对象的代理对象。当调用代理对象的延迟加载属性的 getting 方法时,进入拦截器方法。比如调用 `a.getB().getName()` 方法,进入拦截器的 `invoke(...)` 方法,发现 `a.getB()` 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对象的 SQL ,把 B 查询上来,然后调用`a.setB(b)` 方法,于是 `a` 对象 `b` 属性就有值了,接着完成`a.getB().getName()` 方法的调用。这就是延迟加载的基本原理。 + +当然了,不光是 Mybatis,几乎所有的包括 Hibernate 在内,支持延迟加载的原理都是一样的。 + +------ + +这块的源码解析,可见 [《 精尽 MyBatis 源码分析 —— SQL 执行(五)之延迟加载》](http://svip.iocoder.cn/MyBatis/executor-5) 文章。 + +## Mybatis 能否执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别。 + +> 艿艿:这道题有点难度。理解倒是好理解,主要那块源码的实现,艿艿看的有点懵逼。大体的意思是懂的,但是一些细节没扣完。 + +能,Mybatis 不仅可以执行一对一、一对多的关联查询,还可以执行多对一,多对多的关联查询。 + +> 艿艿:不过貌似,我自己实际开发中,还是比较喜欢自己去查询和拼接映射的数据。😈 + +- 多对一查询,其实就是一对一查询,只需要把 `selectOne(...)` 修改为 `selectList(...)` 即可。案例可见 [《MyBatis:多对一表关系详解》](https://blog.csdn.net/xzm_rainbow/article/details/15336959) 。 +- 多对多查询,其实就是一对多查询,只需要把 `#selectOne(...)` 修改为 `selectList(...)` 即可。案例可见 [《【MyBatis学习10】高级映射之多对多查询》](https://blog.csdn.net/eson_15/article/details/51655188) 。 + +------ + +关联对象查询,有两种实现方式: + +> 艿艿:所有的技术方案,即会有好处,又会有坏处。很难出现,一个完美的银弹方案。 + +- 一种是单独发送一个 SQL 去查询关联对象,赋给主对象,然后返回主对象。好处是多条 SQL 分开,相对简单,坏处是发起的 SQL 可能会比较多。 +- 另一种是使用嵌套查询,嵌套查询的含义为使用 `join` 查询,一部分列是 A 对象的属性值,另外一部分列是关联对象 B 的属性值。好处是只发一个 SQL 查询,就可以把主对象和其关联对象查出来,坏处是 SQL 可能比较复杂。 + +那么问题来了,`join` 查询出来 100 条记录,如何确定主对象是 5 个,而不是 100 个呢?其去重复的原理是 `` 标签内的`` 子标签,指定了唯一确定一条记录的 `id` 列。Mybatis 会根据`` 列值来完成 100 条记录的去重复功能,`` 可以有多个,代表了联合主键的语意。 + +同样主对象的关联对象,也是根据这个原理去重复的。尽管一般情况下,只有主对象会有重复记录,关联对象一般不会重复。例如:下面 `join` 查询出来6条记录,一、二列是 Teacher 对象列,第三列为 Student 对象列。Mybatis 去重复处理后,结果为 1 个老师和 6 个学生,而不是 6 个老师和 6 个学生。 + +| t_id | t_name | s_id | +| :--- | :------ | :--- | +| 1 | teacher | 38 | +| 1 | teacher | 39 | +| 1 | teacher | 40 | +| 1 | teacher | 41 | +| 1 | teacher | 42 | +| 1 | teacher | 43 | + +## 简述 Mybatis 的插件运行原理?以及如何编写一个插件? + +Mybatis 仅可以编写针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这 4 种接口的插件。 + +Mybatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler 的 `#invoke(...)`方法。当然,只会拦截那些你指定需要拦截的方法。 + +------ + +编写一个 MyBatis 插件的步骤如下: + +1. 首先,实现 Mybatis 的 Interceptor 接口,并实现 `#intercept(...)` 方法。 +2. 然后,在给插件编写注解,指定要拦截哪一个接口的哪些方法即可 +3. 最后,在配置文件中配置你编写的插件。 + +具体的,可以参考 [《MyBatis 官方文档 —— 插件》](http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins) 。 + +------ + +插件的详细解析,可以看看 [《精尽 MyBatis 源码分析 —— 插件体系(一)之原理》](http://svip.iocoder.cn/MyBatis/plugin-1) 。 + +## Mybatis 是如何进行分页的?分页插件的原理是什么? + +Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的**内存分页**,而非**数据库分页**。 + +所以,实际场景下,不适合直接使用 MyBatis 原有的 RowBounds 对象进行分页。而是使用如下两种方案: + +- 在 SQL 内直接书写带有数据库分页的参数来完成数据库分页功能 +- 也可以使用分页插件来完成数据库分页。 + +这两者都是基于数据库分页,差别在于前者是工程师**手动**编写分页条件,后者是插件**自动**添加分页条件。 + +------ + +分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义分页插件。在插件的拦截方法内,拦截待执行的 SQL ,然后重写 SQL ,根据dialect 方言,添加对应的物理分页语句和物理分页参数。 + +举例:`SELECT * FROM student` ,拦截 SQL 后重写为:`select * FROM student LIMI 0,10` 。 + +目前市面上目前使用比较广泛的 MyBatis 分页插件有: + +- [Mybatis-PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) +- [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) + +从现在看来,[MyBatis-Plus](https://github.com/baomidou/mybatis-plus) 逐步使用的更加广泛。 + +关于 MyBatis 分页插件的原理深入,可以看看 [《精尽 MyBatis 源码分析 —— 插件体系(二)之 PageHelper》](http://svip.iocoder.cn/MyBatis/plugin-2) 。 + +## MyBatis 与 Hibernate 有哪些不同? + +Mybatis 和 Hibernate 不同,它**不完全是**一个 ORM 框架,因为MyBatis 需要程序员自己编写 SQL 语句。不过 MyBatis 可以通过 XML 或注解方式灵活配置要运行的 SQL 语句,并将 Java 对象和 SQL 语句映射生成最终执行的 SQL ,最后将 SQL 执行的结果再映射生成 Java 对象。 + +Mybatis 学习门槛低,简单易学,程序员直接编写原生态 SQL ,可严格控制 SQL 执行性能,灵活度高。但是灵活的前提是 MyBatis 无法做到数据库无关性,如果需要实现支持多种数据库的软件则需要自定义多套 SQL 映射文件,工作量大。 + +Hibernate 对象/关系映射能力强,数据库无关性好。如果用 Hibernate 开发可以节省很多代码,提高效率。但是 Hibernate 的缺点是学习门槛高,要精通门槛更高,而且怎么设计 O/R 映射,在性能和对象模型之间如何权衡,以及怎样用好 Hibernate 需要具有很强的经验和能力才行。 + +总之,按照用户的需求在有限的资源环境下只要能做出维护性、扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。简单总结如下: + +- Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取。 +- Mybatis 属于半自动 ORM 映射工具,在查询关联对象或关联集合对象时,需要手动编写 SQL 来完成。 + +另外,在 [《浅析 Mybatis 与 Hibernate 的区别与用途》](https://www.jianshu.com/p/96171e647885) 文章,也是写的非常不错的。 + +当然,实际上,MyBatis 也可以搭配自动生成代码的工具,提升开发效率,还可以使用 [MyBatis-Plus](http://mp.baomidou.com/) 框架,已经内置常用的 SQL 操作,也是非常不错的。 + +## JDBC 编程有哪些不足之处,MyBatis是如何解决这些问题的? + +问题一:SQL 语句写在代码中造成代码不易维护,且代码会比较混乱。 + +解决方式:将 SQL 语句配置在 Mapper XML 文件中,与 Java 代码分离。 + +------ + +问题二:根据参数不同,拼接不同的 SQL 语句非常麻烦。例如 SQL 语句的 WHERE 条件不一定,可能多也可能少,占位符需要和参数一一对应。 + +解决方式:MyBatis 提供 ``、`` 等等动态语句所需要的标签,并支持 OGNL 表达式,简化了动态 SQL 拼接的代码,提升了开发效率。 + +------ + +问题三,对结果集解析麻烦,SQL 变化可能导致解析代码变化,且解析前需要遍历。 + +解决方式:Mybatis 自动将 SQL 执行结果映射成 Java 对象。 + +------ + +问题四,数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。 + +解决方式:在 `mybatis-config.xml` 中,配置数据链接池,使用连接池管理数据库链接。 + +😈 当然,即使不使用 MyBatis ,也可以使用数据库连接池。 +另外,MyBatis 默认提供了数据库连接池的实现,只是说,因为其它开源的数据库连接池性能更好,所以一般很少使用 MyBatis 自带的连接池实现。 + +## Mybatis 比 IBatis 比较大的几个改进是什么? + +> 这是一个选择性了解的问题,因为可能现在很多面试官,都没用过 IBatis 框架。 + +1. 有接口绑定,包括注解绑定 SQL 和 XML 绑定 SQL 。 +2. 动态 SQL 由原来的节点配置变成 OGNL 表达式。 +3. 在一对一或一对多的时候,引进了 `association` ,在一对多的时候,引入了 `collection`节点,不过都是在 `` 里面配置。 + +## Mybatis 映射文件中,如果 A 标签通过 include 引用了B标签的内容,请问,B 标签能否定义在 A 标签的后面,还是说必须定义在A标签的前面? + +> 老艿艿:这道题目,已经和源码实现,有点关系了。 + +虽然 Mybatis 解析 XML 映射文件是**按照顺序**解析的。但是,被引用的 B 标签依然可以定义在任何地方,Mybatis 都可以正确识别。**也就是说,无需按照顺序,进行定义**。 + +原理是,Mybatis 解析 A 标签,发现 A 标签引用了 B 标签,但是 B 标签尚未解析到,尚不存在,此时,Mybatis 会将 A 标签标记为**未解析状态**。然后,继续解析余下的标签,包含 B 标签,待所有标签解析完毕,Mybatis 会重新解析那些被标记为未解析的标签,此时再解析A标签时,B 标签已经存在,A 标签也就可以正常解析完成了。 + +可能有一些绕,胖友可以看看 [《精尽 MyBatis 源码解析 —— MyBatis 初始化(一)之加载 mybatis-config》](http://svip.iocoder.cn/MyBatis/builder-package-1) 。 + +此处,我们在引申一个问题,Spring IOC 中,存在互相依赖的 Bean 对象,该如何解决呢?答案见 [《【死磕 Spring】—— IoC 之加载 Bean:创建 Bean(五)之循环依赖处理》](http://svip.iocoder.cn/Spring/IoC-get-Bean-createBean-5/) 。 + +## 简述 Mybatis 的 XML 映射文件和 Mybatis 内部数据结构之间的映射关系? + +> 老艿艿:这道题目,已经和源码实现,有点关系了。 + +Mybatis 将所有 XML 配置信息都封装到 All-In-One 重量级对象Configuration内部。 + +在 XML Mapper 文件中: + +- `` 标签,会被解析为 ParameterMap 对象,其每个子元素会被解析为 ParameterMapping 对象。 +- `` 标签,会被解析为 ResultMap 对象,其每个子元素会被解析为 ResultMapping 对象。 +- 每一个 `` 或者 `` 等等,都对应一个 MappedStatement 对象。 +- `<3>` 处,如果没有,并且当前方法就是 `declaringClass` 声明的,则说明真的找不到。 +- `<4>` 处,遍历父接口,**递归**继续获得 MappedStatement 对象。**因为,该该方法定义在父接口中**。 + +# 7. MethodSignature + +MethodSignature ,是 MapperMethod 的内部静态类,方法签名。 + +## 7.1 构造方法 + +``` +// MethodSignature.java + +/** + * 返回类型是否为集合 + */ +private final boolean returnsMany; +/** + * 返回类型是否为 Map + */ +private final boolean returnsMap; +/** + * 返回类型是否为 void + */ +private final boolean returnsVoid; +/** + * 返回类型是否为 {@link org.apache.ibatis.cursor.Cursor} + */ +private final boolean returnsCursor; +/** + * 返回类型是否为 {@link java.util.Optional} + */ +private final boolean returnsOptional; +/** + * 返回类型 + */ +private final Class returnType; +/** + * 返回方法上的 {@link MapKey#value()} ,前提是返回类型为 Map + */ +private final String mapKey; +/** + * 获得 {@link ResultHandler} 在方法参数中的位置。 + * + * 如果为 null ,说明不存在这个类型 + */ +private final Integer resultHandlerIndex; +/** + * 获得 {@link RowBounds} 在方法参数中的位置。 + * + * 如果为 null ,说明不存在这个类型 + */ +private final Integer rowBoundsIndex; +/** + * ParamNameResolver 对象 + */ +private final ParamNameResolver paramNameResolver; + +public MethodSignature(Configuration configuration, Class mapperInterface, Method method) { + // 初始化 returnType 属性 + Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface); + if (resolvedReturnType instanceof Class) { // 普通类 + this.returnType = (Class) resolvedReturnType; + } else if (resolvedReturnType instanceof ParameterizedType) { // 泛型 + this.returnType = (Class) ((ParameterizedType) resolvedReturnType).getRawType(); + } else { // 内部类等等 + this.returnType = method.getReturnType(); + } + // 初始化 returnsVoid 属性 + this.returnsVoid = void.class.equals(this.returnType); + // 初始化 returnsMany 属性 + this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray(); + // 初始化 returnsCursor 属性 + this.returnsCursor = Cursor.class.equals(this.returnType); + // 初始化 returnsOptional 属性 + this.returnsOptional = Optional.class.equals(this.returnType); + // <1> 初始化 mapKey + this.mapKey = getMapKey(method); + // 初始化 returnsMap + this.returnsMap = this.mapKey != null; + // <2> 初始化 rowBoundsIndex、resultHandlerIndex + this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); + this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); + // 初始化 paramNameResolver + this.paramNameResolver = new ParamNameResolver(configuration, method); +} +``` + +- `<1>` 处,调用 `#getMapKey(Method method)` 方法,获得注解的 `{@link MapKey#value()}` 。代码如下: + + ``` + // MethodSignature.java + + private String getMapKey(Method method) { + String mapKey = null; + // 返回类型为 Map + if (Map.class.isAssignableFrom(method.getReturnType())) { + // 使用 @MapKey 注解 + final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class); + // 获得 @MapKey 注解的键 + if (mapKeyAnnotation != null) { + mapKey = mapKeyAnnotation.value(); + } + } + return mapKey; + } + ``` + +- `<2>` 处,调用 `#getUniqueParamIndex(Method method, Class paramType)` 方法,获得指定参数类型在方法参数中的位置。代码如下: + + ``` + // MethodSignature.java + + private Integer getUniqueParamIndex(Method method, Class paramType) { + Integer index = null; + // 遍历方法参数 + final Class[] argTypes = method.getParameterTypes(); + for (int i = 0; i < argTypes.length; i++) { + if (paramType.isAssignableFrom(argTypes[i])) { // 类型符合 + // 获得第一次的位置 + if (index == null) { + index = i; + // 如果重复类型了,则抛出 BindingException 异常 + } else { + throw new BindingException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters"); + } + } + } + return index; + } + ``` + +## 7.2 convertArgsToSqlCommandParam + +`#convertArgsToSqlCommandParam(Object[] args)` 方法,获得 SQL 通用参数。代码如下: + +``` +// MethodSignature.java + +public Object convertArgsToSqlCommandParam(Object[] args) { + return paramNameResolver.getNamedParams(args); +} +``` + +- 在内部,会调用 `ParamNameResolver#getNamedParams(Object[] args)` 方法。在 [《精尽 MyBatis 源码分析 —— 反射模块》](http://svip.iocoder.cn/MyBatis/reflection-package) 中,有详细解析。 +- 如下图是一个 `args` 和转换后的结果的示例:[示例](http://static.iocoder.cn/images/MyBatis/2020_02_07/03.png) + +# 666. 彩蛋 + +木有彩蛋,哈哈哈哈。 + +参考和推荐如下文章: + +- 祖大俊 [《Mybatis3.3.x技术内幕(二):动态代理之投鞭断流(自动映射器Mapper的底层实现原理)》](https://my.oschina.net/zudajun/blog/666223) +- 徐郡明 [《MyBatis 技术内幕》](https://item.jd.com/12125531.html) 的 [「2.8 bind 模块」](https://svip.iocoder.cn/MyBatis/binding-package/#) 小节 \ No newline at end of file diff --git a/mybatis/15-mybatis-MyBatis 初始化(一)之加载 mybatis-config.assets/01.png b/mybatis/15-mybatis-MyBatis 初始化(一)之加载 mybatis-config.assets/01.png new file mode 100644 index 0000000..faa5ec9 Binary files /dev/null and b/mybatis/15-mybatis-MyBatis 初始化(一)之加载 mybatis-config.assets/01.png differ diff --git a/mybatis/15-mybatis-MyBatis 初始化(一)之加载 mybatis-config.md b/mybatis/15-mybatis-MyBatis 初始化(一)之加载 mybatis-config.md new file mode 100644 index 0000000..edca214 --- /dev/null +++ b/mybatis/15-mybatis-MyBatis 初始化(一)之加载 mybatis-config.md @@ -0,0 +1,1293 @@ +# 精尽 MyBatis 源码分析 —— MyBatis 初始化(一)之加载 mybatis-config + +# 1. 概述 + +从本文开始,我们来分享 MyBatis 初始化的流程。在 [《精尽 MyBatis 源码分析 —— 项目结构一览》](http://svip.iocoder.cn/MyBatis/intro) 中,我们简单介绍这个流程如下: + +> 在 MyBatis 初始化过程中,会加载 `mybatis-config.xml` 配置文件、映射配置文件以及 Mapper 接口中的注解信息,解析后的配置信息会形成相应的对象并保存到 Configuration 对象中。例如: +> +> - ``节点(即 ResultSet 的映射规则) 会被解析成 ResultMap 对象。 +> - `` 节点(即属性映射)会被解析成 ResultMapping 对象。 +> +> 之后,利用该 Configuration 对象创建 SqlSessionFactory对象。待 MyBatis 初始化之后,开发人员可以通过初始化得到 SqlSessionFactory 创建 SqlSession 对象并完成数据库操作。 + +- 对应 `builder` 模块,为配置**解析过程** +- 对应 `mapping` 模块,主要为 SQL 操作解析后的**映射**。 + +因为整个 MyBatis 的初始化流程涉及代码颇多,所以拆分成三篇文章: + +- 加载 `mybatis-config.xml` 配置文件。 +- 加载 Mapper 映射配置文件。 +- 加载 Mapper 接口中的注解信息。 + +本文就主要分享第一部分「加载 `mybatis-config.xml` 配置文件」。 + +------ + +MyBatis 的初始化流程的**入口**是 SqlSessionFactoryBuilder 的 `#build(Reader reader, String environment, Properties properties)` 方法,代码如下: + +> SqlSessionFactoryBuilder 中,build 方法有多种重载方式。这里就选取一个。 + +``` +// SqlSessionFactoryBuilder.java + +/** + * 构造 SqlSessionFactory 对象 + * + * @param reader Reader 对象 + * @param environment 环境 + * @param properties Properties 变量 + * @return SqlSessionFactory 对象 + */ +public SqlSessionFactory build(Reader reader, String environment, Properties properties) { + try { + // <1> 创建 XMLConfigBuilder 对象 + XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); + // <2> 执行 XML 解析 + // <3> 创建 DefaultSqlSessionFactory 对象 + return build(parser.parse()); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error building SqlSession.", e); + } finally { + ErrorContext.instance().reset(); + try { + reader.close(); + } catch (IOException e) { + // Intentionally ignore. Prefer previous error. + } + } +} +``` + +- `<1>` 处,创建 XMLConfigBuilder 对象。 +- `<2>` 处,调用 `XMLConfigBuilder#parse()` 方法,执行 XML 解析,返回 Configuration 对象。 +- `<3>` 处,创建 DefaultSqlSessionFactory 对象。 +- 本文的重点是 `<1>` 和 `<2>` 处,即 XMLConfigBuilder 类。详细解析,见 [「3. XMLConfigBuilder」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 + +# 2. BaseBuilder + +`org.apache.ibatis.builder.BaseBuilder` ,基础构造器抽象类,为子类提供通用的工具类。 + +为什么不直接讲 XMLConfigBuilder ,而是先讲 BaseBuilder 呢?因为,BaseBuilder 是 XMLConfigBuilder 的父类,并且它还有其他的子类。如下图所示:[![BaseBuilder 类图](15-mybatis-MyBatis 初始化(一)之加载 mybatis-config.assets/01.png)](http://static.iocoder.cn/images/MyBatis/2020_02_10/01.png)BaseBuilder 类图 + +## 2.1 构造方法 + +``` +// BaseBuilder.java + +/** + * MyBatis Configuration 对象 + */ +protected final Configuration configuration; +protected final TypeAliasRegistry typeAliasRegistry; +protected final TypeHandlerRegistry typeHandlerRegistry; + +public BaseBuilder(Configuration configuration) { + this.configuration = configuration; + this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); + this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); +} +``` + +- `configuration` 属性,MyBatis Configuration 对象。XML 和注解中解析到的配置,最终都会设置到 [`org.apache.ibatis.session.Configuration`](https://github.com/YunaiV/mybatis-3/blob/master/src/main/java/org/apache/ibatis/session/Configuration.java) 中。感兴趣的胖友,可以先点击瞅一眼。抽完之后,马上回来。 + +## 2.2 parseExpression + +`#parseExpression(String regex, String defaultValue)` 方法,创建正则表达式。代码如下: + +``` +// BaseBuilder.java + +/** + * 创建正则表达式 + * + * @param regex 指定表达式 + * @param defaultValue 默认表达式 + * @return 正则表达式 + */ +@SuppressWarnings("SameParameterValue") +protected Pattern parseExpression(String regex, String defaultValue) { + return Pattern.compile(regex == null ? defaultValue : regex); +} +``` + +## 2.3 xxxValueOf + +`#xxxValueOf(...)` 方法,将字符串转换成对应的数据类型的值。代码如下: + +``` +// BaseBuilder.java + +protected Boolean booleanValueOf(String value, Boolean defaultValue) { + return value == null ? defaultValue : Boolean.valueOf(value); +} + +protected Integer integerValueOf(String value, Integer defaultValue) { + return value == null ? defaultValue : Integer.valueOf(value); +} + +protected Set stringSetValueOf(String value, String defaultValue) { + value = (value == null ? defaultValue : value); + return new HashSet<>(Arrays.asList(value.split(","))); +} +``` + +## 2.4 resolveJdbcType + +`#resolveJdbcType(String alias)` 方法,解析对应的 JdbcType 类型。代码如下: + +``` +// BaseBuilder.java + +protected JdbcType resolveJdbcType(String alias) { + if (alias == null) { + return null; + } + try { + return JdbcType.valueOf(alias); + } catch (IllegalArgumentException e) { + throw new BuilderException("Error resolving JdbcType. Cause: " + e, e); + } +} +``` + +## 2.5 resolveResultSetType + +`#resolveResultSetType(String alias)` 方法,解析对应的 ResultSetType 类型。代码如下: + +``` +// BaseBuilder.java + +protected ResultSetType resolveResultSetType(String alias) { + if (alias == null) { + return null; + } + try { + return ResultSetType.valueOf(alias); + } catch (IllegalArgumentException e) { + throw new BuilderException("Error resolving ResultSetType. Cause: " + e, e); + } +} +``` + +## 2.6 resolveParameterMode + +`#resolveParameterMode(String alias)` 方法,解析对应的 ParameterMode 类型。代码如下: + +``` +// BaseBuilder.java + +protected ParameterMode resolveParameterMode(String alias) { + if (alias == null) { + return null; + } + try { + return ParameterMode.valueOf(alias); + } catch (IllegalArgumentException e) { + throw new BuilderException("Error resolving ParameterMode. Cause: " + e, e); + } +} +``` + +## 2.7 createInstance + +`#createInstance(String alias)` 方法,创建指定对象。代码如下: + +``` +// BaseBuilder.java + +protected Object createInstance(String alias) { + // <1> 获得对应的类型 + Class clazz = resolveClass(alias); + if (clazz == null) { + return null; + } + try { + // <2> 创建对象 + return resolveClass(alias).newInstance(); // 这里重复获得了一次 + } catch (Exception e) { + throw new BuilderException("Error creating instance. Cause: " + e, e); + } +} +``` + +- `<1>` 处,调用 `#resolveClass(String alias)` 方法,获得对应的类型。代码如下: + + ``` + // BaseBuilder.java + + protected Class resolveClass(String alias) { + if (alias == null) { + return null; + } + try { + return resolveAlias(alias); + } catch (Exception e) { + throw new BuilderException("Error resolving class. Cause: " + e, e); + } + } + + protected Class resolveAlias(String alias) { + return typeAliasRegistry.resolveAlias(alias); + } + ``` + + - 从 `typeAliasRegistry` 中,通过别名或类全名,获得对应的类。 + +- `<2>` 处,创建对象。 + +## 2.8 resolveTypeHandler + +`#resolveTypeHandler(Class javaType, String typeHandlerAlias)` 方法,从 `typeHandlerRegistry` 中获得或创建对应的 TypeHandler 对象。代码如下: + +``` +// BaseBuilder.java + +protected TypeHandler resolveTypeHandler(Class javaType, Class> typeHandlerType) { + if (typeHandlerType == null) { + return null; + } + // javaType ignored for injected handlers see issue #746 for full detail + // 先获得 TypeHandler 对象 + TypeHandler handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType); + if (handler == null) { // 如果不存在,进行创建 TypeHandler 对象 + // not in registry, create a new one + handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType); + } + return handler; +} +``` + +# 3. XMLConfigBuilder + +`org.apache.ibatis.builder.xml.XMLConfigBuilder` ,继承 BaseBuilder 抽象类,XML 配置构建器,主要负责解析 mybatis-config.xml 配置文件。即对应 [《MyBatis 文档 —— XML 映射配置文件》](http://www.mybatis.org/mybatis-3/zh/configuration.html) 。 + +## 3.1 构造方法 + +``` +// XMLConfigBuilder.java + +/** + * 是否已解析 + */ +private boolean parsed; +/** + * 基于 Java XPath 解析器 + */ +private final XPathParser parser; +/** + * 环境 + */ +private String environment; +/** + * ReflectorFactory 对象 + */ +private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory(); + +public XMLConfigBuilder(Reader reader) { + this(reader, null, null); +} + +public XMLConfigBuilder(Reader reader, String environment) { + this(reader, environment, null); +} + +public XMLConfigBuilder(Reader reader, String environment, Properties props) { + this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props); +} + +public XMLConfigBuilder(InputStream inputStream) { + this(inputStream, null, null); +} + +public XMLConfigBuilder(InputStream inputStream, String environment) { + this(inputStream, environment, null); +} + +public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) { + this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props); +} + +private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { + // <1> 创建 Configuration 对象 + super(new Configuration()); + ErrorContext.instance().resource("SQL Mapper Configuration"); + // <2> 设置 Configuration 的 variables 属性 + this.configuration.setVariables(props); + this.parsed = false; + this.environment = environment; + this.parser = parser; +} +``` + +- `parser` 属性,XPathParser 对象。在 [《精尽 MyBatis 源码分析 —— 解析器模块》](http://svip.iocoder.cn/MyBatis/parsing-package) 中,已经详细解析。 + +- `localReflectorFactory` 属性,DefaultReflectorFactory 对象。在 [《精尽 MyBatis 源码分析 —— 反射模块》](http://svip.iocoder.cn/MyBatis/reflection-package) 中,已经详细解析。 + +- 构造方法重载了比较多,只需要看最后一个。 + + - `<1>` 处,创建 Configuration 对象。 + + - `<2>` 处,设置 Configuration 对象的 `variables` 属性。代码如下: + + ``` + // Configuration.java + + /** + * 变量 Properties 对象。 + * + * 参见 {@link org.apache.ibatis.builder.xml.XMLConfigBuilder#propertiesElement(XNode context)} 方法 + */ + protected Properties variables = new Properties(); + + public void setVariables(Properties variables) { + this.variables = variables; + } + ``` + +## 3.2 parse + +`#parse()` 方法,解析 XML 成 Configuration 对象。代码如下: + +``` +// XMLConfigBuilder.java + +public Configuration parse() { + // <1.1> 若已解析,抛出 BuilderException 异常 + if (parsed) { + throw new BuilderException("Each XMLConfigBuilder can only be used once."); + } + // <1.2> 标记已解析 + parsed = true; + // <2> 解析 XML configuration 节点 + parseConfiguration(parser.evalNode("/configuration")); + return configuration; +} +``` + +- `<1.1>` 处,若已解析,抛出 BuilderException 异常。 +- `<1.2>` 处,标记已解析。 +- `<2>` 处,调用 `XPathParser#evalNode(String expression)` 方法,获得 XML `` 节点,后调用 `#parseConfiguration(XNode root)` 方法,解析该节点。详细解析,见 [「3.3 parseConfiguration」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 + +## 3.3 parseConfiguration + +`#parseConfiguration(XNode root)` 方法,解析 `` 节点。代码如下: + +``` +// XMLConfigBuilder.java + +private void parseConfiguration(XNode root) { + try { + //issue #117 read properties first + // <1> 解析 标签 + propertiesElement(root.evalNode("properties")); + // <2> 解析 标签 + Properties settings = settingsAsProperties(root.evalNode("settings")); + // <3> 加载自定义 VFS 实现类 + loadCustomVfs(settings); + // <4> 解析 标签 + typeAliasesElement(root.evalNode("typeAliases")); + // <5> 解析 标签 + pluginElement(root.evalNode("plugins")); + // <6> 解析 标签 + objectFactoryElement(root.evalNode("objectFactory")); + // <7> 解析 标签 + objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); + // <8> 解析 标签 + reflectorFactoryElement(root.evalNode("reflectorFactory")); + // <9> 赋值 到 Configuration 属性 + settingsElement(settings); + // read it after objectFactory and objectWrapperFactory issue #631 + // <10> 解析 标签 + environmentsElement(root.evalNode("environments")); + // <11> 解析 标签 + databaseIdProviderElement(root.evalNode("databaseIdProvider")); + // <12> 解析 标签 + typeHandlerElement(root.evalNode("typeHandlers")); + // <13> 解析 标签 + mapperElement(root.evalNode("mappers")); + } catch (Exception e) { + throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); + } +} +``` + +- `<1>` 处,调用 `#propertiesElement(XNode context)` 方法,解析 `` 节点。详细解析,见 [「3.3.1 propertiesElement」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<2>` 处,调用 `#settingsAsProperties(XNode context)` 方法,解析 `` 节点。详细解析,见 [「3.3.2 settingsAsProperties」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<3>` 处,调用 `#loadCustomVfs(Properties settings)` 方法,加载自定义 VFS 实现类。详细解析,见 [「3.3.3 loadCustomVfs」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<4>` 处,调用 `#typeAliasesElement(XNode parent)` 方法,解析 `` 节点。详细解析,见 [「3.3.4 typeAliasesElement」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<5>` 处,调用 `#typeAliasesElement(XNode parent)` 方法,解析 `` 节点。详细解析,见 [「3.3.5 pluginElement」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<6>` 处,调用 `#objectFactoryElement(XNode parent)` 方法,解析 `` 节点。详细解析,见 [「3.3.6 pluginElement」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<7>` 处,调用 `#objectWrapperFactoryElement(XNode parent)` 方法,解析 `` 节点。详细解析,见 [「3.3.7 objectWrapperFactoryElement」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<8>` 处,调用 `#reflectorFactoryElement(XNode parent)` 方法,解析 `` 节点。详细解析,见 [「3.3.8 reflectorFactoryElement」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<9>` 处,调用 `#settingsElement(Properties props)` 方法,赋值 `` 到 Configuration 属性。详细解析,见 [「3.3.9 settingsElement」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<10>` 处,调用 `#environmentsElement(XNode context)` 方法,解析 `` 标签。详细解析,见 [「3.3.10 environmentsElement」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<11>` 处,调用 `#databaseIdProviderElement(XNode context)` 方法,解析 `` 标签。详细解析,见 [「3.3.11 databaseIdProviderElement」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<12>` 处,调用 `#typeHandlerElement(XNode context)` 方法,解析 `` 标签。详细解析,见 [「3.3.12 typeHandlerElement」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 +- `<13>` 处,调用 `#mapperElement(XNode context)` 方法,解析 `` 标签。详细解析,见 [「3.3.13 mapperElement」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 。 + +### 3.3.1 propertiesElement + +`#propertiesElement(XNode context)` 方法,解析 `` 节点。大体逻辑如下: + +1. 解析 `` 标签,成 Properties 对象。 +2. 覆盖 `configuration` 中的 Properties 对象到上面的结果。 +3. 设置结果到 `parser` 和 `configuration` 中。 + +代码如下: + +``` +// XMLConfigBuilder.java + +private void propertiesElement(XNode context) throws Exception { + if (context != null) { + // 读取子标签们,为 Properties 对象 + Properties defaults = context.getChildrenAsProperties(); + // 读取 resource 和 url 属性 + String resource = context.getStringAttribute("resource"); + String url = context.getStringAttribute("url"); + if (resource != null && url != null) { // resource 和 url 都存在的情况下,抛出 BuilderException 异常 + throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); + } + // 读取本地 Properties 配置文件到 defaults 中。 + if (resource != null) { + defaults.putAll(Resources.getResourceAsProperties(resource)); + // 读取远程 Properties 配置文件到 defaults 中。 + } else if (url != null) { + defaults.putAll(Resources.getUrlAsProperties(url)); + } + // 覆盖 configuration 中的 Properties 对象到 defaults 中。 + Properties vars = configuration.getVariables(); + if (vars != null) { + defaults.putAll(vars); + } + // 设置 defaults 到 parser 和 configuration 中。 + parser.setVariables(defaults); + configuration.setVariables(defaults); + } +} +``` + +### 3.3.2 settingsAsProperties + +`#settingsElement(Properties props)` 方法,将 `` 标签解析为 Properties 对象。代码如下: + +``` +// XMLConfigBuilder.java + +private Properties settingsAsProperties(XNode context) { + // 将子标签,解析成 Properties 对象 + if (context == null) { + return new Properties(); + } + Properties props = context.getChildrenAsProperties(); + // Check that all settings are known to the configuration class + // 校验每个属性,在 Configuration 中,有相应的 setting 方法,否则抛出 BuilderException 异常 + MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); + for (Object key : props.keySet()) { + if (!metaConfig.hasSetter(String.valueOf(key))) { + throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); + } + } + return props; +} +``` + +### 3.3.3 loadCustomVfs + +`#loadCustomVfs(Properties settings)` 方法,加载自定义 VFS 实现类。代码如下: + +``` +// XMLConfigBuilder.java + +private void loadCustomVfs(Properties props) throws ClassNotFoundException { + // 获得 vfsImpl 属性 + String value = props.getProperty("vfsImpl"); + if (value != null) { + // 使用 , 作为分隔符,拆成 VFS 类名的数组 + String[] clazzes = value.split(","); + // 遍历 VFS 类名的数组 + for (String clazz : clazzes) { + if (!clazz.isEmpty()) { + // 获得 VFS 类 + @SuppressWarnings("unchecked") + Class vfsImpl = (Class) Resources.classForName(clazz); + // 设置到 Configuration 中 + configuration.setVfsImpl(vfsImpl); + } + } + } +} + +// Configuration.java + +/** + * VFS 实现类 + */ +protected Class vfsImpl; + +public void setVfsImpl(Class vfsImpl) { + if (vfsImpl != null) { + // 设置 vfsImpl 属性 + this.vfsImpl = vfsImpl; + // 添加到 VFS 中的自定义 VFS 类的集合 + VFS.addImplClass(this.vfsImpl); + } +} +``` + +### 3.3.4 typeAliasesElement + +`#typeAliasesElement(XNode parent)` 方法,解析 `` 标签,将配置类注册到 `typeAliasRegistry` 中。代码如下: + +``` +// XMLConfigBuilder.java + +private void typeAliasesElement(XNode parent) { + if (parent != null) { + // 遍历子节点 + for (XNode child : parent.getChildren()) { + // 指定为包的情况下,注册包下的每个类 + if ("package".equals(child.getName())) { + String typeAliasPackage = child.getStringAttribute("name"); + configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); + // 指定为类的情况下,直接注册类和别名 + } else { + String alias = child.getStringAttribute("alias"); + String type = child.getStringAttribute("type"); + try { + Class clazz = Resources.classForName(type); // 获得类是否存在 + // 注册到 typeAliasRegistry 中 + if (alias == null) { + typeAliasRegistry.registerAlias(clazz); + } else { + typeAliasRegistry.registerAlias(alias, clazz); + } + } catch (ClassNotFoundException e) { // 若类不存在,则抛出 BuilderException 异常 + throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); + } + } + } + } +} +``` + +### 3.3.5 pluginElement + +`#pluginElement(XNode parent)` 方法,解析 `` 标签,添加到 `Configuration#interceptorChain` 中。代码如下: + +``` +// XMLConfigBuilder.java + +private void pluginElement(XNode parent) throws Exception { + if (parent != null) { + // 遍历 标签 + for (XNode child : parent.getChildren()) { + String interceptor = child.getStringAttribute("interceptor"); + Properties properties = child.getChildrenAsProperties(); + // <1> 创建 Interceptor 对象,并设置属性 + Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); + interceptorInstance.setProperties(properties); + // <2> 添加到 configuration 中 + configuration.addInterceptor(interceptorInstance); + } + } +} +``` + +- `<1>` 处,创建 Interceptor 对象,并设置属性。关于 Interceptor 类,后续文章,详细解析。 + +- `<2>` 处,调用 `Configuration#addInterceptor(Interceptor interceptor)` 方法,添加到 `configuration` 中。代码如下: + + ``` + // Configuration.java + + /** + * 拦截器链 + */ + protected final InterceptorChain interceptorChain = new InterceptorChain(); + + public void addInterceptor(Interceptor interceptor) { + interceptorChain.addInterceptor(interceptor); + } + ``` + + - 关于 InterceptorChain 类,后续文章,详细解析。 + +### 3.3.6 objectFactoryElement + +`#objectFactoryElement(XNode parent)` 方法,解析 `` 节点。代码如下: + +``` +// XMLConfigBuilder.java + +private void objectFactoryElement(XNode context) throws Exception { + if (context != null) { + // 获得 ObjectFactory 的实现类 + String type = context.getStringAttribute("type"); + // 获得 Properties 属性 + Properties properties = context.getChildrenAsProperties(); + // <1> 创建 ObjectFactory 对象,并设置 Properties 属性 + ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance(); + factory.setProperties(properties); + // <2> 设置 Configuration 的 objectFactory 属性 + configuration.setObjectFactory(factory); + } +} +``` + +- `<1>` 处,创建 ObjectFactory 对象,并设置 Properties 属性。 + +- `<2>` 处,调用 `Configuration#setObjectFactory(ObjectFactory objectFactory)` 方法,设置 Configuration 的 `objectFactory` 属性。代码如下: + + ``` + // Configuration.java + + /** + * ObjectFactory 对象 + */ + protected ObjectFactory objectFactory = new DefaultObjectFactory(); + + public void setObjectFactory(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + ``` + +### 3.3.7 objectWrapperFactoryElement + +`#objectWrapperFactoryElement(XNode context)` 方法,解析 `` 节点。代码如下: + +``` +// XMLConfigBuilder.java + +private void objectWrapperFactoryElement(XNode context) throws Exception { + if (context != null) { + // 获得 ObjectFactory 的实现类 + String type = context.getStringAttribute("type"); + // <1> 创建 ObjectWrapperFactory 对象 + ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).newInstance(); + // 设置 Configuration 的 objectWrapperFactory 属性 + configuration.setObjectWrapperFactory(factory); + } +} +``` + +- `<1>` 处,创建 ObjectWrapperFactory 对象。 + +- `<2>` 处,调用 `Configuration#setObjectWrapperFactory(ObjectWrapperFactory objectWrapperFactory)` 方法,设置 Configuration 的 `objectWrapperFactory` 属性。代码如下: + + ``` + // Configuration.java + + /** + * ObjectWrapperFactory 对象 + */ + protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory(); + + public void setObjectWrapperFactory(ObjectWrapperFactory objectWrapperFactory) { + this.objectWrapperFactory = objectWrapperFactory; + } + ``` + +### 3.3.8 reflectorFactoryElement + +`#reflectorFactoryElement(XNode parent)` 方法,解析 `` 节点。代码如下: + +``` +// XMLConfigBuilder.java + +private void reflectorFactoryElement(XNode context) throws Exception { + if (context != null) { + // 获得 ReflectorFactory 的实现类 + String type = context.getStringAttribute("type"); + // 创建 ReflectorFactory 对象 + ReflectorFactory factory = (ReflectorFactory) resolveClass(type).newInstance(); + // 设置 Configuration 的 reflectorFactory 属性 + configuration.setReflectorFactory(factory); + } +} +``` + +- `<1>` 处,创建 ReflectorFactory 对象。 + +- `<2>` 处,调用 `Configuration#setReflectorFactory(ReflectorFactory reflectorFactory)` 方法,设置 Configuration 的 `reflectorFactory` 属性。代码如下: + + ``` + // Configuration.java + + /** + * ReflectorFactory 对象 + */ + protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory(); + + public void setReflectorFactory(ReflectorFactory reflectorFactory) { + this.reflectorFactory = reflectorFactory; + } + ``` + +### 3.3.9 settingsElement + +`#settingsElement(Properties props)` 方法,赋值 `` 到 Configuration 属性。代码如下: + +``` +// XMLConfigBuilder.java + +private void settingsElement(Properties props) throws Exception { + configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL"))); + configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE"))); + configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true)); + configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory"))); + configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false)); + configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false)); + configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true)); + configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true)); + configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false)); + configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE"))); + configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null)); + configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null)); + configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false)); + configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false)); + configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION"))); + configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER"))); + configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString")); + configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true)); + configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage"))); + @SuppressWarnings("unchecked") + Class typeHandler = resolveClass(props.getProperty("defaultEnumTypeHandler")); + configuration.setDefaultEnumTypeHandler(typeHandler); + configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false)); + configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true)); + configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false)); + configuration.setLogPrefix(props.getProperty("logPrefix")); + @SuppressWarnings("unchecked") + Class logImpl = resolveClass(props.getProperty("logImpl")); + configuration.setLogImpl(logImpl); + configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory"))); +} +``` + +- 属性比较多,瞟一眼就行。 + +### 3.3.10 environmentsElement + +`#environmentsElement(XNode context)` 方法,解析 `` 标签。代码如下: + +``` +// XMLConfigBuilder.java + +private void environmentsElement(XNode context) throws Exception { + if (context != null) { + // <1> environment 属性非空,从 default 属性获得 + if (environment == null) { + environment = context.getStringAttribute("default"); + } + // 遍历 XNode 节点 + for (XNode child : context.getChildren()) { + // <2> 判断 environment 是否匹配 + String id = child.getStringAttribute("id"); + if (isSpecifiedEnvironment(id)) { + // <3> 解析 `` 标签,返回 TransactionFactory 对象 + TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); + // <4> 解析 `` 标签,返回 DataSourceFactory 对象 + DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); + DataSource dataSource = dsFactory.getDataSource(); + // <5> 创建 Environment.Builder 对象 + Environment.Builder environmentBuilder = new Environment.Builder(id) + .transactionFactory(txFactory) + .dataSource(dataSource); + // <6> 构造 Environment 对象,并设置到 configuration 中 + configuration.setEnvironment(environmentBuilder.build()); + } + } + } +} +``` + +- `<1>` 处,若 `environment` 属性非空,从 `default` 属性种获得 `environment` 属性。 + +- `<2>` 处,遍历 XNode 节点,获得其 `id` 属性,后调用 `#isSpecifiedEnvironment(String id)` 方法,判断 `environment` 和 `id` 是否匹配。代码如下: + + ``` + // XMLConfigBuilder.java + + private boolean isSpecifiedEnvironment(String id) { + if (environment == null) { + throw new BuilderException("No environment specified."); + } else if (id == null) { + throw new BuilderException("Environment requires an id attribute."); + } else if (environment.equals(id)) { // 相等 + return true; + } + return false; + } + ``` + +- `<3>` 处,调用 `#transactionManagerElement(XNode context)` 方法,解析 `` 标签,返回 TransactionFactory 对象。代码如下: + + ``` + // XMLConfigBuilder.java + + private TransactionFactory transactionManagerElement(XNode context) throws Exception { + if (context != null) { + // 获得 TransactionFactory 的类 + String type = context.getStringAttribute("type"); + // 获得 Properties 属性 + Properties props = context.getChildrenAsProperties(); + // 创建 TransactionFactory 对象,并设置属性 + TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance(); + factory.setProperties(props); + return factory; + } + throw new BuilderException("Environment declaration requires a TransactionFactory."); + } + ``` + +- `<4>` 处,调用 `#dataSourceElement(XNode context)` 方法,解析 `` 标签,返回 DataSourceFactory 对象,而后返回 DataSource 对象。代码如下: + + ``` + // XMLConfigBuilder.java + + private DataSourceFactory dataSourceElement(XNode context) throws Exception { + if (context != null) { + // 获得 DataSourceFactory 的类 + String type = context.getStringAttribute("type"); + // 获得 Properties 属性 + Properties props = context.getChildrenAsProperties(); + // 创建 DataSourceFactory 对象,并设置属性 + DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); + factory.setProperties(props); + return factory; + } + throw new BuilderException("Environment declaration requires a DataSourceFactory."); + } + ``` + +- `<5>` 处,创建 Environment.Builder 对象。 + +- `<6>` 处,构造 Environment 对象,并设置到 `configuration` 中。代码如下: + + ``` + // Configuration.java + + /** + * DB Environment 对象 + */ + protected Environment environment; + + public void setEnvironment(Environment environment) { + this.environment = environment; + } + ``` + +#### 3.3.10.1 Environment + +`org.apache.ibatis.mapping.Environment` ,DB 环境。代码如下: + +``` +// Environment.java + +public final class Environment { + + /** + * 环境变好 + */ + private final String id; + /** + * TransactionFactory 对象 + */ + private final TransactionFactory transactionFactory; + /** + * DataSource 对象 + */ + private final DataSource dataSource; + + public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) { + if (id == null) { + throw new IllegalArgumentException("Parameter 'id' must not be null"); + } + if (transactionFactory == null) { + throw new IllegalArgumentException("Parameter 'transactionFactory' must not be null"); + } + this.id = id; + if (dataSource == null) { + throw new IllegalArgumentException("Parameter 'dataSource' must not be null"); + } + this.transactionFactory = transactionFactory; + this.dataSource = dataSource; + } + + /** + * 构造器 + */ + public static class Builder { + + /** + * 环境变好 + */ + private String id; + /** + * TransactionFactory 对象 + */ + private TransactionFactory transactionFactory; + /** + * DataSource 对象 + */ + private DataSource dataSource; + + public Builder(String id) { + this.id = id; + } + + public Builder transactionFactory(TransactionFactory transactionFactory) { + this.transactionFactory = transactionFactory; + return this; + } + + public Builder dataSource(DataSource dataSource) { + this.dataSource = dataSource; + return this; + } + + public String id() { + return this.id; + } + + public Environment build() { + return new Environment(this.id, this.transactionFactory, this.dataSource); + } + + } + + public String getId() { + return this.id; + } + + public TransactionFactory getTransactionFactory() { + return this.transactionFactory; + } + + public DataSource getDataSource() { + return this.dataSource; + } + +} +``` + +### 3.3.11 databaseIdProviderElement + +`#databaseIdProviderElement(XNode context)` 方法,解析 `` 标签。代码如下: + +``` +// XMLConfigBuilder.java + +private void databaseIdProviderElement(XNode context) throws Exception { + DatabaseIdProvider databaseIdProvider = null; + if (context != null) { + // <1> 获得 DatabaseIdProvider 的类 + String type = context.getStringAttribute("type"); + // awful patch to keep backward compatibility 保持兼容 + if ("VENDOR".equals(type)) { + type = "DB_VENDOR"; + } + // <2> 获得 Properties 对象 + Properties properties = context.getChildrenAsProperties(); + // <3> 创建 DatabaseIdProvider 对象,并设置对应的属性 + databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance(); + databaseIdProvider.setProperties(properties); + } + Environment environment = configuration.getEnvironment(); + if (environment != null && databaseIdProvider != null) { + // <4> 获得对应的 databaseId 编号 + String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource()); + // <5> 设置到 configuration 中 + configuration.setDatabaseId(databaseId); + } +} +``` + +- 不了解的胖友,可以先看看 [《MyBatis 文档 —— XML 映射配置文件 —— databaseIdProvider 数据库厂商标识》](http://www.mybatis.org/mybatis-3/zh/configuration.html#databaseIdProvider) 。 + +- `<1>` 处,获得 DatabaseIdProvider 的类。 + +- `<2>` 处,获得 Properties 对象。 + +- `<3>` 处,创建 DatabaseIdProvider 对象,并设置对应的属性。 + +- `<4>` 处,调用 `DatabaseIdProvider#getDatabaseId(DataSource dataSource)` 方法,获得对应的 `databaseId` **标识**。 + +- `<5>` 处,设置到 `configuration` 中。代码如下: + + ``` + // Configuration.java + + /** + * 数据库标识 + */ + protected String databaseId; + + public void setDatabaseId(String databaseId) { + this.databaseId = databaseId; + } + ``` + +#### 3.3.11.1 DatabaseIdProvider + +`org.apache.ibatis.mapping.DatabaseIdProvider` ,数据库标识提供器接口。代码如下: + +``` +public interface DatabaseIdProvider { + + /** + * 设置属性 + * + * @param p Properties 对象 + */ + void setProperties(Properties p); + + /** + * 获得数据库标识 + * + * @param dataSource 数据源 + * @return 数据库标识 + * @throws SQLException 当 DB 发生异常时 + */ + String getDatabaseId(DataSource dataSource) throws SQLException; + +} +``` + +#### 3.3.11.2 VendorDatabaseIdProvider + +`org.apache.ibatis.mapping.VendorDatabaseIdProvider` ,实现 DatabaseIdProvider 接口,供应商数据库标识提供器实现类。 + +**① 构造方法** + +``` +// VendorDatabaseIdProvider.java + +/** + * Properties 对象 + */ +private Properties properties; + +@Override +public String getDatabaseId(DataSource dataSource) { + if (dataSource == null) { + throw new NullPointerException("dataSource cannot be null"); + } + try { + return getDatabaseName(dataSource); + } catch (Exception e) { + log.error("Could not get a databaseId from dataSource", e); + } + return null; +} + +@Override +public void setProperties(Properties p) { + this.properties = p; +} +``` + +**② 获得数据库标识** + +`#getDatabaseId(DataSource dataSource)` 方法,代码如下: + +``` +// VendorDatabaseIdProvider.java + +@Override +public String getDatabaseId(DataSource dataSource) { + if (dataSource == null) { + throw new NullPointerException("dataSource cannot be null"); + } + try { + // 获得数据库标识 + return getDatabaseName(dataSource); + } catch (Exception e) { + log.error("Could not get a databaseId from dataSource", e); + } + return null; +} + +private String getDatabaseName(DataSource dataSource) throws SQLException { + // <1> 获得数据库产品名 + String productName = getDatabaseProductName(dataSource); + if (this.properties != null) { + for (Map.Entry property : properties.entrySet()) { + // 如果产品名包含 KEY ,则返回对应的 VALUE + if (productName.contains((String) property.getKey())) { + return (String) property.getValue(); + } + } + // no match, return null + return null; + } + // <3> 不存在 properties ,则直接返回 productName + return productName; +} +``` + +- `<1>` 处,调用 `#getDatabaseProductName(DataSource dataSource)` 方法,获得数据库产品名。代码如下: + + ``` + // VendorDatabaseIdProvider.java + + private String getDatabaseProductName(DataSource dataSource) throws SQLException { + try (Connection con = dataSource.getConnection()) { + // 获得数据库连接 + DatabaseMetaData metaData = con.getMetaData(); + // 获得数据库产品名 + return metaData.getDatabaseProductName(); + } + } + ``` + + - 通过从 Connection 获得数据库产品名。 + +- `<2>` 处,如果 `properties` 非空,则从 `properties` 中匹配 `KEY` ?若成功,则返回 `VALUE` ,否则,返回 `null` 。 + +- `<3>` 处,如果 `properties` 为空,则直接返回 `productName` 。 + +### 3.3.12 typeHandlerElement + +`#typeHandlerElement(XNode parent)` 方法,解析 `` 标签。代码如下: + +``` +// XMLConfigBuilder.java + +private void typeHandlerElement(XNode parent) throws Exception { + if (parent != null) { + // 遍历子节点 + for (XNode child : parent.getChildren()) { + // <1> 如果是 package 标签,则扫描该包 + if ("package".equals(child.getName())) { + String typeHandlerPackage = child.getStringAttribute("name"); + typeHandlerRegistry.register(typeHandlerPackage); + // <2> 如果是 typeHandler 标签,则注册该 typeHandler 信息 + } else { + // 获得 javaType、jdbcType、handler + String javaTypeName = child.getStringAttribute("javaType"); + String jdbcTypeName = child.getStringAttribute("jdbcType"); + String handlerTypeName = child.getStringAttribute("handler"); + Class javaTypeClass = resolveClass(javaTypeName); + JdbcType jdbcType = resolveJdbcType(jdbcTypeName); + Class typeHandlerClass = resolveClass(handlerTypeName); // 非空 + // 注册 typeHandler + if (javaTypeClass != null) { + if (jdbcType == null) { + typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); + } else { + typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); + } + } else { + typeHandlerRegistry.register(typeHandlerClass); + } + } + } + } +} +``` + +- 遍历子节点,分别处理 `<1>` 是 `` 和 `<2>` 是 `` 两种标签的情况。逻辑比较简单,最终都是注册到 `typeHandlerRegistry` 中。 + +### 3.3.13 mapperElement + +`#mapperElement(XNode context)` 方法,解析 `` 标签。代码如下: + +``` +// XMLConfigBuilder.java + +private void mapperElement(XNode parent) throws Exception { + if (parent != null) { + // <0> 遍历子节点 + for (XNode child : parent.getChildren()) { + // <1> 如果是 package 标签,则扫描该包 + if ("package".equals(child.getName())) { + // 获得包名 + String mapperPackage = child.getStringAttribute("name"); + // 添加到 configuration 中 + configuration.addMappers(mapperPackage); + // 如果是 mapper 标签, + } else { + // 获得 resource、url、class 属性 + String resource = child.getStringAttribute("resource"); + String url = child.getStringAttribute("url"); + String mapperClass = child.getStringAttribute("class"); + // <2> 使用相对于类路径的资源引用 + if (resource != null && url == null && mapperClass == null) { + ErrorContext.instance().resource(resource); + // 获得 resource 的 InputStream 对象 + InputStream inputStream = Resources.getResourceAsStream(resource); + // 创建 XMLMapperBuilder 对象 + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); + // 执行解析 + mapperParser.parse(); + // <3> 使用完全限定资源定位符(URL) + } else if (resource == null && url != null && mapperClass == null) { + ErrorContext.instance().resource(url); + // 获得 url 的 InputStream 对象 + InputStream inputStream = Resources.getUrlAsStream(url); + // 创建 XMLMapperBuilder 对象 + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); + // 执行解析 + mapperParser.parse(); + // <4> 使用映射器接口实现类的完全限定类名 + } else if (resource == null && url == null && mapperClass != null) { + // 获得 Mapper 接口 + Class mapperInterface = Resources.classForName(mapperClass); + // 添加到 configuration 中 + configuration.addMapper(mapperInterface); + } else { + throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); + } + } + } + } +} +``` + +- `<0>` 处,遍历子节点,处理每一个节点。根据节点情况,会分成 `<1>`、`<2>`、`<3>`、`<4>` 种情况,并且第一个是处理 `` 标签,后三个是处理 `` 标签。 + +- `<1>` 处,如果是 `` 标签,则获得 `name` 报名,并调用 `Configuration#addMappers(String packageName)` 方法,扫描该包下的所有 Mapper 接口。代码如下: + + ``` + // Configuration.java + + /** + * MapperRegistry 对象 + */ + protected final MapperRegistry mapperRegistry = new MapperRegistry(this); + + public void addMappers(String packageName) { + // 扫描该包下所有的 Mapper 接口,并添加到 mapperRegistry 中 + mapperRegistry.addMappers(packageName); + } + ``` + +- `<4>` 处,如果是 `mapperClass` 非空,则是使用映射器接口实现类的完全限定类名,则获得 Mapper 接口,并调用 `Configuration#addMapper(Class type)` 方法,直接添加到 `configuration` 中。代码如下: + + ``` + // Configuration.java + + public void addMapper(Class type) { + mapperRegistry.addMapper(type); + } + ``` + + - 实际上,`<1>` 和 `<4>` 是相似情况,差别在于前者需要扫描,才能获取到所有的 Mapper 接口,而后者明确知道是哪个 Mapper 接口。 + +- `<2>` 处,如果是 `resource` 非空,则是使用相对于类路径的资源引用,则需要创建 XMLMapperBuilder 对象,并调用 `XMLMapperBuilder#parse()` 方法,执行解析 Mapper XML 配置。执行之后,我们就能知道这个 Mapper XML 配置对应的 Mapper 接口。关于 XMLMapperBuilder 类,我们放在下一篇博客中,详细解析。 + +- `<3>` 处,如果是 `url` 非空,则是使用完全限定资源定位符(URL),情况和 `<2>` 是类似的。 + +# 666. 彩蛋 + +一篇体力活的文章,有点无趣哈。 + +参考和推荐如下文章: + +- 祖大俊 [《Mybatis3.3.x技术内幕(八):Mybatis初始化流程(上)》](https://my.oschina.net/zudajun/blog/668738) +- 田小波 [《MyBatis 源码分析 - 配置文件解析过程》](https://www.tianxiaobo.com/2018/07/20/MyBatis-源码分析-配置文件解析过程/) +- 无忌 [《MyBatis 源码解读之配置》](https://my.oschina.net/wenjinglian/blog/1833051) +- 徐郡明 [《MyBatis 技术内幕》](https://item.jd.com/12125531.html) 的 [「3.1 MyBatis 初始化」](https://svip.iocoder.cn/MyBatis/builder-package-1/#) 小节 \ No newline at end of file diff --git a/mybatis/16-mybatis-MyBatis 初始化(二)之加载 Mapper 映射配置文件.assets/01.png b/mybatis/16-mybatis-MyBatis 初始化(二)之加载 Mapper 映射配置文件.assets/01.png new file mode 100644 index 0000000..9fc30b2 Binary files /dev/null and b/mybatis/16-mybatis-MyBatis 初始化(二)之加载 Mapper 映射配置文件.assets/01.png differ diff --git a/mybatis/16-mybatis-MyBatis 初始化(二)之加载 Mapper 映射配置文件.assets/02.png b/mybatis/16-mybatis-MyBatis 初始化(二)之加载 Mapper 映射配置文件.assets/02.png new file mode 100644 index 0000000..9dd04f8 Binary files /dev/null and b/mybatis/16-mybatis-MyBatis 初始化(二)之加载 Mapper 映射配置文件.assets/02.png differ diff --git a/mybatis/16-mybatis-MyBatis 初始化(二)之加载 Mapper 映射配置文件.md b/mybatis/16-mybatis-MyBatis 初始化(二)之加载 Mapper 映射配置文件.md new file mode 100644 index 0000000..1e01255 --- /dev/null +++ b/mybatis/16-mybatis-MyBatis 初始化(二)之加载 Mapper 映射配置文件.md @@ -0,0 +1,1436 @@ +# 精尽 MyBatis 源码分析 —— MyBatis 初始化(二)之加载 Mapper 映射配置文件 + +# 1. 概述 + +本文接 [《精尽 MyBatis 源码分析 —— MyBatis 初始化(一)之加载 mybatis-config》](http://svip.iocoder.cn/MyBatis/builder-package-1) 一文,来分享 MyBatis 初始化的第二步,**加载 Mapper 映射配置文件**。而这个步骤的入口是 XMLMapperBuilder 。下面,我们一起来看看它的代码实现。 + +> FROM [《Mybatis3.3.x技术内幕(八):Mybatis初始化流程(上)》](https://my.oschina.net/zudajun/blog/668738) +> +> [![解析](16-mybatis-MyBatis 初始化(二)之加载 Mapper 映射配置文件.assets/01.png)](http://static.iocoder.cn/images/MyBatis/2020_02_13/01.png)解析 + +- 上图,就是 Mapper 映射配置文件的解析结果。 + +# 2. XMLMapperBuilder + +`org.apache.ibatis.builder.xml.XMLMapperBuilder` ,继承 BaseBuilder 抽象类,Mapper XML 配置构建器,主要负责解析 Mapper 映射配置文件。 + +## 2.1 构造方法 + +``` +// XMLMapperBuilder.java + +/** + * 基于 Java XPath 解析器 + */ +private final XPathParser parser; +/** + * Mapper 构造器助手 + */ +private final MapperBuilderAssistant builderAssistant; +/** + * 可被其他语句引用的可重用语句块的集合 + * + * 例如: ${alias}.id,${alias}.username,${alias}.password + */ +private final Map sqlFragments; +/** + * 资源引用的地址 + */ +private final String resource; + +public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map sqlFragments, String namespace) { + this(inputStream, configuration, resource, sqlFragments); + this.builderAssistant.setCurrentNamespace(namespace); +} + +public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map sqlFragments) { + this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()), + configuration, resource, sqlFragments); +} + +private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map sqlFragments) { + super(configuration); + // 创建 MapperBuilderAssistant 对象 + this.builderAssistant = new MapperBuilderAssistant(configuration, resource); + this.parser = parser; + this.sqlFragments = sqlFragments; + this.resource = resource; +} +``` + +- `builderAssistant` 属性,MapperBuilderAssistant 对象,是 XMLMapperBuilder 和 MapperAnnotationBuilder 的小助手,提供了一些公用的方法,例如创建 ParameterMap、MappedStatement 对象等等。关于 MapperBuilderAssistant 类,可见 [「3. MapperBuilderAssistant」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。 + +## 2.2 parse + +`#parse()` 方法,解析 Mapper XML 配置文件。代码如下: + +``` +// XMLMapperBuilder.java + +public void parse() { + // <1> 判断当前 Mapper 是否已经加载过 + if (!configuration.isResourceLoaded(resource)) { + // <2> 解析 `` 节点 + configurationElement(parser.evalNode("/mapper")); + // <3> 标记该 Mapper 已经加载过 + configuration.addLoadedResource(resource); + // <4> 绑定 Mapper + bindMapperForNamespace(); + } + + // <5> 解析待定的 节点 + parsePendingResultMaps(); + // <6> 解析待定的 节点 + parsePendingCacheRefs(); + // <7> 解析待定的 SQL 语句的节点 + parsePendingStatements(); +} +``` + +- `<1>` 处,调用 `Configuration#isResourceLoaded(String resource)` 方法,判断当前 Mapper 是否已经加载过。代码如下: + + ``` + // Configuration.java + + /** + * 已加载资源( Resource )集合 + */ + protected final Set loadedResources = new HashSet<>(); + + public boolean isResourceLoaded(String resource) { + return loadedResources.contains(resource); + } + ``` + +- `<3>` 处,调用 `Configuration#addLoadedResource(String resource)` 方法,标记该 Mapper 已经加载过。代码如下: + + ``` + // Configuration.java + + public void addLoadedResource(String resource) { + loadedResources.add(resource); + } + ``` + +- `<2>` 处,调用 `#configurationElement(XNode context)` 方法,解析 `` 节点。详细解析,见 [「2.3 configurationElement」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。 + +- `<4>` 处,调用 `#bindMapperForNamespace()` 方法,绑定 Mapper 。详细解析, + +- `<5>`、`<6>`、`<7>` 处,解析对应的**待定**的节点。详细解析,见 [「2.5 parsePendingXXX」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。 + +## 2.3 configurationElement + +`#configurationElement(XNode context)` 方法,解析 `` 节点。代码如下: + +``` +// XMLMapperBuilder.java + +private void configurationElement(XNode context) { + try { + // <1> 获得 namespace 属性 + String namespace = context.getStringAttribute("namespace"); + if (namespace == null || namespace.equals("")) { + throw new BuilderException("Mapper's namespace cannot be empty"); + } + // <1> 设置 namespace 属性 + builderAssistant.setCurrentNamespace(namespace); + // <2> 解析 节点 + cacheRefElement(context.evalNode("cache-ref")); + // <3> 解析 节点 + cacheElement(context.evalNode("cache")); + // 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。 + parameterMapElement(context.evalNodes("/mapper/parameterMap")); + // <4> 解析 节点们 + resultMapElements(context.evalNodes("/mapper/resultMap")); + // <5> 解析 节点们 + sqlElement(context.evalNodes("/mapper/sql")); + // <6> 解析 `、``、``、`` 节点们。详细解析,见 [「2.3.5 buildStatementFromContext」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。 + +### 2.3.1 cacheElement + +`#cacheRefElement(XNode context)` 方法,解析 `` 节点。代码如下: + +``` +// XMLMapperBuilder.java + +private void cacheRefElement(XNode context) { + if (context != null) { + // <1> 获得指向的 namespace 名字,并添加到 configuration 的 cacheRefMap 中 + configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); + // <2> 创建 CacheRefResolver 对象,并执行解析 + CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); + try { + cacheRefResolver.resolveCacheRef(); + } catch (IncompleteElementException e) { + // <3> 解析失败,添加到 configuration 的 incompleteCacheRefs 中 + configuration.addIncompleteCacheRef(cacheRefResolver); + } + } +} +``` + +- 示例如下: + + ``` + + ``` + +- `<1>` 处,获得指向的 `namespace` 名字,并调用 `Configuration#addCacheRef(String namespace, String referencedNamespace)` 方法,添加到 `configuration` 的 `cacheRefMap` 中。代码如下: + + ``` + // Configuration.java + + /** + * A map holds cache-ref relationship. The key is the namespace that + * references a cache bound to another namespace and the value is the + * namespace which the actual cache is bound to. + * + * Cache 指向的映射 + * + * @see #addCacheRef(String, String) + * @see org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheRefElement(XNode) + */ + protected final Map cacheRefMap = new HashMap<>(); + + public void addCacheRef(String namespace, String referencedNamespace) { + cacheRefMap.put(namespace, referencedNamespace); + } + ``` + +- `<2>` 处,创建 CacheRefResolver 对象,并调用 `CacheRefResolver#resolveCacheRef()` 方法,执行解析。关于 CacheRefResolver ,在 [「2.3.1.1 CacheRefResolver」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 详细解析。 + +- `<3>` 处,解析失败,因为此处指向的 Cache 对象可能未初始化,则先调用 `Configuration#addIncompleteCacheRef(CacheRefResolver incompleteCacheRef)` 方法,添加到 `configuration` 的 `incompleteCacheRefs` 中。代码如下: + + ``` + // Configuration.java + + /** + * CacheRefResolver 集合 + */ + protected final Collection incompleteCacheRefs = new LinkedList<>(); + + public void addIncompleteCacheRef(CacheRefResolver incompleteCacheRef) { + incompleteCacheRefs.add(incompleteCacheRef); + } + ``` + +#### 2.3.1.1 CacheRefResolver + +`org.apache.ibatis.builder.CacheRefResolver` ,Cache 指向解析器。代码如下: + +``` +// CacheRefResolver.java + +public class CacheRefResolver { + + private final MapperBuilderAssistant assistant; + /** + * Cache 指向的命名空间 + */ + private final String cacheRefNamespace; + + public CacheRefResolver(MapperBuilderAssistant assistant, String cacheRefNamespace) { + this.assistant = assistant; + this.cacheRefNamespace = cacheRefNamespace; + } + + public Cache resolveCacheRef() { + return assistant.useCacheRef(cacheRefNamespace); + } + +} +``` + +- 在 `#resolveCacheRef()` 方法中,会调用 `MapperBuilderAssistant#useCacheRef(String namespace)` 方法,获得指向的 Cache 对象。详细解析,见 [「3.3 useCacheRef」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。 + +### 2.3.2 cacheElement + +`#cacheElement(XNode context)` 方法,解析 `cache />` 标签。代码如下: + +``` +// XMLMapperBuilder.java + +private void cacheElement(XNode context) throws Exception { + if (context != null) { + // <1> 获得负责存储的 Cache 实现类 + String type = context.getStringAttribute("type", "PERPETUAL"); + Class typeClass = typeAliasRegistry.resolveAlias(type); + // <2> 获得负责过期的 Cache 实现类 + String eviction = context.getStringAttribute("eviction", "LRU"); + Class evictionClass = typeAliasRegistry.resolveAlias(eviction); + // <3> 获得 flushInterval、size、readWrite、blocking 属性 + Long flushInterval = context.getLongAttribute("flushInterval"); + Integer size = context.getIntAttribute("size"); + boolean readWrite = !context.getBooleanAttribute("readOnly", false); + boolean blocking = context.getBooleanAttribute("blocking", false); + // <4> 获得 Properties 属性 + Properties props = context.getChildrenAsProperties(); + // <5> 创建 Cache 对象 + builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); + } +} +``` + +- 示例如下: + + ``` + // 使用默认缓存 + + + // 使用自定义缓存 + + + + ``` + +- `<1>`、`<2>`、`<3>`、`<4>` 处,见代码注释即可。 + +- `<5>` 处,调用 `MapperBuilderAssistant#useNewCache(...)` 方法,创建 Cache 对象。详细解析,见 [「3.4 useNewCache」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 中。 + +### 2.3.3 resultMapElements + +> 老艿艿:开始高能,保持耐心。 + +整体流程如下: + +> FROM [《Mybatis3.3.x技术内幕(十):Mybatis初始化流程(下)》](https://my.oschina.net/zudajun/blog/669868) +> +> [![解析``](http://static.iocoder.cn/images/MyBatis/2020_02_13/03.png)](http://static.iocoder.cn/images/MyBatis/2020_02_13/03.png)解析`` + +`#resultMapElements(List list)` 方法,解析 `` 节点们。代码如下: + +``` +// XMLMapperBuilder.java + +// 解析 节点们 +private void resultMapElements(List list) throws Exception { + // 遍历 节点们 + for (XNode resultMapNode : list) { + try { + // 处理单个 节点 + resultMapElement(resultMapNode); + } catch (IncompleteElementException e) { + // ignore, it will be retried + } + } +} + +// 解析 节点 +private ResultMap resultMapElement(XNode resultMapNode) throws Exception { + return resultMapElement(resultMapNode, Collections.emptyList()); +} + +// 解析 节点 +private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings) throws Exception { + ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); + // <1> 获得 id 属性 + String id = resultMapNode.getStringAttribute("id", + resultMapNode.getValueBasedIdentifier()); + // <1> 获得 type 属性 + String type = resultMapNode.getStringAttribute("type", + resultMapNode.getStringAttribute("ofType", + resultMapNode.getStringAttribute("resultType", + resultMapNode.getStringAttribute("javaType")))); + // <1> 获得 extends 属性 + String extend = resultMapNode.getStringAttribute("extends"); + // <1> 获得 autoMapping 属性 + Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); + // <1> 解析 type 对应的类 + Class typeClass = resolveClass(type); + Discriminator discriminator = null; + // <2> 创建 ResultMapping 集合 + List resultMappings = new ArrayList<>(); + resultMappings.addAll(additionalResultMappings); + // <2> 遍历 的子节点 + List resultChildren = resultMapNode.getChildren(); + for (XNode resultChild : resultChildren) { + // <2.1> 处理 节点 + if ("constructor".equals(resultChild.getName())) { + processConstructorElement(resultChild, typeClass, resultMappings); + // <2.2> 处理 节点 + } else if ("discriminator".equals(resultChild.getName())) { + discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings); + // <2.3> 处理其它节点 + } else { + List flags = new ArrayList<>(); + if ("id".equals(resultChild.getName())) { + flags.add(ResultFlag.ID); + } + resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); + } + } + // <3> 创建 ResultMapResolver 对象,执行解析 + ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); + try { + return resultMapResolver.resolve(); + } catch (IncompleteElementException e) { + // <4> 解析失败,添加到 configuration 中 + configuration.addIncompleteResultMap(resultMapResolver); + throw e; + } +} +``` + +- `` 标签的解析,是相对复杂的过程,情况比较多,所以胖友碰到不懂的,可以看看 [《MyBatis 文档 —— Mapper XML 文件》](http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html) 文档。 + +- `<1>` 处,获得 `id`、`type`、`extends`、`autoMapping` 属性,并解析 `type` 对应的类型。 + +- ``` + <2> + ``` + + + + 处,创建 ResultMapping 集合,后遍历 + + + + ``` + + ``` + + + + 的 + + 子 + + 节点们,将每一个子节点解析成一个或多个 ResultMapping 对象,添加到集合中。即如下图所示: + + ![ResultMap 与 ResultMapping 的映射](16-mybatis-MyBatis 初始化(二)之加载 Mapper 映射配置文件.assets/02.png) + + ResultMap 与 ResultMapping 的映射 + + - `<2.1>` 处,调用 `#processConstructorElement(...)` 方法,处理 `` 节点。详细解析,见 [「2.3.3.1 processConstructorElement」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。 + - `<2.2>` 处,调用 `#processDiscriminatorElement(...)` 方法,处理 `` 节点。详细解析,见 [「2.3.3.2 processDiscriminatorElement」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。 + - `<2.3>` 处,调用 `#buildResultMappingFromContext(XNode context, Class resultType, List flags)` 方法,将当前子节点构建成 ResultMapping 对象,并添加到 `resultMappings` 中。详细解析,见 [「2.3.3.3 buildResultMappingFromContext」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。🌞 这一块,和 [「2.3.3.1 processConstructorElement」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 的 `<3>` 是一致的。 + +- `<3>` 处,创建 ResultMapResolver 对象,执行解析。关于 ResultMapResolver ,在 「2.3.1.1 CacheRefResolver」 详细解析。 + +- `<4>` 处,如果解析失败,说明有依赖的信息不全,所以调用 `Configuration#addIncompleteResultMap(ResultMapResolver resultMapResolver)` 方法,添加到 Configuration 的 `incompleteResultMaps` 中。代码如下: + + ``` + // Configuration.java + + /** + * ResultMapResolver 集合 + */ + protected final Collection incompleteResultMaps = new LinkedList<>(); + + public void addIncompleteResultMap(ResultMapResolver resultMapResolver) { + incompleteResultMaps.add(resultMapResolver); + } + ``` + +#### 2.3.3.1 processConstructorElement + +`#processConstructorElement(XNode resultChild, Class resultType, List resultMappings)` 方法,处理 `` 节点。代码如下: + +``` +// XMLMapperBuilder.java + +private void processConstructorElement(XNode resultChild, Class resultType, List resultMappings) throws Exception { + // <1> 遍历 的子节点们 + List argChildren = resultChild.getChildren(); + for (XNode argChild : argChildren) { + // <2> 获得 ResultFlag 集合 + List flags = new ArrayList<>(); + flags.add(ResultFlag.CONSTRUCTOR); + if ("idArg".equals(argChild.getName())) { + flags.add(ResultFlag.ID); + } + // <3> 将当前子节点构建成 ResultMapping 对象,并添加到 resultMappings 中 + resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags)); + } +} +``` + +- `<1>` 和 `<3>` 处,遍历 `` 的子节点们,调用 `#buildResultMappingFromContext(XNode context, Class resultType, List flags)` 方法,将当前子节点构建成 ResultMapping 对象,并添加到 `resultMappings` 中。详细解析,见 [「2.3.3.3 buildResultMappingFromContext」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。 + +- `<2>` 处,我们可以看到一个 `org.apache.ibatis.mapping.ResultFlag` 枚举类,结果标识。代码如下: + + ``` + // ResultFlag.java + + public enum ResultFlag { + + /** + * ID + */ + ID, + /** + * 构造方法 + */ + CONSTRUCTOR + + } + ``` + + - 具体的用途,见下文。 + +#### 2.3.3.2 processDiscriminatorElement + +`#processDiscriminatorElement(XNode context, Class resultType, List resultMappings)` 方法,处理 `` 节点。代码如下: + +``` +// XMLMapperBuilder.java + +private Discriminator processDiscriminatorElement(XNode context, Class resultType, List resultMappings) throws Exception { + // <1> 解析各种属性 + String column = context.getStringAttribute("column"); + String javaType = context.getStringAttribute("javaType"); + String jdbcType = context.getStringAttribute("jdbcType"); + String typeHandler = context.getStringAttribute("typeHandler"); + // <1> 解析各种属性对应的类 + Class javaTypeClass = resolveClass(javaType); + Class> typeHandlerClass = resolveClass(typeHandler); + JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType); + // <2> 遍历 的子节点,解析成 discriminatorMap 集合 + Map discriminatorMap = new HashMap<>(); + for (XNode caseChild : context.getChildren()) { + String value = caseChild.getStringAttribute("value"); + String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings)); // <2.1> + discriminatorMap.put(value, resultMap); + } + // <3> 创建 Discriminator 对象 + return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap); +} +``` + +- 可能大家对 `` 标签不是很熟悉,可以打开 [《MyBatis 文档 —— Mapper XML 文件》](http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html) 文档,然后下【鉴别器】。😈 当然,这块简单了解下就好,实际场景下,艿艿貌似都不知道它的存在,哈哈哈哈。 + +- `<1>` 处,解析各种属性以及属性对应的类。 + +- `<2>` 处,遍历 `` 的子节点,解析成 `discriminatorMap` 集合。 + +- `<2.1>` 处,如果是内嵌的 ResultMap 的情况,则调用 `#processNestedResultMappings(XNode context, List resultMappings)` 方法,处理**内嵌**的 ResultMap 的情况。代码如下: + + ``` + // XMLMapperBuilder.java + + private String processNestedResultMappings(XNode context, List resultMappings) throws Exception { + if ("association".equals(context.getName()) + || "collection".equals(context.getName()) + || "case".equals(context.getName())) { + if (context.getStringAttribute("select") == null) { + // 解析,并返回 ResultMap + ResultMap resultMap = resultMapElement(context, resultMappings); + return resultMap.getId(); + } + } + return null; + } + ``` + + - 该方法,会“递归”调用 `#resultMapElement(XNode context, List resultMappings)` 方法,处理内嵌的 ResultMap 的情况。也就是返回到 [「2.3.3 resultMapElement」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 流程。 + +- `<3>` 处,调用 `MapperBuilderAssistant#buildDiscriminator(...)` 方法,创建 Discriminator 对象。详细解析,见 [「3.6 buildDiscriminator」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。 + +#### 2.3.3.3 buildResultMappingFromContext + +`#buildResultMappingFromContext(XNode context, Class resultType, List flags)` 方法,将当前节点构建成 ResultMapping 对象。代码如下: + +``` +// XMLMapperBuilder.java + +private ResultMapping buildResultMappingFromContext(XNode context, Class resultType, List flags) throws Exception { + // <1> 获得各种属性 + String property; + if (flags.contains(ResultFlag.CONSTRUCTOR)) { + property = context.getStringAttribute("name"); + } else { + property = context.getStringAttribute("property"); + } + String column = context.getStringAttribute("column"); + String javaType = context.getStringAttribute("javaType"); + String jdbcType = context.getStringAttribute("jdbcType"); + String nestedSelect = context.getStringAttribute("select"); + String nestedResultMap = context.getStringAttribute("resultMap", + processNestedResultMappings(context, Collections.emptyList())); + String notNullColumn = context.getStringAttribute("notNullColumn"); + String columnPrefix = context.getStringAttribute("columnPrefix"); + String typeHandler = context.getStringAttribute("typeHandler"); + String resultSet = context.getStringAttribute("resultSet"); + String foreignColumn = context.getStringAttribute("foreignColumn"); + boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager")); + // <1> 获得各种属性对应的类 + Class javaTypeClass = resolveClass(javaType); + Class> typeHandlerClass = resolveClass(typeHandler); + JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType); + // <2> 构建 ResultMapping 对象 + return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy); +} +``` + +- `<1>` 处,解析各种属性以及属性对应的类。 +- `<2>` 处,调用 `MapperBuilderAssistant#buildResultMapping(...)` 方法,构建 ResultMapping 对象。详细解析,见 [「3.5 buildResultMapping」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。 + +#### 2.3.3.4 ResultMapResolver + +`org.apache.ibatis.builder.ResultMapResolver`,ResultMap 解析器。代码如下: + +``` +// ResultMapResolver.java + +public class ResultMapResolver { + + private final MapperBuilderAssistant assistant; + /** + * ResultMap 编号 + */ + private final String id; + /** + * 类型 + */ + private final Class type; + /** + * 继承自哪个 ResultMap + */ + private final String extend; + /** + * Discriminator 对象 + */ + private final Discriminator discriminator; + /** + * ResultMapping 集合 + */ + private final List resultMappings; + /** + * 是否自动匹配 + */ + private final Boolean autoMapping; + + public ResultMapResolver(MapperBuilderAssistant assistant, String id, Class type, String extend, Discriminator discriminator, List resultMappings, Boolean autoMapping) { + this.assistant = assistant; + this.id = id; + this.type = type; + this.extend = extend; + this.discriminator = discriminator; + this.resultMappings = resultMappings; + this.autoMapping = autoMapping; + } + + public ResultMap resolve() { + return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping); + } + +} +``` + +- 在 `#resolve()` 方法中,会调用 `MapperBuilderAssistant#addResultMap(...)` 方法,创建 ResultMap 对象。详细解析,见 [「3.7 addResultMap」](https://svip.iocoder.cn/MyBatis/builder-package-2/#) 。 + +### 2.3.4 sqlElement + +`#sqlElement(List list)` 方法,解析 `` 节点们。代码如下: + +``` +// XMLMapperBuilder.java + +private void sqlElement(List list) throws Exception { + if (configuration.getDatabaseId() != null) { + sqlElement(list, configuration.getDatabaseId()); + } + sqlElement(list, null); + // 上面两块代码,可以简写成 sqlElement(list, configuration.getDatabaseId()); +} + +private void sqlElement(List list, String requiredDatabaseId) throws Exception { + // <1> 遍历所有 节点 + for (XNode context : list) { + // <2> 获得 databaseId 属性 + String databaseId = context.getStringAttribute("databaseId"); + // <3> 获得完整的 id 属性,格式为 `${namespace}.${id}` 。 + String id = context.getStringAttribute("id"); + id = builderAssistant.applyCurrentNamespace(id, false); + // <4> 判断 databaseId 是否匹配 + if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { + // <5> 添加到 sqlFragments 中 + sqlFragments.put(id, context); + } + } +} +``` + +- `<1>` 处,遍历所有 `` 节点,逐个处理。 + +- `<2>` 处,获得 `databaseId` 属性。 + +- `<3>` 处,获得完整的 `id` 属性,格式为 `${namespace}.${id}` 。 + +- `<4>` 处,调用 `#databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId)` 方法,判断 `databaseId` 是否匹配。代码如下: + + ``` + // XMLMapperBuilder.java + + private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) { + // 如果不匹配,则返回 false + if (requiredDatabaseId != null) { + return requiredDatabaseId.equals(databaseId); + } else { + // 如果未设置 requiredDatabaseId ,但是 databaseId 存在,说明还是不匹配,则返回 false + // mmp ,写的好绕 + if (databaseId != null) { + return false; + } + // skip this fragment if there is a previous one with a not null databaseId + // 判断是否已经存在 + if (this.sqlFragments.containsKey(id)) { + XNode context = this.sqlFragments.get(id); + // 若存在,则判断原有的 sqlFragment 是否 databaseId 为空。因为,当前 databaseId 为空,这样两者才能匹配。 + return context.getStringAttribute("databaseId") == null; + } + } + return true; + } + ``` + +- `<5>` 处,添加到 `sqlFragments` 中。因为 `sqlFragments` 是来自 Configuration 的 `sqlFragments` 属性,所以相当于也被添加了。代码如下: + + ``` + // Configuration.java + + /** + * 可被其他语句引用的可重用语句块的集合 + * + * 例如: ${alias}.id,${alias}.username,${alias}.password + */ + protected final Map sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers"); + ``` + +### 2.3.5 buildStatementFromContext + +`#buildStatementFromContext(List list)` 方法,解析 ` 节点们 + for (XNode context : list) { + // <1> 创建 XMLStatementBuilder 对象,执行解析 + final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); + try { + statementParser.parseStatementNode(); + } catch (IncompleteElementException e) { + // <2> 解析失败,添加到 configuration 中 + configuration.addIncompleteStatement(statementParser); + } + } +} +``` + +- `<1>` 处,遍历 ` 节点们 + for (XNode context : list) { + // <1> 创建 XMLStatementBuilder 对象,执行解析 + final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); + try { + statementParser.parseStatementNode(); + } catch (IncompleteElementException e) { + // <2> 解析失败,添加到 configuration 中 + configuration.addIncompleteStatement(statementParser); + } + } +} +``` + +# 2. XMLStatementBuilder + +`org.apache.ibatis.builder.xml.XMLStatementBuilder` ,继承 BaseBuilder 抽象类,Statement XML 配置构建器,主要负责解析 Statement 配置,即 ` 标签 + */ +private final XNode context; +/** + * 要求的 databaseId + */ +private final String requiredDatabaseId; + +public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, String databaseId) { + super(configuration); + this.builderAssistant = builderAssistant; + this.context = context; + this.requiredDatabaseId = databaseId; +} +``` + +## 2.2 parseStatementNode + +`#parseStatementNode()` 方法,执行 Statement 解析。代码如下: + +``` +// XMLStatementBuilder.java + +public void parseStatementNode() { + // <1> 获得 id 属性,编号。 + String id = context.getStringAttribute("id"); + // <2> 获得 databaseId , 判断 databaseId 是否匹配 + String databaseId = context.getStringAttribute("databaseId"); + if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { + return; + } + + // <3> 获得各种属性 + Integer fetchSize = context.getIntAttribute("fetchSize"); + Integer timeout = context.getIntAttribute("timeout"); + String parameterMap = context.getStringAttribute("parameterMap"); + String parameterType = context.getStringAttribute("parameterType"); + Class parameterTypeClass = resolveClass(parameterType); + String resultMap = context.getStringAttribute("resultMap"); + String resultType = context.getStringAttribute("resultType"); + String lang = context.getStringAttribute("lang"); + + // <4> 获得 lang 对应的 LanguageDriver 对象 + LanguageDriver langDriver = getLanguageDriver(lang); + + // <5> 获得 resultType 对应的类 + Class resultTypeClass = resolveClass(resultType); + // <6> 获得 resultSet 对应的枚举值 + String resultSetType = context.getStringAttribute("resultSetType"); + ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); + // <7> 获得 statementType 对应的枚举值 + StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); + + // <8> 获得 SQL 对应的 SqlCommandType 枚举值 + String nodeName = context.getNode().getNodeName(); + SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); + // <9> 获得各种属性 + boolean isSelect = sqlCommandType == SqlCommandType.SELECT; + boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); + boolean useCache = context.getBooleanAttribute("useCache", isSelect); + boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); + + // Include Fragments before parsing + // <10> 创建 XMLIncludeTransformer 对象,并替换 标签相关的内容 + XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); + includeParser.applyIncludes(context.getNode()); + + // Parse selectKey after includes and remove them. + // <11> 解析 标签 + processSelectKeyNodes(id, parameterTypeClass, langDriver); + + // Parse the SQL (pre: and were parsed and removed) + // <12> 创建 SqlSource + SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); + // <13> 获得 KeyGenerator 对象 + String resultSets = context.getStringAttribute("resultSets"); + String keyProperty = context.getStringAttribute("keyProperty"); + String keyColumn = context.getStringAttribute("keyColumn"); + KeyGenerator keyGenerator; + // <13.1> 优先,从 configuration 中获得 KeyGenerator 对象。如果存在,意味着是 标签配置的 + String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; + keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); + if (configuration.hasKeyGenerator(keyStatementId)) { + keyGenerator = configuration.getKeyGenerator(keyStatementId); + // <13.2> 其次,根据标签属性的情况,判断是否使用对应的 Jdbc3KeyGenerator 或者 NoKeyGenerator 对象 + } else { + keyGenerator = context.getBooleanAttribute("useGeneratedKeys", // 优先,基于 useGeneratedKeys 属性判断 + configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) // 其次,基于全局的 useGeneratedKeys 配置 + 是否为插入语句类型 + ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; + } + + // 创建 MappedStatement 对象 + builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, + fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, + resultSetTypeEnum, flushCache, useCache, resultOrdered, + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); +} +``` + +- `<1>` 处,获得 `id` 属性,编号。 +- `<2>` 处,获得 `databaseId` 属性,并调用 `#databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId)` 方法,判断 `databaseId` 是否匹配。详细解析,见 [「2.3 databaseIdMatchesCurrent」](https://svip.iocoder.cn/MyBatis/builder-package-3/#) 。 +- `<3>` 处,获得各种属性。 +- `<4>` 处,调用 `#getLanguageDriver(String lang)` 方法,获得 `lang` 对应的 LanguageDriver 对象。详细解析,见 [「2.4 getLanguageDriver」](https://svip.iocoder.cn/MyBatis/builder-package-3/#) 。 +- `<5>` 处,获得 `resultType` 对应的类。 +- `<6>` 处,获得 `resultSet` 对应的枚举值。关于 `org.apache.ibatis.mapping.ResultSetType` 枚举类,点击[查看](https://github.com/YunaiV/mybatis-3/blob/master/src/main/java/org/apache/ibatis/mapping/ResultSetType.java)。一般情况下,不会设置该值。它是基于 `java.sql.ResultSet` 结果集的几种模式,感兴趣的话,可以看看 [《ResultSet 的 Type 属性》](http://jinguo.iteye.com/blog/365373) 。 +- `<7>` 处,获得 `statementType` 对应的枚举值。关于 `org.apache.ibatis.mapping.StatementType` 枚举类,点击[查看](https://github.com/YunaiV/mybatis-3/blob/master/src/main/java/org/apache/ibatis/mapping/StatementType.java)。 +- `<8>` 处,获得 SQL 对应的 SqlCommandType 枚举值。 +- `<9>` 处,获得各种属性。 +- `<10>` 处,创建 XMLIncludeTransformer 对象,并调用 `XMLIncludeTransformer#applyIncludes(Node source)` 方法,替换 `` 标签相关的内容。详细解析,见 [「3. XMLIncludeTransformer」](https://svip.iocoder.cn/MyBatis/builder-package-3/#) 。 +- `<11>` 处,调用 `#processSelectKeyNodes(String id, Class parameterTypeClass, LanguageDriver langDriver)` 方法,解析 `` 标签。详细解析,见 [「2.5 processSelectKeyNodes」](https://svip.iocoder.cn/MyBatis/builder-package-3/#) 。 +- `<12>` 处,调用 `LanguageDriver#createSqlSource(Configuration configuration, XNode script, Class parameterType)` 方法,创建 SqlSource 对象。详细解析,见后续文章。 +- `<13>` 处,获得 KeyGenerator 对象。分成 `<13.1>` 和 `<13.2>` 两种情况。具体的,胖友耐心看下代码注释。 +- `<14>` 处,调用 `MapperBuilderAssistant#addMappedStatement(...)` 方法,创建 MappedStatement 对象。详细解析,见 [「4.1 addMappedStatement」](https://svip.iocoder.cn/MyBatis/builder-package-3/#) 中。 + +## 2.3 databaseIdMatchesCurrent + +`#databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId)` 方法,判断 `databaseId` 是否匹配。代码如下: + +``` +// XMLStatementBuilder.java + +private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) { + // 如果不匹配,则返回 false + if (requiredDatabaseId != null) { + return requiredDatabaseId.equals(databaseId); + } else { + // 如果未设置 requiredDatabaseId ,但是 databaseId 存在,说明还是不匹配,则返回 false + // mmp ,写的好绕 + if (databaseId != null) { + return false; + } + // skip this statement if there is a previous one with a not null databaseId + // 判断是否已经存在 + id = builderAssistant.applyCurrentNamespace(id, false); + if (this.configuration.hasStatement(id, false)) { + MappedStatement previous = this.configuration.getMappedStatement(id, false); // issue #2 + // 若存在,则判断原有的 sqlFragment 是否 databaseId 为空。因为,当前 databaseId 为空,这样两者才能匹配。 + return previous.getDatabaseId() == null; + } + } + return true; +} +``` + +- 代码比较简单,胖友自己瞅瞅就得。从逻辑上,和我们在 XMLMapperBuilder 看到的同名方法 `#databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId)` 方法是一致的。 + +## 2.4 getLanguageDriver + +`#getLanguageDriver(String lang)` 方法,获得 `lang` 对应的 LanguageDriver 对象。代码如下: + +``` +// XMLStatementBuilder.java + +private LanguageDriver getLanguageDriver(String lang) { + // 解析 lang 对应的类 + Class langClass = null; + if (lang != null) { + langClass = resolveClass(lang); + } + // 获得 LanguageDriver 对象 + return builderAssistant.getLanguageDriver(langClass); +} +``` + +- 调用 `MapperBuilderAssistant#getLanguageDriver(lass langClass)` 方法,获得 LanguageDriver 对象。代码如下: + + ``` + // MapperBuilderAssistant.java + + public LanguageDriver getLanguageDriver(Class langClass) { + // 获得 langClass 类 + if (langClass != null) { + configuration.getLanguageRegistry().register(langClass); + } else { // 如果为空,则使用默认类 + langClass = configuration.getLanguageRegistry().getDefaultDriverClass(); + } + // 获得 LanguageDriver 对象 + return configuration.getLanguageRegistry().getDriver(langClass); + } + ``` + + - 关于 `org.apache.ibatis.scripting.LanguageDriverRegistry` 类,我们在后续的文章,详细解析。 + +## 2.5 processSelectKeyNodes + +`#processSelectKeyNodes(String id, Class parameterTypeClass, LanguageDriver langDriver)` 方法,解析 `` 标签。代码如下: + +``` +// XMLStatementBuilder.java + +private void processSelectKeyNodes(String id, Class parameterTypeClass, LanguageDriver langDriver) { + // <1> 获得 节点们 + List selectKeyNodes = context.evalNodes("selectKey"); + // <2> 执行解析 节点们 + if (configuration.getDatabaseId() != null) { + parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId()); + } + parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null); + // <3> 移除 节点们 + removeSelectKeyNodes(selectKeyNodes); +} +``` + +- `<1>` 处,获得 `` 节点们。 + +- `<2>` 处,调用 `#parseSelectKeyNodes(...)` 方法,执行解析 `` 节点们。详细解析,见 [「2.5.1 parseSelectKeyNodes」](https://svip.iocoder.cn/MyBatis/builder-package-3/#) 。 + +- `<3>` 处,调用 `#removeSelectKeyNodes(List selectKeyNodes)` 方法,移除 `` 节点们。代码如下: + + ``` + // XMLStatementBuilder.java + + private void removeSelectKeyNodes(List selectKeyNodes) { + for (XNode nodeToHandle : selectKeyNodes) { + nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode()); + } + } + ``` + +### 2.5.1 parseSelectKeyNodes + +`#parseSelectKeyNodes(String parentId, List list, Class parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId)` 方法,执行解析 `` 子节点们。代码如下: + +``` +// XMLStatementBuilder.java + +private void parseSelectKeyNodes(String parentId, List list, Class parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) { + // <1> 遍历 节点们 + for (XNode nodeToHandle : list) { + // <2> 获得完整 id ,格式为 `${id}!selectKey` + String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX; + // <3> 获得 databaseId , 判断 databaseId 是否匹配 + String databaseId = nodeToHandle.getStringAttribute("databaseId"); + if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) { + // <4> 执行解析单个 节点 + parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId); + } + } +} +``` + +- `<1>` 处,遍历 `` 节点们,逐个处理。 +- `<2>` 处,获得完整 `id` 编号,格式为 `${id}!selectKey` 。这里很重要,最终解析的 `` 节点,会创建成一个 MappedStatement 对象。而该对象的编号,就是 `id` 。 +- `<3>` 处,获得 `databaseId` ,并调用 `#databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId)` 方法,判断 `databaseId` 是否匹配。😈 通过此处,我们可以看到,即使有多个 `` 节点,但是最终只会有一个节点被解析,就是符合的 `databaseId` 对应的。因为不同的数据库实现不同,对于获取主键的方式也会不同。 +- `<4>` 处,调用 `#parseSelectKeyNode(...)` 方法,执行解析**单个** `` 节点。详细解析,见 [「2.5.2 parseSelectKeyNode」](https://svip.iocoder.cn/MyBatis/builder-package-3/#) 。 + +### 2.5.2 parseSelectKeyNode + +`#parseSelectKeyNode(...)` 方法,执行解析**单个** `` 节点。代码如下: + +``` +// XMLStatementBuilder.java + +private void parseSelectKeyNode(String id, XNode nodeToHandle, Class parameterTypeClass, LanguageDriver langDriver, String databaseId) { + // <1.1> 获得各种属性和对应的类 + String resultType = nodeToHandle.getStringAttribute("resultType"); + Class resultTypeClass = resolveClass(resultType); + StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString())); + String keyProperty = nodeToHandle.getStringAttribute("keyProperty"); + String keyColumn = nodeToHandle.getStringAttribute("keyColumn"); + boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER")); + + // defaults + // <1.2> 创建 MappedStatement 需要用到的默认值 + boolean useCache = false; + boolean resultOrdered = false; + KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; + Integer fetchSize = null; + Integer timeout = null; + boolean flushCache = false; + String parameterMap = null; + String resultMap = null; + ResultSetType resultSetTypeEnum = null; + + // <1.3> 创建 SqlSource 对象 + SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass); + SqlCommandType sqlCommandType = SqlCommandType.SELECT; + + // <1.4> 创建 MappedStatement 对象 + builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, + fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, + resultSetTypeEnum, flushCache, useCache, resultOrdered, + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null); + + // <2.1> 获得 SelectKeyGenerator 的编号,格式为 `${namespace}.${id}` + id = builderAssistant.applyCurrentNamespace(id, false); + // <2.2> 获得 MappedStatement 对象 + MappedStatement keyStatement = configuration.getMappedStatement(id, false); + // <2.3> 创建 SelectKeyGenerator 对象,并添加到 configuration 中 + configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore)); +} +``` + +- `<1.1>` 处理,获得各种属性和对应的类。 + +- `<1.2>` 处理,创建 MappedStatement 需要用到的默认值。 + +- `<1.3>` 处理,调用 `LanguageDriver#createSqlSource(Configuration configuration, XNode script, Class parameterType)` 方法,创建 SqlSource 对象。详细解析,见后续文章。 + +- `<1.4>` 处理,调用 `MapperBuilderAssistant#addMappedStatement(...)` 方法,创建 MappedStatement 对象。 + +- `<2.1>` 处理,获得 SelectKeyGenerator 的编号,格式为 `${namespace}.${id}` 。 + +- `<2.2>` 处理,获得 MappedStatement 对象。该对象,实际就是 `<1.4>` 处创建的 MappedStatement 对象。 + +- `<2.3>` 处理,调用 `Configuration#addKeyGenerator(String id, KeyGenerator keyGenerator)` 方法,创建 SelectKeyGenerator 对象,并添加到 `configuration` 中。代码如下: + + ``` + // Configuration.java + + /** + * KeyGenerator 的映射 + * + * KEY:在 {@link #mappedStatements} 的 KEY 的基础上,跟上 {@link SelectKeyGenerator#SELECT_KEY_SUFFIX} + */ + protected final Map keyGenerators = new StrictMap<>("Key Generators collection"); + + public void addKeyGenerator(String id, KeyGenerator keyGenerator) { + keyGenerators.put(id, keyGenerator); + } + ``` + +# 3. XMLIncludeTransformer + +`org.apache.ibatis.builder.xml.XMLIncludeTransformer` ,XML `` 标签的转换器,负责将 SQL 中的 `` 标签转换成对应的 `` 的内容。 + +## 3.1 构造方法 + +``` +// XMLIncludeTransformer.java + +private final Configuration configuration; +private final MapperBuilderAssistant builderAssistant; + +public XMLIncludeTransformer(Configuration configuration, MapperBuilderAssistant builderAssistant) { + this.configuration = configuration; + this.builderAssistant = builderAssistant; +} +``` + +## 3.2 applyIncludes + +`#applyIncludes(Node source)` 方法,将 `` 标签,替换成引用的 `` 。代码如下: + +``` +// XMLIncludeTransformer.java + +public void applyIncludes(Node source) { + // <1> 创建 variablesContext ,并将 configurationVariables 添加到其中 + Properties variablesContext = new Properties(); + Properties configurationVariables = configuration.getVariables(); + if (configurationVariables != null) { + variablesContext.putAll(configurationVariables); + } + // <2> 处理 + applyIncludes(source, variablesContext, false); +} +``` + +- `<1>` 处,创建 `variablesContext` ,并将 `configurationVariables` 添加到其中。这里的目的是,避免 `configurationVariables` 被下面使用时候,可能被修改。实际上,从下面的实现上,不存在这个情况。 +- `<2>` 处,调用 `#applyIncludes(Node source, final Properties variablesContext, boolean included)` 方法,处理 `` 。 + +------ + +`#applyIncludes(Node source, final Properties variablesContext, boolean included)` 方法,使用递归的方式,将 `` 标签,替换成引用的 `` 。代码如下: + +``` +// XMLIncludeTransformer.java + +private void applyIncludes(Node source, final Properties variablesContext, boolean included) { + // <1> 如果是 标签 + if (source.getNodeName().equals("include")) { + // <1.1> 获得 对应的节点 + Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); + // <1.2> 获得包含 标签内的属性 + Properties toIncludeContext = getVariablesContext(source, variablesContext); + // <1.3> 递归调用 #applyIncludes(...) 方法,继续替换。注意,此处是 对应的节点 + applyIncludes(toInclude, toIncludeContext, true); + if (toInclude.getOwnerDocument() != source.getOwnerDocument()) { // 这个情况,艿艿暂时没调试出来 + toInclude = source.getOwnerDocument().importNode(toInclude, true); + } + // <1.4> 将 节点替换成 节点 + source.getParentNode().replaceChild(toInclude, source); // 注意,这是一个奇葩的 API ,前者为 newNode ,后者为 oldNode + // <1.4> 将 子节点添加到 节点前面 + while (toInclude.hasChildNodes()) { + toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); // 这里有个点,一定要注意,卡了艿艿很久。当子节点添加到其它节点下面后,这个子节点会不见了,相当于是“移动操作” + } + // <1.4> 移除 标签自身 + toInclude.getParentNode().removeChild(toInclude); + // <2> 如果节点类型为 Node.ELEMENT_NODE + } else if (source.getNodeType() == Node.ELEMENT_NODE) { + // <2.1> 如果在处理 标签中,则替换其上的属性,例如 的情况,lang 属性是可以被替换的 + if (included && !variablesContext.isEmpty()) { + // replace variables in attribute values + NamedNodeMap attributes = source.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node attr = attributes.item(i); + attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext)); + } + } + // <2.2> 遍历子节点,递归调用 #applyIncludes(...) 方法,继续替换 + NodeList children = source.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + applyIncludes(children.item(i), variablesContext, included); + } + // <3> 如果在处理 标签中,并且节点类型为 Node.TEXT_NODE ,并且变量非空 + // 则进行变量的替换,并修改原节点 source + } else if (included && source.getNodeType() == Node.TEXT_NODE + && !variablesContext.isEmpty()) { + // replace variables in text node + source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext)); + } +} +``` + +- 这是个有**自递归逻辑**的方法,所以理解起来会有点绕,实际上还是蛮简单的。为了更好的解释,我们假设示例如下: + + ``` + // mybatis-config.xml + + + + + + + // Mapper.xml + + + ${cpu} + aoteman + qqqq + + + + ``` + +- 方法参数 `included` ,是否**正在**处理 `` 标签中。😈 一脸懵逼?不要方,继续往下看。 + +- 在上述示例的 + + + + ``` + + select + , + + from some_table t1 + cross join some_table t2 + + ``` + +# 4. MapperBuilderAssistant + +## 4.1 addMappedStatement + +`#addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class parameterType, String resultMap, Class resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets)` 方法,构建 MappedStatement 对象。代码如下: + +``` +// MapperBuilderAssistant.java + +public MappedStatement addMappedStatement( + String id, + SqlSource sqlSource, + StatementType statementType, + SqlCommandType sqlCommandType, + Integer fetchSize, + Integer timeout, + String parameterMap, + Class parameterType, + String resultMap, + Class resultType, + ResultSetType resultSetType, + boolean flushCache, + boolean useCache, + boolean resultOrdered, + KeyGenerator keyGenerator, + String keyProperty, + String keyColumn, + String databaseId, + LanguageDriver lang, + String resultSets) { + + // <1> 如果只想的 Cache 未解析,抛出 IncompleteElementException 异常 + if (unresolvedCacheRef) { + throw new IncompleteElementException("Cache-ref not yet resolved"); + } + + // <2> 获得 id 编号,格式为 `${namespace}.${id}` + id = applyCurrentNamespace(id, false); + boolean isSelect = sqlCommandType == SqlCommandType.SELECT; + + // <3> 创建 MappedStatement.Builder 对象 + MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType) + .resource(resource) + .fetchSize(fetchSize) + .timeout(timeout) + .statementType(statementType) + .keyGenerator(keyGenerator) + .keyProperty(keyProperty) + .keyColumn(keyColumn) + .databaseId(databaseId) + .lang(lang) + .resultOrdered(resultOrdered) + .resultSets(resultSets) + .resultMaps(getStatementResultMaps(resultMap, resultType, id)) // <3.1> 获得 ResultMap 集合 + .resultSetType(resultSetType) + .flushCacheRequired(valueOrDefault(flushCache, !isSelect)) + .useCache(valueOrDefault(useCache, isSelect)) + .cache(currentCache); + + // <3.2> 获得 ParameterMap ,并设置到 MappedStatement.Builder 中 + ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id); + if (statementParameterMap != null) { + statementBuilder.parameterMap(statementParameterMap); + } + + // <4> 创建 MappedStatement 对象 + MappedStatement statement = statementBuilder.build(); + // <5> 添加到 configuration 中 + configuration.addMappedStatement(statement); + return statement; +} +``` + +- `<1>` 处,如果只想的 Cache 未解析,抛出 IncompleteElementException 异常。 + +- `<2>` 处,获得 `id` 编号,格式为 `${namespace}.${id}` 。 + +- ``` + <3> + ``` + + + + 处,创建 MappedStatement.Builder 对象。详细解析,见 + + + + 「4.1.3 MappedStatement」 + + + + 。 + + - `<3.1>` 处,调用 `#getStatementResultMaps(...)` 方法,获得 ResultMap 集合。详细解析,见 [「4.1.3 getStatementResultMaps」](https://svip.iocoder.cn/MyBatis/builder-package-3/#) 。 + - `<3.2>` 处,调用 `#getStatementParameterMap(...)` 方法,获得 ParameterMap ,并设置到 MappedStatement.Builder 中。详细解析,见 [4.1.4 getStatementResultMaps」](https://svip.iocoder.cn/MyBatis/builder-package-3/#) 。 + +- `<4>` 处,创建 MappedStatement 对象。详细解析,见 [「4.1.1 MappedStatement」](https://svip.iocoder.cn/MyBatis/builder-package-3/#) 。 + +- `<5>` 处,调用 `Configuration#addMappedStatement(statement)` 方法,添加到 `configuration` 中。代码如下: + + ``` + // Configuration.java + + /** + * MappedStatement 映射 + * + * KEY:`${namespace}.${id}` + */ + protected final Map mappedStatements = new StrictMap<>("Mapped Statements collection"); + + public void addMappedStatement(MappedStatement ms) { + mappedStatements.put(ms.getId(), ms); + } + ``` + +### 4.1.1 MappedStatement + +`org.apache.ibatis.mapping.MappedStatement` ,映射的语句,每个 ` 等。 + * + * @param configuration The MyBatis configuration + * @param script XNode parsed from a XML file + * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null. + * @return + */ + SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType); + + /** + * Creates an {@link SqlSource} that will hold the statement read from an annotation. + * It is called during startup, when the mapped statement is read from a class or an xml file. + * + * 创建 SqlSource 对象,从方法注解配置,即 @Select 等。 + * + * @param configuration The MyBatis configuration + * @param script The content of the annotation + * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null. + * @return + */ + SqlSource createSqlSource(Configuration configuration, String script, Class parameterType); + +} +``` + +## 2.1 XMLLanguageDriver + +`org.apache.ibatis.scripting.xmltags.XMLLanguageDriver` ,实现 LanguageDriver 接口,XML 语言驱动实现类。 + +### 2.1.1 createParameterHandler + +`#createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql)` 方法,代码如下: + +``` +// XMLLanguageDriver.java + +@Override +public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { + // 创建 DefaultParameterHandler 对象 + return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql); +} +``` + +- 创建的是 DefaultParameterHandler 对象。详细解析,见 [《精尽 MyBatis 源码分析 —— SQL 初始化(下)之 SqlSource》](http://svip.iocoder.cn/MyBatis/scripting-2) 的 [「7.1 DefaultParameterHandler」](https://svip.iocoder.cn/MyBatis/scripting-1/#) 。 + +### 2.1.2 createSqlSource + +`#createSqlSource(Configuration configuration, XNode script, Class parameterType)` 方法,代码如下: + +``` +// XMLLanguageDriver.java + +@Override +public SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType) { + // 创建 XMLScriptBuilder 对象,执行解析 + XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); + return builder.parseScriptNode(); +} +``` + +- 创建 XMLScriptBuilder 对象,执行 `XMLScriptBuilder#parseScriptNode()` 方法,执行解析。详细解析,见 [「3. XMLScriptBuilder」](https://svip.iocoder.cn/MyBatis/scripting-1/#) 。 + +### 2.1.3 createSqlSource + +`#createSqlSource(Configuration configuration, String script, Class parameterType)` 方法,代码如下: + +``` +// XMLLanguageDriver.java + +@Override +public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType) { + // issue #3 + // <1> 如果是 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + mybatis-spring/src/main/java/org/mybatis/spring/config/mybatis-spring.xsd at master · YunaiV/mybatis-spring · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ Skip to content + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + +