1.什么是集合
集合框架:用于存储数据的容器
集合框架是为表示和操作集合而规定的一种统一的标准的体系结构
任何集合框架都包含三大块内容:对外的接口,接口的实现,对集合运算的算法
接口:表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现,从而达到多态。
实现:集合接口的具体实现
算法:在接口上完成查找,排序等
2.集合的特点
- 对象封装数据,对象多了也需要存储。集合用于存储对象
- 对象的个数确定可以使用数组,对象的个数不确定可以使用集合。集合长度可变
3.集合和数组的区别
- 数组是固定长度的,集合可变长度
- 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型
- 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型的
4.常用集合类
5.哪些集合是线程安全的
- vector:就比ArrayList多了个同步化机制(线程安全),因为效率较低,现在已经不建议使用了
- stack:堆栈类,先进后出
- hashtable:就比HashMap多了个线程安全
- enumeration:枚举,相当于迭代器
6.ArrayList和LinkedList有什么区别
- 数据结构不同
ArrayList基于数组实现
LinkedList基于双向链表实现
-
ArrayList利于查找,LinkedList利于增删
-
是否支持随机访问
ArrayList支持,也实现了RandomAccess接口(这个接口是用来标识是否支持随机访问)LinkedList不支持
-
内存占用
ArrayList占用连续的内存
LinkedList内存空间不连续,需要存储前驱和后继
7.ArrayList的扩容机制
ArrayList是基于数组的集合,数组的容量是在定义的时候确定的,所以在插入的时候会先检查是否需要扩容。
ArrayList的扩容是创建一个1.5倍的新数组,然后把原数组的值拷贝过去。
ArrayList初始值是10吧
8.有哪几种实现ArrayList线程安全的方法
- 使用Vector代替ArrayList
- 使用Collections.synchronized包装ArrayList,然后操作包装后的list
- 使用CopyOnWriteArrayList代替ArrayList
- 在使用ArrayList时,应用程序通过同步机制去控制ArrayList的读写
HashMap追魂连问
HashMap是一种面试中常问的集合,当然也是我们编程生活中常用的,而且相对难一点的。在面试中常涉及到原理,数据结构,并发等。希望大家好好研究一下这个。
9.能说一下HashMap的数据结构吗
其中,桶数组是用来存储数据元素,链表是用来解决冲突的,红黑树是为了提高查询的效率。
- 数据元素通过映射关系,也就是散列函数,映射到桶数组对应索引的位置
- 如果发生冲突,从冲突的位置拉一个链表,插入冲突的元素
- 如果链表长度>8 & 数组大小>=64,链表转为红黑树
- 如果红黑树节点个数<6,转为链表
第三点,当数组的长度没有超过64的时候,数组的每个节点都是链表,只会扩容,不会转换成红黑树
10.你对红黑树了解多少,为什么不用二叉树呢
红黑树本质上是一种二叉查找树,为了保持平衡,它又在二叉查找树的基础上增加了一些规则:
1)每个节点要么是红色,要么是黑色
2)根节点永远是黑色的
3)所有的叶子节点都是黑色的
4)每个红色节点的两个子节点一定是黑色的
5)从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点
11.红黑树怎么保持平衡的
红黑树有两种方式保持平衡:旋转和染色
12.HashMap的put流程
1)首先进行哈希值的扰动,获取一个新的哈希值。(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
2)判断tab是否为空或者长度为0,如果是则进行扩容操作
3)根据哈希值计算下标,如果对应小标正好没有存放数据,则直接插入即可否则需要覆盖。
4)判断tab[i]是否为树节点,是则向树中插入节点,否则向链表中插入节点
5)如果链表插入节点的时候,链表长度大于等于8,则需要把链表转换为红黑树
6)最后所有元素处理完成后,判断是否超过阈值,超过则扩容
13.HashMap怎么查找元素的
1)使用扰动函数,获取新的哈希值
2)计算数组下标,获取节点
3)当前节点和key匹配,直接返回
4)否则,当前节点为树节点,查找红黑树
5)否则,遍历链表查找
14.HashMap的哈希/扰动函数是怎么设计的
15.为什么哈希/扰动函数能降hash碰撞
因为 key.hashCode() 函数调用的是 key 键值类型自带的哈希函数,返回 int 型散列值。int 值范围为 -2147483648~2147483647,加起来大概 40 亿的映射空间。
16.为什么HashMap的容量是2的倍数
第一个原因:为了方便哈希取余
第二个原因:在扩容时,利用扩容后的大小也是2的倍数,将已经产生hash碰撞的元素完美的转移到新的table
17.如果初始化HashMap,传一个17的值new HashMap<>,它会怎么处理
初始化的时候不是2的倍数时,HashMap会向上寻找离得最近的2的倍数
18.你还知道哪些哈希函数的构造方法呢
除留取余法
直接定址法
数字分析法
平均取中法
19.ConcurrentHashMap
在jdk1.8基于CAS+synchronized实现
最重要的:在多线程的环境下,ConcurrentHashMap如何保证线程安全
- 初始化数组或头节点时,ConcurrentHashMap并没有加锁,而是CAS的方式进行原子替换(原子操作,基于Unsafe类的原子操作API)
- 插入数据时会进行加锁处理,但锁定的不是整个数组,而是槽中的头节点。所以,ConcurrentHashMap中锁的粒度是槽,并发性能很好
- 扩容时会进行加锁处理,锁定的仍然是头节点。并且,支持多个线程同时对数组扩容,提高并发能力。每个线程需先以CAS操作抢任务,争抢一段连续槽位的数据转移权。抢到任务后,该线程会锁定槽内的头节点,然后将链表或树中的数据迁移到新的数组里。
- 查找数据时并不会加锁,所以性能很好。另外,在扩容的过程中,依然可以支持查找操作。如果某个槽还未进行迁移,则直接可以从旧数组里找到数据。如果某个槽已经迁移完毕,但是整个扩容还没结束,则扩容线程会创建一个转发节点存入旧数组,届时查找线程根据转发节点的提示,从新数组中找到目标数据。
如果一个线程对这个ConcurrentHashMap进行插入(put)时,发现数组正在扩容,那么它就会立即参与扩容操作,完成扩容后再插入数据到新数组。
插入也就是put操作,如果我们追溯put的源码,会发现在真正操作时有if判断,if判断有四个分支
- 第一个分支,是整个数组的初始化
- 第二个分支,是所在的槽为空,说明该元素是该槽的第一个元素,直接新建一个头结点,然后返回
- 第三个分支,说明该槽正在进行扩容,帮助其扩容
- 第四个分支,就是把元素放在槽内。槽可能是链表,可能是红黑树。第4个分支是包裹在synchronized(f)里面的,f对应的数组下标位置的头结点,意味着每个数组元素有一把锁,并发度等于数组的长度。