JAVA集合重要知识点

一、集合简介

在这里插入图片描述
list,有序可重复;set,无序,不可重复;map,键值对。

二、List

ArrayList与LinkedList比较

ArrayList基于数组实现
LinkedList基于双向链表实现
ArrayList更利于查找,LinkedList更利于增删

ArrayList的扩容机制:

ArrayList是基于数组的集合,数组的容量是在定义的时候确定的,每次扩容时,创建一个1.5倍的新数组,然后把原数组的值拷贝过去。

快速失败和安全失败

快速失败
java集合的错误检测机制,线程a在遍历一个集合对象时,线程b对集合进行了修改,则会抛出异常。
迭代器在遍历集合时使用了一个modCount变量,集合在遍历开始会把modCount的值赋给expectedmodCount,集合如果在遍历器发生变化,就会改变modcount的值。每当迭代器)遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
不能依赖快速失败去判断集合是否被另一个线程修改,因为修改后的modCount值也可能和之前一样。
java.util包下的集合类都是快速失败的。

安全失败
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
遍历期间原集合如果发生修改,迭代器也是不知道的。
java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如CopyOnWriteArrayList类。

ArrayList如何线程安全

使用 Vector 代替 ArrayList,不推荐使用。
加锁,使用 Collections.synchronizedList 包装 ArrayList,然后操作包装后的 list;程序通过同步机制。
使用 CopyOnWriteArrayList 代替 ArrayList。

CopyOnWriteArrayList

读写分离,写时复制
读操作是无锁的,允许并发读,性能高
写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

三、Map

HashMap的数据结构

JDK1.7的数据结构是数组+链表
JDK1.8的数据结构是数组+链表+红黑树
HashMap的实际容量指数组大小。
在这里插入图片描述桶数组是用来存储数据元素,链表是用来解决冲突,红黑树是为了提高查询的效率。
数据元素通过映射关系,也就是散列函数,映射到桶数组对应索引的位置
当发生冲突时,会拉出一个链表,插入冲突数据
如果链表长度>8且数组大小>=64,链表转为红黑树
当红黑树节点个数小于6,转化回链表。

红黑树简介

介于简单的二叉树和平衡二叉树之间。
本质上是一种二叉查找树,为了保持平衡,它又在二叉查找树的基础上增加了一些规则,使查找效率变高。
相比于平衡二叉树,平衡二叉树是比红黑树更严格的平衡树,也就是说平衡二叉树保持平衡的效率更低,所以平衡二叉树插入和删除的效率比红黑树要低。

HashMap的put流程

在这里插入图片描述

HashMap怎么查找元素的

使用哈希函数获取hash值
计算数组下标获取节点
当前节点和key匹配,直接返回,否则则查找红黑树或者遍历链表

HashMap的哈希函数是怎么设计

先拿到key的hashcode,是一个32位的int类型数值,然后让key的hashCode和key的hashCode右移16位做异或运算
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

为什么哈希函数能降hash碰撞

hashmap寻找数组下标的时候,会拿key的hashcode的值经过其哈希函数的运算得到的值去计算其数组下标,这个值在源码函数中为h,数组长度为length。
在这里插入图片描述
算法如上,就是把h和length-1去做一个&运算。
这里也体现了为什么hashmap的数组长度要取2的整数次幂,因为2的整数次幂-1的二进制是高位全为0,低位全取1的状态。这样的值与上述哈希算法算出的散列值做与运算,才能只保留低位值,从而得到数组下标。
但是,就算散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。下面来看这个哈希算法,key为要put的key值。
在这里插入图片描述
key.hashCode() 函数返回的是一个32位的int值,命名为h
源码中先对h进行16位右移,再与它本身进行异或运算。异或运算即相同为0,不同为1。
也就相当于把h的高16位和低16位拿来做异或操作,这样等于是综合了key的hashcode值的高低位,使得到得低位值更具有随机性。
在这里插入图片描述

为什么HashMap的容量是2的倍数

两个原因。
一是hashmap获取下标的方式是hash值&(数组大小-1),这种方式比直接取余要快,却能达到取余的效果。
只有数组大小为2的整数幂的时候,数组大小减一的二进制形式才是…1111 这种格式。
这个值与hashmap的哈希算法得到的值进行与运算,才能得到一个数组大小内的下标数字且尽量减少hash碰撞。
二是在扩容时,利用扩容后的大小也是2的倍数,将元素效率更高的转移到新的table中去。因为扩容后的数组大小仍然是2的倍数,这样的话元素的下标位置是否移动取决于hash值的高一位是否为1,如果为1则移动,如果为0则下标不变。而不需要每个下标都重新计算。

初始化HashMap,传一个17的值new HashMap<>,会发生什么

初始化时,传的不是2的倍数时,HashMap会向上寻找离得最近的2的倍数,传入17,HashMap的实际容量是32。
可以从源码来看:
在这里插入图片描述
在这里插入图片描述
当initialCapacity为17时候,也就是传入tableSizeFor的cap的值是17
MAXIMUM_CAPACITY是Map容量的最大值
计算时候,不断地将n=cap-1 向右移位然后与原值做或运算,其实就等于是把二进制的所有位置都填充成1,最后加1,就是向上的离得最近的2的倍数了。

解决哈希冲突的一些方法

链地址法:在冲突的位置拉一个链表,把冲突的元素放进去。也就是HashMap的方法。
开放定址法:开放定址法就是从冲突的位置再接着往下找,给冲突元素找个空位。
再哈希法:换种哈希函数,重新计算冲突元素的地址。
建立公共溢出区:再建一个数组,把冲突的元素放进去。

为什么HashMap链表转红黑树的阈值为8

树化发生在table数组的长度大于64,且链表的长度大于8的时候。
红黑树节点的大小大概是普通节点大小的两倍,所以转红黑树,牺牲了空间换时间。所以设计时应当尽量减少红黑树的出现。
理想情况下,使用哈希算法,链表里的节点个数增加的概率是递减的,节点个数为8的情况,发生概率特别小。
至于红黑树转回链表的阈值为什么是6,而不是8?是因为如果这个阈值也设置成8,假如发生碰撞,节点增减刚好在8附近,会发生链表和红黑树的不断转换,导致资源浪费。

HashMap扩容在什么时候,为什么扩容因子是0.75

为了减少哈希冲突发生的概率,当当前HashMap的元素个数达到一个临界值的时候,就会触发扩容。
当元素个数大于HashMap容量*扩容因子时,就会触发扩容。
HashMap的散列构造方式是Hash算法寻找数组下标,负载因子决定元素个数达到多少时候扩容。
假如我们设的比较大,元素比较多,空位比较少的时候才扩容,那么发生哈希冲突的概率就增加了,查找的时间成本就增加了。我们设的比较小的话,元素比较少,空位比较多的时候就扩容了,发生哈希碰撞的概率就降低了,查找时间成本降低,但是就需要更多的空间去存储元素,空间成本就增加了。0.75恰好是一个比较合适的数字。

HashMap扩容机制

当HashMap满足扩容条件时,就开始扩容
HashMap的数组长度总是2的整数幂次方,扩容后的长度是原来的2倍。综合上面HashMap的寻址算法,扩容后的元素,要么在原位置,要么移动扩容的长度。这个取决于元素的key的经过哈希算法得到的数值,如下图
在这里插入图片描述
因为扩容后,数组长度-1得高一位就会变成1,与该数值进行&操作,当得到得该位值为0的时候,则元素下标位置不表,为1的话则移动扩容位置的长度。比如原数组长度为16,该元素下标位置为5,则扩容后的下标位置为5或者21

jdk1.8对HashMap主要做了哪些优化

数组 + 链表改成了数组 + 链表或红黑树
链表的插入方式从头插法改成了尾插法(1.7是有bug的)
扩容rehash:扩容的时候 1.7 需要对原数组中的元素进行重新 hash 定位在新数组的位置,1.8 采用更简单的判断逻辑,不需要重新通过哈希函数计算位置,新的位置不变或索引 + 新增容量大小。
扩容时机:在插入时,1.7 先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容;

线程安全的HashMap

HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap

HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个table数组,粒度比较大;
Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现;
以上两者都可以理解为简单粗暴的加锁。
比较推荐使用ConcurrentHashMap。

ConcurrentHashMap介绍

1.7版本:
ConcurrentHashmap线程安全在jdk1.7版本是基于分段锁实现
在这里插入图片描述
每个segment都可以理解为一个加了锁的包含HashEntry的数组,HashEntry本身即为链表结构。
实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。

put流程其实和HasnMap很像。先定位到具体的Segment,然后通过ReentrantLock去操作,后面的流程,就和HashMap基本上是一样的。
先计算hash,定位到segment,segment如果是空就先初始化。
之后是加锁,获取锁,获取不到会先自旋后阻塞,直到获取锁。
最后则是定位下标,替换或插入链表。

get流程则是直接定位取值,无需加锁。因为value是volatile修饰的,所以不会读到错误的值。

1.8版本:
据结构和HashMap是一样的,数组+链表+红黑树。它实现线程安全的关键点在于put流程。
在这里插入图片描述
如上图所示,除了向链表或者红黑树中插入数据这一步,之前的所有步骤,都采用了CAS思想,这样最大化的提高了多线程HashMap的性能。
get流程和HashMap的一样,是不需要加锁的。

有序的Map,LinkedHashMap

LinkedHashMap维护了一个双向链表,有头尾节点
同时 LinkedHashMap 节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 before 和 after 用于标识前置节点和后置节点。
在这里插入图片描述
从而实现有序。

TreeMap

TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现
所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用于 key 的比较。

四、Set

HashSet

HashSet 底层就是基于 HashMap 实现的
HashSet的add方法,直接调用HashMap的put方法,将添加的元素作为key,new一个Object作为value,据返回值是否为空来判断是否插入元素成功。因为HashMap的put方法当key在table数组中不存在的时候,才会返回插入的值。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值