Java -- 集合 -- HashMap的实现及HashTable,LinkedHashMap, WeakHashMap区别

上一篇文章中介绍了集合类的框架及相关的类区别。这一节我们来学习一下 HashMap 到底是怎么实现的,散列表的实现原理,扩容及树化的过程(基于JDK 1.8)。

HashMap 底层存储结构

JAVA语言中,最基本的存储结构只有两种:数组引用(模拟指针)。所有的数据结构都是用这两个基本结构来构造的,HashMap 当然也是。

那先用一张图来展示一下 HashMap 的存储结构:


显而易见,HashMap 实际是一个 “数组加链表”的存储结构,链表其实就是引用实现的。这种结构,我们又称之为“哈希表”或“散列表”。

散列表中,元素的存储并不是顺序的,而是根据散列算法实现。


散列表原理

数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)

线性链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)

二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。

哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。

  我们知道,数据结构的物理存储结构只有两种:顺序存储结构链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组

  比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。

        存储位置 = f(关键字)

  其中,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。举个例子,比如我们要在哈希表中执行插入操作:

  

  查找操作同理,先通过哈希函数计算出实际存储地址,然后从数组中对应地址取出即可。

  哈希冲突

  然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。


HashMap JAVA实现原理

在JAVA中,将数组中的每个位置称之为 , 而 桶 后面的每个数据称之为 bin (来自JDK1.8)。

先看一些比较重要的变量(摘自 JDK 1.8):

    transient Node<K,V>[] table;

    transient Set<Map.Entry<K,V>> entrySet;

    transient int size;

    transient int modCount;

    int threshold;

    final float loadFactor;
table

保存元素的散列表,是一个数组,数组的每一个元素称之为桶,桶中可以使用线性链表或二叉树,桶中的元素称之为bin。

entrySet


size

map中元素的个数。

capacity

容量,散列表中桶的个数,即 table 数组的大小。

默认值为16,每次都是2倍扩容。容量都是2的幂次。最大值为 1<<30。

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    static final int MAXIMUM_CAPACITY = 1 << 30;
loadFactor

装载因子,用来衡量hashmap 满的程度,影响扩容时机,默认值为0.75。

计算实时装载因子的方法:size / capacity 。

  static final float DEFAULT_LOAD_FACTOR = 0.75f;
threshold
扩容阈值,当 hashmap 中 元素的个数 size > threshold 时, 进行扩容 resize 操作。

threshold = capacity * loadfactor

下面还有3个重要的常量:

TREEIFY_THRESHOLD

由线性链表转化为树的阈值,默认值为 8。桶中bin的数量超过该阈值,就由树来代替链表。

static final int TREEIFY_THRESHOLD = 8;
UNTREEIFY_THRESHOLD

由树转化为链表的阈值,默认值为 6。当桶中bin的数量小于该阈值,就将树转化为链表。

 static final int UNTREEIFY_THRESHOLD = 6;
MIN_TREEIFY_CAPACITY

桶中bin 被树化时,最小的hash表容量,默认为 64 。当散列表容量小于该阈值,即使桶中bin的数量超过了 treeify_threshold ,也不会进行树化,只会进行扩容操作。 

min_treeify_capacity 至少是 treeify_threshold 的4倍。

static final int MIN_TREEIFY_CAPACITY = 64;

接下来,看一下 HashMap 的主干:

HashMap 的主干是一个 Node(Entry) 数组。 Node 是 HashMap 的基本组成单元,也就是一个元素,包含 key-value 对。

Node 是 HashMap 的一个静态内部类,实现了 Entry 接口(JDK1.8):

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;        // key的hash值进行hash运算的结果
        final K key;
        V value;
        Node<K,V> next;        // 下一个Node 的引用,单链表结构

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
}

HashMap 的元素结构如下(图片摘自):



可以看出,HashMap 就是由“数组 + 链表”实现的。数组是主体,链表是为了解决 hash 冲突而存在的。

如果定位到的数组不含有链表,那么对于元素的查找,添加等操作很快,只需要一次寻址就可以;如果含有链表,对于添加操作依然是 O(1),但是对于查询来说,此时还需要遍历链表,通过 key 对象的 equals 方法依次对比。因此,根据性能来说,hashmap中的链表越少越好。

元素的Hash计算方法

hashCode() 方法是 Object 对象的 native 方法,所有类都继承自 Object 对象,默认都是该方法生成的。


HashMap的扩容树化过程

创建一个HashMap,在里面不停地增加 key hash值相同的 bin<key, value>,也就是这些数据都会被添加到同一个桶中。然后再debug模式下,查看HashMap的内存结构:

初始化HashMap    capacity = 16    threshold = 12     

                              treeify_threshold = 8    min_treeify_threshold = 64     untreeify_threshold = 6

执行PUT操作后,HashMap中元素的数量为 q,由于都在同一个桶中,也为桶中元素的数量。

q = 8:  capacity = 16, 不扩容。

            由于 q<threshold,不进行扩容;q<= treeify_threshold, 桶中元素仍为链表结构。

q = 9 :    capacity = 32, threshold = 24 进行扩容,不树化。 

            由于 q<threshold , 不扩容;

            但 q>treeify_threshold,需要进行树化,又因为 capacity(16) < min_treeify_threshold (64), 不允许树化, 强制扩容。

q = 10 :  capacity = 64, threshold = 48 进行扩容,树化。 

            由于 q<threshold , 不扩容;

            但 q>treeify_threshold,需要进行树化,又因为 capacity(16) < min_treeify_threshold (64), 不允许树化, 强制扩容。

q = 11 :  capacity = 64, threshold = 48 进行树化。 

            由于 q<threshold , 不扩容;

            但 q>treeify_threshold,且capacity(64) >= min_treeify_threshold (64) ,进行树化。

q = 49:  capacity = 128, threshold = 96 进行扩容 。 

            由于 q<threshold , 不扩容;

            但 q>treeify_threshold,且capacity(64) >= min_treeify_threshold (64) ,进行树化


HashMap的扩容树化原理

根据扩容源码来分析:

final Node<K, V>[] resize() {
		Node<K, V>[] oldTab = table;
		int oldCap = (oldTab == null) ? 0 : oldTab.length;
		int oldThr = threshold;
		int newCap, newThr = 0;
		
		if (oldCap > 0) {
			if (oldCap >= MAXIMUM_CAPACITY) {
				// 如果capacity已经扩容到最大(2^31-1),则不进行扩容
				threshold = Integer.MAX_VALUE;
				return oldTab;
			} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
				// capacity > 16  且  capacity*2 < MAXIMUM_CAPACITY, 则进行扩容2倍
				newThr = oldThr << 1; 
		} else if (oldThr > 0)
			// 如果capacity < 0  且  threshold > 0, 则 capacity = threshold
			newCap = oldThr;
		else {
			// 如果capacity < 0  且  threshold < 0, 初始化table(都使用默认值)
			newCap = DEFAULT_INITIAL_CAPACITY;
			newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
		}
		
		// 计算新的threshold
		if (newThr == 0) {
			float ft = (float) newCap * loadFactor;
			newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
		}
		threshold = newThr;
		
		// 将旧table中的数据移到扩容后的table中
		@SuppressWarnings({ "rawtypes", "unchecked" })
		Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
		table = newTab;
		if (oldTab != null) {
			for (int j = 0; j < oldCap; ++j) {
				Node<K, V> e;
				if ((e = oldTab[j]) != null) {
					oldTab[j] = null;
					if (e.next == null)
						// 如果旧table的桶中只有一个bin, 将bin直接剪切到新table中
						newTab[e.hash & (newCap - 1)] = e;
					else if (e instanceof TreeNode)
						// 如果旧table的桶是树形bin, 使用树复制方式
						((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
					else {
						// 如果旧table的桶是线性链表bin, 使用链表复制方式
						Node<K, V> loHead = null, loTail = null;
						Node<K, V> hiHead = null, hiTail = null;
						Node<K, V> next;
						do {
							next = e.next;
							// 由于 存储位置 = Key.hashCode ^ (capacity-1), capacity扩大2倍后,key的hash值也会向左多取1位
							// 若多取的最高为0, hash值保持不变; 若为1, hash值则扩大2倍
							// 下面的代码就是将原来的链表, 根据扩大后的新hash值,拆分为两个链表,分别存储在新table中的不同桶中。
							// lo 代表高位为0, hi 代表高位为1, tail 为链尾, head 为链头
							if ((e.hash & oldCap) == 0) {	// 如果
								if (loTail == null)
									loHead = e;
								else
									loTail.next = e;
								loTail = e;
							} else {
								if (hiTail == null)
									hiHead = e;
								else
									hiTail.next = e;
								hiTail = e;
							}
						} while ((e = next) != null);
						// 将 lo 链表放到新table的 j 位置, 将 hi链表放置到新链表的 j+oldCapacity 位置
						if (loTail != null) {
							loTail.next = null;
							newTab[j] = loHead;
						}
						if (hiTail != null) {
							hiTail.next = null;
							newTab[j + oldCap] = hiHead;
						}
					}
				}
			}
		}
		return newTab;
	}

上面源码中可以很清楚的看出HashMap的扩容机制。 在扩容中会将旧table中的元素复制到新的table中,但是元素的位置可能会发生改变。单bin 和 链表bin 的复制原理很简单,在代码中注释中已经表述的很清楚了。树形Bin扩容方法有点区别,如下:

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
         TreeNode<K,V> b = this;
         // Relink into lo and hi lists, preserving order
         TreeNode<K,V> loHead = null, loTail = null;
         TreeNode<K,V> hiHead = null, hiTail = null;
         int lc = 0, hc = 0;
         // 树形Bin的复制其实与线性链表bin很相似, 也是根据扩容后hash值的最高位, 分解成两个链表
         // 区别在于分解后的两个链表, 如果 元素个数  < UNTREEIFY_THRESHOLD ,会将树转化为线性链表bin; 否则就会进行树化
         for (TreeNode<K,V> e = b, next; e != null; e = next) {
             next = (TreeNode<K,V>)e.next;
             e.next = null;
             if ((e.hash & bit) == 0) {
                 if ((e.prev = loTail) == null)
                     loHead = e;
                 else
                     loTail.next = e;
                 loTail = e;
                 ++lc;
             }
             else {
                 if ((e.prev = hiTail) == null)
                     hiHead = e;
                 else
                     hiTail.next = e;
                 hiTail = e;
                 ++hc;
             }
         }

         if (loHead != null) {
             if (lc <= UNTREEIFY_THRESHOLD)
                 tab[index] = loHead.untreeify(map);	// 转化为线性链表Bin
             else {
                 tab[index] = loHead;
                 if (hiHead != null) 
                     loHead.treeify(tab);		// 树化
             }
         }
         if (hiHead != null) {
             if (hc <= UNTREEIFY_THRESHOLD)
                 tab[index + bit] = hiHead.untreeify(map);
             else {
                 tab[index + bit] = hiHead;
                 if (loHead != null)
                     hiHead.treeify(tab);
             }
         }
     }

HashMap中的树是红黑树(R-B Tree), 关于红黑树的理论请参考:

HashMap的Put操作
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值