一、Map接口 (键值对集合)
1.实现类
(1).线程不安全
HashMap
1.特点:
①无序
②查找效率高:根据key,查找value
2.数据结构:数组(哈希表)+ 链表(链地址法解决哈希表冲突) + 红黑树(自平衡二叉树,提高查找效率)
①数组(哈希表):
HashMap内部定义了一个数组,数组中的每个位置被称为“桶”( Bucket ),这是HashMap的基础结构【哈希表】
下标位置:当添加一个新的key-value键值对时,会根据key的 hashcode(),通过哈希函数计算出一个新的哈希值 hash,并通过这个hash值,计算key-value键值对在数组中的下标位置(桶Bucket ) ;
数组容量:在添加第一个key-value键值对时,数组容量被初始化为16,并且可以根据key-value 键值对的数量和负载因子,数组会自动按照2倍进行扩容;
②链表:
数组的每个位置( Bucket桶)可以保存一个或多个key-value键值对;
当两个或更多的 key-value键值对,被映射保存到数组的同一个位置(桶Bucket)时,就产生了哈希表冲突;
HashMap使用链地址法,解决哈希冲突,这些键值对将以链表的形式存储在产生冲突的位置(桶Bucket ) ;
③红黑树:
为了优化链表的查询性能,当链表长度超过一个阈值(默认是8)并且数组的容量大于等于64时,链表会转换成红黑树;
红黑树是一种自平衡的二叉查找树,它可以基于二分查找的方式,进行元素的查找,提高查找搜索性能,这对于较长的链表来说是一个明显的性能提升;
当红黑树中的节点数量减少到6个或更少时,红黑树将转换回链表;
源码分析:
3.关键计算:计算key-value键值对在哈希表中的存储位置(桶)
哈希运算,计算新的哈希值,hash( )扰动函数 (h = key.hashCode()) ^ (h >>> 16)
目的:计算新的哈希值,降低哈希冲突概率
通过新哈希值,计算下标位置(桶位置) ,(n - 1) & hash
4.转换成红黑树的条件:链表的元素个数超过8,并且数组长度大于等于64
5.转换成红黑树的优点:
- 链表过长,会导致搜索效率降低,使用红黑树提高查找效率;
- 红黑树,是一颗自平衡的二叉查找树,树中所有节点均自动排序,并且自平衡,可以使用二分查找,提高查找效率。
6.影响性能的关键参数:
①数组容量:默认为16
- 容量越大,内存占用越多,产生冲突的概率越小
- 必须为2的N次幂
- 每次按照2倍进行扩容,最大容量不超过2的30次幂
②加载因子:默认为0.75
- 加载因子越高,代表利用率越高,产生冲突的概率越高
- 加载因子越低,代表利用率越低,产生冲突的概率越低
5.扩容条件:
- 默认情况下,数组长度为16(自定义时,必须保证数组长度为2^n 次幂)
- 默认情况下,数组长度为16(自定义时,必须保证数组长度为2^n 次幂)
- 达到扩容阈值threshold,按照2倍进行扩容
- 链表长度大于8,数组长度小于64,链表不会转换成红黑树,执行数组扩容
LinkedHashMap
1.特点:有序
2.数据结构:HashMap的子类,多维护了一条双向链表,保存顺序
TreeMap
1.特点:自动按照key排序
2.数据结构:红黑树
(2).线程安全
Hashtable
1.特点:无序、key和value不允许为null
2.数据结构:数组+链表
3. 线程安全:通过“synchronized”同步锁实现
ConcurrentHashMap
1.特点:无序
2.数据结构:数组+链表+红黑树
3.线程安全:synchronized同步锁 + CAS无锁化编程模型
二、Collections工具类
封装了集合的常见算法和操作
三、常见的面试题
1.HashMap . LinkedHashMap .TreeMap的区别?
HashMap :无序,基于数组+链表+红黑树实现;
LinkedHashMap:有序,HashMap的子类;
TreeMap :自动排序,按照key或者自定义Comparator比较器,进行排序;
2.HashMap和 Hashtable的区别?
HashMap和 Hashtable都是Map接口的键值对集合实现类,它们的区别主要包括:
- 线程安全: HashMap是非线程安全的,而 HashTable线程安全;
- 执行效率:由于 HashTable使用synchronized同步锁实现线程安全所,以 HashTable效率要比HashMap略低;
- 使用Null做key和value :
HashMap:可以使用null 作为key 和value;
- HashTable:不允许有null键和null值,否则会抛出NullPointerException异常;
- 数据结构:HashMap:数组+链表+红黑树;HashTable:数组+链表;
- 扩容方式:
HashMap : 默认的初始化大小为16。之后每次扩充,容量变为原来的 2倍;
HashTable:默认的初始大小为11,之后每次扩充,容量变为原来的 2n+1 ;
3.HashMap的数据结构是什么?
①数组(哈希表):
HashMap内部定义了一个数组,数组中的每个位置被称为“桶”( Bucket ),这是HashMap的基础结构【哈希表】
下标位置:当添加一个新的key-value键值对时,会根据key的 hashcode(),通过哈希函数计算出一个新的哈希值 hash,并通过这个hash值,计算key-value键值对在数组中的下标位置(桶Bucket ) ;
数组容量:在添加第一个key-value键值对时,数组容量被初始化为 16,并且可以根据key-value 键值对的数量和负载因子,数组会自动按照2倍进行扩容;
②链表:
数组的每个位置( Bucket桶)可以保存一个或多个key-value键值对;
当两个或更多的 key-value键值对,被映射保存到数组的同一个位置(桶Bucket)时,就产生了哈希表冲突;
HashMap使用链地址法,解决哈希冲突,这些键值对将以链表的形式存储在产生冲突的位置(桶Bucket ) ;
③红黑树:
为了优化链表的查询性能,当链表长度超过一个阈值(默认是8)并且数组的容量大于等于64时,链表会转换成红黑树;
红黑树是一种自平衡的二叉查找树,它可以基于二分查找的方式,进行元素的查找,提高查找搜索性能,这对于较长的链表来说是一个明显的性能提升;
当红黑树中的节点数量减少到6个或更少时,红黑树将转换回链表;
4.HashMap中put( )的执行过程?
①计算哈希码:
。向HashMap中添加一个新的 key-value键值对时,首先会调用key键的hashcode()方法来计算一个hash哈希值。(这个哈希值将被用于计算key-value键值对,在数组中保存时的下标位置)
②计算数组下标:
。计算完hash哈希值后,HashMap会通过数组长度n,按照(n-1)& hash 的方式,将哈希值转化为数组下标;。(n-1) & hash的作用等同于hash % n,位运算比算术运算的效率高;
③处理哈蒂冲突:
。如果不同key按照各自不同的hash哈希值,计算的数组下标位置相同,这种情况称为哈希表冲突;
。HashMap使用链地址法来处理哈希表冲突,在数组的每个位置存放一个链表或红黑树(当链表长度达到一定阈值时会转换为红黑树)。因此,新的键值对会被添加到当前位置的链表或红黑树中;
④检查键是否已存在:
。在将键值对放入对应位置之前,HashMap会检查该位置是否存在与新键相等的对象;
。会调用equals()方法来比较键之间的相等性。如果找到相同的键,那么旧的值将被新值替换,返回旧值;否则,新键值对将被添加;
⑤插入新节点:
。如果没有找到相等的键,新键值对将被插入到该位置的链表头部或红黑树中;
。当链表长度达到8且数组容量大于64时,链表会转化为红黑树,用来优化查找性能;
⑥检查容量和扩容:
。每次插入新键值对后,HashMap会检查当前元素数量是否超过了它的扩容阈值(容量乘以加载因子(默认为0.75 ) ) ;。如果超过了扩容阈值,HashMap会按照2倍进行扩容;
⑦返回旧值:
。如果在插入过程中发现键已经存在,put()方法会返回旧的值;。如果键不存在,插入了一个新键值对,put()方法将返回null ;
5.HashMap如何计算key-value键值对元素在数组中的存储位置?
。为了使key-value元素可以均匀散列的保存在(HashMap的数组中,所以使用key的哈希值进行哈希扰动计算出一个新的 hash值。
。在JDK 1.8版本以前,Hashwap会将这个hash值与数组长度进行%模运算出一个下标值,从而确定key-value在数组中的存储位置;
。例如:数组长度默认为16,12580 % 16 = 4,753951 % 16 = 15。所以哈希值为12580 、 753951的键值对,会存储在数组下标为4和3的位置。
。在JDK 1.8版本以后,由于“%”模运算性能消耗比较大,所以采用(长度– 1) & hash的位运算方式来计算存储位置;
。例如:数组长度默认为16,(16 - 1) & 12580=4,(16 - 1) & 753951。所以哈希值为12580 、 753951的键值对,会继续存储在数组下标为4和3的位置。
6.什么是哈希冲突?
。哈希冲突是在使用哈希函数或哈希表时遇到的一种特殊情况;
。如果发生在哈希函数,哈希冲突是指当发生在两个不同的输入值,经过哈希函数处理后产生了相同的哈希值;
。如果发生在哈希表,哈希冲突是由于哈希表的大小是有限的,而输入的数据集合可能是无限的,因此,当不同的输入数据映射到哈希表的同一个位置时,就会出现哈希冲突;
7.如何解决哈希冲突?
处理哈希冲突的方法主要有以下几种:
1,开放寻址法(open Addressing )∶当哈希冲突发生时,使用某种探测技术在哈希表中寻找下一个空位来存储数据。
2,链地址法(chaining )∶每个哈希表的槽位都维护一个链表。当哈希冲突发生时,所有哈希值相同的元素都被存储在同一个位置
对应的链表中。这种方法也称为拉链法。
3,再哈希法(Rehashing )∶当哈希冲突发生时,使用第二个哈希函数再次计算哈希值,直到找到空槽位。这种方法会增加计算复
杂度,但可以减少哈希表的装载因子,从而提高查找效率。
4,建立一个公共溢出区︰将哈希表分为基本表和溢出表两部分。所有哈希地址不冲突的元素都存放在基本表中,所有哈希地址冲突的元
素都存放在溢出表中。
8.HashMap如何解决哈希冲突?
HashMap使用链地址法来处理哈希表冲突,当产生哈希冲突时,HashMap会将产生冲突的Key-value键值对,通过一个链表保存在数组产生冲突的位置中;
9.HashMap的长度为什么是2的幂次方(2的倍数)?
HashMap为什么按照2倍进行扩容?
因为必须保持HashMap的的容量为2的幂次方
。降低哈希冲突概率:
。当HashMap中的数组长度为②的幂次方,不同的key计算得到 index相同的几率较小,不容易产生冲突;
。提高计算效率:
HashMap中使用哈希表保存时,索引计算公式为i = (n - 1) & hash 。
。如果n为②的幂次方,那么n-1的低位全是1,那么使用hash哈希值进行&与操作时,可以保证低位的值不变的情况下,高位更多的参与运算,从而保证分布均匀散列;
。同时,(n - 1) & hash效果等同于 hash % n,但是&与运算属于位运算,效率比“%”模运算性能要高;
10.HashMap影响性能的两个参数?
。构建HashMap实例时有两个重要的参数会影响其性能:初始容量和加载因子
。初始容量:用来规定哈希表数组的容量长度,默认为16,因为16是②的幂次方的原因,所以在小数据量的情况下,能减少哈希冲突,提高性能。如果存储大容量数据的时候,最好预先判断数据量,按照②的幂次方,提前预设初始容量;
。加载因子:用来表示哈希表中元素的填满程度,默认为 0.75,越大则表示允许填满的元素就越多,哈希表的空间利用率就越高,但是冲突的机会增加。反之,越小则冲突的机会就会越少,但是空间很多就浪费。
。所以,在设置初始容量时,应该考虑到初始容量及其加载因子,预估设置初始容量,最大限度降低扩容操作频率。
11.HashMap的扩容机制?
HashMap的用来进行扩容的方法是resize()方法;HashMap在以下三种场景下,会触发扩容机制:
1.当HashMap通过无参构造方法创建,在第一次调用put()方法添加key-value键值对时,数组初始化为16 ;2当HashMap中的元素个数超过扩容阈值 threshold时,数组的容量按照原容量的②倍进行扩容:
。扩容阈值threshold=数组容量×加载因子LoadFactor ;
。加载因子LoadFactor的默认值为 0.75,数组容量默认为16,扩容阈值 threshold默认为12 ( 16 × 0.75 =12 ) ;所以,当Hashwap中元素个数超过12时,数组的容量按照原容量的②倍进行扩容(( 16 x 2 = 32 );3.加入元素时,如果链表长度大于阈值(默认为8)并且数组长度小于64,会产生数组扩容;
13.HashMap为什么使用链表?
。HashMap使用哈希表作为基础数据结构,当两个不同的 key-value键值对,通过 hash哈希值计算数组下标,出现相同下标情况时,产生哈希冲突;
。HashMap使用“链地址法”解决哈希冲突,所以需要使用链表,来保存产生哈希冲突的 key-value键值对;
14.HashMap为什么使用红黑树?
。HashMap中的链表长度增长到一定长度,会导致搜索性能下降;(链表是线性方式搜索)
·所以,Hashap会在链表过长时,将链表转换为红黑树,通过红黑树提供搜索性能;(红黑树是二分查找方式搜索)
。HashMap不直接用红黑树的原因是:链表简单易于维护,红黑树维护复杂。所以首选使用链表,只有链表长度过长时,才会转换成红黑树来提高搜索查找的性能;
15.红黑树有哪些特点?
HashMap中的红黑树
红黑树是一种自平衡二叉查找树:
。二叉查找树:树中的所有节点满足排序规则(left < root < right ),可以基于二分查找进行搜索;
。自平衡:保持左右子树的平衡,保持搜索性能。不会出现退化成链表的极端情况(所有子节点都保存在左子树或右子树);。所以,HashMap使用红黑树来优化长链表;
16. HashMap链表转换红黑树的条件?
在HashMap中,当链表转换为红黑树,需要同时满足两个条件︰链表长度大于等于8,并且数组容量大于等于64 ;
1,链表长度:当链表的长度大于等于⑧时(有8个或更多的元素在(HashMap的同一个桶( bucket) 中形成链表),这是因为在链表长度较小的情况下,链表的线性查找效率还可以接受,但是一旦当链表长度较长时,必须通过将链表转换成红黑树这种方式,才能够提供更好的查找性能。
2,数组容量︰在链表达到阈值8时,同时还要检查 HashNap的底层数组(哈希表)的容量大于等于64。因为HashMap不会轻易使用红黑树,虽然红黑树的搜索性能比链表好,但是维护更复杂。只有在链表足够长,只有红黑树才能带来明显性能提升时,才会进行转换。
。所以,如果数组容量小于64,即使链表长度超过8,也不会立即进行红黑树的转换,而是先进行数组的扩容,将HashMap中的所有元素重新散列保存,用来降低链表的长度。(优先使用链表)
17.HashMap红黑树退化成链表的条件?
当红黑树中的节点数量减少到6个或更少时,红黑树将转换回链表;·因为,红黑树中的节点过少时,使用链表来保存。维护会更简单高效;·这种机制,可以让HashMap在性能和资源使用之间找到一个平衡点;