HashMap的概述:

     基于哈希表的 Map 接口的实现。

     此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。

  (除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)

     此类不保证映射的顺序,特别是它不保证该顺序恒久不变。


         此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。

    迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。

     所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。


         HashMap 的实例有两个参数影响其性能:初始容量 和 加载因子。

    容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。

    加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。

    当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。


         通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。

    加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。

    在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。

    如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。


        如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。

    注意:此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。

       (结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)

       这一般通过对自然封装该映射的对象进行同步操作来完成。

         如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。

         最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:

Map m = Collections.synchronizedMap(new HashMap(…));

由所有此类的“collection 视图方法”所返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。

因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。

注意:迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。

      快速失败迭代器尽最大努力抛出 ConcurrentModificationException。

      因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

此类是 Java Collections Framework 的成员。


HashMap的桶(容量):

 // 默认的初始桶(容量)是16,每次扩容都是x2
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // 最大容量为(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子为0.75,
static final float DEFAULT_LOAD_FACTOR = 0.75f;

加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作(即扩容)。

下面说下加载因子,如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。

如果我们在构造方法中不指定,则系统默认加载因子为0.75,这是一个比较理想的值,一般情况下我们是无需修改的。

另外,无论我们指定的容量为多少,构造方法都会将实际容量设为不小于指定容量的2的次方的一个数,且最大值不能超过2的30次方。

HashMap的key和value可以为null:

get():

  // 获取key对应的value
public V get(Object key) {
if (key == null)
//如果key为null,调用getForNullKey()
return getForNullKey();
//key不为null,调用getEntry(key);
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
//当key为null时,获取value
private V getForNullKey() {
if (size == 0) {
return null;//链表为空,返回null
}
//链表不为空,将“key为null”的元素存储在table[0]位置,但不一定是该链表的第一个位置!
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
} //key不为null,获取value
final Entry<K,V> getEntry(Object key) {
if (size == 0) {//判断链表中是否有值
//链表中没值,也就是没有value
return null;
}
//链表中有值,获取key的hash值
int hash = (key == null) ? 0 : hash(key);
// 在“该hash值对应的链表”上查找“键值等于key”的元素
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//判断key是否相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;//key相等,返回相应的value
}
return null;//链表中没有相应的key
}

首先,如果key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找。

记住,key为null的键值对永远都放在以table[0]为头结点的链表中,当然不一定是存放在头结点table[0]中。

如果key不为null,则先求的key的hash值,根据hash值找到在table中的索引,在该索引对应的单链表中查找是否有键值对的key与目标key相等,有就返回对应的value,没有则返回null。

put():

  // 将“key-value”添加到HashMap中
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)// 若“key为null”,则将该键值对添加到table[0]中。
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key);//获取key的hash值
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))) {
// 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“key”对应的键值对不存在,则将“key-value”添加到table中
modCount++;
//将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}

如果key为null,则将其添加到table[0]对应的链表中,putForNullKey的源码如下

// putForNullKey()的作用是将“key为null”键值对添加到table[0]位置
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果没有存在key为null的键值对,则直接添加到table[0]处!
modCount++;
addEntry(0, null, value, 0);
return null;
}

如果key不为null,则同样先求出key的hash值,根据hash值得出在table中的索引,而后遍历对应的单链表,如果单链表中存在与目标key相等的键值对,

则将新的value覆盖旧的value,比将旧的value返回,如果找不到与目标key相等的键值对,或者该单链表为空,调用addEntry()方法将该键值对插入到改单链表的头结点位置(每次新插入的节点都是放在头结点的位置).

如果key为null,调用putForNullKey(),直接去遍历table[0]Entry链表,寻找e.key==null的Entry或者没有找到遍历结束。

如果找到了e.key==null,就保存null值对应的原值oldValue,然后覆盖原值,并返回oldValue

如果在putForNullKey()中,在table[0]Entry链表中没有找到也会调用addEntry方法添加一个key为null的Entry。

下面是addEntry的源码:

void addEntry(int hash, K key, V value, int bucketIndex) {
//先判断大小
if ((size >= threshold) && (null != table[bucketIndex])) {
// 若HashMap的实际大小不小于 “阈值”,则调整HashMap的大小
resize(2 * table.length);//扩容,每次增长2倍
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);//新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
}
void createEntry(int hash, K key, V value, int bucketIndex) {
// 保存“bucketIndex”位置的值到“e”中
Entry<K,V> e = table[bucketIndex];
// 设置“bucketIndex”位置的元素为“新Entry”,
// 设置“e”为“新Entry的下一个节点”
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;

注意这里new Entry<>()的构造方法,将key-value键值对赋给table[bucketIndex],并将其next指向元素e,这便将key-value放到了头结点中,并将之前的头结点接在了它的后面。

该方法也说明,每次put键值对的时候,总是将新的该键值对放在table[bucketIndex]处(即头结点处)。

同时也要注意,这个方法首先会判断是否要扩容,当现在的HashMap中的Entry数大于等于扩容临界值(capacity*load factor)并且index对应的地方没有Entry就扩容.HashMap每次扩容的大小为2倍原容量,

默认容量为16,hashmap的capacity会一直是2的整数幂。

// 重新调整HashMap的大小,newCapacity是调整后的单位
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
//旧容量不小于最大容量(一般不会发生,反正我没遇到过)
threshold = Integer.MAX_VALUE;
return;
}
//一般扩容
// 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,
// 然后,将“新HashMap”赋值给“旧HashMap”。
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

很明显,是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。

transfer方法的源码如下:

// 将HashMap中的全部元素都添加到newTable中
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;
}
}
}

遍历原表table ,从table[0]开始,e=table[0],不为null就创建一个临时Entry next引用e.的下一个Entry,然后把e放到新表中,头插到table[i]中,i由indexFor方法决定i(h&newCapacity),然后让e=next,继续遍历拷贝。

扩容之后继续插入要插入的Entry,这个时候就要重新hash了,因为旧表已经扩容了,若果key为nul任然是0。

然后进行真正的插入,调用 createEntry(hash, key, value, bucketIndex),

下面是源码:

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++;
}
//进行头插,创建一个新的entry,
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}

新的entry复制到table[bucketIndex],并next引用原来的table[bucketIndex],完成。
……..

仅仅只是HashMap的普通扩容,就这么麻烦,如果再加上线程安全,而加同步的话,那么效率可想而知.并且,HashMap在高并发场景下调用transfer方法,可能会出现环形链表,导致程序死循环。

coding++:jdk1.7 HashMap 的get()和put() 源码的更多相关文章

  1. 【转】HashMap,ArrayMap,SparseArray源码分析及性能对比

    HashMap,ArrayMap,SparseArray源码分析及性能对比 jjlanbupt 关注 2016.06.03 20:19* 字数 2165 阅读 7967评论 13喜欢 43 Array ...

  2. JDK1.7的HashMap的put(key, value)源码剖析

    目录 HashMap的put操作源码解析 1.官方文档 2.put(key, value) 3.完结 HashMap的put操作源码解析 1.官方文档 1.1.继承结构 java.lang.Objec ...

  3. [源码解析]HashMap和HashTable的区别(源码分析解读)

    前言: 又是一个大好的周末, 可惜今天起来有点晚, 扒开HashMap和HashTable, 看看他们到底有什么区别吧. 先来一段比较拗口的定义: Hashtable 的实例有两个参数影响其性能:初始 ...

  4. 给jdk写注释系列之jdk1.6容器(11)-Queue之ArrayDeque源码解析

    前面讲了Stack是一种先进后出的数据结构:栈,那么对应的Queue是一种先进先出(First In First Out)的数据结构:队列.      对比一下Stack,Queue是一种先进先出的容 ...

  5. 给jdk写注释系列之jdk1.6容器(8)-TreeSet&NavigableMap&NavigableSet源码解析

    TreeSet是一个有序的Set集合. 既然是有序,那么它是靠什么来维持顺序的呢,回忆一下TreeMap中是怎么比较两个key大小的,是通过一个比较器Comparator对不对,不过遗憾的是,今天仍然 ...

  6. HashMap就是这么简单【源码剖析】

    前言 声明,本文用得是jdk1.8 前面已经讲了Collection的总览和剖析List集合以及散列表.Map集合.红黑树的基础了: Collection总览 List集合就这么简单[源码剖析] Ma ...

  7. HashMap 与 ConcrrentHashMap 使用以及源码原理分析

    前奏一:HashMap面试中常见问题汇总 HashMap的工作原理是近年来常见的Java面试题,几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道HashTable和Has ...

  8. HashMap put、get方法源码分析

    HashMap.java的实现是面试必问的问题. JDK版本 java version "1.8.0_91" Java(TM) SE Runtime Environment (bu ...

  9. HashMap和Hashtable的区别 源码分析

    一:以前只知道HashMap和HashTable区别,死记硬背的记住HashMap 允许key value为空 而Hashtable 不允许为空 HashMap线程是非线程安全的,而Hashtable ...

随机推荐

  1. 由国产性能测试工具WEB压力测试仿真能力对比让我想到的

    软件的行业在中国已得到长足的发展,软件的性能测试在软件研发过程显得越来越重要.国产的性能工具在好多大公司都在提供云服务的有偿收费测试.如:阿里的PTS(Performance Testing Serv ...

  2. 4L-线性表之数组

    关注公众号 MageByte,设置星标点「在看」是我们创造好文的动力.后台回复 "加群" 进入技术交流群获更多技术成长. 数组对于每一门编程语言来说都是重要的数据结构之一,当然不同 ...

  3. Python3之turtle的基本用法#Python学习01#

    一.turtle基本语法 1.导入turtle 模块import turtle 2.显示箭头turtle.showturtle() 3.写字符串turtle.write("因小米" ...

  4. 使用Keras进行深度学习:(五)RNN和双向RNN讲解及实践

    欢迎大家关注我们的网站和系列教程:http://www.tensorflownews.com/,学习更多的机器学习.深度学习的知识! 笔者:Ray 介绍 通过对前面文章的学习,对深度神经网络(DNN) ...

  5. TensorFlow官方发布剪枝优化工具:参数减少80%,精度几乎不变

    去年TensorFlow官方推出了模型优化工具,最多能将模型尺寸减小4倍,运行速度提高3倍. 最近现又有一款新工具加入模型优化"豪华套餐",这就是基于Keras的剪枝优化工具. 训 ...

  6. Redis数据结构——quicklist

    之前的文章我们曾总结到了Redis数据结构--链表和Redis数据结构--压缩列表这两种数据结构,他们是Redis List(列表)对象的底层实现方式.但是考虑到链表的附加空间相对太高,prev 和 ...

  7. SpringBoot 集成ehcache

    1, 项目实在springboot 集成mybatis 的基础上的: https://www.cnblogs.com/pickKnow/p/11189729.html 2,pom 如下,有的不需要加, ...

  8. Array.forEach原理,仿造一个类似功能

    Array.forEach原理,仿造一个类似功能 array.forEach // 设一个arr数组 let arr = [12,45,78,165,68,124]; let sum = 0; // ...

  9. 解决Pycharm导入当前项目的.py文件错误

    如图所示错误,由左边导航栏可见.py文件存在: 解决办法:右键单击导包错误文件所在目录,选择[Mark Directory as]+[Sources Root] 错误已解决:

  10. 关于 JavaScript 的 精度丢失 与 近似舍入

    一.背景 最近做 dashborad 图表时,涉及计算小数且四舍五入精确到 N 位.后发现 js 算出来的结果跟我预想的不一样,看来这里面并不简单-- 二.JS 与 精度 1.精度处理 首先明确两点: ...