同事总结的map的知识点,记录下来,方便以后模糊的时候再查看。
1.Map: 映射关系 存储 key-value 值
2.底层是一个 Node 数组 (entry 数组 是 JDK 1.6 的版本)
3.JDK 1.8 的hashMap 新增了红黑树的概念,所以,这个版本之后的hashmap是由数组,链表,红黑树结合实现的。
4.Node 是hashMap的一个静态内部类,它实现了Map.Entry。1.8版本的hashMap的每个桶位存储的都是一个个 Node 对象
5.Node对象有四个属性值:
key 键
value 值
hash 集合中元素的hash值(是一个经过一系列计算得到的整型数据)
next 该元素所在的Node数组下标位(桶位)上的但项链中,该元素的下一个Node元素
6.hashMap 存储元素时采用的是链地址法,每个Node数组位置都有一条单向的链表结构,存储元素
7.hashMap在储存元素的时候,会先调用键的hashCode方法获得它的hashCode值,再将这个值经过hashMap的hash算法,得到一个 int 类型的hash数据,然后用这个数与 Node 数组的长度取余,得到的余数就是该元素在这个Node数组中的下标。
8.在存储过程中,难免会出现多个元素的hash数据跟数组长度取余的结果是一样的,这被称为 hash 碰撞,出现这个结果的解决办法就是将这些元素以链表的形式存储在一个桶位上。
9.hash 算法的结果越分散,hash 碰撞的几率就越小,此hashMap 的效率就越高
10.hashMap 集合的
默认长度是 16 ,默认的加载因子
(
loadFactor
)是 0.75f,切记,这个加载因子是float型的数据,且 在0~1之间的数。(负载因子可以大于1,但一般不建议使用大于1 的数据作为加载因子)
11.
threshold 称为容量边界,这个边界是指hashmap中的元素个数达到这个值时,需要map扩容以增加容量;扩容边界的计算方式为 容量*加载因子
12.
size 这个变量是指该集合中实际存储的元素的个数;这个变量和底层数组长度还有集合最大容量的区别应该分清楚。
13.
modCount
属于非线程安全的集合中的一个变量,它专门负责记录对该集合的结构做出改变的操作。是线程非安全的集合在使用迭代器遍历时,快速失败机制中的依据。(fail-fast)
14.
hashMap的
容量都是 2 的 N 次幂
的,
15.
为了进一步的提高hashmap的性能,JDK 1.8 把hashMap进行了一定的优化,特别是在桶位上出现多个值之后的优化,有以下规定:
hashMap的任意一个桶位出现的Node对象 超过8个时,该桶位的链表将转化成 红黑树 的数据结构 利用红黑树的特点提高集合的性能。
16.
hashMap 的默认加载因子是 0.75,也就是说当集合中存储的元素的数量到达总体容量的 75% 之后,再添加元素就需要扩容了。而此处的 0.75 是为了平衡map集合效率的综合结果。map集合要求有高性能,那这个加载因子就应该变小,但这样浪费内存;map集合要求放的元素多,就增大这个加载因子,这样内存浪费少,但性能低。
17.
hashMap确定一个要存入集合元素所在底层Node数组中的下标,需要通过以下几个步骤去计算:
1.通过该元素的 hashCode()方法获取该元素的 hash 码,
2.再通过 hashMap 类中提供的 hash 方法算出 hash 值,
3.将这个 hash 值与当前的集合的底层数组长度 做 取余运算,得到一个 int 类型的结果,便是该元素在底层数组中的下标。
然而,直接取余效率要低很多,就出现以下方法:
hash & (table.length -1)
上述 & 运算得到的结果与 模运算的结果是一样样的,但它基于二进制,所以要比直接取余效率更高。所以使用此方法。
19.map集合的存取过程:
存:put。这个方法需要两个参数:key and value
put 的存储过程涉及一系列的逻辑判断、扩容操作、链表和红黑树之间的转换等操作:
1.首先判断这个map的底层数组是否为空或者这个数组的长度是否为0,如果是,进行扩容操作,扩容完成后,继续执行后续操作。如果不是,则执行后续操作。
2.根据传入put方法的键的 hash 值去与数组长度-1 做 & 运算,得出这个键应该在集合底层数组中的下标,并且判断这个位置是否为null。则执行后续
如果为空直接依靠所需要的数据创建一个Node对象插入当前下标位置中。否操作。
3.如过这个位置上不为 null,表示该桶位已有数据,开始判断该桶位的第一个键是否与传入的数据相同,判断依据有以下几点:
1.传入方法的键的 hash 值与该位置的键的 hash 是否相等;
2.传入方法的键的引用和该桶位的键的引用是否相等,或者,在键不为空的情况下,传入方法的键所指向的地址是否与该桶位元素的键指向的内存地址相同;
若同时满足以上两点,则将传入的的输入暂时存储在 一个 Node 对象中;
4.如果步骤3的两个条件不同时满足的话,执行这个步骤。
1.首先判断当前桶位的数据是不是成 红黑树 结构,如果是,就将传入方法的一系列数据当做参数 调用 红黑树的存值方法中,将这些数据存放在红黑树上;
5.如果当前程序既不满足步骤3,也不满足步骤4,则执行下列操作:
1.当找到集合中键的位置,值不为空并且桶位的链式中第一个元素又不是这个键,后续也不是红黑树的前提下, 集合需要在该单向链上继续向后遍历,并且在遍历的过程中设立标记位,标记遍历次数,表示该单向链上的元素个数;
2.在遍历过程中,发现有null值,就以传入数据创建一个Node对象存储在此单向链上,之后再计算链上的元素个数,当元素个数大于8时,调用hashMap中将单向链转为红黑树的方法,将该链转成红黑树结构。
6.此步骤建立在步骤2成立的基础上。表示桶位的链式结构中有一个元素的键和传入数据的键是一致的,所以发生覆盖行为,将新的value覆盖老的value,并将老的value返回。
7.上面的步骤执行完之后,整体判断一次当前集合的实际元素个数是否大于扩容边界,如果大于,调用扩容方法,进行扩容。
8.最后的 afterNodeInsertion(evict); 属于预留方法,目前灭有实际意义;
9.在上述步骤6中,涉及到一个判断,判断条件 if (!onlyIfAbsent || oldValue == null) ,其中 onlyIfAbsent 是调用put方法时传入的一个boolean值,!onlyIfAbsent的结果必然是true,该条件必然成立。
10.由于是插入元素,改变了原先 map 集合的结构,所以还需要将快速失败机制的变量 modCound 加1;
JDK 1.8 的hashMap的 get(). 方法
1.这个方法会先声明一个 Node 键值对对象,调用获取Node元素的方法,将传入方法的 key 的 hash 值作为参数传入调用 getNode() 方法,三目运算符得出最后的结果;
2.调用的 getNode() 方法中声明了需要用到的所有参数,
1.首先判断几个条件
1.集合不为空
2.集合长度大于0
3.在传入方法参数的key的值得到和集合底层数组下标位置的元素不为null
同时满足上述三个条件,继续执行分支语句;
2.找到key对应的桶位后,判断该桶位的第一个元素:
1.key的 hash 值是否和第一个元素的 hash 一致
2.key 的引用和第一个元素的键的引用是否一致 或者 两个键指向的地址值是否一致
满足上述两个条件,第一个元素就是传入参数key所对应的元素,将第一个元素返回即可。
3.如果不满足上述条件,进入下一条判断:
1.第一个元素的下一个元素不为空 (first.next)
满足上述条件即可进入分支;
4.当第一个元素不是要找的元素,且有下一个元素时,首先需要判断:
1.这个桶位上的单向链是不是一个红黑树
若是红黑树,则将key作为参数调用红黑树查找元素的方法,获取红黑树中的数据
5.若不满足上述条件,则表示要判断第一个元素的下一个元素是否是需要获取的元素,依次类推,利用一个do-while循环即可
该循环中有以下判断:
1.key的 hash 值是否和第一个元素的 hash 一致
2.key 的引用和第一个元素的键的引用是否一致 或者 两个键指向的地址值是否一致
若满足以上条件,返回该次循环的元素即可。
6.若果上述所有的判断都不满足的话,方法返回 null ;表示该集合中没有该键对应的值。
JDK 1.8 中hashMap 的扩容机制
1.扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。
2.
hashMap 的键不建议使用可变对象
因为可变对象在当做键存在于一个 key-value 中存放在集合中,存放时,会根据当前的key的hash值去确定这个键值对存放在哪个桶中。原本使用不可变对象当做键的时候,存进去在什么位置,元素就一直会在什么位置,但是当使用可变对象当做key存储键值对时,存进去的键可能会被改变,键改变了之后,这个键的 hash 值也已经改变了,但这个元素不会因为 hash 的改变而改变存储位置,当使用改变后的键获取元素时,集合会将这个键的hash算出来并算出这个键应该存在的桶位,然后遍历这个桶位的链,未能找到这个元素;再以修改之前的对象去获取集合中的元素,集合会将这个键的hash计算出来,并计算出这个键应该存在的桶位,然后在这个桶位中查找目标元素,经过一系列比较,这个链中又没有与之相同的hash值,所以,只要是可变对象做为键的话,若键发生了改变,那就通过键获取不到这个元素了。新旧对象获取的都是 null;