1.Java 容器都有哪些?
在 Java 中,容器(Collection)是指用于存储和管理对象的集合类,主要分为两大类:Collection(单元素集合)和 Map(键值对集合)
(1)Collection 接口(单元素集合)
- List(有序、可重复)
- ArrayList:
- 特点:基于动态数组,查询快,增删慢
- 适用场景:高频随机访问,如缓存列表
- LinkedList:
- 特点:基于双向链表,增删快,查询慢
- 适用场景:频繁插入/删除,如队列、栈
- Vector:
- 特点:线程安全的动态数组(方法同步),性能差
- 适用场景:遗留代码(已被 Collections.synchronizedList 替代)
- Stack:
- 特点:后进先出(LIFO)的栈结构,继承自Vector
- 适用场景:需要栈操作的场景(推荐用 Deque 替代)
- ArrayList:
- Set(无序、不可重复)
- HashSet:
- 特点:基于 HashMap 实现,无序
- 适用场景:快速去重,不关心顺序
- LinkedHashSet:
- 特点:维护插入顺序的 HashSet
- 适用场景:需要保持插入顺序的去重
- TreeSet:
- 特点:基于红黑树实现,元素按自然顺序或自定义比较器排序
- 适用场景:需要有序且去重的集合
- HashSet:
- Queue/Deque(队列)
- LinkedList:
- 特点:可作双向队列(Deque接口)
- 适用场景:通用队列/栈实现
- PriorityQueue:
- 特点:基于堆的优先级队列,元素按优先级排序
- 适用场景:任务调度(如定时任务)
- ArrayDeque:
- 特点:基于循环数组的双端队列,性能优于LinkedList
- 适用场景:高频队列/栈操作(推荐替代Stack)
- LinkedList:
(2)Map接口(键值对集合)
- HashMap:
- 特点:基于哈希表,键无序
- 适用场景:通用键值存储,高频读写
- LinkedHashMap:
- 特点:维护插入顺序或访问顺序的 HashMap
- 适用场景:需要保持顺序的缓存(如LRU)
- TreeMap:
- 特点:基于红黑树,键按自然顺序或自定义比较器排序
- 适用场景:需要有序键值对
- Hashtable:
- 特点:线程安全的 Map(方法同步),性能差
- 适用场景:遗留代码(推荐用ConcurrentHashMap)
- ConcurrentHashMap:
- 特点:分段锁实现的线程安全 HashMap,高并发优化
- 适用场景:多线程环境下的键值存储
- WeakHashMap:
- 特点:键为弱引用,GC 时自动移除无引用的键值对
- 适用场景:缓存临时映射关系
2.Collection 和 Collections 有什么区别?
(1)本质不同
- Collection
- 类型:接口
- 作用:定义单列集合的通用行为(如 List/Set)
- Collections
- 类型:工具类
- 作用:提供操作集合的静态工具方法
(2)核心功能不同
- Collection
- 地位:是 Java 集合框架的根接口之一(另一个是Map)
- 子接口:
- List(有序可重复,如 ArrayList)
- Set(无序不可重复,如 HashSet)
- Queue(队列,如 LinkedList)
- 常用方法:
boolean add(E e); // 添加元素
boolean remove(Object o); // 删除元素
int size(); // 获取元素数量
Iterator<E> iterator(); // 获取迭代器
- Collections
- 地位:一个包含静态方法的工具类,用于操作或返回集合
- 常用方法:
// 排序
static void sort(List<T> list);
// 线程安全包装
static <T> List<T> synchronizedList(List<T> list);
// 不可变集合
static <T> List<T> unmodifiableList(List<? extends T> list);
// 二分查找
static int binarySearch(List<? extends Comparable<? super T>> list, T key);
// 反转集合
static void reverse(List<?> list);
3.List、Set、Map 之间的区别是什么?
(1)核心区别
- List
- 接口归属:Collection 子接口
- 数据结构:有序、可重复的序列
- 元素访问:通过索引(get(index))
- Set
- 接口归属:Collection 子接口
- 数据结构:无序、不可重复的集合
- 元素访问:只能遍历(无直接访问方式)
- Map
- 接口归属:独立接口(非 Collection)
- 数据结构:键值对(Key-Value)存储
- 元素访问:通过键(get(key))
(2)扩展问题
- List 和 Set 能否互相转换?
- 可以,但会丢失特性
- List -> Set:自动去重,但顺序可能丢失
- Set -> List:保留去重后的元素,转为有序列表
- 可以,但会丢失特性
List<String> list = Arrays.asList("A", "B", "A");
Set<String> set = new HashSet<>(list); // 去重 → [A, B]
List<String> newList = new ArrayList<>(set); // 转为 List
- Map 的 key 能否为 null?
- 依赖具体实现
- HashMap/LinkedHashMap:允许一个 null 键
- TreeMap:不允许(需支持排序,null 无法比较)
- 依赖具体实现
4.HashMap 和 Hashtable 有什么区别?
(1)核心区别
- HashMap
- 线程安全:非线程安全(需手动同步)
- 性能:更高(无同步开销)
- 是否允许 null:允许 null 键和 null 值
- 初始容量与扩容:默认16,扩容为2倍
- 推荐使用场景:单线程环境、高并发时用 ConcurrentHashMap
- Hashtable
- 线程安全:线程安全(方法用 synchronized 修饰)
- 性能:较低(同步锁导致性能损耗)
- 是否允许 null:不允许 null 键和 null 值
- 初始容量与扩容:默认11,扩容为2倍+1
- 推荐使用场景:遗留代码(已不推荐使用)
(2)扩展问题
- 为什么 Hashtable 不允许 null 键或值?
- 早期设计时,Hashtable 的 get() 和 put() 方法用 null 表示键不存在,导致歧义。而 HashMap 后来明确区分了 null 键和键不存在的场景
- HashMap 和 Hashtable 的哈希算法有何不同?
- HashMap 通过 hash(key) 计算哈希值(Java 8 引入树化优化)
- Hashtable 直接使用 key.hashCode()(无二次哈希处理,易冲突)
5. 详细解释ConcurrentHashMap
ConcurrentHashMap 是 Java 并发包 (java.util.concurrent) 中提供的线程安全哈希表实现,它通过精妙的设计在高并发环境下提供了优异的性能。
(1)设计演进
- JDK 7 实现(分段锁)
- 分段锁机制:将整个哈希表分成多个段(Segment),每个段相当于一个小的 HashMap
- 锁粒度:每次只锁定一个段,其他段仍可被其他线程访问
- 并发级别:默认创建 16 个段,意味着最多支持 16 个线程并发写操作
- JDK 8 实现(CAS + synchronized)
- 锁粒度细化:放弃分段锁,改为对每个桶(bucket)的头节点加锁
- CAS 操作:使用 Compare-And-Swap 实现无锁化读取和部分更新
- 树化优化:当链表长度超过阈值(默认8)时转换为红黑树
(2)核心特性
- 线程安全实现
- 读操作:完全无锁,基于 volatile 保持可见性
- 写操作:
- 对空桶使用 CAS 插入
- 对非空桶使用 synchronized 锁定头结点
- 扩容:支持多线程协同扩容
- 数据结构
transient volatile Node<K,V>[] table; // 哈希桶数组
private transient volatile int sizeCtl; // 控制标识符
(3)关键操作分析
- put 操作流程
- 计算 key 的哈希值
- 如果表为空,初始化表
- 定位到具体桶:
- 如果桶为空,CAS 插入新节点
- 如果桶不为空,synchronized 锁定头节点后处理:
- 链表:遍历查找,存在则更新,不存在则追加
- 红黑树:按树结构插入
- 判断是否需要树化或扩容
- get 操作流程
- 计算 key 的哈希值
- 定位到具体桶
- 遍历链表或树查找匹配节点
- 返回找到的值(全程无锁)
(4)并发控制机制
- sizeCtl 字段
- 负数:表示表正在初始化或扩容
- 0:默认初始值
- 正数:表示扩容阈值或初始容量
- 扩容策略
- 触发条件:元素数量超过容量*负载因子(默认0.75)
- 多线程协助:其他线程检测到扩容可参与迁移工作
- 渐进式迁移:不会一次性完成所有迁移
注:
(1)弱一致性迭代器:迭代器反映的是创建时刻或之前的状态
(2)批量操作:如 putAll 不是原子性的
(3)null 限制:不允许 null 键或值(与 HashMap 不同)
(4)内存消耗:比 HashMap 占用更多内存
6.如何选择 HashMap 和 TreeMap?
- 需要极速查询 -> HashMap
- 需要键有序 -> TreeMap
7.说一下 HashMap 的实现原理?
HashMap 是 Java 中最常用的哈希表实现,它基于键值对存储数据,提供高效的插入、删除和查找操作
(1)底层数据结构
- 数组 + 链表的基本结构
- 当链表长度超过阈值(默认8)时,转换为红黑树
- 当树节点数小于阈值(默认6)时,退化为链表
(2)哈希算法
- 哈希计算过程
- 首先调用键对象的 hashCode() 方法获取原始哈希值
- 通过扰动函数对原始哈希值进行二次处理:
- 高16位与低16位进行异或运算
- 目的是减少哈希冲突
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 最终通过 (n - 1) & hash 计算桶下标(n是数组长度)
- 解决哈希冲突的方法
- 链地址法:相同桶中的元素以链表形式存储
- 红黑树优化:当链表过长时转为树结构
(3)关键操作分析
- put 操作流程
- 计算键的哈希值
- 如果哈希表为空,调用 resize() 初始化
- 计算桶下标 (n - 1) & hash
- 处理该桶的情况:
- 无节点:直接新建节点插入
- 有节点:
- 如果是树节点,调用红黑树的插入方法
- 如果是链表,遍历链表:
- 找到相同key则更新value
- 未找到则在链表尾部插入
- 插入后检查:
- 链表长度是否超过树化阈值
- 总节点数是否超过阈值(容量×负载因子)
- 如需要则进行扩容或树化
- get 操作流程
- 计算键的哈希值
- 如果表不为空且目标桶不为空:
- 如果是树节点,调用红黑树的查找方法
- 如果是链表,顺序遍历查找
- 找到则返回对应值,否则返回null
(4)扩展问题
- 为什么选择红黑树而不是其他平衡树?
- 红黑树在插入/删除时需要的旋转操作较少,综合性能更好
- 为什么树化阈值是8?
- 根据泊松分布,哈希冲突达到8的概率极低(约0.00000006),此时转为树结构是合理的权衡
- HashMap允许null键的原理是什么?
- 当key为null时,哈希值固定为0,存储在数组的第一个桶中
8.说一下 HashSet 的实现原理?
HashSet 是 Java 集合框架中基于 HashMap 实现的 Set 集合,它用于存储不重复的元素
(1)底层实现
- 基本结构
- 基于 HashMap 实现:HashSet 内部使用 HashMap 来存储元素
- 值存储方式:所有元素作为 HashMap 的 key 存储
- 虚拟值:HashMap 的 value 统一使用一个静态的 PRESENT 对象
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object(); // 虚拟值
(2)核心特性
- 元素唯一性:依赖元素的 hashCode() 和 equals() 方法
- 允许null元素:可以包含一个null元素
- 无序性:不保证元素的迭代顺序
- 非线性安全:多线程访问需要外部同步
- 快速访问:平均时间复杂度O(1)
(3)关键操作实现
- 添加元素(add)
- 实际调用 HashMap 的 put 方法
- 如果元素已存在(返回旧值非null),则添加失败
- 如果元素不存在(返回null),则添加成功
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
- 删除元素(remove)
- 调用 HashMap 的 remove 方法
- 如果元素存在(返回PRESENT),则删除成功
- 如果元素不存在(返回null),则删除失败
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
- 包含判断(contains)
- 直接调用 HashMap 的 containsKey 方法
public boolean contains(Object o) {
return map.containsKey(o);
}
(4)扩展问题
- 为什么HashSet允许null元素?
- 因为HashMap允许null键,HashSet内部使用HashMap存储元素
- HashSet的迭代顺序是否稳定?
- 不保证稳定,即使不修改集合,多次迭代的顺序也可能不同
- 如何保证自定义对象在HashSet中的唯一性?
- 必须正确重写hashCode()和equals()方法,确保逻辑相等的对象返回相同的哈希码
- HashSet和TreeSet如何选择?
- 需要快速访问和无序存储选HashSet;需要元素有序选TreeSet
9.ArrayList 和 LinkedList 的区别是什么?
(1)底层数据结构
- ArrayList
- 数据结构:动态数组
- 内存布局:连续内存空间
- 节点结构:无
- LinkedList
- 数据结构:双向链表
- 内存布局:非连续内存(通过指针连接)
- 节点结构:包含前驱/后继指针的节点对象
(2)时间复杂度
- 随机访问(get/set)
- ArrayList:时间复杂度O(1)
- LinkedList:时间复杂度O(n)
- 原因
- ArrayList直接通过下标计算内存偏移量访问
- LinkedList需要从头到尾遍历到指定位置
- 插入/删除操作
- ArrayList
- 头部:O(n)
- 中间:O(n)
- 尾部:O(1)(均摊)
- LinkedList
- 头部:O(1)
- 中间:O(n)
- 尾部:O(1)
- 说明
- ArrayList 的尾部插入在未扩容时为O(1),扩容时为O(n)
- LinkedList 在任意位置插入删除只需修改指针,但中间位置需要先遍历到该位置
- ArrayList
(3)使用场景
- ArrayList
- 需要频繁随机访问元素
- 主要操作是在列表尾部添加/删除元素
- 内存资源较紧张
- 需要遍历操作(CPU缓存命中率高)
- LinkedList
- 需要频繁在头部/中间插入删除元素
- 需要实现队列/双端队列功能
- 列表大小变化较大且难以预估
- 不需要频繁随机访问
(4)常见误区
- LinkedList在任何位置插入都更快?
- 事实:只有在已知节点引用时才是O(1),按索引插入仍需O(n)遍历
- ArrayList查找比LinkedList快?
- 限定条件:仅指按索引查找,按值查找两者都是O(n)
- LinkedList更节省内存?
- 事实:对于小型对象,LinkedList的内存开销更大
- 应该总是优先使用ArrayList?
- 事实:在特定场景(如高频头部操作)LinkedList表现更好
10.如何实现数组和 List 之间的转换?
(1)数组 转 List
- Arrays.asList() (最常用)
- 特点
- 返回的是固定大小的 List(基于原数组)
- 不支持添加/删除操作(会抛 UnsupportedOperationException)
- 修改 List 会影响原数组(两者共享存储空间)
- 特点
String[] array = {"A", "B", "C"};
List<String> list = Arrays.asList(array);
- new ArrayList<>(Arrays.asList())
- 优点
- 创建独立的 ArrayList,可自由增删元素
- 修改 List 不会影响原数组
- 优点
String[] array = {"A", "B", "C"};
List<String> list = new ArrayList<>(Arrays.asList(array));
- Collections.addAll()
- 适用场景
- 需要将多个数组合并到一个已存在的 List
- 适用场景
String[] array = {"A", "B", "C"};
List<String> list = new ArrayList<>();
Collections.addAll(list, array);
- Java 8 Stream (推荐)
- 优势
- 可以方便地进行过滤、映射等操作
- 优势
String[] array = {"A", "B", "C"};
List<String> list = Arrays.stream(array).collect(Collectors.toList());
//只转换符合条件的元素
List<String> filteredList = Arrays.stream(array)
.filter(s -> s.startsWith("A"))
.collect(Collectors.toList());
- 手动遍历
- 适用场景
- 需要特殊处理每个元素时
- 适用场景
String[] array = {"A", "B", "C"};
List<String> list = new ArrayList<>();
for (String s : array) {
list.add(s);
}
(2)List 转 数组
- toArray() (无参)
- 缺点
- 只能得到 Object[] 类型数组
- 缺点
List<String> list = Arrays.asList("A", "B", "C");
Object[] array = list.toArray(); // 返回Object数组
- toArray(T[]) (推荐)
List<String> list = Arrays.asList("A", "B", "C");
String[] array = list.toArray(new String[0]);
//传入大小足够的数组可提升性能
String[] array = list.toArray(new String[list.size()]);
- Java 8 Stream
- 优势
- 可结合其他 Stream 操作
- 优势
List<String> list = Arrays.asList("A", "B", "C");
String[] array = list.stream().toArray(String[]::new);
//过滤后转数组
String[] filteredArray = list.stream()
.filter(s -> s.length() > 1)
.toArray(String[]::new);
- 手动遍历
- 适用场景
- 需要特殊处理每个元素时
- 适用场景
List<String> list = Arrays.asList("A", "B", "C");
String[] array = new String[list.size()];
for (int i = 0; i < list.size(); i++) {
array[i] = list.get(i);
}
(3)特殊类型处理
- 基本类型数组
int[] intArray = {1, 2, 3};
// 需要装箱处理
List<Integer> intList = Arrays.stream(intArray)
.boxed()
.collect(Collectors.toList());
// 反向转换
int[] newArray = intList.stream()
.mapToInt(Integer::intValue)
.toArray();
- 多维数组
String[][] multiArray = {{"A","B"}, {"C","D"}};
List<List<String>> multiList = Arrays.stream(multiArray)
.map(Arrays::asList)
.collect(Collectors.toList());
(4)注意事项
- Arrays.asList() 的陷阱
- 返回的 List 不可变大小
- 对原始数组的修改会影响 List
String[] array = {"A", "B"};
List<String> list = Arrays.asList(array);
array[0] = "AA"; // list.get(0) 也会变成 "AA"
- 泛型数组问题
- Java 不允许直接创建泛型数组
- 解决方案:
List<String> list = ...;
String[] array = list.toArray(new String[0]);
- 性能考虑
- 大数组转换时,new ArrayList<>(Arrays.asList()) 比手动添加更高效
- List 转数组时,预分配正确大小的数组可避免二次拷贝