位运算操做是由处理器支持的底层操做,底层硬件只支持01这样的数字,所以位运算运行速度很快。尽管现代计算机处理器拥有了更长的指令流水线和更优的架构设计,使得加法和乘法运算几乎与位运算同样快,可是位运算消耗更少的资源。经常使用的位运算以下:html
位与 & (1&1=1 1&0=0 0&0=0)java
位或 | (1|1=1 1|0=1 0|0=0)web
位非 ~ ( ~1=0 ~0=1)redis
位异或 ^ (1^1=0 1^0=1 0^0=0)算法
有符号右移 >> 在执行右移操做时,若参与运算的数字为正数,则在高位补0;若为负数,则在高位补1。shell
无符号右移 >>> 不管参与运算的数字为正数或为负数,在执运算时,都会在高位补0。数组
左移 对于左移是没有正数跟负数这一说的,由于负数在CPU中是以补码的形式存储的,对于正数左移至关于乘以2的N次幂。安全
敲重点:上面的重重都是简单的只是为了引出下面的结论:数据结构
a % (Math.pow(2,n))
等价于a&( Math.pow(2,n)-1)
多线程
好比a%16
最终的结果必定是0~15
之间的数字,而a&1111
正好把a除16后的余数有效的现实出来了由于若是是1 1111这样的话最前面一位其实表明的16,也就是说二进制从倒数第五位开始只要出现了1那绝对表明的是16的倍数。 结论:位运算比除法运算在运行效率上更高,对一个数取余尽可能用a&二进制数
这样能够更好的提速。
咱们知道ArrayList
是一个数组队列,至关于动态数组。与Java中的基本数组相比,它的容量能动态增加。它具备如下几个重点。
ArrayList 其实是经过一个 数组去保存数据的。当咱们构造ArrayList时;若使用默认构造函数,则ArrayList的默认容量大小是 10。 当ArrayList容量不足以容纳所有元素时,ArrayList会从新设置容量:原来容量的1.5倍。 ArrayList的克隆函数,便是将所有元素 克隆到一个数组中。 克隆的底层是System.arraycopy(0,oldsrc,0,newsrc,length); ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每个元素”;当读出输入流时,先读取“容量”,再依次读取“每个元素”。
优势:
根据下标遍历元素效率较高。 根据下标访问元素效率较高。 在数组的基础上封装了对元素操做的方法。 这样的动态数组在内地地址上是空间连续的。 能够自动扩容。
缺点:
插入和删除的效率比较低。 根据内容查找元素的效率较低。 扩容规则:每次扩容现有容量的50%。
双向链表每个节点包含三部分(data,prev,next),它不要求空间是连续的。相似于节点跟节点之间经过先后两条线串联起来的。
ArrayList和LinkedList总结:
ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表结构。 对于随机访问的get和set方法,ArrayList要优于LinkedList,由于LinkedList要移动指针。 对于新增和删除操做add和remove,LinkedList比较占优点,由于ArrayList要移动数据。 ArrayList使用在 查询比较多,可是插入和删除比较少的状况,而LinkedList用在查询比较少而插入删除比较多的状况
首先你须要对二叉树有个了解,知道这是什么样子的一个数据组合方式,而后知道二叉树查找的时候缺点,可能发生数据倾斜。所以引入了平衡二叉树,平衡二叉树的左右节点深度之差不会超过1,查找方便构建麻烦,所以又出现了红黑树。红黑树是一种平衡的二叉查找树,是一种计算机科学中经常使用的数据结构,最典型的应用是实现数据的关联,例如map等数据结构的实现,红黑树重要特性是( 左节点 < 根节点 < 右节点) 红黑树有如下限制:
节点必须是红色或者是黑色 根节点是黑色的 全部的叶子节点是黑色的。 每一个红色节点的两个子节点是黑色的,也就是不能存在父子两个节点全是红色 从任意每一个节点到其每一个叶子节点的全部简单路径上黑色节点的数量是相同的。
若是您对红黑树还不太了解推荐看下博主之前写的RBT
Hash表是一种特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区别,它可以快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。这个源于Hash表设计的特殊性,它采用了==函数映射==的思想将记录的存储位置与记录的关键字关联起来,从而可以很快速地进行查找。评价函数的性能关键在于==装填因子==,以及如何合理的解决哈希冲突,具体的可看博主之前写的完全搞定哈希表
一般具有前面一些知识点的铺垫就能够很好的开展HashMap的讲解了,既然ArrayList
,LinkedList
,Red Black Tree
各有优缺点,咱们能不能集百家之长实现一个综合产物呢 === >HashMap
,本文因此讲解都是基于JDK8。
HashMap
的组成部分:数组 + 链表 + 红黑树。HashMap
的主干是一个Node
数组。Node
是HashMap
的基本组成单元,每个Node
包含一个key-value
键值对。HashMap
的时间复杂读几乎能够接近O(1)
(若是出现了 哈希冲突可能会波动下),而且HashMap
的空间利用率通常就是在40%左右。HashMap
的大体图以下: PS:其中几个重要节点关系以下:
interface
定义了一些比较的接口函数。
HashMap
中存储的基本的KV。
Enrty
这个类继承自
HashMap.Node
这个类,
Enrty
是
LIinkedHashMap
的一个内部类,
TreeNode
的构造函数向上追溯继承了
LinkedHashMap.Entry
,然后者又继承了
HashMap.Node
。因此
TreeNode
既保有
Node
的属性,同时因为添加了
prev
这个前驱指针使得==链表==变为了==双向==。前三个节点跟第五个红黑树相关,第四个跟
next
跟双向链表相关。
HashTable
里的映射函数来决定将该数据放到数组的那个地方,数组初始化时候必定是2的次幂,默认16,初始化传入的任何数字都会通过
tableSizeFor
调整为2次幂。
TREEIFY_THRESHOLD=8
且数组长度 >=
MIN_TREEIFY_CAPACITY=64
,则会将该链表进化位
RedBlackTree
,若是
RedBlackTree
中节点个数小于
UNTREEIFY_THRESHOLD=6
会退化为链表。
特别提醒:读HashMap源码以前须要知道它大体特性以下:
HashMap的存取是没有顺序的 KV均容许为NULL 多线程状况下该类安全,能够考虑用HashTable。 JDk8底层是数组 + 链表 + 红黑树,JDK7底层是数组 + 链表。 初始容量和装载因子是决定整个类性能的关键点,轻易不要动。 HashMap是 懒汉式建立的,只有在你put数据时候才会build 单向链表转换为红黑树的时候会先变化为 双向链表最终转换为 红黑树,双向链表跟红黑树是 共存
的,切记。对于传入的两个 key
,会强制性的判别出个高低,判别高低主要是为了决定向左仍是向右。链表转红黑树后会努力将红黑树的 root
节点和链表的头节点 跟table[i]
节点融合成一个。在删除的时候是先判断删除节点红黑树个数是否须要转链表,不转链表就跟 RBT
相似,找个合适的节点来填充已删除的节点。红黑树的 root
节点不必定
跟table[i]
也就是链表的头节点是同一个哦,三者同步是靠MoveRootToFront
实现的。而HashIterator.remove()
会在调用removeNode
的时候movable=false
。
初始容量,默认容量=16,箱子的个数不能太多或太少。若是太少,很容易触发扩容,若是太多,遍历哈希表会比较慢。
数组的最大容量,通常状况下只要内存够用,哈希表不会出现问题
默认的负载因子。所以初始状况下,当存储的全部节点数 > (16 * 0.75 = 12 )时,就会触发扩容。 默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会下降空间开销,但提升查找成本(体如今大多数的HashMap类的操做,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,而且尽可能减小rehash操做的次数。若是初始容量大于最大条目数除以负载因子,rehash操做将不会发生。
从上面的表中能够看到当桶中元素到达8个的时候,几率已经变得很是小,也就是说用0.75做为加载因子,每一个碰撞位置的链表长度超过8个是几乎不可能的。 4. static final int TREEIFY_THRESHOLD = 8
这个值表示当某个箱子(数组的某个item)中,链表长度 >= 8 时,有可能会转化成树。设置为8,是系统根据泊松分布的数据分布图来设定的。
在哈希表扩容时,若是发现链表长度 <= 6,则会由树从新退化为链表。 设置为6猜想是由于时间和空间的权衡
当链表长度为6时 查询的平均长度为 n/2=3,红黑树 log(6) = 2.6 为8时 :链表 8/2=4, 红黑树 log(8)=3
链表转变成树以前,还会有一次判断,只有数组长度大于 64 才会发生转换。这是为了不在哈希表创建初期,多个键值对刚好被放入了同一个链表中而致使没必要要的转化。
HashMap的链表数组。不管咱们初始化时候是否传参,它在自扩容时老是2的次幂。
HashMap实例中的Entry的Set集合
HashMap表中存储的实例KV个数。
凡是咱们作的增删改都会引起
modCount
值的变化,跟版本控制功能相似,能够理解成version
,在特定的操做下须要对version
进行检查,适用于Fai-Fast
机制。在java的集合类中存在一种
Fail-Fast
的错误检测机制,当多个线程对同一集合的内容进行操做时,可能就会产生此类异常。 好比当A经过iterator去遍历某集合的过程当中,其余线程修改了此集合,此时会抛出ConcurrentModificationException
异常。 此类机制就是经过modCount
实现的,在迭代器初始化时,会赋值expectedModCount
,在迭代过程当中判断modCount
和expectedModCount
是否一致。
扩容阈值 threshold = capacity * loadFactor
可自定义的负载因子,不过通常都是用系统自带的0.75。
先来分析有关n位操做部分:先来假设n的二进制为01xxx...xxx。接着 对n右移1位:001xx...xxx,再位或:011xx...xxx 对n右移2为:00011...xxx,再位或:01111...xxx 此时前面已经有四个1了,再右移4位且位或可得8个1 同理,有8个1,右移8位确定会让后八位也为1。
综上可得,该算法让最高位的1后面的位全变为1。最后再让结果n+1,即获得了2的整数次幂的值了。 如今回来看看第一条语句:
int n = cap - 1;
让cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。若是不对它减1而直接操做,将获得答案10000,即16。显然不是结果。减1后二进制为111,再进行操做则会获得原来的数值1000,这种二进制方法的效率很是高。
map
的大小来反推须要的
threshold
,同时还可能会涉到
resize
,而后住个
put
到 容器中。
不管咱们put
数据仍是get
数据都要先得到该数据在这个哈希表中对应的位置。好比put
数据,它的流程分为2步。
1.先得到key对应的hash值。 2. 将该数据的hash值A,跟将A右无符号移动16位后再
^
获得最终值。这个操做叫扰动
,缘由是怕低几位出现想同的几率太大,尽量的将数据实现均匀分布。
同时JDK8跟JDK7的扰动目的同样,不过复杂程度不同。
相对来讲很简单,为方便理解先说下代码大体流程思路。
得到key的hash而后根据hash和key按照插入时候的思路去查找 get
。若是数组位置为NULL则直接返回 NULL。 若是数组位置不为NULL则先直接看数组位置是否符合。 若是数组位置有类型说红黑树类型,则按照红黑树类型查找返回。 若是数组有next,则按照遍历链表的方式查找返回。
宏观查找函数细节:
红黑树查找节点细节:
先得到根节点,左节点,右节点。 根据 左节点 < 根节点 < 右节点 对对数据进行逐步范围的缩小查找。 若是实现了Comparable方法则直接对比。 不然若是根节点不符合则递归性的调用find查找函数。
查询该key是否实现了Comparable
接口。
既然实现了Comparable接口就用该实现进行对比判断如何何去何从。
跟随源码梳理下put操做的大体流程。 数据插入的时候大体流程以下:
Hash
值计算。
table
的状态,若是
table
是空须要调用
resize
来进行初始化。
key
的目标位置。并判断当前位置状况。
putTreeVal
。
在JDK8中寻找待插入点
e
是经过==尾插法==(相似与排队在最后面),而在JDK7中是==前插法==(相似与加塞在最前面,之因此这样作是HashMap发明者认为后插入节被访问几率更大),对应代码以下。
e
进行判断
oldValue对应的旧值若是为NULL,那么不管onlyIfAbsent是否决定替换。都将被替换。 oldValue对应的旧值若是不为NULL,那么若是onlyIfAbsent是false就替换。 onlyIfAbsent:只有在缺席的状况下才替换,不缺席不替换。跟redis Setnx
一样的功能。
modCount
加1,同时看最新的总的节点数是否须要扩容了,若是是就扩容。
主要功能是根据参数的阈值范围绝对是否将链表转化为红黑树,而后首先将单项链表转化为双向链表,再调用treeify
以头节点为根节点构建红黑树。
双向链表跟红黑树建立,主要步骤分3步。
table[i]
对应好。
确保将root
节点挪动到table[first]
上,若是红黑树构建成功而没成功执行这个任务会致使tablle[first]
对应的节点不是红黑树的root
节点。正常执行的时候主要步骤分2步。
root
节点放到跟节点,至此关于红黑树到操做搞定。
first
节点,如今将多是中间节点的
root
节点挪到
first
节点前面。
checkInvariants
函数的做用:校验
TreeNode
对象是否知足红黑树和双链表的特性。由于并发状况下会发生不少异常。
得到老table数据,若是老table已经足够大则再也不扩容,只调节阈值。 老table扩容后的范围也 符合要求直接将容器大小跟阈值都扩容 。 若是是带参数构造函数则须要将阈值复制给容器容量。 不然认为该容器初始化时未传参,需初始化。 若是老table有数据,新他变了大小设置好了可是阈值没设置成功。此时要设置新阈值。 建立新容器。 老table成功扩容为新table,涉及到数据的转移。
数据不为空是单独的节点则直接从新hash分配新位置。 数据不为空后面是一个链表,则要把链表数据进行区分看那些分到老地方那些分到新地方。 若是该节点类型是个红黑树则调用split.
链表形式的从新划分解释以下: 注意:不是
(e.hash & (oldCap-1))
而是(e.hash & oldCap)
, 后一个获得的是 元素的在数组中的位置是否须要移动,示例以下
示例1: e.hash= 10 0000 1010 oldCap=16 0001 0000 & =0 0000 0000 比较高位的第一位 0 结论:元素位置在扩容后数组中的位置没有发生改变 示例2: e.hash= 17 0001 0001 oldCap=16 0001 0000 & =1 0001 0000 比较高位的第一位 1 结论:元素位置在扩容后数组中的位置发生了改变,新的下标位置是原下标位置+原数组长度
扩容后如何处理原来一个table[i]
上的红黑树,代码的总体思路跟处理链表的时候差很少,只要理解节点关系保存红黑树的时候也保存了双向链表就OK了。
函数功能就是以指定的一个节点为根节点,根据指定的key
跟value
进行查找。
经过hash值判断 左边找仍是右边找。 若是找到的很简单直接返回。 可能出现hash值相等但是 key
不同,继续查找分为三种状况。
左节点为空则找右节点 右节点为空则找左节点 左右节点都不会空,尝试通 Comparable
对数据看向左仍是向右。没法经过comparable比较或者比较以后仍是相等。
直接从右节点递归查找下。 不然就从左边查找。
对两个对象进行比较,必定能比出个高低。
a 跟 b 都是字符串则直接在if判断里比拼完毕 a 跟 b 都是对象则直接查看对象在JVM中的hash地址,而后比较。
函数入口而已:
removeNode
无非就是查看table[i]是否存在,而后是否在首节点上,是否在红黑树上,是否在链表上。这几种状况,找到了则直接删除,同时注意平衡性。
该函数的 目的就是移除调用此方法的节点,也就是该方法中的this节点。移除包括链表的处理和红黑树的处理。能够看之前写过的RBT,删除的时候思路大体是同样的,这里大体分为3步骤。
红黑树也是双向链表,以链表的角度来删除节点,而后判断是否须要退化为链表。 根据当前的 p
节点尝试从pr
找最 小的或者从pl
找最 大的目标节点s
,将两点兑换。找到要 replacement
来跟p
进行替换。实施替换。 替换后为保持红黑树特性可能须要进行 balance
。
红黑树退化成链表
关于这个问题能够直接看博主之前写的红黑叔添加跟删除RBT
JDK7对旧table
数据重定位到新table
的函数transfer
以下,其中重点关注部分以标出。
Entry<K,V> next = e.next
就被挂起了,而线程2正常执行完毕,结果图以下:
7中找 Hash
用了4次,8中只用了1次。7 = 数组 + 链表,8 = 数组 + 链表 + 红黑树 7中是头插法,多线程容易形成环,8中是尾插法。 7的扩容是所有数据从新定位,8中是位置不变+ 移动旧size大小来实现更好些。 7是先判断是否要扩容再插入,8中是先插入再看是否要扩容。 HashMap
无论78都是现场不安全的,多线程状况下记得用ConcurrentHashmap
。ConcurrentHashmap
下篇文章说。
随机搜罗了一些常见HashMap
问题,若是把上述代码都看懂了应付这些应该没问题。
HashMap原理,内部数据结构。 HashMap中的put,get,remove大体过程。 HashMap中 hash函数实现。 HashMap如何扩容。 HashMap几个重要参数为何这样设定。 HashMap为何线程不安全,如何替换。 HashMap在JDK7跟JDK8中的区别。 HashMap中链表跟红黑树切换思路。
HashMap讲解 HashMap详解 疫苗JAVA HASHMAP的死循环
本文使用 mdnice 排版