今天要分享的Java集合是Map,主要是针对它的常见实现类HashMap进行讲解(jdk1.8)

什么是Map核心方法源码剖析1.文档注释2.成员变量3.构造方法4.put()5.get()

什么是Map

  Map是非线性数据结构的主要实现,用来存放一组键-值型数据,我们称之为散列表。在其他语言中,也被称为字典。
  HashMap是一个非常重要的数据结构,在我们的项目中有大量的运用,或充当参数,或用于缓存。而在面试中,HashMap也几乎成为了一个“必考项”,所以今天我们就从源码的角度,对这种数据结构进行剖析。
  首先我们先就使用上,给出几个先入为主的概念。有了概念,再去看源码就会容易很多。
  HashMap的查询速度是O(1),它为什么会这么快呢?因为散列表利用的是数组支持按下标随机访问的特性,所以散列表其实是对数组的一种扩充,是由数组演化而来的。
  我们来举一个例子,A班一共有64个学生,学号是唯一的9位数。在一场期末考试中,如果我们想知道一个学生的成绩,那么就可以通过学号来定位学生,然后获取所有的考试信息。为了便于存储和查询,我们将学生的学号,通过编码映射到下标从1-63的数组中。


  将例子和HashMap中的概念对应起来:学号就是键(key),也叫做关键字,学生的信息就是,将学号转换成数组下标的映射方法就叫做散列函数(散列函数是散列表的核心),散列函数计算得到的值就叫作散列值,也叫做Hash值哈希值
  如果这个班扩充到了100个人,存储成绩的方法不变,那么一定会有至少2个同学的成绩经过散列函数计算出的值相同。这种现象叫做散列冲突。为了解决散列冲突,不出现数据相互覆盖的情况,散列表会将这两个学生的信息组成一个链表,存储在数组中,从而保存多个同学的考试信息,这种解决散列冲突方法叫做链表法。(解决散列冲突的方法不止这一种,还有其他的方法,比如线性探测法)。
  对这些只要有一个大致的印象就可以,接下来我们会通过剖析源码的方式,对散列表的工作原理进行深入的分析。

核心方法源码剖析

  这一部分,选取了HashMap的一些核心内容进行讲解。分别是:文档注释,成员变量,构造方法,put()、hash()、get()、remove()。

1.文档注释

  permits null values and the null key
  允许存储null值和null

  The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.
  HashMap近似于Hashtable,除了它不同步并且允许null值

  This class makes no guarantees as to the order of the map
  这个类存储数据是无序

  An instance of HashMap has two parameters that affect its performance: initial capacity and load factor
  一个散列表有两个影响其性能的参数:初始值负载因子

  so that the hash table has approximately twice the number of buckets
  每次扩容2倍

2.成员变量

 1// 默认容量
2static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
3
4// 最大容量
5static final int MAXIMUM_CAPACITY = 1 << 30;
6
7// 负载因子:已用空间 / 总空间,负载因子平衡了时间和空间成本,如果负载因子过高,导致空间成本减少,查找成本增加,反之亦然。
8static final float DEFAULT_LOAD_FACTOR = 0.75f;
9
10// 一个哈系桶存储的元素超过8,就会将链表转换成红黑二叉树
11static final int TREEIFY_THRESHOLD = 8;
12
13// 一个哈希桶存储的元素小于6,并且是红黑二叉树存储,那么此时就会将红黑二叉树重新转换成链表
14static final int UNTREEIFY_THRESHOLD = 6;
15
16// 数据量阈值,数据量只有大于这一个值,才会发生树化
17static final int MIN_TREEIFY_CAPACITY = 64;

3.构造方法

HashMap的构造方法有4种,我们一般不会修改它的负载因子,常用的构造方法只有无参构造和传入初始值的构造方法。

1HashMap()
2HashMap(int initialCapacity)
3HashMap(int initialCapacity, float loadFactor)
4HashMap(Map<? extends K,? extends V> m)

4.put()

  put中有一个核心的方法,hash(),即散列方法

1public V put(K key, V value) {
2   return putVal(hash(key), key, value, false, true);
3}
4
5static final int hash(Object key) {
6    int h;
7    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
8}

  从hash方法中可以看出,如果传入的key是null,就会固定的返回0位置。如果传入的key不是null,那么就会取key的hashCode方法的返回值,记做h,返回h与h高16位的异或值。
  hash()方法的这段代码,又被称为扰动函数,我们可以思考一下,h的值是一个int,它的范围是[-2147483648, 2147483647],大概包含了40亿的映射空间。如果直接存到内存中,肯定是存不下的,而且HashMap的初始值只有16,那么我们就需要将h映射到这16个哈希桶中,可以直接取模,但是这样的效率不是很高,所以这里jdk使用了位与的方法,代码抽象如下:

1private int getIndex(int length, int h){
2    return h & (length - 1);
3}

  这里可以解释一下,为什么HashMap要求初始值是2的整次幂?,这样length - 1正好相当于一个低位掩码,只截取了低位的值.


  但是这里有一个问题,即使我们的散列函数设计的再松散,那么当屏蔽掉高位,只看低位的时候,还是会容易发生散列冲突。此时扰动函数的价值就体现出来了,它将自身的高半区(32bit)和低半区(32bit)做异或,这样既增加了随机性,又将高位的信息变相的参杂了进来。
  这里的getIndex函数除了位运算性能高,还有一个好处,这里扩容前后,h的值是不变的,只跟key有关。那么length - 1的值,只会增加一个高位1,所以经过getIndex计算的值,有一定几率保持不变(增加的高位,对应h的位是0),扩容时,就会少进行一些数据的搬运,即扩容时数据是黏性的。
  讲完了hash(),put()方法的核心思想就差不多讲完了,接下来我们将屏蔽一些实现细节,来讲一下剩下的putVal()。

 1final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
2    Node<K,V>[] tab;
3    Node<K,V> p;
4     int n, i;
5    // 当散列表为null的时候,调用resize()进行初始化
6    if ((tab = table) == null || (n = tab.length) == 0)
7        n = (tab = resize()).length;
8    // 如果没有发生哈希碰撞,直接将元素存进哈希桶
9    if ((p = tab[i = (n - 1) & hash]) == null)
10        tab[i] = newNode(hash, key, value, null);
11    else {
12        // 如果发生了哈希碰撞
13        Node<K,V> e; K k;
14        if (p.hash == hash &&
15            ((k = p.key) == key || (key != null && key.equals(k))))
16            // 记录要插入的元素
17            e = p;
18        else if (p instanceof TreeNode)
19            // 如果是树结构,就调用树的插入方法
20            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
21        else {
22            // 如果是链表结构,就调用链表的插入方法
23            for (int binCount = 0; ; ++binCount) {
24                if ((e = p.next) == null) {
25                    p.next = newNode(hash, key, value, null);
26                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
27                        treeifyBin(tab, hash);
28                    break;
29                }
30                if (e.hash == hash &&
31                    ((k = e.key) == key || (key != null && key.equals(k))))
32                    break;
33                p = e;
34            }
35        }
36        if (e != null) { // 覆盖元素
37            V oldValue = e.value;
38            if (!onlyIfAbsent || oldValue == null)
39                e.value = value;
40            afterNodeAccess(e);
41            return oldValue;
42        }
43    }
44    ++modCount;
45    if (++size > threshold)
46        resize();
47    afterNodeInsertion(evict);
48    return null;
49}    

  这里的扩容方法是resize(),每次扩容2倍,采用的也是数据搬运的方式,所以我们要尽可能的去避免HashMap的扩容。

5.get()

 1public V get(Object key) {
2    Node<K,V> e;
3    return (e = getNode(hash(key), key)) == null ? null : e.value;
4}
5
6final Node<K,V> getNode(int hash, Object key) {
7    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
8    if ((tab = table) != null && (n = tab.length) > 0 &&
9        (first = tab[(n - 1) & hash]) != null) {
10        // 如果在桶的首位就可以找到,那么就直接返回(提升效率,哈希冲突并不那么容易出现)
11        if (first.hash == hash && // always check first node
12            ((k = first.key) == key || (key != null && key.equals(k))))
13            return first;
14        if ((e = first.next) != null) {
15            // 根据节点类型,在红黑二叉树或者链表中查询数据
16            if (first instanceof TreeNode)
17                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
18            do {
19                if (e.hash == hash &&
20                    ((k = e.key) == key || (key != null && key.equals(k))))
21                    return e;
22            } while ((e = e.next) != null);
23        }
24    }
25    return null;
26}

  get()的思想和put()类似,根据不同的Node类型,进行查找

  最后,期待您的订阅和点赞,专栏每周都会更新,希望可以和您一起进步,同时也期待您的批评与指正!

一篇文章带您读懂Map集合(源码分析)的更多相关文章

  1. 一篇文章带您读懂List集合(源码分析)

    今天要分享的Java集合是List,主要是针对它的常见实现类ArrayList进行讲解 内容目录 什么是List核心方法源码剖析1.文档注释2.构造方法3.add()3.remove()如何提升Arr ...

  2. 一篇文章让你读懂Pivotal的GemFire家族产品

    一篇文章让你读懂Pivotal的GemFire家族产品 学习了:https://www.sohu.com/a/217157517_747818

  3. java集合源码分析(六):HashMap

    概述 HashMap 是 Map 接口下一个线程不安全的,基于哈希表的实现类.由于他解决哈希冲突的方式是分离链表法,也就是拉链法,因此他的数据结构是数组+链表,在 JDK8 以后,当哈希冲突严重时,H ...

  4. Java 集合源码分析(一)HashMap

    目录 Java 集合源码分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5. JDK 1.7 的 Con ...

  5. java集合源码分析(三):ArrayList

    概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ...

  6. 从Generator入手读懂co模块源码

    这篇文章是讲JS异步原理和实现方式的第四篇文章,前面三篇是: setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop 从发布订阅模式入手读懂Node.js的E ...

  7. Java集合源码分析(二)ArrayList

    ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...

  8. Java集合源码分析(六)TreeSet<E>

    TreeSet简介 TreeSet 是一个有序的集合,它的作用是提供有序的Set集合.它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, j ...

  9. Java集合源码分析(一)ArrayList

    前言 在前面的学习集合中只是介绍了集合的相关用法,我们想要更深入的去了解集合那就要通过我们去分析它的源码来了解它.希望对集合有一个更进一步的理解! 既然是看源码那我们要怎么看一个类的源码呢?这里我推荐 ...

随机推荐

  1. 【Mongodb】mongoDB与mongoose---Scheme和Collections对应问题

    mongodb是一个基于分布式文件存储的文档型数据库 MongoDB 是一个介于关系数据库和非关系数据库之间的产品 MongoDB 最大的特点是他支持的查询语言非常强大,而且还支持对数据建立索引 官方 ...

  2. Windows和Linux下实现ssh免密登录

    ------------恢复内容开始------------ SSH是一种通讯协议,可以实现远程安全登录.可以通过如putty.MobaXterm等工具通过ssh安全登录到虚拟机进行操作. Opens ...

  3. iTOP-4418开发板_重实力_优势突出_有原理图源码开源

    核心板参数 尺寸:50mm*60mm 高度:核心板连接器组合高度1.5mm PCB层数:6层PCB沉金设计 4418 CPU:ARM Cortex-A9 四核 S5P4418处理器 1.4GHz 68 ...

  4. 论文翻译——Character-level Convolutional Networks for Text Classification

    论文地址 Abstract Open-text semantic parsers are designed to interpret any statement in natural language ...

  5. K3CLOUD日志目录

    业务站点安装目录\K3Cloud\WebSite\App_Data\Log下面找

  6. ionic3 修改页面切换动画

    在app.module.ts中 配置pageTransition属性 [ BrowserModule, IonicModule.forRoot(MyApp, { pageTransition: 'io ...

  7. Python - 使用 xlwt 写入表格

    # -*- coding: utf-8 -*- import xlwt def write_excel(): f = xlwt.Workbook() fenlei = ['一类','二类','三类', ...

  8. 提高线程数,解决redis超时问题

    根据压测结果做出的修改历史: 第一步:只针对maxWorkerThreads.maxIoThreads和minWorkerThreads做了修改<processModel autoConfig= ...

  9. 感觉自己out了

    看了公司混乱而落后的框架,想自己开发一个. 无意中到开源网站看到,开源的控件已经非常多了,基本上说应有尽有. 感叹这个知识大爆炸的年代. 自己现在是坐在井底的蛤蟆?

  10. python取出前端传入execl文件中的数据

    from openpyxl import load_workbook #获取前台传入的文件 uploadedFile = request.FILES.get('file') #获取execl文件 wb ...