一篇文章带您读懂Map集合(源码分析)
今天要分享的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集合(源码分析)的更多相关文章
- 一篇文章带您读懂List集合(源码分析)
今天要分享的Java集合是List,主要是针对它的常见实现类ArrayList进行讲解 内容目录 什么是List核心方法源码剖析1.文档注释2.构造方法3.add()3.remove()如何提升Arr ...
- 一篇文章让你读懂Pivotal的GemFire家族产品
一篇文章让你读懂Pivotal的GemFire家族产品 学习了:https://www.sohu.com/a/217157517_747818
- java集合源码分析(六):HashMap
概述 HashMap 是 Map 接口下一个线程不安全的,基于哈希表的实现类.由于他解决哈希冲突的方式是分离链表法,也就是拉链法,因此他的数据结构是数组+链表,在 JDK8 以后,当哈希冲突严重时,H ...
- Java 集合源码分析(一)HashMap
目录 Java 集合源码分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5. JDK 1.7 的 Con ...
- java集合源码分析(三):ArrayList
概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ...
- 从Generator入手读懂co模块源码
这篇文章是讲JS异步原理和实现方式的第四篇文章,前面三篇是: setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop 从发布订阅模式入手读懂Node.js的E ...
- Java集合源码分析(二)ArrayList
ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...
- Java集合源码分析(六)TreeSet<E>
TreeSet简介 TreeSet 是一个有序的集合,它的作用是提供有序的Set集合.它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, j ...
- Java集合源码分析(一)ArrayList
前言 在前面的学习集合中只是介绍了集合的相关用法,我们想要更深入的去了解集合那就要通过我们去分析它的源码来了解它.希望对集合有一个更进一步的理解! 既然是看源码那我们要怎么看一个类的源码呢?这里我推荐 ...
随机推荐
- TPO4-1 Deer Populations of the Puget Sound
The causes of this population rebound are consequences of other human actions. First, the major pred ...
- com.google.zxing:core 生成二维码的简单使用
String content = ""; int size = 240; Hashtable<EncodeHintType, String> hints = new H ...
- Smarty使用-模版中编写js
在smarty模版中编写js使用literal标签, Literal 标签区域内的数据将被当作文本处理,此时模板将忽略其内部的所有字符信息. 该特性用于显示有可能包含大括号等字符信息的 javas ...
- Leetcode9_回文数
哈哈哈哈哈哈哈太开心了,今天的代码耗时和内存消耗比官方少了一半哈哈 (因为官方用C#写的,我用C++,手动狗头) 题目 判断一个整数是否是回文数.回文数是指正序(从左向右)和倒序(从右向左)读都是一样 ...
- Qt 使用自带的OpenGL模块开发程序
QT中使用opengl .pro文件中添加 QT += opengl 1.使用指定版本的OpenGL如下使用opengl4.5调用方法,使用指定版本的接口,必须设备图形显示设备支持对应OpenGL版本 ...
- sqlite基础API
/* 打开/创建数据库文件 * 如果数据库文件不存在就创建数据库文件. * 数据库操作句柄保存在第二个参数中. * 第一个参数:文件路径及其文件名 * 第二个参数:sqlite3操作句柄 * 返回值: ...
- E丢丢App重设计总结
E丢丢学习App是华夏大地教育可以有限公司旗下的一款产品,专为提升学历者打造,它整合了线上+跟踪的 (E平台)功能,方便工作人员随时随地管理账号.跟进学员:同时还可以随时了解教育行业的新闻资讯.一对一 ...
- 使用mybatis的动态sql解析能力生成sql
需求: 计算平台,有很多表,打算提供一个基于sql的服务接口, sql不能完全在配置页面写死, 要能根据参数不同执行不同的语义,防止sql个数爆炸 把mybatis原码down下来, 改造一下测试用例 ...
- First Django app(各个文件以及文件夹解析)
mkdir mysite cd mysite django-admin.py startproject mysite 执行上面的命令,得到一下内容: mysite/ manage.py mysite/ ...
- [LC] 257. Binary Tree Paths
Given a binary tree, return all root-to-leaf paths. Note: A leaf is a node with no children. Example ...