Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

年初也转投 IDEA 阵营了,相比 Eclipse 确实顺畅很多,尤其是多开项目的时候,已经有种回不去的感觉了。这里介绍一些两个 IDE 之间的区别。

版本选择

首先是 IDEA 版本的选择,建议使用 Community 社区免费版即可,对比 Ultimate 收费版并无太大区别。都支持 Java、Maven、Git 等基本功能。但社区版会少了一些插件,例如 Spring Initializr。

版本区别详见:https://www.jetbrains.com/idea/features/editions_comparison_matrix.html

创建项目

其次创建项目,需要了解 IDEA 的 Project 和 Module 两个概念:

  • IntelliJ 系中的 Module 相当于 Eclipse 系中的 Project;
  • IntelliJ 系中的 Project 相当于 Eclipse 系中的 Workspace。可以选择创建一个 Empty project without modules,之后再放入同一类型的 module 集合,此时只会生成一个 .idea 目录。也可以选择直接创建某一类型的 Project,如 Maven Project,此时需要添加 Maven 项目信息,完成后会生成 pom.xml、src 目录、*.iml。

最后尝试新建一个 HelloWorld 示例项目,.idea 文件夹和 HelloWorld.iml 是 IDEA 帮助我们建立的辅助文件夹和文件,类似于 Eclipse 在我们的 Workspace 下建立的 .settings 文件夹和 .classpath.project 文件。

IDEA Hello World

Project 作为工作空间可以自由切换、多开,从 File > Open Recent 中选择即可。

配置 Tomcat

1、下载 Tomcat

2、IDEA 新建 Tomcat(Run > Edit Configurations)

add_new_tomcat_server_config

3、新建完毕,配置本地 Tomcat 安装目录:

tomcat_server_config

4、配置端口号、必要的 VM options:

tomcat_server_config2

5、配置运行项目,配置上下文:

deployment

创建 java/resource 目录

Ctrl+Alt+Shift+S,打开 Project Structure > Modules:

Project Structure > Modules

场景演示

以特性开发与合版这个场景为例,看下如何利用 IDEA 完成整个流程:

特性开发

第一步,本地建一个工作目录,以特性名称命名:

1
$ mkdir feature-xxx

第二步,从远程仓库 clone 该特性涉及的所有模块到该工作目录:

1
2
3
$ cd !$
$ git clone module1
$ git clone moduleN

第三步,新建 Project。打开 IDEA,File > New > Project from Existing Sources… > 选择该工作目录,并按如下操作:

Import Module

Import Module

第四步,开始特性开发,批量从 master 分支 checkout 创建出特性分支 feature-xxx。并批量修改 pom.xml 的版本号:

new_branch

特性合版

第一步,项目管理员创建发布分支,根据本次发布周期涉及的所有特性,批量从 master 分支 checkout 创建出发布分支 release-xxx(操作同上)。该操作只能由项目管理员操作,因为 release-* 是受保护分支。

第二步,升级 pom.xml 版本号并推送。

第三步,批量 merge 特性分支:

merge_into_current

第四步,解决冲突(例如 pom.xml 版本号冲突、代码冲突,如有):

conflicts

version_control_local_changes

第五步,合版完毕,Ctrl+K 批量提交并推送到远程分支 release-xxx 即可。

全局配置

File > Other Setting > Default Settings

全局配置 IDE 的界面、编辑器、VCS、构建、终端等

File > Other Setting > Default Project Structure

全局配置项目的 SDK 版本、类库等。

JDK

File > Other Setting > Default Settings,Projects SDK 可控制全局版本,Language Level 可控制语言级别,方便使用其特性。配置后全部 project modules 都会生效。

Maven

File > Other Setting > Default Settings > Build, Execution, Deployment > Build Tools > Maven

  • 可配置 Maven home directoryUser settings file (自定义 setting.xml)、Local repository 本地仓库。

  • 自动下载源码:Importing,找到Automatically download 并勾选 SourcesDocumentation,然后 reimport 即可。

Terminal

File > Other Setting > Default Settings > Terminal,修改 Shell path 为:D:\Developer\PortableGit\bin\sh.exe,可配置 Terminal 终端为 GitBash。

Layout

调整完当前窗口的布局之后,可以保存当前的窗口布局,以便全局生效:Window > Store Current Layout as Default

Code Template

File > Other Setting > Default Settings > 搜索 File and Code Templates,打开 Includes > File Header,配置:

1
2
3
4
5
6
7
8
/**
* <p>
*
* </p>
*
* @author abc@xyz.com
* @since ${DATE}
**/

Auto Import

File > Settings > Editor > General > Auto Import,然后勾选:

Add unambiguous imports on the fly:快速添加明确的导入。

Optimize imports on the fly:快速优化导入,优化的意思即自动帮助删除无用的导入。

Quick documentation

File > Settings > Editor > General > 开启 Show quick documentation on mouse move,可用于鼠标放到类、方法、变量上时显示完整 java doc 注释

Show tabs in one row

File > Settings > Editor > General > Editor Tabs,去掉勾选 Show tabs in one row

字体

老版本可以安装 JetBrains Mono 字体,v2019.3 版本后自带。

插件

官方插件(内置):

  • Maven Integration:官方 Maven 插件,参考:IDEA Maven 插件
  • JUnit:快速创建、运行、查看单元测试,在单元测试和目标之间跳转。

官方插件(需安装):

  • IdeaVim:Vim 程序员必装。
  • NodeJS:运行前端项目、or 构建前端项目时都是需要的。

规范类:

代码生成类:

  • Lombok Plugin:用于精简冗余代码,必装。
  • Builder-Generator:如果项目可以使用 Lombok,就不必装。

MyBatis:

  • MyBatis Plugin:最强大的 MyBatis 插件,不过是收费版。
  • Free MyBatis plugin:免费的 MyBatis 插件,提供了一些基本的跳转和代码生成功能。
  • MyBatis Log Plugin:把 MyBatis 输出的 SQL 日志还原成完整的 SQL 语句。将日志输出的 SQL 语句中的问号 ? 替换成真正的参数值。

辅助类:

  • Maven Helper:方便查询依赖树及排除依赖
  • Grep Console:Grep, tail, filter, highlight… everything you need for a console. Also can highlight the editor - nice for analyzing logs…
  • SequenceDiagram for IntelliJ IDEA:查看类调用时序图
  • Rainbow Brackets:彩色括号
  • HighlightBracketPair:高亮提示括号的开始结尾

其它:

IdeaVim 如果与原 IDEA 快捷键冲突,可以修改如下:

IdeaVim

如果被墙导致无法连接到 Marketplace 或插件无法下载,可以配置如下:

IDEA 无法下载插件

Git

分支管理

git branch management

分支操作 对应命令
Checkout git checkout [selected-branch]
New Branch from Selected… git checkout -b [new-branch] [selected-branch]
Checkout and Rebase onto Current git rebase HEAD [selected-branch]
Rebase Current onto Selected git rebase [selected-branch]
Merge into Current git merge [selected-branch]
Delete git branch -d [selected-branch]

快捷键

自定义 keymap

自定义快捷键,例如我配置了:

功能 快捷键
Close Alt+W
Hide All Tool Windows Alt+M
Delete Line Alt+D
Redo Ctrl+Y
Rename Alt+R
Completion Alt+/

VCS

Git 自定义快捷键:

功能 快捷键
Reset HEAD Ctrl+Alt+Q
Annotate(git blame) Ctrl+Alt+1
Compare with the Same Repository Version Ctrl+Alt+2
Show History Ctrl+Alt+3
Commit File Ctrl+Alt+4
PULL Ctrl+Alt+5

Git 默认的其它命令:

功能 快捷键
Add Ctrl+Alt+A
Revert Changes 回退更改 Ctrl+Alt+Z
Commit Changes 提交更改 Ctrl+K
Push Commits 推送 Ctrl+Shift+K
Update Project(批量 git pull) Ctrl+T

创建类

Alt + Insert 创造万物

查找/替换类

Ctrl + F 在当前文件中查找(关键字)

Ctrl + R 在当前文件中替换(关键字)

Ctrl + Shift + F 在路径中查找(关键字)

Ctrl + Shift + R 在路径中替换(关键字)

浏览类

Ctrl+N 查找类

Ctrl+Shift+N 查找文件

Shift+Shift 查找所有(包括类、资源、配置项、方法等等)

Ctrl+E 最近打开的文件

Alt+F7 查找引用(Find Usage)

Ctrl+Shift+Backspace 回到上次修改的地方

Ctrl+Alt+Left/Right 返回/前进至上次浏览的位置

Ctrl+鼠标左键:打开接口(Declaration)

Ctrl+Alt+鼠标左键:打开实现(Implementation(s))

Ctrl+H 打开类层次窗口(继承关系)

Ctrl+F12 查看当前类的所有方法

Ctrl+Alt+U 类关系图、Maven关系图

编辑类

Ctrl+D 复制行

Ctrl+Y 删除行

Alt+Shift+向上箭头 Move Line Up

Alt+Shift+向下箭头 Move Line Down

Ctrl+Shift+U 大小写转换

Ctrl+Alt+L 格式化代码

Ctrl+W、Ctrl+Shift+W 这个动作的实际操作是选中更上一层的语法结构。例如,如果你在一个字符串的一个单词中,按一下Ctrl+W,会选中光标所在单词。再按一下,会选中整个字符串的内容,不包括引号。再按一下,会选中包括引号的字符串。再按一下,会选中整个表达式(如果表达式含有括号,会逐层选中)。再按一下,会选中整个语句块。再按一下,会选中整个方法。再按一下,会选中整个类。

重构类

// Ctrl+Alt+Shift+T 重构菜单

// Ctrl+f6 修改类\方法签名
https://www.jetbrains.com/help/idea/change-class-signature.html

// F6 Move 类、方法、变量移动

// Ctrl+Shift+f6 重构变量的类型

// Shift+f6 重命类、方法、变量

// Ctrl+Alt+C 提取常量
// Ctrl+Alt+V 提取变量
// Ctrl+Alt+F 提取成员变量
// Ctrl+Alt+P 提取方法入参

// Refactor | Generify 泛型自动补全
// Refactor | Invert Boolean 布尔类型取反
// Refactor | Make Static 将类、方法设置成静态
// Refactor | Find and Replace Code Duplicates

// Safe Delete 安全删除

// Ctrl+Alt+M 方法提取
// Ctrl+alt+N 方法内联(变量\方法\构造函数)
https://www.jetbrains.com/help/idea/inline.html

// Pull Members Up 将方法提升到父类
// Pull Members Down 将方法推迟到之类

调试类

调试:F7/F8/F9 分别对应 Step into,Step over,Continue。

运行:Alt+Shift+F10 运行程序,Shift+F9 启动调试,Ctrl+F2 停止。

总结

IDEA 快捷键

摘录 10 个最常用的快捷键:

Top #10切来切去:Ctrl+Tab

Top #9选你所想:Ctrl+W

Top #8代码生成:Template/Postfix +Tab

Top #7发号施令:Ctrl+Shift+A

Top #6无处藏身:Shift+Shift

Top #5自动完成:Ctrl+Shift+Enter

Top #4创造万物:Alt+Insert

Top #3智能补全:Ctrl+Shift+Space

Top #2自我修复:Alt+Enter

Top #1重构一切:Ctrl+Shift+Alt+T

参考

下载地址:
http://www.jetbrains.com/idea/download/#section=windows

插件:
https://plugins.jetbrains.com/idea

常用插件推荐

IDEA 重构官方文档:
https://www.jetbrains.com/help/idea/refactoring-source-code.html

IDEA 高级调试技巧:

http://www.mamicode.com/info-detail-1761996.html

https://en.wikipedia.org/wiki/Cryptography

encryption_algorithm

加密散列函数

https://en.wikipedia.org/wiki/Cryptographic_hash_function

Cryptographic Hash Functions are cryptographic algorithms that are ways to generate and utilize specific keys to encrypt data for either symmetric or asymmetric encryption, and such functions may be viewed as keys themselves. They take a message of any length as input, and output a short, fixed-length hash, which can be used in (for example) a digital signature. For good hash functions, an attacker cannot find two messages that produce the same hash.

加密散列函数(Cryptographic Hash Function)又称为不可逆算法,使用单向散列函数实现。

加密散列函数一般用于生成消息摘要(Message Digest),可用于对数据进行签名和验签,以保证数据的:

  • 完整性(Integrity)
  • 真实性(Authenticity)
  • 不可抵赖性(Non-repudiation)

例如下图摘要(Digest)由 SHA-1 生成。SHA1 摘要长度为 20 Bytes (160 bit),一般用 40 位十六进制数表示(4 bit (2^4) = 1 位十六进制数,因此为 160 bit / 4 = 40 位十六进制数):

digest

两种破解方式:

Ideally, the only way to find a message that produces a given hash is to attempt a brute-force search of possible inputs to see if they produce a match, or use a rainbow table of matched hashes.

用途:

  • Verifying the integrity of messages and files

    Using a cryptographic hash and a chain of trust detects malicious changes to the file. Non-cryptographic error-detecting codes such as cyclic redundancy checks only prevent against non-malicious alterations of the file, since an intentional spoof can readily be crafted to have the colliding code value.

    • HMAC

      The MAC value protects a message’s data integrity, as well as its authenticity, by allowing verifiers (who also possess the secret key) to detect any changes to the message content.

  • Signature generation and verification

    数字签名作为维护数据信息安全的重要方法之一,可以解决伪造、抵赖、冒充和篡改等问题。其主要作用体现在以下几个方面:

    • 防冒充(身份认证)。在数字签名中,当使用私钥签名时,如果接收方或验证方用其公钥进行验证并获通过,那么可以肯定,签名人就是拥有私钥的那个人,因为私钥只有签名人拥有,其他人不可能冒充签名者。
    • 防伪造。其他人不能伪造对数据的签名,因为私钥只有签名者自己拥有,所以其他人不可能构造出正确的签名结果数据。
    • 防篡改。私钥加签后的摘要和数据一起发送给接收者,一旦信息被篡改,接收者可通过计算摘要和验证签名来判断该文件无效,从而保证了文件的完整性。
    • 防抵赖。数字签名既可以作为身份认证的依据,也可以作为签名者签名操作的证据。要防止接收者抵赖,可以在数字签名系统中要求接收者返回一个自己签名的表示收到的报文,给发送者或受信任第三方。如果接收者不返回任何消息,此次通信可终止或重新开始,签名方也没有任何损失,由此双方均不可抵赖。
    • 保密性。手写签字的文件一旦丢失,文件信息就极可能泄露。但在网络传输中,数字签名的公钥可以用于加密报文,以保证信息机密性。
  • Password verification

  • Proof-of-work

  • File or data identifier

    A message digest can also serve as a means of reliably identifying a file

    One of the main applications of a hash function is to allow the fast look-up of data in a hash table.

    • Being hash functions of a particular kind, cryptographic hash functions lend themselves well to this application too.
    • However, compared with standard hash functions, cryptographic hash functions tend to be much more expensive computationally. For this reason, they tend to be used in contexts where it is necessary for users to protect themselves against the possibility of forgery (the creation of data with the same digest as the expected data) by potentially malicious participants.

MD

Computes and checks MD2/MD4/MD5 message digest

Java 使用例子:https://www.baeldung.com/java-md5

SHA

Computes and checks SHA-1/SHA-2 message digests

Java 使用例子:https://www.baeldung.com/sha-256-hashing-java

对称加密算法

https://en.wikipedia.org/wiki/Symmetric-key_algorithm

Symmetric key encryption

3DES

Java 使用例子:https://www.baeldung.com/java-3des

AES

Java 使用例子:https://www.baeldung.com/java-secure-aes-key

Java 使用例子:https://www.baeldung.com/java-aes-encryption-decryption

非对称加密算法

https://en.wikipedia.org/wiki/Public-key_cryptography

公钥加密,私钥解密:

Public key encryption

私钥签名,公钥验签:

Private key signing

sign

RSA

Java 使用例子:https://www.baeldung.com/java-rsa

参考

科普 | 《码书:编码与解码的战争》:一场加解密的战争

图片理解数字签名和验签过程

日常工作中,经常需要关注互金及支付行业相关的政策法规及最新动向,本文整理了相关的资料,以供集中查阅,后续会持续更新:

央行支付类

编号 文件 解读 时间
261 号文 《中国人民银行关于加强支付结算管理防范电信网络新型违法犯罪有关事项的通知》 点我 2016.09.30 发布,2016.12.1 起实行
302 号文 《中国人民银行关于落实个人银行账户分类管理制度的通知》 点我 2016.11.25 晚间紧急发布
296 号文 《中国人民银行关于印发《条码支付业务规范(试行)》的通知》 点我 2017.12.25
217 号文 《关于进一步加强无证经营支付业务整治工作的通知》 点我 2017
281 号文 《关于规范支付创新业务通知》 点我 2017

互联网金融

全国性综合法律规范指引

文件 发文部门 发布时间
《关于开展 P2P 网络借贷机构合规检查工作的通知》 P2P 网贷风险专项整治工作领导小组办公室 2018.08.13
《网络借贷信息中介机构合规检查问题清单》 P2P 网贷风险专项整治工作领导小组办公室 2018.08.13
《互联网金融个体网络借贷电子合同安全规范》 中国互联网金融协会 2018.04.23
《互联网金融逾期债务催收自律公约(试行)》 中国互联网金融协会 2018.03.28
《关于做好 P2P 网络借贷风险专项整治改验收工作的通知》 P2P 网贷风险专项整治工作领导小组办公室 2017.12.13
《小额贷款公司网络业务风险专项整治实施方案》 P2P 网贷风险专项整治工作领导小组办公室 2017.12.08
《互联网金融个体络借贷资存管业务规范》 中国互联网金融协会 2017.12.07
《关于规范整顿 “现金贷 ”业务的通知》 P2P 网贷风险专项整治工作领导小组办公室 2017.12.01
《互联网金融个体网络借贷 借贷合同要素》 中国互联网金融协会 2017.09.21

地方性法律法规

广东省

文件 发布时间
广州市网络借贷信息中介机构业务退出指引(试行) 2018.07.30
广州互联网金融协会发布《关于下架计划类理财产品及打击逃废债行为的通知》 2018.07.27
深圳市关于积极稳妥应对当前 P2P 行业流动性风险及做好退出工作的通知 2018.07.13
广州市网络借贷信息中介机构整改验收工作方案 2018.03.21
广东省《关于贯彻落实网络借贷信息中介机构业务活动管理暂行办法的通知》 2018.02.11
广东省《网络借贷中介机构现场检查细则( 征求意见稿)》 2017.12.26
深圳市网络借贷信息中介机构业务退出指引 2017.09.29
深圳市网络借贷信息中介机构备案登记管理办法(征求意见稿) 2017.07.03

北京市

文件 发布时间
北京市网络借贷信息中介机构业务退出指引(草案) 2018.07.20
北京市加强业务合规性的风险提示函 2018.07.19
北京互金协会:关于防范化解 北京互金协会:关于防范化解 “多头借贷 ”、“高收益转贷 ”、“羊毛党”风险的提示函 2018.04.04

上海市

文件 发布时间
上海市网络借贷信息中介机构业务退出指导意见(试行) 2018.08.03
上海市规范整顿“现金贷”业务实施方案的通知 2017.01.13

附录

文件 发布时间
江苏省网络借贷信息中介机构备案登记管理暂行办法(征求意见稿) 2017.12.29
浙江省网络借贷信息中介机构备案登记管理实施细则(试行)(征求意见稿) 2017.12.18

电子合同

在合规备案的过程中,我主要负责了电子合同的工作。这里整理出一些相关的背景知识:

发展历程

电子签名的发展历程

中国电子签约法律法规

中国电子签约法律法规

2018年4月,中国互联网金融协会发布《互联网金融个体网络借贷电子合同安全规范》,从内容上看,主要分为电子签名合法性要求、电子合同订立、电子合同存储和司法举证要求四个部分,对互联网金融领域中个体网络借贷合同的电子签名的合法性,及电子合同订立、存储、举证,做出了进一步详细的规定。

电子合同样式

电子合同样式

并发编程时,集合操作必须小心谨慎。Java SE 提供了两种并发编程方法:

  • 方案一:为集合通用实现添加线程同步功能。Collections 工具类提供了称为同步包装器(synchronization wrappers)的静态工厂方法可用于向许多非线程同步的集合添加同步行为。
  • 方案二:使用并发集合。Java SE 提供了各种并发友好的集合接口和实现类。这些接口和类性能优于同步包装器(synchronization wrappers),提供了并发编程中经常需要的各种功能。

线程同步包装器

Collections 工具类提供如下静态工厂方法:

1
2
3
4
5
6
7
8
synchronizedCollection
synchronizedSet
synchronizedSortedSet
synchronizedNavigableSet
synchronizedList
synchronizedMap
synchronizedSortedMap
synchronizedNavigableMap

synchronizedCollection 静态方法为例,源码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Returns a synchronized (thread-safe) collection backed by the specified
* collection. In order to guarantee serial access, it is critical that
* <strong>all</strong> access to the backing collection is accomplished
* through the returned collection.<p>
*
* It is imperative that the user manually synchronize on the returned
* collection when traversing it via {@link Iterator}, {@link Spliterator}
* or {@link Stream}:
* <pre>
* Collection c = Collections.synchronizedCollection(myCollection);
* ...
* synchronized (c) {
* Iterator i = c.iterator(); // Must be in the synchronized block
* while (i.hasNext())
* foo(i.next());
* }
* </pre>
* Failure to follow this advice may result in non-deterministic behavior.
*
* <p>The returned collection does <i>not</i> pass the {@code hashCode}
* and {@code equals} operations through to the backing collection, but
* relies on {@code Object}'s equals and hashCode methods. This is
* necessary to preserve the contracts of these operations in the case
* that the backing collection is a set or a list.<p>
*
* The returned collection will be serializable if the specified collection
* is serializable.
*
* @param <T> the class of the objects in the collection
* @param c the collection to be "wrapped" in a synchronized collection.
* @return a synchronized view of the specified collection.
*/
public static <T> Collection<T> synchronizedCollection(Collection<T> c) {
return new SynchronizedCollection<>(c);
}

实现就是一行代码,返回一个静态内部类 SynchronizedCollection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 3053995032091335093L;

final Collection<E> c; // Backing Collection
final Object mutex; // Object on which to synchronize

SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}

SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = Objects.requireNonNull(c);
this.mutex = Objects.requireNonNull(mutex);
}

public int size() {
synchronized (mutex) {return c.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return c.isEmpty();}
}
public boolean contains(Object o) {
synchronized (mutex) {return c.contains(o);}
}
public Object[] toArray() {
synchronized (mutex) {return c.toArray();}
}
public <T> T[] toArray(T[] a) {
synchronized (mutex) {return c.toArray(a);}
}

public Iterator<E> iterator() {
return c.iterator(); // Must be manually synched by user!
}

public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
public boolean remove(Object o) {
synchronized (mutex) {return c.remove(o);}
}

public boolean containsAll(Collection<?> coll) {
synchronized (mutex) {return c.containsAll(coll);}
}
public boolean addAll(Collection<? extends E> coll) {
synchronized (mutex) {return c.addAll(coll);}
}
public boolean removeAll(Collection<?> coll) {
synchronized (mutex) {return c.removeAll(coll);}
}
public boolean retainAll(Collection<?> coll) {
synchronized (mutex) {return c.retainAll(coll);}
}
public void clear() {
synchronized (mutex) {c.clear();}
}
public String toString() {
synchronized (mutex) {return c.toString();}
}
// Override default methods in Collection
@Override
public void forEach(Consumer<? super E> consumer) {
synchronized (mutex) {c.forEach(consumer);}
}
@Override
public boolean removeIf(Predicate<? super E> filter) {
synchronized (mutex) {return c.removeIf(filter);}
}
@Override
public Spliterator<E> spliterator() {
return c.spliterator(); // Must be manually synched by user!
}
@Override
public Stream<E> stream() {
return c.stream(); // Must be manually synched by user!
}
@Override
public Stream<E> parallelStream() {
return c.parallelStream(); // Must be manually synched by user!
}
private void writeObject(ObjectOutputStream s) throws IOException {
synchronized (mutex) {s.defaultWriteObject();}
}
}

从源码可见,其实就是返回一个包装类,内部实现使用了 synchronized 关键字加 mutex 对象作为互斥锁,对被包装集合的所有增删改查方法加锁,相当暴力。

并发集合

并发集合作为 java.util.concurrent 包的一部分,提供的并发集合类如下:

concurrent_collections

阻塞队列

Queue_implementations

参考:《Java 集合框架系列(六)线性表之 Queue 总结

并发队列(非阻塞实现)

ConcurrentLinkedQueue

ConcurrentLinkedDeque

并发散列表

ConcurrentMap

  • ConcurrentHashMap

ConcurrentHashMap

并发跳表

ConcurrentSkipListSet

ConcurrentNavigableMap

  • ConcurrentSkipListMap

散列表

散列表的要点总结如下:

  • 数据元素的 key 和存储位置之间建立的对应关系 H 称为散列函数(Hash function)
  • key 通过散列函数获取存储位置的这种存储方式构造的存储结构称为散列表(Hash table)
  • 这一映射过程称为散列(Hash)
  • 如果选定了某个散列函数 H 及相应的散列表 L,则对每个数据元素 X,函数值 H(X.key) 就是 X 在散列表 L 中的存储位置,这个存储位置也称为散列地址(Hash code)
  • 理想情况下,应用的散列函数使每个 key 与散列地址是一一对应的,但在实际应用中,这种情况很少出现。设有散列函数 Hkey1key2,若 key1 ≠ key2,但是 H(key1) = H(key2),则称这种现象为散列沖突(Hash collision),且称 key1key2是相对于 H同义词
  • 因为,散列函数是从 key 集合到地址集合的映像,所以在一般情况下,冲突只能尽可能减少而不能完全避免。因此,采用散列技术时需要考虑两个问题:
    1. 如何构造(选择)“均匀的” 散列函数?
    2. 用什么方法有效地解決冲突?

HashTable Keynote

HashMap

节点设计

HashMap 的节点设计如下图:

Entry

接口和类 描述
java.util.Map#Entry Entry 接口定义了 getKeygetValuesetValueequalshashCode 五个待实现方法。
java.util.HashMap#Node 单链表节点实现,用于解决散列冲突
java.util.LinkedHashMap#Entry 单链表节点实现,并在此基础上增加了前驱节点 before、后继节点 after 以实现节点的顺序遍历
java.util.HashMap#TreeNode 红黑树节点实现,用于解决散列冲突。同时为了避免链表过长及散列表碰撞攻击,如果节点数超过 8 个,则进行树化 treeifyBin,避免大量散列冲突导致散列表退化成单链表,导致查询时间复杂度从 O(1) 退化成 O(n)。

单链表节点实现

单链表节点的散列表结构,简化如下图。其中

  • 散列函数(Hash function)为 index=hash(key) % 16
  • 散列冲突(Hash collision)的解决方案为:链地址法(separate chaining

HashTable Collision resolution

实际存储结构如下图,bucket 的 slot 指向的是一个个完整的 entry,如果有多个 entry(如 152),则逐一比较 entry 的 key 与查询中的 key 是否相同,相同则返回该 entry 的 value

HashTable Collision resolution

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue()))
return true;
}
return false;
}

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final String toString() { return key + "=" + value; }
}

红黑树节点实现

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;

TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}

......

}

常量及字段

HashMap_fields

常量值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* The default initial capacity - MUST be a power of two.
* 数组默认初始容量为 16,必须是 2 的 n 次幂
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* 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.
*
* 数组最大容量,,必须是 2 的 n 次幂,且小于等于 2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* The load factor used when none specified in constructor.
* 默认装载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
* 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;

/**
* 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;

/**
* 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;

字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 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<K,V>[] table;

/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K,V>> entrySet;

/**
* The number of key-value mappings contained in this map.
*
* 实际存储的键值对数量
*/
transient int size;

/**
* 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;

/**
* The next size value at which to resize (capacity * load factor).
*
* resize 操作的阈值(capacity * load factor)
*/
int threshold;

/**
* The load factor for the hash table.
*
* 装载因子
*/
final float loadFactor;

关键的几个字段:

  • table 散列表的底层数据结构为数组,其容量用 capacity 表示。此外,Java 通过链表法解决散列冲突的问题,因此数组类型为 Node<K,V>[] 节点数组。
  • loadFactor 散列表的装载因子。当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,会尽可能保证散列表中有一定比例的空闲槽位。我们用装载因子来表示散列表满的程度,默认值是 0.75f,也就是说默认情况下,当散列表中元素个数达到了容量的 3/4 时就会进行扩容。
  • threshold 散列表的扩容阈值,计算公式:threshold = capacity * load factor,即默认配置下的扩容阈值为:12=16*0.75
  • size 散列表实际存储的键值对数量,如果 size > threshold 则对 hash table 进行双倍扩容(resize),并对原数组每个元素进行重新散列(rehash)。
  • modCount 修改次数。

HashTable Dynamic resizing

构造方法

构造方法的参数主要用于指定数组的初始容量 initialCapacity、装载因子 loadFactor,并计算扩容阈值 threshold

HashMap_constructors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
// 参数校验及默认值设置
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);

// 装载因子及扩容阈值赋值
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}

初始容量

在集合初始化时,建议指定 initialCapacity 初始容量:

map_constructor

如果不想手工计算初始容量 initialCapacity,可以使用 Guava 的静态工厂方法 Maps.newHashMapWithExpectedSize,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* Creates a {@code HashMap} instance, with a high enough "initial capacity"
* that it <i>should</i> hold {@code expectedSize} elements without growth.
* This behavior cannot be broadly guaranteed, but it is observed to be true
* for OpenJDK 1.7. It also can't be guaranteed that the method isn't
* inadvertently <i>oversizing</i> the returned map.
*
* @param expectedSize the number of entries you expect to add to the
* returned map
* @return a new, empty {@code HashMap} with enough capacity to hold {@code
* expectedSize} entries without resizing
* @throws IllegalArgumentException if {@code expectedSize} is negative
*/
public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
return new HashMap<K, V>(capacity(expectedSize));
}

/**
* Returns a capacity that is sufficient to keep the map from being resized as
* long as it grows no larger than expectedSize and the load factor is >= its
* default (0.75).
*/
static int capacity(int expectedSize) {
if (expectedSize < 3) {
checkNonnegative(expectedSize, "expectedSize");
return expectedSize + 1;
}
if (expectedSize < Ints.MAX_POWER_OF_TWO) {
// This is the calculation used in JDK8 to resize when a putAll
// happens; it seems to be the most conservative calculation we
// can make. 0.75 is the default load factor.
return (int) ((float) expectedSize / 0.75F + 1.0F);
}
return Integer.MAX_VALUE; // any large value
}

不管是通过手工指定还是通过 Guava 的静态工厂方法,计算出来的初始容量都只是一个参考值。因为在随后 resize 会重新计算。

扩容阈值

第一个构造方法中调用了 tableSizeFor 方法,用于产生一个大于等于 initialCapacity 的最小的 2 的整数次幂。做法是通过右移操作让每一位都变为 1,最后 +1 变成 2 的 n 次幂:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

关键私有方法

hash

HashMap 源码中,散列函数是分三步走的:

hashcode

第一步,获取 keyhashcodehashCode 需要遵循以下规则:

equals_and_hashcode

扰动函数

第二步,hash 值的计算,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

该函数也叫扰动函数:先获取 32 位长度的 hashCode,再进行高 16 位与低 16 位异或,在低位加入高位特征,目的是减少碰撞冲突的概率。

参考:《HashMap 扰动函数解读

除留余数法

第三步,在更新、插入或删除的时候,计算 Key 被映射到哪个 bucket:

1
2
// capacity 表示 table.length
int index = hash(key) & (capacity - 1)

该按位与运算相当于通过取模法(除留余数法)计算存放的数组下标。

总结:

JDK HashMap 中 hash 函数的设计,确实很巧妙:

首先 hashcode 本身是个 32 位整型值,在系统中,这个值对于不同的对象必须保证唯一(JAVA 规范),这也是大家常说的,重写 equals 必须重写 hashcode 的重要原因。

获取对象的 hashcode 以后,先进行移位运算,然后再和自己做按位异或运算(^,即:hashcode ^ (hashcode >>> 16),这一步甚是巧妙,是将高 16 位右移(>>>到低 16 位,这样计算出来的整型值将“具有”高位和低位的性质。

最后,用 hash 表当前的容量减去一,再和刚刚计算出来的整型值做按位与运算(&。进行位与运算,很好理解,是为了计算出数组中的位置。但这里有个问题:

为什么要用容量减去一?

因为 A % B = A & (B - 1),注意这个公式只有当 B 为 2 的幂次方时才有效,所以 HashMap 中的数组容量要求必须是 2 的幂次方。

最后,(h ^ (h >>> 16)) & (capitity -1) = (h ^ (h >>> 16)) % capitity,可以看出这里本质上是使用了「除留余数法」

综上,可以看出,hashcode 的随机性,加上移位异或算法,得到一个非常随机的 hash 值,再通过「除留余数法」,得到 index。

整个散列函数的整体过程如下:

1
2
3
4
5
int hash(Object key) {
int h = key.hashCode();
h = h ^ (h >>> 16);
return h & (capitity - 1); //capicity 表示散列表的大小
}

putVal

putVal 的核心流程如下:

putval_of_hashmap

一些关键代码分析:

  • 散列表只在首次设值时,才初始化;
  • 散列函数:(n - 1) & hash,通过取模法计算存放的数组下标。通过该散列函数将元素的键值(key)映射为数组下标,然后将数据存储在数组中对应的下标位置。当我们按照键值查询元素时,用同样的散列函数,将键值转化为数组下标,从对应的数组下标的位置取数据。因此散列函数在散列表中起着非常关键的作用。
  • 为了避免散列值冲突,除了对比散列值是否相等之外,还需要对比 key 是否相等;
  • 散列冲突解决方法:链表法。同时为了避免链表过长及散列表碰撞攻击,如果节点数超过 8 个,则进行树化 treeifyBin,避免大量散列冲突导致散列表退化成单链表,导致查询时间复杂度从 O(1) 退化成 O(n)
  • 如果实际映射数量超过阈值,则进行 resize 扩容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab 表示底层数组
// p 表示头结点
// n 表示数组长度
// i 表示数组下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 节点数组为空,则首次初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;

// 计算数组下标,并获取指定下标元素,如果为空,表示无散列冲突,则创建节点并放入指定下标对应的桶
// 无散列冲突情况:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 有散列冲突情况:
else {
// e 表示当前节点
Node<K,V> e; K k;
// 判断头结点是否为重复键,如果是则后续覆盖值
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果是根节点是树节点类型,则进行相关操作
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 否则就是单链表节点
else {
// 遍历单链表
for (int binCount = 0; ; ++binCount) {
// 找到尾结点
if ((e = p.next) == null) {
// 创建新节点并追加到单链表末尾
p.next = newNode(hash, key, value, null);
// 如果节点数超过 8 个,则进行树化,避免大量散列冲突导致散列表退化成单链表,导致查询时间复杂度从 0(1) 退化成 0(n)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 树化操作可能会引起数组容量翻倍、扩容阈值翻倍
treeifyBin(tab, hash);
break;
}
// 如果桶中的节点有重复键,则后续覆盖值
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果存在重复键,则覆盖值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 实际数量加一。同时如果超过阈值,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

树化前,会先判断数组容量是否达到阈值 MIN_TREEIFY_CAPACITY = 64,如果是则将指定散列值对应数组下标的桶中所有链表节点都替换为红黑树节点,否则进行 resize 扩容操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> 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);
}
}

getNode

关键的 getNode 查找节点方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 判断散列表不为空,且通过散列函数(hash % n)得到的头节点存在
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 如是,且头节点散列值、key 都匹配,则返回该命中节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 否则遍历下一个节点
if ((e = first.next) != null) {
// 树节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 顺序遍历单链表,查找匹配结果
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

removeNode

关键的 removeNode 删除节点方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* Implements Map.remove and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;

++modCount;
--size;
afterNodeRemoval(node);

return node;
}
}
return null;
}

resize

用于初始化或扩容散列表。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
...
}

常用方法

设值

主要使用到了关键的 hashputValresize 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Override
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}

/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map.
*
* @param m mappings to be stored in this map
* @throws NullPointerException if the specified map is null
*/
@Override
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}

/**
* Implements Map.putAll and Map constructor
*
* @param m the map
* @param evict false when initially constructing this map, else
* true (relayed to method afterNodeInsertion).
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}

查找和替换

下面这组方法都使用到了关键的 getNode 方法查找指定节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Override
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}


@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}

@Override
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}

@Override
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}

@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}

@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}

删除节点

1
2
3
4
5
6
7
8
9
10
11
@Override
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}

@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}

清空 map

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void clear() {
Node<K,V>[] tab;
modCount++;

// 内部数组不为空,则遍历并清空所有元素
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}

参考

https://en.wikipedia.org/wiki/Hash_table

散列函数设计:除留余数法

Java中hash算法细述

HashMap的负载因子不设置成1?

HashMap的loadFactor为什么是0.75?

自问自答 - HashMap的底层原理探索

java.util.Map 用于映射键值对(map keys to values)。

本文总结了几种 Map 的使用场景,例如:

  • 如何实现有序遍历;
  • 如何实现缓存淘汰策略;
  • 如何解决 Top K 问题。

创建 Map

使用 Guava 快速创建不可变的 Map:

1
Map<String, String> map = ImmutableMap.of("A", "Apple", "B", "Boy", "C", "Cat");

使用 Guava 快速创建指定 initialCapacity 初始容量的 Map:

1
Maps.newHashMapWithExpectedSize(10);

Guava 的 Maps 还提供了更多的 API,可以自行研究使用。

实现有序遍历

集合视图

Map 接口提供了三种集合视图(collection views):

Map Views

注意:从 keySet() 返回类型可知,由于受限于 Set 的三大特性之一「互异性」,Map 不能包含重复的键(Key)。两个键重复与否取决于 equalshashCode() 方法。

Many methods in Collections Framework interfaces are defined in terms of the equals method. For example, the specification for the containsKey(Object key) method says: “returns true if and only if this map contains a mapping for a key k such that (key==null ? k==null : key.equals(k)).” This specification should not be construed to imply that invoking Map.containsKey with a non-null argument key will cause key.equals(k) to be invoked for any key k. Implementations are free to implement optimizations whereby the equals invocation is avoided, for example, by first comparing the hash codes of the two keys. (The Object.hashCode() specification guarantees that two objects with unequal hash codes cannot be equal.) More generally, implementations of the various Collections Framework interfaces are free to take advantage of the specified behavior of underlying Object methods wherever the implementor deems it appropriate.

hashCode 需要遵循以下规则:

equals_and_hashcode

遍历 API

Java 提供了下面几种 API 用于遍历 Map 接口的三种集合视图(collection views):

The order of a map is defined as the order in which the iterators on the map’s collection views return their elements. Some map implementations, like the TreeMap class, make specific guarantees as to their order; others, like the HashMap class, do not.

内部循环 API

1
2
// key 遍历
map.forEach((key, value) -> {});

外部循环 API

1
2
3
4
5
6
7
8
// key 遍历
for (String key : map.keySet()) {}

// value 遍历
for (String value : map.values()) {}

// entry 遍历
for (Map.Entry<String, String> entry : map.entrySet()) {}
1
2
3
4
5
6
7
8
// entry 显式迭代器
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
}

// foreach 循环增强,不再需要显式迭代器,简化迭代集合操作
for (Map.Entry<String, String> entry : map.entrySet()) {}

map_entryset

遍历顺序

遍历顺序取决于 Map 接口使用了何种数据结构实现。

众所周知,散列表这种动态数据结构虽然支持非常高效的数据插入、删除、查找操作(平均时间复杂度都为常数阶 O(1)),但由于散列表中的数据都是通过散列函数运算之后无序存储的,因此散列表的遍历结果也是无序的,例如 HashMap

要实现某种有序遍历的解决方案:

  • 排序算法。每当我们希望按照某种顺序遍历散列表中的数据时,自行将散列表中的数据拷贝到数组中,然后通过某种排序算法完成排序,再进行遍历。但如此一来,效率势必会很低。

  • 换一种数据结构实现,例如:红黑树(TreeMap)。

  • 设计一种复合型数据结构,例如将散列表与链式队列结合在一起(LinkedHashMap),实现某种排序,从而达到有序遍历。

无序(no order)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* HashMap 按散列函数(取余运算)后获得的索引顺序进行存储
*
* (32, 1), 32 % 16 = 0
* (16, 2), 16 % 16 = 0
* (65, 4), 65 % 16 = 1
* (4, 5), 4 % 16 = 4
* (15, 3), 15 % 16 = 15
*/
@Test
public void testHashMap() {
Map<Integer, String> map = new HashMap<>(16);
map.put(32, "1");
map.put(16, "2");
map.put(15, "3");
map.put(65, "4");
map.put(4, "5");
map.forEach((key, value) -> log.info("({}, {}), {} % 16 = {}", key, value, key, key % 16));
}

按自然顺序(natual order)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* TreeMap 按自然顺序进行排序(红黑树实现,查找的时间复杂度为 O(log(n)))
*
* (4, 5)
* (15, 3)
* (16, 2)
* (32, 1)
* (65, 4)
*/
@Test
public void testTreeMap() {
// Map<Integer, String> map = new TreeMap<>((o1, o2) -> o1.compareTo(o2));
// Map<Integer, String> map = new TreeMap<>(Comparator.naturalOrder());
Map<Integer, String> map = new TreeMap<>();
map.put(32, "1");
map.put(16, "2");
map.put(15, "3");
map.put(65, "4");
map.put(4, "5");
map.forEach((key, value) -> log.info("({}, {})", key, value));
}

排序由键(Key)的自然顺序决定,通过 ComparatorComparable

按插入顺序(insertion order)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* LinkedHashMap 大部分构造方法 默认按插入顺序进行排序(accessOrder=false)。底层维护了一个链式队列,可用于实现 FIFO 缓存淘汰策略
*
* (32, 1)
* (16, 2)
* (15, 3)
* (65, 4)
* (4, 5)
*/
@Test
public void testLinkedHashMap() {
Map<Integer, String> map = new LinkedHashMap<>(16);
map.put(32, "1");
map.put(16, "2");
map.put(15, "3");
map.put(65, "4");
map.put(4, "5");
map.forEach((key, value) -> log.info("({}, {})", key, value));
}

按访问顺序(access order)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* LinkedHashMap 唯一一个构造方法 用于按访问顺序进行排序(accessOrder=true)。底层维护了一个链式队列,可用于实现 LRU 缓存淘汰策略
* (15, 3)
* (65, 4)
* (4, 5)
* (16, 2)
* (32, 1)
*/
@Test
public void testLinkedHashMap2() {
Map<Integer, String> map = new LinkedHashMap<>(16, .75F, true);
map.put(32, "1");
map.put(16, "2");
map.put(15, "3");
map.put(65, "4");
map.put(4, "5");

map.get(16);
map.get(32);

map.forEach((key, value) -> log.info("({}, {})", key, value));
}

数据结构实现

HashMap

参考:《Map 接口的散列表实现总结

TreeMap

红黑树实现。

LinkedHashMap

LinkedHashMap 继承自 HashMap,是一种复合型数据结构。它在散列表的基础上,通过维护一个有界队列(双向链表实现)来实现「键值对」排序。

注意:LinkedHashMap 中的“Linked”实际上是指「链式队列」,而非指用链表法解决散列冲突。

这种行为适用于一些特定应用场景,例如:构建一个空间占用敏感的有限资源池,按某种淘汰策略自动淘汰「过期」元素:

排序方式 使用场景
按插入顺序(accessOrder = false 实现 FIFO 缓存淘汰策略
按访问顺序(accessOrder = true 实现 LRU 缓存淘汰策略

通过源码分析,LinkedHashMap 继承自 HashMap,同时 LinkedHashMap 的节点 Entry 也继承自 HashMapNode,并且在此基础上增加了两个属性:

  • 前驱节点 Entry<K, V> before
  • 后继节点 Entry<K, V> after

LinkedHashMap Entry

通过这两个属性就可以维护一条有序排列的双向链表,如下图:

LinkedHashMap Entry

实现缓存淘汰策略

FIFO (First In First Out)

FIFO (First In First Out)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* (1: one) removed
* (2: two) removed
* (3, three) hit
* (4, four) hit
* (5, five) hit
*/
@Test
public void FIFO() {
Map<Integer, String> map = new LinkedHashMap<Integer, String>(3, .75F, false) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
if (size() > 3) {
log.info("({}: {}) removed", eldest.getKey(), eldest.getValue());
return true;
} else {
return false;
}
}
};
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.get(1);
map.put(4, "four");
map.get(3);
map.put(5, "five");
map.forEach((key, value) -> log.info("({}, {}) hit", key, value));
}

LRU (Least Recently Used)

LRU (Least Recently Used)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* (2: two) removed
* (1: one) removed
* (4, four) hit
* (3, three) hit
* (5, five) hit
*/
@Test
public void LRU() {
Map<Integer, String> map = new LinkedHashMap<Integer, String>(3, .75F, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
if (size() > 3) {
log.info("({}: {}) removed", eldest.getKey(), eldest.getValue());
return true;
} else {
return false;
}
}
};
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.get(1);
map.put(4, "four");
map.get(3);
map.put(5, "five");
map.forEach((key, value) -> log.info("({}, {}) hit", key, value));
}

LFU (Least Frequently Used)

LFU (Least Frequently Used)

实现合并

Modifier and Type Method and Description
default V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)
If the specified key is not already associated with a value or is associated with null, associates it with the given non-null value.

Merging Two Maps with Java 8

实现计数器

Java 8 为 Map 接口引入了一组新的 default 默认方法,用于简化 Map 的日常使用。API 如下:

Modifier and Type Method and Description
default V putIfAbsent(K key, V value)
If the specified key is not already associated with a value (or is mapped to null) associates it with the given value and returns null, else returns the current value.
default V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)
If the specified key is not already associated with a value (or is mapped to null), attempts to compute its value using the given mapping function and enters it into this map unless null.
default V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)
If the value for the specified key is present and non-null, attempts to compute a new mapping given the key and its current mapped value.
default V compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)
Attempts to compute a mapping for the specified key and its current mapped value (or null if there is no current mapping).

要实现类似 Redis 散列表的原子递增命令 HINCRBY key field increment 的效果,使用 compute 实现的代码,对比传统代码更紧凑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static final Map<String, Integer> IP_STATS = new HashMap<>();

// 老版本
public synchronized int oldIpStats(String ip) {
if (!IP_STATS.containsKey(ip)) {
IP_STATS.put(ip, 1);
} else {
IP_STATS.put(ip, IP_STATS.get(ip) + 1);
}
return IP_STATS.get(ip);
}

// 新版本
public synchronized int newIpStats(String ip) {
return IP_STATS.compute(ip, (key, oldValue) -> {
if (oldValue == null) {
return 1;
} else {
return oldValue + 1;
}
});
}

最终结果:

1
2
3
4
5
6
// result is 1
log.info("result is {}", newIpStats("127.0.0.1"));
// result is 2
log.info("result is {}", newIpStats("127.0.0.1"));
// result is 3
log.info("result is {}", newIpStats("127.0.0.1"));

解决 Top K 问题

解题思路:

  • 数据规模大的,就先分而治之(hash 映射);
  • 数据规模小的:
    • 首先,按「键值对」保存统计数据(key 为关键字,value 为统计次数)
    • 然后,按「值」倒序
    • 最后,取 Top K 个「键」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 统计 N 个随机数中最热门的 K 个
*/
@Test
public void topK() {
int N = 100;
// 「键值对」底层实现使用散列表即可,因为无须有序
Map<Integer, Integer> numStats = Maps.newHashMapWithExpectedSize(N);

Random random = new Random();
for (int i = 0; i < N; i++) {
int num = random.nextInt(10);
// 首先,按「键值对」保存统计数据(key 为关键字,value 为统计次数)
numStats.compute(num, (key, oldVal) -> {
if (oldVal == null) {
return 1;
} else {
return oldVal + 1;
}
});
}
log.info("Before sort:");
numStats.forEach((key, val) -> log.info("({}, {})", key, val));

log.info("After sorted by statistics desc:");
// 然后,按「值」倒序
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(numStats.entrySet());
entries.sort(Map.Entry.comparingByValue(Comparator.reverseOrder()));
// 最后,取 Top K 个「键」
entries.forEach(entry -> log.info("({}, {})", entry.getKey(), entry.getValue()));
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Before sort:
(0, 12)
(1, 15)
(2, 6)
(3, 8)
(4, 12)
(5, 16)
(6, 5)
(7, 10)
(8, 9)
(9, 7)

After sorted by statistics desc:
(5, 16)
(1, 15)
(0, 12)
(4, 12)
(7, 10)
(8, 9)
(3, 8)
(9, 7)
(2, 6)
(6, 5)

解码 Huffman coding

参考维基百科 Huffman coding 的定义:

In computer science and information theory, a Huffman code is a particular type of optimal prefix code that is commonly used for lossless data compression.

前缀编码(prefix code) 的定义:

如果在一个编码方案中,任何一个编码都不是其它任何编码的前缀(最左子串),则称该编码是前缀编码。前缀编码可以保证对压缩数据进行解码时不产生二义性,确保正确解码。

前缀编码有两种编码方案:

Huffman coding 是一种最优前缀编码,属于不等长编码方案。

参考:https://www.csdn.net/tags/MtTaMgysMjE3NjItYmxvZwO0O0OO0O0O.html

题目:已知字符对应的前缀编码表,给定一串编码,将其解码为字符串。

解答:https://blog.csdn.net/qq_45273552/article/details/109176832

参考

https://docs.oracle.com/javase/8/docs/api/java/util/Map.html

Guava 源码分析之Cache的实现原理

本文介绍一种运算受限的线性表 —— Queue 队列在 Java 中的实现与使用。

队列接口

Queue 接口的继承关系及提供的方法如下:

Queue 接口提供的方法

入队/出队/队头

Queue 接口提供的六个方法差别如下:

Throws exception Returns special value
Insert add(e) offer(e)
Remove remove() poll()
Examine element() peek()

遍历队列

参考:《Iterate over a Queue in Java

队列实现

Java 提供的队列实现如下图:

collection_impl

顺序队列

使用如下:

1
2
3
4
5
// 单端顺序队列
Queue<Integer> queue = new ArrayDeque<>(10);

// 双端顺序队列
Deque<Integer> deque = new ArrayDeque<>(10);

ArrayDeque 底层存储结构为一维数组,默认队列容量为 16。其成员变量定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* The array in which the elements of the deque are stored.
* The capacity of the deque is the length of this array, which is
* always a power of two. The array is never allowed to become
* full, except transiently within an addX method where it is
* resized (see doubleCapacity) immediately upon becoming full,
* thus avoiding head and tail wrapping around to equal each
* other. We also guarantee that all array cells not holding
* deque elements are always null.
*/
transient Object[] elements; // non-private to simplify nested class access

/**
* The index of the element at the head of the deque (which is the
* element that would be removed by remove() or pop()); or an
* arbitrary number equal to tail if the deque is empty.
*/
transient int head;

/**
* The index at which the next element would be added to the tail
* of the deque (via addLast(E), add(E), or push(E)).
*/
transient int tail;

在反复入队、出队时,为了解决了一维数组的“空间浪费”及“假溢出”问题,有两种解决方案:

  • 在出队时,将剩余数组元素全部往前挪动一个位置,但这个动作的时间复杂度 O(n),影响性能。
  • 使用循环队列。

ArrayDeque 是一个循环队列。当进行出队、入队操作时,并不是简单是 head++tail++,而是自增后进行取模运算,确保 headtail 在容量范围内进行反复循环。其实现如下图:

入队/出队

ArrayDeque 是一个无界队列。当队满时,会进行双倍空间的动态扩容(底层实现为新建一个双倍容量的新数组,并使用 System#arraycopy 方法进行数组拷贝)。源码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Doubles the capacity of this deque. Call only when full, i.e.,
* when head and tail have wrapped around to become equal.
*/
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p; // number of elements to the right of p
int newCapacity = n << 1;
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
System.arraycopy(elements, p, a, 0, r);
System.arraycopy(elements, 0, a, r, p);
elements = a;
head = 0;
tail = n;
}

这里我用 C 语言自己实现了一个简单的循环队列,代码如下:

https://github.com/qidawu/cpp-data-structure/blob/main/src/cycleQueue.cpp

实现过程中,有一些注意点:

队列实现注意点

链式队列

使用如下。链式队列也是一个无界队列

1
2
3
4
5
// 单端链式队列
Queue<Integer> queue = new LinkedList<>();

// 双端链式队列
Deque<Integer> deque = new LinkedList<>();

阻塞队列

并发队列有两种实现方式:

  • 阻塞队列(BlockingQueue
  • 并发队列(非阻塞实现)

Queue_implementations

下面重点看下 BlockingQueue,其扩展自 Queue 接口,主要新增了两组接口(下表后两列),其常用方法差别如下表:

Throws exception Returns Special value Times out Blocks
Insert add(e) offer(e) offer(e, time, unit) put(e)
Remove remove() poll() poll(time, unit) take()
Examine element() peek() not applicable not applicable

而线程池 ThreadPoolExecutor 目前就使用了 BlockingQueue 的这些方法:

  • 入队 offer(e)
  • 出队 poll(time, unit)take()

阻塞队列实现

阻塞队列 BlockingQueue 的实现如下图:

BlockingQueue

  • 有界队列(数组实现):

    • ArrayBlockingQueue,基于数组实现的有界阻塞队列。
  • 无界队列(链表实现):

    • LinkedBlockingQueue,基于链表实现的可选无界阻塞队列。默认用于 Executors.newFixedThreadPool(...)newSingleThreadExecutor(...)
    • LinkedTransferQueue
    • PriorityBlockingQueue,基于堆实现(底层是数组)的具有优先级的无界阻塞队列。
    • DelayedQueue
  • 无容量队列:

    • SynchronousQueue,无容量阻塞队列,每个插入操作都必须等待另一个线程的移除操作,反之亦然。默认用于 Executors.newCachedThreadPool(...)

阻塞双端队列 BlockingDeque 的实现如下图:

BlockingDeque_implementations

参考

[java 队列]——ArrayBlockingQueue

ConcurrentLinkedQueue 和 LinkedBlockingQueue 区别

常用方法

List partition

集合 List 分片的 5 种方法

Array to List

方法一:转两次

1
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"))  // from varargs

方法二:Java 8

1
2
3
4
5
6
// 包装类型
Integer [] myArray = { 1, 2, 3 };
List<Integer> myList = Arrays.stream(myArray).collect(Collectors.toList());
// 基本类型也可以实现转换(依赖 boxed 的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List<Integer> myList2 = Arrays.stream(myArray2).boxed().collect(Collectors.toList());

方法三:Guava

1
2
3
4
5
6
7
8
9
// 不可变集合
List<String> il = ImmutableList.of("string", "elements"); // from varargs
List<String> i2 = ImmutableList.copyOf(new String[]{"string", "elements"}); // from array

// 可变集合
List<String> l0 = Lists.newLinkedList(Arrays.asList("a", "b", "c")); // from collection
List<String> l1 = Lists.newArrayList(Arrays.asList("a", "b", "c")); // from collection
List<String> l2 = Lists.newArrayList(new String[]{"string", "elements"}); // from array
List<String> l3 = Lists.newArrayList("or", "string", "elements"); // from varargs

参考

Arrays.asList() 原来是这样用的

千万不要这样使用 Arrays.asList !

本文总结下集合元素排序的常用 API:

java.util.Comparator

基于可比较对象排序

如果集合元素已实现 Comparable 接口,可以直接使用 naturalOrderreverseOrder 方法进行排序:

1
2
3
4
5
6
7
8
9
List<Integer> integers = Arrays.asList(3, 1, 2, 4);

// 升序
// [1, 2, 3, 4]
integers.sort(Comparator.naturalOrder());

// 降序
// [4, 3, 2, 1]
integers.sort(Comparator.reverseOrder());

上述排序不支持 null 值(会抛 NPE 异常),如果自定义实现的话,代码比较冗余,容易出错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
List<Integer> integers = Arrays.asList(3, 1, null, 2, 4);

// [null, 1, 2, 3, 4]
integers.sort((o1, o2) -> {
// 写法1:
if (o1 != null && o2 != null) {
return o1.compareTo(o2);
} else {
return o1 == null ? -1 : 1;
}
// 写法2:
// return o1 == null ? -1 : (o2 == null ? 1 : o1.compareTo(o2));
});

// [4, 3, 2, 1, null]
integers.sort((o1, o2) -> {
// 写法1:
if (o1 != null && o2 != null) {
return o2.compareTo(o1);
} else {
return o1 == null ? 1 : -1;
}
// 写法2:
// return o1 == null ? 1 : (o2 == null ? -1 : o2.compareTo(o1));
});

可采用 nullsFirstnullsLast 方法兼容 null 值情况:

1
2
3
4
5
6
7
8
9
List<Integer> integers = Arrays.asList(3, 1, null, 2, 4);

// null 值在前
// [null, 1, 2, 3, 4]
integers.sort(Comparator.nullsFirst(Comparator.naturalOrder()));

// null 值在后
// [4, 3, 2, 1, null]
integers.sort(Comparator.nullsLast(Comparator.reverseOrder()));

基于不可比较对象排序

例子:

1
2
3
4
5
6
@Data
@AllArgsConstructor
public class IdName {
private Integer id;
private String name;
}

如果集合元素未实现 Comparable 接口,需要抽取关键字(关键字需实现 Comparable 接口)排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
List<IdName> idNames = Arrays.asList(
new IdName(3, "Pete"),
new IdName(1, "Tom"),
new IdName(2, "Ben"),
new IdName(2, "Allen"));

// 根据 ID 升序
// [IdName(id=1, name=Tom), IdName(id=2, name=Ben), IdName(id=2, name=Allen), IdName(id=3, name=Pete)]
idNames.sort(Comparator.comparing(IdName::getId));

// 根据 ID 升序,null 值在后
// [IdName(id=1, name=Tom), IdName(id=2, name=Ben), IdName(id=2, name=Allen), IdName(id=3, name=Pete)]
idNames.sort(Comparator.comparing(IdName::getId, Comparator.nullsLast(Comparator.naturalOrder())));

// 根据 ID、Name 复合排序(升序)
// [IdName(id=1, name=Tom), IdName(id=2, name=Allen), IdName(id=2, name=Ben), IdName(id=3, name=Pete)]
idNames.sort(Comparator.comparing(IdName::getId)
.thenComparing(IdName::getName));

// 根据 ID、Name 复合排序(降序)
// [IdName(id=3, name=Pete), IdName(id=2, name=Ben), IdName(id=2, name=Allen), IdName(id=1, name=Tom)]
idNames.sort(Comparator.comparing(IdName::getId)
.thenComparing(IdName::getName)
.reversed());

参考

java comparator 升序、降序、倒序从源码角度理解

https://blog.csdn.net/weixin_44270183/article/details/87026995

本文总结下集合元素迭代的常用 API。

迭代器模式

迭代器模式(Iterator)是一种行为型设计模式,让你能在不暴露集合底层表现形式(列表、栈和树等)的情况下遍历集合中所有的元素。

Iterator

迭代器实现

在 Java 中,迭代器模式的实现有以下几种:

Iterator Interface

  • java.util.Enumeration<E>:Java 1.0 引入,用于枚举集合元素。这种传统接口已被 Iterator 迭代器取代,虽然 Enumeration 还未被废弃,但在现代代码中已经被很少使用了。主要用于诸如 java.util.Vectorjava.util.Properties 这些传统集合类。
  • java.util.Iterator<E>:Java 1.2 引入。作为 Java 集合框架的成员,迭代器取代了枚举。迭代器与枚举有两个不同之处:
    • 引入 remove 方法,允许调用者在迭代期间从集合中删除元素。
    • 方法名改进。
  • java.lang.Iterable<T>:Java 1.5 引入。For-each Loop 语法糖的底层实现,实现这个接口的对象可以用于 “For-each Loop”语句,简化迭代器繁琐的使用语法。

上述三种迭代器实现都属于命令式编程范式,即使访问值的方法仅由迭代器负责实现。但实际上,是由开发者来决定何时访问序列中的 next() 项。

与 Stream API 对比

java.util.stream.Stream<T>:Java 8 引入,用于实现 Stream API:

Stream Interface

Stream Interface

与迭代器的区别在于:

  • Iterator 外部迭代,使用命令式编程范式,完全由用户来决定”做什么“和”怎么做“,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Test
    public void iterator() {
    List<String> example = Arrays.asList("A", "B", "C");
    List<String> result = new ArrayList<>(example.size());
    // 怎么做(通过 Iterator API 遍历 List)
    Iterator<String> iterator = example.iterator();
    while (iterator.hasNext()) {
    // 做什么(把大写转成小写)
    result.add(iterator.next().toLowerCase());
    }
    }

    @Test
    public void iterable() {
    List<String> example = Arrays.asList("A", "B", "C");
    List<String> result = new ArrayList<>(example.size());
    // 怎么做(通过 For-each Loop 遍历 List)
    for (String s : example) {
    // 做什么(把大写转成小写)
    result.add(s.toLowerCase());
    }
    }

    outer_iterator

  • Stream 内部迭代,使用声明式编程范式 > 函数式编程,用户仅需要决定“做什么”,而把“怎么做”的任务交给 JVM:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Test
    public void streamApi() {
    List<String> result = Arrays.asList("A", "B", "C")
    // 这里遍历方式由 Stream API 实现,用户仅调用相应 API,数据量小可以用串行,数据量大可以用并行,怎么做、怎么实现用户并不用关心
    .stream()
    // 做什么(把大写转成小写)
    .map(String::toLowerCase)
    // 这里转成线性表由 Stream API 实现,用户仅调用相应 API,也可以转成集合、散列表,怎么实现用户并不关心。
    .collect(Collectors.toList());
    }

    inner_iterator

使用内部迭代的优势在于:

  • 用户只需要关注问题本身,无需关注如何解决问题的细节。
  • Java 可以利用短路、并行等对性能进行优化,用户无需关心。

与 Reactive Stream 对比

响应式编程范式通常在面向对象语言中作为观察者模式的扩展出现。可以将其与大家熟知的迭代器模式作对比,主要区别在于:

  • 迭代器、Stream API 基于拉模式(PULL)
  • 响应式流基于推模式(PUSH)

参考:响应式编程总结

参考

https://refactoringguru.cn/design-patterns/iterator