HashMap实现原理分析

本文深入探讨了HashMap的工作原理,包括其内部结构、散列函数构造方法、冲突处理方式以及拉链法的具体实现。通过实例分析了put方法的过程,揭示了HashMap如何通过键的hashCode快速存取元素。

前言:        

HashMap的包结构

HashMap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

*注意,此实现不是同步的。

好吧“哈希表”是啥,内部结构是什么样的?

哈希表:

什么是哈希表?

哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
散列函数的构造方法: (1)直接定址法 (2)除留余数法 (3)平方取中法 (4)折叠法 (5)数值分析法
构造散列函数的目的是减少冲突,但要完全避免冲突是不可能的,只能尽可能减少冲突。 处理冲突的方法: (1)开放定址法 (2)二次探测法 (3)链地址法(拉链法)

HashMap是怎么实现散列函数和处理冲突的?

public static void main(String[] args) {
		HashMap<String, String> blogMap=new HashMap<>();
		blogMap.put("博文测试", "博文测试Value");
		blogMap.get("博文测试");//此行打断点
	}

观察Variables,如下图


在图中我们看到了,table是一个数组,看看我们数据在数组中是什么样的

数组索引为9,该位置下存储的数据节点为,看到关键字next,该结构这样看应该是一个数组加链表没错了,那么这样映射了hash表中哪种解决冲突的方法了,对比刚刚列出的几种方法,结构一致的只有拉链法了。


拉链法的简单介绍:

拉链法,我们可以理解为“链表的数组”,他是一种数组加链表的结构,其数据结构如图:


拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。
*->具体构造过程(动画演示: 点击打开链接) 草鸡生动形象啊!,简单生动,建议耐心看完


HashMap 拉链法的实现分析

来我们看看,hashMap是如何计算key的hash的,并如何决定将value放置到具体的数组索引中的,打开源码(可配合Java6的中文API文档,文档注释是人工翻译比较准确,笔者开的是Java8的源码)Java Doc  在线地址(http://tool.oschina.net/uploads/apidocs/jdk-zh/java/util/HashMap.html)
HashMap中的m(散列表长度)和a(装载因子)
JDK源码中定义如下默认值

默认加载因子:
/**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认散列表长度:
/**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
HashMap中的PUT方法
我们先从HashMap最常用的两个方法(put,get)为入口对代码进行分析
.put方法源码:
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);//我们看到在方法内部对key做了一次hash运算,然后调用putVal方法,我们暂且不关心后两个参数在干嘛,回头解释
    }

由以上源码,我们知道,在将key和value放入到hashMap中以前,先对key做了一次hash运算,hash算法是什么样的呢

static final int hash(Object key) {//先判断key是否为null,如果为null,hash值为0,否则以hashCode方法值为准
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
*以上引出一个知识点,这就是为什么重写equals方法时,为什么强烈建议重写hashCode方法的原因了
1、集合类判断两个对象是否相等,是先判断equals是否相等,如果equals返回TRUE,还要再判断HashCode返回值是否ture,只有两者都返回ture,才认为该两个对象是相等的。
2、由于Object的hashCode返回的是对象的hash值,所以即使equals返回TRUE,集合也可能判定两个对象不等,所以必须重写hashCode方法,以保证当equals返回TRUE时,hashCode也返回Ture,这样才能使得集合中存放的对象唯一。

接着来,进入到putVal方法内部
    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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) //这一行的意思是如果table(主存储结构)没有初始话,则初始化一个数组并赋值给tab,初始化代码在resize方法中
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) //根据散列值算出数组索引((n - 1) & hash)并判断该索引下是否有值,如果没有则新建一个首节点
            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))))//如果该节点的key一致,那么e=p
                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指针为null
                        p.next = newNode(hash, key, value, null);  //为p后面添加一个节点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st   //如果链表过长时,转换成红黑树。这个值表示当某个箱子中,链表长度大于 8 时,有可能会转化成树。
                            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) //map里的元素个数(size)大于一个阈值(threshold)时,map将自动扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }
可以发现HashMap通过键的hashCode来快速的存取元素。
当不同的对象hashCode发生碰撞时,HashMap通过单链表来解决,将新元素加入链表表头,通过next指向原有的元素。单链表在Java中的实现就是对象的引用(复合)。

综上一次put的大概逻辑是
1.计算key的hashCode
2.根据hashCode计算bucketIndex(  (DEFAULT_INITIAL_CAPACITY  - 1) & hash)
在bucketIndex中的处理分三种情形
3.1 BucketIndex没有元素时,直接添加节点
3.2 对象相同时会替换原有KEY
3.3 BucketIndex位置有元素时


到这里,我们了解了HashMap工作原理的一部分,那还有另一部分,如,加载因子及resize(),HashMap通常的使用规则,这些会留在下一章说明。





评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值