7. hashmap的底层实现
(1).HashMap的概述
HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 建和null 值, 因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。
(2).HashMap的数据结构
hashMap的存储原理为哈希表(hash table),也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表。
数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式)。而哈希表的主干部分为数组。哈希表的新增或者查找就是通过哈希函数(关键字)计算出实际存储的位置,从而进行新增或者查询。(这个函数的设计好坏会直接影响到哈希表的优劣)
哈希冲突
然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。
(3).实现原理
成员变量:
源码分析:
/** 初始容量,默认16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** 最大初始容量,2^30 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** 负载因子,默认0.75,负载因子越小,hash冲突机率越低 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** 初始化一个Entry的空数组 */ static final Entry<?,?>[] EMPTY_TABLE = {}; /** 将初始化好的空数组赋值给table,table数组是HashMap实际存储数据的地方,并不在EMPTY_TABLE数组中 */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; /** HashMap实际存储的元素个数 */ transient int size; /** 临界值(HashMap 实际能存储的大小),公式为(threshold = capacity * loadFactor) */ int threshold; /** 负载因子 */ final float loadFactor; /** HashMap的结构被修改的次数,用于迭代器 */ transient int modCount;
构造方法
源码分析:
public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } public HashMap(int initialCapacity) {//指定初始容量的构造方法 this(initialCapacity, DEFAULT_LOAD_FACTOR); } 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); // 设置负载因子,临界值此时为容量大小,后面第一次put时由inflateTable(int toSize)方法计算设置 this.loadFactor = loadFactor; threshold = initialCapacity; init(); } public HashMap(Map<? extends K, ? extends V> m) { //指定集合,转化为HashMap this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); inflateTable(threshold); putAllForCreate(m); }
put方法:
源码分析:
public V put(K key, V value) { // 如果table引用指向成员变量EMPTY_TABLE,那么初始化HashMap(设置容量、临界值,新的Entry数组引用) if (table == EMPTY_TABLE) { inflateTable(threshold); } // 若“key为null”,则将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头 // 所以table[0]的位置上,永远最多存储1个Entry对象,形成不了链表。key为null的Entry存在这里 if (key == null) return putForNullKey(value); // 若“key不为null”,则计算该key的哈希值 int hash = hash(key); // 搜索指定hash值在对应table中的索引 int i = indexFor(hash, table.length); // 循环遍历table数组上的Entry对象,判断该位置上key是否已存在 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))) { // 如果这个key对应的键值对已经存在,就用新的value代替老的value,然后退出! V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 修改次数+1 modCount++; // table数组中没有key对应的键值对,就将key-value添加到table[i]处 addEntry(hash, key, value, i); return null; }
可以看到,当我们给put()方法传递键和值时,HashMap会由key来调用hash()(也就是上面说到的哈希函数())方法,返回键的hash值,计算Index后用于找到bucket(哈希桶)的位置来储存Entry对象。
如果两个对象key的hash值相同,那么它们的bucket位置也相同,但equals()不相同,添加元素时会发生hash碰撞,也叫hash冲突,HashMap使用链表来解决碰撞问题。
分析源码可知,put()时,HashMap会先遍历table数组,用hash值和equals()判断数组中是否存在完全相同的key对象, 如果这个key对象在table数组中已经存在,就用新的value代替老的value。如果不存在,就创建一个新的Entry对象添加到table[ i ]处。
如果该table[ i ]已经存在其他元素,那么新Entry对象将会储存在bucket链表的表头,通过next指向原有的Entry对象,形成链表结构(hash碰撞解决方案)。
结论:也就是我们常说的:hashcode(位置)一样,值不一定相同;值相同,hashcode(位置)一定一样
addEntry()
源码分析:
/* * hash hash值 * key 键值 * value value值 * bucketIndex Entry[]数组中的存储索引 * / void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) { //如果数组长度大于等于容量*负载因子,并且要添加的位置为null resize(2 * table.length); //扩容操作,将数据元素重新计算位置后放入newTable中,链表的顺序与之前的顺序相反 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
/*
* 在链表中添加一个新的Entry的对象在链表的表头(1.8以前)
*/ void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
注意:在1.8之前,新插入的元素都是放在了链表的头部位置,但是这种操作在高并发的环境下容易导致死锁,所以1.8之后,新插入的元素都放在了链表的尾部。
get方法:
源码分析:
public V get(Object key) { // 若key为null,遍历table[0]处的链表(实际上要么没有元素,要么只有一个Entry对象),取出key为null的value if (key == null) return getForNullKey(); // 若key不为null,用key获取Entry对象 Entry<K,V> entry = getEntry(key); // 若链表中找到的Entry不为null,返回该Entry中的value return null == entry ? null : entry.getValue(); } final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } // 计算key的hash值 int hash = (key == null) ? 0 : hash(key); // 计算key在数组中对应位置,遍历该位置的链表 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; // 若key完全相同,返回链表中对应的Entry对象 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } // 链表中没找到对应的key,返回null return null; }
在get方法中,首先计算hash值,然后调用indexFor()方法得到该key在table中的存储位置,得到该位置的单链表,遍历列表找到key和指定key内容相等的Entry,返回entry.value值。
remove方法:
源码分析:
public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value); } 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);//先计算指定key的hash值 Entry<K,V> prev = table[i]; Entry<K,V> e = prev; //判断当前位置是否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--; if (prev == e) //若相等表明,table的当前位置只有一个元素,直接将table[i] = next = null 。 table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; //则让prev -> next ,e 失去引用。 e = next; } return e; }