Java容器HashMap源代码解析
来源:互联网 发布:淘宝开服装店教程 编辑:程序博客网 时间:2024/05/16 01:36
写在前面的话
本文针对的是Java1.6进行的源码分析,与其他版本可能存在差异。
哈希表
HashMap是基于哈希表来实现的,在介绍HashMap前,我们先了解一下哈希表。哈希表查找效率非常高,只需要O(1)的时间,相比之下,在一个大小为n的数组中查找数据,则需要O(n)时间。哈希表是基于数组来实现的,它的设计思路是把关键字key通过hash函数映射到数组的不同位置上。这样,当进行查找操作时,可以根据key直接得到数据在数组中的下标,只用常数时间就可以完成查找工作。
hash函数有多种实现方法:除法散列法、乘法散列法、全域散列法等。最常用的就是除法散列法,HashMap也是用改方法实现的hash函数。除法散列法就是通过取key除以数组大小m的余数,来将关键字k映射到m个槽的某一个中去。
在哈希表中,数组下标是通过hash函数计算出来的,这样不可避免的会发生多个关键字key映射到同一个数组下标的位置,这种情况我们称之为“碰撞”。主要有两种方法来解决碰撞,一种是链表法,一种是开放寻址法。开放寻址法是在发生碰撞后,根据一定的探查算法,继续探查,直到找到空槽为止。链表法是把散列到同一个槽中的所有元素都放在一个链表中。HashMap就是采用链表法来解决碰撞的。
HashMap源代码解析
1.HashMap的底层数据结构
上面说到HashMap是用哈希表来实现的,采用的是链表法来解决碰撞,所以哈希表的底层数据结构就是数组和链表。HashMap定义了一个Entry类型的数组table用来存放数据,我们就先从HashMap的内部类Entry看起。源代码如下:
//实现了map.Entry接口 static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; //存放链表的下一个值,用于解决碰撞 Entry<K,V> next; final int hash; /** * Creates new entry. */ 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; } //实现equal方法,如果key和value都相等,则返回true 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; } //实现hashCode方法 public final int hashCode() { return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); } //在添加元素时,会调用此方法,在这不进行任何操作 //在LinkedHashMap中会重写该方法 void recordAccess(HashMap<K,V> m) { } //在删除元素时,会调用此方法,在这不进行任何操作 //在LinkedHashMap中会重写该方法 void recordRemoval(HashMap<K,V> m) { } }
Entry实现了map.Entry接口,包含了键和值,next也是一个Entry对象,用于形成一个链表来解决碰撞。
2.HashMap属性
知道了HashMap的底层数据结构后,先来看HashMap中定义的一些重要属性:
/** * Entry类型数组,用于存放数据,可以根据需要扩容 */ transient Entry[] table; /** * HashMap的大小 */ transient int size; /** * 临界值,当HashMap大小大于临界值时,会进行扩容,threshold=capacity * load factor */ int threshold; /** * 加载因子 */ final float loadFactor; /** * 修改次数,同其他容器一样 */ transient volatile int modCount;
这里,详细说一下加载因子。加载因子其实就是HashMap中存储数据的饱和度,当HashMap中存储存储数据的个数size大于它的容量和加载因子的乘积后,HashMap就会自动扩容。所以,如果加载因子取值过大,虽然容器利用率高了,但是也加大了碰撞的可能性,导致查询效率低下;如果加载因子取值过小,虽然减少了碰撞的概率,但是容器利用率会很低,可能容器中还没存储多少数据,就要扩容了,造成很大的浪费。加载因子的取值需要折中考虑,HashMap给定了加载因子的默认值0.75,我们一般用给定的默认值即可。顺便再看一下HashMap给定的几个默认值:
/** * 默认容量大小,容量大小必须是2的幂次方 */ static final int DEFAULT_INITIAL_CAPACITY = 16; /** * 最大容量 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默认加载因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
对于容量大小,注释特别提到必须是2的幂次方。那么,为什么会有这个要求呢?我们可以从HashMap的indexFor()方法找到答案。该方法代码如下:
/** *返回hashCode的索引值 */ static int indexFor(int h, int length) { return h & (length-1); }
这个方法的作用是计算出hashCode对应数组table中的索引值。上面已经说了HashMap是通过取余来进行散列,但是取余要用到除法,计算效率比较低。当length大小为2的幂次方时,h&(length-1)与h%length是等效的,但是运算速度提升很大。所以,HashMap要求容量必须是2的整次幂。
3.构造方法
HashMap提供了四种构造方法,如下:
/** * 给定容量和加载因子的构造方法 */ 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); //找到大于initialCapacity的最小的2的幂次方,保证容量一直都是2的幂次方 int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; //计算临界值 threshold = (int)(capacity * loadFactor); //初始化数组 table = new Entry[capacity]; //初始化方法,在HashMap中并没有做任何操作,在LinkedHashMap中会重写 init(); } /** * 只给了初始容量的构造方法,加载因子会直接用默认值 */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 默认构造函数,加载因子和容量都用默认值 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); } /** * 带有map参数的构造函数 */ 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方法 putAllForCreate(m); }
前三个构造函数都比较好理解,我们可以看看最后一个构造函数的putAllForCreate方法是如何实现的。代码如下:
private void putAllForCreate(Map<? extends K, ? extends V> m) { for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) { Map.Entry<? extends K, ? extends V> e = i.next(); //遍历Iterator,把每个键值对放到table中 putForCreate(e.getKey(), e.getValue()); } } private void putForCreate(K key, V value) { //hashMap是允许键为null的,如果键为null,则hashCode值就为0 int hash = (key == null) ? 0 : hash(key.hashCode()); //计算hashCode在table数组中的索引,上面已经介绍过这个方法 int i = indexFor(hash, table.length); //计算出位置i后,遍历table[i]的next链表,判断是否是重复的key,如果是重复的 //则直接替换原先的value值 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { e.value = value; return; } } //如果不是重复的key,则调用createEntry方法,新建Entry createEntry(hash, key, value, i); } //计算hash值 static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } //创建Entry,并存入到table中 void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; //新建Entry,并把它放在链表的头部 table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //大小加1 size++; }
4.获取数据
public V get(Object key) { //HashMap允许key为null,如果key为null,单独处理 if (key == null) return getForNullKey(); //计算hash值,并得到索引,然后遍历所引处的链表,查找value int hash = hash(key.hashCode()); 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; } //单独处理key为null的方法 private V getForNullKey() { //HashMap会把key为null的Entry直接放在table[0]位置上,直接在该位置遍历链表就可以了 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
HashMap的put和get方法基本上是最常用的了,get方法的实现是先判断key值是否为null,这是因为HashMap是允许key值为null的,如果是null,则调用getForNullKey()方法单独处理;如果不为null,则计算key的哈希值,然后计算出在table中的索引,遍历索引处的链表,判断是否有与key值相等的,如果有,则返回对应的value,没有就返回null。注意,在HashMap里如果查找的键不存在,会返回null;而如果在python的字典中查找不存在的键,则就会报异常。
5.存储数据
public V put(K key, V value) { //HashMap允许key为null,如果key为null,单独处理 if (key == null) return putForNullKey(value); //计算hash值,得到索引 int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //遍历索引处链表,如果key值已经存在,则替换原先的value for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; //此方法在HashMap中没有任何操作,在LinkedHashMap中会重写 e.recordAccess(this); return oldValue; } } //修改数加1 modCount++; //如果key值不存在,在调用此方法添加Entry addEntry(hash, key, value, i); return null; } //单独处理key为null的方法 private V putForNullKey(V value) { //key为null的entry存放在table[0]处,所以先在table[0]处查找是否有key为null 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++; //如果不存在key为null的entry,则在table[0]处添加entry addEntry(0, null, value, 0); return null; } //添加entry方法 void addEntry(int hash, K key, V value, int bucketIndex) { //添加entry Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //如果HashMap的大小已经到达了临界值,则需要对table扩容 //为了保证容量一直是2的幂次方,每次直接扩容到原先的2倍 if (size++ >= threshold) resize(2 * table.length); } //扩容方法 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; //如果已经达到最大容量了,不再扩容 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; //把原来的数据都存放到新的table中 transfer(newTable); table = newTable; //重新计算临界值 threshold = (int)(newCapacity * loadFactor); } //转换方法 void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; //遍历原先的table for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; //遍历链表 do { Entry<K,V> next = e.next; //计算在新table中的索引值 int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
HashMap的put方法稍微复杂些,与get方法类似,它也会先判断key值是否为null,如果为null,则调用putForNullKey()方法处理;如果不为null,则找到索引值,遍历索引处的链表,判断key是否已存在,如果存在,则直接替换value,不存在的话,就调用addEntry()方法。我们再来看addEntry()这个方法,它与上文已经介绍过的createEntry()方法相似,不同之处在于多了扩容的步骤。这是因为createEntry()方法只在构造函数用到,这种情形下,table的容量已经提前计算出来,肯定够用,不必再考虑扩容的情形。而addEntry()是在table中新增数据,是有可能使得size达到临界值的,所以必须要考虑扩容。为了保证容量一直都是2的幂次方,所以每次扩容都是扩到原先的2倍。
下面我们再来看下HashMap是如何去扩容的。它会先判断是否已经达到最大容量了,如果已经达到最大容量了,则不再扩容。然后会通过调用transfer()方法,把原先的table中的所有值存入到新的table中,最后再重新计算临界值。
除了put方法,HashMap还提供了putAll()方法,如下:
public void putAll(Map<? extends K, ? extends V> m) { int numKeysToBeAdded = m.size(); //如果m没有要添加的key,不做任何除了 if (numKeysToBeAdded == 0) return; //如果要添加的key的数量已经超过了临界值,则计算新的容量并扩容 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,把m的键值对依次put到table中 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.判断数据是否存在
HashMap提供了containsKey()和containsValue()方法,分别用来判断是否存在某个key和某个value值。先来看containsKey()方法:
public boolean containsKey(Object key) { return getEntry(key) != null; } final Entry<K,V> getEntry(Object key) { //计算hash值,得到索引 int hash = (key == null) ? 0 : hash(key.hashCode()); //遍历索引处链表,如果Key存在,就返回相对应的entry,如果不存在,则返回null 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; }
这段代码与get()方法中去查找value的代码类似,不再详述。下面再看containsValue()方法。
public boolean containsValue(Object value) { //HashMap允许value为null,null不能用equals方法直接比较,所以单独处理 if (value == null) return containsNullValue(); Entry[] tab = table; //查找value,无法得到索引,只能遍历table for (int i = 0; i < tab.length ; i++) for (Entry e = tab[i] ; e != null ; e = e.next) if (value.equals(e.value)) return true; return false; } //单独处理value为null的情形 private boolean containsNullValue() { Entry[] tab = table; for (int i = 0; i < tab.length ; i++) for (Entry e = tab[i] ; e != null ; e = e.next) if (e.value == null) return true; return false; }
7.删除数据
public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); //如果key存在,则返回对应的value;key不存在,则返回null return (e == null ? null : e.value); } //删除entry方法 final Entry<K,V> removeEntryForKey(Object key) { //计算hash值,得到索引 int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; //遍历链表,找到对应的entry,如果是链表的头结点,则直接table[i] = next; //如果是中间结点,则让entry的上一个结点的next指向entry的下一个结点 while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; //如果是链表头结点,直接将下一个结点赋值给table[i] if (prev == e) table[i] = next; else prev.next = next; //此方法在HashMap中没有任何操作,在LinkedHashMap中会重写 e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
remove()方法的逻辑是先计算出索引值,然后遍历索引处的链表,找到与给定key相等的entry。如果entry是链表的头结点,则直接将entry的下一个结点赋值给table[i];如果是链表的中间结点,则将entry的上一个结点的next指向entry的下一个结点。如果key存在,则删除后返回对应的entry,如果不存在,则返回null。所以,即使去删除HashMap中不存在的Key,也不会出现异常的。
8.其它方法
clear()方法:把数组的所有值置为null,size置为0,如下:
public void clear() { modCount++; Entry[] tab = table; //把table的所有值置为null for (int i = 0; i < tab.length; i++) tab[i] = null; size = 0; }
isEmpty()方法:判断是否为空
public boolean isEmpty() { return size == 0; }
size()方法:返回大小
public int size() { return size; }
9.遍历哈希表
关于哈希表的遍历用法和代码解析请参考Java HashMap遍历方法和源代码解析
- Java容器HashMap源代码解析
- Java容器HashMap遍历方法和源代码解析
- Java容器ArrayList源代码解析
- Java容器LinkedHashMap源代码解析
- Java容器LinkedList源代码解析
- Java容器:HashMap和HashSet解析
- Java容器HashSet和LinkedHashSet源代码解析
- HashMap源代码解析
- Java容器 HashMap
- Java容器之HashMap
- Java容器三:HashMap
- java容器----HashMap
- Java HashMap源代码详解
- Java HashMap源代码详解
- Java HashMap源代码详解
- Java HashMap源代码详解
- Java HashMap源代码详解
- java hashmap源代码了解
- 店铺突然被淘宝给永久封号, 恨死淘宝霸王恨死马云!!
- WebService(二)jdk发布webservice服务
- Centos6搭建git
- Android webview 清除历史访问记录
- ActiveMQ在分布式系统作用和安装
- Java容器HashMap源代码解析
- ROC曲线
- element-ui 组件的 table 多选
- C++/MFC-List Control(图标、报表、列表)风格
- Html和CSS学习总结
- 排序算法之直接插入排序
- DPM——训练源码分析
- MySQL 事务介绍及使用
- Ambari2.5.0与HDP2.6.0集群安装配置