• Home
  • About
    • hzcforever photo

      hzcforever

      自律给人掌控生活的力量

    • Learn More
    • Email
    • Github
  • Posts
    • All Posts
    • All Tags

HashMap源码分析

27 Aug 2018

1.概念解释

最近在看Java集合的底层源码实现,打算挑几个重要的内容写个小结,也对比学习了很多篇博客,如有错误,还望指出,不胜感激。

HashMap是一个数组+链表的实现,数组中存储了所有的头结点,每个头结点所在的一格称为一个桶,桶后面存放的每一个数据称为bin。

HashMap由数组+链表组成,数组是HashMap的主体,当添加一个元素时,通过key计算hash值,并确定插入数组中的位置。如果插入的位置已经有元素存在,则通过equals方法进一步判断,若返回true则覆盖该位置上的Node,若返回false则添加到该元素的后面形成了链表。当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。

JDK 1.8 以前的HashMapd的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。针对这种情况,JDK 1.8 引入了红黑树(查找时间复杂度为O(logn))来优化这个问题,当同一个hash值的节点数不小于8时,不再采用单链表形式存储,而是采用红黑树的存储形式。

类的数据成员:

DEFAULT_INITIAL_CAPACITY:表示HashMap中桶的数量,即数组长度,默认值为16。每一次扩容都是原来的2倍,总之,容量都是2的幂。

/**
*The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

DEFAULT_LOAD_FACTOR:表示装载因子,用来衡量HashMap满的程度,其默认值为0.75f,计算装载因子的公式为:size / capacity,并非用占用桶的数量除以capacity。

/**
*The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

threshold:threshold表示临界值,当HashMap的size大于threshold时会执行resize操作。threshold = capacity * loadFactor。

size:表示HashMap中存放的key-value键值对的个数。

TREEIFY_THRESHOLD:当桶上的结点大于这个值时,链表就会转成红黑树。

UNTREEIFY_THRESHOLD:当桶上的结点小于这个值时,红黑树就会转回链表。

MIN_TREEIFY_CAPACITY:桶中结构转化为红黑树对应的table的最小大小。

transient Node<K,V>[] table:存放元素的数组,总是2的幂次倍。

transient Set<map.entry<K,V» entrySet:存放具体元素的值。

2.基本方法

public class HashMapTest {
 
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		HashMap<String, String> hashMap = new HashMap<>();
		//添加方法
		hashMap.put("1", "string1");
   			//遍历方法1
		Set<String> keys = hashMap.keySet();
		for(String key  :keys){
			System.out.println(key + " = " + hashMap.get(key));
		}
		//遍历方法2
		Set<Entry<String, String>> entrys =  hashMap.entrySet();
		for(Entry<String, String> entry : entrys){
			String key = entry.getKey();
			String value = entry.getValue();
			System.out.println(key + " = " + value);
		}
		//遍历方法3(for和iterator实现原理相同)
		Iterator iter = hashMap.keySet().iterator(); 
		while (iter.hasNext()) { 
	 		String key = (String) iter.next(); 
			String value = hashMap.get(key); 
			System.out.println(key + " = " + value);
		}
		//遍历方法4
		Iterator<Entry<String, String>> iterator = hashMap.entrySet().iterator();
		while(iterator.hasNext()){
			Entry<String, String> entry = iterator.next();
			String key = entry.getKey();
			String value = entry.getValue();
			System.out.println(key + " = " + value);
		}
		//查询方法
		System.out.println(hashMap.get("1"));
		//删除方法
		hashMap.remove("1");		
	}
 
}

3.源码分析

1.HashMap<String, String> hashMap = new HashMap<>();

public HashMap(int initialCapacity, float loadFactor) {
	//初始容量不能<0
	if (initialCapacity < 0)
		throw new IllegalArgumentException("Illegal initial capacity: " +
   		initialCapacity);
	//初始容量不能 > 最大容量值,HashMap的最大容量值为2^30 
	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity = MAXIMUM_CAPACITY;
	//负载因子不能 < 0
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
		throw new IllegalArgumentException("Illegal load factor: " +
  	    loadFactor);
	this.loadFactor = loadFactor;
	this.threshold = tableSizeFor(initialCapacity);
   	}

class Node<K,V>是HashMap的一个内部类,实现了Map.Entry<K,V>接口。

static class Node<K,V> implements Map.Entry<K,V> {
	final int hash;
	final K key;
	V value;
	Node<K,V> next;//指向下一个结点
	
	//构造函数
	Node(int hash, K key, V value, Node<K,V> next) {
		this.hash = hash;
		this.key = key;
		this.value = value;
		this.next = next;
	}

	public final K getKey(){ return key; }
	public final V getValue()  { return value; }
   		public final String toString() { return key + "=" + value; }

	public final int hashCode() {
		return Objects.hashCode(key) ^ Objects.hashCode(value);
	}

	public final V setValue(V newValue) {
		V oldValue = value;
		value = newValue;
		return oldValue;
	}

	//判断两个Node是否相等,如果key和value都相等则返回true
	public final boolean equals(Object o) {
		if (o == this)
			return true;
		if (o instanceof Map.Entry) {
			Map.Entry<?,?> e = (Map.Entry<?,?>)o;
   		 		if (Objects.equals(key, e.getKey()) &&
				Objects.equals(value, e.getValue()))
				return true;
			}
		return false;
	}
   	}

HashMap其实就是一个Entry(Node)数组,Entry对象中包含了键和值,其中next也是一个Entry对象,它就是用来处理hash冲突的,形成一个链表。

2.红黑树

//红黑树
static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {
	TreeNode<k,v> parent;  // 父节点
	TreeNode<k,v> left; //左子树
	TreeNode<k,v> right;//右子树
	TreeNode<k,v> prev;// needed to unlink next upon deletion
	boolean red;//颜色属性
	TreeNode(int hash, K key, V val, Node<k,v> next) {
		super(hash, key, val, next);
	}
 
	//返回当前节点的根节点
	final TreeNode<k,v> root() {
		for (TreeNode<k,v> r = this, p;;) {
			if ((p = r.parent) == null)
				return r;
			r = p;
		}
	}
}

红黑树比链表多了四个变量,parent父节点、left左节点、right右节点、prev上一个同级节点。

桶的树形化:

(1)根据哈希表中元素个数确定是扩容还是树形化;

(2)如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系;

(3)然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容。

//将桶内所有的链表结点替换成红黑树结点
final void treeifyBin(Node<K,V>[] tab, int hash) {
	int n, index;
	Node<K,V> e;
	//如果当前哈希表为空,或者哈希表中元素的个数小于 进行树形化的阈值(默认为64),就去新建/扩容
	if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
		resize();
	else if ((e = tab[index = (n - 1) & hash]) != null) {
		//如果哈希表中的元素个数超过了树形化阈值,进行树形化
		//e是哈希表中指定位置桶里的链表结点,从第一个开始
		TreeNode<K,V> hd = null, tl = null;//红黑树的头、尾结点
		do {
			//新建一个树形结点,内容和当前链表结点e一致
			TreeNode<K,V> p = replacementTreeNode(e, null);
			if (tl == null)//确定树头结点
				hd = p;
			else {
				p.prev = tl;
				tl.next = p;
			}
			tl = p;
		} while ((e = e.next) != null);
		//让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而不是链表了
		if ((tab[index] = hd) != null)
			hd.treeify(tab);
	}
}

3.hashMap.put(“1”,”string1”);

public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

当我们调用put存值时,HashMap首先会获取key的哈希值,通过哈希值快速找到某个存放位置,这个位置可以被称之为bucketIndex。

对于一个key,如果hashCode不同,equals一定为false,如果hashCode相同,equals不一定为true。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
	Node<K,V>[] tab; 
	Node<K,V> p; 
	int n, i;
	//若table未初始化或长度为0,则进行扩容
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
	//计算index,并对null做处理
	//(n - 1) & hash确定元素存放的位置,桶为空,则将结点放入桶中
	if ((p = tab[i = (n - 1) & hash]) == null)
		tab[i] = newNode(hash, key, value, null);
	//桶中已经存在元素
	else {
		Node<K,V> e; K k;
		//比较桶中第一个节点的hash值和key值,若相等则覆盖
		if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
			e = p;//将第一个元素赋值给e,用e来记录
		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 = newNode(hash, key, value, null);
					如果结点数达到阈值,转换为红黑树
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
					treeifyBin(tab, hash);
					break;
				}
				//判断链表中结点的key值与插入元素的key值是否相等
				if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;
			}
		}
		//表示在桶中找到key值和hash值与插入元素相等的结点
		if (e != null) { // existing mapping for key
			V oldValue = e.value;
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;
			afterNodeAccess(e);
			return oldValue;
		}
	}
	++modCount;//修改次数+1
	//超过阈值则扩容
	if (++size > threshold)
		resize();
	afterNodeInsertion(evict);
	return null;
}

数据存储实现原理流程小结:

根据key计算得到key.hash = (h = k.hashCode()) ^ (h »> 16);

根据key.hash计算得到桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了:

① 如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null;

② 如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作;

③ 如果该位置有数据是一个链表,分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样:如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null;如果该链表已经有这个节点了,那么找到该节点并更新新数据,返回老数据。

4.hashMap.get(“1”);

final Node<K,V> getNode(int hash, Object key) {
	Node<K,V>[] tab; 
	Node<K,V> first, e; 
	int n; 
	K k;
	// table已经初始化,长度大于0,根据hash寻找table中的项也不为空
	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<K,V>)first).getTreeNode(hash, key);
			// 否则,在链表中查找
			do {
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					return e;
			} while ((e = e.next) != null);
		}
	}
	return null;
}

说明:HashMap并没有直接提供getNode接口给用户调用,而是提供的get函数,而get函数就是通过getNode来取得元素的。



Java Like Tweet +1