mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-16 18:10:13 +08:00
Update Java集合框架常见面试题.md
This commit is contained in:
parent
0ebf7cae54
commit
5a9f2d451c
@ -1,89 +1,91 @@
|
||||
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [剖析面试最常见问题之 Java 集合框架](#剖析面试最常见问题之-java-集合框架)
|
||||
- [集合概述](#集合概述)
|
||||
- [Java 集合概览](#java-集合概览)
|
||||
- [说说 List,Set,Map 三者的区别?](#说说-listsetmap-三者的区别)
|
||||
- [集合框架底层数据结构总结](#集合框架底层数据结构总结)
|
||||
- [List](#list)
|
||||
- [Set](#set)
|
||||
- [Map](#map)
|
||||
- [如何选用集合?](#如何选用集合)
|
||||
- [为什么要使用集合?](#为什么要使用集合)
|
||||
- [Iterator 迭代器](#iterator-迭代器)
|
||||
- [迭代器 Iterator 是什么?](#迭代器-iterator-是什么)
|
||||
- [迭代器 Iterator 有啥用?](#迭代器-iterator-有啥用)
|
||||
- [如何使用?](#如何使用)
|
||||
- [有哪些集合是线程不安全的?怎么解决呢?](#有哪些集合是线程不安全的怎么解决呢)
|
||||
- [Collection 子接口之 List](#collection-子接口之-list)
|
||||
- [Arraylist 和 Vector 的区别?](#arraylist-和-vector-的区别)
|
||||
- [Arraylist 与 LinkedList 区别?](#arraylist-与-linkedlist-区别)
|
||||
- [补充内容:双向链表和双向循环链表](#补充内容双向链表和双向循环链表)
|
||||
- [补充内容:RandomAccess 接口](#补充内容randomaccess-接口)
|
||||
- [说一说 ArrayList 的扩容机制吧](#说一说-arraylist-的扩容机制吧)
|
||||
- [Collection 子接口之 Set](#collection-子接口之-set)
|
||||
- [comparable 和 Comparator 的区别](#comparable-和-comparator-的区别)
|
||||
- [Comparator 定制排序](#comparator-定制排序)
|
||||
- [重写 compareTo 方法实现按年龄来排序](#重写-compareto-方法实现按年龄来排序)
|
||||
- [无序性和不可重复性的含义是什么](#无序性和不可重复性的含义是什么)
|
||||
- [比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同](#比较-hashsetlinkedhashset-和-treeset-三者的异同)
|
||||
- [Map 接口](#map-接口)
|
||||
- [HashMap 和 Hashtable 的区别](#hashmap-和-hashtable-的区别)
|
||||
- [HashMap 和 HashSet 区别](#hashmap-和-hashset-区别)
|
||||
- [HashMap 和 TreeMap 区别](#hashmap-和-treemap-区别)
|
||||
- [HashSet 如何检查重复](#hashset-如何检查重复)
|
||||
- [HashMap 的底层实现](#hashmap-的底层实现)
|
||||
- [JDK1.8 之前](#jdk18-之前)
|
||||
- [JDK1.8 之后](#jdk18-之后)
|
||||
- [HashMap 的长度为什么是 2 的幂次方](#hashmap-的长度为什么是-2-的幂次方)
|
||||
- [HashMap 多线程操作导致死循环问题](#hashmap-多线程操作导致死循环问题)
|
||||
- [HashMap 有哪几种常见的遍历方式?](#hashmap-有哪几种常见的遍历方式)
|
||||
- [ConcurrentHashMap 和 Hashtable 的区别](#concurrenthashmap-和-hashtable-的区别)
|
||||
- [ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](#concurrenthashmap-线程安全的具体实现方式底层具体实现)
|
||||
- [JDK1.7(上面有示意图)](#jdk17上面有示意图)
|
||||
- [JDK1.8 (上面有示意图)](#jdk18-上面有示意图)
|
||||
- [Collections 工具类](#collections-工具类)
|
||||
- [排序操作](#排序操作)
|
||||
- [查找,替换操作](#查找替换操作)
|
||||
- [同步控制](#同步控制)
|
||||
- [其他重要问题](#其他重要问题)
|
||||
- [什么是快速失败(fail-fast)?](#什么是快速失败fail-fast)
|
||||
- [什么是安全失败(fail-safe)呢?](#什么是安全失败fail-safe呢)
|
||||
- [公众号](#公众号)
|
||||
- [1. 剖析面试最常见问题之 Java 集合框架](#1-剖析面试最常见问题之-java-集合框架)
|
||||
- [1.1. 集合概述](#11-集合概述)
|
||||
- [1.1.1. Java 集合概览](#111-java-集合概览)
|
||||
- [1.1.2. 说说 List,Set,Map 三者的区别?](#112-说说-listsetmap-三者的区别)
|
||||
- [1.1.3. 集合框架底层数据结构总结](#113-集合框架底层数据结构总结)
|
||||
- [1.1.3.1. List](#1131-list)
|
||||
- [1.1.3.2. Set](#1132-set)
|
||||
- [1.1.3.3. Map](#1133-map)
|
||||
- [1.1.4. 如何选用集合?](#114-如何选用集合)
|
||||
- [1.1.5. 为什么要使用集合?](#115-为什么要使用集合)
|
||||
- [1.1.6. Iterator 迭代器](#116-iterator-迭代器)
|
||||
- [1.1.6.1. 迭代器 Iterator 是什么?](#1161-迭代器-iterator-是什么)
|
||||
- [1.1.6.2. 迭代器 Iterator 有啥用?](#1162-迭代器-iterator-有啥用)
|
||||
- [1.1.6.3. 如何使用?](#1163-如何使用)
|
||||
- [1.1.7. 有哪些集合是线程不安全的?怎么解决呢?](#117-有哪些集合是线程不安全的怎么解决呢)
|
||||
- [1.2. Collection 子接口之 List](#12-collection-子接口之-list)
|
||||
- [1.2.1. Arraylist 和 Vector 的区别?](#121-arraylist-和-vector-的区别)
|
||||
- [1.2.2. Arraylist 与 LinkedList 区别?](#122-arraylist-与-linkedlist-区别)
|
||||
- [1.2.2.1. 补充内容:双向链表和双向循环链表](#1221-补充内容双向链表和双向循环链表)
|
||||
- [1.2.2.2. 补充内容:RandomAccess 接口](#1222-补充内容randomaccess-接口)
|
||||
- [1.2.3. 说一说 ArrayList 的扩容机制吧](#123-说一说-arraylist-的扩容机制吧)
|
||||
- [1.3. Collection 子接口之 Set](#13-collection-子接口之-set)
|
||||
- [1.3.1. comparable 和 Comparator 的区别](#131-comparable-和-comparator-的区别)
|
||||
- [1.3.1.1. Comparator 定制排序](#1311-comparator-定制排序)
|
||||
- [1.3.1.2. 重写 compareTo 方法实现按年龄来排序](#1312-重写-compareto-方法实现按年龄来排序)
|
||||
- [1.3.2. 无序性和不可重复性的含义是什么](#132-无序性和不可重复性的含义是什么)
|
||||
- [1.3.3. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同](#133-比较-hashsetlinkedhashset-和-treeset-三者的异同)
|
||||
- [1.4. Map 接口](#14-map-接口)
|
||||
- [1.4.1. HashMap 和 Hashtable 的区别](#141-hashmap-和-hashtable-的区别)
|
||||
- [1.4.2. HashMap 和 HashSet 区别](#142-hashmap-和-hashset-区别)
|
||||
- [1.4.3. HashMap 和 TreeMap 区别](#143-hashmap-和-treemap-区别)
|
||||
- [1.4.4. HashSet 如何检查重复](#144-hashset-如何检查重复)
|
||||
- [1.4.5. HashMap 的底层实现](#145-hashmap-的底层实现)
|
||||
- [1.4.5.1. JDK1.8 之前](#1451-jdk18-之前)
|
||||
- [1.4.5.2. JDK1.8 之后](#1452-jdk18-之后)
|
||||
- [1.4.6. HashMap 的长度为什么是 2 的幂次方](#146-hashmap-的长度为什么是-2-的幂次方)
|
||||
- [1.4.7. HashMap 多线程操作导致死循环问题](#147-hashmap-多线程操作导致死循环问题)
|
||||
- [1.4.8. HashMap 有哪几种常见的遍历方式?](#148-hashmap-有哪几种常见的遍历方式)
|
||||
- [1.4.9. ConcurrentHashMap 和 Hashtable 的区别](#149-concurrenthashmap-和-hashtable-的区别)
|
||||
- [1.4.10. ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](#1410-concurrenthashmap-线程安全的具体实现方式底层具体实现)
|
||||
- [1.4.10.1. JDK1.7(上面有示意图)](#14101-jdk17上面有示意图)
|
||||
- [1.4.10.2. JDK1.8 (上面有示意图)](#14102-jdk18-上面有示意图)
|
||||
- [1.5. Collections 工具类](#15-collections-工具类)
|
||||
- [1.5.1. 排序操作](#151-排序操作)
|
||||
- [1.5.2. 查找,替换操作](#152-查找替换操作)
|
||||
- [1.5.3. 同步控制](#153-同步控制)
|
||||
- [1.6. 其他重要问题](#16-其他重要问题)
|
||||
- [1.6.1. 什么是快速失败(fail-fast)?](#161-什么是快速失败fail-fast)
|
||||
- [1.6.2. 什么是安全失败(fail-safe)呢?](#162-什么是安全失败fail-safe呢)
|
||||
- [1.6.3. Arrays.asList()避坑指南](#163-arraysaslist避坑指南)
|
||||
- [1.6.3.1. 简介](#1631-简介)
|
||||
- [1.6.3.2. 《阿里巴巴 Java 开发手册》对其的描述](#1632-阿里巴巴-java-开发手册对其的描述)
|
||||
- [1.6.3.3. 使用时的注意事项总结](#1633-使用时的注意事项总结)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
# 剖析面试最常见问题之 Java 集合框架
|
||||
|
||||
## 集合概述
|
||||
# 1. 剖析面试最常见问题之 Java 集合框架
|
||||
|
||||
### Java 集合概览
|
||||
## 1.1. 集合概述
|
||||
|
||||
从下图可以看出,在 Java 中除了以 Map 结尾的类之外, 其他类都实现了 `Collection` 接口。
|
||||
### 1.1.1. Java 集合概览
|
||||
|
||||
从下图可以看出,在 Java 中除了以 `Map` 结尾的类之外, 其他类都实现了 `Collection` 接口。
|
||||
|
||||
并且,以 `Map` 结尾的类都实现了 `Map` 接口。
|
||||
|
||||

|
||||
|
||||
### 说说 List,Set,Map 三者的区别?
|
||||
### 1.1.2. 说说 List,Set,Map 三者的区别?
|
||||
|
||||
- `List`(对付顺序的好帮手): 存储的元素是有序的、可重复的。
|
||||
- `Set`(注重独一无二的性质): 存储的元素是无序的、不可重复的。
|
||||
- `Map`(用 Key 来搜索的专家): 使用键值对(kye-value)存储,类似于数学上的函数 y=f(x),“x”代表 key,"y"代表 value,Key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
|
||||
|
||||
### 集合框架底层数据结构总结
|
||||
### 1.1.3. 集合框架底层数据结构总结
|
||||
|
||||
先来看一下 `Collection` 接口下面的集合。
|
||||
|
||||
#### List
|
||||
#### 1.1.3.1. List
|
||||
|
||||
- `Arraylist`: `Object[]`数组
|
||||
- `Vector`:`Object[]`数组
|
||||
- `LinkedList`: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
|
||||
|
||||
#### Set
|
||||
#### 1.1.3.2. Set
|
||||
|
||||
- `HashSet`(无序,唯一): 基于 `HashMap` 实现的,底层采用 `HashMap` 来保存元素
|
||||
- `LinkedHashSet`:`LinkedHashSet` 是 `HashSet` 的子类,并且其内部是通过 `LinkedHashMap` 来实现的。有点类似于我们之前说的 `LinkedHashMap` 其内部是基于 `HashMap` 实现一样,不过还是有一点点区别的
|
||||
@ -91,20 +93,20 @@
|
||||
|
||||
再来看看 `Map` 接口下面的集合。
|
||||
|
||||
#### Map
|
||||
#### 1.1.3.3. Map
|
||||
|
||||
- `HashMap`: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
|
||||
- `LinkedHashMap`: `LinkedHashMap` 继承自 `HashMap`,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,`LinkedHashMap` 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931)
|
||||
- `Hashtable`: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
|
||||
- `TreeMap`: 红黑树(自平衡的排序二叉树)
|
||||
|
||||
### 如何选用集合?
|
||||
### 1.1.4. 如何选用集合?
|
||||
|
||||
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map 接口下的集合,需要排序时选择 TreeMap,不需要排序时就选择 HashMap,需要保证线程安全就选用 ConcurrentHashMap。
|
||||
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 `Map` 接口下的集合,需要排序时选择 `TreeMap`,不需要排序时就选择 `HashMap`,需要保证线程安全就选用 `ConcurrentHashMap`。
|
||||
|
||||
当我们只需要存放元素值时,就选择实现 Collection 接口的集合,需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSet 或 HashSet,不需要就选择实现 List 接口的比如 ArrayList 或 LinkedList,然后再根据实现这些接口的集合的特点来选用。
|
||||
当我们只需要存放元素值时,就选择实现`Collection` 接口的集合,需要保证元素唯一时选择实现 `Set` 接口的集合比如 `TreeSet` 或 `HashSet`,不需要就选择实现 `List` 接口的比如 `ArrayList` 或 `LinkedList`,然后再根据实现这些接口的集合的特点来选用。
|
||||
|
||||
### 为什么要使用集合?
|
||||
### 1.1.5. 为什么要使用集合?
|
||||
|
||||
当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端,
|
||||
因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。
|
||||
@ -112,9 +114,9 @@
|
||||
数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。
|
||||
但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据
|
||||
|
||||
### Iterator 迭代器
|
||||
### 1.1.6. Iterator 迭代器
|
||||
|
||||
#### 迭代器 Iterator 是什么?
|
||||
#### 1.1.6.1. 迭代器 Iterator 是什么?
|
||||
|
||||
```java
|
||||
public interface Iterator<E> {
|
||||
@ -130,11 +132,11 @@ public interface Iterator<E> {
|
||||
|
||||
迭代器是将这样的方法抽取出接口,然后在每个类的内部,定义自己迭代方式,这样做就规定了整个集合体系的遍历方式都是 `hasNext()`和`next()`方法,使用者不用管怎么实现的,会用即可。迭代器的定义为:提供一种方法访问一个容器对象中各个元素,而又不需要暴露该对象的内部细节。
|
||||
|
||||
#### 迭代器 Iterator 有啥用?
|
||||
#### 1.1.6.2. 迭代器 Iterator 有啥用?
|
||||
|
||||
`Iterator` 主要是用来遍历集合用的,它的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 `ConcurrentModificationException` 异常。
|
||||
|
||||
#### 如何使用?
|
||||
#### 1.1.6.3. 如何使用?
|
||||
|
||||
我们通过使用迭代器来遍历 `HashMap`,演示一下 迭代器 Iterator 的使用。
|
||||
|
||||
@ -151,7 +153,7 @@ while (iterator.hasNext()) {
|
||||
}
|
||||
```
|
||||
|
||||
### 有哪些集合是线程不安全的?怎么解决呢?
|
||||
### 1.1.7. 有哪些集合是线程不安全的?怎么解决呢?
|
||||
|
||||
我们常用的 `Arraylist` ,`LinkedList`,`Hashmap`,`HashSet`,`TreeSet`,`TreeMap`,`PriorityQueue` 都不是线程安全的。解决办法很简单,可以使用线程安全的集合来代替。
|
||||
|
||||
@ -163,14 +165,14 @@ while (iterator.hasNext()) {
|
||||
4. `BlockingQueue`: 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
|
||||
5. `ConcurrentSkipListMap` :跳表的实现。这是一个`Map`,使用跳表的数据结构进行快速查找。
|
||||
|
||||
## Collection 子接口之 List
|
||||
## 1.2. Collection 子接口之 List
|
||||
|
||||
### Arraylist 和 Vector 的区别?
|
||||
### 1.2.1. Arraylist 和 Vector 的区别?
|
||||
|
||||
1. ArrayList 是 List 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ;
|
||||
2. Vector 是 List 的古老实现类,底层使用 Object[ ]存储,线程安全的。
|
||||
|
||||
### Arraylist 与 LinkedList 区别?
|
||||
### 1.2.2. Arraylist 与 LinkedList 区别?
|
||||
|
||||
1. **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全;
|
||||
2. **底层数据结构:** `Arraylist` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
|
||||
@ -178,7 +180,7 @@ while (iterator.hasNext()) {
|
||||
4. **是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList` 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。
|
||||
5. **内存空间占用:** ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
|
||||
|
||||
#### 补充内容:双向链表和双向循环链表
|
||||
#### 1.2.2.1. 补充内容:双向链表和双向循环链表
|
||||
|
||||
**双向链表:** 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。
|
||||
|
||||
@ -190,7 +192,7 @@ while (iterator.hasNext()) {
|
||||
|
||||

|
||||
|
||||
#### 补充内容:RandomAccess 接口
|
||||
#### 1.2.2.2. 补充内容:RandomAccess 接口
|
||||
|
||||
```java
|
||||
public interface RandomAccess {
|
||||
@ -213,20 +215,20 @@ public interface RandomAccess {
|
||||
|
||||
`ArrayList` 实现了 `RandomAccess` 接口, 而 `LinkedList` 没有实现。为什么呢?我觉得还是和底层数据结构有关!`ArrayList` 底层是数组,而 `LinkedList` 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,`ArrayList` 实现了 `RandomAccess` 接口,就表明了他具有快速随机访问功能。 `RandomAccess` 接口只是标识,并不是说 `ArrayList` 实现 `RandomAccess` 接口才具有快速随机访问功能的!
|
||||
|
||||
### 说一说 ArrayList 的扩容机制吧
|
||||
### 1.2.3. 说一说 ArrayList 的扩容机制吧
|
||||
|
||||
详见笔主的这篇文章:[通过源码一步一步分析 ArrayList 扩容机制](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md)
|
||||
|
||||
## Collection 子接口之 Set
|
||||
## 1.3. Collection 子接口之 Set
|
||||
|
||||
### comparable 和 Comparator 的区别
|
||||
### 1.3.1. comparable 和 Comparator 的区别
|
||||
|
||||
- `comparable` 接口实际上是出自`java.lang`包 它有一个 `compareTo(Object obj)`方法用来排序
|
||||
- `comparator`接口实际上是出自 java.util 包它有一个`compare(Object obj1, Object obj2)`方法用来排序
|
||||
|
||||
一般我们需要对一个集合使用自定义排序时,我们就要重写`compareTo()`方法或`compare()`方法,当我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写`compareTo()`方法和使用自制的`Comparator`方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 `Collections.sort()`.
|
||||
|
||||
#### Comparator 定制排序
|
||||
#### 1.3.1.1. Comparator 定制排序
|
||||
|
||||
```java
|
||||
ArrayList<Integer> arrayList = new ArrayList<Integer>();
|
||||
@ -274,7 +276,7 @@ Collections.sort(arrayList):
|
||||
[7, 4, 3, 3, -1, -5, -7, -9]
|
||||
```
|
||||
|
||||
#### 重写 compareTo 方法实现按年龄来排序
|
||||
#### 1.3.1.2. 重写 compareTo 方法实现按年龄来排序
|
||||
|
||||
```java
|
||||
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
|
||||
@ -348,13 +350,13 @@ Output:
|
||||
30-张三
|
||||
```
|
||||
|
||||
### 无序性和不可重复性的含义是什么
|
||||
### 1.3.2. 无序性和不可重复性的含义是什么
|
||||
|
||||
1、什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
|
||||
|
||||
2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法。
|
||||
|
||||
### 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
|
||||
### 1.3.3. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
|
||||
|
||||
HashSet 是 Set 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;
|
||||
|
||||
@ -362,9 +364,9 @@ LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历;
|
||||
|
||||
TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。
|
||||
|
||||
## Map 接口
|
||||
## 1.4. Map 接口
|
||||
|
||||
### HashMap 和 Hashtable 的区别
|
||||
### 1.4.1. HashMap 和 Hashtable 的区别
|
||||
|
||||
1. **线程是否安全:** HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
|
||||
2. **效率:** 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
|
||||
@ -409,18 +411,18 @@ TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,
|
||||
}
|
||||
```
|
||||
|
||||
### HashMap 和 HashSet 区别
|
||||
### 1.4.2. HashMap 和 HashSet 区别
|
||||
|
||||
如果你看过 `HashSet` 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 `clone()`、`writeObject()`、`readObject()`是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
|
||||
|
||||
| HashMap | HashSet |
|
||||
| :--------------------------------: | :-----------------------------------------------------------------------------------------------------------------: |
|
||||
| 实现了 Map 接口 | 实现 Set 接口 |
|
||||
| 存储键值对 | 仅存储对象 |
|
||||
| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 Set 中添加元素 |
|
||||
| HashMap | HashSet |
|
||||
| :--------------------------------: | :----------------------------------------------------------: |
|
||||
| 实现了 Map 接口 | 实现 Set 接口 |
|
||||
| 存储键值对 | 仅存储对象 |
|
||||
| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 Set 中添加元素 |
|
||||
| HashMap 使用键(Key)计算 Hashcode | HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals()方法用来判断对象的相等性, |
|
||||
|
||||
### HashMap 和 TreeMap 区别
|
||||
### 1.4.3. HashMap 和 TreeMap 区别
|
||||
|
||||
`TreeMap` 和`HashMap` 都继承自`AbstractMap` ,但是需要注意的是`TreeMap`它还实现了`NavigableMap`接口和`SortedMap` 接口。
|
||||
|
||||
@ -488,7 +490,7 @@ TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) -> {
|
||||
|
||||
**综上,相比于`HashMap`来说 `TreeMap` 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。**
|
||||
|
||||
### HashSet 如何检查重复
|
||||
### 1.4.4. HashSet 如何检查重复
|
||||
|
||||
当你把对象加入`HashSet`时,HashSet 会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用`equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。(摘自我的 Java 启蒙书《Head fist java》第二版)
|
||||
|
||||
@ -508,9 +510,9 @@ TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) -> {
|
||||
|
||||
对于引用类型(包括包装类型)来说,equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String),则比较的是地址里的内容。
|
||||
|
||||
### HashMap 的底层实现
|
||||
### 1.4.5. HashMap 的底层实现
|
||||
|
||||
#### JDK1.8 之前
|
||||
#### 1.4.5.1. JDK1.8 之前
|
||||
|
||||
JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。**
|
||||
|
||||
@ -547,21 +549,17 @@ static int hash(int h) {
|
||||
|
||||
所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
|
||||
|
||||

|
||||

|
||||
|
||||
#### JDK1.8 之后
|
||||
#### 1.4.5.2. JDK1.8 之后
|
||||
|
||||
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
|
||||
|
||||

|
||||

|
||||
|
||||
> TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
|
||||
|
||||
**推荐阅读:**
|
||||
|
||||
- 《Java 8 系列之重新认识 HashMap》 :<https://zhuanlan.zhihu.com/p/21673805>
|
||||
|
||||
### HashMap 的长度为什么是 2 的幂次方
|
||||
### 1.4.6. HashMap 的长度为什么是 2 的幂次方
|
||||
|
||||
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
|
||||
|
||||
@ -569,17 +567,17 @@ static int hash(int h) {
|
||||
|
||||
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。**
|
||||
|
||||
### HashMap 多线程操作导致死循环问题
|
||||
### 1.4.7. HashMap 多线程操作导致死循环问题
|
||||
|
||||
主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。
|
||||
|
||||
详情请查看:<https://coolshell.cn/articles/9606.html>
|
||||
|
||||
### HashMap 有哪几种常见的遍历方式?
|
||||
### 1.4.8. HashMap 有哪几种常见的遍历方式?
|
||||
|
||||
[HashMap 的 7 种遍历方式与性能分析!(强烈推荐)](https://mp.weixin.qq.com/s/Zz6mofCtmYpABDL1ap04ow)
|
||||
[HashMap 的 7 种遍历方式与性能分析!](https://mp.weixin.qq.com/s/Zz6mofCtmYpABDL1ap04ow)
|
||||
|
||||
### ConcurrentHashMap 和 Hashtable 的区别
|
||||
### 1.4.9. ConcurrentHashMap 和 Hashtable 的区别
|
||||
|
||||
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
|
||||
|
||||
@ -588,23 +586,27 @@ ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方
|
||||
|
||||
**两者的对比图:**
|
||||
|
||||
图片来源:<http://www.cnblogs.com/chengxiao/p/6842045.html>
|
||||
|
||||
**HashTable:**
|
||||
|
||||

|
||||
|
||||
<p style="text-align:right;font-size:13px;color:gray">http://www.cnblogs.com/chengxiao/p/6842045.html></p>
|
||||
|
||||
**JDK1.7 的 ConcurrentHashMap:**
|
||||
|
||||

|
||||
|
||||
**JDK1.8 的 ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):**
|
||||
<p style="text-align:right;font-size:13px;color:gray">http://www.cnblogs.com/chengxiao/p/6842045.html></p>
|
||||
|
||||

|
||||
**JDK1.8 的 ConcurrentHashMap:**
|
||||
|
||||
### ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
|
||||

|
||||
|
||||
#### JDK1.7(上面有示意图)
|
||||
JDK1.8 的 `ConcurrentHashMap` 不在是 **Segment 数组 + HashEntry 数组 + 链表**,而是 **Node 数组 + 链表 / 红黑树**。不过,Node 只能用于链表的情况,红黑树的情况需要使用 **`TreeNode`**。当冲突链表达到一定长度时,链表会转换成红黑树。
|
||||
|
||||
### 1.4.10. ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
|
||||
|
||||
#### 1.4.10.1. JDK1.7(上面有示意图)
|
||||
|
||||
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
|
||||
|
||||
@ -619,13 +621,13 @@ static class Segment<K,V> extends ReentrantLock implements Serializable {
|
||||
|
||||
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。
|
||||
|
||||
#### JDK1.8 (上面有示意图)
|
||||
#### 1.4.10.2. JDK1.8 (上面有示意图)
|
||||
|
||||
ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))
|
||||
|
||||
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
|
||||
|
||||
## Collections 工具类
|
||||
## 1.5. Collections 工具类
|
||||
|
||||
Collections 工具类常用方法:
|
||||
|
||||
@ -633,7 +635,7 @@ Collections 工具类常用方法:
|
||||
2. 查找,替换操作
|
||||
3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)
|
||||
|
||||
### 排序操作
|
||||
### 1.5.1. 排序操作
|
||||
|
||||
```java
|
||||
void reverse(List list)//反转
|
||||
@ -644,7 +646,7 @@ void swap(List list, int i , int j)//交换两个索引位置的元素
|
||||
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
|
||||
```
|
||||
|
||||
### 查找,替换操作
|
||||
### 1.5.2. 查找,替换操作
|
||||
|
||||
```java
|
||||
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
|
||||
@ -656,7 +658,7 @@ int indexOfSubList(List list, List target)//统计target在list中第一次出
|
||||
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素
|
||||
```
|
||||
|
||||
### 同步控制
|
||||
### 1.5.3. 同步控制
|
||||
|
||||
`Collections` 提供了多个`synchronizedXxx()`方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
|
||||
|
||||
@ -673,9 +675,9 @@ synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安
|
||||
synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的)set。
|
||||
```
|
||||
|
||||
## 其他重要问题
|
||||
## 1.6. 其他重要问题
|
||||
|
||||
### 什么是快速失败(fail-fast)?
|
||||
### 1.6.1. 什么是快速失败(fail-fast)?
|
||||
|
||||
**快速失败(fail-fast)** 是 Java 集合的一种错误检测机制。**在使用迭代器对集合进行遍历的时候,我们在多线程下操作非安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出 `ConcurrentModificationException` 异常。 另外,在单线程下,如果在遍历过程中对集合对象的内容进行了修改的话也会触发 fail-fast 机制。**
|
||||
|
||||
@ -706,18 +708,139 @@ final void checkForComodification() {
|
||||
|
||||
有了前面讲的基础,我们应该知道:使用 `Iterator` 提供的 `remove` 方法,可以修改到 `expectedModCount` 的值。所以,才不会再抛出`ConcurrentModificationException` 异常。
|
||||
|
||||
### 什么是安全失败(fail-safe)呢?
|
||||
### 1.6.2. 什么是安全失败(fail-safe)呢?
|
||||
|
||||
明白了快速失败(fail-fast)之后,安全失败(fail-safe)我们就很好理解了。
|
||||
|
||||
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以,在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 `ConcurrentModificationException` 异常
|
||||
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以,在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 `ConcurrentModificationException` 异常。
|
||||
|
||||
## 公众号
|
||||
### 1.6.3. Arrays.asList()避坑指南
|
||||
|
||||
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
|
||||
最近使用`Arrays.asList()`遇到了一些坑,然后在网上看到这篇文章:[Java Array to List Examples](http://javadevnotes.com/java-array-to-list-examples) 感觉挺不错的,但是还不是特别全面。所以,自己对于这块小知识点进行了简单的总结。
|
||||
|
||||
**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java 面试突击"** 即可免费领取!
|
||||
#### 1.6.3.1. 简介
|
||||
|
||||
**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
||||
`Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个 List 集合。
|
||||
|
||||

|
||||
```java
|
||||
String[] myArray = { "Apple", "Banana", "Orange" };
|
||||
List<String> myList = Arrays.asList(myArray);
|
||||
//上面两个语句等价于下面一条语句
|
||||
List<String> myList = Arrays.asList("Apple","Banana", "Orange");
|
||||
```
|
||||
|
||||
JDK 源码对于这个方法的说明:
|
||||
|
||||
```java
|
||||
/**
|
||||
*返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。
|
||||
*/
|
||||
public static <T> List<T> asList(T... a) {
|
||||
return new ArrayList<>(a);
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.6.3.2. 《阿里巴巴 Java 开发手册》对其的描述
|
||||
|
||||
`Arrays.asList()`将数组转换为集合后,底层其实还是数组,《阿里巴巴 Java 开发手册》对于这个方法有如下描述:
|
||||
|
||||
方法.png>)
|
||||
|
||||
#### 1.6.3.3. 使用时的注意事项总结
|
||||
|
||||
**传递的数组必须是对象数组,而不是基本类型。**
|
||||
|
||||
`Arrays.asList()`是泛型方法,传入的对象必须是对象数组。
|
||||
|
||||
```java
|
||||
int[] myArray = { 1, 2, 3 };
|
||||
List myList = Arrays.asList(myArray);
|
||||
System.out.println(myList.size());//1
|
||||
System.out.println(myList.get(0));//数组地址值
|
||||
System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException
|
||||
int [] array=(int[]) myList.get(0);
|
||||
System.out.println(array[0]);//1
|
||||
```
|
||||
|
||||
当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时 List 的唯一元素就是这个数组,这也就解释了上面的代码。
|
||||
|
||||
我们使用包装类型数组就可以解决这个问题。
|
||||
|
||||
```java
|
||||
Integer[] myArray = { 1, 2, 3 };
|
||||
```
|
||||
|
||||
**使用集合的修改方法:`add()`、`remove()`、`clear()`会抛出异常。**
|
||||
|
||||
```java
|
||||
List myList = Arrays.asList(1, 2, 3);
|
||||
myList.add(4);//运行时报错:UnsupportedOperationException
|
||||
myList.remove(1);//运行时报错:UnsupportedOperationException
|
||||
myList.clear();//运行时报错:UnsupportedOperationException
|
||||
```
|
||||
|
||||
`Arrays.asList()` 方法返回的并不是 `java.util.ArrayList` ,而是 `java.util.Arrays` 的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。
|
||||
|
||||
```java
|
||||
List myList = Arrays.asList(1, 2, 3);
|
||||
System.out.println(myList.getClass());//class java.util.Arrays$ArrayList
|
||||
```
|
||||
|
||||
下图是`java.util.Arrays$ArrayList`的简易源码,我们可以看到这个类重写的方法有哪些。
|
||||
|
||||
```java
|
||||
private static class ArrayList<E> extends AbstractList<E>
|
||||
implements RandomAccess, java.io.Serializable
|
||||
{
|
||||
...
|
||||
|
||||
@Override
|
||||
public E get(int index) {
|
||||
...
|
||||
}
|
||||
|
||||
@Override
|
||||
public E set(int index, E element) {
|
||||
...
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOf(Object o) {
|
||||
...
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(Consumer<? super E> action) {
|
||||
...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replaceAll(UnaryOperator<E> operator) {
|
||||
...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sort(Comparator<? super E> c) {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们再看一下`java.util.AbstractList`的`remove()`方法,这样我们就明白为啥会抛出`UnsupportedOperationException`。
|
||||
|
||||
```java
|
||||
public E remove(int index) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**《Java面试突击》:** Java 程序员面试必备的《Java面试突击》V3.0 PDF 版本扫码关注下面的公众号,在后台回复 **"面试突击"** 即可免费领取!
|
||||
|
||||

|
Loading…
x
Reference in New Issue
Block a user