从源码看HashMap键值对集合
之前我们看过了两种类型的集合,ArrayList集合和LinkedList集合,两种集合各有优势,我们不具体说了,但是本篇要看的集合可以完成它们完成不了的任务。比如:现有一篇文章,要你统计其中出现了哪些单词,每个单词总共出现了几次。这个问题很明显需要记录两个变量(某单词及其出现次数),但是我们之前介绍的集合都只能同时存储一种类型的变量,无法实现对应的效果。
我们的HashMap又可以叫做键值对集合(官方名称映射),比如:
public class Test_Class {
public static void main(String[] args){
HashMap<String,Integer> map = new HashMap<String, Integer>();
map.put("hello",10);
}
}
/*第一条记录,保存了hello这个单词在文章中总共出现10次*/
一、超接口Map
想要研究一个具体的类,先要看看它的父类或者父接口。HashMap实现了Map<K,V>接口,K表示键,V表示值,一个键对应一个值,整个集合中键是唯一的不可重复。
public interface Map<K,V> {
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
V get(Object key);
V put(K key, V value);
V remove(Object key);
void putAll(Map<? extends K, ? extends V> m);
void clear();
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
......
//源码中方法很多,我们简单介绍几个常用的
}
接口中定义了 V get(Object key); 方法用来根据指定的键值,返回该键所对应的value值,有方法 V put(K key, V value); 添加指定键值和value的一条记录到集合中,有方法 V remove(Object key); 根据指定的value值删除一条或者多条记录等等,当然这些方法都只是声明,我们马上可以看看HashMap对这些方法的具体的实现是什么。
二、HashMap的实现原理
HashMap是Map的一种实现,它具体实现了上述Map接口中的所有方法。我们看看它的内部是以什么样的形式组织和存储内容的。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Node<K,V>[] table;
transient int size;
transient int modCount;
final float loadFactor;
/*成员内部类,很重要的类*/
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 String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(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;
}
}
}
上述代码主要从代码的角度展现了HashMap的内部数据是以什么样的形式来存储的。我们可以看到,HashMap中定义了初始容量为16,最大容量,默认的 DEFAULT_LOAD_FACTOR(具体是什么下面再说) 参数值,实际上HashMap内部存储为数组加链表,定义了一个Node类数组,每个Node结点指向一个链表的头部串联整个链表。(单向链表)
Node结点其实构成的是一个单向链表,每个结点由 final K key;,不可更改的键,可以修改的 V value; 值,还有指向下一个结点的指针Node<K,V> next; 组成,当然还包含了对结点的操作方法。
三、添加元素到集合中
我们使用put方法添加元素到集合中:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//调用了一个不可被重写的方法
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;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
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);
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;
}
接下来我们来解释一下整体的调用过程,首先在调用put方法时候,调用了另外的一个方法 putVal(看起来十分恶心),传过去一个key个hash值,还有key和value,还有两个布尔参数(后面我们会知道他们的作用)。
首先if判断这个HashMap是否是null,如果是的话,调用resize方法初始化一个HashMap(容量为16,loadFactor参数为0.75等其他参数值),然后 (p = tab[i = (n - 1) & hash]) == null 用我们想要插入的元素的hash值和HashMap容量取模判断将要存储的位置,因为我们知道每个对象的hash值是不同的,所以在插入元素的时候用他们的hash值和整个HashMap数组长度取模使得他们根据自己的hash存储在HashMap的固定位置上,如果我们将要插入节点的位置上是空的就 tab[i] = newNode(hash, key, value, null);,新建节点直接存放在此位置上。
假如我们使用hash取模之后得到的索引位置为3,发现此处为空,于是占用他。
我们接着看,刚才是预定的位置没被使用,那如果已经被占用了怎么办?
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
这行代码紧接着,对占用我们位置的结点键的值和我们的键的值进行比较,如果一样说明,这是对这条记录的修改,于是准备覆盖原位置的结点将p赋值给e。紧接着的一行代码可以先跳过设计到树,最后一个else中主要是将我们将要插入的结点链接到某个链表上。方法的尾部判断e是否为空,如果不为空说明在某条链表中找到与我们将要插入的结点的键的值一样的结点(就是需要覆盖原链表中某个结点的值),于是覆盖某个结点的值返回该结点的值。当然方法的最后对容量进行了判断,如果超过需要扩充的界限就会调用方法扩充HashMap数组的长度。
最后总结一下整个put方法的几个重要的过程:
- 判断HashMap是否为空,是就初始化一个HashMap
- 判断预定位置是否被占用,未被占用则占用
- 预定位置被占用则判断是否有键值相等的结点,有则覆盖
- 遍历之后没有找到键值相等的结点,就将此结点链接在某链表的末尾
- 判断容量是否越界,是则扩充容量
四、删除操作
remove方法的源码如下:
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/*主要还是调用方法removeNode*/
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;
}
我们具体看看代码,第一个if语句主要是判断:如果当前的HashMap不为null并且将要删除的结点的key和capcity取模后存在于HashMap数组中,就执行余下的代码。(主要还是为了保证将要删除的元素是存在的),接下来的一句if判断主要是,如果第一个位置的结点就是我们将要删除的结点的话,就将此结点赋值node,接下来就是遍历整个链表查找将要删除的键值,如果找到赋值给node。最后根据不同情况进行删除操作,如果p=node 说明将要删除的结点是第一个结点直接将此位置赋null即可,否则,将p的next赋null即可(经过遍历p始终指向将要删除的前一个结点),最后完成删除。
最后小结一下整个删除操作的过程:
- 传入指定的键值,将键值及其hash值传入方法removeNode
- 通过判断HashMap是否为null以及hash取模之后的位置是否为null,提高函数执行效率
- 在链表中寻找键值,并且做好标记准备删除
- 删除元素结点
至于get查找返回指定的值,是否包含指定键,是否包含指定值等方法类似。
最后,因为HashMap存取没有顺序,如果对于一些需要按键存取数值的数据并且对存取顺序没有要求的话,可以考虑使用HashMap提升效率。
本篇文章是博主查看jdk源码总结而来,如果有不当之处,望大家指出来。
从源码看HashMap键值对集合的更多相关文章
- 【JDK1.8】JDK1.8集合源码阅读——HashMap
一.前言 笔者之前看过一篇关于jdk1.8的HashMap源码分析,作者对里面的解读很到位,将代码里关键的地方都说了一遍,值得推荐.笔者也会顺着他的顺序来阅读一遍,除了基础的方法外,添加了其他补充内容 ...
- 浅析Java源码之HashMap
写这篇文章还是下了一定决心的,因为这个源码看的头疼得很. 老规矩,源码来源于JRE1.8,java.util.HashMap,不讨论I/O及序列化相关内容. 该数据结构简介:使用了散列码来进行快速搜索 ...
- JDK源码解析---HashMap源码解析
HashMap简介 HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. HashMap是非线程安全的,只是 ...
- jdk1.8.0_45源码解读——HashMap的实现
jdk1.8.0_45源码解读——HashMap的实现 一.HashMap概述 HashMap是基于哈希表的Map接口实现的,此实现提供所有可选的映射操作.存储的是<key,value>对 ...
- JDK8源码解析 -- HashMap(一)
最近一直在忙于项目开发的事情,没有时间去学习一些新知识,但用忙里偷闲的时间把jdk8的hashMap源码看完了,也做了详细的笔记,我会把一些重要知识点分享给大家.大家都知道,HashMap类型也是面试 ...
- JDK1.8源码学习-HashMap
JDK1.8源码学习-HashMap 目录 一.HashMap简介 HashMap 主要用来存放键值对,它是基于哈希表的Map接口实现的,是常用的Java集合之一. 我们都知道在JDK1.8 之前 的 ...
- 从源码看Azkaban作业流下发过程
上一篇零散地罗列了看源码时记录的一些类的信息,这篇完整介绍一个作业流在Azkaban中的执行过程,希望可以帮助刚刚接手Azkaban相关工作的开发.测试. 一.Azkaban简介 Azkaban作为开 ...
- 解密随机数生成器(二)——从java源码看线性同余算法
Random Java中的Random类生成的是伪随机数,使用的是48-bit的种子,然后调用一个linear congruential formula线性同余方程(Donald Knuth的编程艺术 ...
- 从Chrome源码看浏览器的事件机制
.aligncenter { clear: both; display: block; margin-left: auto; margin-right: auto } .crayon-line spa ...
随机推荐
- 如何获取DOM中当前获取焦点的元素
<script type="text/javascript"> function msg(e) // e = event { var target; //initial ...
- python 日期格式化常用标记
符号 说明 例子 %a 英文星期的简写 Mon %A 英文星期的完整编写 Monday %b 英文月份的简写 Jun %B 英文月份的完整编写 June ...
- Vim 第一天
记得第一次接触vi编辑器,好像是在海尔的机房吧,那是时候还是没有毕业的小菜鸟一只(PS:现在也是菜鸟),记得是测试一个云存储的产品.看着他们用vi编辑器,当时也没有感觉有什么(现在也没感觉……),只是 ...
- MySQL服务器的线程数查看方法
mysql重启命令:/etc/init.d/mysql restart MySQL服务器的线程数需要在一个合理的范围之内,这样才能保证MySQL服务器健康平稳地运行.Threads_created表示 ...
- Struts2拦截器介绍
一.拦截器简介 Struts拦截器和Action的关系如图: 为了在使用拦截器时制定参数值,应通过<interceptor-ref -/>元素添加<param -/>子元素来为 ...
- Windows Server 2008 R2防火墙入站规则
一般服务器的端口都设置了外网无法访问,iis中创建的网站外网也是访问不了的,需要创建指定端口的入站规则后方可访问. 方法/步骤 服务器管理器-->配置-->高级安全windows防 ...
- 分析NGINX 健康检查和负载均衡机制
nginx 是优秀的反向代理服务器,这里主要讲它的健康检查和负载均衡机制,以及这种机制带来的问题.所谓健康检查,就是当后端出现问题(具体什么叫出现问题,依赖于具体实现,各个实现定义不一样),不再往这个 ...
- NodeJS异步I/O解析
在现在的项目开发中,任何一个大型项目绝对不是简简单单的采用一个种语言和一种框架,因为每种语言和框架各有优势,与其死守一个,不与取各家之所长,依次得到一个高性能.搞扩展的产品. 对于一个.NET开发者, ...
- Linux笔记(十三) - 系统管理
(1)进程管理1.判断服务器健康状态2.查看进程a.查看系统中所有进程:ps 例:ps aux(使用BSD操作系统命令格式) a 显示所有前台进程 x 显示所有后台进程 u 显 ...
- [CSS3] 学习笔记-选择器详解(二)
1.选择器first-child.last-child.nth-child和nth-last-child 利用first-child.last-child.nth-child和nth-last-chi ...