Java:HashMap原理与设计缘由

前言

Java中使用最多的数据结构基本就是ArrayList和HashMap,HashMap的原理也常常出现在各种面试题中,本文就HashMap的设计与设计缘由作出一一讲解,并点明面试常见的一些问题。

一 HashMap数据结构

HashMap是一张哈希表(即数组),表中的每个元素都是键值对(Map.Entry类)。并且每个元素都是一个链表(红黑树)的节点。并且HashMap的数组长度一定是2的次幂

1.1 为何数组长度一定是2的次幂

正常情况下,新增节点时,会对节点进行取模运算,确定节点在哈希表中的位置。但是当哈希表(数组)长度为2的次幂时,取模运算可以修改为位与运算

源码如下:

static final int hash(Object key) {
if (key == null){
return 0;
}
int h;
h = key.hashCode();返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
//其中n是数组的长度,即Map的数组部分初始化长度
return (n-1)&(h ^ (h >>> 16));
}

具体原理可以参考专门讲解该算法的文章:

由HashMap哈希算法引出的求余%和与运算&转换问题

二 HashMap的键值存储

我们给 put() 方法传递键和值时,我们先对键调用 hashCode() 方法,计算并返回 hashCode,然后使用HashMap内部的hash算法,将hashCode计算为表中的具体位置,找到 Map 数组的 bucket 位置来储存 Node 对象。

三 解决Hash碰撞

使用拉链法

如果hash到的数组位置已存在对象,即为Hash碰撞。JDK使用拉链法解决Hash碰撞问题。

即以原有的Node节点为基础,构造链表。将新的Node节点设为链表表头。

3.1 JDK7中新节点为表头

如果已原有节点为表头,则需要遍历链表,徒增不必要的性能消耗

3.2 JDK8中新节点为表尾

因为JDK8中链表在长度大于等于8时会转变为红黑树,所以每次在链表中添加节点,都必须遍历链表计算一次链表长度,所以新节点直接在遍历完链表后添加到表尾。

3.3 链表过长导致的复杂度问题

HashMap的查询操作最佳时间复杂度是O(1),但是当表中的某个链表过长时,查询该链表上的元素时间复杂度为O(n)JDK1.8中解决了该问题,当HashMap中某链表长度大于8时,链表会重构为红黑树,这样,HashMap的最坏时间复杂度为O(n)。同理,为了不必要的消耗,当链表长度小于6时,红黑树会重新变回链表

3.4 还有什么方法解决Hash碰撞

开放寻址法,再哈希法

感兴趣可以参看此文:

Hash碰撞和解决策略

四 HashMap的扩容

4.1 扩容时机

当size超过阈值(数组长度*负载因子)时,即开始扩容,HashMap的负载因子为0.75。

4.1.1 为何要数组未满就扩容

避免频繁出现Hash碰撞,造成拉链过长(红黑树过长)。这样会导致查询复杂度频繁出现最坏情况

4.2 扩容过程

创建原本数组容量*2的新数组,将节点从原本的数组中迁移过去。

4.2.1 为何扩容的倍数是2倍

原因一上文已说明,方便进行哈希运算。

原因二是不需要重新计算Hash值(JDK1.8优化)。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,经过rehash之后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。对应的就是下方的resize的注释。

/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() { }

看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希值(也就是根据key1算出来的hashcode值)与高位与运算的结果。



元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:



因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。

五 重写equals方法需同时重写hashCode方法

这个是老生常谈的问题了,如果顺利理解了HashMap的底层结构那么这个问题就很好理解了。equals相同的key理论上必定有相同hashCode,所以必须也重写hashCode方法。可以思考下如果没重写,在put,get过程中会导致什么问题。

Java:HashMap原理与设计缘由的更多相关文章

  1. Java HashMap原理

    HashMap存储结构 HashMap中数据的存储是由数组与链表一起实现的 数组寻址非常容易,其时间复杂度为O(1),但是当要插入或删除数据时,时间复杂度就会变为O(n).链表插入和删除操作的内存复杂 ...

  2. java - HashMap原理及实现 (转)

    众所周知,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry.这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干. HashMap ...

  3. Java HashMap工作原理及实现

    Java HashMap工作原理及实现 2016/03/20 | 分类: 基础技术 | 0 条评论 | 标签: HASHMAP 分享到:3 原文出处: Yikun 1. 概述 从本文你可以学习到: 什 ...

  4. Java 7 和 Java 8 中的 HashMap原理解析

    HashMap 可能是面试的时候必问的题目了,面试官为什么都偏爱拿这个问应聘者?因为 HashMap 它的设计结构和原理比较有意思,它既可以考初学者对 Java 集合的了解又可以深度的发现应聘者的数据 ...

  5. Java基础-hashMap原理剖析

    Java基础-hashMap原理剖析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.   一.什么是哈希(Hash) 答:Hash就是散列,即把对象打散.举个例子,有100000条数 ...

  6. Java HashMap实现原理分析

    参考链接:https://www.cnblogs.com/xiarongjin/p/8310011.html 1. HashMap的数据结构 数据结构中有数组和链表来实现对数据的存储,但这两者基本上是 ...

  7. java中HashMap原理?

    参考:https://www.cnblogs.com/yuanblog/p/4441017.html(推荐) https://blog.csdn.net/a745233700/article/deta ...

  8. [翻译]Java HashMap工作原理

    大部分Java开发者都在使用Map,特别是HashMap.HashMap是一种简单但强大的方式去存储和获取数据.但有多少开发者知道HashMap内部如何工作呢?几天前,我阅读了java.util.Ha ...

  9. atitit.木马病毒webshell的原理and设计 java c# .net php.

    atitit.木马病毒webshell的原理and设计 java c# .net php. 1. 隐蔽性 编辑 WebShell后门具有隐蔽性,一般有隐藏在正常文件中并修改文件时间达到隐蔽的,还有利用 ...

随机推荐

  1. 利用tcpdump分析工具来验证tcp连接的建立和关闭过程

    本文要求读者在阅读之前应该对TCP通过三次握手建立和关闭连接有一定的了解,本文并没有详细讲解三次握手,只是通过一个实例对三次握手进行了一下验证. tcp连接的建立和关闭想必大家都已经非常熟悉了!通过三 ...

  2. Python多线程 简明例子

    综述   多线程是程序设计中的一个重要方面,尤其是在服务器Deamon程序方面.无论何种系统,线程调度的开销都比传统的进程要快得多. Python可以方便地支持多线程.可以快速创建线程.互斥锁.信号量 ...

  3. memmove、memccpy和memcpy

    这三个函数都是内存拷贝,目的都是将N个字节的源内存地址的内容拷贝到目标内存地址中.        void *memmove(void *dest,void*src,int count)      v ...

  4. 入骨三分的“我们是谁”IT行业版

    这组漫画用简单的台词.粗犷的线条,把很多人心中对甲方(客户)的不满彻底地发泄了一通,并且出现了很多变种版本,引发了传播热潮. http://news.sina.com.cn/c/nd/2017-08- ...

  5. C++学习笔记27,虚函数作品

    C++它指定虚函数的行为,但实现的作者编译器. 通常,编译器处理虚函数的方法是给每个对象加入一个隐藏成员.隐藏成员中保存了一个指向函数地址数组的指针. 这个数组称为虚函数表(virtual funct ...

  6. linux C 内存管理方式之半动态

    看到半动态申请内存,第一反应这是什么鬼? 实际上半动态内存申请很容易理解,在GNU C中使用alloca函数来实现 #include <stdlib.h> void *alloca (si ...

  7. AY写给国人的教程- VS2017 Live Unit Testing[2/2]-C#人爱学不学-aaronyang技术分享

    原文:AY写给国人的教程- VS2017 Live Unit Testing[2/2]-C#人爱学不学-aaronyang技术分享 谢谢大家观看-AY的 VS2017推广系列 Live Unit Te ...

  8. 自绘LISTVIEW的滚动条(Delphi实现)

    因项目需要准备对LISTVIEW的滚动条进行自绘.于是在网上搜了一下,问题没解决,却搜出一篇令人不愉快的帖子 .确实,那时候实力是不够的,但现在应该是没问题了,为这个目的才不断磨练自己的. LISTV ...

  9. Windows程序设计画图实现哆啦A梦

    在看雪论坛上看到的一个帖子,很喜欢,转载一下.原文地址:http://bbs.pediy.com/showthread.php?t=138630哆啦A梦是画出来的,不知道作者算这些坐标位置算了多久,真 ...

  10. Qt打开外部程序和文件夹需要注意的细节(注意QProcess的空格问题,以及打开本地文件时,需要QUrl::fromLocalFile才可以)

    下午写程序中遇到几个小细节,需要在这里记录一下. ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 QProcess *process = new QProcess(this ...