HashMap源码分析-jdk1.7
注:转载请注明出处!!!!!!!这里咱们看的是JDK1.7版本的HashMap
学习HashMap前先知道熟悉运算符合
*左移 << :就是该数对应二进制码整体左移,左边超出的部分舍弃,右边补零。举个例子:253的二进制码1111 1101,在经过运算253<<2后得到1111 0100。很简单
*右移 >> :该数对应的二进制码整体右移,左边的用原有标志位补充,右边超出的部分舍弃。
*无符号右移 >>> :不管正负标志位为0还是1,将该数的二进制码整体右移,左边部分总是以0填充,右边部分舍弃。
*取模运算 h & (length-1) 就是 h%lenght ,但是&比%具有更高的效率
总结下 X>>Y 相当于X/(2^Y) X<<Y 相当于X*(2^Y)
知道了运算后,先看创建HashMap的4个构造方法
/**
* 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
*/ //初始化HashMap,使用传入的初始容量和加载因子
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能小于0否则抛异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果自定义的初始容量大于默认的最大容量(1<<30=2^30)则将默认
//最大容量赋值给传入的初始容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果加载因子小于等于0或者加载因子不是一数字,抛异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor); //当前加载因子=传入加载因子
this.loadFactor = loadFactor;
//扩容变量 = 传入初始容量
threshold = initialCapacity;
//初始化
init();
} /**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/ //初始化HashMap传入一个自定义的初始容量,默认的加载因子(0.75)
public HashMap(int initialCapacity) {
//走上面的初始化方法
this(initialCapacity, DEFAULT_LOAD_FACTOR);
} /**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
//初始化HashMap,使用默认的初始容量(1<<4= 2^4 =16)
//和默认的加载因子(0.75)
public HashMap() {
//同上
this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);
} /**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/ //构造一个映射关系与指定 Map 相同的新 HashMap
public HashMap(Map<? extends K, ? extends V> m) {
//调用自己的构造,取出最大初始容量,默认加载因子
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//创建一个Entry[],初始化Hash掩饰码
inflateTable(threshold);
//将m的值put到新的HashMap中或者创建一个新的HashMap
putAllForCreate(m);
}
以上是HashMap的4种构造方法,下面我们来一步步分析源码
/**
* The default initial capacity - MUST be a power of two.
*/
//HashMap的默认初始化容量 2^4 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 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.
*/
//HashMap的默认最大容量 2^30 = 1073741824
static final int MAXIMUM_CAPACITY = 1 << 30; /**
* The load factor used when none specified in constructor.
*/
//HashMap的默认加载因子
//(注:当前容量 >= 最大容量 * 0.75 时会进行扩容)
static final float DEFAULT_LOAD_FACTOR = 0.75f; /**
* An empty table instance to share when the table is not inflated.
*/
//一个空的Entry[] 哈希表
static final Entry<?,?>[] EMPTY_TABLE = {}; /**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
//(transient:表示不进行序列化)将上面的Entry赋值过来,始终未2的幂
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; /**
* The number of key-value mappings contained in this map.
*/
// 表示HashMap的键值映射数目,已存元素个数
transient int size; /**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
//扩容变量 size>=threshold 时就会扩容
int threshold; /**
* The load factor for the hash table.
*
* @serial
*/
//加载因子
final float loadFactor; /**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
//修改次数
transient int modCount; /**
* The default threshold of map capacity above which alternative hashing is
* used for String keys. Alternative hashing reduces the incidence of
* collisions due to weak hash code calculation for String keys.
* <p/>
* This value may be overridden by defining the system property
* {@code jdk.map.althashing.threshold}. A property value of {@code 1}
* forces alternative hashing to be used at all times whereas
* {@code -1} value ensures that alternative hashing is never used.
*/
//这个常量在静态类部类中Holder使用过,可能影响初始化hash掩饰码
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
以上HashMap的参数解释,下面看看我们最常用的 put(K,V) 和 get(K)
先来看看put(K,V)
/**
* 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>.)
*/ //这里put 会返回 value 一般情况来说为空,发送冲突后value会返回发生冲突前的值
public V put(K key, V value) {
//根据上面的参数解释可以得到结果 第一次为true
if (table == EMPTY_TABLE) {
//第一次put会新建一个Entry[]赋值给table,初始化table的大小 2的幂
inflateTable(threshold);
}
//map的key可以为空就是在这
if (key == null)
//key为null都会将value存放在table[0]中
//第一次加入时:修改次数+1,元素数量+1
//如果有冲突则返回老值
//这里也会进行容量判断如果当前容量大于等于默认容量则扩容
//扩容为table.length*2 具体扩容放在后面解释
return putForNullKey(value);
//生成当前key的哈希值
int hash = hash(key);
//生成一个下标
int i = indexFor(hash, table.length);
//这里看当前下标中table是否有值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果有值则判断 hash值和当前生成的hash值是否相同且当前的key和Entry里面的key是否相同
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//如果相同
//将以前的value 赋值给 oldValue这个局部变量
V oldValue = e.value;
//当前的value赋盖原本已有值的value
e.value = value;
//一个空的预留方法,可以重写
e.recordAccess(this);
//返回老值
return oldValue;
}
}
//如果没有相同的key则修改次数+1
modCount++;
//将当前元素加入到新的Entry里面,存入table[i]中,元素数量+1
addEntry(hash, key, value, i);
//如果是一个新key都会返回null
return null;
}
解读put(K,V)中 putForNullKey(value) , indexFor(hash,table.length) , addEntry(hash,key,value,i)
/**
* Offloaded version of put for null keys
*/
//put的key为NULL
private V putForNullKey(V value) {
//先去查找table[0]中是否有值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//table[0]中的Entry key 为null 时
if (e.key == null) {
//这里和put里面的处理重复key,覆盖value
//将当前值赋值给一个局部变量
V oldValue = e.value;
//新的值覆盖老的值
e.value = value;
//空方法
e.recordAccess(this);
//返回以前的值
return oldValue;
}
}
//如果table[0]没有元素时 修改次数+1
modCount++;
//新建一个Entry将它放入table[0]中
addEntry(0, null, value, 0);
//返回null
return null;
} /**
* A randomizing value associated with this instance that is applied to
* hash code of keys to make hash collisions harder to find. If 0 then
* alternative hashing is disabled.
*/
//hash种子
transient int hashSeed = 0; /**
* Retrieve object hash code and applies a supplemental hash function to the
* result hash, 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.
*/
//生成哈希码
final int hash(Object k) {
//hash种子,在第一次put的时候会生成一次 下面的算法有兴趣的可以看看
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);
} /**
* Returns index for hash code h.
*/
//根据hash值和table的容量计算出一个下标
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);
} /**
* 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.
*/
//这里比较关键了,细心的朋友可能看到了当key为空的时候传入进来的值
//(0,null,value,0)
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果元素个数>=容量 且 当前出现碰撞
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容
resize(2 * table.length);
//如果当前key不为null则hash(key) 否则 hash为0,这里和上面key为0时hash保持一致
hash = (null != key) ? hash(key) : 0;
//获取一个下标覆盖原有下标
bucketIndex = indexFor(hash, table.length);
}
//创建一个Entry存入table[bucketIndex]中,元素+1
createEntry(hash, key, value, bucketIndex);
} /**
* Like addEntry except that this version is used when creating entries
* as part of Map construction or "pseudo-construction" (cloning,
* deserialization). This version needn't worry about resizing the table.
*
* Subclass overrides this to alter the behavior of HashMap(Map),
* clone, and readObject.
*/
//创建一个Entry
void createEntry(int hash, K key, V value, int bucketIndex) {
//这里会将当前下标(bucketIndex)的table里的值放入局部类e中
//这里的操作是为了解决碰撞具体往下面看new Entry
Entry<K,V> e = table[bucketIndex];
//创建一个新的Entry放入当前下标中
table[bucketIndex] = new Entry<>(hash, key, value, e);
//元素+1
size++;
} //一个静态的内部类 实现了 map.Entry接口
//这里只解释下碰撞处理过程,具体里面的内部类方法有兴趣可以自己看下很简单
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash; /**
* Creates new entry.
*/
//上面我们看到new实现了4个参数的构造,里面接收了一个局部类是做什么用的呢我们往下看
Entry(int h, K k, V v, Entry<K,V> n) {
//将新的value赋值给当前类的value
value = v;
//划重点----------------------------------
//这里是老的entry赋值给新的entry的next
//所以从这里我们可以得出key的hash发生冲突后不能说覆盖以前的值
//以前的值要想获取就在新的值的next里面
//他们的hash相同key不相同所以在上面源码我们可以发现只有当hash相同时才会进行碰撞处理
//因为下标是 hash 和 表容量 计算出来的
//注:没有发生碰撞时next为null
next = n;
//新的key
key = k;
//新的hash值
hash = h;
} }
以上就是put需要执行的一些动作
下面咱们进入扩容方法 resize(2*table.length)
//扩容传入一个新的容量
void resize(int newCapacity) {
//将当前table放入一个局部数组变量中
Entry[] oldTable = table;
//获得当前table的大小
int oldCapacity = oldTable.length;
//如果当前容量为1<<30=2^30 时
if (oldCapacity == MAXIMUM_CAPACITY) {
//当前的扩容因子大小赋值为2^31-1
threshold = Integer.MAX_VALUE;
//高并发的时候会进入,隐患
//划重点---------------------
//如果一直高并发会出大问题,譬如数组越界
return;
}
//创建一个新的table,容量为2*table.length 记住一定是2倍
Entry[] newTable = new Entry[newCapacity];
//创建一个新的hash种子,将以前的值放入newTable中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//扩容后的table覆盖现在的table
//来,这里有个坑,划重点--------------
//如果现在很多线程在get 这里会怎样?
//对应的value全为null
table = newTable;
//将扩容因子赋值为倆数中小的那一个
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
} //老的数据迁移到新table里面
void transfer(Entry[] newTable, boolean rehash) {
//新的表的容量赋值给一个局部变量
int newCapacity = newTable.length;
//循环以前的table,这里还没进行赋值所以table是以前的数据
for (Entry<K,V> e : table) {
while(null != e) {
//将entry里面的next赋值给局部类
Entry<K,V> next = e.next;
if (rehash) {
//如果是新的hash种子,生成新的hash值
e.hash = null == e.key ? 0 : hash(e.key);
}
//获取一个新的table的index
int i = indexFor(e.hash, newCapacity);
//将当前index下的table赋值给当前entry.next
e.next = newTable[i];
//将当前老table中的数据放入新table当前下标中
newTable[i] = e;
//将局部变量的值赋值给局部变量e
//while循环依据,主要是保存碰撞元素中next的值
//这里建议写自己个例子帮助理解,仿Entry类就好
e = next;
}
}
}
至此put(K,V)结束了,扩容也一并在里面讲解了
梳理一下:
1.第一次put值的时候初始化表空间
2.key为空的时候会加入
3.会根据key生成哈希值
4.如果发生碰撞(hash值相等)的时候,会将新的值存放在新的entry里,老的值会存放在新值的entry.next里
5.正常情况下 修改次数+1,元素数量+1,新建一个Entry放入table[bucketIndex]中
6.bucketIndex是根据hash值和当前table长度计算出来的
7.如果遇到扩容,新容器是旧容器的2倍,新的容器将重新生成hash种子,老元素会赋值到新容器中,注意:高并发的时候取值可能为null,严重时会出现数组越界,死循环的问题,所以HashMap是线程不安全的
下面咱们看看get(K)
//根据key获取value
public V get(Object key) {
//如果key为null,则去table[0]上的值
if (key == null)
return getForNullKey();
//根据key获取value
Entry<K,V> entry = getEntry(key);
//如果entry不为null则返回当前entry的value
return null == entry ? null : entry.getValue();
} //获取key为null的value
private V getForNullKey() {
//判断元素个数为0返回null
if (size == 0) {
return null;
}
//如果table[0]上有元素则返回当前table[0]的value
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
//否则返回null
return null;
} //key不为null时
final Entry<K,V> getEntry(Object key) {
//判断元素个数为0返回null
if (size == 0) {
return null;
}
//根据key获取hash值
int hash = (key == null) ? 0 : hash(key);
//通过hash和table容量取出下标获取当前下标table的entry
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//如果hash值和key都相等则返回对应的entry
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
//否则返回空
return null;
}
get相对来说就简单很多了!
咱们来继续来看看remove操作
//移除key对应table[]中的entry
public V remove(Object key) {
//移除key对应table[]中的entry
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
} //移除key对应table[]中的entry
final Entry<K,V> removeEntryForKey(Object key) {
//元素个数为0返回null
if (size == 0) {
return null;
}
//判断hash是否为null
int hash = (key == null) ? 0 : hash(key);
//生成table的index
int i = indexFor(hash, table.length);
//将当前下标的entry放在局部变量中
Entry<K,V> prev = table[i];
//局部变量pre赋值给局部变量e
Entry<K,V> e = prev;
//如果当前元素不为空
while (e != null) {
//先取出当前元素的next
Entry<K,V> next = e.next;
Object k;
//判断hash值和key是否相等,注意碰撞只是hash值相同key不同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
//相等修改次数+1
modCount++;
//元素-1
size--;
//一般情况下两个内存地址是相等的
if (prev == e)
//将next的值覆盖当前下标的值,没有碰撞时这时候table[i]当前值为null
table[i] = next;
else
//碰撞情况下将next的值覆盖pre.next也就是说这时候prev是有值的,移除的只是相同hashcode但key不同的碰撞体
prev.next = next;
//空方法
e.recordRemoval(this);
//返回移除的entry
return e;
}
//我们上面知道了碰撞后旧值在新值的next中所以这一段理解就是如果当前下标中有碰撞存在那么将e赋值给prev
prev = e;
//e等于当前e.next,如果不为空则继续进行移除
e = next;
}
//e为null直接返回
return e;
}
清除HashMap
//清除hashmap
public void clear() {
//修改+1
modCount++;
//循环table将每个下标对应的值都替换成null
Arrays.fill(table, null);
//元素个数归零
size = 0;
}
//循环table将每个下标对应的值都替换成null
public static void fill(Object[] a, Object val) {
for (int i = 0, len = a.length; i < len; i++)
a[i] = val;
}
我们注意到主要传入key那么就一定会进行hash然后通过hash和表容量计算出对应table的index也可以说bucketIndex.这个流程一定要注意
以上就是基本的HashMap源码解读了,有讲解不清楚,容易混淆的文字或者错误的地方欢迎指出.
HashMap源码分析-jdk1.7的更多相关文章
- HashMap源码分析 JDK1.8
本文按以下顺序叙述: HashMap的感性认识. 官方文档中对HashMap介绍的解读. 到源码中看看HashMap这些特性到底是如何实现的. 把源码啃下来有一种很爽的感觉, 相信你读完后也能体会到~ ...
- HashMap源码分析jdk1.6
HashMap数组每个元素的初始值为NULL 1.定义 public interface Map<K,V> { int size(); boolean isEmpty(); boolea ...
- ArrayList源码分析--jdk1.8
ArrayList概述 1. ArrayList是可以动态扩容和动态删除冗余容量的索引序列,基于数组实现的集合. 2. ArrayList支持随机访问.克隆.序列化,元素有序且可以重复. 3. ...
- ReentrantLock源码分析--jdk1.8
JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8ReentrantLock源码分 ...
- JDK1.8 HashMap源码分析
一.HashMap概述 在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时 ...
- HashMap 源码分析 基于jdk1.8分析
HashMap 源码分析 基于jdk1.8分析 1:数据结构: transient Node<K,V>[] table; //这里维护了一个 Node的数组结构: 下面看看Node的数 ...
- 源码分析系列1:HashMap源码分析(基于JDK1.8)
1.HashMap的底层实现图示 如上图所示: HashMap底层是由 数组+(链表)+(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...
- 【JAVA集合】HashMap源码分析(转载)
原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储 ...
- 【Java】HashMap源码分析——基本概念
在JDK1.8后,对HashMap源码进行了更改,引入了红黑树.在这之前,HashMap实际上就是就是数组+链表的结构,由于HashMap是一张哈希表,其会产生哈希冲突,为了解决哈希冲突,HashMa ...
随机推荐
- HDU-6546-Function(贪心)
链接: https://vjudge.net/problem/HDU-6546 题意: wls 有 n 个二次函数 Fi(x) = aix2 + bix + ci (1 ≤ i ≤ n). 现在他想在 ...
- AJAX - 向服务器发送请求请求
AJAX - 向服务器发送请求请求 XMLHttpRequest 对象用于和服务器交换数据.直线电机生产厂家 向服务器发送请求 如需将请求发送到服务器,我们使用 XMLHttpRequest 对象的 ...
- 详解cocos2dx 3.0的release版本在android平台的签名过程
当您的游戏准备发布前,需要编译成为release版本,命令中需要增加 -m release,编译命令如下: cocos compile -p android -m release 在编译结束后,生成x ...
- ubuntu彻底删除nginx
1.先执行一下命令: 1.1 删除nginx,–purge包括配置文件 sudo apt-get --purge remove nginx 1 1.2 自动移除全部不使用的软件包 sudo apt-g ...
- 南京网络赛C
分段打表大法好!!! 打表40min,A题1s https://nanti.jisuanke.com/t/41300 #include<bits/stdc++.h> #define int ...
- 初始化Thread
此处初始化的步骤和上文中介绍的一样,也是调用runClinit方法.首先设置初始化线程为CurrentThread,然后由于其父类Object此时的状态为CLASS_READY,因此就不需要初始化父类 ...
- Linux shell - cut命令用法(转载)
cut [-bn] [file] 或 cut [-c] [file] 或 cut [-df] [file] 使用说明 cut 命令从文件的每一行剪切字节.字符和字段并将这些字节.字符和字段写至标 ...
- vue 全局引用jq(打包后可能会遇到的问题)
问题描述:全局引用jquery打包到线上可能会不好使. 第一步: var path = require('path') var webpack = require('webpack') functio ...
- legend3---Laravel Homestead的安装和使用
legend3---Laravel Homestead的安装和使用 一.总结 一句话总结: 配置好homestead之后编码非常方便:在虚拟机或者外部机器里面操作代码两者都会同时改变. 1.Homes ...
- leetcode-mid-design-297. Serialize and Deserialize Binary Tree¶-NO -??
mycode 将list转换成树的时候没有思路 参考: deque 是双边队列(double-ended queue),具有队列和栈的性质,在 list 的基础上增加了移动.旋转和增删等 class ...