2.ArrayList
和
LinkedList
有什么区别?
(
1
)
数据结构不同
ArrayList
基于数组实现
LinkedList
基于双向链表实现
(
2
)
多数情况下,
ArrayList
更利于查找,
LinkedList
更利于增删
ArrayList
基于数组实现,
get(int index)
可以直接通过数组下标获取,时间复杂度是
O(1)
;
LinkedList
基于链表实现,
get(int index)
需要遍历链表,时间复杂度是
O(n)
;当然,
get(E
element)
这种查找,两种集合都需要遍历,时间复杂度都是
O(n)
。
ArrayList
增删如果是数组末尾的位置,直接插⼊或者删除就可以了,但是如果插⼊中间的
位置,就需要把插⼊位置后的元素都向前或者向后移动,甚⾄还有可能触发扩容;双向链
表的插⼊和删除只需要改变前驱节点、后继节点和插⼊节点的指向就⾏了,不需要移动元
素。

注意,这个地⽅可能会出陷阱,
LinkedList
更利于增删更多是体现在平均步长上,不是体
现在时间复杂度上,⼆者增删的时间复杂度都是
O(n)
(
3
)
是否⽀持随机访问
ArrayList
基于数组,所以它可以根据下标查找,⽀持随机访问,当然,它也实现了
RandmoAccess
接⼜,这个接⼜只是⽤来标识是否⽀持随机访问。
LinkedList
基于链表,所以它没法根据序号直接获取元素,它没有实现
RandmoAccess
接
⼜,标记不⽀持随机访问。
(
4
)
内存占⽤,
ArrayList
基于数组,是⼀块连续的内存空间,
LinkedList
基于链表,内存空
间不连续,它们在空间占⽤上都有⼀些额外的消耗:
ArrayList
是预先定义好的数组,可能会有空的内存空间,存在⼀定空间浪费
LinkedList
每个节点,需要存储前驱和后继,所以每个节点会占⽤更多的空间
3.ArrayList
的扩容机制了解吗?
ArrayList
是基于数组的集合,数组的容量是在定义的时候确定的,如果数组满了,再插⼊,
就会数组溢出。所以在插⼊时候,会先检查是否需要扩容,如果当前容量
+1
超过数组长度,
就会进⾏扩容。
ArrayList
的扩容是创建⼀个
1.5
倍
的新数组,然后把原数组的值拷贝过去。
4.ArrayList
怎么序列化的知道吗? 为什么⽤
transient
修饰数组?
ArrayList
的序列化不太⼀样,它使⽤
transient
修饰存储元素的
elementData
的数
组,
transient
关键字的作⽤是让被修饰的成员属性不被序列化。
为什么最
ArrayList
不直接序列化元素数组呢?
出于效率的考虑,数组可能长度
100
,但实际只⽤了
50
,剩下的
50
不⽤其实不⽤序列化,这样
可以提⾼序列化和反序列化的效率,还可以节省内存空间。
那
ArrayList
怎么序列化呢?
ArrayList
通过两个⽅法
readObject
、
writeObject
⾃定义序列化和反序列化策略,实际直接使
⽤两个流
ObjectOutputStream
和
ObjectInputStream
来进⾏序列化和反序列化。
5.
快速失败
(fail-fast)
和安全失败
(fail-safe)
了解吗?
快速失败(
fail—fast
)
:快速失败是
Java
集合的⼀种错误检测机制
在⽤迭代器遍历⼀个集合对象时,如果线程
A
遍历过程中,线程
B
对集合对象的内容进⾏
了修改(增加、删除、修改),则会抛出
Concurrent Modification Exception
。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使⽤⼀个
modCount
变量。集合在被遍历期间如果内容发⽣变化,就会改变
modCount
的值。每当迭代器使⽤
hashNext()/next()
遍历下⼀个元素之前,都会检测
modCount
变量是否为
expectedmodCount
值,是的话就返回遍历;否则抛出异常,终⽌遍历。
注意:这⾥异常的抛出条件是检测到
modCount
!
=expectedmodCount
这个条件。如果集
合发⽣变化时修改
modCount
值刚好又设置为了
expectedmodCount
值,则异常不会抛出。因
此,不能依赖于这个异常是否抛出⽽进⾏并发操作的编程,这个异常只建议⽤于检测并发
修改的
bug
。
场景:
java.util
包下的集合类都是快速失败的,不能在多线程下发⽣并发修改(迭代过程
中被修改),⽐如
ArrayList
类。
安全失败(
fail—safe
)
采⽤安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,⽽是先复制原有
集合内容,在拷贝的集合上进⾏遍历。
原理:由于迭代时是对原集合的拷贝进⾏遍历,所以在遍历过程中对原集合所作的修改并
不能被迭代器检测到,所以不会触发
Concurrent Modification Exception
。
缺点:基于拷贝内容的优点是避免了
Concurrent Modification Exception
,但同样地,迭代
器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那⼀刻拿到的集合拷贝,在
遍历期间原集合发⽣的修改迭代器是不知道的。
场景:
java.util.concurrent
包下的容器都是安全失败,可以在多线程下并发使⽤,并发修
改,⽐如
CopyOnWriteArrayList
类。
6.
有哪⼏种实现
ArrayList
线程安全的⽅法?
fail-fast
是⼀种可能触发的机制,实际上,
ArrayList
的线程安全仍然没有保证,⼀般,保证
ArrayList
的线程安全可以通过这些⽅案:
使⽤
Vector
代替
ArrayList
。(不推荐,
Vector
是⼀个历史遗留类)
使⽤
Collections.synchronizedList
包装
ArrayList
,然后操作包装后的
list
。
使⽤
CopyOnWriteArrayList
代替
ArrayList
。
在使⽤
ArrayList
时,应⽤程序通过同步机制去控制
ArrayList
的读写。
7.CopyOnWriteArrayList
了解多少?
CopyOnWriteArrayList
就是线程安全版本的
ArrayList
。
它的名字叫
CopyOnWrite
——
写时复制,已经明⽰了它的原理。
CopyOnWriteArrayList
采⽤了⼀种读写分离的并发策略。
CopyOnWriteArrayList
容器允许并发
读,读操作是⽆锁的,性能较⾼。⾄于写操作,⽐如向容器中添加⼀个元素,则⾸先将当前
容器复制⼀份,然后在新副本上执⾏写操作,结束之后再将原容器的引⽤指向新容器。

Map
Map
中,毫⽆疑问,最重要的就是
HashMap
,⾯试基本被盘出包浆了,各种问法,⼀定要好好
准备。
8.
能说⼀下
HashMap
的数据结构吗?
JDK1.7
的数据结构是
数组
+
链表
,
JDK1.7
还有⼈在⽤?不会吧
……
说⼀下
JDK1.8
的数据结构吧:
JDK1.8
的数据结构是
数组
+
链表
+
红⿊树
。
数据结构⽰意图如下:

其中,桶数组是⽤来存储数据元素,链表是⽤来解决冲突,红⿊树是为了提⾼查询的效率。
数据元素通过映射关系,也就是散列函数,映射到桶数组对应索引的位置
如果发⽣冲突,从冲突的位置拉⼀个链表,插⼊冲突的元素
如果链表长度
>8&
数组⼤⼩
>=64
,链表转为红⿊树
如果红⿊树节点个数
<6
,转为链表
9.
你对红⿊树了解多少?为什么不⽤⼆叉树
/
平衡树呢?
红⿊树本质上是⼀种⼆叉查找树,为了保持平衡,它又在⼆叉查找树的基础上增加了⼀些规
则:
1.
每个节点要么是红⾊,要么是⿊⾊;
2.
根节点永远是⿊⾊的;
3.
所有的叶⼦节点都是是⿊⾊的(注意这⾥说叶⼦节点其实是图中的
NULL
节点);
4.
每个红⾊节点的两个⼦节点⼀定都是⿊⾊;
5.
从任⼀节点到其⼦树中每个叶⼦节点的路径都包含相同数量的⿊⾊节点;

之所以不⽤⼆叉树:
红⿊树是⼀种平衡的⼆叉树,插⼊、删除、查找的最坏时间复杂度都为
O(logn)
,避免了⼆叉
树最坏情况下的
O(n)
时间复杂度。
之所以不⽤平衡⼆叉树:
平衡⼆叉树是⽐红⿊树更严格的平衡树,为了保持保持平衡,需要旋转的次数更多,也就是
说平衡⼆叉树保持平衡的效率更低,所以平衡⼆叉树插⼊和删除的效率⽐红⿊树要低。
10.
红⿊树怎么保持平衡的知道吗?
红⿊树有两种⽅式保持平衡:
旋转
和
染⾊
。
旋转:旋转分为两种,左旋和右旋

1.
⾸先进⾏哈希值的扰动,获取⼀个新的哈希值。
(key == null) ? 0 : (h =
key.hashCode()) ^ (h >>> 16);
2.
判断
tab
是否位空或者长度为
0
,如果是则进⾏扩容操作。
公众号 【三分恶】 出品
公众号 【三分恶】 出品
关注公众号:【三分恶】,获取手册最新动态!
73
if
((
tab
=
table
)
==
null
||
(
n
=
tab
.
length
)
==
0
)
n
=
(
tab
=
resize
()).
length
;
3.
根据哈希值计算下标,如果对应⼩标正好没有存放数据,则直接插⼊即可否则需要覆
盖。
tab[i = (n - 1) & hash])
4.
判断
tab[i]
是否为树节点,否则向链表中插⼊数据,是则向树中插⼊节点。
5.
如果链表中插⼊节点的时候,链表长度⼤于等于
8
,则需要把链表转换为红⿊
树。
treeifyBin(tab, hash);
6.
最后所有元素处理完成后,判断是否超过阈值;
threshold
,超过则扩容。


14.
为什么哈希
/
扰动函数能降
hash
碰撞?
因为
key.hashCode()
函数调⽤的是
key
键值类型⾃带的哈希函数,返回
int
型散列值。
int
值
范围为
-2147483648~2147483647
,加起来⼤概
40
亿的映射空间。
只要哈希函数映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个
40
亿长度的
数组,内存是放不下的。
假如
HashMap
数组的初始⼤⼩才
16
,就需要⽤之前需要对数组的长度取模运算,得到的余数
才能⽤来访问数组下标。
源码中模运算就是把散列值和数组长度
- 1
做⼀个
"
与
&
"
操作,位运算⽐取余
%
运算要快。
bucketIndex
=
indexFor(hash, table.length);
static
int
indexFor
(
int
h,
int
length) {
return
h
&
(length
-
1
);
}
顺便说⼀下,这也正好解释了为什么
HashMap
的数组长度要取
2
的整数幂。因为这样(数组
长度
- 1
)正好相当于⼀个
“
低位掩码
”
。
与
操作的结果就是散列值的⾼位全部归零,只保留
低位值,⽤来做数组下标访问。以初始长度
16
为例,
16-1=15
。
2
进制表⽰是
0000 0000
0000 0000 0000 0000 0000 1111
。和某个散列值做
与
操作如下,结果就是截取了最低
的四位值。
这样是要快捷⼀些,但是新的问题来了,就算散列值分布再松散,要是只取最后⼏位的话,
碰撞也会很严重。如果散列本⾝做得不好,分布上成等差数列的漏洞,如果正好让最后⼏个
低位呈现规律性重复,那就更难搞了。
这时候
扰动函数
的价值就体现出来了,看⼀下扰动函数的⽰意图:
右移
16
位,正好是
32bit
的⼀半,⾃⼰的⾼半区和低半区做异或,就是为了混合原始哈希码
的⾼位和低位,以此来加⼤低位的随机性。⽽且混合后的低位掺杂了⾼位的部分特征,这样
⾼位的信息也被变相保留下来。
15.
为什么
HashMap
的容量是
2
的倍数呢?
第⼀个原因是为了⽅便哈希取余:
将元素放在
table
数组上⾯,是⽤
hash
值
%
数组⼤⼩定位位置,⽽
HashMap
是⽤
hash
值
&(
数组⼤
⼩
-1)
,却能和前⾯达到⼀样的效果,这就得益于
HashMap
的⼤⼩是
2
的倍数,
2
的倍数意味着
该数的⼆进制位只有⼀位为
1
,⽽该数
-1
就可以得到⼆进制位上
1
变成
0
,后⾯的
0
变成
1
,再通
过
&
运算,就可以得到和
%
⼀样的效果,并且位运算⽐
%
的效率⾼得多
HashMap
的容量是
2
的
n
次幂时,
(n-1)
的
2
进制也就是
1111111***111
这样形式的,这样与添加元
素的
hash
值进⾏位运算时,能够充分的散列,使得添加的元素均匀分布在
HashMap
的每个位置
上,减少
hash
碰撞。
第⼆个⽅⾯是在扩容时,利⽤扩容后的⼤⼩也是
2
的倍数,将已经产⽣
hash
碰撞的元素完
美的转移到新的
table
中去
我们可以简单看看
HashMap
的扩容机制,
HashMap
中的元素在超过
负载因⼦
*HashMap
⼤⼩时
就会产⽣扩容。
16.
如果初始化
HashMap
,传⼀个
17
的值
new HashMap<> new HashMap<>
,它会怎么处
理?
简单来说,就是初始化时,传的不是
2
的倍数时,
HashMap
会向上寻找
离得最近的
2
的倍数
,
所以传⼊
17
,但
HashMap
的实际容量是
32
。
我们来看看详情,在
HashMap
的初始化中,有这样⼀段⽅法;
public
HashMap(
int
initialCapacity,
float
loadFactor) {
...
this
.loadFactor
=
loadFactor;
this
.threshold
=
tableSizeFor(initialCapacity);
}
阀值
threshold
,通过⽅法
tableSizeFor
进⾏计算,是根据初始化传的参数来计算的。
78
同时,这个⽅法也要要寻找⽐初始值⼤的,最⼩的那个
2
进制数值。⽐如传了
17
,我应该
找到的是
32
。
static final
int
tableSizeFor
(
int
cap) {
int
n
=
cap
-
1
;
n
|=
n
>>>
1
;
n
|=
n
>>>
2
;
n
|=
n
>>>
4
;
n
|=
n
>>>
8
;
n
|=
n
>>>
16
;
return
(n
<
0
)
?
1
: (n
>=
MAXIMUM_CAPACITY)
?
MAXIMUM_CAPACITY : n
+
1
;
}
MAXIMUM_CAPACITY = 1 << 30
,这个是临界范围,也就是最⼤的
Map
集合。
计算过程是向右移位
1
、
2
、
4
、
8
、
16
,和原来的数做
|
运算,这主要是为了把⼆进制的
各个位置都填上
1
,当⼆进制的各个位置都是
1
以后,就是⼀个标准的
2
的倍数减
1
了,最后
把结果加
1
再返回即可。
以
17
为例,看⼀下初始化计算
table
容量的过程:

17.
你还知道哪些哈希函数的构造⽅法呢?
HashMap
⾥哈希构造函数的⽅法叫:
除留取余法 :
H
(
key)=key%p
(
p<=N
)
,
关键字除以⼀个不⼤于哈希表长度的正整数
p
,
所得余数为地址,当然
HashMap
⾥进⾏了优化改造,效率更⾼,散列也更均衡。
除此之外,还有这⼏种常见的哈希函数构造⽅法:
直接定址法
直接根据
key
来映射到对应的数组位置,例如
1232
放到下标
1232
的位置。
数字分析法
取
key
的某些数字(例如⼗位和百位)作为映射的位置
平⽅取中法
取
key
平⽅的中间⼏位作为映射的位置
折叠法
将
key
分割成位数相同的⼏段,然后把它们的叠加和作为映射的位置
18.
解决哈希冲突有哪些⽅法呢?
我们到现在已经知道,
HashMap
使⽤链表的原因为了处理哈希冲突,这种⽅法就是所谓的:
链地址法 :在冲突的位置拉⼀个链表,把冲突的元素放进去。
除此之外,还有⼀些常见的解决冲突的办法:
开放定址法
:开放定址法就是从冲突的位置再接着往下找,给冲突元素找个空位。
找到空闲位置的⽅法也有很多种:

线⾏探查法
:
从冲突的位置开始,依次判断下⼀个位置是否空闲,直⾄找到空闲位置
平⽅探查法
:
从冲突的位置
x
开始,第⼀次增加
1^2
个位置,第⼆次增加
2^2
…
,直
⾄找到空闲的位置
……
再哈希法 :换种哈希函数,重新计算冲突元素的地址。
建⽴公共溢出区 :再建⼀个数组,把冲突的元素放进去。
19.
为什么
HashMap
链表转红⿊树的阈值为
8
呢?
树化发⽣在
table
数组的长度⼤于
64
,且链表的长度⼤于
8
的时候。
为什么是
8
呢?源码的注释也给出了答案。
红⿊树节点的⼤⼩⼤概是普通节点⼤⼩的两倍,所以转红⿊树,牺牲了空间换时间,更多的
是⼀种兜底的策略,保证极端情况下的查找效率。
阈值为什么要选
8
呢?和统计学有关。理想情况下,使⽤随机哈希码,链表⾥的节点符合泊松
分布,出现节点个数的概率是递减的,节点个数为
8
的情况,发⽣概率仅为
0.00000006
。
⾄于红⿊树转回链表的阈值为什么是
6
,⽽不是
8
?是因为如果这个阈值也设置成
8
,假如发⽣
碰撞,节点增减刚好在
8
附近,会发⽣链表和红⿊树的不断转换,导致资源浪费。
20.
扩容在什么时候呢?为什么扩容因⼦是
0.75
?
为了减少哈希冲突发⽣的概率,当当前
HashMap
的元素个数达到⼀个临界值的时候,就会触
发扩容,把所有元素
rehash
之后再放在扩容后的容器中,这是⼀个相当耗时的操作。
⽽这个
临界值
threshold
就是由加载因⼦和当前容器的容量⼤⼩来确定的,假如采⽤默认的
构造⽅法:
临界值(
threshold
)
=
默认容量(
DEFAULT_INITIAL_CAPACITY
)
*
默认扩容因⼦
(
DEFAULT_LOAD_FACTOR
)
那就是⼤于
16x0.75=12
时,就会触发扩容操作。
那么为什么选择了
0.75
作为
HashMap
的默认加载因⼦呢?
简单来说,这是对
空间
成本和
时间
成本平衡的考虑。
在
HashMap
中有这样⼀段注释:
我们都知道,
HashMap
的散列构造⽅式是
Hash
取余,负载因⼦决定元素个数达到多少时候扩
容。
假如我们设的⽐较⼤,元素⽐较多,空位⽐较少的时候才扩容,那么发⽣哈希冲突的概率就
增加了,查找的时间成本就增加了。
我们设的⽐较⼩的话,元素⽐较少,空位⽐较多的时候就扩容了,发⽣哈希碰撞的概率就降
低了,查找时间成本降低,但是就需要更多的空间去存储元素,空间成本就增加了。
22.jdk1.8
对
HashMap
主要做了哪些优化呢?为什么?
jdk1.8
的
HashMap
主要有五点优化:
1.
数据结构
:数组
+
链表改成了数组
+
链表或红⿊树
原因
:发⽣
hash
冲突,元素会存⼊链表,链表过长转为红⿊树,将时间复杂度由
O(n)
降为
O(logn)
2.
链表插⼊⽅式
:链表的插⼊⽅式从头插法改成了尾插法
简单说就是插⼊时,如果数组位置上已经有元素,
1.7
将新元素放到数组中,原始节点作
为新节点的后继节点,
1.8
遍历链表,将元素放置到链表的最后。
原因
:因为
1.7
头插法扩容时,头插法会使链表发⽣反转,多线程环境下会产⽣环。
3.
扩容
rehash
:扩容的时候
1.7
需要对原数组中的元素进⾏重新
hash
定位在新数组的位
置,
1.8
采⽤更简单的判断逻辑,不需要重新通过哈希函数计算位置,新的位置不变或索
引
+
新增容量⼤⼩。
原因:
提⾼扩容的效率,更快地扩容。
4.
扩容时机
:在插⼊时,
1.7
先判断是否需要扩容,再插⼊,
1.8
先进⾏插⼊,插⼊完成再
判断是否需要扩容;
5.
散列函数
:
1.7
做了四次移位和四次异或,
jdk1.8
只做⼀次。
原因
:做
4
次的话,边际效⽤也不⼤,改为⼀次,提升效率。
24.HashMap
是线程安全的吗?多线程下会有什么问题?
HashMap
不是线程安全的,可能会发⽣这些问题:
多线程下扩容死循环。
JDK1.7
中的
HashMap
使⽤头插法插⼊元素,在多线程的环境下,
扩容的时候有可能导致环形链表的出现,形成死循环。因此,
JDK1.8
使⽤尾插法插⼊元
素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
多线程的
put
可能导致元素的丢失。多线程同时执⾏
put
操作,如果计算出来的索引位置
是相同的,那会造成前⼀个
key
被后⼀个
key
覆盖,从⽽导致元素的丢失。此问题在
JDK
1.7
和
JDK 1.8
中都存在。
put
和
get
并发时,可能导致
get
为
null
。线程
1
执⾏
put
时,因为元素个数超出
threshold
⽽导致
rehash
,线程
2
此时执⾏
get
,有可能导致这个问题。这个问题在
JDK 1.7
和
JDK
1.8
中都存在。
25.
有什么办法能解决
HashMap
线程不安全的问题呢?
Java
中有
HashTable
、
Collections.synchronizedMap
、以及
ConcurrentHashMap
可以实现线程安
全的
Map
。
HashTable
是直接在操作⽅法上加
synchronized
关键字,锁住整个
table
数组,粒度⽐较
⼤;
Collections.synchronizedMap
是使⽤
Collections
集合⼯具的内部类,通过传⼊
Map
封装出
⼀个
SynchronizedMap
对象,内部定义了⼀个对象锁,⽅法内通过对象锁实现;
ConcurrentHashMap
在
jdk1.7
中使⽤分段锁,在
jdk1.8
中使⽤
CAS+synchronized
。
26.
能具体说⼀下
ConcurrentHashmap
的实现吗?
ConcurrentHashmap
线程安全在
jdk1.7
版本是基于
分段锁
实现,在
jdk1.8
是基于
CAS+synchronized
实现。
1.7
分段锁
从结构上说,
1.7
版本的
ConcurrentHashMap
采⽤分段锁机制,⾥⾯包含⼀个
Segment
数组,
Segment
继承于
ReentrantLock
,
Segment
则包含
HashEntry
的数组,
HashEntry
本⾝就是⼀个链表
的结构,具有保存
key
、
value
的能⼒能指向下⼀个节点的指针。
实际上就是相当于每个
Segment
都是⼀个
HashMap
,默认的
Segment
长度是
16
,也就是⽀持
16
个线程的并发写,
Segment
之间相互不会受到影响。

整个流程和
HashMap
⾮常类似,只不过是先定位到具体的
Segment
,然后通过
ReentrantLock
去
操作⽽已,后⾯的流程,就和
HashMap
基本上是⼀样的。
1.
计算
hash
,定位到
segment
,
segment
如果是空就先初始化
2.
使⽤
ReentrantLock
加锁,如果获取锁失败则尝试⾃旋,⾃旋超过次数就阻塞获取,保证⼀
定获取锁成功
3.
遍历
HashEntry
,就是和
HashMap
⼀样,数组中
key
和
hash
⼀样就直接替换,不存在就再插
⼊链表,链表同样操作
get
流程
get
也很简单,
key
通过
hash
定位到
segment
,再遍历链表定位到具体的元素上,需要注意的是
value
是
volatile
的,所以
get
是不需要加锁的。
1.8 CAS+synchronized
jdk1.8
实现线程安全不是在数据结构上下功夫,它的数据结构和
HashMap
是⼀样的,数组
+
链
表
+
红⿊树。它实现线程安全的关键点在于
put
流程。
put
流程
1.
⾸先计算
hash
,遍历
node
数组,如果
node
是空的话,就通过
CAS+
⾃旋的⽅式初始化

27.HashMap
内部节点是有序的吗?
HashMap
是⽆序的,根据
hash
值随机插⼊。如果想使⽤有序的
Map
,可以使⽤
LinkedHashMap
或者
TreeMap
。
28.
讲讲
LinkedHashMap
怎么实现有序的?
LinkedHashMap
维护了⼀个双向链表,有头尾节点,同时
LinkedHashMap
节点
Entry
内部除了
继承
HashMap
的
Node
属性,还有
before
和
after
⽤于标识前置节点和后置节点。
可以实现按插⼊的顺序或访问顺序排序。

29.
讲讲
TreeMap
怎么实现有序的?
TreeMap
是按照
Key
的⾃然顺序或者
Comprator
的顺序进⾏排序,内部是通过红⿊树来实
现。所以要么
key
所属的类实现
Comparable
接⼜,或者⾃定义⼀个实现了
Comparator
接⼜
的⽐较器,传给
TreeMap
⽤于
key
的⽐较。
30.
讲讲
HashSet
的底层实现?
HashSet
底层就是基于
HashMap
实现的。(
HashSet
的源码⾮常⾮常少,因为除了
clone()
、
writeObject()
、
readObject()
是
HashSet
⾃⼰不得不实现之外,其他⽅法都是直接调⽤
HashMap
中的⽅法。
HashSet
的
add
⽅法,直接调⽤
HashMap
的
put
⽅法,将添加的元素作为
key
,
new
⼀个
Object
作为
value
,直接调⽤
HashMap
的
put
⽅法,它会根据返回值是否为空来判断是否插⼊元素成功。