Java提高(一)---- HashMap
阅读博客
1,
java提高篇(二三)-----HashMap
这一篇由chenssy发表于2014年1月,是根据JDK1.6的源码讲的。
2,Java类集框架之HashMap(JDK1.8)源码剖析
这一篇由push_pop发表于2015年5月,根据JDK1.8讲的。
先说1.6的HashMap
1.6的HashMap代码较少,写的比较容易看懂。
HashMap里存的对象是Entry,HashMap实际上是Entry数组。数组的大小是2的n次方,默认为16,对应属性是capacity。
HashMap有4个构造函数,但其实最终调用的都是HashMap(int initialCapacity,float loadFactor)这个。可以看到,构造HashMap需要两个参数 容量initialCapacity和负载因子loadFactor。
initialCapacity这个参数并不是说 HashMap能装多少组元素,而是HashMap里有多少个桶,也就是Entry数组的大小。它并不会总使用你设定的值,而是会取一个比initialCapacity大的2的n次方。
比如你设置initialCapacity=9,实际上桶的大小会是16。桶大小不是一成不变的,会根据HashMap中总元素的增加而成倍的增加。比如现在capacity是16,下一次调整就是32,再增加就是64.
HashMap在构造时,会设置扩容的阈值shreshold=initialCapacity*loadFactor。
下面说常用的put方法。1.6源码
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
} modCount++;
addEntry(hash, key, value, i);
return null;
}
1.6根据key是否为null,会走不同的分支,key==null的,会单独走一个方法putForNullKey(). null这个key会固定放到table[0]
hash值,等于key.hashCode再进入一个hash函数算出来的值,这个hash()函数做了几次移位和取或运算。
indexFor() 等于用hash对table的长度取模,来得到将来放到数组的哪个位置。这里用按位与,速度比%要快多。
put函数是有返回值的。
这时会有三种情况
- 情况1,i位置上没有元素,那么for循环不会进去,直接新建一个Entry节点,放在i位置上,返回null。
- 情况2,i位置上已经有元素了,而且是一列元素,链表存储方式,但是都是不同的key,那么,就新建一个Entry节点,放在i的位置,之前这里的链表挂在新建的节点后面,或者说,新建的节点指向以前挂在这里的链表,返回null
这里有2点注意,
1,新建的节点,是挂在原来列表的头上,而不是末尾。这样就省得遍历到末尾了。
2,如何判断key相同呢?
e.hash == hash && ((k = e.key) == key || key.equals(k))
key的hash值相等,且key的equals方法相等,或者根本就是一个key(引用相同),这也是为什么一个对象要做Key必须要重载hashCode()和equals()方法的原因吧。
- 情况3,其实前面两种情况都是一个新key加入,只不过1是没有发生hash碰撞,2是发生碰撞了。 情况3是说 对一个已经存在key,再次put会新值怎么样。首先新值覆盖旧值,然后返回旧值。
所以通过put返回值,可以判断put的是一个新key,还是旧key。
get方法
1.6源码
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
如果key==null会单独走一个方法。
其他的 先算出hash值,找到对应的桶,遍历。
如果有相同的key就返回对应的value ,否则返回null。
get比较简单。
size属性,记录HashMap中有多少元素(Entry)。每次添加Entry时,size都会自增1,同时会和阈值shreshold比较,当size>=shreshold时,就是调用resize方法 扩容
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
resize方法
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
} Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
每次容量扩成之前的2倍
先new一个新数组,
再通过transfer方法把之前的元素都重新hash到新的数组中,
阈值按照新的容量重新计算。
remove()
还有删除的方法,删除也有返回值,如果返回null,说明要删除的key不存在,如果不是null,就是删除元素的value。
可以看到1.6的HashMap采用的是数组形式,哈希值取模后就是数组的索引,所以在不冲突时,时间复杂度为1,当冲突时,多个节点一个接一个形成一个链结构,后来者插入到链表头。
===分割线=======================================
上面是jdk1.6的HashMap源代码
下面聊聊jdk1.8的HashMap,这个版本的HashMap变化还是比较大的,整个源文件从1000行增加到了2300行(包含所有注释空行)。
第一个改变是把以前的Entry类 改名字 成了Node。
Entry和Node都实现了Map.Entry接口。 这个接口有4个static的方法,不需要实现。
在Java8中,接口中支持了静态方法,这个静态方法需要有body,不需要实现。
HashMap还是有4个构造函数,最主要的是HashMap(int,float) , 1.8中,不会在构造时就new table数组,而是会延迟到第一次put数据时。构造函数只设定了loadFactor和threshold参数,threshold的计算方法和之前不同,它调用了一个tableSizeFor的方法,这个方法返回大于initialCapacity的2的n次幂
/**
* 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值,依然是对 key的hashCode进行再hash, 不过hash函数和1.6的又有不同。
put方法
1.8的put实际上是调用putVal方法,这个方法多了一个onlyIfAbsent参数,貌似考虑在将来在put的时候,如果该key已经存在,就不覆盖了。目前这个参数还是false,每次put如果key存在,会用新值替换旧值。
1.8源码,putVal
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;
}
1.8的put比1.6的看起来复杂多了。代码写的更紧凑,不容易看懂,许多赋值和比较都在一个语句中写着。可能是为了执行效率吧,阅读起来体验下降。
新版的resize方法是一个自适应的方法,不需要参数,这个resize比以前复杂,有初始化table的任务。
没有对key=null去特殊处理。
情况1,如果table位上没有节点,new一个新节点挂上即可,返回null。
情况2,table[i]为上有节点,且第一个节点的key就是本次要put的key,那么用新值覆盖旧值,返回旧值。
后面就是要遍历这个位置上的挂接节点了。1.8的这些节点有两种组织方式 ,对于7个一下的,还是链表的结构。对于7个以上的,会转化成红黑树的结构来存储。
情况3,后面节点是树的结构。那么就调用TreeNode的put方法,有存在key,就替换,返回旧值,不存在就新建节点,返回null。
情况4,后面节点是链表的结构。如果遍历这些节点,存在相同的key,那么同情况2,新值覆盖旧值,返回旧值。如果遍历完,没有存在key,就新建节点,这个节点是挂在链表尾,返回null。1.6是挂在链表头。然后,如果链表长度大于阈值7,那么需要将这个链表结构,转化成树的结构。
1.8的HashMap新增一个内部类TreeNode,是继承LinkHashMap.Entry而来。当一个桶内的元素数量大于阈值(TREEIFY_THRESHOLD=8),会转化为树形,小于阈值(
UNTREEIFY_THRESHOLD)又会转换成链表结构。 这对于大量hash碰撞的情况有查找的优化,对于hash碰撞较小时,则不会用到TreeNode结构。
get()方法
1.8源码
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
} 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 && // 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;
}
get方法,如果找到了key,则返回value,否则,返回null。
会根据节点结构是红黑树,还是链表用不用的方式去遍历。
put的函数名是putVal,而get的函数名是getNode,这里不统一,有点奇怪。
remove()
删除方法,如果找到了key,返回删除的value,否则返回null。
根据node的结构,选择在树中遍历,还是以链表的方式遍历。
1.8源码
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;
//如果桶内没元素,直接返回null
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;
//第一个节点就找到了key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
//后面是TreeNode结构
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)))) {
//匹配到了key,根据是链表还是树,用不同方式删除。
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;
}
1.8中增加的代码主要都是因为引入了红黑树而引起的。导致许多地方都要考虑两种情况。
HashMap碰撞有没有那么频繁呢?如果碰撞真的这么厉害,这也算是一种优化吧。
Java提高(一)---- HashMap的更多相关文章
- java提高篇---HashMap
HashMap也是我们使用非常多的Collection,它是基于哈希表的 Map 接口的实现,以key-value的形式存在.在HashMap中,key-value总是会当做一个整体来处理,系统会根据 ...
- 【java提高】---HashMap解析(一)
HashMap解析(一) 平时一直再用hashmap并没有稍微深入的去了解它,自己花点时间想往里面在深入一点,发现它比arraylist难理解很多,好多东西目前还不太能理解等以后自己知识更加丰富在过来 ...
- Java提高篇(三三)-----Map总结
在前面LZ详细介绍了HashMap.HashTable.TreeMap的实现方法,从数据结构.实现原理.源码分析三个方面进行阐述,对这个三个类应该有了比较清晰的了解,下面LZ就Map做一个简单的总结. ...
- Java提高篇(二八)------TreeSet
与HashSet是基于HashMap实现一样,TreeSet同样是基于TreeMap实现的.在<Java提高篇(二七)-----TreeMap>中LZ详细讲解了TreeMap实现机制,如果 ...
- Java提高篇(二六)-----hashCode
在前面三篇博文中LZ讲解了(HashMap.HashSet.HashTable),在其中LZ不断地讲解他们的put和get方法,在这两个方法中计算key的hashCode应该是最重要也是最 ...
- java提高篇(二四)-----HashSet
在前篇博文中(java提高篇(二三)-----HashMap)详细讲解了HashMap的实现过程,对于HashSet而言,它是基于HashMap来实现的,底层采用HashMap来保存元素. ...
- java面试之Hashmap
在java面试中hashMap应该说一个必考的题目,而且HashMap 和 HashSet 是 Java Collection Framework 的两个重要成员,其中 HashMap 是 Map 接 ...
- Java基础系列--HashMap(JDK1.8)
原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/10022092.html Java基础系列-HashMap 1.8 概述 HashMap是 ...
- 死磕 java集合之HashMap源码分析
欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 HashMap采用key/value存储结构,每个key对应唯一的value,查询和修改 ...
随机推荐
- PHP语言开发微信公众平台(订阅号)之curl命令
在开发过程中,经常会遇到要求用curl命令调用接口的情况 那么,什么是curl,简单来说curl是一个利用url语法规定来传输文件和哦数据的工具,支持很多协议,如 http.ftp.telent 等, ...
- Java中实现短信发送
最近跟着做公司的项目偶然接触到的,顺势把这个给记录下来,给自己梳理一下. 采用引入第三方工具的方式,网上查了半天,发现简单的实现方式便是注册一个中国网建的账号,新建账号的时候会附带赠几条免费短信,彩信 ...
- Linux云自动化运维第三课
Linux云自动化运维第三课 一.正则表达式 1.匹配符 * ###匹配0到任意字符 ? ###匹配单个字符 [[:alpha:]] ###匹配单个字母 [[:lower:]] ###匹配单个小写字母 ...
- 常见端口、端口查询及TCP状态
查看电脑端口的开放情况命令:cmd——netstat -a -n -a:显示所有连接和监听端口:-n:以数字形式显示地址和端口号 “本地地址”指本地IP地址及其正在使用的端口号,“外部地址”指连接某端 ...
- 解决ubuntu更新中断后报错问题
今天在更新ubuntu的时候,更新了一半被我强制关闭了,就报错了 当再使用sudo apt-get update命令时出现了dpkg was interrupted,you must manually ...
- jst通用删除数组中重复的值和删除字符串中重复的字符
以下内容属于个人原创,转载请注明出处,非常感谢! 删除数组中重复的值或者删除字符串重复的字符,是我们前端开发人员碰到很多这样的场景.还有求职者在被面试时也会碰到这样的问题!比如:问删除字符串重复的字符 ...
- vueJS 获取后台数据 绑定data
//vue 环境安装http://blog.csdn.net/u013182762/article/details/53021374 一开始使用安装环境配置一些东西 ,后来发现太麻烦了 . 直接CD ...
- lsb_release 命令找不到
yum install redhat-lsb -y 参考:http://blog.chinaunix.net/uid-20606164-id-3485376.html
- Struts2之i18N国际化
对于i18n其实没有太多内容,一般的公司用不到这些内容,除非是跨国公司,但即便是跨国公司也不一定会使用i18n来进行国际化处理,所以本篇内容仅供大家了解,不做深入的探讨,希望通过本篇内容,可以帮助大家 ...
- Vue.js 学习笔记 一
本文的Demo和源代码已放到GitHub,如果您觉得本篇内容不错,请点个赞,或在GitHub上加个星星! https://github.com/zwl-jasmine95/Vue_test 以下所有知 ...