JAVA之HashMap源码分析
来源:互联网 发布:java中基本数据类型 编辑:程序博客网 时间:2024/05/17 07:28
hashmap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。另外,HashMap是非线程安全的,也就是说在多线程的环境下,可能会存在问题,而Hashtable是线程安全的。
hashmap的数据结构结合了数组和链表的优点(如下图),既能像线性数组那样随机存取,又便于插入删除。
1.HashMap的属性
/** 2 * 默认的初始容量16. 3 */ 4 static final int DEFAULT_INITIAL_CAPACITY = 16; 5 /** 6 * 最大容量 7 */ 8 static final int MAXIMUM_CAPACITY = 1 << 30; 9 /**10 * 默认装载因子0.75f.11 */12 static final float DEFAULT_LOAD_FACTOR = 0.75f;13 /**14 * 存储数据的Entry数组15 */16 transient Entry[] table;17 /**18 * map中目前保存的键值对的数量19 */20 transient int size;21 /**22 * 需要调整大小的极限值(容量*装载因子)(map在当前capacity能存储的最大键值对数量)23 */24 int threshold;25 /**26 *装载因子,当HashMap的数据大小>=容量*加载因子时,HashMap会将容量扩容27 */28 final float loadFactor;29 /**30 * map结构被改变的次数31 */32 transient volatile int modCount;
size:该变量保存了该 HashMap 中所包含的 key-value 对的数量。threshold:该变量包含了 HashMap 能容纳的 key-value 对的极限,它的值等于 HashMap 的容量乘以负载因子(load factor)。
当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。
其中的负载因子loadFactor的理解为:HashMap中的数据量/HashMap的总容量(initialCapacity),当loadFactor达到指定值或者0.75时候,HashMap的总容量自动扩展一倍,以此类推。
hashmap的API
void clear()
从此映射中移除所有映射关系。
Object clone()
返回此 HashMap 实例的浅表副本:并不复制键和值本身。
boolean containsKey(Object key)
如果此映射包含对于指定键的映射关系,则返回 true。
boolean containsValue(Object value)
如果此映射将一个或多个键映射到指定值,则返回 true。
Set
/** *使用默认的容量及装载因子构造一个空的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); }
上面代码中粗体字代码包含了一个简洁的代码实现:找出大于 initialCapacity 的、最小的 2 的 n 次方值,并将其作为 HashMap 的实际容量(由 capacity 变量保存)。例如给定 initialCapacity 为 10,那么该 HashMap 的实际容量就是 16。
同时我们可以在创建 HashMap 时根据实际需要适当地调整 load factor 的值;如果程序比较关心空间开销、内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子
3.Entry的实现
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//对下一个节点的引用 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(); } void recordAccess(HashMap<K,V> m) { } void recordRemoval(HashMap<K,V> m) { } }
4.put()
这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现,我们应该已经清楚了。
public V put(K key, V value) { // 如果 key 为 null,调用 putForNullKey 方法进行处理 if (key == null) return putForNullKey(value); // 根据 key 的 keyCode 计算 Hash 值 int hash = hash(key.hashCode()); // 搜索指定 hash 值在对应 table 中的索引 int i = indexFor(hash, table.length); // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 找到指定 key 与需要放入的 key 相等(hash 值相同 // 通过 equals 比较放回 true) if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 如果 i 索引处的 Entry 为 null,表明此处还没有 Entry或者在i位置无法覆盖之前的内容 modCount++; // 将 key、value 添加到 i 索引处 addEntry(hash, key, value, i); return null; }
如果key值为空,我们来看看putForNullKey的处理过程:
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-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
上面方法提供了一个根据 hashCode() 返回值来计算 Hash 码的方法:hash(),这个方法是一个纯粹的数学计算,其方法如下:
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 Hash 码值总是相同的。接下来程序会调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:
static int indexFor(int h, int length) { return h & (length-1); }
当 length 总是 2 的倍数时,h & (length-1)将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。(其实就是一个简单的mod运算)。
但要注意的相同的key的hascode肯定是一样的,但hashcode相同的话key不一定相同。
根据上面 put 方法的源代码可以看出,当程序试图将一个 key-value 对放入 HashMap 中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但 key 不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。
如:
hashmap.put("数学",80);hashmap.put("英语",90);
如果“数学”和“英语”计算出来的hashcode是一样的,分别调用hash(hashcode)后,得到的index也是相同,意味着他们在那它们的存储位置相同,但是“英语”并不会覆盖掉“数学”,因为((k = e.key) == key || key.equals(k))会判断出他们两者key本就是不相同的,导致“数学”这个entry会存储在tabel[index]这个位置上,然后再指向“数学”entry。
5.addEntry()
void addEntry(int hash, K key, V value, int bucketIndex) { // 获取指定 bucketIndex 索引处的 Entry Entry<K,V> e = table[bucketIndex]; // ① // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //**指向e** // 如果 Map 中的 key-value 对的数量超过了极限 if (size++ >= threshold) // 把 table 对象的长度扩充到 2 倍。 resize(2 * table.length); // ②}
系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。
以上过程就是新建一个Entry对象,并放在当前位置的Entry链表的头部。然后判断size是否达到了需要扩容的界限并让size增加1,如果达到了扩容的界限则调用resize(int capacity)方法。
注意threshold和capacity的区别,threshold指map里存储的entry的个数(HashMap中的数据量),而initialCapacity指的是tabel数组的长度。
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; // 这个if块表明,如果容量已经到达允许的最大值,即MAXIMUN_CAPACITY,则不再拓展容量,而将装载拓展的界限值设为计算机允许的最大值。 // 不会再触发resize方法,而是不断的向map中添加内容,即table数组中的链表可以不断变长,但数组长度不再改变 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 创建新数组,容量为指定的容量 Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; // 设置下一次需要调整数组大小的界限 threshold = (int)(newCapacity * loadFactor); }
这里需要重点看看transfer方法:
void transfer(Entry[] newTable) { // 保留原数组的引用到src中, Entry[] src = table; // 新容量使新数组的长度 int newCapacity = newTable.length; // 遍历原数组 for (int j = 0; j < src.length; j++) { // 获取元素e Entry<K,V> e = src[j]; if (e != null) { // 将原数组中的元素置为null src[j] = null; // 遍历原数组中j位置指向的链表 do { Entry<K,V> next = e.next; // 根据新的容量计算e在新数组中的位置 int i = indexFor(e.hash, newCapacity); // 将e插入到newTable[i]指向的链表的头部 e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
tranfer方法将所有的元素重新哈希,因为新的容量变大,所以每个元素的哈希值和位置都是不一样的。
6.get()
public V get(Object key) { // 如果 key 是 null,调用 getForNullKey 取出对应的 value if (key == null) return getForNullKey(); // 根据该 key 的 hashCode 值计算它的 hash 码 int hash = hash(key.hashCode()); // 直接取出 table 数组中指定索引处的值, for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; // 搜索该 Entry 链的下一个 Entr e = e.next) // ① { Object k; // 如果该 Entry 的 key 与被搜索 key 相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
从上面代码中可以看出,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。
HashMap运用举例
package com.sort; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Scanner; import java.util.Set; /** * 统计一句英语的简单统计各个单词出现的次数 * * @author Owner * */ public class MapTest3 { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.println("请输入一句英语,单词间用空格隔开:"); String sentence = sc.nextLine(); String[] arr = sentence.split(" "); // 键代表着单词,值代表着次数 Map<String, Integer> map = new HashMap<String, Integer>(); for (int i = 0; i < arr.length; i++) { if (!map.containsKey(arr[i])) { map.put(arr[i], 1); } else { // 说明map中,存在该元素 int num = map.get(arr[i]); map.put(arr[i], ++num); } } System.out.println("统计单词出现的个数,结果如下:"); Set<String> set = map.keySet(); for (Iterator<String> iterator = set.iterator(); iterator.hasNext();) { String key = iterator.next(); Integer value = map.get(key); System.out.println(key + "=" + value); } } }
注:以上内容是我在学习HashMap相关内容时,瞻仰了几位大牛的笔记,进行了整理供以后学习,并非原创,请见谅。
- java源码分析之HashMap
- Java源码分析之HashMap
- java源码分析之HashMap
- Java源码分析之HashMap
- JAVA之HashMap源码分析
- Java源码分析之HashMap
- java源码分析之HashMap
- JAVA源码分析之HashMap
- Thinking in Java之HashMap源码分析
- Java集合类之HashMap源码分析
- Java集合之HashMap源码实现分析
- Java集合系列之HashMap源码分析
- Java进阶之----HashMap源码分析
- Java集合之HashMap源码实现分析
- Java 8 分析源码之 HashMap
- Java源码分析之HashMap(JDK1.8)
- Java集合之HashMap源码分析
- Java集合之HashMap源码分析
- Hadoop 2.7.2 源码导入eclipse
- JavaScript知识结构
- 集合框架之泛型
- 找不到或无法加载主类sun.tools.jps.jps
- C++ 容器
- JAVA之HashMap源码分析
- 自定义JSON.stringify方法,兼容老版本浏览器
- leetcode 187. Repeated DNA Sequences
- NY187快速查找素数
- HRBUST 2044 方方正的棋盘
- mina 使用线程池例子及socket并发测试
- 解决Spring Boot OTS parsing error: Failed to convert WOFF 2.0
- Android开发环境笔记
- 一个图理解Appium的设计原理