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之后其 ...
随机推荐
- 移动端点击出现阴影 css解决方案
a,img,button,input,textarea,div{-webkit-tap-highlight-color:rgba(255,255,255,0);}
- mysql解决外网不能连接
mysql解决外网不能连接 Author:SimpleWu 或许有些时候会遇到通过ip地址访问项目的数据库,可是访问失败了. 现在给大家一种解决方案: #进入mysql数据库 USE mysql; # ...
- hdu4009最小树形图板子题
/*调了一下午的最小树形图,昨天刚刚看懂模板..最小树形图,就是有向图的最小生成树,很神奇==*/ #include<iostream> #include<cstring> # ...
- JS:事件循环机制、调用栈以及任务队列
点击查看原文 写在前面 js里的事件循环机制十分有趣.从很多面试题也可以看出来,考察简单的setTimeout也就是考察这个机制的. 在之前,我只是简单地认为由于函数执行很快,setTimeout执行 ...
- docker 给none镜像打镜像
1.遇到none的镜像打tag方式: docker tag + docker ID + 命名:版本名 案例:docker tag 41b7307026c0 gitlab:test 这就 ...
- 饮冰三年-人工智能-Python-10之C#与Python的对比
1:注释 C# 中 单行注释:// 多行注释:/**/ python 中 单行注释:# 多行注释:“““内容””” 2:字符串 C#中 "" 用双引号如("我是字符串&q ...
- python 装饰器--对有无参数的函数进行装饰
# 使用装饰器无参数的函数进行装饰# def func(funcionName): # print('-----1------') # def func_in(): # print('--func_i ...
- STL容器之优先队列
STL容器之优先队列 优先级队列,以前刷题的时候用的比较熟,现在竟然我只能记得它的关键字是priority_queue(太伤了).在一些定义了权重的地方这个数据结构是很有用的. 先回顾队列的定义:队列 ...
- caffe关闭建立网络的log输出
C++ google::InitGoogleLogging("XXX"); google::SetCommandLineOption("GLOG_minloglevel& ...
- Spring Boot配置大全
Spring Boot项目使用一个全局的配置文件application.properties或者是application.yml,在resources目录下或者类路径下的/config下,一般我们放到 ...