HashMap详解

HashMap

1.预备知识

1.1 equals

= =用于比较引用和比较基本数据类型时具有不同的功能:比较基本数据类型,如果两个值相同,则结果为true而在比较引用时,如果引用指向内存中的同一对象,结果为true;
equals()作为方法,实现对象的比较。由于= =运算符不允许我们进行覆盖,也就是说它限制了我们的表达。因此我们复写equals()方法,达到比较对象内容是否相同的目的。而这些通过==运算符是做不到的。

1.2 HashCode

hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的。
使用的时候用一些前提:

  • hashCode相同,equals不一定相同
  • equals相同,hashCode必须相同
  • 重写equals的时候尽量重写hashCode方法

2.HashMap概述

HashMap是基于哈希表的Map接口的非同步实现。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
HashMap详解_第1张图片

3.HashMap实现原理

下面所有说的实现原理全部基于JDK1.8实现的。
HashMap是数组和链表的结合体,根据Hash值去做数组角标tab[i = (n - 1) & hash],真正的Key-Value键值对在链表(Node节点,定义:Node implements Map.Entry

3.1 存储

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; Node p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
	计算Hash值,获取node信息
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        ……
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
					//计算当前链表,如果超过TREEIFY_THRESHOLD的定义,就变成树状结构
                    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;
	//当前size大于threshold时,需要重新划分hashMap的大小
    if (++size > threshold)
        resize();
    return null;
}

根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

3.2 读取

final Node getNode(int hash, Object key) {
	Node[] tab; Node first, e; int n; K k;
	//先计算hash角标,获取数组位置
	if ((tab = table) != null && (n = tab.length) > 0 &&
		(first = tab[(n - 1) & hash]) != null) {
		if (first.hash == hash && // always check first node
			((k = first.key) == key || (key != null && key.equals(k))))
			return first;
		if ((e = first.next) != null) {
			if (first instanceof TreeNode)
				return ((TreeNode)first).getTreeNode(hash, key);
			//如果不是树状结构,那么轮询查看,其实也最多就TREEIFY_THRESHOLD-1的大小
			do {
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					return e;
			} while ((e = e.next) != null);
		}
	}
	return null;
}

根据获取的key的hash值,确认数组的角标;然后通过这个去循环获取真正的value值。(如果是树状结构那就另说)

3.3 resize

当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容。
方法就是上面看见过的resize(),那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置。

final Node[] resize() {
	Node[] oldTab = table;
	int oldCap = (oldTab == null) ? 0 : oldTab.length;
	int oldThr = threshold;
	int newCap, newThr = 0;
	if (oldCap > 0) {
		if (oldCap >= MAXIMUM_CAPACITY) {
			threshold = Integer.MAX_VALUE;
			return oldTab;
		}
		//达到定义数量以后,就会Double定义的数量
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
				oldCap >= DEFAULT_INITIAL_CAPACITY)
			newThr = oldThr << 1; // double threshold
	}
	else if (oldThr > 0) // initial capacity was placed in threshold
		newCap = oldThr;
	else {               // zero initial threshold signifies using defaults
		newCap = DEFAULT_INITIAL_CAPACITY;
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}
	if (newThr == 0) {
		float ft = (float)newCap * loadFactor;
		newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
				(int)ft : Integer.MAX_VALUE);
	}
	threshold = newThr;
	@SuppressWarnings({"rawtypes","unchecked"})
		Node[] newTab = (Node[])new Node[newCap];
	table = newTab;
	//重新排序列表,其实也就是计算一下以前所有的数据一遍
	if (oldTab != null) {
		for (int j = 0; j < oldCap; ++j) {
			Node e;
			if ((e = oldTab[j]) != null) {
				oldTab[j] = null;
				if (e.next == null)
					newTab[e.hash & (newCap - 1)] = e;
				else if (e instanceof TreeNode)
					((TreeNode)e).split(this, newTab, j, oldCap);
				else { // preserve order
					Node loHead = null, loTail = null;
					Node hiHead = null, hiTail = null;
					Node next;
					do {
						next = e.next;
						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);
					if (loTail != null) {
						loTail.next = null;
						newTab[j] = loHead;
					}
					if (hiTail != null) {
						hiTail.next = null;
						newTab[j + oldCap] = hiHead;
					}
				}
			}
		}
	}
	return newTab;
}

NOTE:resize是很耗费性能的,但是一开始定义的数量太大的话那就浪费空间;如果能够一开始就知道大小那就最好是定义好
定义大小的时候是会乘以加载因子(0.75),所以比如说你是要存储1000个数据,理论上是需要1000/0.75=1333个大小的;然后因为是计算机二进制,那就是最好定义2048的大小。这样才能有效的避免resize的问题

3.4 树状结构

当链表到达8,从链表变成树状结构了。如果少于8个的时候就会从树状结构,变成链表。
该树状结构是红黑树,主要的作用是在查找数据的时候减少查找需要遍历的数量。
红黑树怎么查找的,我暂时看不懂;左旋右旋的,有点脑子炸裂。遗留一个问题吧

HashMap线程不安全

当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。(多线程的环境下不使用HashMap)

遗留问题

  • 红黑树在hashMap中的使用

参考资料

  • Java中HashMap的实现原理
  • 对HashMap实现原理的理解
  • java中HashMap原理

你可能感兴趣的:(java学习,技术博客)