HashMap的实现原理及部分源码分析
HashMap的实现原理及部分源码分析
Java8拥有增强的Map集合,Map是用来保存具有映射关系的数据。Map接口提供了大量的实现类,典型的实现有 HashMap和Hashtable等。Hashtable从它的类名看起来就很古老,因为它的命名都没有遵守Java的命名规范:每个单词首字母都应该大写。 HashMap(哈希表又叫散列表),在数据结构我们已经接触过,它的应用非常广泛,比如现在的缓存技术…
一、哈希表的概念
哈希表利用了数组的根据下标一次定位查询某个元素的特性,所以哈希表的主干是数组。
在不考虑哈希冲突时,对于在哈希表中进行添加,删除,查找等操作,性能高至仅需要一次定位即可完成,时间复杂度为O(1).
哈希函数: 存储位置 = F(key)
二、HashMap的特点
1.HashMap基于哈希表的Map而实现。
2.HashMap是线程不安全的。
3.HashMap的主干是一个初始值为空,长度为2的次幂的Entry数组。Entry 是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
4.HashMap保存数据的时候通过计算key的hash值来去决定存储的位置。
三、HashMap实现原理
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
可见HashMap继承自父类AbstractMap,实现了接口Map。还有Cloneable,Serializable接口
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 序列号 private static final long serialVersionUID = 362498820763181265L; // 默认的初始容量为16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认填充因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 当桶(bucket)上的结点数大于这个值时会转成红黑树 static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中结构转化为红黑树对应的table的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; // 存储元素的数组,总是2的幂次倍 transient Node<k,v>[] table; // 存放具体元素的集 transient Set<map.entry<k,v>> entrySet; // 存放元素的个数,注意这个不等于数组的长度。 transient int size; // 每次扩容和更改map结构的计数器 transient int modCount; // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 int threshold; // 填充因子 final float loadFactor; }
Map接口定义了通用的一些操作;Cloneable接口代表可以进行拷贝; Serializable 接口代表HashMap可被序列化。
源码:
public HashMap(int initialCapacity,float loadFactor){ //初始容量不能小于0,抛错 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); this.loadFactor = loadFactor; threshold = initialCapacity; init(); }
我们知道,在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),只有在执行put操作的时候才会真正构建table数组。
put操作:
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀 int i = indexFor(hash, table.length);//得到table中的实际位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value 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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败 addEntry(hash, key, value, i);//新增一个entry return null; }
在put方法中,直接判断table是否为null,如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)。再判断key是否为null,如果key为null,存储位置为table[0]或table[0]的冲突链上。
HashMap的hash表中的属性解读:
<1> capacity(容量):hash 表中桶的数量。
<2> initialCapacity(初始化容量):创建hash表时桶的数量。HashMap允许在构造器中指定初始化容量。
<3> size(尺寸):当前hash表中记载的数量。
<4> load factor(负载因子):负载因子等于“size/capacity”。负载因子为0,表示hash为空;为0.5,表示hash为半空;轻负载的hash表具有冲突少,适宜插入与查询的特点。
transient Entry<key,value>[] table = (Entry<k,y>[]) EMPTY_TABLE;
Entry其实是HashMap中的一个静态内部类,里面最重要的属性有key,value,next三个属性值,而这里的key和value是我们put时的key和value。Entry是一个单链表,而next属性的值是Entry,表示的是当前节点的下一个节点是哪一个Entry。
static class Entry<k,y> implements Map.Entry<k,y>{ fianl K key; V value; Entry<k,y> next; int hash; /** *Creates new entry. */ Entry(int h,K k,V v,Entry<k,y> n){ vlaue = v; key = k; hash = h; }
HashMap由数组和链表组成的,主体是数组,链表主要是为了解决哈希冲突的。在当前Entry的next指向Null时,即仅需一次寻址就可以完成(时间复杂度为O(1)),此时定位到的数组位置不含链表。若定位到的数组含有链表,则 添加操作的时间复杂度为O(n);查找操作则是根据key对象的equals方法来逐一比对查找。
Java8改进了HashMap的实现,使得HashMap在存在Key冲突时,依旧能有较好的性能。
HashMap和Hashtable的两点区别
<1> Hashtable 是一个自JDK1.0出现的古典的Map实现类,它是线程安全的。而HashMap是线程不安全的,所以HashMap性能较高一些。在有多个线程访问同一个Map对象时,使用Hashtable实现类会更好。
<2> HashMap可以使用null作为key或value,而Hashtable不允许使用null作为key或value,将引起NullPointerException异常。
public class NullInHashMap { public static void main(Sting[] args){ HashMap hm = new HashMap(); hm.put(null,null); hm.put(null,null); //并不能再次放入 hm.put("a",null); //可以放入 System.out.println(hm); } }
分析上述的代码:程序试图将三个Key-value对放入HashMap。因为Map中只能有一个Key-value对的key为null值,而可以有多个value为null。
输出结果:
{null = null, a = null}
HashMap和Hashtable判断两个value相等的标准:两个对象通过equals()方法比较返回true即可证明相等。
创建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 IllgalArgumentException("Illrcal load factor :"+loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity);} //指定初始容量 public HashMap(int initialCapacity){ this(initialCapacity,DEFAULT_LOAD_FACTOR); } //无参构造 public HashMap(){ this.loadFactor = DEFAULT_LOAD_FACTOR); } //添加指定的Map public HashMap(Map< ? extend K,? extends V > m){ this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m,false); }
静态工具方法tableSizeFor()
static final int MAXIMUM_CAPACITY = 1 << 30; /** * Returns a power of two size for the given target capacity. 作用:找出大于等于initialCapacity最小的2的整数幂 */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
这个方法在哪里被调用?下面:
this.threshold = tableSizeFor(initialCapacity);
在实例化HashMap实例时,如果给定了initialCapacity,由于HashMap的capacity都是2的幂,因此这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数)。
putMapEntries
//将m的所有元素存入本HashMap实例中 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { // 判断table是否已经初始化 if (table == null) { // pre-size // 未初始化,s为m的实际元素个数 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); // 判断是否需要扩容 if (t > threshold) threshold = tableSizeFor(t); } // 已初始化,并且m元素个数大于阈值,进行扩容处理 else if (s > threshold) resize(); // 将m中的所有元素添加至HashMap中 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
删除指定结点:
public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value); } // 删除“键为key”的元素 final Entry<K,V> removeEntryForKey(Object key) { // 获取哈希值。若key为null,则哈希值为0;否则调用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; // 删除链表中“键为key”的元素 // 本质是“删除单向链表中的节点” 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; }
remove()的作用就是删除“键为key”的元素。
- HashMap实现原理及源码分析
- HashMap、ConcurrentHashMap实现原理及源码分析
- HashMap实现原理及源码分析
- JDK8中的HashMap实现原理及源码分析
- HashMap 实现原理及源码分析
- 【Java集合学习系列】HashMap实现原理及源码分析
- HashMap实现原理及源码分析
- (10) java源码分析 ---- HashMap源码分析 及其 实现原理分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- HashMap实现原理及源码分析
- HashMap实现原理及源码分析
- HashMap实现原理及源码分析
- HashMap实现原理及源码分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- HashMap实现原理及源码分析
- HashMap实现原理及源码分析
- (10) java源码分析 ---- HashMap源码分析 及其 实现原理分析
- HashMap实现原理及源码分析
- HashMap实现原理及源码分析
- HashMap实现原理及源码分析