JAVA集合源码分析系列:HashMap源码分析

一.大学《数据结构》散列表复习

散列表(Hash)表,你要记住一句话:根据给定的关键字来计算出关键字在表中的地址。在Hash表中,关键字和关键字的地址是有确定的关系的,这种关系可以用Hash函数H来表示。例如,关键字为key,则H(key)成为Hash地址,就是key在查找表中的地址。

1.什么是Hash表

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

2.什么是Hash

Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

HASH主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做HASH值. 也可以说,hash就是找到一种数据内容和数据存放地址之间的映射关系。

3.散列表的建立方法和冲突解决方法

例题:

关键字序列为{7,4,1,14,100,30,5,9,20,134},设Hash函数为H(key)=key Mod 13,试给出表长为13的Hash表。

结果:

《JAVA集合源码分析系列:HashMap源码分析》

常用的Hash函数的构造方法

直接定址法
取关键字或关键字的某个线性函数为Hash地址,即H(key)=key或者H(key)=a * key + b,其中a和b为常数。

数字分析法

平方取中法
取关键字平方后的中间几位作为Hash地址。通常在选定Hash函数的时候不一定能知道关键字的全部情况,仅取其中的几位为地址不一定合适,而一个数平方后的中间几位数和数的每一位都相关,因此得到的Hash地址随机性更大,取的位数由表长决定。

除留余数法
取关键字被某个不大于Hash表表长m的数p除后所得的余数为Hash地址,即:
H(key) = key Mod p(p <= m)

常用的Hash冲突处理方法

开放定址法(线性探查法、平方探查法)

链地址法
这里着重讲解链地址法,因为下文我们分析的HashMap的内部实现就是采用的这种方案。链地址法是把所有的同义词用单链表连接起来的方法。在这种方法中,Hash表每个单元中存放的不再是记录本身,而是相应同义词单链表的表头指针。如下图:

《JAVA集合源码分析系列:HashMap源码分析》

填装因子是关键字个数和表长度的比值。通过它可以计算表长度。

4.散列表的性能分析

哈希表是种数据结构,它可以提供快速的插入操作和查找操作。第一次接触哈希表时,它的优点多得让人难以置信。不论哈希表中有多少数据,插入和删除(有时包括侧除)只需要接近常量的时间即0(1)的时间级。实际上,这只需要几条机器指令。

对哈希表的使用者一一人来说,这是一瞬间的事。哈希表运算得非常快,在计算机程序中,如果需要在一秒种内查找上千条记录通常使用哈希表(例如拼写检查器)哈希表的速度明显比树快,树的操作通常需要O(N)的时间级。哈希表不仅速度快,编程实现也相对容易。

哈希表也有一些缺点它是基于数组的,数组创建后难于扩展某些哈希表被基本填满时,性能下降得非常严重,所以程序虽必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。

hash 表查询是被设计成O(1)的 但是事实上要根据hash算法的碰撞几率定的,在最差的情况下也是个O(n)的。

二.HashMap源码分析

HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。

内部结构:

《JAVA集合源码分析系列:HashMap源码分析》

1、和上图显示的一样,HashMap内部包含了一个Entry类型的数组table, table里的每一个数据都是一个Entry对象。

2、再来看table里面存储的Entry类型,Entry类里包含了hashcode变量,key,value 和另外一个Entry对象。为什么要有一个Entry对象呢?其实如果你看过linkedList的源码,你可能会知道这就是一个链表结构。通过我找到你,你再找到他。不过这里的Entry并不是LinkedList,它是单独为HashMap服务的一个内部单链表结构的类。

3、那么Entry是一个单链表结构的意义又是什么呢?在我们了解了HashMap的存储过程之后,你就会很清楚了,接着让我们来看HashMap怎么工作的。

1.构造函数

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

有多个构造方法这里拿一个分析,DEFAULT_LOAD_FACTOR = 0.75f;这里主要是初始化填充因子为0.75.前面讲过填充因子是关键字个数和表长度的比值。

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

3.案例说明

HashMap hashMap = new HashMap();//line1
hashMap.put("one","hello1");//line2
hashMap.put("two","hello2");//line3
hashMap.put("three","hello3");//line4
hashMap.put("four","hello4");//line5
hashMap.put("five","hello5");//line6
hashMap.put("six","hello6");//line7
hashMap.put("seven","hello7");//line8

《JAVA集合源码分析系列:HashMap源码分析》

《JAVA集合源码分析系列:HashMap源码分析》

参考资料

十一、从头到尾解析Hash表算法
http://blog.csdn.net/v_JULY_v/article/details/6256463

hash算法 (hashmap 实现原理)
http://zha-zi.iteye.com/blog/1124484

Hashtable 的实现原理
http://wiki.jikexueyuan.com/project/java-collection/hashtable.html

哈希表查询的时间复杂度是多少?
https://www.zhihu.com/question/34039763

分析的最透彻的一个
http://blog.csdn.net/yissan/article/details/50888070

http://www.jianshu.com/p/138ccbc75803

以下文章最全面
https://github.com/GeniusVJR/LearningNotes/blob/master/Part2/JavaSE/HashMap%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90.md

http://blog.csdn.net/codeemperor/article/details/51351247

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/a910626/article/details/52357077
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞