博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【JDK】:HashMap详解
阅读量:4286 次
发布时间:2019-05-27

本文共 5510 字,大约阅读时间需要 18 分钟。

Hash散列基本思想

哈希表使用数组和链表共同实现散列存储,每一个数组元素可以认为是散列表中的桶位(buket),每个桶位存放一个链表,该链表由散列码(hashCode)相同的节点构成。Hash表的查找就是根据需要查找的对象(key, value)中的key,利用散列函数计算key对应的hashCode,即数组的下标(buket的索引),在O(1)时间内找到对应的桶位,再遍历该桶位内的链表,查找对应的value值即可。

在JDK8中,当桶位数目过多(默认至少64)或者某一个桶位的链表长度过长时(默认是8),查找效率会显著降低。因此HashMap会将链表的普通节点转化为树节点(TreeNode)存储,链表List也将转为Tree树将搜索效率提升到O(logn),但是TreeNode的空间消耗是普通Node空间消耗的两倍,在HashMap进行多次remove操作之后,如果桶位数目和链表长度低于阈值,TreeNode重新转化为Node,Tree树转为List链表。

Hash表长度为 2n 与查找效率

Hash表中的table数组存放node,table的长度size必须为2的幂,在这个前提下有如下规律:对任意一个哈希码hashCode利用求余运算进行散列,即index=hashCode%size时,index为hashCode所在的数组桶位下标,由于求余取模运算效率低下,在size为2的幂的前提下,可以用位与运算代替,即index=hashCode & (size – 1),得到的是相同的结果。(参见博文)

下面是保证输入一个表的初始长度,是table的size总是2的幂:

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

重点函数实现

初始容量和负载因子

在HashMap中有两个很重要的参数,容量(Capacity)和负载因子(Load factor):

  • Initial capacity The capacity is the number of buckets in the hash table, The initial capacity is simply the capacity at the time the hash table is created.
  • Load factor The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.

简单的说,Capacity就是bucket的大小,Load factor就是bucket填满程度的最大比例。如果对迭代性能要求很高的话不要把capacity设置过大,也不要把load factor设置过小。当bucket中的entries的数目大于capacity*load factor时就需要调整bucket的大小为当前的2倍。

put函数的实现

  1. 对key的hashCode()做hash,然后再计算index;
  2. 如果没碰撞直接放到bucket里;
  3. 如果碰撞了,以链表的形式存在buckets后;
  4. 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
  5. 如果节点已经存在就替换old value(保证key的唯一性;
  6. 如果bucket满了(超过load factor*current capacity),就要resize。
public V put(K key, V value) {    // 对key的hashCode()做hash    return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,               boolean evict) {    Node
[] tab; Node
p; int n, i; // tab为空则创建 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 计算index,并对null做处理 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node
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
)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; // 超过load factor*current capacity,resize if (++size > threshold) resize(); afterNodeInsertion(evict); return null;}

get函数实现

  1. bucket里的第一个节点,直接命中;
  2. 如果有冲突,则通过key.equals(k)去查找对应的entry
    • 若为树,则在树中通过key.equals(k)查找,O(logn);
    • 若为链表,则在链表中通过key.equals(k)查找,O(n)。
public V get(Object key) {    Node
e; 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; 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) { // 在树中get if (first instanceof TreeNode) return ((TreeNode
)first).getTreeNode(hash, key); // 在链表中get do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;}

hash()、resize()

强烈推荐阅读,里面有详细的介绍。

相关问题

  1. 什么时候会使用HashMap?他有什么特点?

    是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。

  2. 你知道HashMap的工作原理吗?

    通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

  3. 你知道get和put的原理吗?equals()和hashCode()的都有什么作用?

    通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点

  4. 你知道hash的实现吗?为什么要这样实现?

    在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。

  5. 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

    如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。

推荐阅读

转载地址:http://guxgi.baihongyu.com/

你可能感兴趣的文章
javaScript使用Lodop实现网页表格套打功能
查看>>
技术大牛如何寻找下一个风口
查看>>
大数据学习路线大纲
查看>>
html入门之meta
查看>>
mvn不是内部或外部命令,也不是可运行的程序或批处理文件
查看>>
JAVA:JDBC连接MySQL数据库
查看>>
struts2流程简述
查看>>
struts2文件上传和下载
查看>>
值栈与OGNL
查看>>
struts2标签库--分类入门
查看>>
Struts2声明式异常处理
查看>>
Web.xml中jsp-config元素简述
查看>>
Struts2类型转换和自定义类型
查看>>
Java面向对象特征有那些
查看>>
hibernate单表继承映射
查看>>
继承结构中每个子类单独一张表
查看>>
HQL查询-分页-条件-连接-过滤使用
查看>>
hibernate加载策略之lazy
查看>>
hibernate抓取策略fetch
查看>>
Hibernate的N+1条SQL查询问题-------Iterate
查看>>