文章目录
  1. HashMap源码分析
  2. 1. HashMap简介
    1. 1.1 结构
    2. 1.2 hash算法
    3. 1.3 hashCode和equals
  3. 2. HashMap分析
    1. 2.1 关键属性
    2. 2.2 构造函数
    3. 2.3 put(K key, V value)
    4. 2.4 get(Object key)
    5. 2.5 remove(Object key)
    6. 2.6 Set<Map.Entry<K,V>> entrySet()
    7. 2.7 Set<K> keySet()
    8. 2.8 杂鱼方法
  4. 3. 总结

[TOC]

HashMap源码分析

HashMap类的源码分析,本文是基于 java1.6java1.7 的源码。HashMap 是基于哈希表的Map接口的实现,主要用来提供键和值的映射操作,并且键值不能“重复”,且无序。java6和7 两个版本中如果有明显不同,我会明确标出。

1. HashMap简介

1.1 结构

HashMap存储结构图:

从图中可以看出HashMap结合了数组链表两种数据结构,众所周知

    1. 数组在检索的速度快,插入和删除的速度慢
    1. 链表在插入和删除的速度快,检索的速度慢

这样在取长补短的情况下产生了哈希散列表的存储方式

图中,最左边竖着的存储结构是数组,每一个格子维护一个链表,这些格子叫做 。每个链表的节点在java中是一个 Entry类,维护者一对键和值。这个看起来就像将一个大的链表拆分成多个小的链表,然后将每个小链表的头放在数组中一样,至于数据应该放入在哪一个桶中那就要根据对象的 hash 值来进行计算了。

1.2 hash算法

那么算出hash值(散列码)的算法就是hash算法, 由于我们需要将对象(而对象包括很多,比如数字,字符,类等等)存放到每个桶中, 那么就需要计算出一个整数,然后与桶的数量进行模运算(HashMap中不是采用取模运算)才能确定处于哪一个桶之中。那么hash算法的目的就是将这些不统一的对象统一算出一个整数(int型)。
优秀的hash算法需要从时间,空间和散列程度等等进行考虑,那么关于算法就不多多说明了。提供一个下载地址,自行研究

///////////////////////
http://download.csdn.net/detail/u013082133/9378254
///////////////////////
下面看看hashMap中的hash算法,1.6和1.7中的关键部分基本相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1.6, 传入的 h 是某个对象o的hashCode()方法后的值, 进行了二次散列
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

// 1.7, hashSeed默认是0, 这里仅仅多了对string的处理
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
// 同样进行的二次散列
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

1.3 hashCode和equals

说完hash算法之后,还有一个问题就是如果最后算出来的两个对象在同一个桶中(hashCode相同或者最后的取模相同),那该如何处理(这种现象称为散列冲突)。此时就需要每个对象的equals方法出场了,所以这也是java建议在覆盖hashCode()方法同时也覆盖一下equals方法的原因。当两个对象放生散列冲突的时候,这个时候就要将该桶中链表中每个元素用equals进行比较,如果为true,那么就覆盖该元素,否则挂到链表的后面。

2. HashMap分析

2.1 关键属性

java1.6和1.7的属性基本一样,1.7中就多了一个空数组的定义。这个是一个性能的改善吧,就像之前ArrayList类中一样,1.6在初始化实例的就会把默认的容量直接赋值给数组;而在1.7后初始化实例的时候只会赋值一个空的数组,直到调用添加方法的时候才会去初始化容量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1.6
static final int DEFAULT_INITIAL_CAPACITY = 16; // 默认容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 默认最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子
transient Entry[] table; // 链表数组
transient int size; // 实际数量
int threshold; // 容量的临界值
final float loadFactor; //负载因子
transient volatile int modCount; // 修改次数
// 1.7
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;

其次这里也对这个负载因子做一下说明:负载因子是容量和临界值的商,对于HashMap的检索性能其实是和容量hash算法以及数据的数量有一定的关系。

  1. 如果数组容量为1,那么基本和单链表一模一样。
  2. 如果容量过大,每个桶只放一个Entry,那么和普通的数组又基本一致。
  3. 好的hash算法可以使得数据有更好的分散性

其次负载因子是数组容量进行扩容的一个依据,如果当临界值超过了负载值,那么就会对数组的容量进行扩容,一般扩容的大小是上一次的2倍。负载因子也是时间和空间的一种平衡,一般采用jdk的默认0.75f就好了。

2.2 构造函数

java1.6中调用了默认构造函数之后,会初始化负载因子,临界值和链表数组

1
2
3
4
5
6
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 0.75f
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); // 16 * 0.75 = 12
table = new Entry[DEFAULT_INITIAL_CAPACITY]; // 链表数组大小16
init(); // 空的函数, 估计为以后java版本更新提供
}

而在1.7中,临界值和链表数组大小都是在调用了put方法之后进行初始化的
注意:要是调用了其他构造方法,如自己传入一个默认的容量,那么java会寻找这个数字的最近2次幂来作为默认容量

1
2
3
4
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;

2.3 put(K key, V value)

1.6和1.7的两个方法基本一样,除了1.7中多了个初始化。

1
2
3
4
// 1.7中的 inflateTable 与1.6的初始化过程基本类似
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}

下面说一下put方法的逻辑:

  • 1. 插入null键

(首先这个是与HashTable的一个区别,HashMap支持key为null, 当然HashTable也不支持null值)
null键的这个Entry是插入在table(链表数组的0下标位置),代码是先判断该链表上有没有null键对应的Entry,如果有就替换,否则添加到链表头

addEntry(…)方法中,做了两个操作:1. 添加数据(这里是添加到链表头),2. 是否扩容(扩容的大小是上次的两倍)
这里数据的添加是将当前数据作为链表的头,然后将当前Entry的下一个指针指向原先的头。
这里扩容的判断上面java1.6和1.7又有点不一样,1.7中多了对当前桶的头Entry进行null判断。这就会导致在1.6中如果默认情况下,当前的size大于了临界值就会对数组进行扩容,但是在1.7中如果当前桶的头Entry不是null的情况下才会进行扩容(后来看了下java8的源码,在判断上面又把1.7多余的这个null判断取消掉了。。。无语中)。反正不管怎么样,肯定是要根据当前实际size和临界值threshold的大小进行数组的扩容的。
这里建议大家去读一读这里的源代码,就不黏贴了。。在这里的Entry拷贝上面进行的判断和内存的释放逻辑上简直让我觉得代码写的好严谨。。。

  • 2. 插入非null键

首先算出hash值(HashMap的hash算法在上面黏贴了),然后在求出所在桶的索引位置

1
2
3
4
5
// 根据求出的hash值和数组容量大小-1进行与运算, 保证返回值始终小于length-1
// 这里讲下HashTable中的实现是采用取模运算的, 没有这种位运算效率高
static int indexFor(int h, int length) {
return h & (length-1);
}

然后下面的处理就跟null键插入的逻辑基本一样,这里应该注意一个设计思想,就是大家在以后替换某个元素的时候,记得把旧的元素返回出去,以防止被调用者使用。
putAll()也没啥好讲的,基本就是扩容的区别上,其次就是将数据一个个调用put()方法。

2.4 get(Object key)

这里get方法同样也分为取null的key和非null的key之分。感觉也没啥好讲的,随便看下源码就一目了然了。
基本思路:获取hash值,获取桶位置,桶中的链表,判断Entry的保存的hash值,key值使用相同,都一致返回value,否则返回null。

2.5 remove(Object key)

remove方法的基本思路和get差不多,无非也就是获取对应key的value后进行链表中数据的删除罢了。。。就是移动指针进行删除,最后将删除的值返回出去。

2.6 Set<Map.Entry<K,V>> entrySet()

1
2
3
4
private Set<Map.Entry<K,V>> entrySet0() {
Set<Map.Entry<K,V>> es = entrySet;
return es != null ? es : (entrySet = new EntrySet());
}

这里返回的entrySet始终是一个实例,所以在调用HashMap的put和remove等方法后,会改变entrySet实例。
继续跟踪new EntrySet(), EntrySet这个类继承了AbstractSet类,实现了iterator()等方法,所以当我们调用iterator()的时候返回一个迭代器。
调用之后实际调用newEntryIterator(),返回new EntryIterator()。然后迭代的时候调用hasNext()和next(),当调用next()方法的时候实际调用nextEntry(),这里面的逻辑其实就跟一般的迭代相差不大。EntryIterator类是HashIterator的子类,调用EntryIterator类的next()方法其实是调用了父类中的nextEntry()方法,感觉有一种静态代理的感觉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private abstract class HashIterator<E> implements Iterator<E> {     
Entry<K,V> next; // next entry to return
int expectedModCount; // For fast-fail
int index; // current slot
Entry<K,V> current; // current entry
// 初始化
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
// 判断是否有下一个
public final boolean hasNext() {
return next != null;
}
// 获取下一个元素
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();

if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}

public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}

}

2.7 Set<K> keySet()

熟悉上面的entrySet()方法之后,keySet()方法其实就是回去上面的Entry然后再返回一个key罢了,是基于entrySet()方法的一个实现。这里需要注意的是,keySet()返回的实例和entrySet()一样,也是只有一个实例,所以对HashMap的put,remove等操作之后,也会改变这个实例。

2.8 杂鱼方法

剩余的size(),isEmpty(),containsKey(Object key),containsValue(Object value)就不说了,实现也是比较简单的。。

3. 总结

  1. 总体上java1.6和1.7基本没什么大的差异,比较集合分析来看,1.7在实例的初始化上采用类似懒汉的方式,不是一开始就是初始化维护数据的数组的大小,而是在调用方法的时候进行初始化。其余就是一些算法上的优化吧。
  2. 学完HashMap,基本HashTable也就基本ok了,与HashTable的不同就不详细总结了,大家自行百度。。有几个说下:
    • (1) HashTable大多数方法都是同步的,HashMap不是。
    • (2) HashMap支持空的key和空的value,HashTable不支持。
    • (3) HashMap没有了contains(Object o)方法
  3. 一个比较好的消息就是学完HashMap之后,HashSet类基本上就不用看了,实现90%的代码采用了HashMap类的方法。。。HashSet其实就是将值存储在HashMap的key中,所以有用的代码100行都不到。。。。
  4. 最后读一读源码可以在理解是豁然开朗,大家加油。
文章目录
  1. HashMap源码分析
  2. 1. HashMap简介
    1. 1.1 结构
    2. 1.2 hash算法
    3. 1.3 hashCode和equals
  3. 2. HashMap分析
    1. 2.1 关键属性
    2. 2.2 构造函数
    3. 2.3 put(K key, V value)
    4. 2.4 get(Object key)
    5. 2.5 remove(Object key)
    6. 2.6 Set<Map.Entry<K,V>> entrySet()
    7. 2.7 Set<K> keySet()
    8. 2.8 杂鱼方法
  4. 3. 总结