HashMap与HashTable区别以及原理解析

前言

需要知道的是,HashMap在Java8里发生了比较大的变化。在Java8中,table的桶(bucket)中,每个桶跟之前一样,是维护一个链表。但是如果链表大小超过阈值(TREEIFY_THRESHOLD=8),链表就会被改造为红黑树结构。
这是因为,在大量hash碰撞下,Map就会被弱化为类似链表的结构(全部存在一个bucket中),这样会大大降低性能。在超过阈值以后,改为红黑树的方式,可以提高查找性能。

本文最下方还介绍了 LinkedHashMap 和 TreeMap 的区别。


Map整体结构

Map通常被包含在集合框架里,但是Map并不继承自 Collection 接口,最底层接口是Map。
类的继承结构可以参考下图:

Map整体结构图



HashMap和HashTable区别

  1. 最主要区别在于线程安全,HashMap不是线程安全的,而HashTable是线程安全的。
    hashtable内部添加了synchronized关键字来确保线程同步。而HashMap没有,因此性能上来说,HashMap会相对高一些。
    我们平时使用一般情况下是用HashMap,而如果要在多线程情况下使用HashMap,可以使用Collections.synchronizedMap()来获取一个线程安全的集合。

Collections.synchronizedMap() 的实现原理是,Collections内部定义了一个SynchronizedMap的内部类,这个类实现了Map接口,使用synchronized关键字在复写的方法中实现线程安全,然后操作传入的实例对象。

//以下是Collections.SynchronizedMap 内部的put方法实现
public V put(K key, V value) {
    synchronized (mutex) {return m.put(key, value);}
}


  1. HashMap可以使用null作为key,而HashTable不允许这样做。
    虽然HashMap支持null作为key,但一般不建议这样做。因为这样做比较坑╮(╯▽╰)╭。另外HastTable已经不建议在代码中使用,如果需要线程安全的场景,建议使用ConcurrentHashMap
    顺便说一下,如果HashMap以null作为key的话,Entry节点对象总是存在table数组的第一个节点上

  2. HashMap是对Map接口的实现,HashTable在实现了Map接口的同时,继承了Dictionary抽象类
    这俩都实现了 Map Coneable 和 Serializable 接口,但是没有实现Collection接口。

  3. 构造器有两个可选参数:int initialCapacity, float loadFactor。分别表示初始容量 和,加载因子
    两个默认的加载因子都是0.75。如果容量达到荷载因子*size的话,就要进行扩容(resize),扩容的同时要重新计算每个元素在新数组中的位置,因此性能开销较大。
    HashMap的初始容量是16,扩容时按2的指数幂扩容。
    HashTable的初始容量是11,扩容时按2的指数幂+1扩容
    而关于加载因子为什么选用0.75

Hash表为解决冲突,可以使用开放地址法和链地址法来解决,Java的HashTable采用了链地址法,即数组+链表。荷载因子就是一个特别重要的参数,应该严格限制在0.7~0.8以下。超过0.8查表时CPU缓存不命中概率将按照指数级上升,严重降低性能。hash表的平均查找长度,是关于荷载因子a的函数


  1. 两者计算hash的方法不同
    都是根据key进行hash运算后确定存储地址index。
    HashTable计算hash是用key的hash对数组长度进行取模运算
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

而hashMap为了减少Hash碰撞概率,进行了二次hash
以下是源码实现。注意jdk1.7和jdk1.8的实现方法不一样

//JDK1.7 方法一:
final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();

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

//JDK1.8 方法一:
static final int hash(Object key) {   //jdk1.8
     int h;
     // h = key.hashCode() 为第一步 取hashCode值
     // h ^ (h >>> 16)  为第二步 高位参与运算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//方法二:
static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
     return h & (length-1);  //第三步 取模运算
}

这里要注意,为什么这里需要将高位数据移位到低位进行异或运算呢?
这是因为有些数据计算出的哈希值差异主要在高位,而 HashMap 里的哈希寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。

为了分布均匀,我们首先想到的就是把hash值对数组长度取模运算,这样元素的分布相对均匀。但是取模运算对于性能消耗还是比较大的。
在HashMap中是这样做的,调用方法二来计算index。
这个方法非常巧妙。它通过h & (table.length -1)得到该对象的保存位,而HashMap低层数组的长度总是2的n次方,这是HashMap在速度上的优化。而当length总是等2的n次方时,h & (table.length -1) 运算等价于对length取模,也就是h%table.length,但是&操作更有效率。

  1. HashMap和HashTable都是 使用链地址法解决Hash碰撞,使用 数组+链表 的方式存储。(JDK1.8中链表改为了红黑树)
    如果hash碰撞,则会用equals()比较key到底是否相同。相同则覆盖,如果不相同,则会放到相同的bukectIndex下面,用链表存储下一个元素的位置。
    Entry是HashMap的一个静态内部类,实现一个链表结构,保存了之前的Enrty对象为next元素。
    以下为JDK1.7的Map部分源码。
static class Entry<K,V> implements Map.Entry<K,V>{ 
       final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
}

//put源码 JDK1.7
public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            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;
    }

//校验并扩容
void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

//创建Entry节点,相同bucketIndex创建为链表节点
void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }



HashMap和HashTable的实现原理

  • resize()方法
    由第6点可以看到,在addEntry()方法内,每次执行createEntry()之前,都需要校验一遍是否需要扩容。
    (size >= threshold) && (null != table[bucketIndex])
    如果size达到了临界值,并且当前要添加的节点不为null。就要进行扩容。
    具体过程为:
    先创建一个容量为table.length*2(也就是2次方翻倍)的新table,修改临界值。然后把HashMap里的所有元素根据新的length重新计算index放入新的table中。
    这里要注意,是用 每个元素的hash重新计算Index,而不是简单的把原table的index放到新table中。(因为计算index时,是根据length长度均匀分布的)所以resize()是比较消耗性能的操作。如果在创建map时能大概预见到大小,那么设置一个相对大点的初始容量是一个不错的选择。
    resize()源码就不放了。JDK1.8有新实现

  • hash() 和 indexFor()
    hash()是方法是为了对key.hashCode()进行二次散列,以获得更好的散列值,减少碰撞概率。
    indexFor()是根据散列值计算出存储在table中的下标,与table.length取模。均匀分布。



LinkedHashMap 和 TreeMap

这两种Map都可以保证某种顺序,但是实现原理是不同的。

  • LinkedHashMap

提供的是遍历顺序符合插入顺序,它的实现是通过为Entry(键值对)维护一个双向链表。
部分源码如下:

/**
 * HashMap.Node subclass for normal LinkedHashMap entries.
 */
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}


  • TreeMap
    它的整体顺序是由键来确定的,通过 Comparator 或 Comparable(自然顺序)来决定。



最后说一下HashSet

内部实现是一个HashMap,只不过value是一个固定对象。
HashSet部分源码如下

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

参考资料:
HashMap原理解析(较全面)
HashMap与HashTable实现原理浅析
Java8系列之重新认识HashMap(深度解析)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容