前言

​ 前段时间研究了一下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,也就是HashMaptable数组的大小不能超过该数字。 从上面代码可以看出来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 源码分析的更多相关文章

  1. 【JAVA集合】HashMap源码分析(转载)

    原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储 ...

  2. JDK1.8 HashMap源码分析

      一.HashMap概述 在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时 ...

  3. Java HashMap源码分析(含散列表、红黑树、扰动函数等重点问题分析)

    写在最前面 这个项目是从20年末就立好的 flag,经过几年的学习,回过头再去看很多知识点又有新的理解.所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项 ...

  4. Java中HashMap源码分析

    一.HashMap概述 HashMap基于哈希表的Map接口的实现.此实现提供所有可选的映射操作,并允许使用null值和null键.(除了不同步和允许使用null之外,HashMap类与Hashtab ...

  5. HashMap源码分析和应用实例的介绍

    1.HashMap介绍 HashMap 是一个散列表,它存储的内容是键值对(key-value)映射.HashMap 继承于AbstractMap,实现了Map.Cloneable.java.io.S ...

  6. 【Java】HashMap源码分析——常用方法详解

    上一篇介绍了HashMap的基本概念,这一篇着重介绍HasHMap中的一些常用方法:put()get()**resize()** 首先介绍resize()这个方法,在我看来这是HashMap中一个非常 ...

  7. 【Java】HashMap源码分析——基本概念

    在JDK1.8后,对HashMap源码进行了更改,引入了红黑树.在这之前,HashMap实际上就是就是数组+链表的结构,由于HashMap是一张哈希表,其会产生哈希冲突,为了解决哈希冲突,HashMa ...

  8. Java BAT大型公司面试必考技能视频-1.HashMap源码分析与实现

    视频通过以下四个方面介绍了HASHMAP的内容 一. 什么是HashMap Hash散列将一个任意的长度通过某种算法(Hash函数算法)转换成一个固定的值. MAP:地图 x,y 存储 总结:通过HA ...

  9. Java源码解析——集合框架(五)——HashMap源码分析

    HashMap源码分析 HashMap的底层实现是面试中问到最多的,其原理也更加复杂,涉及的知识也越多,在项目中的使用也最多.因此清晰分析出其底层源码对于深刻理解其实现有重要的意义,jdk1.8之后其 ...

随机推荐

  1. AngularJs中,如何在ng-repeat完成之后,执行Js脚本

    //ng-repeat生成4个li,生成后再执行自定义方法fn在每个li后加一根横线 <script> var myapp=angular.module('myapp',[]); myap ...

  2. 用flask实现的分页

    一.flask实现的分页组件 from urllib.parse import urlencode,quote,unquote class Pagination(object): "&quo ...

  3. java报错:The type java.lang.Object cannot be resolved. It is indirectly referenced from required .class files

    看包的路径是否对对:比如这样不对(...src/object/obietc) 其它解决方法转载: https://www.cnblogs.com/yadongliang/p/5918228.html ...

  4. Web Penetration Testing

    1.国外使用的一款在线工具,对web的信息收集很有帮助 地址http://archive.org  , WayBack Machine 主界面如下:对百度存档的历史信息进行查询. 2.IP地址归属信息 ...

  5. C++ Primer 笔记——union

    1.union是一种特殊的类.一个union可以有多个数据成员,但是在任意时刻,只有一个数据成员可以有值.当我们给union的某个成员赋值之后,该union的其他成员就变成未定义的状态了.分配给一个u ...

  6. C/C++遍历二维数组,列优先(column-major)比行优先(row-major)慢,why?

    C/C++遍历二维数组,列优先(column-major)比行优先(row-major)慢,why? 简单粗暴的答案:存在Cache机制! 稍微啰嗦一点:CPU访问内存(读/写,遍历数组的话主要是读) ...

  7. JAVA 数据类型数组

    普通int: public class Array { //成员变量 private int[] data; private int size; //构造函数,传入数组的容量capacity构造Arr ...

  8. Ubuntu下VS Code 字体设置 + 标签匹配、括号匹配插件

    Ubuntu下比较好看的字体有: Courier NewSource Code ProWenQuanYi Micro HeiWenQuanYi Micro Hei MonoUbuntuDroid Sa ...

  9. ROW_NUMBER() OVER(PARTITION BY COLUMN ORDER BY COLUMN DESC)函数的使用

    ROW_NUMBER() OVER(PARTITION BY COLUMN ORDER BY COLUMN DESC)函数的作用是指定COLUMN(列)进行分区,在分区内指定COLUMN(列)进行排序 ...

  10. webpack学习笔记--多种配置类型

    除了通过导出一个 Object 来描述 Webpack 所需的配置外,还有其它更灵活的方式,以简化不同场景的配置. 下面来一一介绍它们. 导出一个 Function 在大多数时候你需要从同一份源代码中 ...