好看的网站 你明白吗/市场调研与分析
前言:
HashMap是我们在编程过程中非常常见的一个数据结构
主要功能是存放键值对(K-V)
我们都知道HashMap在jdk7中是以数组+链表的形式构成的(JDK8的改动还是比较大的,数组+链表/红黑树构成)
HashMap中数组和链表的关系如下
类似这个形状:
那么HashMap源码的原理到底是怎么样的呢,我们来分析一下
一、图解流程
二、源码分析
分析一:put方法分析
1.我们new一个HashMap对象
2.点进方法
这里有两个成员变量,通过看注释我们知道
DEFAULT_INITIAL_CAPACITY是默认初始容量-必须是2的幂。
DEFAULT_LOAD_FACTOR是构造函数中未指定时使用的负载因子。
3.我们使用put存入一个键值对
点进put方法查看
public V put(K key, V value) {if (table == EMPTY_TABLE) {//若table空 就是哈希表没有初始化//使用构造函数时设置的阈值(初始容量)初始化数组tableinflateTable(threshold);}if (key == null)return putForNullKey(value);//如果key是null,就没办法计算hash值,所以放到table的第一个位置int hash = hash(key);//根据key计算hash值的方法int i = indexFor(hash, table.length);//根据hash值 获得key应该存放的数组table中位置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已经存在了,就直接覆盖掉V oldValue = e.value;e.value = value;e.recordAccess(this);//返回被覆盖掉的valuereturn oldValue;}}modCount++;//该key不存在,放入链表中(jdk7是采用的头插的方法) addEntry(hash, key, value, i);return null;}
put方法流程图如下:
4.看初始化数组方法
private void inflateTable(int toSize) {// Find a power of 2 >= toSizeint capacity = roundUpToPowerOf2(toSize);//这个方法是将toSize值转变成2的幂,如果toSize=6,则capacity=8,2的4次方threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//重新计算阈值 threshold = 容量 * 加载因子table = new Entry[capacity];//用新计算的容量初始化数组table initHashSeedAsNeeded(capacity);}
分析二:hash计算分析
final int hash(Object k) {
//将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)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);}
但是这个hash值并不是数组的下标还要有一个运算才能得出下标:
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值与数组长度-1作与运算就是数组的下标}
以上的种种干扰变化可以有效的减少碰撞
分析三:transfer扩容机制分析:
我们在put方法的时候发现一个给数组赋值的方法addEntry
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);}createEntry(hash, key, value, bucketIndex);}
扩容函数:
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);//新的阈值}
转移数组函数:
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;if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}int i = indexFor(e.hash, newCapacity);//重新计算元素对应的新数组下标值e.next = newTable[i];newTable[i] = e;e = next;//链表下一个节点,防止链表丢失}}}
流程图解:
分析四:获取元素源码分析
get方法
public V get(Object key) {if (key == null)//如果key是null直接返回数组第一个元素,因为数组第一个元素就是放的key=nullreturn getForNullKey();Entry<K,V> entry = getEntry(key);//获取value方法return null == entry ? null : entry.getValue();}
进入getEntry方法查看
final Entry<K,V> getEntry(Object key) {if (size == 0) {//数组长度为0,没有元素也就是获取不到就返回nullreturn null;}int hash = (key == null) ? 0 : hash(key);//计算该key的hash值for (Entry<K,V> e = table[indexFor(hash, table.length)];//indexFor函数算出下标,根据下标获取键值对对象e != null;e = e.next) {//遍历该位置的链表直到找到对应的key,如果没有旧直接返回null就好了Object k;if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;}return null;}
有了put方法的基础,get方法不论实现还是理解都是比较好理解的
获取元素流程图解:
到这里我们分析完了HashMap的put和get最主要的两个方法,这篇博客先写这么多,休息一下太累了