前言

在Java中我们最常用的集合类毫无疑问就是Map,其中HashMap作为Map最重要的实现类在我们代码中出现的评率也是很高的。

我们对HashMap最常用的操作就是putget了,那么你知道它是怎么实现的吗?知道HashMap的底层原理吗?你知道从jdk7到jdk8,HashMap发生了什么变化吗?

那么我们就带着这些疑问,一起来探秘HashMap。

首先声明本文这次讲的HashMap基于Jdk1.7.0_79,不同版本略有差异。Jdk1.8版本的日后讨论。

1. HashMap的数据结构

我们先从HashMap的数据结构谈起,先了解它的数据结构存储结构,后面看它的代码实现就容易多了。

HashMap底层数据结构是由数组和链表来实现对数据的存储,但数组和链表基本上是两个极端。为什么这么说咱们继续往下看。

数组

数组的存储空间是连续的,占用内存严重,即使是空的也要分配内存,故空间复杂度很大。但数组的二分查找时间复杂度小,为O(1);

数组的特点是:寻址容易,插入和删除困难;

链表

链表的存储空间是可以离散的,占用内存比较宽松,可以动态增加链表长度,故空间复杂度很小,但由于它是不连续的,查找插入删除操作会很麻烦,故时间复杂度很大,为O(N);

链表的特点是:寻址困难,插入和删除容易。

哈希表

那么我们能不能综合两者的优点,设计出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要讲的哈希表。

哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。

哈希表有多种不同的实现方式,接下来我们探讨的是最常用的一种实现方式--拉链法,我们可以理解为“链表的数组” ,如图:

从上图我们可以发现哈希表是由数组+链表组成的,在一个长度为16的数组中,每个元素存储的是一个链表的头结点。

那么这些元素是按照什么样的规则存储到数组中呢?

一般情况是通过hash(key)%len获得,也就是计算出元素的key的哈希值,然后用哈希值对数组长度取模得到的即为该元素存储到数组中的index。

比如上述哈希表中,85%16=5,149%16=5,197%16=5。

所以hash值为85、149、197的k-v对都存储在数组下标为5的位置。

HashMap底层数据结构的原理就是使用的上述的哈希表,HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。

这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?

如上图所示,HashMap做了一些处理。

首先HashMap里面实现了一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean。

我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。

这数组里存的其实是每一个链表的head节点,然后根据Entry的next属性关联链表的每个节点。

  1. /**
  2. * The table, resized as necessary. Length MUST Always be a power of two.
  3. */
  4. transient Entry[] table;

2. HashMap的存取实现

既然是线性数组,为什么能随机存取?这里HashMap用了一个小算法,利用hash算法散列,大致是这样实现的:

  1. // 存储时:
  2. // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值
  3. int hash = key.hashCode();
  4. int index = hash % Entry[].length;
  5. Entry[index] = value;
  6. // 取值时:
  7. int hash = key.hashCode();
  8. int index = hash % Entry[].length;
  9. return Entry[index];

1)put

疑问:如果两个key通过hash%Entry[].length得到的index相同,会不会有覆盖的危险?

答案是不会,因为这里HashMap里面也用到了链式数据结构的一个概念。形如上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。

举个例子, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。

一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?

HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;

这样我们发现index=0的地方其实存了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。

也就是说数组中存储的是最后插入的元素,相同位置的其他元素用next属性关联。即,通过散列后计算得到相同index的元素会在一条链上,并通过next属性连接。

到这里为止,HashMap的大致实现,我们应该已经清楚了。具体代码如下:

  1. public V put(K key, V value) {
  2. if(table == EMPTY_TABLE){
  3. inflateTable(threshold);
  4. }
  5. if (key == null)
  6. return putForNullKey(value); //null总是放在数组的第一个链表中
  7. int hash = hash(key.hashCode());
  8. int i = indexFor(hash, table.length);
  9. //确定当前元素所在的链表,遍历链表
  10. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  11. Object k;
  12. //如果key在链表中已存在,则替换为新value
  13. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  14. V oldValue = e.value;
  15. e.value = value;
  16. e.recordAccess(this);
  17. return oldValue;
  18. }
  19. }
  20. modCount++;
  21. addEntry(hash, key, value, i);
  22. return null;
  23. }
  24. void addEntry(int hash, K key, V value, int bucketIndex) {
  25. //如果size超过threshold,则扩充table大小。hash(key)%new length再散列
  26. if((size >= threshold) && (null != table[bucketIndex])){
  27. resize(2*table.length);
  28. hash = (null != key) ? hash(key) : 0;
  29. bucketIndex = indexFor(hash,table.length);
  30. }
  31. createEntry(hash, key, value, bunketIndex);
  32. }
  33. void createEntry(int hash, K key, V value, int bunketIndex){
  34. Entry<K,V> e = table[bucketIndex];
  35. table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //参数e, 是Entry.next
  36. size++;
  37. }

当然HashMap里面也包含一些优化方面的实现,这里也说一下。

比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个index的链就会很长,肯定会影响性能。

所以HashMap里面设置了一个增长因子,随着map的size越来越大,Entry[]会以一定的规则加长长度。

这个就是上面代码注释里提到的如果size超过threshold,则扩充table大小。hash(key)%new length再散列,这个一会儿在下面详细说明。

2)get

  1. public V get(Object key) {
  2. if (key == null)
  3. return getForNullKey();
  4. Entry<K,V> entry = getEntry(key);
  5. return null == entry ? null : entry.getValue();
  6. }
  7. /**
  8. * Returns the entry associated with the specified key in the
  9. * HashMap. Returns null if the HashMap contains no mapping
  10. * for the key.
  11. */
  12. final Entry<K,V> getEntry(Object key) {
  13. if (size == 0) {
  14. return null;
  15. }
  16. int hash = (key == null) ? 0 : hash(key);
  17. for (Entry<K,V> e = table[indexFor(hash, table.length)];
  18. e != null;
  19. e = e.next) {
  20. Object k;
  21. if (e.hash == hash &&
  22. ((k = e.key) == key || (key != null && key.equals(k))))
  23. return e;
  24. }
  25. return null;
  26. }

3)null key的存取

null key总是存放在Entry[]数组的第一个元素。

  1. private V putForNullKey(V value) {
  2. for (Entry<K,V> e = table[0]; e != null; e = e.next) {
  3. if (e.key == null) {
  4. V oldValue = e.value;
  5. e.value = value;
  6. e.recordAccess(this);
  7. return oldValue;
  8. }
  9. }
  10. modCount++;
  11. addEntry(0, null, value, 0);
  12. return null;
  13. }
  14. private V getForNullKey() {
  15. if (size == 0) {
  16. return null;
  17. }
  18. for (Entry<K,V> e = table[0]; e != null; e = e.next) {
  19. if (e.key == null)
  20. return e.value;
  21. }
  22. return null;
  23. }

4)确定数组index:hashcode % table.length取模

HashMap存取时,都需要计算当前key应该对应Entry[]数组哪个元素,即计算数组下标;算法如下:

  1. /**
  2. * Returns index for hash code h.
  3. */
  4. static int indexFor(int h, int length) {
  5. return h & (length-1);
  6. }

按位取并,作用上相当于取模mod或者取余%。

这意味着数组下标相同,并不表示hashCode相同。

5)table初始大小

  1. /**
  2. * Constructs an empty <tt>HashMap</tt> with the specified initial
  3. * capacity and load factor.
  4. *
  5. * @param initialCapacity the initial capacity
  6. * @param loadFactor the load factor
  7. * @throws IllegalArgumentException if the initial capacity is negative
  8. * or the load factor is nonpositive
  9. */
  10. public HashMap(int initialCapacity, float loadFactor) {
  11. if (initialCapacity < 0)
  12. throw new IllegalArgumentException("Illegal initial capacity: " +
  13. initialCapacity);
  14. if (initialCapacity > MAXIMUM_CAPACITY)
  15. initialCapacity = MAXIMUM_CAPACITY;
  16. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  17. throw new IllegalArgumentException("Illegal load factor: " +
  18. loadFactor);
  19. this.loadFactor = loadFactor;
  20. threshold = initialCapacity;
  21. init();
  22. }
  1. /**
  2. * Inflates the table.
  3. */
  4. private void inflateTable(int toSize) {
  5. // Find a power of 2 >= toSize
  6. int capacity = roundUpToPowerOf2(toSize);
  7. threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
  8. table = new Entry[capacity];
  9. initHashSeedAsNeeded(capacity);
  10. }

Jdk1.7和之前的版本不同的是我们推迟初始化直到我们确实需要它。即并没有在new的时候就初始化数组,在put的时候判断数组为空时再调用inflateTable初始化HashMap。

另外需要注意的是table初始大小并不是构造函数中的initialCapacity!!

而是 >= initialCapacity的2的n次幂!!!——为什么这么设计呢?——

另外,调用无参构造时的初始化大小是16,增长因子是0.75

3. 解决hash冲突的办法

*开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)

*再哈希法

*链地址法

*建立一个公共溢出区

Java中HashMap的解决办法就是采用的链地址法。

4. 再散列rehash过程

从上面的代码addEntry方法可以看出,每次都会判断size是不是已经超过threshold,即当哈希表的容量超过默认容量时,必须调整table的大小,对数组进行扩容。

当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回。

扩容时,需要创建一张新表,将原表的映射到新表中。具体代码如下:

  1. void resize(int newCapacity) {
  2. Entry[] oldTable = table;
  3. int oldCapacity = oldTable.length;
  4. if (oldCapacity == MAXIMUM_CAPACITY) {
  5. threshold = Integer.MAX_VALUE;
  6. return;
  7. }
  8. Entry[] newTable = new Entry[newCapacity];
  9. transfer(newTable, initHashSeedAsNeeded(newCapacity));//把原来的数组放到新数组里
  10. table = newTable;
  11. threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//新的阈值
  12. }
  13. /**
  14. * Transfers all entries from current table to newTable.
  15. */
  16. void transfer(Entry[] newTable, boolean rehash) {
  17. int newCapacity = newTable.length;
  18. for (Entry<K,V> e : table) {
  19. while(null != e) {
  20. Entry<K,V> next = e.next;
  21. if (rehash) {
  22. e.hash = null == e.key ? 0 : hash(e.key);
  23. }
  24. //根据新的数组长度重新计算index
  25. int i = indexFor(e.hash, newCapacity);
  26. e.next = newTable[i];
  27. newTable[i] = e;
  28. e = next;
  29. }
  30. }
  31. }

由于篇幅原因,将在下一篇探秘jdk1.8的HashMap。

参考:

jdk1.7.0_79源码

https://zhuanlan.zhihu.com/p/44478231

HashMap的底层原理(jdk1.7.0_79)的更多相关文章

  1. 深度解析HashMap集合底层原理

    目录 前置知识 ==和equals的区别 为什么要重写equals和HashCode 时间复杂度 (不带符号右移) >>> ^异或运算 &(与运算) 位移操作:1<&l ...

  2. HashMap的底层原理

    简单说: 底层原理就是采用数组加链表: 两张图片很清晰地表明存储结构: 既然是线性数组,为什么能随机存取?这里HashMap用了一个小算法,大致是这样实现: // 存储时: int hash = ke ...

  3. 谈一下HashMap的底层原理是什么?

    底层原理:Map + 无序 + 键唯一 + 哈希表 (数组+Entry)+ 存取值 1.HashMap是Map接口的实现类.实现HashMap对数据的操作,允许有一个null键,多个null值. Co ...

  4. HashMap的底层原理 cr:csdn:zhangshixi

    1.    HashMap概述: HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变 ...

  5. 浅谈HashMap 的底层原理

    本文整理自漫画:什么是HashMap? -小灰的文章 .已获得作者授权. HashMap 是一个用于存储Key-Value 键值对的集合,每一个键值对也叫做Entry.这些个Entry 分散存储在一个 ...

  6. HashMap 的底层原理

    1. HashMap的数据结构 数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端. 数组 数组存储区间是连续的,占用内存严重,故空间复杂的很大.但数组的二分查找时间复杂度小,为O(1 ...

  7. Java中HashMap底层实现原理(JDK1.8)源码分析

    这几天学习了HashMap的底层实现,但是发现好几个版本的,代码不一,而且看了Android包的HashMap和JDK中的HashMap的也不是一样,原来他们没有指定JDK版本,很多文章都是旧版本JD ...

  8. HashMap底层实现原理(JDK1.8)源码分析

    ref:https://blog.csdn.net/tuke_tuke/article/details/51588156 http://www.cnblogs.com/xiaolovewei/p/79 ...

  9. HashMap底层原理及jdk1.8源码解读

    一.前言 写在前面:小编码字收集资料花了一天的时间整理出来,对你有帮助一键三连走一波哈,谢谢啦!! HashMap在我们日常开发中可谓经常遇到,HashMap 源码和底层原理在现在面试中是必问的.所以 ...

随机推荐

  1. JS字符串替换,将一个字符串中的特定字符串换成其他字符串

  2. PHP面试题2019年滴滴出行工程师面试题及答案解析

    一.单选题(共30题,每题5分) 1.下列关于PHP垃圾回收的说法,错误的是? A.开启/关闭垃圾回收机制可以通过修改php配置实现 B.可以在程序中使用gc_enable() 和 gc_disabl ...

  3. java核心技术第二篇之数据库SQL语法

    #查询products表记录SELECT * FROM products WHERE price > 2000;-- 单行注释/* 多行注释*/#创建数据库CREATE DATABASE hei ...

  4. python中字符串

    字符串:可用单引号 双引号 三引号 来表示 可用来定义国籍,姓名,家庭住址等选项:#.join 把可迭代的对象转化成字符串 (字符串,列表,元组,字典等),列表 元组 合并为字符串,字典合并的是key ...

  5. MySQL基础之数据管理【3】

    MySQL中的多表联查 --查询emp的id username age depName create table emp( id int unsigned auto_increment key, us ...

  6. Angular 学习笔记(四)

    作用域和控制器的交互情况: 1.控制器通过作用域对模板暴露一些方法供其调用 2.控制器中定义的一些方法可改变注册在作用域下的数据模型 3.控制器在某些场合可能需设置监听器来监听作用域中的数据模型:这些 ...

  7. stl源码学习(版本2.91)--list

    stl源码学习(版本2.91)--list 一,阅读list()构造函数的收获 1,默认构造函数的作用和被调用的时机 struct no{ no(int i){} //no(){ // std::co ...

  8. 无序数组中用 快速排序的分治思想 寻找第k大元素

    #include <stdio.h> int *ga; int galen; void print_a(){ ; i < galen; i++){ printf("%d & ...

  9. c# WF 第2节 窗体的添加与删除

    本节内容: 1: 窗体的添加 2: 窗体的删除 1: 窗体的添加 2: 窗体的删除 3:窗口的运行,发现只有一个form1 是因为

  10. c# 第21节 方法声明和调用

    本节内容: 1:为什么要有方法 2:方法的声明及使用 3:方法params 传入接收数组 4:值传递和引用传递 5:输出参数out 1:为什么要有方法 2:方法的声明及使用 声明实例: 3:方法par ...