集合容器概述
1.
什么是集合
集合就是一个放数据的容器,准确的说是放数据对象引用的容器
集合类存放的都是对象的引用,而不是对象的本身
集合类型主要有
3
种:
set(
集)、
list(
列表)和
map(
映射
)
。
2.
集合的特点
集合的特点主要有如下两点:
集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。
和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小
3.
集合和数组的区别
数组是固定长度的;集合可变长度的。
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
4.
使用集合框架的好处
1.
容量自增长;
2.
提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
3.
可以方便地扩展或改写集合,提高代码复用性和可操作性。
4.
通过使用
JDK
自带的集合类,可以降低代码维护和学习新
API
成本。
5.
常用的集合类有哪些?
Map
接口和
Collection
接口是所有集合框架的父接口:
1. Collection
接口的子接口包括:
Set
接口和
List
接口
2. Map
接口的实现类主要有:
HashMap
、
TreeMap
、
Hashtable
、
ConcurrentHashMap
以及
Properties
等
3. Set
接口的实现类主要有:
HashSet
、
TreeSet
、
LinkedHashSet
等
4. List
接口的实现类主要有:
ArrayList
、
LinkedList
、
Stack
以及
Vector
等
6. List
,
Set
,
Map
三者的区别?
Java
容器分为
Collection
和
Map
两大类,
Collection
集合的子接口有
Set
、
List
、
Queue
三种子接
口。我们比较常用的是
Set
、
List
,
Map
接口不是
collection
的子接口。
Collection
集合主要有
List
和
Set
两大接口
List
:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多
个
null
元素,元素都有索引。常用的实现类有
ArrayList
、
LinkedList
和
Vector
。
Set
:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一
个
null
元素,必须保证元素唯一性。
Set
接口常用实现类是
HashSet
、
LinkedHashSet
以及
TreeSet
。
Map
是一个键值对集合,存储键、值和之间的映射。
Key
无序,唯一;
value
不要求有序,允许重
复。
Map
没有继承于
Collection
接口,从
Map
集合中检索元素时,只要给出键对象,就会返回对应
的值对象。
Map
的常用实现类:
HashMap
、
TreeMap
、
HashTable
、
LinkedHashMap
、
ConcurrentHashMap
7.
集合框架底层数据结构
Collection
1. List
Arraylist
:
Object
数组
Vector
:
Object
数组
LinkedList
: 双向循环链表
2. Set
HashSet
(无序,唯一):基于
HashMap
实现的,底层采用
HashMap
来保存元素
LinkedHashSet
:
LinkedHashSet
继承与
HashSet
,并且其内部是通过
LinkedHashMap
来
实现的。有点类似于我们之前说的
LinkedHashMap
其内部是基于
Hashmap
实现一样,不
过还是有一点点区别的。
TreeSet
(有序,唯一): 红黑树
(
自平衡的排序二叉树。
)
Map
HashMap
:
JDK1.8
之前
HashMap
由数组
+
链表组成的,数组是
HashMap
的主体,链表则是
主要为了解决哈希冲突而存在的(
“
拉链法
”
解决冲突)
.JDK1.8
以后在解决哈希冲突时有了较
大的变化,当链表长度大于阈值(默认为
8
)时,将链表转化为红黑树,以减少搜索时间
LinkedHashMap
:
LinkedHashMap
继承自
HashMap
,所以它的底层仍然是基于拉链式散
列结构即由数组和链表或红黑树组成。另外,
LinkedHashMap
在上面结构的基础上,增加
了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的
操作,实现了访问顺序相关逻辑。
HashTable
: 数组
+
链表组成的,数组是
HashMap
的主体,链表则是主要为了解决哈希冲突
而存在的
TreeMap
: 红黑树(自平衡的排序二叉树)
8.
哪些集合类是线程安全的?
Vector
:就比
Arraylist
多了个
synchronized
(线程安全),因为效率较低,现在已经不太建议使
用。
hashTable
:就比
hashMap
多了个
synchronized (
线程安全
)
,不建议使用。
ConcurrentHashMap
:是
Java5
中支持高并发、高吞吐量的线程安全
HashMap
实现。它由
Segment
数组结构和
HashEntry
数组结构组成。
Segment
数组在
ConcurrentHashMap
里扮演锁的
角色,
HashEntry
则用于存储键
-
值对数据。一个
ConcurrentHashMap
里包含一个
Segment
数组,
Segment
的结构和
HashMap
类似,是一种数组和链表结构;一个
Segment
里包含一个
HashEntry
数组,每个
HashEntry
是一个链表结构的元素;每个
Segment
守护着一个
HashEntry
数组里的元
素,当对
HashEntry
数组的数据进行修改时,必须首先获得它对应的
Segment
锁。(推荐使用)
...
9. Java
集合的快速失败机制
“fail-fast”
?
是
java
集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生
fail-fast
机制。
例如:假设存在两个线程(线程
1
、线程
2
),线程
1
通过
Iterator
在遍历集合
A
中的元素,在某个时
候线程
2
修改了集合
A
的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这
个时候程序就会抛出
ConcurrentModifificationException
异常,从而产生
fail-fast
机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个
modCount
变量。集
合在被遍历期间如果内容发生变化,就会改变
modCount
的值。每当迭代器使用
hashNext()/next()
遍历下一个元素之前,都会检测
modCount
变量是否为
expectedmodCount
值,是的话就返回遍
历;否则抛出异常,终止遍历。
解决办法:
1.
在遍历过程中,所有涉及到改变
modCount
值得地方全部加上
synchronized
。
2.
使用
CopyOnWriteArrayList
来替换
ArrayList
10.
怎么确保一个集合不能被修改?
可以使用
Collections. unmodififiableCollection(Collection c)
方法来创建一个只读集合,这样改变
集合的任何操作都会抛出
Java. lang. UnsupportedOperationException
异常。
示例代码如下:
Collection
接口
List
接口
11.
迭代器
Iterator
是什么?
Iterator
接口提供遍历任何
Collection
的接口。我们可以从一个
Collection
中使用迭代器方法来
获取迭代器实例。迭代器取代了
Java
集合框架中的
Enumeration
,迭代器允许调用者在迭代过程
中移除元素。
因为所有
Collection
接继承了
Iterator
迭代器
12. Iterator
怎么使用?有什么特点?
Iterator
使用代码如下:
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); //
运行时此行报错
System. out. println(list. size());
List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
String obj = it. next();
System. out. println(obj);
}
Iterator
的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改
的时候,就会抛出
ConcurrentModifificationException
异常。
13.
如何边遍历边移除
Collection
中的元素?
边遍历边修改
Collection
的唯一正确方式是使用
Iterator.remove()
方法,如下:
一种最常见的
错误
代码如下:
运行以上错误代码会报
ConcurrentModifificationException
异常
。这是因为当使用
foreach(for(Integer i : list))
语句时,会自动生成一个
iterator
来遍历该
list
,但同时该
list
正在被
Iterator.remove()
修改。
Java
一般不允许一个线程在遍历
Collection
时另一个线程修改它。
14. Iterator
和
ListIterator
有什么区别?
Iterator
可以遍历
Set
和
List
集合,而
ListIterator
只能遍历
List
。
Iterator
只能单向遍历,而
ListIterator
可以双向遍历(向前
/
后遍历)。
ListIterator
实现
Iterator
接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元
素、获取前面或后面元素的索引位置。
15.
遍历一个
List
有哪些不同的方式?每种方法的实现原理是什么?
Java
中
List
遍历的最佳实践是什么?
遍历方式有以下几种:
1. for
循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,
当读取到最后一个元素后停止。
2.
迭代器遍历,
Iterator
。
Iterator
是面向对象的一个设计模式,目的是屏蔽不同数据集合的特
点,统一遍历集合的接口。
Java
在
Collections
中支持了
Iterator
模式。
3. foreach
循环遍历。
foreach
内部也是采用了
Iterator
的方式实现,使用时不需要显式声明
Iterator
或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过
程中操作数据集合,例如删除、替换。
最佳实践:
Java Collections
框架中提供了一个
RandomAccess
接口,用来标记
List
实现是否支
持
Random Access
。
如果一个数据集合实现了该接口,就意味着它支持
Random Access
,按位置读取元素的平均
时间复杂度为
O(1)
,如
ArrayList
。
如果没有实现该接口,表示不支持
Random Access
,如
LinkedList
。
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
*// do something*
it.remove();
}
for(Integer i : list){
list.remove(i)
}
推荐的做法就是,支持
Random Access
的列表可用
for
循环遍历,否则建议用
Iterator
或
foreach
遍历。
16.
说一下
ArrayList
的优缺点
ArrayList
的优点如下:
ArrayList
底层以数组实现,是一种随机访问模式。
ArrayList
实现了
RandomAccess
接口,
因此查找的时候非常快。
ArrayList
在顺序添加一个元素的时候非常方便。
ArrayList
的缺点如下:
删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性
能。
插入元素的时候,也需要做一次元素复制操作,缺点同上。
ArrayList
比较适合顺序添加、随机访问的场景。
17.
如何实现数组和
List
之间的转换?
数组转
List
:使用
Arrays. asList(array)
进行转换。
List
转数组:使用
List
自带的
toArray()
方法。
代码示例:
18. ArrayList
和
LinkedList
的区别是什么?
数据结构实现:
ArrayList
是动态数组的数据结构实现,而
LinkedList
是双向链表的数据结构实
现。
随机访问效率:
ArrayList
比
LinkedList
在随机访问的时候效率要高,因为
LinkedList
是线性的数
据存储方式,所以需要移动指针从前往后依次查找。
增加和删除效率:在非首尾的增加和删除操作,
LinkedList
要比
ArrayList
效率要高,因为
ArrayList
增删操作要影响数组内的其他数据的下标。
内存空间占用:
LinkedList
比
ArrayList
更占内存,因为
LinkedList
的节点除了存储数据,还存储
了两个引用,一个指向前一个元素,一个指向后一个元素。
线程安全:
ArrayList
和
LinkedList
都是不同步的,也就是不保证线程安全;
综合来说,在需要频繁读取集合中的元素时,更推荐使用
ArrayList
,而在插入和删除操作较多
时,更推荐使用
LinkedList
。
LinkedList
的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向
直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结
点和后继结点。
// list to array
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();
// array to list
String[] array = new String[]{"123","456"};
Arrays.asList(array);
19. ArrayList
和
Vector
的区别是什么?
这两个类都实现了
List
接口(
List
接口继承了
Collection
接口),他们都是有序集合
线程安全:
Vector
使用了
Synchronized
来实现线程同步,是线程安全的,而
ArrayList
是非
线程安全的。
性能:
ArrayList
在性能方面要优于
Vector
。
扩容:
ArrayList
和
Vector
都会根据实际的需要动态的调整容量,只不过在
Vector
扩容每次
会增加
1
倍,而
ArrayList
只会增加
50%
。
Vector
类的所有方法都是同步的。可以由两个线程安全地访问一个
Vector
对象、但是一个线程访问
Vector
的话代码要在同步操作上耗费大量的时间。
Arraylist
不是同步的,所以在不需要保证线程安全时时建议使用
Arraylist
。
20.
插入数据时,
ArrayList
、
LinkedList
、
Vector
谁速度较快?阐述
ArrayList
、
Vector
、
LinkedList
的存储性能和特性?
ArrayList
和
Vector
底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便
增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操
作,所以索引数据快而插入数据慢。
Vector
中的方法由于加了
synchronized
修饰,因此
Vector
是线程安全容器,但性能上较
ArrayList
差
。
LinkedList
使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需
要记录当前项的前后项即可,所以
LinkedList
插入速度较快
。
21.
多线程场景下如何使用
ArrayList
?
ArrayList
不是线程安全的,如果遇到多线程场景,可以通过
Collections
的
synchronizedList
方
法将其转换成线程安全的容器后再使用。例如像下面这样:
22.
为什么
ArrayList
的
elementData
加上
transient
修饰?
ArrayList
中的数组定义如下:
private transient Object[] elementData;
再看一下
ArrayList
的定义:
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++) {
System.out.println(synchronizedList.get(i));
}
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
可以看到
ArrayList
实现了
Serializable
接口,这意味着
ArrayList
支持序列化。
transient
的作用
是说不希望
elementData
数组被序列化,重写了
writeObject
实现:
每次序列化时,先调用
defaultWriteObject()
方法序列化
ArrayList
中的非
transient
元素,然后
遍历
elementData
,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后
的文件大小。
23. List
和
Set
的区别
List , Set
都是继承自
Collection
接口
List
特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多
个
null
元素,元素都有索引。常用的实现类有
ArrayList
、
LinkedList
和
Vector
。
Set
特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一
个
null
元素,必须保证元素唯一性。
Set
接口常用实现类是
HashSet
、
LinkedHashSet
以及
TreeSet
。
另外
List
支持
for
循环,也就是通过下标来遍历,也可以用迭代器,但是
set
只能用迭代,因为他无
序,无法用下标来取得想要的值。
Set
和
List
对比
Set
:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List
:和数组类似,
List
可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起
其他元素位置改变
Set
接口
24.
说一下
HashSet
的实现原理?
HashSet
是基于
HashMap
实现的,
HashSet
的值存放于
HashMap
的
key
上,
HashMap
的
value
统
一为
present
,因此
HashSet
的实现比较简单,相关
HashSet
的操作,基本上都是直接调用底层
HashMap
的相关方法来完成,
HashSet
不允许重复的值。
25. HashSet
如何检查重复?
HashSet
是如何保证数据不可重复的?
向
HashSet
中
add ()
元素时,判断元素是否存在的依据,不仅要比较
hash
值,同时还要结合
equles
方法比较。
HashSet
中的
add ()
方法会使用
HashMap
的
put()
方法。
private void writeObject(java.io.ObjectOutputStream s) throws
java.io.IOException{
*// Write out element count, and any hidden stuff*
int expectedModCount = modCount;
s.defaultWriteObject();
*// Write out array length*
s.writeInt(elementData.length);
*// Write out all elements in the proper order.*
for (int i=0; i<size; i++)
s.writeObject(elementData[i]);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
HashMap
HashSet
实现了
Map
接口
实现
Set
接口
存储键值对
仅存储对象
调用
put
()向
map
中
添加元素
调用
add
()方法向
Set
中添加元素
HashMap
使用键
(
Key
)计算
Hashcode
HashSet
使用成员对象来计算
hashcode
值,对于两个对象来说
hashcode
可能相同,所以
equals()
方法用来判断对象的相等性,
如果两个对象不同的话,那么返回
false
HashMap
相对于
HashSet
较快,因为它
是使用唯一的键获取对
象
HashSet
较
HashMap
来说比较慢
HashMap
的
key
是唯一的,由源码可以看出
HashSet
添加进去的值就是作为
HashMap
的
key
,
并且在
HashMap
中如果
K/V
相同时,会用新的
V
覆盖掉旧的
V
,然后返回旧的
V
。所以不会重复(
HashMap
比较
key
是否相等是先比较
hashcode
再比较
equals
)。
以下是
HashSet
部分源码:
hashCode
()与
equals
()的相关规定
:
1.
如果两个对象相等,则
hashcode
一定也是相同的
hashCode
是
jdk
根据对象的地址或者字符串或者数字算出来的
int
类型的数值
2.
两个对象相等
,
对两个
equals
方法返回
true
3.
两个对象有相同的
hashcode
值,它们也不一定是相等的
4.
综上,
equals
方法被覆盖过,则
hashCode
方法也必须被覆盖
5. hashCode()
的默认行为是对堆上的对象产生独特值。如果没有重写
hashCode()
,则该
class
的两个
对象无论如何都不会相等(即使这两个对象指向相同的数据)。
==
与
equals
的区别
1. ==
是判断两个变量或实例是不是指向同一个内存空间
equals
是判断两个变量或实例所指向的内存
空间的值是不是相同
2. ==
是指对内存地址进行比较
equals()
是对字符串的内容进行比较
26. HashSet
与
HashMap
的区别
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
//
调用
HashMap
的
put
方法
,PRESENT
是一个至始至终都相同的虚值
return map.put(e, PRESENT)==null;
}
Map
接口
27.
什么是
Hash
算法
哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈
希值。
28.
什么是链表
链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查
等功能。
链表大致分为单链表和双向链表
1.
单链表
:
每个节点包含两部分
,
一部分存放数据变量的
data,
另一部分是指向下一节点的
next
指
针
2.
双向链表
:
除了包含单链表的部分
,
还增加的
pre
前一个节点的指针
链表的优点
插入删除速度快(因为有
next
指针指向其下一个节点,通过改变指针的指向可以方便的增加
删除元素)
内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于
node
节点的大
小),并且在需要空间的时候才创建空间)
大小没有固定,拓展很灵活。
链表的缺点
不能随机查找,必须从第一个开始遍历,查找效率低
29.
说一下
HashMap
的实现原理?
HashMap
概述:
HashMap
是基于哈希表的
Map
接口的非同步实现。此实现提供所有可选的映射
操作,并允许使用
null
值和
null
键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap
的数据结构: 在
Java
编程语言中,最基本的结构就是两种,一个是数组,另外一个是模
拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,
HashMap
也不例外。
HashMap
实际上是一个
“
链表散列
”
的数据结构,即数组和链表的结合体。
HashMap
基于
Hash
算法实现的
1.
当我们往
HashMap
中
put
元素时,利用
key
的
hashCode
重新
hash
计算出当前对象的元素在数
组中的下标
2.
存储时,如果出现
hash
值相同的
key
,此时有两种情况。
(1)
如果
key
相同,则覆盖原始值;
(2)
如果
key
不同(出现冲突),则将当前的
key-value
放入链表中
3.
获取时,直接找到
hash
值对应的下标,在进一步判断
key
是否相同,从而找到对应值。
4.
理解了以上过程就不难明白
HashMap
是如何解决
hash
冲突的问题,核心就是使用了数组的
存储方式,然后将冲突的
key
的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
需要注意
Jdk 1.8
中对
HashMap
的实现做了优化,当链表中的节点数据超过八个之后,该链表会转
为红黑树来提高查询效率,从原来的
O(n)
到
O(logn)
30. HashMap
在
JDK1.7
和
JDK1.8
中有哪些不同?
HashMap
的底层
实现
在
Java
中,保存数据有两种比较简单的数据结构:数组和链表。
数组的特点是:寻址容易,插入和
删除困难;链表的特点是:寻址困难,但插入和删除容易;
所以我们将数组和链表结合在一起,发
挥两者各自的优势,使用一种叫做
拉链法
的方式可以解决哈希冲突。
HashMap JDK1.8
之前
JDK1.8
之前采用的是拉链法。
拉链法
:将链表和数组相结合。也就是说创建一个链表数组,数组
中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
HashMap JDK1.8
之后
相比于之前的版本,
jdk1.8
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为
8
)
时,将链表转化为红黑树,以减少搜索时间。
JDK1.7 VS JDK1.8
比较
JDK1.8
主要解决或优化了一下问题:
1. resize
扩容优化
2.
引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
3.
解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
不同
JDK 1.7
JDK 1.8
存储结构
数组
+
链表
数组
+
链表
+
红黑树
初始化方
式
单独函数:
inflateTable()
直接集成到了扩容函数
resize()
中
hash
值
计算方式
扰动处理
= 9
次扰动
= 4
次位运
算
+ 5
次异或运算
扰动处理
= 2
次扰动
= 1
次位运算
+ 1
次异
或运算
存放数据
的规则
无冲突时,存放数组;冲突
时,存放链表
无冲突时,存放数组;冲突
&
链表长度
<
8
:存放单链表;冲突
&
链表长度
> 8
:
树化并存放红黑树
插入数据
方式
头插法(先讲原位置的数据移
到后
1
位,再插入数据到该位
置)
尾插法(直接插入到链表尾部
/
红黑树)
扩容后存
储位置的
计算方式
全部按照原来方法进行计算
(即
hashCode ->>
扰动函数
-
>> (h&length-1)
)
按照扩容后的规律计算(即扩容后的位置
=
原位置
or
原位置
+
旧容量)
31.
什么是红黑树
说道红黑树先讲什么是二叉树
二叉树简单来说就是 每一个节上可以关联俩个子节点
红黑树
红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红
(Red)
或黑
(Black)
。
红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点
代表终结、结尾的节点)也是黑色
[
注意:这里叶子结点,是指为空
(NIL
或
NULL)
的叶子结点!
]
。
如果一个结点是红色的,则它的子结点必须是黑色的。
每个结点到叶子结点
NIL
所经过的黑色结点的个数一样的。
[
确保没有一条路径会比其他路径长出俩
倍,所以红黑树是相对接近平衡的二叉树的!
]
红黑树的基本操作是
添加、删除
。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么
呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面
三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新
成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。
大概就是这样子:
a
/ \
b c
/ \ / \
d e f g
/ \ / \ / \ / \
h i j k l m n o
32. HashMap
的
put
方法的具体流程?
当我们
put
的时候,首先计算
key
的
hash
值,这里调用了
hash
方法,
hash
方法实际是让
key.hashCode()
与
key.hashCode()>>>16
进行异或操作,高
16bit
补
0
,一个数和
0
异或不变,
所以
hash
函数大概的作用就是:
高
16bit
不变,低
16bit
和高
16bit
做了一个异或,目的是减少碰
撞
。按照函数注释,因为
bucket
数组大小是
2
的幂,计算下标
index = (table.length
-
1) &
hash
,如果不做
hash
处理,相当于散列生效的只有几个低
bit
位,为了减少散列的碰撞,设计者
综合考虑了速度、作用、质量之后,使用高
16bit
和低
16bit
异或来简单处理减少碰撞,而且
JDK8
中
用了复杂度
O
(
logn
)的树结构来提升碰撞下的性能。
putVal
方法执行流程图
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//
实现
Map.put
和相关方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//
步骤
①
:
tab
为空则创建
// table
未初始化或者长度为
0
,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//
步骤
②
:计算
index
,并对
null
做处理
// (n - 1) & hash
确定元素存放在哪个桶中,桶为空,新生成结点放入桶中
(
此时,这个结点是放在数
组中
)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//
桶中已经存在元素
else {
Node<K,V> e; K k;
//
步骤
③
:节点
key
存在,直接覆盖
value
//
比较桶中第一个元素
(
数组中的结点
)
的
hash
值相等,
key
相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//
将第一个元素赋值给
e
,用
e
来记录
e = p;
//
步骤
④
:判断该链为红黑树
// hash
值不相等,即
key
不相等;为红黑树结点
//
如果当前元素类型为
TreeNode
,表示为红黑树,
putTreeVal
返回待存放的
node, e
可能为
null
else if (p instanceof TreeNode)
//
放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//
步骤
⑤
:该链为链表
//
为链表结点
else {
//
在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
//
到达链表的尾部
//
判断该链表尾部指针是不是空的
if ((e = p.next) == null) {
//
在尾部插入新结点
p.next = newNode(hash, key, value, null);
//
判断链表的长度是否达到转化红黑树的临界值,临界值为
8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//
链表结构转树形结构
treeifyBin(tab, hash);
//
跳出循环
break;
}
//
判断链表中结点的
key
值与插入的元素的
key
值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//
相等,跳出循环
break;
//
用于遍历桶中的链表,与前面的
e = p.next
组合,可以遍历链表
p = e;
}
}
//
判断当前的
key
已经存在的情况下,再来一个相同的
hash
值、
key
值时,返回新来的
value
这个
值
if (e != null) {
//
记录
e
的
value
V oldValue = e.value;
// onlyIfAbsent
为
false
或者旧值为
null
if (!onlyIfAbsent || oldValue == null)
//
用新值替换旧值
e.value = value;
//
访问后回调
afterNodeAccess(e);
//
返回旧值
return oldValue;
}
}
//
结构性修改
1.
判断键值对数组
table[i]
是否为空或为
null
,否则执行
resize()
进行扩容;
2.
根据键值
key
计算
hash
值得到插入的数组索引
i
,如果
table[i]==null
,直接新建节点添加,转向
⑥,如果
table[i]
不为空,转向③;
3.
判断
table[i]
的首个元素是否和
key
一样,如果相同直接覆盖
value
,否则转向④,这里的相同指的
是
hashCode
以及
equals
;
4.
判断
table[i]
是否为
treeNode
,即
table[i]
是否是红黑树,如果是红黑树,则直接在树中插入键值
对,否则转向
5
;
5.
遍历
table[i]
,判断链表长度是否大于
8
,大于
8
的话把链表转换为红黑树,在红黑树中执行插入操
作,否则进行链表的插入操作;遍历过程中若发现
key
已经存在直接覆盖
value
即可;
6.
插入成功后,判断实际存在的键值对数量
size
是否超多了最大容量
threshold
,如果超过,进行扩
容。
33. HashMap
的扩容操作是怎么实现的?
1.
在
jdk1.8
中,
resize
方法是在
hashmap
中的键值对大于阀值时或者初始化时,就调用
resize
方法进
行扩容;
2.
每次扩展的时候,都是扩展
2
倍;
3.
扩展后
Node
对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
在
putVal()
中,我们看到在这个函数里面使用到了
2
次
resize()
方法,
resize()
方法表示的在进行第一
次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值
(
第一次为
12),
这个时候在扩
容的同时也会伴随的桶上面的元素进行重新分发,这也是
JDK1.8
版本的一个优化的地方,在
1.7
中,扩容之后需要重新去计算其
Hash
值,根据
Hash
值对其进行分发,但在
1.8
版本中,则是根据
在同一个桶的位置中进行判断
(e.hash & oldCap)
是否为
0
,重新进行
hash
分配后,该元素的位置
要么停留在原始位置,要么移动到原始位置
+
增加的数组大小这个位置上
++modCount;
//
步骤
⑥
:超过最大容量就扩容
//
实际大小大于阈值则扩容
if (++size > threshold)
resize();
//
插入后回调
afterNodeInsertion(evict);
return null;
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//oldTab
指向
hash
桶数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//
如果
oldCap
不为空的话,就是
hash
桶数组不为空
if (oldCap >= MAXIMUM_CAPACITY) {//
如果大于最大容量了,就赋值为整数最大的阀值
threshold = Integer.MAX_VALUE;
return oldTab;//
返回
}//
如果当前
hash
桶数组的长度在扩容后仍然小于最大容量 并且
oldCap
大于默认值
16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
双倍扩容阀值
threshold
}
//
旧的容量为
0
,但
threshold
大于零,代表有参构造有
cap
传入,
threshold
已经被初始化
成最小
2
的
n
次幂
//
直接将该值赋给新的容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//
无参构造创建的
map
,给出默认容量和
threshold 16, 16*0.75
else { // zero initial threshold signifies using
defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//
新的
threshold =
新的
cap * 0.75
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft <
(float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//
计算出新的数组长度后赋给当前成员变量
table
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//
新建
hash
桶数组
table = newTab;//
将新数组的值复制给旧的
hash
桶数组
//
如果原先的数组没有初始化,那么
resize
的初始化工作到此结束,否则进入扩容元素重排逻辑,使
其均匀的分散
if (oldTab != null) {
//
遍历新数组的所有桶下标
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//
旧数组的桶下标赋给临时变量
e
,并且解除旧数组中的引用,否则就数组无
法被
GC
回收
oldTab[j] = null;
//
如果
e.next==null
,代表桶中就一个元素,不存在链表或者红黑树
if (e.next == null)
//
用同样的
hash
映射算法把该元素加入新的数组
newTab[e.hash & (newCap - 1)] = e;
//
如果
e
是
TreeNode
并且
e.next!=null
,那么处理树中元素的重排
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// e
是链表的头并且
e.next!=null
,那么处理链表中元素重排
else { // preserve order
// loHead,loTail
代表扩容后不用变换下标,见注
1
Node<K,V> loHead = null, loTail = null;
// hiHead,hiTail
代表扩容后变换下标,见注
1
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//
遍历链表
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
//
初始化
head
指向链表当前元素
e
,
e
不一定是链表的
第一个元素,初始化后
loHead
//
代表下标保持不变的链表的头元素
loHead = e;
else
// loTail.next
指向当前
e
loTail.next = e;
// loTail
指向当前的元素
e
34. HashMap
是怎么解决哈希冲突的?
答:在解决这个问题之前,我们首先需要知道
什么是哈希冲突
,而在了解哈希冲突之前我们还要知
道
什么是哈希
才行;
什么是哈希?
Hash
,一般翻译为
“
散列
”
,也有直接音译为
“
哈希
”
的,
Hash
就是指使用哈希算法是指把任意长度
的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。
什么是哈希冲突?
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希
碰撞)
。
HashMap
的数据结构
在
Java
中,保存数据有两种比较简单的数据结构:数组和链表。
数组的特点是:寻址容易,插入和删除困难;
链表的特点是:寻址困难,但插入和删除容易;
//
初始化后,
loTail
和
loHead
指向相同的内存,所以当
loTail.next
指向下一个元素时,
//
底层数组中的元素的
next
引用也相应发生变化,造成
lowHead.next.next.....
//
跟随
loTail
同步,使得
lowHead
可以链接到所有属于该链
表的元素。
loTail = e;
}
else {
if (hiTail == null)
//
初始化
head
指向链表当前元素
e,
初始化后
hiHead
代表下标更改的链表头元素
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//
遍历结束
,
将
tail
指向
null
,并把链表头放入新数组的相应下标,
形成新的映射。
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
所以我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式:链地址法和开放
地址法可以解决哈希冲突:
链表法就是将相同
hash
值的对象组织成一个链表放在
hash
值对应的槽位;
开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽
位。
但相比于
hashCode
返回的
int
类型,我们
HashMap
初始的容量大小
DEFAULT_INITIAL_CAPACITY = 1 << 4
(即
2
的四次方
16
)要远小于
int
类型的范围,所以我们
如果只是单纯的用
hashCode
取余来获取对应的
bucket
这将会大大增加哈希碰撞的概率,并且最
坏情况下还会将
HashMap
变成一个单链表
,所以我们还需要对
hashCode
作一定的优化
hash()
函数
上面提到的问题,主要是因为如果使用
hashCode
取余,那么相当于
参与运算的只有
hashCode
的
低位
,高位是没有起到任何作用的,所以我们的思路就是让
hashCode
取值出的高位也参与运算,
进一步降低
hash
碰撞的概率,使得数据分布更平均,我们把这样的操作称为
扰动
,在
JDK 1.8
中的
hash()
函数如下:
这比在
JDK 1.7
中,更为简洁,
相比在
1.7
中的
4
次位运算,
5
次异或运算(
9
次扰动),在
1.8
中,只
进行了
1
次位运算和
1
次异或运算(
2
次扰动)
;
总结
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//
与自己
右移
16
位进行异或运算(高低位异或)
}
简单总结一下
HashMap
是使用了哪些方法来有效解决哈希冲突的:
链表法就是将相同
hash
值的对象组织成一个链表放在
hash
值对应的槽位;
开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用
的槽位。
35.
能否使用任何类作为
Map
的
key
?
可以使用任何类作为
Map
的
key
,然而在使用之前,需要考虑以下几点:
如果类重写了
equals()
方法,也应该重写
hashCode()
方法。
类的所有实例需要遵循与
equals()
和
hashCode()
相关的规则。
如果一个类没有使用
equals()
,不应该在
hashCode()
中使用它。
用户自定义
Key
类最佳实践是使之为不可变的,这样
hashCode()
值可以被缓存起来,拥有更好的
性能。不可变的类也可以确保
hashCode()
和
equals()
在未来不会改变,这样就会解决与可变相关
的问题了。
36.
为什么
HashMap
中
String
、
Integer
这样的包装类适合作为
K
?
答:
String
、
Integer
等包装类的特性能够保证
Hash
值的不可更改性和计算准确性,能够有效的减
少
Hash
碰撞的几率
都是
fifinal
类型,即不可变性,保证
key
的不可更改性,不会存在获取
hash
值不同的情况
内部已重写了
equals()
、
hashCode()
等方法,遵守了
HashMap
内部的规范(不清楚可以
去上面看看
putValue
的过程),不容易出现
Hash
值计算错误的情况;
37.
如果使用
Object
作为
HashMap
的
Key
,应该怎么办呢?
答:重写
hashCode()
和
equals()
方法
1.
重写
hashCode()
是因为需要计算存储数据的存储位置
,需要注意不要试图从散列码计算中
排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的
Hash
碰撞;
2.
重写
equals()
方法
,需要遵守自反性、对称性、传递性、一致性以及对于任何非
null
的引用
值
x
,
x.equals(null)
必须返回
false
的这几个特性,
目的是为了保证
key
在哈希表中的唯一性
;
38. HashMap
为什么不直接使用
hashCode()
处理后的哈希值直接作
为
table
的下标?
答:
hashCode()
方法返回的是
int
整数类型,其范围为
-(2 ^ 31)~(2 ^ 31 - 1)
,约有
40
亿个映射空
间,而
HashMap
的容量范围是在
16
(初始化默认值)
~2 ^ 30
,
HashMap
通常情况下是取不到最
大值的,并且设备上也难以提供这么多的存储空间,从而导致通过
hashCode()
计算出的哈希值可
能不在数组大小范围内,进而无法匹配存储位置;
那怎么解决呢?
1. HashMap
自己实现了自己的
hash()
方法,通过两次扰动使得它自己的哈希值高低位自行进
行异或运算,降低哈希碰撞概率也使得数据分布更平均;
2.
在保证数组长度为
2
的幂次方的时候,使用
hash()
运算之后的值与运算(
&
)(数组长度
-
1
)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有
当数组长度为
2
的幂次方时,
h&(length-1)
才等价于
h%length
,三来解决了
“
哈希值与数组大
小范围不匹配
”
的问题;
39. HashMap
的长度为什么是
2
的幂次方
为了能让
HashMap
存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表
/
红黑树
长度大致相同。这个实现就是把数据存到哪个链表
/
红黑树中的算法。
这个算法应该如何设计呢?
我们首先可能会想到采用
%
取余的操作来实现。但是,重点来了:
“
取余
(%)
操作中如果除数是
2
的幂次则等价于与其除数减一的与
(&)
操作(也就是说
hash%length==hash&(length-1)
的
前提是
length
是
2
的
n
次方;)。
”
并且 采用二进制位操作
&
,相对于
%
能够提高运算效
率,这就解释了
HashMap
的长度为什么是
2
的幂次方。
那为什么是两次扰动呢?
答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置
的随机性
&
均匀性,最终减少
Hash
冲突,两次就够了,已经达到了高位低位同时参与运算的
目的;
40. HashMap
与
HashTable
有什么区别?
1.
线程安全
:
HashMap
是非线程安全的,
HashTable
是线程安全的;
HashTable
内部的方法基本
都经过
synchronized
修饰。(如果你要保证线程安全的话就使用
ConcurrentHashMap
);
2.
效率
: 因为线程安全的问题,
HashMap
要比
HashTable
效率高一点。另外,
HashTable
基本被
淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用
ConcurrentHashMap
);
3.
对
Null key
和
Null value
的支持
:
HashMap
中,
null
可以作为键,这样的键只有一个,可以有
一个或多个键所对应的值为
null
。但是在
HashTable
中
put
进的键值只要有一个
null
,直接抛
NullPointerException
。
4.
初始容量大小和每次扩充容量大小的不同
:
5.
创建时如果不指定容量初始值,
Hashtable
默认的初始大小为
11
,之后每次扩充,容量变为原来
的
2n+1
。
HashMap
默认的初始化大小为
16
。之后每次扩充,容量变为原来的
2
倍。
6.
创建时如果给定了容量初始值,那么
Hashtable
会直接使用你给定的大小,而
HashMap
会将其
扩充为
2
的幂次方大小。也就是说
HashMap
总是使用
2
的幂作为哈希表的大小,后面会介绍到为
什么是
2
的幂次方。
7.
底层数据结构
:
JDK1.8
以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈
值(默认为
8
)时,将链表转化为红黑树,以减少搜索时间。
Hashtable
没有这样的机制。
8.
推荐使用:在
Hashtable
的类注释可以看到,
Hashtable
是保留类不建议使用,推荐在单线程环
境下使用
HashMap
替代,如果需要多线程使用则用
ConcurrentHashMap
替代。
41.
什么是
TreeMap
简介
TreeMap
是一个
有序的
key-value
集合
,它是通过红黑树实现的。
TreeMap
基于
红黑树(
Red-Black tree
)实现
。该映射根据
其键的自然顺序进行排序
,或者根据
创建映射时提供的
Comparator
进行排序
,具体取决于使用的构造方法。
TreeMap
是线程
非同步
的。
42.
如何决定使用
HashMap
还是
TreeMap
?
对于在
Map
中插入、删除和定位元素这类操作,
HashMap
是最好的选择。然而,假如你需要对一
个有序的
key
集合进行遍历,
TreeMap
是更好的选择。基于你的
collection
的大小,也许向
HashMap
中添加元素会更快,将
map
换为
TreeMap
进行有序
key
的遍历。
43. HashMap
和
ConcurrentHashMap
的区别
1. ConcurrentHashMap
对整个桶数组进行了分割分段
(Segment)
,然后在每一个分段上都用
lock
锁
进行保护,相对于
HashTable
的
synchronized
锁的粒度更精细了一些,并发性能更好,而
HashMap
没有锁机制,不是线程安全的。(
JDK1.8
之后
ConcurrentHashMap
启用了一种全新的
方式实现
,
利用
CAS
算法。)
2. HashMap
的键值对允许有
null
,但是
ConCurrentHashMap
都不允许。
44. ConcurrentHashMap
和
Hashtable
的区别?
ConcurrentHashMap
和
Hashtable
的区别主要体现在实现线程安全的方式上不同。
底层数据结构
:
JDK1.7
的
ConcurrentHashMap
底层采用
分段的数组
+
链表
实现,
JDK1.8
采用的数据结构跟
HashMap1.8
的结构一样,数组
+
链表
/
红黑二叉树。
Hashtable
和
JDK1.8
之前的
HashMap
的底层数据结构类似都是采用
数组
+
链表
的形式,数组是
HashMap
的主
体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式
:
1.
在
JDK1.7
的时候,
ConcurrentHashMap
(分段锁)
对整个桶数组进行了分割分段
(Segment)
,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就
不会存在锁竞争,提高并发访问率。(默认分配
16
个
Segment
,比
Hashtable
效率提高
16
倍。)
到了
JDK1.8
的时候已经摒弃了
Segment
的概念,而是直接用
Node
数组
+
链表
+
红黑
树的数据结构来实现,并发控制使用
synchronized
和
CAS
来操作。(
JDK1.6
以后 对
synchronized
锁做了很多优化)
整个看起来就像是优化过且线程安全的
HashMap
,虽然在
JDK1.8
中还能看到
Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本;
2.
②
Hashtable(
同一把锁
)
:
使用
synchronized
来保证线程安全,效率非常低下。当一个线程
访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用
put
添加
元素,另一个线程不能使用
put
添加元素,也不能使用
get
,竞争会越来越激烈效率越低。
两者的对比图
:
1
、
HashTable:
2
、
JDK1.7
的
ConcurrentHashMap
:
3
、
JDK1.8
的
ConcurrentHashMap
(
TreeBin:
红黑二叉树节点
Node:
链表节点):
答:
ConcurrentHashMap
结合了
HashMap
和
HashTable
二者的优势。
HashMap
没有考虑同
步,
HashTable
考虑了同步的问题使用了
synchronized
关键字,所以
HashTable
在每次同步执行
时都要锁住整个结构。
ConcurrentHashMap
锁的方式是稍微细粒度的。
45. ConcurrentHashMap
底层具体实现知道吗?实现原理是什
么?
JDK1.7
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段
数据时,其他段的数据也能被其他线程访问。
在
JDK1.7
中,
ConcurrentHashMap
采用
Segment + HashEntry
的方式进行实现,结构如下:
一个
ConcurrentHashMap
里包含一个
Segment
数组。
Segment
的结构和
HashMap
类似,是一
种数组和链表结构,一个
Segment
包含一个
HashEntry
数组,每个
HashEntry
是一个链表结构
的元素,每个
Segment
守护着一个
HashEntry
数组里的元素,当对
HashEntry
数组的数据进行修
改时,必须首先获得对应的
Segment
的锁。
1.
该类包含两个静态内部类
HashEntry
和
Segment
;前者用来封装映射表的键值对,后者用来充当
锁的角色;
2. Segment
是一种可重入的锁
ReentrantLock
,每个
Segment
守护一个
HashEntry
数组里得元
素,当对
HashEntry
数组的数据进行修改时,必须首先获得对应的
Segment
锁。
JDK1.8
在
JDK1.8
中,放弃了
Segment
臃肿的设计,取而代之的是采用
Node + CAS + Synchronized
来保
证并发安全进行实现
,
synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要
hash
不冲
突,就不会产生并发,效率又提升
N
倍。
结构如下:
附加源码,有需要的可以看看
插入元素过程(建议去看看源码):
如果相应位置的
Node
还没有初始化,则调用
CAS
插入相应的数据;
如果相应位置的
Node
不为空,且当前该节点不处于移动状态,则对该节点加
synchronized
锁,如
果该节点的
hash
不小于
0
,则遍历链表更新节点或插入新节点;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
1.
如果该节点是
TreeBin
类型的节点,说明是红黑树结构,则通过
putTreeVal
方法往红黑树中插入节
点;如果
binCount
不为
0
,说明
put
操作对数据产生了影响,如果当前链表的个数达到
8
个,则通
过
treeifyBin
方法转化为红黑树,如果
oldVal
不为空,说明是一次更新操作,没有对元素个数产生
影响,则直接返回旧值;
2.
如果插入的是一个新节点,则执行
addCount()
方法尝试更新元素个数
baseCount
;
辅助工具类
46. Array
和
ArrayList
有何区别?
Array
可以存储基本数据类型和对象,
ArrayList
只能存储对象。
Array
是指定固定大小的,而
ArrayList
大小是自动扩展的。
Array
内置方法没有
ArrayList
多,比如
addAll
、
removeAll
、
iteration
等方法只有
ArrayList
有。
对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方
式相对比较慢。
47.
如何实现
Array
和
List
之间的转换?
Array
转
List
:
Arrays. asList(array)
;
List
转
Array
:
List
的
toArray()
方法。
48. comparable
和
comparator
的区别?
comparable
接口实际上是出自
java.lang
包,它有一个
compareTo(Object obj)
方法用来排序
comparator
接口实际上是出自
java.util
包,它有一个
compare(Object obj1, Object obj2)
方法用
来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写
compareTo
方法或
compare
方法,当
我们需要对某一个集合实现两种排序方式,比如一个
song
对象中的歌名和歌手名分别采用一种排
序方法的话,我们可以重写
compareTo
方法和使用自制的
Comparator
方法或者以两个
Comparator
来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的
Collections.sort().
49. Collection
和
Collections
有什么区别?
java.util.Collection
是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操
作的通用接口方法。
Collection
接口在
Java
类库中有很多具体的实现。
Collection
接口的意义是为
各种具体的集合提供了最大化的统一操作方式,其直接继承接口有
List
与
Set
。
Collections
则是集合类的一个工具类
/
帮助类,其中提供了一系列静态方法,用于对集合中元素进
行排序、搜索以及线程安全等各种操作。
50. TreeMap
和
TreeSet
在排序时如何比较元素?
Collections
工
具类中的
sort()
方法如何比较元素?
TreeSet
要求存放的对象所属的类必须实现
Comparable
接口,该接口提供了比较元素的
compareTo()
方法,当插入元素时会回调该方法比较元素的大小。
TreeMap
要求存放的键值对映
射的键必须实现
Comparable
接口从而根据键对元素进 行排 序。
Collections
工具类的
sort
方法有两种重载的形式,
第一种要求传入的待排序容器中存放的对象比较实现
Comparable
接口以实现元素的比较;
?
comparable
接口实际上是出自
java.lang
包,它有一个
compareTo(Object obj)
方法用来排序
comparator
接口实际上是出自
java.util
包,它有一个
compare(Object obj1, Object obj2)
方法用
来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写
compareTo
方法或
compare
方法,当
我们需要对某一个集合实现两种排序方式,比如一个
song
对象中的歌名和歌手名分别采用一种排
序方法的话,我们可以重写
compareTo
方法和使用自制的
Comparator
方法或者以两个
Comparator
来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的
Collections.sort().
51. Collection
和
Collections
有什么区别?
java.util.Collection
是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操
作的通用接口方法。
Collection
接口在
Java
类库中有很多具体的实现。
Collection
接口的意义是为
各种具体的集合提供了最大化的统一操作方式,其直接继承接口有
List
与
Set
。
Collections
则是集合类的一个工具类
/
帮助类,其中提供了一系列静态方法,用于对集合中元素进
行排序、搜索以及线程安全等各种操作。
52. TreeMap
和
TreeSet
在排序时如何比较元素?
Collections
工
具类中的
sort()
方法如何比较元素?
TreeSet
要求存放的对象所属的类必须实现
Comparable
接口,该接口提供了比较元素的
compareTo()
方法,当插入元素时会回调该方法比较元素的大小。
TreeMap
要求存放的键值对映
射的键必须实现
Comparable
接口从而根据键对元素进 行排 序。
Collections
工具类的
sort
方法有两种重载的形式,
第一种要求传入的待排序容器中存放的对象比较实现
Comparable
接口以实现元素的比较;
第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是
Comparator
接口的子类型(需要重写
compare
方法实现元素的比较),相当于一个临时定义的排序规则,其
实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(
Java
中对函数式编程的支
持)。