集合源码分析(六)HashMap集合

1、HashMap概述:

底层是哈希算法,针对键。HashMap允许null键和null值,线程不安全,效率高。键不可以重复存储,值可以。

哈希结构:不能保证数据的迭代顺序,也不能保证顺序的恒久不变。

Map集合(无序、无索引、不可以重复)是双列集合,一个键对应一个值。键和值之间有一对一的关系。其中键不可以重复,值可以。键要重写hashCode()方法和equals()方法。

1.1、基本分析:

HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。

HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。

HashMap存数据的过程:

HashMap内部维护了一个存储数据的Entry数组,HashMap采用链表解决冲突,每一个Entry本质上是一个单向链表。当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后通过indexFor(hash,length)求该key-value对的存储位置,计算方法是先用hash&0x7FFFFFFF后,再对length取模,这就保证每一个key-value对都能存入HashMap中,当计算出的位置相同时,由于存入位置是一个链表,则把这个key-value对插入链表头。

HashMap中key和value都允许为null。key为null的键值对永远都放在以table[0]为头结点的链表中。

1.2、什么是哈希表:

在讨论哈希表之前,我们先大概了解下其他数据结构在新增,查找等基础操作执行性能

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

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

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

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

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

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

1.3、HashMap工作原理:

HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用LinkedList来解决碰撞问题,当发生碰撞了,对象将会储存在LinkedList的下一个节点中。 HashMap在每个LinkedList节点中储存键值对对象。

  当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的LinkedList中。键对象的equals()方法用来找到键值对。

2、基本使用:

HashMap<String, String> hm = new HashMap<>();
	hm.put("a", "a");
	hm.put("b", "b");
	hm.put("c", "c");
	hm.put("d", "d");

//遍历一:keySet()获取所有键的集合

Set<String> set = hm.keySet();	//多态 set 即不是HashSet也不是TreeSet。
			//是keySet。keySet是HashMap的一个内部类。也继承了AbstractSet
			//相当于是HashSet的兄弟类
for (String key : set) {
	String value = hm.get(key);
	System.out.println(key + ":" + value);
}

//遍历二:获取所有键值对对象的集合

Set<Entry<String, String>> set = hm.entrySet();//多态。set即不是HashSet也不是TreeSet。
			//是entrySet。entrySet是HashMap的一个内部类。也继承了AbstractSet
			//相当于是HashSet和keySet的兄弟类
for (Entry<String, String> entry : set) {
	String key = entry.getKey();
	String value = entry.getValue();
	System.out.println(key + ":" + value);
}

3、常用方法源码解析:

3.1、V put(K key, V value) :以键=值的方式存入Map集合

//源码解析
public V put(K key, V value) {
	int hash = hash(key);//计算哈希值
	int i = indexFor(hash, table.length);//通过h & (length-1)计算应存入数组中哪个索引处。
	for (Entry<K,V> e = table[i]; e != null; e = e.next) {//遍历
		Object k;
		//先比较哈希值
		//再比较是否是同一个对象
		//用equals再比较内部属性值是否相同
		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);//添加。i表示计算出来的应存入的索引
	return null;
}
//-----------------------------------------------
   void addEntry(int hash, K key, V value, int bucketIndex) {
		if ((size >= threshold) && (null != table[bucketIndex])) {//如果数组长度超过极限
			resize(2 * table.length);//则扩容,每次扩容都是原先的两倍。
						//resize方法会新建一个新的两倍容量的数组
						//把原来的值利用indexFor方法重新计算应存入的索引
					        //这也就是为什么不保证顺序恒久不变的原因之一
						//因为每次扩容每个元素都会重新计算应存入的索引

			hash = (null != key) ? hash(key) : 0;
			bucketIndex = indexFor(hash, table.length);//计算应存入的索引
		}
		createEntry(hash, key, value, 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);//把要添加的键值对封装成整体Entry
														  //数组原来的值被记录在新添加的值的next
														  //形成哈希桶结构。
	size++;
}

3.2、V get(Object key):根据键获取值

//源码解析
public V get(Object key) {
	if (key == null)	//判断是否为null
		return getForNullKey();
	Entry<K,V> entry = getEntry(key);//调用getEntry
	return null == entry ? null : entry.getValue();
}
//-----------------------------
final Entry<K,V> getEntry(Object key) {
	if (size == 0) {
		return null;
	}
	int hash = (key == null) ? 0 : hash(key);//先计算哈希值
	for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
				//利用indexFor计算要查的key应该在数组中哪个索引
				//从这个索引处记录的数开始遍历
				//从链表(哈希桶结构中)
				//挨个进行判断
		Object k;
		if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
			return e;//如果有就返回
	}
	return null;//如果没有就返回null
}

3.3、int size():返回Map中键值对的个数

//源码解析
public int size() {
	return size;//返回size成员变量
//此成员变量在每一次put的底层中的createEntry方法中都会++。
//在每一次删除元素remove的时候都会--。
}

3.4、boolean containsKey(Object key):判断Map集合中是否包含键为key的键值对

//源码解析
public boolean containsKey(Object key) {
	return getEntry(key) != null;//请参见get源码解析中getEntry方法解析
}

3.5、boolean containsValue(Object value):判断Map集合中是否包含值为value键值对

//源码解析
public boolean containsValue(Object value) {//利用双重for循环,判断是否包含值
	Entry[] tab = table;
	for (int i = 0; i < tab.length ; i++)//遍历数组
		for (Entry e = tab[i] ; e != null ; e = e.next)//遍历链表
			if (value.equals(e.value))
				return true;
	return false;
}

3.6、boolean isEmpty():判断Map集合中是否没有任何键值对

//源码解析
public boolean isEmpty() {
	return size == 0;//判断成员变量是否为0.
}			 //size成员变量在每一次添加的时候都++
			 //每一次删除的时候都--
			 //请参见size()方法。

3.7、void clear():清空Map集合中所有的键值对

//源码解析
public void clear() {
	modCount++;
	Arrays.fill(table, null);//将数组置为null
	size = 0;				 //成员变量size置为0
}
//--------------------------------------
public static void fill(Object[] a, Object val) {
	for (int i = 0, len = a.length; i < len; i++)//循环数组a将每一个元素都置为val
		a[i] = val;//clear中调用。就是将数组都置为null
}

3.8、V remove(Object key):根据键值删除Map中键值对

//源码解析
public V remove(Object key) {
	Entry<K,V> e = removeEntryForKey(key);
	return (e == null ? null : e.value);
}
//---------------------------------------------------------------------
final Entry<K,V> removeEntryForKey(Object key) {
	int hash = (key == null) ? 0 : hash(key);//计算hashcode值
	int i = indexFor(hash, table.length);//利用hashcode计算在数组中应存入的索引
	Entry<K,V> prev = table[i];
	Entry<K,V> e = prev;
	while (e != null) {//循环遍历链表
		Entry<K,V> next = e.next;
		Object k;
		//先匹配hashcode值,再对比是否同一个对象,再调用equals方法比较内部属性值
		if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) {
			modCount++;
			size--;
			if (prev == e)
				table[i] = next;
			else
				prev.next = next;
			e.recordRemoval(this);
			return e;
		}
		prev = e;
		e = next;
	}
	return e;
}	

4、HashMap和Hashtable的区别?

HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口。主要的区别有:线程安全性,同步(synchronization),以及速度。

1.Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。

2.HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。

3.HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。(在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步(Collections.synchronizedMap))

4.另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。fail-fast机制如果不理解原理,可以查看这篇文章:http://www.cnblogs.com/alexlo/archive/2013/03/14/2959233.html

5.由于HashMap非线程安全,在只有一个线程访问的情况下,效率要高于HashTable。

6.HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。 

7.Hashtable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

8..两者通过hash值散列到hash表的算法不一样:

        HashTbale是古老的除留余数法,直接使用hashcode

int hash = key.hashCode();  
int index = (hash & 0x7FFFFFFF) % tab.length; 

而后者是强制容量为2的幂,重新根据hashcode计算hash值,在使用hash 位与 (hash表长度 – 1),也等价取膜,但更加高效,取得的位置更加分散,偶数,奇数保证了都会分散到。前者就不能保证

猜你喜欢

转载自blog.csdn.net/MyronCham/article/details/82898470
今日推荐