【JavaSE 复盘】集合框架底层原理与常见陷阱从 List、Set 到 Map

Java集合框架底层原理与常见陷阱

目录

一、引言

二、List 接口详解

✅ ArrayList:动态数组实现

📌 示例1:基本使用

📌 底层结构:

📌 示例2:扩容测试

📌 常见误区:

✅ LinkedList:链表实现

📌 示例3:插入效率对比

📌 底层结构:

三、Set 接口详解

✅ HashSet:基于 HashMap 实现的无序不可重复集合

📌 示例4:基本使用

📌 去重机制:

📌 示例5:自定义类的去重

✅ TreeSet:有序 Set,支持自然排序或定制排序

📌 示例6:自然排序

📌 示例7:定制排序

四、Map 接口详解

✅ HashMap:哈希表实现,非线程安全

📌 示例8:基本使用

📌 JDK8 的优化:

📌 示例9:哈希冲突演示

✅ ConcurrentHashMap:线程安全的 HashMap 替代品

📌 示例10:多线程 put

五、迭代器与并发修改异常

✅ ConcurrentModificationException 是怎么产生的?

📌 示例11:错误的遍历删除方式

✅ 正确做法:使用迭代器删除

六、总结


 

一、引言

在 Java 开发中,集合类是我们最常用的工具之一。但很多同学只停留在“会用”的层面,而没有真正理解其底层原理和潜在陷阱。

本文将带你:

  • 理解 ArrayList 的扩容机制;
  • 掌握 HashMap 在 JDK7 和 JDK8 的差异;
  • 学会避免 ConcurrentModificationException
  • 了解 HashSet 如何实现去重;
  • 明确线程安全集合类的选择;
  • 结合实际代码示例,帮助你在面试中游刃有余。

二、List 接口详解

✅ ArrayList:动态数组实现

📌 示例1:基本使用
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
System.out.println(list); // 输出 [A, B]
📌 底层结构:
  • 内部维护一个 Object[] 数组
  • 默认初始容量为 10
  • 超出容量时自动扩容(增长原容量的 50%);
📌 示例2:扩容测试
ArrayList<Integer> list = new ArrayList<>(5);
for (int i = 0; i < 20; i++) {
    list.add(i);
    System.out.println("Size: " + list.size() + ", Capacity: " + getCapacity(list));
}

📌 说明:

  • 可通过反射获取 ArrayList 的 elementData 数组长度来查看当前容量;
  • 扩容是一个性能开销较大的操作,频繁添加建议指定初始容量
📌 常见误区:

❌ 不要在循环中频繁添加元素而不指定初始容量;
✅ 合理预估大小可以提高性能。


✅ LinkedList:链表实现

📌 示例3:插入效率对比
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();

// 插入到中间位置
for (int i = 0; i < 10000; i++) {
    arrayList.add(i);
    linkedList.add(i);
}

long start = System.currentTimeMillis();
arrayList.add(5000, 999);
System.out.println("ArrayList 插入耗时:" + (System.currentTimeMillis() - start));

start = System.currentTimeMillis();
linkedList.add(5000, 999);
System.out.println("LinkedList 插入耗时:" + (System.currentTimeMillis() - start));

📌 输出:

ArrayList 插入耗时:3ms
LinkedList 插入耗时:1ms
📌 底层结构:
  • 使用双向链表;
  • 插入删除快(O(1));
  • 随机访问慢(O(n));

📌 适用场景:

  • 频繁插入/删除;
  • 不需要随机访问;

三、Set 接口详解

✅ HashSet:基于 HashMap 实现的无序不可重复集合

📌 示例4:基本使用
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // 重复元素不会被添加
System.out.println(set); // 输出 [banana, apple]
📌 去重机制:
  • 依赖 hashCode() 和 equals() 方法;
  • 如果两个对象的 hashCode() 相同,再调用 equals() 比较;
  • 若返回 true,则视为重复;
📌 示例5:自定义类的去重
class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Person)) return false;
        return this.name.equals(((Person) obj).name);
    }
}

Set<Person> people = new HashSet<>();
people.add(new Person("Tom"));
people.add(new Person("Tom")); // 会被认为是重复元素

📌 开发建议:

  • 自定义类放入 HashSet 前必须重写 hashCode() 和 equals()
  • 否则默认使用 Object 类的方法,导致无法正确去重。

✅ TreeSet:有序 Set,支持自然排序或定制排序

📌 示例6:自然排序
Set<Integer> set = new TreeSet<>();
set.add(3);
set.add(1);
set.add(2);
System.out.println(set); // 输出 [1, 2, 3]
📌 示例7:定制排序
Set<String> set = new TreeSet<>((o1, o2) -> o2.compareTo(o1));
set.add("a");
set.add("b");
set.add("c");
System.out.println(set); // 输出 [c, b, a]

📌 注意:

  • TreeSet 不允许插入 null;
  • 否则抛出 NullPointerException(因为要比较);

四、Map 接口详解

✅ HashMap:哈希表实现,非线程安全

📌 示例8:基本使用
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
System.out.println(map.get("A")); // 输出 1
📌 JDK8 的优化:
  • 引入红黑树解决哈希冲突;
  • 当链表长度 ≥ 8 且桶数 ≥ 64 时,链表转为红黑树;
  • 查询效率从 O(n) 提升到 O(log n);
📌 示例9:哈希冲突演示
class BadKey {
    @Override
    public int hashCode() {
        return 1; // 所有对象 hash 相同
    }

    @Override
    public boolean equals(Object obj) {
        return super.equals(obj);
    }
}

Map<BadKey, String> map = new HashMap<>();
for (int i = 0; i < 1000; i++) {
    map.put(new BadKey(), "value" + i);
}

📌 分析:

  • 所有 key 都落在同一个桶里;
  • 形成一条长链表,查询效率下降;
  • JDK8 会自动将其转为红黑树优化查找效率;

✅ ConcurrentHashMap:线程安全的 HashMap 替代品

📌 示例10:多线程 put
Map<Integer, Integer> map = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 1000; i++) {
    final int key = i;
    executor.submit(() -> map.put(key, key * 2));
}

executor.shutdown();
System.out.println("Size: " + map.size()); // 输出 1000

📌 特点:

  • 使用分段锁(JDK7)或 CAS + synchronized(JDK8);
  • 支持高并发读写;
  • 多线程下推荐使用;

五、迭代器与并发修改异常

✅ ConcurrentModificationException 是怎么产生的?

📌 示例11:错误的遍历删除方式
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");

for (String s : list) {
    if (s.equals("A")) {
        list.remove(s); // 抛出 ConcurrentModificationException
    }
}

📌 原因:

  • 使用增强型 for 循环时,内部使用了迭代器;
  • 修改集合结构(非通过迭代器)会触发 fail-fast 机制;
✅ 正确做法:使用迭代器删除
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("A")) {
        it.remove(); // 安全删除
    }
}

📌 总结:

  • 不要使用普通 for 或增强型 for 删除元素;
  • 应使用迭代器的 remove()
  • 或者使用并发集合类(如 CopyOnWriteArrayList);

六、总结

核心知识点内容
ArrayList动态数组,适合随机访问
LinkedList链表结构,适合插入删除
HashSet基于 HashMap,通过 hashCode 和 equals 去重
TreeSet红黑树实现,按键排序
HashMap哈希表 + 链表/红黑树,非线程安全
ConcurrentHashMap线程安全的 Map 实现
ConcurrentModificationException遍历时不能直接修改集合结构
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值