Java7源码浅析——对HashMap的理解
2016-09-04 16:03
281 查看
HashMap作为Java的集合框架成员中的一种,平时开发用得也不少了,心血来潮想着去看看它的具体实现,于是就去研究了一下Java7的对于HashMap部分的源码,同时也翻阅了一些网上优秀的博客,受益匪浅,决定记录一下自己对于HashMap的理解。
一、HashMap的定义
HashMap基于HashTable的Map接口实现(实现了Map接口,继承AbstractMap),此实现提供所有可选的映射操作,并且允许NULL值和NULL键(HashMap类大致相当于HashTable类,但它是非同步的,而且允许空值。)此类不保证映射顺序,特别是它不保证该顺序保持恒定一段时间。
一个HashMap的实例有两个影响其性能的参数:初始容量(capacity默认是16)和加载因子(load factor默认是0.75),通常缺省的加载因子较好地实现了时间和空间的均衡,增大加载因子虽然说可以节省空间但相对地会增加对应的查找成本,这样会影响HashMap的get和put操作。加载因子表示Hash表中的元素的填满程度,填充得越多对应的查找的时候发生冲突的机率就越大,查找的成本就越高。
Java基本的数据结构无非就是线性表、链表、哈希表。HashMap就是基于哈希表实现的Map:
从图中我们也可以看出其实它就是线性表和链表的结合,下面是HashMap的构造函数:
三、HashMap的存取实现
1、存储
我们先看源码:
(1)先判断key是否为null,如果是null,返回putForNullKey方法;
(2)如果不为空,则先计算hash值(根据key的hashCode方法得出对应的hashCode值,再通过hash方法自己重新计算,这里看源代码)
(3)得到计算出来的hash值之后,将hash值和表的长度进行逻辑与操作
(4)拿到下标,table[index]拿到这个位置上的元素e,看看这个元素,如果为null直接跳出循环addEntry;如果不为null则迭代判断是否存在相同的key,如果存在就会覆盖原来的,这就可以解释为什么HashMap里面没有相同的key了,因为它对没对key进行处理。
(5)最后是addEntry这个方法
(6)如果容量不够大,那就扩容resize。
2、读取
读取就比较好理解了,找到对应的key就能拿到value了,先看看源代码:
3、Fast-Fail机制
在HashMap里面定义了一个迭代器。大家都知道HashMap不是线程安全的,如果在迭代器的过程中别的线程修改了HashMap,将会抛出ConcurrentModificationException。在HashMap里定义了一个修改次数modCount,在初始化迭代器的时候会把这个modCount赋值给迭代器的expectedModCount。如源码所示:
到这里就是我对HashMap的所有理解了,有说得不对的地方还望给我指出。
一、HashMap的定义
HashMap基于HashTable的Map接口实现(实现了Map接口,继承AbstractMap),此实现提供所有可选的映射操作,并且允许NULL值和NULL键(HashMap类大致相当于HashTable类,但它是非同步的,而且允许空值。)此类不保证映射顺序,特别是它不保证该顺序保持恒定一段时间。
一个HashMap的实例有两个影响其性能的参数:初始容量(capacity默认是16)和加载因子(load factor默认是0.75),通常缺省的加载因子较好地实现了时间和空间的均衡,增大加载因子虽然说可以节省空间但相对地会增加对应的查找成本,这样会影响HashMap的get和put操作。加载因子表示Hash表中的元素的填满程度,填充得越多对应的查找的时候发生冲突的机率就越大,查找的成本就越高。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { /** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 16; /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;二、HashMap的数据结构
Java基本的数据结构无非就是线性表、链表、哈希表。HashMap就是基于哈希表实现的Map:
从图中我们也可以看出其实它就是线性表和链表的结合,下面是HashMap的构造函数:
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @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) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); }通过查看源码我们可以看到每次初始化一个HashMap都会去初始化一个table
/** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry[] table;Entry是HashMap里面的一个静态内部类:
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; }Entry是table中的元素,而Entry中又包含了key-value键值对,这样就可以通过key去指向下一个value,这就形成了链表,这样就可以呈现出如上图所示的数据结构了。
三、HashMap的存取实现
1、存储
我们先看源码:
/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); 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; }通过源码,我们可以大概知道HashMap存储的过程:
(1)先判断key是否为null,如果是null,返回putForNullKey方法;
(2)如果不为空,则先计算hash值(根据key的hashCode方法得出对应的hashCode值,再通过hash方法自己重新计算,这里看源代码)
/** * Applies a supplemental hash function to a given hashCode, which * defends against poor quality hash functions. This is critical * because HashMap uses power-of-two length hash tables, that * otherwise encounter collisions for hashCodes that do not differ * in lower bits. Note: Null keys always map to hash 0, thus index 0. */ 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); }这个算法是个纯粹的数学运算了,此算法加入高位计算,防止低位不变、高位变化时造成的Hash冲突。这是HashMap里面的精华所在,Java8已经做了简化:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }具体原理解释起来好长,反正就是很精妙,到现在我还是似懂非懂。。。。详情见这里JDK 源码中 HashMap 的 hash 方法原理
(3)得到计算出来的hash值之后,将hash值和表的长度进行逻辑与操作
/** * Returns index for hash code h. */ static int indexFor(int h, int length) { return h & (length-1); }返回一个index下标(可以理解成要见元素放进去的位置)
(4)拿到下标,table[index]拿到这个位置上的元素e,看看这个元素,如果为null直接跳出循环addEntry;如果不为null则迭代判断是否存在相同的key,如果存在就会覆盖原来的,这就可以解释为什么HashMap里面没有相同的key了,因为它对没对key进行处理。
(5)最后是addEntry这个方法
/** * Adds a new entry with the specified key, value and hash code to * the specified bucket. It is the responsibility of this * method to resize the table if appropriate. * * Subclass overrides this to alter the behavior of put method. */ void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }把原来table[index]位置上的Entry赋值给新的Entry的next,也就是说将新的Entry放入链头。
(6)如果容量不够大,那就扩容resize。
2、读取
读取就比较好理解了,找到对应的key就能拿到value了,先看看源代码:
public V get(Object key) { if (key == null) return getForNullKey(); 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; }在整个HashMap的实现中,它在底层用一个Entry数组去存储所有的key和value的值,找的时候判断一下hash值和key的值相等就可以返回value了。不同的东西放在不同的地方,找起来只要知道位置就能减少查找时间。
3、Fast-Fail机制
在HashMap里面定义了一个迭代器。大家都知道HashMap不是线程安全的,如果在迭代器的过程中别的线程修改了HashMap,将会抛出ConcurrentModificationException。在HashMap里定义了一个修改次数modCount,在初始化迭代器的时候会把这个modCount赋值给迭代器的expectedModCount。如源码所示:
private abstract class HashIterator<E> implements Iterator<E> { Entry<K,V> next; // next entry to return int expectedModCount; // For fast-fail int index; // current slot Entry<K,V> current; // current entry HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } }然后再迭代的过程中会判断:
if (modCount != expectedModCount) throw new ConcurrentModificationException();
到这里就是我对HashMap的所有理解了,有说得不对的地方还望给我指出。
相关文章推荐
- c语言实现hashmap(转载)
- JS hashMap实例详解
- 解析WeakHashMap与HashMap的区别详解
- 全面解析Java中的HashMap类
- 基于Java HashMap的死循环的启示详解
- Java中HashMap和Hashtable的区别浅析
- 重载toString实现JS HashMap分析
- Android中实现HashMap排序的方法
- 举例详解Java编程中HashMap的初始化以及遍历的方法
- Java8 HashMap的实现原理分析
- 深入理解Java中的HashMap的实现机制
- 深入解析java HashMap实现原理
- java HashMap的keyset实例
- Java中Hashtable类与HashMap类的区别详解
- java使用hashMap缓存保存数据的方法
- 浅析java中ArrayList与Vector的区别以及HashMap与Hashtable的区别
- java中Hashtable和HashMap的区别分析
- JAVA HashMap详细介绍和示例
- java中HashMap的原理分析
- 剖析Java中HashMap数据结构的源码及其性能优化