聊聊HashMap

作者: 你是妖怪吧 | 来源:发表于2017-08-16 03:32 被阅读0次

HashMap是Java程序猿平时用的较多的一种数据结构,也经常会在各种面试中被问起。喜欢看JDK源码的同学肯定会发现随着JDK版本的更新迭代,HashMap的底层实现也在不断地优化,那么本文我们就来聊聊HashMap的实现结构和原理吧

概述

java.util.Map接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap,和TreeMap,类继承关系如下图所示:

1.HashMap:它根据键的hashCode值存储数据,HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,如果需要线程安全,可以用Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

2.Hashtable:Hashtable继承自Dictionary类,线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap使用分段锁。Hashtable不建议使用,需要线程安全的场合可以用ConcurrentHashMap替换。

3.LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,使用Iterator遍历时,先得到的记录肯定是先插入的。

4.TreeMap:TreeMap实现SortedMap接口,保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

存储结构

从JDK1.8之后,HashMap的存储结构使用哈希桶数组+链表/红黑树实现。为了解决哈希冲突,HashMap采用了链地址法,但是这样避免不了链表过长导致HashMap性能严重下降。所以当链表长度超过默认值8的时候,链表就会转为红黑树。红黑树是一棵二叉查找树,但它在二叉查找树的基础上增加了相关特性使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。

重要方法

1.tableSizeFor()

源码

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

调用的地方

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

HashMap的capacity都是2的幂,这个方法是用于找到大于或者等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数,这就是为什么第一步需要int n = cap -1的原因。

下面看看这几个无符号右移操作:

第一次右移

n |= n >>> 1;

由于n不等于0,则n的二进制表示中总会有一bit为1,这时考虑最高位的1,假设n为01xxxxxxx。通过无符号右移1位,则将最高位的1右移了1位,再做或操作,使得n的二进制表示中与最高位的1紧邻的右边一位也为1,如011xxxxxxxx。

第二次右移

n |= n >>> 2;

此时n为011xxxxxxxx ,则n无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1。如01111xxxxxx。

第三次右移

n |= n >>> 4;

这次把已经有的高位中的连续的4个1,右移4位,再做或操作,这样n的二进制表示的高位中会有8个连续的1。如011111111xxxxxx 。

。。。

以此类推,容量最大也就是32bit的正数,因此最后n |= n >>> 16 ,最多也就32个1,但是这时已经大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY,不大于MAXIMUM_CAPACITY的时候则n+1,这样就华丽丽的取到了大于或等于n的那个最大2次幂值,作为HashMap的容量。还是蛮巧妙的一个算法哈~

2.hash()

1.8版本

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

1.7版本

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

这段代码其实叫扰动函数,为啥要这样搞呢,不能取到key的hashCode的时候直接用下面的方法去求模找对应哈希桶数组的下标位置吗?

static int indexFor(int h, int length) {  
    //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
    return h & (length-1);
}

顺带说一下,这也正好解释了为什么HashMap的数组长度要取2的整次幂。因为这样(数组长度-1)正好相当于一个"低位掩码"。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。2进制表示是<b>00000000 00000000 00001111</b>。和某散列值做“与”操作如下,结果就是截取了最低的四位值。

10100101 11000100 00100101
00000000 00000000 00001111
----------------------------------
00000000 00000000 00000101    //高位全部归零,只保留末四位

但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就无比蛋疼。

这时候“<b>扰动函数</b>”的价值就体现出来了,说到这里大家应该猜出来了。看下面这个图:


向位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

但明显Java 8觉得扰动做一次就够了,像1.7做4次的话,多了可能边际效用也不大,所谓为了效率考虑就改成一次了。

相关文章

  • 聊聊HashMap

    HashMap jdk1.7实现版本 数组+链表 引入数组+链表。很简单,初始化一个数组,解决冲突的办法就是开链法...

  • 聊聊HashMap

    HashMap是Java程序猿平时用的较多的一种数据结构,也经常会在各种面试中被问起。喜欢看JDK源码的同学肯定会...

  • 聊聊HashMap原理

    能力要进阶,必然就要多了解了解原理。看了一些Java的数据结构,简单的总结下HashMap原理。所谓map就是一个...

  • 简单聊聊 HashMap

    哈喽,今天我们来聊聊HashMap。 HashMap相信大家在平时开发的时候也会经常用到,它是基于哈希表的 Map...

  • 聊聊java中的哪些Map:(六)ConcurrentHashM

    [toc] 在聊完HashTable和HashMap的区别之后,自然该到了聊聊ConcurrentHashMap的...

  • ConcurrentHashMap锁机制进化的考量

    前言 又是有一段时间没写过Java相关的东西了。本来是想返璞归真一把,聊聊HashMap的,但HashMap的内容...

  • HashMap原理介绍

    今天咋来聊聊hashMap的原理。hashMap是使用频率最高的集合对象之一了,说到集合框架就不得不先来看一幅图。...

  • 聊聊HashMap、ConcurrentHashMap和Hash

    1、HashMap1.1、了解HashMap首先了解以下几个参数:①capacity 容量 默认是static f...

  • Java源码解读 --- ArrayList

    上次说到HashMap的源码,这次来聊聊ArrayList的源码。 ArrayList,顾名思义,底层是用Arra...

  • 聊聊java中的哪些Map:(二)HashMap中的TreeNo

    [toc]本文是上一篇聊聊java中的哪些Map:(一)HashMap(1.8)源码分析中对于treeNode的补...

网友评论

    本文标题:聊聊HashMap

    本文链接:https://www.haomeiwen.com/subject/erpbrxtx.html