1、关键属性与内部类

1.1属性

  使用拉链法解决hash冲突的hash表,hash表里存了多少个元素、数组有多长以及当什么情况下需要扩容是重要的参数。

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
transient int size;
int threshold;
final float loadFactor;
  • table,用来实现拉链法的数组
  • size,该数组里一共有多少个节点包括拉链法链表里的节点
  • threshold,临界值,当size大于threhold的时候HashMap会扩容。threshold=Capacity*loadFactory
  • loadFactory,负载因子,用来衡量HashMap的满的程度
  • Capacity,数组的长度

1.2内部类

 static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;

  存储的值、指向下一个node的指针、hash值。HashMap拉链法使用的是单链表。

2、构造器

2.1 无参构造器

  一般的集合类都会提供一个无参的构造器,无参构造器使用默认的参数传入有参构造器。

    public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

2.2 有参构造器

  • initialCapacity。用于初始化threshold

  • loadFactory。初始化loadFactory

  有参构造器在两个地方有点违反我的理解:

  • 我知道HashMap内部维护一个数组,但是HashMap的构造器并没有新建该数组
  • 数组的长度用Capacity变量存储,该构造器有一个initialCapacity的参数,但这个参数没有赋值给Capacity而是threshold。init方法里也没有执行新建数组的操作。结合之前阅读源码的经验:数组的初始化一定发生在第一次put的时候。
    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); this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}

4、put  

  果然table的初始化在put里,这可以可以理解成HashMap的懒加载机制,只有用户在Put一个值的时候说明HashMap才真正被使用,也只有在这个时候才需要初始化数组。

  整个put方法可以分为三个部分

  • 检查是否是第一次使用HashMap,如果是就初始化table
  • 如果传入的KV中的K是Null放到一个固定的地方
  • 找到K的Index并对可能出现的哈希冲突使用拉链发解决
  • 如果到了扩容条件就扩容
 public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
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))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
} modCount++;
addEntry(hash, key, value, i);
return null;
}

4.1 第一次扩容

  在Put方法的开始检查table是否为EMPTY_TABLE。EMPTY_TABLE是static修饰的数组,在HashMap类被加载的时候一同加载。当新建一个HashMap对象且没有使用的时候,table指向EMPTY_TABLE。所以检查table是否指向EMPTY_TABLE数组就是检查HashMap是否已经初始化过,如果没有就需要初始化。

    /**
* An empty table instance to share when the table is not inflated.
*/
static final Entry<?,?>[] EMPTY_TABLE = {}; /**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

  新建数组传入的参数是threshold,但最终table的大小并不是threshold。在inflaatTable中:首先计算出比threshold大的最小的2的幂次作为Capacity,其次根据Capacity和loadFactory计算出threshold替换原有的threshold。

这是一个很别扭的过程:threshold--->Capacity-->threshold。也就是说传入的threshold并不是最终的threshold。 

    private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}

4.2 如果key为Null

  HashMap支持放入key为null的节点,key为Null的节点会同一放到table[0]的位置,即在table[0]的位置形成一条链。

4.3 解决可能哈希冲突

  在key不等于null的情况下,首先计算出key的哈希值,然后根据key的哈希值计算出key在table里的位置i。接下来就是解决在位置i处可能存在的哈希冲突了,遍历table[i]处的链表,如果在该链表上找到一个节点的key和待插入的节点的key相同,用新的value替换掉旧的value并返回旧的value。如果没有找到就执行addEntry方法。

4.4 插!

  当走到addEntry方法的时候有两种可能:1、table[i]是一个空的链表,即for循环一次没有执行 2、table[i]的链表里没有找到和key重复的节点 ,总而言之需要在table[i]处插入一个节点。在插入节点之前首先要检查是否需要resize一次,然后才执行createEntry插入节点。

 void addEntry(int hash, K key, V value, int bucketIndex) {
    //检查是否需要扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
    //插入节点
createEntry(hash, key, value, bucketIndex);
}

  典型的头插法插入链表,插入链表之后size++

    void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}

5、resize

 void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
} createEntry(hash, key, value, bucketIndex);
}

  当两个条件同时达到时才可以扩容:

  • size>threshold
  • table[i]处有节点

  总结起来就是容量不太够的情况下又发生了哈希冲突,所以此时需要扩容,扩容的大小是原来旧数组大小的两倍。

 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);
}

  扩容是新建数组再把旧数组拷贝过去,关键是这个拷贝的过程需要重新计算哈希transfer,其实就是执行了一次插入过程,也是头插法。

 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;
}
}
}

4、get

  和put方法相比get方法平平无奇,当key不为Null的时候计算出key的索引,然后遍历该索引上的链表直至找到需要的值

    public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue();
}
    final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
} int hash = (key == null) ? 0 : hash(key);
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 != null && key.equals(k))))
return e;
}
return null;
}

HashMap 1.7的更多相关文章

  1. HashMap与TreeMap源码分析

    1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...

  2. HashMap的工作原理

    HashMap的工作原理   HashMap的工作原理是近年来常见的Java面试题.几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道HashTable和HashMap之间 ...

  3. 计算机程序的思维逻辑 (40) - 剖析HashMap

    前面两节介绍了ArrayList和LinkedList,它们的一个共同特点是,查找元素的效率都比较低,都需要逐个进行比较,本节介绍HashMap,它的查找效率则要高的多,HashMap是什么?怎么用? ...

  4. Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

    2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...

  5. 学习Redis你必须了解的数据结构——HashMap实现

    本文版权归博客园和作者吴双本人共同所有,转载和爬虫请注明原文链接博客园蜗牛 cnblogs.com\tdws . 首先提供一种获取hashCode的方法,是一种比较受欢迎的方式,该方法参照了一位园友的 ...

  6. HashMap与HashTable的区别

    HashMap和HashSet的区别是Java面试中最常被问到的问题.如果没有涉及到Collection框架以及多线程的面试,可以说是不完整.而Collection框架的问题不涉及到HashSet和H ...

  7. JDK1.8 HashMap 源码分析

    一.概述 以键值对的形式存储,是基于Map接口的实现,可以接收null的键值,不保证有序(比如插入顺序),存储着Entry(hash, key, value, next)对象. 二.示例 public ...

  8. HashMap 源码解析

    HashMap简介: HashMap在日常的开发中应用的非常之广泛,它是基于Hash表,实现了Map接口,以键值对(key-value)形式进行数据存储,HashMap在数据结构上使用的是数组+链表. ...

  9. java面试题——HashMap和Hashtable 的区别

    一.HashMap 和Hashtable 的区别 我们先看2个类的定义 public class Hashtable extends Dictionary implements Map, Clonea ...

  10. 再谈HashMap

    HashMap是一个高效通用的数据结构,它在每一个Java程序中都随处可见.先来介绍些基础知识.你可能也知 道,HashMap使用key的hashCode()和equals()方法来将值划分到不同的桶 ...

随机推荐

  1. SaaS技术栈的走势

    本地部署时代 在软件还是“本地部署(on-premise)”的时候,SaaS的版图被大型玩家把持着,几乎所有的垂直领域(营销.支持.销售.人力)都被微软.SAP等大公司的解决方案占据.那时候的用户并没 ...

  2. python爬虫入门urllib库的使用

    urllib库的使用,非常简单. import urllib2 response = urllib2.urlopen("http://www.baidu.com") print r ...

  3. 小tips:JS严格模式(use strict)下不能使用arguments.callee的替代方案

    在函数内部,有两个特殊的对象:arguments 和 this.其中, arguments 的主要用途是保存函数参数, 但这个对象还有一个名叫 callee 的属性,该属性是一个指针,指向拥有这个 a ...

  4. 2018-08-13 Head First OO分析设计一书略读与例子中文化

    注: 此笔记仅为个人学习此教程的布局和材料组织之用. 如有兴趣请自行详阅. 第一章是以吉他商店的存货系统作例子. 第二章设计有狗洞的门. 第三章对第二章基础上, 更改需求后对应设计. 第四章继续改进此 ...

  5. iOS中时间与时间戳的相互转化

    //获取当前系统时间的时间戳 #pragma mark - 获取当前时间的 时间戳 +(NSInteger)getNowTimestamp{ NSDateFormatter *formatter = ...

  6. Android内存优化(三)详解内存分析工具MAT

    前言 在这个系列的前四篇文章中,我分别介绍了DVM.ART.内存泄漏和内存检测工具的相关知识点,这一篇我们通过一个小例子,来学习如何使用内存分析工具MAT. 1.概述 在进行内存分析时,我们可以使用M ...

  7. SQL学习总结-思维导图

  8. 使用nginx代理后以及配置https后,如何获取真实的ip地址

    使用nginx代理后以及配置https后,如何获取真实的ip地址 Date:2018-8-27 14:15:51 使用nginx, apache等反向代理后,如果想获取请求的真实ip,要在nginx中 ...

  9. Linux中安装硬盘后对硬盘的分区以及挂载

    我将使用VM来进行模拟 先使用df看下我的电脑硬盘信息: df -h 可以看到只有一个sda1分区装载/boot,还有一个扩展分区 查看dev下的硬盘: 只有一个硬盘(两个分区) 注意: 如果你是ID ...

  10. 翻译:MySQL "Got an Error Reading Communication Packet" Errors

    前言: 本文是对Muhammad Irfan的这篇博客MySQL "Got an Error Reading Communication Packet" Errors的翻译,如有翻 ...