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. Java先比较日期再比较时间

    package com.bihang.seaya; import lombok.Data; import java.text.ParseException; import java.text.Simp ...

  2. cas单点登录

    一般一家企业内部会有多个业务系统,多个系统相互协作,但每个系统都需要登陆密码,比如:权限系统,发布系统,配置系统,邮件系统等,用户使用每个系统都需要登陆操作,使用起来比较繁琐,工作效率低下.此时就需要 ...

  3. 微信小程序之封装http请求

    下面将封装http请求服务部分的服务以及引用部分 // 本服务用于封装请求 // 返回的是一个promisepromise var sendRrquest = function (url, metho ...

  4. 魔幻般冒泡背景的CSS3按钮动画

    这是一款非常有特点的CSS3按钮,按钮的背景不是北京图片,也不是单纯的颜色,而是一组魔幻般的冒泡背景动画.当我们将鼠标滑过按钮时,按钮的冒泡背景动画就可以展示出来.可以说这款CSS3按钮的设计风格相当 ...

  5. 51nod1238 最小公倍数之和 V3(莫比乌斯反演)

    题意 题目链接 Sol 不想打公式了,最后就是求一个 \(\sum_{i=1}^n ig(\frac{N}{i})\) \(g(i) = \sum_{i=1}^n \phi(i) i^2\) 拉个\( ...

  6. ionic开发中,输入法键盘弹出遮挡住div元素

    采用ionic 开发中,遇到键盘弹出遮挡元素的问题. 以登陆页面为例,输入用户名和密码时,键盘遮挡了登陆按钮. 最终采用自定义指令解决了问题: .directive('popupKeyBoardSho ...

  7. (后端)SpringBoot中Mybatis打印sql(转)

    原文地址:https://www.cnblogs.com/expiator/p/8664977.html 如果使用的是application.properties文件,加入如下配置: logging. ...

  8. ERP口碑后付关于如何设置后厨小票打印时间的问题解决方法

    1. 2.

  9. SQL 中常用存储过程xp_cmdshell运行cmd命令

    目的: 使用SQL语句,在D盘创建一个文件夹myfile 首先查询系统配置 SELECT * FROM sys.configurations WHERE name='xp_cmdshell' OR n ...

  10. github提交代码失败

    向github上面提交代码,提示代码里面有大文件,建议使用git-lfs. 1,安装git-lfs yum install git-lfs 2,配置需要追踪的打文件(由于我这里提交的是jar包) gi ...