HashMap源码分析
序言
本来是在讲解List接口系列的集合,但是接下来我要讲的是那个HashSet,要明白HashSet就必须先要明白HashMap,所以在此出穿插一篇hashMap的文章,为了更好的学习HashSet。个人感觉初次看HashMap源码比较难,但是明白了,其实也不是很难,
--WZY
一、准备工作。
建议:先去看一下我的另一篇讲解hashcode的文章,让自己知道为什么使用hashcode值进行查询会很快。如果你已经懂了hashcode的工作原理,那么就可以直接往下看了。http://www.cnblogs.com/whgk/p/6071617.html
1、链表散列
什么是链表散列呢?通过数组和链表结合在一起使用,就叫做链表散列。这其实就是hashmap存储的原理图。
2、hashMap的数据结构和存储原理
HashMap的数据结构就是用的链表散列,大概是怎么存储的呢?分两步
1、HashMap内部有一个entry的内部类,其中有四个属性,我们要存储一个值,则需要一个key和一个value,存到map中就会先将key和value保存在这个Entry类创建的对象中。
//这里只看这一小部分,其他重点的在下面详细解释 static class Entry<K,V> implements Map.Entry<K,V> { final K key; //就是我们说的map的key V value; //value值,这两个都不陌生 Entry<K,V> next;//指向下一个entry对象 int hash;//通过key算过来的你hashcode值。
物理模型就是这样
2、构造好了entry对象,然后将该对象放入数组中,如何存放就是这hashMap的精华所在了。
大概的一个存放过程是:通过entry对象中的hash值来确定将该对象存放在数组中的哪个位置上,如果在这个位置上还有其他元素,则通过链表来存储这个元素。
3、HashMap存放元素的大概过程?
通过key、value封装成一个entry对象,然后通过key的值来计算该entry的hash值,通过entry的hash值和数组的长度length来计算出entry放在数组中的哪个位置上面,每次存放都是将entry放在第一个位置。在这个过程中,就是通过hash
3、loadFactor加载因子
loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,那么数组中存放的数据也就越稀,也就是可能数组中每个位置上就放一个元素。那有人说,就把loadFactor变为1最好吗,存的数据很多,但是这样会有一个问题,就是我们在通过key拿到我们的value时,是先通过key的hashcode值,找到对应数组中的位置,如果该位置中有很多元素,则需要通过equals来依次比较链表中的元素,拿到我们的value值,这样花费的性能就很高,如果能让数组上的每个位置尽量只有一个元素最好,我们就能直接得到value值了,所以有人又会说,那把loadFactor变得很小不就好了,但是如果变得太小,在数组中的位置就会太稀,也就是分散的太开,浪费很多空间,这样也不好,所以在hashMap中loadFactor的初始值就是0.75,一般情况下不需要更改它。
4、Size的意思?
size就是在该HashMap的实例中实际存储的元素的个数
5、threshold的作用?
threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。注意这里说的是考虑,因为实际上要扩增数组,除了这个size>=threshold条件外,还需要另外一个条件,这个就等在源码中看吧。
6、什么是桶?
根据前面画的HashMap存储的数据结构图,你这样想,数组中每一个位置上都放有一个桶,每个桶里就是装一个链表,链表中可以有很多个元素(entry),这就是桶的意思。也就相当于把元素都放在桶中。
7、capacity
这个就代表的数组的容量,也就是数组的长度,同时也是HashMap中桶的个数。默认值是16.
通过一张截图和图中的文字,来熟悉一下我们上面说的各种属性,介绍这些属性的博文:http://blog.csdn.net/fan2012huan/article/details/51087722
二、初识HashMap
惯例:查看HashMapAPI,申明一下,如果还暂时看不懂说的是什么意思,很正常,有疑惑的地方不用一直抓着不放,先看下面的源码分析,然后再回过头来看这个api文档讲的东西。就会发现pai说的东西就是我们源码中看到的那样。
//1、哈希表基于map接口的实现,这个实现提供了map所有的操作,并且提供了key和value可以为null,(HashMap和HashTable大致上市一样的除了hashmap是异步的和允许key和value为null),
这个类不确定map中元素的位置,特别要提的是,这个类也不确定元素的位置随着时间会不会保持不变。 Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key.
(The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map;
in particular, it does not guarantee that the order will remain constant over time.
//假设哈希函数将元素合适的分到了每个桶(其实就是指的数组中位置上的链表)中,则这个实现为基本的操作(get、put)提供了稳定的性能,迭代这个集合视图需要的时间跟hashMap实例(key-value映射的数量)的容量(在桶中)
成正比,因此,如果迭代的性能很重要的话,就不要将初始容量设置的太高或者loadfactor设置的太低,【这里的桶,相当于在数组中每个位置上放一个桶装元素】
This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets.
Iteration over collection views requires time proportional to the "capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings
). Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.
//HashMap的实例有两个参数影响性能,初始化容量(initialCapacity)和loadFactor加载因子,在哈希表中这个容量是桶的数量【也就是数组的长度】,一个初始化容量仅仅是在哈希表被创建时容量,在
容量自动增长之前加载因子是衡量哈希表被允许达到的多少的。当entry的数量在哈希表中超过了加载因子乘以当前的容量,那么哈希表被修改(内部的数据结构会被重新建立)所以哈希表有大约两倍的桶的数量
An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table,
and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before
its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table
is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
//通常来讲,默认的加载因子(0.75)能够在时间和空间上提供一个好的平衡,更高的值会减少空间上的开支但是会增加查询花费的时间(体现在HashMap类中get、put方法上),当设置初始化容量时,应该考虑到map中会存放
entry的数量和加载因子,以便最少次数的进行rehash操作,如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup
cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken
into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of
entries divided by the load factor, no rehash operations will ever occur.
//如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting
it perform automatic rehashing as needed to grow the table
三、HashMap的继承结构和实现的接口
继承结构很简单:上面就继承了一个abstractMap,也就是用来减轻实现Map接口的编写负担
实现的接口:
Map<K,V>:在AbstractMap抽象类中已经实现过的接口,这里又实现,实际上是多余的。但每个集合都有这样的错误,也没过大影响
Cloneable:能够使用Clone()方法
Serializable:能够使之序列化
四、HashMap的构造方法
有四个构造方法,构造方法的作用就是记录一下16这个数给threshold(这个数值最终会当作第一次数组的长度。),和初始化加载因子。注意,hashMap中table数组一开始就已经是个没有长度的数组了。
static final Entry<?,?>[] EMPTY_TABLE = {}; /** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
HashMap()
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ //看上面的注释就已经知道,DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR=0.75 //初始化容量:也就是初始化数组的大小 //加载因子:数组上的存放数据疏密程度。 public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
HashMap(int)
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
HashMap(int,float)
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0)//initialCapacity不能小于0 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY)//initialCapacity大于最大容量时 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor))//isNaN的作用是检测loadFactor是否是一个浮点数 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; //初始化loadFactor threshold = initialCapacity;//将threshold变为16,其实这里赋值为16没什么用,只是单纯的记录一下这个16的值,好在后面讲此值给数组的长度。后面再初始化数组长度的时候,就会把该值给重新赋值 init();//一个空的初始化方法 }
HashMap(Map<? extends K, ? extends V> m)
//将参数Map转换为一个HashMap。
public HashMap(Map<? extends K, ? extends V> m) {
//根据m中的size来设置初始化容量为多少合适,如果(m.size()/DEFAULT_LOAD_FACTOR)+1>DEFAULT_INITLAL_CAPACITY那么久取(m.size()/DEFAULT_LOAD_FACTOR)+1为初始化容量,反之取默认的,而加载因子就一直是默认的0.75 this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//增大table的容量的方法,table也就是链表散列的数组。threshold就是初始化的大小(m.size()/DEFAULT_LOAD_FACTOR)+1或者DEFAULT_INITLAL_CAPACITY16 inflateTable(threshold); putAllForCreate(m); }
inflateTable(threshold):这个方法只是在第一次对数组进行操作的时候,需要对数组进行增加来存储元素,因为其中什么元素都没有,就调用该方法。
/** * Inflates the table. */ //扩展table的功能 private void inflateTable(int toSize) { // Find a power of 2 >= toSize //返回一个大于等于 最接近toSize的2的幂数。 什么意思呢?2的3次方的幂数就是8.2的幂数就是每次都市2的几次方,2的幂数有可能是1,2,4,8,16等。
比如toSize=16,则16的2的幂数就是2的4次方还是16,比如toSize=17,那么最接近他的2的幂数就是2的5次方,也就是32.
//这里不点进去看了,因为里面就是为了实现上面这个目的而写的一些算法,有兴趣的可以自己去读一读 int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//设置一个需要扩增数组的一个标准 上面也有说到这个变量,不清楚的回头再看看
//将table数组大大小改变为capacity。 table = new Entry[capacity];
//初始化一个容量为capacity的哈希表,等用到的时候才真正初始化,返回值是boolean,先放一放。。。。。。。。。。 initHashSeedAsNeeded(capacity); }
五、常用的方法
介绍方法前,先看一下entry这个内部类,前面刚开始介绍了那么多,现在来看一下entry类中是如何构建一个什么样的结构
、put(K,V) 这一个put方法,真是有一大堆,大家应该慢慢消化完。一步一步,不懂得就看我写的注释,看看到底做了些什么。
//添加元素
public V put(K key, V value) { if (table == EMPTY_TABLE) {//判断是不是一个空的数组,也就是数组没有长度,通过构造方法创建,还没开始用,所以长度为0,这里就开始对数组进行增长。 inflateTable(threshold);//这个方法在第四个构造方法中介绍过,就是用来将数组变为大于等于threshold的2次幂。一开始threshold为16,那么根据算法,数组的开始长度也就是为16. } if (key == null)//这里可以看到,HashMap为什么可以使用null值了。 return putForNullKey(value);//将key为null的值存放到table中去。具体看下面putForNullKey的分析。 int hash = hash(key);//通过hash函数来将key转换为一个hash值 int i = indexFor(hash, table.length);//通过这个方法,将key转换来的hash值和数组的长度进行操作,得到在数组中的位置。 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))) {//只有相同的hash值并且 (可能key相同但是Key的hashcode不同也算key一样或者用equals比较得到相同)这说明里面有相同的key值。 V oldValue = e.value;//将老value用新value来替代。 e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i);//增加元素,方法内部实现很简单,就是将新增加的元素放第一位。而不是往后追加。 return null; }
putForNullKey
//key为null的元素,默认放在table数组中的第一个位置上。并且可以知道,如果第一个位置上有元素,则将原来的值覆盖掉,如果第一个位置上没有entry。那么就将自己放在第一个位置。 private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) {//遍历在数组第一个位置上的链表的每个元素(entry),其实就一个,因为null就一个。 if (e.key == null) {//如果发现有key为null的值,将现在的值赋值给原来key为null的value。 V oldValue = e.value; e.value = value; e.recordAccess(this);//一个空的方法。 return oldValue; } } modCount++; addEntry(0, null, value, 0);//上面的情况是有key为null的元素。现在这里是没有key为null的元素,则要在第一个位置上放上自己。请看下面对这个方法的解析。 return null; }
addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
//增加元素前,看一下元素的个数是否大于等于了我们规定存放在数组中的个数(threshold=数组容量*加载因子,只是一个存放判定数组是否需要扩增标准的变量),并且在table这个指定位置上有元素,则对数组进行扩展
//前面一个条件成立,扩展数组,可以理解,为什么还要加上后面一个条件呢?原因是:我们希望尽量在每个数组的每个位置上只有一个元素是最好的,数组的容量是大于threshold的,也就是说size虽然到了要扩增的那个标准,
但是在数组中可能还是有很多位置上没有放元素,所以在这些位置上增加元素是合理的,不需要扩增。只有等到在size达到了扩增的标准并且添加元素的位置上有别的元素的情况下,才进行阔增。 if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//扩增数组,看下面的分析。 hash = (null != key) ? hash(key) : 0;//扩增完数组后,原来的那些参数就没用了。需要重新计算,计算添加元素的hash值 bucketIndex = indexFor(hash, table.length);//通过hash值和数组的长度来计算出在数组中哪个位置 } createEntry(hash, key, value, bucketIndex);//如果没有扩增,则直接用传过来的参数进行创建entry,很简单,将添加进入的元素放桶中的第一个元素,也就是数组对应位置就是该元素,然后把之后的元素给现在元素的next,具体可以看这个方法的源码,很简单 }
resize()
void resize(int newCapacity) { Entry[] oldTable = table;//将老的数组存放在oldTable中 int oldCapacity = oldTable.length;//老的数组容量 if (oldCapacity == MAXIMUM_CAPACITY) {//判断老的容量 threshold = Integer.MAX_VALUE;//数组已经扩增到最大值了,所以将判定的标准增加到最大。 return; } Entry[] newTable = new Entry[newCapacity];//创建一个是原先两倍大小的数组。 transfer(newTable, initHashSeedAsNeeded(newCapacity));//因为新数组的长度改变了,所以每个元素在新数组中的位置也会改变,所以需要将每个元素的key得到的hashcode值重新算一遍,放入新数组中 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//这里就可以知道threshold的真正作用了,就是上面说的,作为判定是否需要扩增数组的一个标准。 }
transfer()
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) {//遍历老数组中的每一个桶,其实就是遍历数组的每一个位置。 while(null != e) {//遍历桶中的元素。e==null的情况是在一个桶中的最后一个元素的next为指向null,或者一开始这个桶就是空的。则需要遍历下一个桶。 Entry<K,V> next = e.next;//将e元素的下一个元素保存到next中。 if (rehash) {// e.hash = null == e.key ? 0 : hash(e.key);//将每个元素的hash值算出来,通过的是每个元素的key,这个算法感兴趣的就点进去看。key和value为null的hash就为0,所以都在数组的第一个位置。 } int i = indexFor(e.hash, newCapacity);//通过每个元素的hash值和所在数组的长度,计算出放在数组中哪个位置,这里就揭示了一开始我们的疑惑,不知道通过hash值怎么得到对应数组中的位置。 e.next = newTable[i];//每次在桶中添加新的数据,都是把新数据放在开头,旧数据放后面,这个桶就相当于是一个栈,先进去的就在最底层。 newTable[i] = e;//将自己放入数组中的对应位置 e = next;//桶中下一个元素。 } } }
indexFor()
static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1);//通过与运算,将h的二进制,和length-1的二进制进行与运算得出的结果就是数组中的位置。 }
经过这个方法,我们可以知道以下几点
1、构造方法中,并没有初始化数组的大小,数组在一开始就已经被创建了,构造方法只做两件事情,一个是初始化加载因子,另一个是用threshold记录下数组初始化的大小。注意是记录。
2、什么时候会扩增数组的大小?在put一个元素时先size>=threshold并且还要在对应数组位置上有元素,这才能扩增数组
3、对几个重要的方法的实现了解其作用,
putForNullKey:在put时,先判断可以是不是null值,是null值则用该方法进行处理
addEntry:增加元素的方法,其中会先判断是不是需要扩增数组,
不需要则调用createEntry():将以拥有的四个属性创建entry,并且做添加元素的逻辑代码,在第一位添加,而不是在末尾追加
需要扩增调用resize():这里面就是扩增的操作,将数组扩增为原来的两倍。扩增后,就需要使用transfer方法进行一些元素的移动,因为数组长度变化了,原来的元素就不会呆在原来的地方不动。
indexFor:算出元素在数组中的位置索引。
Remove
//通过key删除entry并返回其value值, public V remove(Object key) { //通过removeEntryForKey来完成删除功能 Entry<K,V> e = removeEntryForKey(key); //返回值。 return (e == null ? null : e.value); }
removeEntryForKey:里面代码很简单,就是找到key,然后将单链表的一些指向改一下。
final Entry<K,V> removeEntryForKey(Object key) { if (size == 0) {//看hashMap中有没有值 return null; } int hash = (key == null) ? 0 : hash(key);//看key是不是为null,如果为null,就直接返回0,否则通过hash函数计算出hash值 int i = indexFor(hash, table.length);//得到在数组中的位置。 Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) {//开始遍历桶中所有的元素,看有没有该key值,这个下面,prev代表前一个元素、e代表当前要检测的元素,next代表e的后一个元素,除了第一次prev=e,其他时候都市像前面这样。 Entry<K,V> next = e.next;//next记下一个元素 Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {//判断是该key值, modCount++; size--;//要删除元素了,size自减 if (prev == e)//只有在刚开始,自己处于第一个元素的时候,这个才会等于,其他情况prev代表的是删除元素的前一个元素, table[i] = next;//如果是第一个元素,直接把桶中第一个元素指向next,在next中保存着原先的第二个元素,现在变为第一个元素了 else //删除的就不是第一个元素,而是之后的,由于是单链表,就只需要改变一个指向引用,就是在要删除的元素之前的元素的next指向要删除的元素的next。 prev.next = next;//next:删除元素之后的一个元素,prev:删除元素的前一个元素,所以就有了这句话 e.recordRemoval(this);//一个空方法 return e; } prev = e;//记录要删除元素的前一个元素 e = next;//这个就是可能要删除的元素。 } return e; }
get(key):通过key来找到对应的value值
//通过key获得value,知道了hashMap的原理后,其实这些都市大同小异。 public V get(Object key) { if (key == null)//判断是否为null return getForNullKey();//这个方法里太简单了,做两件事情,第一,如果size=0,返回null,反之到数组的第一个位置获取null对应的value值,前提是有,没有也返回null。 Entry<K,V> entry = getEntry(key);//通过key获得entry对象,看一下里面是如何获得的,我猜跟那个通过key删除元素差不多。也还是先找到对应位置,然后遍历链表。 return null == entry ? null : entry.getValue();//返回 }
getEntry
//和remove(key)这个方法的逻辑一样,但是简单得多,因为不用删除,只需要找到然后返回entry对象即可 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) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
个人感觉其他方法都是大同小异,没有什么特别需要讲解的了,知道了上面的原理,基本上已经没有什么难度。
接下来看一下hashMap的迭代器有哪些特别的没有?
发现四个迭代器内部类都市私有的,并没有什么特别,HashMap对象不支持直接调用迭代器,但是可以获得对象中所有的key集合(keySet)或者entrySet等,然后通过这些来调用迭代器获得自己所有的key或者entry对象或者value值。
六、总结
我感觉这个hashMap花了我快一天的时间了,还是基础太差,不懂得都要去翻阅资料。通过阅读了HashMap源码,看一下我们学到了什么东西。
1、对于有些人可能还有一个疑问,就是为什么在使用inflateTable的时候需要数组的长度大于等于 最接近指定大小的2的幂呢?
这个问题,是关于hash算法的问题了,这里推荐一篇博文,就可以帮助你理解好这个,http://blog.csdn.net/oqqYeYi/article/details/39831029
2、通过源码的学习,hashMap是一个能快速通过key获取到value值得一个集合,原因是内部使用的是hash查找值得方法
3、要知道hashMap是一个链表散列这样一个数据结构
4、hashMap中的几个变量要知道什么意思,比如加载因子等知道这些,才能看得懂源码。