HashMap源码阅读
来源:互联网 发布:idea运行单个java文件 编辑:程序博客网 时间:2024/05/16 07:47
本文内容来自于HashMap的源码内容,作为学习的记录。
HashMap是一种存储key-value对的对象。每个map不能包含重复的key,每个key至多映射一个value。我们可以看到HashMap实现自Map接口,继承自AbstractMap类。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{ //HashMap的默认容量为16、这个值通过移位来操作,而不是通过乘法运算符 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //HashMap的最大容量。(为什么是2的30次方?参考String的最大长度,可以猜想其受限于Integer类型的最大值是2^32-1和真实物理内存大小) static final int MAXIMUM_CAPACITY = 1 << 30; //默认加载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //HashMap存储内容的初始值。一个空的Entry数组 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //HashMap进行必要的调整时,table会进行变化。而且table的长度,必须,是2的整数次幂 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //HashMap中包含的key-value对的个数 transient int size; ···
如下图,HashMap的方法很多,选出最有特点的几个方法重点研究:
1. containsKey()
...... //采用getEntry(key)的方式,直接拿指定key向HashMap中取value,取到了就是true,取不到就是false public boolean containsKey(Object key) { return getEntry(key) != null; } /** * 如果在HashMap中不存在指定key的映射就会返回null */ final Entry<K,V> getEntry(Object key) { //防御式编程,如果hashMap中没有key-value映射对,则返回null if (size == 0) { return null; } //求出key对应的hashCode,直接使用hashCode向entry数组中拿值 //编程风格:如果key==null,则hashCode为0 int hash = (key == null) ? 0 : hash(key); //可以看到HashMap从table[hash]这个节点开始,以链表的方式一个个向后遍历,若一直没有找到符合条件的key,将一直遍历直到e=null才停止 //若在该条链中找到指定key对应的映射(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))),就返回e //for循环外部还有一个return null;就意味着如果整条链遍历结束,仍没有找到指定key,就说明hashMap中没有该key,就返回null,因此containsKey方法将返回false for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; //判断条件很长.e是当前节点(指针),判断条件认为有两种情况说明找到指定节点,1. e.hash=key.hash; //2. 将e的当前值与指定key使用==比较,或在保证key不是null的前提下,使用key.equals()方法来判断是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
2.put()
//使用指定key和指定value在HashMap中建立映射关系//如果HashMap中已经有指定的key,则新value会替换旧value//put方法其实会有返回值的,每次插入新值,会返回该key对应的旧value,如果key是新的key,返回nullpublic V put(K key, V value) { //如果当前的Entry数组为空,则将数组容量扩充为2的整数次幂 if (table == EMPTY_TABLE) { inflateTable(threshold); } //如果key为空则使用另一种方式添加 if (key == null) return putForNullKey(value); //key是不是null,其实区别并不大,只是如果key为null,就直接放在table的第一个位置 //计算key的hash值并由hash计算该key对应的index,是跟null型key的区别 int hash = hash(key); int i = indexFor(hash, table.length); //遍历链表,找到key的位置,e.recordAccess(this)方法是无法理解的.本来这个方法是就是空的 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; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null;} //如果key为null时,从Entry数组的第一个元素开始遍历,找到第一个key为null的 //entry,将旧值替换为新值,并将旧值返回 //e.recordAccess(this)是空方法,不知道是什么设计原则 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; } } //HashMap结构化 修改次数 +1.如果有另一个进程进行了非同步的修改,就会抛出ConcurrentModificationException异常 //问题:HashMap是怎样确定现在的modCount跟原本的modCount不一致? modCount++; // addEntry(0, null, value, 0); return null; }...... //实际插入Entry元素的操作 //threshold是一个阈值,如果当前key-value个数大于等于threshold,说明HashMap需要扩容了 //bucketIndex是桶索引,因为HashMap的数据结构是链表数组,每个数组元素都可能挂了一个链表 //一种类似于门帘式的结构,因为这个bucketIndex是要插入的key对应的那个桶的索引,只有size个数达到了阈值,并且bucketIndex处的Entry已经有元素了,这样才需要扩容了.... //扩容后,需要对hash和bucketIndex进行重新计算 void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } //具体addEntry的操作,只是不用担心resie的问题 createEntry(hash, key, value, bucketIndex); }//对这个map的内容进行rehash成一个新的容量更大的数组,当map中key的数量达到这个数组的threshold阈值时会自动调用//PS:得亏我看了源码,现在知道了,不是说桶的多少达到了数组长度的threshold,而是key的个数,而且不管这些key有多少映射到了同一个桶中,//也是,这种情况可以有效避免出现哈希表退化为链表的问题.//如果当前容量是MAXIMUM_CAPACITY,就会将Integer.MAXVALUE设为threshold,这样可以防止将来再调用 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]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } //将当前entries中的所有元素transfer到新的table中,新table的规格已经在resize中设置好了,具体操作由transfer来完成 void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; //看不懂,把null赋给e.hash然后再与e.key相比,相等的话返回0,否则返回e.key的hash值 if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } //根据e.hash和newCapacity新的容量来计算e元素的桶索引 int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }
- remove(Object key)
//删除节点操作,并且会返回删除节点的value. public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value); } //移除HashMap中指定的key,若该hashMap不包含该key的映射,则返回为空 final Entry<K,V> removeEntryForKey(Object key) { if (size == 0) { return null; } //编程风格学习 //三元表达式的输出类型需要注意 int hash = (key == null) ? 0 : hash(key); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; 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--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
- keySet()
/** * Returns a {@link Set} view of the keys contained in this map. * The set is backed by the map, so changes to the map are * reflected in the set, and vice-versa. If the map is modified * while an iteration over the set is in progress (except through * the iterator's own <tt>remove</tt> operation), the results of * the iteration are undefined. The set supports element removal, * which removes the corresponding mapping from the map, via the * <tt>Iterator.remove</tt>, <tt>Set.remove</tt>, * <tt>removeAll</tt>, <tt>retainAll</tt>, and <tt>clear</tt> * operations. It does not support the <tt>add</tt> or <tt>addAll</tt> * operations. * 返回map中的所有key 的set视图,此set是由map支持的,因此对map的修改,都将对set产生影响,反之亦然。 如果当set上的iterator正在执行中时,map被修改了,那么iterator的结果就是未 定义的(除非是对iterator进行remove操作。该set支持对元素的remove操作,可以使用remove/removeAll/Iterator.remove/retainAll/clear等操作,都将会同时移除元素在map中的映射。但不支持add/addAll操作 * */ public Set<K> keySet() { Set<K> ks = keySet; return (ks != null ? ks : (keySet = new KeySet())); } private final class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return newKeyIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsKey(o); } public boolean remove(Object o) { return HashMap.this.removeEntryForKey(o) != null; } public void clear() { HashMap.this.clear(); } }
从上可以了解到:
1、HashMap的数据结构一个Entry数组,而数组中的元素又是一个链表
2、HashMap的get操作都会先按key的hash得出该key对应的Entry数组的元素索引,然后以该元素为起点,以链表的方式往后遍历查找
3、HashMap中的Entry数组的size,原始长度是16,当元素个数达到了size * loadFactor=threshold时,将进行一次resize操作,就会声明一个新的长度为原来2倍的Entry数组,并将旧的Entry数组内容拷贝进去。而loadFactor加载因子是一个空间和时间效率上的平衡,如果loadFactor太高,空间利用率较高,但查找效率会降低,loadFactor过低,查找很快,但空间利用率不高
4、进行resize操作的目的有两个,一、扩展Entry数组。二、可以在一定程度上避免哈希表退货成链表的问题
5、关于put操作有个问题,在源码中可以看到,如果key是null,hashMap依旧可以插入到Entry数组的第一个元素后,然后返回旧key(null)的旧值,如果key已经存在,则put操作会找到key对应的桶,然后将key的旧值替换为新值,并返回该key对应的旧值,但对于key是新值,或key的hash为新值的情况,先调用addEntry(),进行必要的resize,重新计算hash和bucketIndex,然后调用createEntry(),为table[bucketIndex]申请空间,为new Entry()。
问题是:为什么可以确定不同的key,如果具有相同的hash,就一定具有相同的内容?因为以上并没有对按照key.hash找到了桶,但没有相同的key,需要在Entry后新增加节点的情况进行处理。对于新的要put的key,有两种情况,一、key为null,这种将直接插入到Entry[0]中,二、key!=null,若key!=null,也有两种情况,一、key.hash不存在,则将在进行的resize之后,新建一个Entry元素并申请空间,二、key.hash存在,我认为key.hash存在,也会有两种情况,一、key.hash存在,并且key已存在,将替换旧值为新值,二、这是key.hash存在,但key不存在的情况。HashMap没有处理。即,为什么在HashMap中没有向链表添加节点的操作,但是遍历链表的操作??
看完源码,还是有很多收获,其它收获有java的位运算、严谨的编程风格等,当然也限于本人现在的水平,有些操作是无法理解的,比较每次添加节点,都会执行一次e.recordAccess(this);而此方法的方法体是空的,暂时无法理解。还有其他比如keySet方法来获取HashMap的所有key的set视图,但是这个方法也看不懂,还有HashIterator、ValueIterator、KeyIterator内部数的设计,都不好理解。总之,这些都留待以后水平提高以后再去理解。
- hashmap 源码阅读
- HashMap源码阅读
- HashMap源码阅读
- HashMap源码阅读
- HashMap源码阅读笔记
- HashMap源码阅读
- Java源码阅读-HashMap
- HashMap源码阅读
- HashMap源码阅读
- HashMap源码阅读
- HashMap源码阅读笔记
- HashMap 1.7源码阅读
- 阅读HashMap源码
- HASHMAP源码阅读分析
- Java源码阅读-HashMap
- HashMap源码阅读
- Jdk8 HashMap源码阅读
- HashMap源码阅读笔记
- c++数据类型各占多少字节
- POJ1830 开关问题
- WiFi简介
- lvs、haproxy、nginx 负载均衡的比较分析
- JavaScript encode
- HashMap源码阅读
- Activity中onNewIntent的使用
- bzoj1085
- mysql 建立索引
- linux下文件写入模式
- xxxx
- on duplicate key update简单使用
- Open Judge 1.6 10
- Java 理论与实践: 正确使用 Volatile 变量