以下内容来源自Java 面试指南 | JavaGuide(Java面试 + 学习指南)和自己的学习笔记整理,这里我整理了自己比较感兴趣的点,也有助于我自己理解~
集合
什么是集合
集合是对象的容器,定义了对多个对象进行操作的常用方法,和数组类似。可以实现数组的功能。
和数组的区别:
- 数组长度固定,集合长度不固定。
- 数组可以存储基本类型和引用类型,集合只能存储引用类型。如果要存储基本类型,需要用到包装类装箱。
集合框架底层数据结构总结
先来看一下
Collection
接口下面的集合。# List
ArrayList
:Object[]
数组Vector
:Object[]
数组LinkedList
:双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)# Set
HashSet
(无序,唯一): 基于HashMap
实现的,底层采用HashMap
来保存元素LinkedHashSet
:LinkedHashSet
是HashSet
的子类,并且其内部是通过LinkedHashMap
来实现的。有点类似于我们之前说的LinkedHashMap
其内部是基于HashMap
实现一样,不过还是有一点点区别的TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树)# Queue
PriorityQueue
:Object[]
数组来实现二叉堆ArrayQueue
:Object[]
数组 + 双指针再来看看
Map
接口下面的集合。# Map
HashMap
:JDK1.8 之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间LinkedHashMap
:LinkedHashMap
继承自HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》open in new windowHashtable
:数组+链表组成的,数组是Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的TreeMap
:红黑树(自平衡的排序二叉树)
著作权归Guide所有 原文链接:Java集合常见面试题总结(上) | JavaGuide(Java面试 + 学习指南)
Collection体系集合

Collection
接口下面分为两个接口,List
和Set
;
- List:有序(添加元素和获取元素、遍历元素顺序是一致的)、有下标(可以用for循环)、元素可以重复。
- Set:无序、无下标、元素不可重复。
List实现类
- ArrayList(数组列表集合)【重点**】:**
- 数组结构实现(底层是数组),查询快(数组空间连续)、增删慢;
- JDK1.2加入,运行效率快、线程不安全。
- 默认初始容量:10
- Vector(向量集合):现在用的不多了,和ArrayList比较像
- 数组结构实现,查询快、增删慢;
- JDK1.0加入,运行效率慢、但是线程安全。
- LinkedList(链表集合):
- 链表结构实现,增删快、查询慢。(双向链表,前一个节点指向后一个节点)线程不安全
ArrayList和LinkedList区别

- ArrayList:必须开辟连续空间,查询快,增删慢。
- LinkedList:无需开辟连续空间,查询慢,增删快。(删除的元素并没有被垃圾回收器回收,只是从集合中删除了)
- 是否保证线程安全:
ArrayList
和LinkedList
都是不同步的,也就是不保证线程安全;- 底层数据结构:
ArrayList
底层使用的是Object
数组;LinkedList
底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)- 插入和删除是否受元素位置的影响:
ArrayList
采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)
方法的时候,ArrayList
会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)
),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。LinkedList
采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(add(E e)
、addFirst(E e)
、addLast(E e)
、removeFirst()
、removeLast()
),时间复杂度为 O(1),如果是要在指定位置i
插入和删除元素的话(add(int index, E element)
,remove(Object o)
,remove(int index)
), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。- 是否支持快速随机访问:
LinkedList
不支持高效的随机元素访问,而ArrayList
(实现了RandomAccess
接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。- 内存空间占用:
ArrayList
的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。我们在项目中一般是不会使用到
LinkedList
的,需要用到LinkedList
的场景几乎都可以使用ArrayList
来代替,并且,性能通常会更好!就连LinkedList
的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用LinkedList
。
另外,不要下意识地认为
LinkedList
作为链表就最适合元素增删的场景。我在上面也说了,LinkedList
仅仅在头尾插入或者删除元素的时候时间复杂度近似 O(1),其他情况增删元素的平均时间复杂度都是 O(n) 。
著作权归Guide所有 原文链接:Java集合常见面试题总结(上) | JavaGuide(Java面试 + 学习指南)
虽然两者都是线程不安全的,但是使用ArrayList通常性能会更好,所以一般情况我们会选择使用ArrayList而不是LinkedList。
ArrayList 和 Array(数组)的区别?
ArrayList
内部基于动态数组实现,比Array
(静态数组) 使用起来更加灵活:
ArrayList
会根据实际存储的元素动态地扩容或缩容,而Array
被创建之后就不能改变它的长度了。ArrayList
允许你使用泛型来确保类型安全,Array
则不可以。ArrayList
中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。Array
可以直接存储基本类型数据,也可以存储对象。ArrayList
支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如add()
、remove()
等。Array
只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。ArrayList
创建时不需要指定大小,而Array
创建时必须指定大小。下面是二者使用的简单对比:
Array
:// 初始化一个 String 类型的数组 String[] stringArr = new String[]{"hello", "world", "!"}; // 修改数组元素的值 stringArr[0] = "goodbye"; System.out.println(Arrays.toString(stringArr));// [goodbye, world, !] // 删除数组中的元素,需要手动移动后面的元素 for (int i = 0; i < stringArr.length - 1; i++) { stringArr[i] = stringArr[i + 1]; } stringArr[stringArr.length - 1] = null; System.out.println(Arrays.toString(stringArr));// [world, !, null]
ArrayList
:// 初始化一个 String 类型的 ArrayList ArrayList<String> stringList = new ArrayList<>(Arrays.asList("hello", "world", "!")); // 添加元素到 ArrayList 中 stringList.add("goodbye"); System.out.println(stringList);// [hello, world, !, goodbye] // 修改 ArrayList 中的元素 stringList.set(0, "hi"); System.out.println(stringList);// [hi, world, !, goodbye] // 删除 ArrayList 中的元素 stringList.remove(0); System.out.println(stringList); // [world, !, goodbye]
著作权归Guide所有 原文链接:Java集合常见面试题总结(上) | JavaGuide(Java面试 + 学习指南)
主要就是Array长度是固定的,创建时要明确指定长度,ArrayList集合长度不固定,具备动态添加、删除元素的能力,只是存储的对象都是引用类型,如果要存基本类型,需要用到包装类装箱(基本类型转换成引用类型),ArrayList还可以使用泛型来保证类型安全。
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
举例:
Integer i = 10; //装箱 int n = i; //拆箱
Set实现类
- HashSet【重点】
- 基于HashCode实现元素不重复。
- 线程不安全。
- 当存入元素的哈希码相同时,会调用equals确认,若果结果为true,则拒绝后者存入。
- LinkedHashSet:
- 跟HashSet一模一样,只是有序。
- TreeSet:
- 基于排列顺序实现元素不重复。
- 存储结构为红黑树(二叉树的一种,平衡二叉树)
- 实现了SortedSet接口,对集合元素自动排序。
- 元素对象的类型必须实现Comparable接口,指定排序规则。
- 通过compareTo方法确定是否为重复元素。
比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
HashSet
、LinkedHashSet
和TreeSet
都是Set
接口的实现类,都能保证元素唯一,并且都不是线程安全的。HashSet
、LinkedHashSet
和TreeSet
的主要区别在于底层数据结构不同。HashSet
的底层数据结构是哈希表(基于HashMap
实现)。LinkedHashSet
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。- 底层数据结构不同又导致这三者的应用场景不同。
HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。
著作权归Guide所有 原文链接:Java集合常见面试题总结(上) | JavaGuide(Java面试 + 学习指南)
Map体系集合

存储一堆数据,无序、无下标、键不可以重复,值可以重复。(HashMap(哈希表)、HashTable、TreeMap(红黑树))
HashMap 简介
关于HashMap这块的内容,我是一起配合着Guide哥的源码分析看的~
HashMap 源码分析 | JavaGuide(Java面试 + 学习指南)
HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。
HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且,HashMap
总是使用 2 的幂作为哈希表的大小。
著作权归Guide所有 原文链接:HashMap 源码分析 | JavaGuide(Java面试 + 学习指南)
HashMap,主要用来存放键值对,是常用的Java集合之一,线程不安全,运行效率快,初始化大小为16,后续每次扩容大小会变为原来的2倍,允许用null作为key或是value,存储结构在JDK1.8之前是哈希表(数组+链表),JDK1.8之后,当每个链表的长度大于8,且数组元素的个数大于等于64时,会调整为红黑树,目的是提高代码执行效率。
这里引用下Guide哥解释的拉链法是个什么
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
HashMap 和 Hashtable 的区别
- 线程是否安全:
HashMap
是非线程安全的,Hashtable
是线程安全的,因为Hashtable
内部的方法基本都经过synchronized
修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap
吧!);- 效率: 因为线程安全的问题,
HashMap
要比Hashtable
效率高一点。另外,Hashtable
基本被淘汰,不要在代码中使用它;- 对 Null key 和 Null value 的支持:
HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出NullPointerException
。- 初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值,
Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么Hashtable
会直接使用你给定的大小,而HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)。也就是说HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。- 底层数据结构: JDK1.8 以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable
没有这样的机制。
著作权归Guide所有 原文链接:Java集合常见面试题总结(下) | JavaGuide(Java面试 + 学习指南)
HashTable,线程安全,运行效率慢;不允许null作为key或是value。现在已经不再使用了。如果想保证线程安全,可以使用 ConcurrentHashMap
。
HashMap 多线程操作导致死循环问题
JDK1.7 及之前版本的
HashMap
在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用
HashMap
,因为多线程下使用HashMap
还是会存在数据覆盖的问题。并发环境下,推荐使用ConcurrentHashMap
。一般面试中这样介绍就差不多,不需要记各种细节,个人觉得也没必要记。
著作权归Guide所有 原文链接:Java集合常见面试题总结(下) | JavaGuide(Java面试 + 学习指南)
ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
# JDK1.8 之前
Java7 ConcurrentHashMap 存储结构
首先将数据分为一段一段(这个“段”就是
Segment
)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap
是由Segment
数组结构和HashEntry
数组结构组成。
Segment
继承了ReentrantLock
,所以Segment
是一种可重入锁,扮演锁的角色。HashEntry
用于存储键值对数据。static class Segment<K,V> extends ReentrantLock implements Serializable { }
一个
ConcurrentHashMap
里包含一个Segment
数组,Segment
的个数一旦初始化就不能改变。Segment
数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。
Segment
的结构和HashMap
类似,是一种数组和链表结构,一个Segment
包含一个HashEntry
数组,每个HashEntry
是一个链表结构的元素,每个Segment
守护着一个HashEntry
数组里的元素,当对HashEntry
数组的数据进行修改时,必须首先获得对应的Segment
的锁。也就是说,对同一Segment
的并发写入会被阻塞,不同Segment
的写入是可以并发执行的。# JDK1.8 之后
Java8 ConcurrentHashMap 存储结构
Java 8 几乎完全重写了
ConcurrentHashMap
,代码量从原来 Java 7 中的 1000 多行,变成了现在的 6000 多行。
ConcurrentHashMap
取消了Segment
分段锁,采用Node + CAS + synchronized
来保证并发安全。数据结构跟HashMap
1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。Java 8 中,锁粒度更细,
synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
著作权归Guide所有 原文链接:Java集合常见面试题总结(下) | JavaGuide(Java面试 + 学习指南)
ConcurrentHashMap就相当于线程安全的HashMap
,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
著作权归Guide所有 原文链接:Java集合常见面试题总结(下) | JavaGuide(Java面试 + 学习指南)
ConcurrentHashMap就相当于线程安全的HashMap