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. 使用mybatis开发dao方法

    使用mybatis开发dao的时候, 主要涉及到SqlSessionFactoryBuilder.SqlSessionFactory.SqlSession 这三个类 现在将这三个类的使用方法简单的说下 ...

  2. cf1136E. Nastya Hasn't Written a Legend(二分 线段树)

    题意 题目链接 Sol yy出了一个暴躁线段树的做法. 因为题目保证了 \(a_i + k_i <= a_{i+1}\) 那么我们每次修改时只需要考虑取max就行了. 显然从一个位置开始能影响到 ...

  3. 洛谷P2000 拯救世界(生成函数)

    题面 题目链接 Sol 生成函数入门题 至多为\(k\)就是\(\frac{1-x^{k+1}}{1-x}\) \(k\)的倍数就是\(\frac{1}{1-x^k}\) 化简完了就只剩下一个\(\f ...

  4. Android studio 下的SDK Manager只显示已安装包的情况

    原因是连接不上Google的更新服务器: 解决方法: 选择第三个Options: 修改Http Proxy Server: mirrors.neusoft.edu.cn Http Proxy Port ...

  5. 【爬虫】在使用xpath时,排除指定标签

    xpath排除某个节点 主要时应用name()这个函数获取便签名 res = html.xpath("//*[name(.)!='style']")

  6. Ubuntu-18.04安装Docker

    Docker 介绍 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化.容器是完全使用沙箱机制 ...

  7. 【redis专题(9)】事务

    Redis支持简单的事务,所谓简单是因为其不支持回滚(回滚是用队列模仿的),与mysql有以下区别 rollback与discard的区别: 如果已经成功执行了2条语句, 第3条语句出错 Rollba ...

  8. C# 反射给对象赋值遇到的问题——类型转换

    反射给对象赋值遇到的问题——类型转换 给一个对象属性赋值可以通过PropertyInfo.SetValue()方式进行赋值,但要注意值的类型要与属性保持一致.    创建对象实例的两种方法: 1. 1 ...

  9. EF Code First列名 'Discriminator' 无效的问题

    新建了一个类继承EF  Model类,运行报错 EF Code First列名 'Discriminator' 无效 EF会把项目中在DbContext中引用的所有的Model类及这些Model类对应 ...

  10. c#qq发邮件

    // SMTP(Simple Mail Transport Protocol)简单邮件传输协议.在.NET Frameword类库中提供SmtpClient类(System.Net.Mail),她提供 ...