在 Java 里,容器类的底层实现机制和设计思想十分复杂,下面将从多个维度深入剖析:
一、Collection 接口体系
1. List 接口
- ArrayList
- 动态扩容机制 :当数组容量不足时,会重新创建一个容量为原数组
1.5
倍的新数组,并把原数组的内容复制过去。比如,初始容量为10
,添加第11
个元素时,就会触发扩容操作,新容量变为15
。 - Fail-Fast 机制 :在使用迭代器遍历 ArrayList 时,如果发现集合的结构被修改(像调用
add
或remove
方法),就会立刻抛出ConcurrentModificationException
,这一机制通过modCount
计数器来实现。
- 动态扩容机制 :当数组容量不足时,会重新创建一个容量为原数组
- LinkedList
- 双向链表结构 :其每个节点(
Node
)都包含三个部分,即前驱节点引用、数据和后继节点引用。这使得在链表头部或尾部进行插入操作的时间复杂度为O(1)
。 - 多接口实现 :LinkedList 同时实现了
List
和Deque
接口,所以它既可以当作列表使用,也能作为双端队列使用。
- 双向链表结构 :其每个节点(
- Vector & Stack
- 线程安全的实现方式 :Vector 的所有公共方法都被
synchronized
修饰,不过这种方式会带来较大的性能开销。 - Stack 的设计缺陷:Stack 继承自 Vector,这违背了组合复用原则,而且它暴露了 Vector 的所有方法,存在安全隐患。
- 线程安全的实现方式 :Vector 的所有公共方法都被
2. Set 接口
- HashSet
- 底层依赖 HashMap :HashSet 内部使用一个
HashMap
来存储元素,元素被存放在 HashMap 的键中,而值则统一为一个静态常量PRESENT
。 - 哈希冲突的处理 :采用链表法(拉链法)来解决哈希冲突。当链表长度超过
8
且数组长度大于64
时,链表会转化为红黑树。
- 底层依赖 HashMap :HashSet 内部使用一个
- TreeSet
- 红黑树结构 :TreeSet 基于
TreeMap
实现,红黑树是一种自平衡的二叉搜索树,它能保证插入、删除和查询操作的时间复杂度都是O(log n)
。 - 元素比较 :TreeSet 要求元素必须实现
Comparable
接口,或者在创建 TreeSet 时传入一个Comparator
。
- 红黑树结构 :TreeSet 基于
3. Queue 接口
- PriorityQueue
- 堆结构:PriorityQueue 基于数组实现的二叉堆(通常是最小堆),父节点的值总是小于或等于子节点的值。
- 排序规则 :元素需要实现
Comparable
接口,或者在创建 PriorityQueue 时传入Comparator
。
- ArrayDeque
- 循环数组:ArrayDeque 使用循环数组来实现双端队列,通过头尾指针来标记队列的边界。
- 无容量限制 :ArrayDeque 的容量会根据需要自动扩容,扩容时新容量是原容量的
2
倍。
二、Map 接口体系
1. HashMap
- 哈希表结构 :在 JDK 8 及以后的版本中,HashMap 采用 数组 + 链表 + 红黑树 的结构。当链表长度超过
8
且数组长度大于64
时,链表会转换为红黑树,以提高查询效率。 - 哈希函数 :
hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
,通过这种方式让高位数据也参与哈希运算,减少哈希冲突。 - 扩容机制 :当元素数量超过
容量 × 负载因子(默认 0.75)
时,HashMap 会进行扩容,新容量是原容量的2
倍,并且需要重新计算哈希值和元素位置。
2. LinkedHashMap
- 双向链表维护顺序:LinkedHashMap 继承自 HashMap,它在 HashMap 的基础上增加了一个双向链表,用于记录元素的插入顺序或访问顺序。
- 访问顺序(access-order) :当
accessOrder
为true
时,每次访问元素(如调用get
方法),该元素会被移动到链表尾部,可用于实现 LRU 缓存。
3. TreeMap
- 红黑树的应用:TreeMap 基于红黑树实现,能够根据键的自然顺序或指定的比较器对键值对进行排序。
- 范围查询 :TreeMap 提供了
subMap()
、headMap()
、tailMap()
等方法,可用于高效地进行范围查询。
4. ConcurrentHashMap
- 分段锁(JDK 7 及以前):在 JDK 7 及以前的版本中,ConcurrentHashMap 使用分段锁(Segment),每个 Segment 相当于一个小的 HashMap,不同的 Segment 可以并发访问。
- CAS + synchronized(JDK 8 及以后) :JDK 8 及以后的版本摒弃了分段锁,采用
CAS
(Compare-And-Swap)和synchronized
来保证并发操作的安全性。对数组的每个槽位(Node)进行加锁,锁的粒度更小,并发性能更高。
三、设计模式与最佳实践
1. 迭代器模式
- Iterator 接口 :Collection 接口继承了
Iterable
接口,所有实现 Collection 接口的类都需要提供iterator()
方法,该方法返回一个Iterator
对象,用于遍历集合中的元素。 - ListIterator :List 接口还提供了
listIterator()
方法,返回的ListIterator
支持双向遍历和元素修改操作。
2. 适配器模式
- Arrays.asList():该方法可以将数组转换为 List,它返回的是一个固定大小的列表,不支持添加或删除元素操作。
- Collections 工具类 :
Collections.synchronizedList()
、Collections.unmodifiableSet()
等方法可以将非线程安全的集合转换为线程安全的集合,或者将可变集合转换为不可变集合。
3. 性能优化建议
- 初始容量设置:在创建 ArrayList、HashMap 等容器时,合理设置初始容量,避免频繁扩容带来的性能开销。
- 选择合适的容器 :
- 如果需要频繁随机访问元素,可选择 ArrayList。
- 如果需要频繁插入或删除元素,可选择 LinkedList。
- 如果需要保证线程安全,在单线程环境下可使用
Collections.synchronizedXXX()
,在多线程环境下推荐使用 ConcurrentHashMap。
四、并发容器详解
1. BlockingQueue
- 阻塞操作 :BlockingQueue 提供了
put()
和take()
方法,当队列满时,put()
方法会阻塞;当队列空时,take()
方法会阻塞。 - 应用场景 :常用于生产者 - 消费者模式,典型实现有
ArrayBlockingQueue
、LinkedBlockingQueue
等。
2. CopyOnWriteArrayList
- 写时复制机制 :在进行写操作(如
add
、remove
)时,CopyOnWriteArrayList 会先复制一份新的数组,在新数组上进行修改,修改完成后再将原数组的引用指向新数组。 - 适用场景:适用于读多写少的场景,因为读操作不需要加锁,性能较高。
五、容器类的常见陷阱
1. 泛型擦除
- 原理 :Java 的泛型在编译时会进行类型擦除,例如
List<Integer>
和List<String>
在运行时实际上是相同的类型。 - 影响:无法在运行时获取泛型的具体类型,可能会导致一些类型转换异常。
2. 快速失败机制
- Fail-Fast 与 Fail-Safe :
- Fail-Fast(如 ArrayList):在遍历过程中,如果发现集合结构被修改,会立即抛出异常。
- Fail-Safe(如 CopyOnWriteArrayList):在遍历过程中允许集合结构被修改,因为它遍历的是原集合的副本。
3. 内存泄漏
- 强引用问题:如果容器持有对象的强引用,即使对象在其他地方已经不再使用,垃圾回收器也无法回收该对象,从而导致内存泄漏。
- 解决方案 :可以使用
WeakHashMap
等弱引用容器,当对象的其他强引用被释放后,容器中的弱引用不会阻止对象被垃圾回收。
六、Java 9+ 的增强
1. 不可变集合工厂方法
- 示例 :
List.of()
、Set.of()
、Map.of()
等方法可以创建不可变集合,这些集合不允许添加、删除或修改元素。 - 优势:代码更简洁,避免了意外修改集合的风险。
2. Stream API
- 流式操作 :容器类可以通过
stream()
或parallelStream()
方法获取流,然后进行过滤、映射、聚合等操作,支持函数式编程。
总结
Java 容器类的设计充分体现了面向对象、数据结构和并发编程的思想。深入理解容器类的底层实现,有助于我们在实际开发中选择合适的容器,优化代码性能,避免常见的陷阱。