HashMap 内部原理

标签: hashmap
5895人阅读 评论(2) 收藏 举报
分类:

HashMap 内部实现

通过名字便可知道的是,HashMap 的原理就是散列。HashMap内部维护一个 Buckets 数组,每个 Bucket 封装为一个 Entry<K, V> 键值对形式的链表结构,这个 Buckets 数组也称为表。表的索引是 密钥K 的散列值(散列码)。如下图所示:

这里写图片描述

链表的每个节点是一个名为 Entry<K,V> 的类的实例。 Entry 类实现了 Map.Entry 接口,下面是Entry类的代码:

private static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    final int hash;
    V value;
    Entry<K,V> next;
}

注: 每个 Entry 对象仅与一个特定 key 相关联,但其 value 是可以改变的(如果相同的 key 之后被重新插入不同的 value) - 因此键是最终的,而值不是。 每个Entry对象都有一个名为 next 的字段,它指向下一个Entry,所以实际上为单链表结构。hash 字段存储了 Entry 对象在 Buckets 数组索引,也就是 key 的散列值。

如果发生Hash碰撞,也就是两个key的hash值相同,或者如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最早加入的放在链尾。

影响 HashMap 性能的两个因素是初始容量和负载因子。容量是表数组的长度,初始容量只是创建哈希表时的容量。负载因子是衡量哈希表在容量自动增加之前是否允许获取的量度(比例)。

当散列表中的 Entry 数量超过负载因子和当前容量的乘积时,将会重新散列该表(也就是重建内部数据结构),使得散列表具有大约两倍的容量(这个其实和ArrayList类似)。

理解 put() 方法

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key); // 计算hash值
        int i = indexFor(hash, table.length); // 计算在数组中的索引
        // 遍历链表
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // hash值相同并且key相等,就直接替换
            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;
    }
注:这个计算出来的hash值被传递给内部哈希函数,哈希函数将返回密钥的散列值。这个值就是 bucket/数组 的索i引。
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

这里就有个疑问了,我们如何计算对应存储数组索引,首先想到的就是把hashcode对数组长度取模运算,也就是h%length,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那中?

首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。

如下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!

这里写图片描述

上图参考自:http://blog.csdn.net/oqqYeYi/article/details/39831029

理解 get() 方法

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key); // 计算hash值
        // 根据索引遍历链表,找出相等的key
        for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

get 与 put 总结

下面总结了 put()get() 发生的三个重要步骤:

  1. 通过调用 计算 Hash Code 方法计算密钥的哈希码。
  2. 将计算的散列码传递到内部散列函数indexFor()以获取表的索引。
  3. 迭代通过在索引处出现的链表,并调用 equals() 方法来查找匹配键。

所以在这之前要先理解 equals()hashCode() 这两个方法。

在 Java8 中的改善

在Java 8中,对HashMap有一个性能上的改进。当密钥中存在许多哈希冲突(不同的密钥最终具有相同的哈希值或索引)时,平衡树将用于存储 Entry 对象,而不是链表。做法是,一旦 bucket 中的 Entry 数量增长超过某一阈值,则 bucket 将从 Entry 链表切换到平衡树。

查看评论

深入理解Java中的HashMap的实现原理

1. HashMap为了提高查找的效率使用了分块查找的原理,对象的hashCode返回的哈希值进行进一步处理,这样就有规律的把不同的元素放到了不同的区块或桶中。下次查找该对象的时候,还是计算其哈希值,...
  • sunqunsunqun
  • sunqunsunqun
  • 2015-06-22 18:44:08
  • 3560

HashMap的设计原理和实现分析

HashMap在Java开发中有着非常重要的角色地位,每一个Java程序员都应该了解HashMap。     本文主要从源码角度来解析HashMap的设计思路,并且详细地阐述HashMap中的几个概...
  • feixiaohuijava
  • feixiaohuijava
  • 2016-04-29 16:55:41
  • 1580

java集合中HashMap原理详解

HashMap在Java开发中有着非常重要的角色地位,每一个Java程序员都应该了解HashMap。 主要从源码角度来解析HashMap的设计思路,并且详细地阐述HashMap中的几个概念,并深入探...
  • chenleixing
  • chenleixing
  • 2015-01-07 16:19:42
  • 1513

HashMap遍历方法和实现原理分析

HashMap遍历方法;HashMap实现原理分析
  • baidu_16757561
  • baidu_16757561
  • 2015-11-15 17:10:50
  • 1190

LRUCache原理及HashMap LinkedHashMap内部实现原理

LRUCache HashMap LinkedHashMap内部实现原理
  • hlglinglong
  • hlglinglong
  • 2015-11-27 17:11:18
  • 2320

了解HashMap的get和put内部的工作原理,需要理解透Java HashMap的原理

了解HashMap的get和put内部的工作原理,需要理解透Java HashMap的原理,今天我们单说get和put 的工作原理。 一、Put : 让我们看下put方法的...
  • chajinglong
  • chajinglong
  • 2016-03-12 22:01:28
  • 1071

HashMap的工作原理--重点----数据结构示意图的理解

HashMap的工作原理是近年来常见的Java面试题。几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道HashTable和HashMap之间的区别,那么为何这道面试题如此...
  • qq_27093465
  • qq_27093465
  • 2016-08-15 11:43:20
  • 8095

HashMap的实现原理以及面试官的提问

HashMap:按照特性来说明一下,储存的是键值对,线程不安全,非Synchronied,储存的比较快,能够接受null。按照工作原理来叙述一下,Map的put(key,value)来储存元素,通过g...
  • qq_32519097
  • qq_32519097
  • 2016-10-04 17:45:07
  • 2457

从代码层读懂 Java HashMap 的实现原理

概述 Hashmap继承于AbstractMap,实现了Map、Cloneable、Java.io.Serializable接口。它的key、value都可以为null,映射不是有序的。 Ha...
  • sdmxdzb
  • sdmxdzb
  • 2017-03-22 13:53:30
  • 1113

HashMap实现原理分析

HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。   首先...
  • vking_wang
  • vking_wang
  • 2013-11-05 15:23:28
  • 302202
    个人资料
    等级:
    访问量: 80万+
    积分: 6143
    排名: 5078