本文一是总结前面两种集合,补充一些遗漏,再者对HashMap进行简单介绍。

回顾

因为前两篇ArrayList和LinkedList都是针对单独的集合类分析的,只见树木未见森林,今天分析HashMap,可以结合起来看一下java中的集合框架。下图只是一小部分,而且为了方便理解去除了抽象类。

Java中的集合(有时也称为容器)是为了存储对象,而且多数时候存储的不止一个对象。

可以简单的将Java集合分为两类:

  • 一类是Collection,存储的是独立的元素,也就是单个对象。细分之下,常见的有List,Set,Queue。其中List保证按照插入的顺序存储元素。Set不能有重复元素。Queue按照队列的规则来存取元素,一般情况下是“先进先出”。

  • 一类是Map,存储的是“键值对”,通过键来查找值。比如现实中通过姓名查找电话号码,通过身份证号查找个人详细信息等。

    理论上说我们完全可以只用Collection体系,比如将键值对封装成对象存入Collection的实现类,之所以提出Map,最主要的原因是效率。

HashMap简介

HashMap用来存储键值对,也就是一次存储两个元素。在jdk1.8中,其实现是基于数组+链表+红黑树,简单说就是普通情况直接用数组,发生哈希冲突时在冲突位置改为链表,当链表超过一定长度时,改为红黑树。

可以简单理解为:在数组中存放链表或者红黑树。

  1. 完全没有哈希冲突时,数组每个元素是一个容量为1的链表。如索引0和1上的元素。
  2. 发生较小哈希冲突时,数组每个元素是一个包含多个元素的链表。如索引2上的元素。
  3. 当冲突数量超过8时,数组每个元素是一棵红黑树。如索引6上的元素。

下图为示意图,相关结构没有严格遵循规范。

类签名

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

如下图

实现Cloneable和Serializable接口,拥有克隆和序列化的能力。

HashMap继承抽象类AbstractMap的同时又实现Map接口的原因同样见上一篇LinkedList。

常量

//序列化版本号
private static final long serialVersionUID = 362498820763181265L; //默认初始化容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量,2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30; //默认负载因子,值为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f; //以下三个常量应结合看
//链表转为树的阈值
static final int TREEIFY_THRESHOLD = 8; //树转为链表的阈值,小于6时树转链表
static final int UNTREEIFY_THRESHOLD = 6; //链表转树时的集合最小容量。只有总容量大于64,且发生冲突的链表大于8才转换为树。
static final int MIN_TREEIFY_CAPACITY = 64;

上述变量的关键在于链表转树和树转链表的时机,综合看:

  • 当数组的容量小于64是,此时不管冲突数量多少,都不树化,而是选择扩容。
  • 当数组的容量大于等于64时,
    • 冲突数量大于8,则进行树化。
    • 当红黑树中元素数量小于6时,将树转为链表。

变量

//存储节点的数组,始终为2的幂
transient Node<K,V>[] table; //批量存入时使用,详见对应构造函数
transient Set<Map.Entry<K,V>> entrySet; //实际存放键值对的个数
transient int size; //修改map的次数,便于快速失败
transient int modCount; //扩容时的临界值,本质是capacity * load factor
int threshold; //负载因子
final float loadFactor;

数组中存储的节点类型,可以看出,除了K和Value外,还包含了指向下一个节点的引用,正如一开始说的,节点实际是一个单向链表。

static class Node<K,V> implements Map.Entry<K,V> {

    final int hash;
final K key;
V value;
Node<K,V> next; //...省略常见方法
}

构造方法

常见的无参构造和一个参数的构造很简单,直接传值,此处省略。看一下两个参数的构造方法。

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;
//将给定容量转换为不小于其自身的2的幂
this.threshold = tableSizeFor(initialCapacity);
}

tableSizeFor方法

上述方法中有一个非常巧妙的方法tableSizeFor,它将给定的数值转换为不小于自身的最小的2的整数幂。

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;
}

比如cap=10,转换为16;cap=32,则结果还是32。用了位运算,保证效率。

有一个问题,为啥非要把容量转换为2的幂?之前讲到的ArrayList为啥就不需要呢?其实关键在于hash,更准确的说是转换为2的幂,一定程度上减小了哈希冲突。

关于这些运算,画个草图很好理解,关键在于能够想到这个方法很牛啊。解释的话配图太多,这里篇幅限制,将内容放在另一篇文章。

添加元素

在上面构造方法中,我们没有看到初始化数组也就是Node<K,V>[] table的情况,这一步骤放在了添加元素put时进行。

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

可以看出put调用的是putVal方法。

putVal方法

在此之前回顾一下HashMap的构成,数组+链表+红黑树。数组对应位置为空,存入数组,不为空,存入链表,链表超载,转换为红黑树。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {

    Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//数组为空,则扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根据key计算hash值得出数组中的位置i,位置i上为空,直接添加。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//数组对应位置不为空
else {
Node<K,V> e;
K k;
//对应节点key上的key存在,直接覆盖value
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);
//链表转换为红黑树
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) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
} ++modCount;
//下次添加前需不需要扩容,若容量已满则提前扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

resize()方法比较复杂,最好是配合IDE工具,debug一下,比较容易弄清楚扩容的方式和时机,如果干讲的话反而容易混淆。

获取元素

根据键获取对应的值,内部调用getNode方法

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

getNode方法

final Node<K,V> getNode(int hash, Object key) {

    Node<K,V>[] tab;
Node<K,V> first,
e; int n;
K k; //数组不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//第一个节点满足则直接返回对应值
if (first.hash == hash &&
((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;
}

总结

HashMap的内容太多,每个内容相关的知识点也很多,篇幅和个人能力限制,很难讲清所有内容,比如最基础的获取hash值的方法,其实也很讲究的。有机会再针对具体的细节慢慢详细写吧。

阅读源码,HashMap回顾的更多相关文章

  1. 阅读源码(III)

    往期系列: <由阅读源码想到> <由阅读源码想到 | 下篇> Medium上有一篇文章Why You Don't Deserve That Dream Developer Jo ...

  2. 阅读源码(IV)

    往期系列: <由阅读源码想到> <由阅读源码想到 | 下篇> <阅读源码(III)> Eric S.Raymond的写于2014年的<How to learn ...

  3. JDK1.8源码分析02之阅读源码顺序

    序言:阅读JDK源码应该从何开始,有计划,有步骤的深入学习呢? 下面就分享一篇比较好的学习源码顺序的文章,给了我们再阅读源码时,一个指导性的标志,而不会迷失方向. 很多java开发的小伙伴都会阅读jd ...

  4. 学会阅读源码后,我觉得自己better了

    我有一个大学同学,名叫石磊,我在之前的文章里提到过几次,我们俩合作过很多项目.只要有他在,我就特别放心,因为几乎所有难搞的问题,到他这,都能够巧妙地化解.他给我印象最深刻的一句话就是,"有啥 ...

  5. 阅读源码,从ArrayList开始

    前言 为啥要阅读源码?一句话,为了写出更好的程序. 一方面,只有了解了代码的执行过程,我们才能更好的使用别人提供的工具和框架,写出高效的程序.另一方面,一些经典的代码背后蕴藏的思想和技巧很值得学习,通 ...

  6. 【转】使用 vim + ctags + cscope + taglist 阅读源码

    原文网址:http://my.oschina.net/u/554995/blog/59927 最近,准备跟学长一起往 linux kernel 的门里瞧瞧里面的世界,虽然我们知道门就在那,但我们还得找 ...

  7. Spring源码解析——如何阅读源码(转)

    最近没什么实质性的工作,正好有点时间,就想学学别人的代码.也看过一点源码,算是有了点阅读的经验,于是下定决心看下spring这种大型的项目的源码,学学它的设计思想. 手码不易,转载请注明:xingoo ...

  8. Spring源码解析——如何阅读源码

    最近没什么实质性的工作,正好有点时间,就想学学别人的代码.也看过一点源码,算是有了点阅读的经验,于是下定决心看下spring这种大型的项目的源码,学学它的设计思想. 手码不易,转载请注明:xingoo ...

  9. How Tomcat works — 一、怎样阅读源码

    在编程的道路上,通过阅读优秀的代码来提升自己是很好的办法.一直想阅读一些开源项目,可是没有合适的机会开始.最近做项目的时候用到了shiro,需要做集群的session共享,经过查找发现tomcat的s ...

  10. 使用 vim + ctags + cscope + taglist 阅读源码

    转自:http://my.oschina.net/u/554995/blog/59927 最近,准备跟学长一起往 linux kernel 的门里瞧瞧里面的世界,虽然我们知道门就在那,但我们还得找到合 ...

随机推荐

  1. B. Queue

    During the lunch break all n Berland State University students lined up in the food court. However, ...

  2. CodeForces - 916C 思维

    题意:给你n,m,表示n个顶点和m条边,让你构造一个图. 要求 1.1->n最短路为素数 2.最小生成树边权和为prime 3.没有重边 4.边大小[1,1e9]. (题目给定m>n-1) ...

  3. C# 异常重试策略

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...

  4. SpringBoot 中使用 Swagger2 出现 whitelabel page error 解决方法

    今天使用Swagger最新版,在pom.xml引入 <dependency> <groupId>io.springfox</groupId> <artifac ...

  5. 【原创】k8s之job和Cronjob

    1.失败任务 apiVersion: batch/v1 kind: Job metadata: name: bad spec: template: metadata: name: bad spec: ...

  6. pycharm 与 anaconda 关联

    anaconda Anaconda指的是一个开源的Python发行版本,集成了许多数据分析的库. 使用tersorflow进行机器学习时常用Anaconda pycharm PyCharm是一种Pyt ...

  7. Ubuntu第一次使用注意点

    第一次装完Ubuntu登录,打开命令行,登录的不是root权限,切换root不成功: 这个问题产生的原因是由于Ubuntu系统默认是没有激活root用户的,需要我们手工进行操作,在命令行界面下,或者在 ...

  8. 5种设置ASP.NET Core应用程序URL的方法

    默认情况下,ASP.NET Core应用程序监听以下URL: http://localhost:5000 https://localhost:5001 在这篇文章中,我展示了5种不同的方式来更改您的应 ...

  9. convert number or string to ASCII in js

    convert number or string to ASCII in js ASCII dictionary generator // const dict = `abcdefghijklmnop ...

  10. 增强 CT & CT & MR

    增强 CT & CT & MR CTA,增强 CT Computed Tomography (CT) CT 计算机断层扫描 Computed Tomography (CT) Angio ...