标签:style blog http color java 使用 os io
JDK源码学习系列08----HashMap
1.HashMap简介
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
<span style="font-size:10px;">public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable</span>
Map接口定义了所有Map子类必须实现的方法。Map接口中还定义了一个内部接口Entry(为什么要弄成内部接口?改天还要学习学习)。Entry将在后面有详细的介绍。
AbstractMap也实现了Map接口,并且提供了两个实现Entry的内部类:SimpleEntry和SimpleImmutableEntry。
2.HashMap的数据结构
Java最基本的数据结构有数组和链表。数组的特点是空间连续(大小固定)、寻址迅速,但是插入和删除时需要移动元素,所以查询快,增加删除慢。链表恰好相反,可动态增加或减少空间以适应新增和删除元素,但查找时只能顺着一个个节点查找,所以增加删除快,查找慢。有没有一种结构综合了数组和链表的优点呢?当然有,那就是哈希表(虽说是综合优点,但实际上查找肯定没有数组快,插入删除没有链表快,一种折中的方式吧)。一般采用拉链法实现哈希表。
ps:图片来源于网络
3.HashMap成员变量
HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生
rehash 操作。
/** * 默认的初始容量,必须是2的幂。 */ static final int DEFAULT_INITIAL_CAPACITY = 16; /** * 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换) */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默认装载因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 存储数据的Entry数组,长度是2的幂。 */ transient Entry[] table; /** * map中保存的键值对的数量 */ transient int size; /** * 需要调整大小的极限值(容量*装载因子) */ int threshold; /** *装载因子 */ final float loadFactor; /** * map结构被改变的次数 */ transient volatile int modCount;HashMap是通过"拉链法"实现的哈希表。
它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
size是HashMap的大小,它是HashMap保存的键值对的数量。
threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值="容量*加载因子",当HashMap中 存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
loadFactor就是加载因子。
modCount是用来实现fail-fast机制的。
4.HashMap构造函数
/** *使用默认的容量及装载因子构造一个空的HashMap */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//计算下次需要调整大小的极限值 table = new Entry[DEFAULT_INITIAL_CAPACITY];//根据默认容量(16)初始化table init(); } /** * 根据给定的初始容量的装载因子创建一个空的HashMap * 初始容量小于0或装载因子小于等于0将报异常 */ public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY)//调整最大容量 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); int capacity = 1; //设置capacity为大于initialCapacity且是2的幂的最小值 while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); } /** *根据指定容量创建一个空的HashMap */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);//调用上面的构造方法,容量为指定的容量,装载因子是默认值 } /** *通过传入的map创建一个HashMap,容量为默认容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的较大者,装载因子为默认值 */ public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); putAllForCreate(m); }
5.HashMap的内部类Entry<K,V>
HashMap底层是用一个Entry<k,v>数组实现的,每个Entry对象的内部又含有指向下一个Entry类型对象的引用。
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//对下一个节点的引用(看到链表的内容,结合定义的Entry数组,是不是想到了哈希表的拉链法实现?!) final int hash;//哈希值 Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue;//返回的是之前的Value } public final boolean equals(Object o) { if (!(o instanceof Map.Entry))//先判断类型是否一致 return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); // Key相等且Value相等则两个Entry相等 if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } // hashCode是Key的hashCode和Value的hashCode的异或的结果 public final int hashCode() { return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } // 重写toString方法,是输出更清晰 public final String toString() { return getKey() + "=" + getValue(); } /** *当调用put(k,v)方法存入键值对时,如果k已经存在,则该方法被调用(为什么没有内容?) */ void recordAccess(HashMap<K,V> m) { } /** * 当Entry被从HashMap中移除时被调用(为什么没有内容?) */ void recordRemoval(HashMap<K,V> m) { } }其中,Map接口:
K getKey();//获取Key V getValue();//获取Value V setValue();//设置Value,至于具体返回什么要看具体实现 boolean equals(Object o);//定义equals方法用于判断两个Entry是否相同 int hashCode();//定义获取hashCode的方法
6.HashMap的常用方法解析
6.1 V put(K key, V value)
public V put(K key, V value) { // 若“key为null”,则将该键值对添加到table[0]中。 if (key == null) return putForNullKey(value); // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。 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; // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出! if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 若“该key”对应的键值对不存在,则将“key-value”添加到table中 modCount++; addEntry(hash, key, value, i); return null; }put时的步骤为:①.若key为null,调用putForNullKey(value);
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }HashMap将“key为null”的元素都放在table的位置0处。
②.key不为null
先用hash()得到key的Hash码,然后通过indexFor得到在数组中的索引。再通过key.equals()在链表中找到插入 的位置
void addEntry(int hash, K key, V value, int bucketIndex) { // 保存“bucketIndex”位置的值到“e”中 Entry<K,V> e = table[bucketIndex]; // 设置“bucketIndex”位置的元素为“新Entry”, // 设置“e”为“新Entry的下一个节点” table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小 if (size++ >= threshold) resize(2 * table.length); }6.2 V get(Object key)
public V get(Object key) { if (key == null) return getForNullKey(); // 获取key的hash值 int hash = hash(key.hashCode()); // 在“该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.equals(k))) return e.value; } return null; }6.3 void putAll(Map<? extends K, ? extends V> m)
public void putAll(Map<? extends K, ? extends V> m) { // 有效性判断 int numKeysToBeAdded = m.size(); if (numKeysToBeAdded == 0) return; // 计算容量是否足够, // 若“当前实际容量 < 需要的容量”,则将容量x2。 if (numKeysToBeAdded > threshold) { int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1); if (targetCapacity > MAXIMUM_CAPACITY) targetCapacity = MAXIMUM_CAPACITY; int newCapacity = table.length; while (newCapacity < targetCapacity) newCapacity <<= 1; if (newCapacity > table.length) resize(newCapacity); } // 通过迭代器,将“m”中的元素逐个添加到HashMap中。 for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) { Map.Entry<? extends K, ? extends V> e = i.next(); put(e.getKey(), e.getValue()); } }6.4 containsKey()
containsKey() 首先通过getEntry(key)获取key对应的Entry,然后判断该Entry是否为null。
public boolean containsKey(Object key) { return getEntry(key) != null; }
final Entry<K,V> getEntry(Object key) { // 获取哈希值 // HashMap将“key为null”的元素存储在table[0]位置,“key不为null”的则调用hash()计算哈希值 int hash = (key == null) ? 0 : hash(key.hashCode()); // 在“该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; }
7.关于Hash冲突
8.HashMap的优化
容量调整
对于容量的调整,这个是HashMap较为重点的部分,仔细想想看,对于hashMap我们应该做的是尽量的避免hash冲突 ,此时对于数组的扩容就应该考虑了。不过一个蛋疼的问题也就 出现了,由于新数组的容量变了,原数组的数据就必须重新计算其再数组中的位置,并放入这就是resize。同时这也是最消耗性能的地方。那么在什么情况下对HashMap进行扩容呢?一般当HashMap的元素个事超过数组大小**loadFactory的时候,就会进行扩容,而loadFactor就是上文所说的负加载因子。默认值为0.75 例如数组空间为16,当元素超过16*0.75=12的时候就把数组大小扩为2*16=32,然后resize这是一个非常消耗性能的是,因此如果我们预料到HashMap中元素的个数,这就能够有效的提高hashMap的性能。
负载因子
为确定何时调整大小,而不是对每个存储桶中的链接列表的深度进行计数,基于hash的 Map使用一个额外的参数并粗略计算存储桶的密度。Map在调整大小之前,使用名为LoadFactory的参数指示Map将承担的“负载”量,即它的负载程度。loadFactory、map大小、容量之间关系: 如果(负载因子)x(容量)>(Map 大小),则调整 Map 大小
当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:
h & (table.length-1) hash table.length-1
8 & (15-1): 0100 & 1110 = 0100
9 & (15-1): 0101 & 1110 = 0100
-----------------------------------------------------------------------------------------------------------------------
8 & (16-1): 0100 & 1111 = 0100
9 & (16-1): 0101 & 1111 = 0101
从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就
产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。
同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,
1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,
这意味着进一步增加了碰撞的几率,减慢了查询的效率!
而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,
加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上
形成链表。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,
相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
9.总结ps:参考以下网友,感谢感谢~~
http://www.cnblogs.com/yuyutianxia/p/3800768.html
http://blog.csdn.net/lcore/article/details/8885961
http://www.cnblogs.com/hzmark/archive/2012/12/24/HashMap.html
JDK源码学习系列08----HashMap,布布扣,bubuko.com
标签:style blog http color java 使用 os io
原文地址:http://blog.csdn.net/sheepmu/article/details/38391561