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),也等价取膜,但更加高效,取得的位置更加分散,偶数,奇数保证了都会分散到。前者就不能保证