缘由
偶然看到一位兄台在面试的时候被问到了java中hashmap的实现。突然来了兴趣,就想着自己也看看hashmap的实现的源码。此外,由于被老师要求使用JE22实现音乐网站的事,最近正在奋力利用传智博客的视频学习java web,所以nginx的事恐怕只能暂时放一下放了。从官方文档来看
综述
- 网址:http://www.javaweb.cc/JavaAPI1.6/
- 基于哈希表的 Map 接口的实现
- 允许使用 null 值和 null 键
- 此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
这话说的很生硬,我觉得就是遍历键或者值的时候,遍历得到的顺序是不是固定的。 - 迭代所需时间和HashMap的大小有关,如果需要迭代,不要设置太高的初始容量。
- HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。
初始容量就是整个hash的数组的长度,加载因子就是决定了一个阈值,是默认是0.75,如果在此hash中的条目的数量大于了0.75*初始容量,使用rehash重建hash表 - hashmap不是同步的。也就是在多线程访问这个hash表的时候,并不是线程安全的。
方法:
看完了上面的可能对这个hashmap的使用没什么问题了。
重要部分
碰撞的解决
我们都知道hash表有两种常见的实现方法,- 拉链法
- 开放寻址法
从源码上看
相关的这个句: /**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table;
这个table变量就是hash表的数组,而且表明table是一个Entry类的数组,也就是每一个table的元素是一个Entry类的实例。看看Entry类的关键:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
上图摘了一点点Entry类的一些内容,我们可以看到关键就是那个
Entry<K,V> next;
这里表明了每一个Entry的对象都有一个指向下一个对象的引用(之前一直在搞C,C里面是指针,这里不知道用引用合适不合适)。那么通过这个对象就很容易找到同一个hash值的下一个对象。
四个构造函数
我只看看其中的较为简单的三个:- HashMap(int initialCapacity, float loadFactor)
- HashMap(int initialCapacity)
- HashMap()
从源码的角度来看,HashMap(int initialCapacity) 与 HashMap()只是利用了默认的参数调用了HashMap(int initialCapacity, float loadFactor)而已。
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
我们来看看关键的HashMap(int initialCapacity, float loadFactor),摘取了部分代码:
public HashMap(int initialCapacity, float loadFactor) {
//如果我们设定的大小大于了最大的,那么就使用默认的最大大小
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// Find a power of 2 >= initialCapacity
// hash表的大小必须是 2 的N次方,所只有用这样的办法来找到
//合适的大小
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
//设置阈值,当大于这个阈值的时候就会自动扩容
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
//这里只是确定是否使用另一种hash计算的方式
//备选哈希函数是只适用于容量大于指定的阈值大小的Map。
//默认情况下,值是-1 。 此值禁用备选哈希函数。
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//留给子类用的,可以在这里写一些子类需要初始化的内容
init();
}
插入一个键值对
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
//算出hash值
int hash = hash(key);
//取模
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果这个key相同,则覆盖
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//并没有做什么实现
//英文解释是用来标记一下,这个e被覆盖过
e.recordAccess(this);
return oldValue;
}
}
//每增加一个元素就会自加1
modCount++;
//加在链表末尾
addEntry(hash, key, value, i);
return null;//成功就返回null
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果容量大于阈值就就扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
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);
//hash表的键值对多了一个
size++;
}
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
//如果是被插入到了table数组同一个key的链表的表头。
next = n;
key = k;
hash = h;
}
取模的方式
static int indexFor(int h, int length) {
return h & (length-1);
}
h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。这是一种非常聪明的取模的方式,比如说:
8 & (16-1):0100 &1111=0100
9 & (16-1):0101 &1111=0101
如果这个数大于16的话,那么除了最右边的四个,都会被忽略掉,而且那么就会取到正确的值。
这里也就注定了为什么必须这个hash表的长度必须是2的大小必须是 2 的N次方。如果不取2的N次会有极大的增加冲突,而且还会导致某些hash表的下标永远得到,分析的过程请看:
modCount的作用
官方文档已经说清楚了这个hashmap 不是线程安全的,所以modCount的作用就是为防止线程不是安全的而引发出来的一些错误的事情。modCount当在被增加、减少等等操作的时候都会自加1。如下所示:public V put(K key, V value) {
//每修改了hash表的元素多少就会自加1
modCount++;
//加在链表末尾
addEntry(hash, key, value, i);
return null;//成功就返回null
}
当我们在迭代hash表的时候,还有有另一个属于迭代器的参数:expectedModCount。
当在初始化迭代器的时候,我们会给这个赋值,而在使用迭代器遍历的时候,都每次很检查这个两个值是否相等,如果不相等就说明,有别的线程修改了这个hash表,那么就会抛出异常。代码如下:
private abstract class HashIterator<E> implements Iterator<E> {
int expectedModCount; // For fast-fail
HashIterator() {
expectedModCount = modCount;
}
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
这虽然是个抽象类,但是我们具体能够使用的对键的迭代、对值的迭代、以及对键值对的迭代是其的实现类
private final class ValueIterator extends HashIterator<V> {
public V next() {
return nextEntry().value;
}
}
private final class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
这就是fast-fail机制,官方文档对其有着较为完整的介绍:
注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作(会导致modCount的值改变);仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:
- Map m = Collections.synchronizedMap(new HashMap(...));
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
从上面的论述可以看出,官方并不建议在需要同步的情况下使用HashMap。
参考博客
Hash碰撞& 拒绝服务漏洞 - 备选哈希函数sun.misc.Hashing.stringHash32