Java集合源码分析之开篇

简介: 初衷Java集合是我们使用最频繁的工具,也是面试的热点,但我们对它的理解仅限于使用上,而且大多数情况没有考虑过其使用规范。本系列文章将跟随源码的思路,分析实现的每个细节,以期在使用时避免各种不规范的坑。在这里,我们会惊艳于开发者优秀的设计,也会感激先辈们付出的艰辛努力,更重要的是知其所以然,少犯错误,写出优秀的代码。许多人对集合类的理解是暴力的,当需要保存对象时就使用ArrayList,当需要保存键值对时就使用HashMap,当需要不可重复时就使用HashSet,等等。而且使用方式也比较单一:

初衷
Java集合是我们使用最频繁的工具,也是面试的热点,但我们对它的理解仅限于使用上,而且大多数情况没有考虑过其使用规范。本系列文章将跟随源码的思路,分析实现的每个细节,以期在使用时避免各种不规范的坑。在这里,我们会惊艳于开发者优秀的设计,也会感激先辈们付出的艰辛努力,更重要的是知其所以然,少犯错误,写出优秀的代码。

许多人对集合类的理解是暴力的,当需要保存对象时就使用ArrayList,当需要保存键值对时就使用HashMap,当需要不可重复时就使用HashSet,等等。而且使用方式也比较单一:

List list = new ArrayList<>();Map<String, String> map = new HashMap<>();Set set = new HashSet<>();// ...
这里我们先不考虑多线程安全问题,这个问题通常有专门的类实现,或者可以通过Collections.synchronizedXXX方法解决。除此之外,我们真的可以如此简单的使用集合吗?

假如数据只有几百、几千个,那么使用何种方式实现差别并不大。但当我们需要处理大数量级的数据时,采用不同的方式效率可能相差百倍甚至更多,这种情况下性能将变得格外重要。例如分别存储于ArrayList和LinkedList的100万条数据,要获取位于位置 i的元素,前者可以瞬间完成,后者则可能需要数秒。这时,使用哪个集合类,怎样合理使用就是我们必须掌握的技能了。

为什么要读本系列文章
如果你也像以上这般使用集合,或者不知道如何优化集合的使用,你都应该读本系列文章。如果你仅有一些点不清晰,也可以在这里找到答案。或者你只是不想阅读枯燥的源码,却对原理很好奇,你也可以阅读本系列文章。如果你只是想应付面试,我想当你坚持把这些文章读完后,你会觉得面试好像也不那么重要了。

本系列文章立足于深刻理解Java集合的原理与实现,读完这些文章后你将获得以下知识:

大量的数据结构知识。

ArrayList有那么多构造函数,使用不同的构造函数会有区别吗?

ArrayList是如何扩容的?

LinkedList如何提供通过位置获取数据的功能的,它的查询效率真的非常低吗?

用数组可以实现队列吗?

影响HashMap性能的因素有哪些?

复杂的红黑树是如何实现的?

LRUCache的底层原理是什么?

……

基础知识概述
对数据的操作,大抵就是增、删、改、查,以及在某些时候根据位置获取数据,有时可能还需要进行排序。改和查又可以理解为一致的操作,因为要修改一条数据需要先找到它,然后替换即可。接下来我们就从增、删、查这三点简要分析下当前使用比较广泛的几种数据结构。

数组
数组在内存中占据一段连续的内存,所有的数据在内存中连续排列。它的大小是固定的,这一特性使得数组对于插入操作并不友好,我们分析ArrayList时就会看到这种操作的复杂。但数组对于位置的访问是极其友好的,它支持所谓RandomAccess特性,这使得基于位置的操作可以迅速完成,其时间复杂度为O(1)。数组的数据顺序与插入顺序一致,所以查询操作需要遍历,其时间复杂度为O(n)。

所以数组最大的优势在于基于位置的访问,在扩展性方面表现无力。

链表
不同于数组,链表是通过指针域来表示数据与数据之间的位置关系的,所以手游账号拍卖平台链表在头部或尾部插入数据的复杂度仅为O(1)。链表不具备RandomAccess特性,所以无法提供基于位置的访问。其查询操作也必须从从到尾遍历,复杂度为O(n)。

所以链表最大的优势在于插入,而查询的表现很一般。

那有没有一种结构能够结合数组和链表的优点,使得查询和插入都具有优秀的表现呢?答案是肯定的,这就是散列表。

散列表
散列表就是Hash Table,这种结构使用key-value形式存储数据,我们经常使用的HashMap、HashTable就基于它。

数组和链表在查询时表现一般的原因在于它们并不记得数据的位置,所以只能用待查询的数据和存储的数据依次比对。散列表使用一种巧妙的方式来减少甚至避免这种依次比对,它的原理是通过一个函数把任何的key转为int,每次查找时只需要执行一次这个函数便可以迅速定位。这个过程是不是像查字典呢?

散列表并不像上述那般完美,因为并不会有一个函数,能够保证所有的key转换结果都不同,也就是会发生所谓的哈希碰撞,而且它必须依赖于其他的数据结构,这部分知识会在后续文章中详细介绍。

良好设计的散列表可以使增、删、查等操作的时间复杂度均为O(1)。

二叉排序树
二叉排序树是解决查询问题的另一方案,如果数据在插入时是有序的,在查询时就可以使用二分法。二分法的原理很简单,比如猜一个在0-100之间的数,第一次猜50就可以直接排除一半的数据,每次按照这个规则就可以很快的获取正确答案。二分法的时间复杂度为O(lg n)。

树的结构对二分法有天然的支持(但这不是树最重要的用途)。二叉排序树牺牲了一部分插入的时间,但提高了查询的速度,同时有序的数据也可以做些其他的操作。如果查询的操作重要性超过了插入,我们应该考虑这种结构。二叉排序树也存在一些不平衡导致效率下降的问题,所以有了AVL树、红黑树,以及用于数据库索引的B树、B+树等概念,关于二叉排序树的知识也会在后续文章中介绍。

分析过程
以上介绍的数据结构的知识是我们理解Java集合类的基础,掌握这些核心原理,我们分析集合类源码时才不会吃力,我们会先对这些数据结构进行简要介绍,其他和本系列文章无关的概念不会涉及,大家可以查阅相关专业书籍进行系统学习。

由于集合类的源码十分庞大,从接口抽象设计到具体实现涉及到数十个类,我们不可能每行代码都进行分析,一些在前面分析过的点在后续部分也会略过,但对于我们应该注意的点都会详细解读。有一些过于复杂的代码,还会用图示进行直观的演示,以帮助理解整个运行机制。

文章中会不可避免地粘贴大量源码,但所有部分都会加上详细的中文注释。另外,粘贴的代码不会截取(某些没必要的会删除),这样便于理解,而不用想看哪行代码再去源码中寻找了。

学习源码的实现仅是我们的目的之一,我们更应该掌握作者优秀的编程思想,理解这样做的初衷,站在更高的角度思考问题。

本系列文章的源码全部基于JDK1.8,不同版本的实现代码可能稍有差别,但核心思想是一致的,希望大家不要被具体的实现带偏了路。

Java集合类分为两大部分:Collection和Map。Collection又主要由List、Queue和Set三大模块组成。本系列文章也会基于这样的结构进行,我们会先了解一些用到的数据结构,然后按照从接口抽象到具体实现的顺序来一步步揭开集合的神秘面纱。

由于Set的结构与Map完全一致,且Set的内部都是基于对应的Map实现的,所以只需要知道Set是什么即可,其具体实现如果感兴趣可以自行阅读源码。

本系列文章不考虑多线程安全问题,与多线程相关的问题十分复杂,以后会对它专门研究。

本系列文章长达20多篇,全部读完需要一定的耐心,但是我相信读完对数据结构和集合一定会有更深的理解,在使用时需要注意哪些点也一定会胸有成竹。

另外由于个人能力有限,文章中若有表达不清晰或解释错误的部分,希望各位看官能够给予批评指正。

目录结构
本系列文章会按照下述结构搭建:

数据结构

Iterable概述

Collection概述

List系列分析

Queue系列分析

Map概述与系列分析

Set简介

本文到此就结束了,接下来的分析内容正在火速连载中,感谢您的关注!

目录
相关文章
|
27天前
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。HashSet基于哈希表实现,提供高效的元素操作;TreeSet则通过红黑树实现元素的自然排序,适合需要有序访问的场景。本文通过示例代码详细介绍了两者的特性和应用场景。
36 6
|
27天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
37 3
|
27天前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
32 2
|
29天前
|
存储 算法 Java
Java Set因其“无重复”特性在集合框架中独树一帜
【10月更文挑战第14天】Java Set因其“无重复”特性在集合框架中独树一帜。本文深入解析Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定的数据结构(哈希表、红黑树)确保元素唯一性,并提供最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的`hashCode()`与`equals()`方法。
28 3
|
7天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
15 2
|
6天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
11天前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
11天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
11天前
|
Java 开发者
|
23天前
|
安全 Java 程序员
深入Java集合框架:解密List的Fail-Fast与Fail-Safe机制
本文介绍了 Java 中 List 的遍历和删除操作,重点讨论了快速失败(fail-fast)和安全失败(fail-safe)机制。通过普通 for 循环、迭代器和 foreach 循环的对比,详细解释了各种方法的优缺点及适用场景,特别是在多线程环境下的表现。最后推荐了适合高并发场景的 fail-safe 容器,如 CopyOnWriteArrayList 和 ConcurrentHashMap。
52 5