Java

来源:互联网 发布:hr管理软件知乎 编辑:程序博客网 时间:2024/05/22 00:08

源码下载地址:jdk1.7源码下载


HashMap的实现

源码分析对应JDK1.7

参考资料:
http://www.codeceo.com/article/java-hashmap-from-code-layer.html#0-tsina-1-24433-397232819ff9a47a7b7e80a40613cfe1
http://www.cnblogs.com/chenssy/p/3521565.html


先记住结论
HahsMap  继承AbstractMap;
HashMap  的key和value都是可以为null;
HashMap  是无序的;
HashMap  不是同步的,如果需要线程安全的HahsMap,可以通过Collections类的静态方法synchronizedMap获得
      线程安全的HashMap。


HashMap 中有两个重要参数:"初始容量","加载因子"
初始容量:hashmap底层table数组(entry)的长度
加载因子:算是一种阀值,当table数组中有效数据条目超出 加载因子与当前容量(table数组总长度)的乘积时,
则对哈希表进行扩容。
(有效数据条目意思:数组长度16,里面存了10条数据,有效条目就是10条数据,不是数组总长)


HashMap的数据结构
说到数据结构,想到这个图,画的很好,直接拿来用,感谢作者


之前在看上面这张HashMap结构图的时候,一直很好奇什么情况下链表会出现多个?
后来偶然测试发现一个例子:
这里我将hashMap源码中的代码拿出来测试

HashMap<String, Integer> map = new HashMap<String, Integer>(); map.put("语文", 1); map.put("数学", 2); map.put("英语", 3); map.put("历史", 4); map.put("政治", 5); map.put("地理", 6); map.put("生物", 7); map.put("化学", 8); map.put("化学", 9);在put的时候,"历史" 和 "语文" 就出现了entry链表然后我将hashMap put时的源码拿出来测试了一下。public static void main(String[] args) {int str = "语文".hashCode();int str2 = "历史".hashCode();System.out.println("hashCode : " + str);System.out.println("hashCode : " + str2);System.out.println("table 数组下标 : " + hash(str) % 16);System.out.println("table 数组下标 : " + (hash(str2) % 16));}/** * 根据key的hash计算出table数组的下标. *  * @param h * @return */static int hash(int h) {// This function ensures that hashCodes that differ only by constant// multiples at each bit position have a bounded number of collisions// (approximately 8 at default load factor).h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);}输出结果:hashCode : 1136442hashCode : 684332table 数组下标 : 0table 数组下标 : 0
你看,通过hashMap中的hash算法之后,计算出他们两个的下标是一样的,然后就出现了entry链表。



HashMap中 Entry的源码:

static class Entry<K, V> implements Map.Entry<K, V> {final K key;V value;// 当put发生碰撞时,指向下一个节点Entry<K, V> next;int hash;/** * Creates new entry.<br> * 构造函数<br> *  */Entry(int h, K k, V v, Entry<K, V> n) {value = v;next = n;// 下一个节点(entry链表)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;}/** * entry中的equlas方法,判断两个entry是否相等<br> * 若两个Entry的key和value都相等,则返回true<br> * 否则返回false *  */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();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;}// entry中的hashCode方法public final int hashCode() {return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());}public final String toString() {return getKey() + "=" + getValue();}/** * This method is invoked whenever the value in an entry is overwritten * by an invocation of put(k,v) for a key k that's already in the * HashMap. */void recordAccess(HashMap<K, V> m) {}/** * This method is invoked whenever the entry is removed from the table. */void recordRemoval(HashMap<K, V> m) {}}
通过上面那个图,可以看出,HashMap底层是一个数组,数组中存放的是Entry<K,V>;
通过entry的源码,发现每个Entry<K,V>的next,维护了entry链表

HashMap的构造方法

HashMap():
构造一个默认初始容量为16,默认加载因子为0.75的空HahsMap
HashMap(int initialCapacity):
构造一个  带指定初始容量 ,默认加载因子为0.75的空HashMap
HashMap(int initialCapacity, float loadFactor):
构造一个  带指定初始容量 和 加载因子的空HashMap
HashMap(Map<? extends K, ? extends V> m):
构造一个 映射关系与指定Map相同的新HashMap


下面看个通用的构造方法:
HashMap(int initialCapacity) 和 HashMap() 构造函数最后都是指向这个构造方法。

/*** 指定容量大小和加载因子的构造函数* * @param initialCapacity* @param loadFactor*/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);// Find a power of 2 >= initialCapacity// 找出"大于initialCapacity(指定容量)的最小2的幂"int capacity = 1;while (capacity < initialCapacity)capacity <<= 1;// 加载因子赋值this.loadFactor = loadFactor;// 设置HashMap的阀值,当HashMap中存储的数据量达到threshold时,需要扩容// Math.min():选择一个小的参数threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);// 创建指定长度的table数组(entry)table = new Entry[capacity];useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);init();}
这里可能有个疑问,或者面试的时候回问道:
为什么扩容一定是2的幂次?

这里直接说结论:

当HahsMap的容量是2的幂次时,不同的hash值发生碰撞的概率比较小,这样数据在table数组中分布的均匀,查询速度也快。


分析:

HashMap的底层数组长度总是2的幂次,在构造方法中有这样几行代码

int capacity = 1;while (capacity < initialCapacity)capacity <<= 1;
这段代码的作用是:找出"大于initialCapacity(指定容量)的最小2的幂"。
当length为2的幂次时indexFor()方法
static int indexFor(int h, int length) {return h & (length - 1);}
就相当于对length取摸,而且速度比直接取模快的多,这是HashMap在速度上的一个优化。

indexFor方法,该方法仅有一条语句:h & (length - 1),
这段代码除了上面说的取模运算外还有一个非常重要的责任:均匀分布table数据和充分利用空间。

这里我们假设length为16 和 15,其中16为2的幂次,h为5,6,7


当length为15时,h的6,7的结果是一样的,这就表示它们在table数组中的存储位置是相同的,也就是产生了碰撞。
6,7就会在一个位置上形成链表,这样子就会导致查询速度降低。

我们扩大h的值,从0 ~ 15,length为15

从上面这个图中发现,一共发生了8次碰撞,同时发现浪费的空间非常大,
1,3,5,7,9,11,13,15处没有记录,也就是没有存放数据。

而当length=16时,length-1=15,即1111,那么进行低位&运算时,值总是与原来得hash值相同,而进行高位运算时,
其值等于其低位值。(0~14 与 0~15 ,与table数组长度比较)

所以说当length=2^n时,不同的hash值发生碰撞的概率比较小,这样会使得数据在table数组中分布均匀,查询速度也快



put(K,V)方法

public V put(K key, V value) {// 如果key为空,将null存放在table[0]第一个位置,这就是HashMap允许存null的原因if (key == null)return putForNullKey(value);// 计算key的hash值int hash = hash(key);    -----------------(1)// 根据hash码和数组长度,计算table数组下标int i = indexFor(hash, table.length);  ---------------(2)// 从i处开始迭代entry链表,找到key保存的位置for (Entry<K, V> e = table[i]; e != null; e = e.next) {Object k;// 判断该链条上是否有hash值相同的(key相同)// 若存在key相同,直接覆盖value,返回旧的valueif (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;// 取出旧值e.value = value;// 赋新值e.recordAccess(this);return oldValue;// 返回旧值}}// 修改次数+1modCount++;// i处没有entry链表(该位置为空),将key,value添加至i处addEntry(hash, key, value, i);return null;}
a. 判断key是否为null,为null直接调用putForNullKey()方法处理
b. key不为null,计算key的hash值
c. 根据hash值,计算table数组中的下标位置
d. 如果该下标位置有entry,则比较是否是相同的key,如果是相同的,覆盖value
key不同则将新的key/value存入entry链表表头,旧的往后移。
e. 如果下标处没有entry,则直接存储


在上面代码的for循环处,此处的迭代为了防止存在相同的key值,如果两个hash值(key)相同,
HashMap的处理方式是用新的value替换旧的value,并没有处理key,这就解释了HashMap中没有两个相同的key,
(以及后面的HashSet,key是唯一的,这个后面再说)


在(1),(2)处,这里是比较关键的地方:
hash()方法

final int hash(Object k) {int h = 0;if (useAltHashing) {if (k instanceof String) {return sun.misc.Hashing.stringHash32((String) k);}h = hashSeed;}h ^= k.hashCode();// This function ensures that hashCodes that differ only by// constant multiples at each bit position have a bounded// number of collisions (approximately 8 at default load factor).h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);}
首先获得k的hashCode,然后对hashCode值进行计算(纯数学计算,各种移位计算)

indexFor()方法

static int indexFor(int h, int length) {return h & (length - 1);}
对于HashMap的table[]而言,数据分布需要均匀(最好每个下标只有一个元素,直接取值),
不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。
indexFor()方法的责任就是:均匀的分布table[]中的数据和充分利用空间

putForNullKey()方法,这个方法处理key为null的情况

/*** 该方法的作用是将"key为null"的键值对,存放到table[0]位置*/private V putForNullKey(V value) {// 直接遍历table[0]位置的Entry链表for (Entry<K, V> e = table[0]; e != null; e = e.next) {// 寻找链表中key为null的Entryif (e.key == null) {V oldValue = e.value;// 取出旧value值e.value = value;// 赋值新的valuee.recordAccess(this);return oldValue;// 返回旧value}}// 如果没有找到key为null的entry,说明table[0]位置没有Entry// HashMap被改变数计数器+1modCount++;// 将key为null的键值对添加入到Entry中addEntry(0, null, value, 0);return null;}

最终将key,value插入Entry的两个方法

void addEntry(int hash, K key, V value, int bucketIndex) {// 首先判断是否需要扩容// 'hashMap的大小' 大于等于 '阀值(加载因子*容量)' && table数组对应下标位置有数据if ((size >= threshold) && (null != table[bucketIndex])) {// 容量扩大两倍resize(2 * table.length);// key为null,hash取0// key不为null,根据key计算hashhash = (null != key) ? hash(key) : 0;// 重新计算哈希码的索引bucketIndex = indexFor(hash, table.length);}// 创建entrycreateEntry(hash, key, value, bucketIndex);}/*** * @param hash*            hash值* @param key* @param value* @param bucketIndex*            table数组下标*/void createEntry(int hash, K key, V value, int bucketIndex) {Entry<K, V> e = table[bucketIndex];table[bucketIndex] = new Entry<>(hash, key, value, e);size++;// hashMap大小+1}

有两点需要注意:
a. 链表的产生
HahsMap总是将新的Entry对象添加到bucketIndex处。
如果bucketIndex处已经有了对象,那么新添加的Entry将指向原有的Entry对象,形成一条Entry链。
但如果bucketIndex处没有Entry对象,直接将数据塞进去,不会形成链表

b. 扩容问题
随着HahsMap中元素得数量越来越多,发生碰撞的概率越来越大,所以产生的链表会越来越长,
为了保证HashMap的速度以及效率,系统必须进行扩容处理,而扩容处理非常耗时,所以如果能
预知HashMap中元素的数量,在构造的时候,直接设置。


get(Object key)方法

get方法源码

public V get(Object key) {// 如果key为null,直接调用getForNullKey()方法if (key == null)return getForNullKey();Entry<K, V> entry = getEntry(key);// 返回valuereturn null == entry ? null : entry.getValue();}private V getForNullKey() {// 默认重table[]数组第一位取Entryfor (Entry<K, V> e = table[0]; e != null; e = e.next) {if (e.key == null)return e.value;// 返回value}return null;}final Entry<K, V> getEntry(Object key) {// 计算key的hash值int hash = (key == null) ? 0 : hash(key);// 根据hash值,算出下标位置,从table数组中取出Entryfor (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {Object k;// 查找的key与entry中的key相同,则返回对应的valueif (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))return e;}return null;}
get()方法还是挺简单的,通过key的hash值找到table数组对应下标位置的Entry,然后取出value


其他的方法,看看源码大概都明白了,个人感觉HahsMap最主要的是Entry、hash计算、下标计算(indexFor)


HashMap小结

1. 什么时候会使用HashMap?它有什么特点?
HashMap是基于Map接口的实现,存储键值对时使用。
HashMap可以存储null的键值,是非同步的,HashMap存储着Entry对象

2. HashMap的工作原理
HashMap通过put / get方法存储和获取对象。
存储对象时,我们将kv传给put方法,它调用hashCode计算出hash值,从而得出存储在table数组中的位置,然后进一步存储。
每次存储时,HashMap会根据table数组的容量,自动调整容量(2的幂次方)
如果发生碰撞时,HashMap通过链表将产生碰撞的元素组织起来.

获取对象时,我们将key传给get,它调用hashCode计算hash从而得出key在table数组中的位置,并进一步调用equals方法确定键值对.

3. HashMap的put和get原理,equals和hashCode的作用是什么?
通过key的hashCode(),再计算出hash值,并计算出下标,从而获得在table数组中的位置.
如果产生碰撞,则利用key.equals()方法区链表中查找对应的节点.

4. 如果HashMap的大小超过了负载因子定义的内容,怎么办?
如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并重新调用hash方法.


原创粉丝点击