剖析⾯试最常⻅问题之 Java 集合框架
集合概述
Java 集合概览
从下图可以看出,在!"#$#!中除了以! %#& !结尾的类之外,!其他类都实现了! '())*+,-(. !接⼝。
并且,以! %#& !结尾的类都实现了! %#& !接⼝。
说说 List,Set,Map 三者的区别?
/-0, 1对付顺序的好帮⼿2:!存储的元素是有序的、可重复的。
3*, 1注重独⼀⽆⼆的性质24!存储的元素是⽆序的、不可重复的。
%#& 1⽤!5*6!来搜索的专家24!使⽤键值对(76*8$#)9*)存储,类似于数学上的函数!6:;1<2,
=<>代表!7*6,?6?代表!$#)9*,5*6!是⽆序的、不可重复的,$#)9*!是⽆序的、可重复的,每个
键最多映射到⼀个值。
集合框架底层数据结构总结
先来看⼀下! '())*+,-(. !接⼝下⾯的集合。
List
@AA#6)-0, :! BCD*+,EF 数组
G*+,(A : BCD*+,EF 数组
/-.7*H/-0, :!双向链表1"I5JKL!之前为循环链表,"I5JKM!取消了循环2
Set
N#0O3*, (⽆序,唯⼀)4!基于! N#0O%#& !实现的,底层采⽤! N#0O%#& !来保存元素
/-.7*HN#0O3*, : /-.7*HN#0O3*, !是! N#0O3*, !的⼦类,并且其内部是通过
/-.7*HN#0O%#& !来实现的。有点类似于我们之前说的! /-.7*HN#0O%#& !其内部是基于
N#0O%#& !实现⼀样,不过还是有⼀点点区别的
PA**3*, (有序,唯⼀):!红⿊树1⾃平衡的排序⼆叉树2
再来看看! %#& !接⼝下⾯的集合。
Map
N#0O%#& :!"I5JKQ!之前!N#0O%#&!由数组R链表组成的,数组是!N#0O%#&!的主体,链表则是主
要为了解决哈希冲突⽽存在的(=拉链法>解决冲突)。"I5JKQ!以后在解决哈希冲突时有了较⼤
的变化,当链表⻓度⼤于阈值(默认为!Q)(将链表转换成红⿊树前会判断,如果当前数组的⻓
度⼩于!LS,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以
减少搜索时间
/-.7*HN#0O%#& :! /-.7*HN#0O%#& !继承⾃! N#0O%#& ,所以它的底层仍然是基于拉链式散
列结构即由数组和链表或红⿊树组成。另外, /-.7*HN#0O%#& !在上⾯结构的基础上,增加了
⼀条双向链表,使得上⾯的结构可以保持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,
实现了访问顺序相关逻辑。详细可以查看:《/-.7*HN#0O%#&!源码详细分析("I5JKQ)》
N#0O,#C)* :!数组R链表组成的,数组是!N#0O%#&!的主体,链表则是主要为了解决哈希冲突
⽽存在的
PA**%#& :!红⿊树(⾃平衡的排序⼆叉树)
如何选⽤集合?
主要根据集合的特点来选⽤,⽐如我们需要根据键值获取到元素值时就选⽤! %#& !接⼝下的集合,需
要排序时选择! PA**%#& T不需要排序时就选择! N#0O%#& T需要保证线程安全就选⽤
'(.+9AA*.,N#0O%#& 。
当我们只需要存放元素值时,就选择实现 '())*+,-(. !接⼝的集合,需要保证元素唯⼀时选择实现
3*, !接⼝的集合⽐如! PA**3*, !或! N#0O3*, ,不需要就选择实现! /-0, !接⼝的⽐如! @AA#6/-0,
或! /-.7*H/-0, ,然后再根据实现这些接⼝的集合的特点来选⽤。
为什么要使⽤集合?
当我们需要保存⼀组类型相同的数据的时候,我们应该是⽤⼀个容器来保存,这个容器就是数组,但
是,使⽤数组存储对象具有⼀定的弊端,!因为我们在实际开发中,存储的数据的类型是多种多样的,
于是,就出现了=集合>,集合同样也是⽤来存储多个数据的。
数组的缺点是⼀旦声明之后,⻓度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数
据的类型;⽽且,数组存储的数据是有序的、可重复的,特点单⼀。!但是集合提⾼了数据存储的灵活
性,"#$#!集合不仅可以⽤来存储不同类型不同数量的对象,还可以保存具有映射关系的数据
Iterator 迭代器
迭代器 Iterator 是什么?
U,*A#,(A !对象称为迭代器(设计模式的⼀种),迭代器可以对集合进⾏遍历,但每⼀个集合内部的
数据结构可能是不尽相同的,所以每⼀个集合存和取都很可能是不⼀样的,虽然我们可以⼈为地在每⼀
个类中定义! O#0V*<,12 !和! .*<,12 !⽅法,但这样做会让整个集合体系过于臃肿。于是就有了迭代
器。
迭代器是将这样的⽅法抽取出接⼝,然后在每个类的内部,定义⾃⼰迭代⽅式,这样做就规定了整个集
合体系的遍历⽅式都是! O#0V*<,12 和 .*<,12 ⽅法,使⽤者不⽤管怎么实现的,会⽤即可。迭代器的
定义为:提供⼀种⽅法访问⼀个容器对象中各个元素,⽽⼜不需要暴露该对象的内部细节。
迭代器 Iterator 有啥⽤?
U,*A#,(A !主要是⽤来遍历集合⽤的,它的特点是更加安全,因为它可以确保,在当前遍历的集合元
素被更改的时候,就会抛出! '(.+9AA*.,%(H-;-+#,-(.W<+*&,-(. !异常。
如何使⽤?
我们通过使⽤迭代器来遍历! N#0O%#& ,演示⼀下!迭代器!U,*A#,(A!的使⽤。
有哪些集合是线程不安全的?怎么解决呢?
我们常⽤的! @AA#6)-0,
T /-.7*H/-0, T N#0OX#& T N#0O3*, T PA**3*, T PA**%#& , YA-(A-,6Z9*9* !都不是线程安全的。
解决办法很简单,可以使⽤线程安全的集合来代替。
如果你要使⽤线程安全的集合的话,! D#$#K9,-)K+(.+9AA*., !包中提供了很多并发容器供你使⽤:
JK! '(.+9AA*.,N#0O%#& 4!可以看作是线程安全的! N#0O%#&
&9C)-+!-.,*A;#+*!U,*A#,(A[W\!]
!!!!^^集合中是否还有元素
!!!!C(()*#.!O#0V*<,12_
!!!!^^获得集合中的下⼀个元素
!!!!W!.*<,12_
!!!!KKKKKK
`
%#&[U.,*a*AT!3,A-.a\!X#&!:!.*b!N#0O%#&12_
X#&K&9,1JT!?"#$#?2_
X#&K&9,1cT!?'RR?2_
X#&K&9,1dT!?YNY?2_
U,*A#,(A[%#&KW.,A6[U.,*a*AT!3,A-.aef!-,*A#,(A!:!
X#&K*.,A63*,12K-,*A#,(A12_
bO-)*!1-,*A#,(AKO#0V*<,122!]
!!%#&KW.,A6[U.,*a*AT!3,A-.a\!*.,A6!:!-,*A#,(AK.*<,12_
!!360,*XK(9,K&A-.,).1*.,A6Ka*,5*612!R!*.,A6Ka*,G#)9*122_
`
cK! '(&6B.gA-,*@AA#6/-0, 4可以看作是线程安全的! @AA#6/-0, ,在读多写少的场合性能⾮常
好,远远好于! G*+,(A K
dK! '(.+9AA*.,/-.7*HZ9*9* 4⾼效的并发队列,使⽤链表实现。可以看做⼀个线程安全的
/-.7*H/-0, ,这是⼀个⾮阻塞队列。
SK! h)(+7-.aZ9*9* 4!这是⼀个接⼝,"I5!内部通过链表、数组等⽅式实现了这个接⼝。表示阻塞
队列,⾮常适合⽤于作为数据共享的通道。
iK! '(.+9AA*.,37-&/-0,%#& !4跳表的实现。这是⼀个 %#& ,使⽤跳表的数据结构进⾏快速查
找。
Collection ⼦接⼝之 List
Arraylist 和 Vector 的区别?
JK! @AA#6/-0,!是!/-0,!的主要实现类,底层使⽤!BCD*+,E!F存储,适⽤于频繁的查找⼯作,线程不
安全!;
cK! G*+,(A!是!/-0,!的古⽼实现类,底层使⽤!BCD*+,E!F存储,线程安全的。
Arraylist 与 LinkedList 区别?
JK! 是否保证线程安全:! @AA#6/-0, !和! /-.7*H/-0, !都是不同步的,也就是不保证线程安全;
cK! 底层数据结构:! @AA#6)-0, !底层使⽤的是! Object 数组; /-.7*H/-0, !底层使⽤的是!双
向链表!数据结构("I5JKL!之前为循环链表,"I5JKM!取消了循环。注意双向链表和双向循环链
表的区别,下⾯有介绍到!)
dK! 插⼊和删除是否受元素位置的影响:!j! ArrayList 采⽤数组存储,所以插⼊和删除元素的
时间复杂度受元素位置的影响。!⽐如:执⾏ #HH1W!*2 ⽅法的时候,! @AA#6/-0, !会默认在
将指定的元素追加到此列表的末尾,这种情况时间复杂度就是!B1J2。但是如果要在指定位置!-
插⼊和删除元素的话( #HH1-.,!-.H*<T!W!*)*X*.,2 )时间复杂度就为!B1.8-2。因为在进
⾏上述操作的时候集合中第!-!和第!-!个元素之后的1.8-2个元素都要执⾏向后位^向前移⼀位的
操作。!k! LinkedList 采⽤链表存储,所以对于 add(E e) ⽅法的插⼊,删除元素时间复杂
度不受元素位置的影响,近似 O(1),如果是要在指定位置 i 插⼊和删除元素的话( (add(int
index, E element) ) 时间复杂度近似为 o(n)) 因为需要先移动到指定位置再插⼊。
SK! 是否⽀持快速随机访问:! /-.7*H/-0, !不⽀持⾼效的随机元素访问,⽽! @AA#6/-0, !⽀持。
快速随机访问就是通过元素的序号快速获取元素对象1对应于 a*,1-.,!-.H*<2 ⽅法2。
iK! 内存空间占⽤:!@AA#6/-0,!的空!间浪费主要体现在在!)-0,!列表的结尾会预留⼀定的容量空
间,⽽!/-.7*H/-0,!的空间花费则体现在它的每⼀个元素都需要消耗⽐!@AA#6/-0,!更多的空间
(因为要存放直接后继和直接前驱以及数据)。
补充内容:双向链表和双向循环链表
双向链表:!包含两个指针,⼀个!&A*$!指向前⼀个节点,⼀个!.*<,!指向后⼀个节点。
另外推荐⼀篇把双向链表讲清楚的⽂章:O,,&04^^D9*D-.K-X^&(0,^iCiHJ#l#;cLiH#m;SMdic;JS
双向循环链表:!最后⼀个节点的!.*<,!指向!O*#H,⽽!O*#H!的!&A*$!指向最后⼀个节点,构成⼀个
环。
补充内容:RandomAccess 接⼝
查看源码我们发现实际上! n#.H(X@++*00 !接⼝中什么都没有定义。所以,在我看来
n#.H(X@++*00 !接⼝不过是⼀个标识罢了。标识什么?!标识实现这个接⼝的类具有随机访问功能。
在! C-.#A63*#A+O(2 !⽅法中,它要判断传⼊的!)-0,!是否! n#XH(X@++*00 !的实例,如果是,调
⽤ -.H*<*Hh-.#A63*#A+O12 ⽅法,如果不是,那么调⽤ -,*A#,(Ah-.#A63*#A+O12 ⽅法
&9C)-+!-.,*A;#+*!n#.H(X@++*00!]
`