JDK 1.6 HashMap 源码分析
前言
前段时间研究了一下JDK 1.6 的 HashMap 源码,把部份重要的方法分析一下,当然HashMap中还有一些值得研究得就交给读者了,如有不正确之处还望留言指正。
准备
需要熟悉数组和链表这两个基本数据结构。如果对链表不太熟悉的话,可以来几道leetcode上的相关的链表算法题。熟悉后看 HashMap 就会快很多了。
基本原理:HashMap中的基本数据结构是数组加链表。table 是一个固定的数组。 数组里面的每个坑里面填的是一个叫Entry类。 其实就是一个固定的Entry数组。如果同一个坑里面存在两个不同的数据,那么两个数据就以链表的形式连接起来。最新的在最前面,原因是认为最新的容易经常被访问。
构造函数
基本原理知道了。现在直接研究带参数的构造函数就可以了,其他的构造函数就是调用该方法。
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();
}
MAXIMUM_CAPACITY = 1 << 30 2的30次方1073741824,也就是HashMap中table数组的大小不能超过该数字。 从上面代码可以看出来table的坑位只能是2的幂次方。如果你传入的initialCapacity为7 那么其实table 的大小为8; 也就是table的大小为传入进来的initialCapacity的数值大于该大小的2的幂次方。threshold 为他的阈值也就是 HashMap 的真正大小不能超过该值,超过了就进行扩容操作。 如果table数组的大小为16时。用它默认的扩容因子0.75f。那么他的阈值就是12。 也就是 table数据,数组中的加上链表的不能超过12。
我们看看第二个构造函数。参数为一个Map 我这里顺便把HashMap中的嵌套类Entry类说一下。可以自己再源码上观看。
public HashMap(Map<? extends K, ? extends V> m) {
// 对比该map的size大小,新的map最新的容量为16
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
// 创建所有map
putAllForCreate(m);
}
private void putAllForCreate(Map<? extends K, ? extends V> m) {
// 对每一个Entry进行迭代
for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
Map.Entry<? extends K, ? extends V> e = i.next();
//创建数据赋值
putForCreate(e.getKey(), e.getValue());
}
}
private void putForCreate(K key, V value) {
int hash = (key == null) ? 0 : hash(key.hashCode());
// 计算table中的位置
int i = indexFor(hash, table.length);
/**
* Look for preexisting entry for key. This will never happen for
* clone or deserialize. It will only happen for construction if the
* input Map is a sorted map whose ordering is inconsistent w/ equals.
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 相同的值覆盖。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
// 创建Entry
createEntry(hash, key, value, i);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
// 头节点插入
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
size++;
}
// 嵌套类 和HashMap类没关系 独立存在 默认权限 只能本包访问 也就是Java.util下的包访问 HashHap中并没有提供 Map.Entry<K,V>这样的返回对象出去。有的只是一个 Set<Map.Entry<K,V>>
//一个代理类。
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;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
put方法
为什么要从put方法研究起呢。因为HashMap中最常用得就是put方法。而且里面还涉及到扩容操作。如果把这些看懂了还是会很舒服得。
public V put(K key, V value) {
if (key == null)
// 如果key为null的话 直接添加到table[0]的位置 for 循环 table[0]上的元素。如果有元素的话 查看该元素的key是不是null 如果是的话 就更新value值,直到table[0]这个链表结束。 如果结束后还是没有的话,就把为null的key 对应的value 头插法 插入头部。 可以查看 putForNullKey(value) 方法。
return putForNullKey(value);
// 计算Hash值
int hash = hash(key.hashCode());
// 取key的Hash值得 二进制数得后几位。 如果key得hash为1101011 。而table这个数组得大小一直都是2的幂次方。 indexFor()方法做的事 key的hash与table.length-1做&运算。假如table数组的大小为16,也就是 11011011 & 1111 会等于 1011 。这个方法的意义也就是只要你得Hash值是随机的,碰撞性低,那么你在table中位置也就是 碰撞低的。
int i = indexFor(hash, table.length);
// 查询该table[i] 位置上的链表。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 如果 key相等 那么就更新 否则 下一位。。。。 直至结束。
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 修改次数加一
modCount++;
// 头插法 并看size是都大于阈值了,如果大于就要扩容了。
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
//扩容操作 2倍扩容
resize(2 * table.length);
}
// 扩容方法 参数为扩容大小
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建一个新得数组 名字叫做newTable length为 newCapacity
Entry[] newTable = new Entry[newCapacity];
// 扩容操作
transfer(newTable);
// 重新赋值
table = newTable;
// 阈值
threshold = (int)(newCapacity * loadFactor);
}
// 扩容操作
void transfer(Entry[] newTable) {
// 将原先的table数组 赋值给 src
Entry[] src = table;
int newCapacity = newTable.length;
// 逐个操作 从 src[0] 位置上的Entry 开始
for (int j = 0; j < src.length; j++) {
// 将src[j]的值给 e变量。
Entry<K,V> e = src[j];
// 对这个e 链表进行往下操作
if (e != null) {
// 清空
src[j] = null;
do {
//e 的下面一位 其实就是 next 后移 (这里如果两个线程同时在这里操作的话,A线程在这里执行这条语句后挂起的话,B线程完成扩容操作后,A线程再唤醒时,有可能发生循环链表。然后使用get方法的时候,导致死循环,cpu利用100%)
Entry<K,V> next = e.next;
// 对e 重新定位。
int i = indexFor(e.hash, newCapacity);
// 将e.next 从e 断开 并把e.next的值 指到 newTable[i]的值
e.next = newTable[i];
// 将 e 赋值给 newTable[i]
newTable[i] = e;
// e 往后移
e = next;
} while (e != null);
}
}
}
舒服了舒服了。 如果想看怎么发生死循环的可以看小灰的文章 高并发下的HashMap 。
get方法
get方法相对而言就比较简单了。
public V get(Object key) {
if (key == null)
// 直接查询table[0] 上链表key为 null的值
return getForNullKey();
// 定位table上的位置
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;
}
remove方法
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) {
int hash = (key == null) ? 0 : hash(key.hashCode());
// 定位位置
int i = indexFor(hash, table.length);
// 将table[i] 这个链表赋值给prev
Entry<K,V> prev = table[i];
// prev 赋值给 e
Entry<K,V> e = prev;
while (e != null) {
// 下面一位
Entry<K,V> next = e.next;
Object k;
// key是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
// 如果要删除的时table[i]的头部数据
if (prev == e)
// table[i] 等于next 删除头部
table[i] = next;
else
// 否则 删除这个
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
总结
HashMap中的学问,远不止这些。 其中还涉及到设计模式,迭代器等等。上面这些只是常用的。个人非常推荐把数组和链表这个两个非常基础的数据结构好好练习一下。虽然说早就把JDK 1.6的HashMap 源码看了一下,顺便把 ConcurrentHashMap中的一些源码也看了。但是写下来的时候,再看一遍,印象果然深刻多了。先把1.6的看了,在看1.8的吧。
JDK 1.6 HashMap 源码分析的更多相关文章
- 【JAVA集合】HashMap源码分析(转载)
原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储 ...
- JDK1.8 HashMap源码分析
一.HashMap概述 在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时 ...
- Java HashMap源码分析(含散列表、红黑树、扰动函数等重点问题分析)
写在最前面 这个项目是从20年末就立好的 flag,经过几年的学习,回过头再去看很多知识点又有新的理解.所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项 ...
- Java中HashMap源码分析
一.HashMap概述 HashMap基于哈希表的Map接口的实现.此实现提供所有可选的映射操作,并允许使用null值和null键.(除了不同步和允许使用null之外,HashMap类与Hashtab ...
- HashMap源码分析和应用实例的介绍
1.HashMap介绍 HashMap 是一个散列表,它存储的内容是键值对(key-value)映射.HashMap 继承于AbstractMap,实现了Map.Cloneable.java.io.S ...
- 【Java】HashMap源码分析——常用方法详解
上一篇介绍了HashMap的基本概念,这一篇着重介绍HasHMap中的一些常用方法:put()get()**resize()** 首先介绍resize()这个方法,在我看来这是HashMap中一个非常 ...
- 【Java】HashMap源码分析——基本概念
在JDK1.8后,对HashMap源码进行了更改,引入了红黑树.在这之前,HashMap实际上就是就是数组+链表的结构,由于HashMap是一张哈希表,其会产生哈希冲突,为了解决哈希冲突,HashMa ...
- Java BAT大型公司面试必考技能视频-1.HashMap源码分析与实现
视频通过以下四个方面介绍了HASHMAP的内容 一. 什么是HashMap Hash散列将一个任意的长度通过某种算法(Hash函数算法)转换成一个固定的值. MAP:地图 x,y 存储 总结:通过HA ...
- Java源码解析——集合框架(五)——HashMap源码分析
HashMap源码分析 HashMap的底层实现是面试中问到最多的,其原理也更加复杂,涉及的知识也越多,在项目中的使用也最多.因此清晰分析出其底层源码对于深刻理解其实现有重要的意义,jdk1.8之后其 ...
随机推荐
- 前端图片缓存之通过img标签加载GIF只能播放一次问题(转载)
最近项目中要求再网页中插入一张gif图片,让用户每次到达该位置时动一次,所以我们就制作了一张只动一次的gif图片通过img标签引入.当用户进入该位置时,通过remove()清除图片然后重新append ...
- Scala-IDE构建Maven项目
本教程演示如何使用Scala-IDE构建一个Scala Maven项目. 1. 下载Scala IDE 通过以下链接下载Scala IDE: http://scala-ide.org/download ...
- Mybait缓存机制
MyBatis同大多数ORM框架一样,提供了一级缓存和二级缓存的支持. 一级缓存:其作用域为session范围内,当session执行flush或close方法后,一级缓存会被清空. 二级缓存:二级缓 ...
- mysql之视图,触发器,事务等。。。
一.视图 视图是一个虚拟表(非真实存在),其本质是[根据SQL语句获取动态的数据集,并为其命名],用户使用时只需使用[名称]即可获取结果集,可以将该结果集当做表来使用. 使用视图我们可以把查询过程中的 ...
- 《剑指offer》旋转数组中的最小数字
本题来自<剑指offer> 旋转数组中的最小数字 题目: 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转. 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素. 例 ...
- HTML&javaSkcript&CSS&jQuery&ajax(七)
’一.HTML5 实例 <video width="430" controls> <source src="mov_nnn.mp4" t ...
- MySQL----数据库练习
一.多对多的正反向查询 class Class(models.Model): name = models.CharField(max_length=32,verbose_name="班级名& ...
- 打包谷歌浏览器 Chrome 已安装的插件
环境: OS - win7 64bit 旗舰版 Chrome - 37.0.2062.120 m 以 Smooth Gestures (一款鼠标手势插件)为例,在扩展程序面板 chrome://ext ...
- gerrit原理
个人理解: 这个就是审核代码是否合理性的工具,一般是资深研发人工确认代码是否存在缺陷,通过发送邮件通知变化. 也可理解为这个是个git服务器,多一个代码审查的功能. 但是它是个web界面,方便管理 ...
- 激活函数的比较,sigmoid,tanh,relu
1. 什么是激活函数 如下图,在神经元中,输入inputs通过加权.求和后,还被作用了一个函数.这个函数就是激活函数Activation Function 2. 为什么要用激活函数 如果不用激活函数, ...