[JDK1.7源码阅读]HashMap
2018-01-03 17:34
429 查看
提示:
* 先挑简单的阅读,标题前面有一个
1 -的标识,首次阅读先看有该标识的;可配合右侧的目录查看整体
* 先看实现接口中定义的方法,再看子类自有的方法
* 很简单的代码就不解读了,比如刚开始可能感觉陌生,以后再复用的方法,就不会怎么解读了
* 本人English特别烂,不要太在意翻译得对不对,大部分都是机器翻译 :fa-meh-o: :fa-meh-o: :fa-meh-o: :fa-meh-o:
[TOCM]
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
散列表百度百科
莫等闲总结的一句话我感觉比较经典:散列表就是 数组 + 单向链表
从结构来看
顶层接口
Serializable:序列化接口没有方法或字段,仅用于标识可序列化的语义
Cloneable:
此类实现了 Cloneable 接口,以指示 Object.clone() 方法可以合法地对该类实例进行按字段复制。
实现/继承
AbstractMap关键属性:
属性 | 说明 |
---|---|
DEFAULT_INITIAL_CAPACITY = 1 << 4 = 16 | 默认table初始容量 |
float DEFAULT_LOAD_FACTOR = 0.75f | 默认的加载因子 |
threshold = DEFAULT_INITIAL_CAPACITY = 计算 | 表扩容临界值,默认值是初始容量,但是在首次初始化表/表扩容的时候会根据加载因子等参数(int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1) 重新计算临界值 |
MAXIMUM_CAPACITY = 1 << 30 = 1073741824 | 表的最大容量,大约10亿 |
AbstractMap的API
1 - HashMap()
构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。总结: 构造中只是校验了下指定容量和加载因子的有效范围。
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * 用指定的初始容量和加载因子构建一个空的HasMap * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) //最大容量 1073741824 大约10亿个哈希桶坐标 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; init(); //该方法是一个模版方法模式,在HashMap中是一个空的实现 }
1- Entry < K,V>
映射项(键-值对)。Map.entrySet 方法返回映射的 collection 视图,其中的元素属于此类。获得映射项引用的唯一 方法是通过此 collection 视图的迭代器来实现。这些 Map.Entry 对象仅 在迭代期间有效;更确切地讲,如果在迭代器返回项之后修改了底层映射,则某些映射项的行为是不确定的,除了通过 setValue 在映射项上执行操作之外。存储值的数组实体;
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; //下一个元素节点,由于计算hash值的时候有可能出现重复数据,所以需要使用该属性来做成一个hash链表,存储冲突的这些元素节点 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; } 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; } public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } 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) { } }
1 - V put(K key,V value)
在此映射中关联指定值与指定键。如果该映射以前包含了一个该键的映射关系,则旧值被替换。总结:
1. put操作中,有两个重要的操作:
1. 如果table为空就初始化table。
2. addEntry,把指定的key和value进行关联映射,该方法里面调用了一个核心函数 resize(int newCapacity)判断是否需要扩容
2. 根据 容量 和 加载因子计算出 扩容条件,每次扩容都是以原表的2倍扩充数组
3. 扩容是需要遍历table,而table中的每个元素都有可能是一个hash链表,所以是一个双层循环进行把oldTable中的数据复制到new tabl中
— 感觉看这一段有点吃力,对于hash不熟悉,链表还行,上一章LinkedList中讲到了链表,这里才明白,LinkedList中的链表是双向链表,而hashMap中的链表是单向链表。
public V put(K key, V value) { if (table == EMPTY_TABLE) { // 判断表是否为空,老套路了,在list里面经常这么干 inflateTable(threshold); //初始化table } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); 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; } /** * Inflates the table. 初始化table,计算有效容量和扩充容量触发条件 */ private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); //得到一个有效范围的容量 //计算扩充表容量的临界值 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); //直接忽略跳过该步骤 } // 不太明白highestOneBit的作用,大体上就是返回一个有效返回的容量 private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; } /** * Initialize the hashing mask value. We defer initialization until we * really need it. 这一步没看懂,就是int hash(Object k) 中需要的一个参数。 */ final boolean initHashSeedAsNeeded(int capacity) { boolean currentAltHashing = hashSeed != 0; boolean useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean switching = currentAltHashing ^ useAltHashing; if (switching) { hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0; } return switching; } /** * Offloaded version of put for null keys * 对key=null 的特殊处理 */ private V putForNullKey(V value) { //把null的值都存放在桶索引0的位置,先判断是否有值,有则覆盖 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++; //没有则添加到表中桶索引为0的位置 addEntry(0, null, value, 0); return null; } // 添加元素 并 判断是否需要进行表扩容 void addEntry(int hash, K key, V value, int bucketIndex) { //这里有两个条件: 表大小 大于等于 临界值 并且 根据桶索引获取到的值不为空,如果为空,就表示这个表还不需要进行扩容 if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); //每次扩容表的容量 是 当前表的2倍 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); //重新计算当前key的桶索引 } //创建entry并添加到表中 createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; //这里创建的entry的时候,把当前桶索引的元素取出来了,并放到了新的entry中。 //说hashMap底层实现是使用hash链表,难道就是指元素是使用了链表存储?因为桶索引会重复 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; } //计算桶下标索引 static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); } //计算hash值:这个算法看不懂。。 final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } 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); } //按容量进行表的扩容 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); } /** * Transfers all entries from current table to newTable. * 把旧表的数据拷贝到新表中,添加到新表中会重新计算桶索引和索引对应的元素链表也会重新排列 * */ void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { //每个entry里面都有可能是个链表,对链表进行遍历迁移到NEW table中 Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); //重新计算旧数据在新表中的桶下标索引,在同一个链表中,索引应该是同一个吧? e.next = newTable[i]; //上面代码如果推算正确,1. 假设这里是第一次进来,下一个就为null,2. 第二次进来,当前元素的next 就等于新表中的链表节点。 newTable[i] = e; //1.1 新表桶为当前元素, 2.1 由于上一步的操作,已经把节点链接起来了,所以这里直接赋值。 e = next; } //这里没有对old table中的索引做置空操作,会影响gc对这个数组的回收吗? 不太懂。。 } }
1 - V remove(Object key)
从此映射中移除指定键的映射关系(如果存在)。。1 - V get(Object key)
返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null。更确切地讲,如果此映射包含一个满足 (key==null ? k==null : key.equals(k)) 的从 k 键到 v 值的映射关系,则此方法返回 v;否则返回 null。(最多只能有一个这样的映射关系。)
返回 null 值并不一定 表明该映射不包含该键的映射关系;也可能该映射将该键显示地映射为 null。可使用 containsKey 操作来区分这两种情况。
1 - boolean containsKey(Object key)
如果此映射包含对于指定键的映射关系,则返回 true。1 - boolean containsValue(Object value)
如果此映射将一个或多个键映射到指定值,则返回 true。Set
Set keySet()
返回此映射中所包含的键的 Set 视图。该 set 受映射的支持,所以对映射的更改将反映在该 set 中,反之亦然。如果在对 set 进行迭代的同时修改了映射(通过迭代器自己的 remove 操作除外),则迭代结果是不确定的。该 set 支持元素的移除,通过 Iterator.remove、Set.remove、removeAll、retainAll 和 clear 操作可从该映射中移除相应的映射关系。它不支持 add 或 addAll 操作。Collection values()
返回此映射所包含的值的 Collection 视图。该 collection 受映射的支持,所以对映射的更改将反映在该 collection 中,反之亦然。如果在对 collection 进行迭代的同时修改了映射(通过迭代器自己的 remove 操作除外),则迭代结果是不确定的。该 collection 支持元素的移除,通过 Iterator.remove、Collection.remove、removeAll、retainAll 和 clear 操作可从该映射中移除相应的映射关系。它不支持 add 或 addAll 操作。相关文章推荐
- JDK 1.7源码阅读笔记(七)集合类之HashMap
- JDK源码阅读——HashMap(1.7)
- JDK1.7 HashMap源码分析
- [JDK1.7源码阅读]ArrayList
- JDK7集合框架源码阅读(三) HashMap
- Java Jdk1.8 HashMap源码阅读
- 【图解JDK源码】HashMap的容量大小增长原理(JDK1.6/1.7/1.8)
- 基于源码(jdk1.7)对HashMap的get()和put()的小结
- jdk 1.7 hashMap源码解读
- jdk 1.7 1.8 HashMap 源码分析
- JDK 1.7源码阅读笔记(一)String,StringBuilder,StringBuffer
- JDK 1.7源码阅读笔记(五)集合类之Collection
- JDK 1.8 HashMap 源码阅读一
- HashMap的put方法源码解析_JDK1.7
- JDK源码阅读之HashMap -- hash值计算方式、下标查找及tableSizeFor方法
- jdk源码剖析四:JDK1.7升级1.8 HashMap原理的变化
- HashMap源码解析(基于JDK1.7)
- JDK 1.7 HashMap原理及源码解析
- [JDK1.7源码阅读]LinkedList
- HashMap源码分析(JDK1.7)