1.概述
基于哈希表的Map接口的实现,提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能。迭代性能与HashMap的容量(桶的数量)及其大小成比例。
2.与HashTable的对比
- 两者最主要的区别在于HashTable是线程安全,而HashMap非线程安全。因此相对而言HashMap性能会高一点,在多线程的环境下若使用HashMap,需要使用Collections.synchronizedMap()方法来获得一个线程安全的集合。//TODO Collections.synchronizedMap()的实现原理
- HashMap可以使用null作为key,但建议避免这样使用。当HashMap以null作为key的时候,总是存储到table数组的0号位置(或者说0号位置对应的链表)。
- Hash方式不同。HashTable是通过key的hashcode对table数组的长度直接取模,而HashMap对其进行了二次hash,以获取更好的散列。
3.数据结构
在数据结构中有数组和链表来对数据进行存储,但两者却各有优缺点。这里不做详细介绍。
哈希表:
综合考虑这两种结构的优点,就是寻址容易,插入删除也容易的数据结构。
哈希表有多种不同的实现方法,拉链法是最常用的一种,即:
链表的数组,每个元素存放链表头结点的数组。
![](http://pic002.cnblogs.com/images/2012/80896/2012060316385671.jpg)
哈希表是由数组+链表组成的。在数组中,每个元素存储的不是实际的数据,而是一个链表的头结点。关于存储规则,我们这里先简单假设性的讲一下,更具体的实现会在后面章节说明。首先,我们可以得到key的hashcode,然后对其hash(key的hashcode)%len得到,通俗的说就是对key的hashcode进行hash算法,然后对数组长度取模。比如说,一个key的hashcode为108,然后len是16,所以通过计算可以得到12,也就是说108这个数据将存到数组12这个位置的链表中。
现在还有一个问题,就是这个线性的数组+链表是如何实现按键值对来存取数据呐?
在HashMap中,实现了一个静态内部类Entry,其重要的属性有key,value,next。通过属性我们可以看出,Entry就相当于HashMap对键值对实现的bean。HashMap的基础是一个线性数组,而这个数组就是Entry的数组。
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
}
4.存取实现
(1) put()
首先,看一下源码:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
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);
return null;
}
然后我们一步一步来看一下上面的代码。
- 首先对key做null检查。如果key==null,调用putForNullKey方法,将其存储到table[0],因为null的hash值总是0。
- key的hashcode()方法会被调用,然后通过hash方法计算hash值,返回int的变量hash。hash值用来找到存储Entry对象的数组的索引。
- indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引。
- 接下来是比较重要的环节了。首先我先举一个例子,然后根据例子来进行分析。假如我们现在有3个键值对(4,"xiaohei"),(6,"xiaobai"),(4,"xiaohong"),然后key的hashcode方法也很简单,我们就假设4和6都返回一样的值,依次put。首先,put进去第一个键值对,当put第二个键值对的时候,会发现hash方法得到的值是一样的,这个时候就会比较value的值是否相等,因为"xiaohei"不等于"xiaobai",所以会对链表进行迭代,一直到Entry->next是null的时候,将此时这个键值对放到下一个节点;当put第三个键值对的时候,同样发现hash得到的值一样,继而比较value也一样,这个时候就会体会value的值。
- 通过上边这个例子分析,我们可以看到如果两个key有相同的hash值(也叫冲突),他们会以链表的形式来存储。所以,这里我们就迭代链表。
- 如果在刚才计算出来的索引位置没有元素,直接把Entry对象放在那个索引上。
- 如果索引上有元素,然后会进行迭代,一直到Entry->next是null。当前的Entry对象变成链表的下一个节点。
- 如果我们再次放入同样的key,在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)),如果这个方法返回true,它就会用当前Entry的value来替换之前的value。
(2) get()
还是先看一下源码:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
当我们理解了HashMap的put方法的工作原理之后,理解get的工作原理就非常简单了。
当你通过一个key准备从HashMap中获取值的时候:
- 首先对key进行检查。如果key是null,table[0]这个位置的元素将被返回。
- 接下俩,key的hashcode()方法被调用,同样调用hash方法来计算hash值。
- 再通过indexFor方法用来计算要获取的Entry对象在table数组中的精确的位置。
- 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。
5.重要参数
HashMap有两个重要的参数:初始容量和加载因子。我们这部分将介绍与这方面相关的知识。
从上面的源代码中可以看出:当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,再根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。所以我们当然希望HashMap中的元素分布的尽量均匀一些,尽量使得每个位置上的元素只有一个,那么我们就能节省遍历链表的时间,能够优化效率。
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash 方法所计算得到的值是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的。
在HashMap中是这样做的:通过indexFor 方法来计算该存到哪个索引。其源码如下:
static int indexFor(int h, int length) {
return h & (length-1);
}
通过 h & (table.length -1) 来得到该对象的索引位置,而
当length是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。也就是说两者等价不等效。
HashMap的初始容量为16,能够尽量保证分布均匀。另外一个参数,加载因子,这个参数决定了HashMap在何时进行扩容,毕竟初始容量只有16,当数据很多的时候...其默认的加载因子为0.75,也就是说当哈希表的容量超过16*0.75的时候,会进行数组大小的扩展,即rehash过程。这个过程非常消耗性能。将创建一张新表,将原表的数据映射到新表中。所以如果我们能够预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
6.总结
- HashMap有一个叫做Entry的内部类,用来存储key-value键值对。
- Entry对象是存储在一个叫做table的Entry数组中。
- table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。
- key的hashcode()方法用来找到Entry对象所在的桶。
- 如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。
- key的equals()方法用来确保key的唯一性。
- value对象的hashcode()方法根本一点用也没有。
参考文档:
HashMap工作原理