Java容器面试题(1)

1.Java 容器都有哪些?

在 Java 中,容器(Collection)是指用于存储和管理对象的集合类,主要分为两大类:Collection(单元素集合)和 Map(键值对集合)
(1)Collection 接口(单元素集合)
  • List(有序、可重复)
    • ArrayList:
      • 特点:基于动态数组,查询快,增删慢
      • 适用场景:高频随机访问,如缓存列表
    • LinkedList:
      • 特点:基于双向链表,增删快,查询慢
      • 适用场景:频繁插入/删除,如队列、栈
    • Vector:
      • 特点:线程安全的动态数组(方法同步),性能差
      • 适用场景:遗留代码(已被 Collections.synchronizedList 替代)
    • Stack:
      • 特点:后进先出(LIFO)的栈结构,继承自Vector
      • 适用场景:需要栈操作的场景(推荐用 Deque 替代)
  • Set(无序、不可重复)
    • HashSet:
      • 特点:基于 HashMap 实现,无序
      • 适用场景:快速去重,不关心顺序
    • LinkedHashSet:
      • 特点:维护插入顺序的 HashSet
      • 适用场景:需要保持插入顺序的去重
    • TreeSet:
      • 特点:基于红黑树实现,元素按自然顺序或自定义比较器排序
      • 适用场景:需要有序且去重的集合
  • Queue/Deque(队列)
    • LinkedList:
      • 特点:可作双向队列(Deque接口)
      • 适用场景:通用队列/栈实现
    • PriorityQueue:
      • 特点:基于堆的优先级队列,元素按优先级排序
      • 适用场景:任务调度(如定时任务)
    • ArrayDeque:
      • 特点:基于循环数组的双端队列,性能优于LinkedList
      • 适用场景:高频队列/栈操作(推荐替代Stack)
(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 在任意位置插入删除只需修改指针,但中间位置需要先遍历到该位置
(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 转数组时,预分配正确大小的数组可避免二次拷贝
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值