Collections类源码初探
最近重温Java知识,遇到不懂的问题搜索互联网/博客很难直接找到答案,还好如今有了chatGPT,比网上大多数CV复读机/纯文档翻译的内容更有用。本文尝试深入探索Java Collections类的底层原理,领悟代码的设计初衷,学习借鉴优秀的编程思想,并启发开发者写出高质量代码。
包 Package
Collections类位于 java.util 包中。源代码第一行 package java.util;
表示Collections所在的包(Package)。java.util包还包含其他的类、接口,在同一个包下Java编译器默认会自动加载这些类。包的设计是为了方便代码的组织和管理,使用时不需要import语句导入每一个文件,也可以避免命名冲突(不同的包名下可以有相同的类名)。
静态成员
Collections是一个算法合集,可以看到构造器是由 private
修饰的,无法实例化。因此成员和方法都是 static 静态的。
接下来的代码是静态成员,注释已经说明了这些常量是为了更好算法性能的调整参数。下面所有阈值都是在元素个数较小或者支持随机访问RandomAccess的列表中依据经验选取的参数。变量名第一个单词表示对应的算法。后面通过源码将分析这些成员的作用。
private static final int BINARYSEARCH_THRESHOLD = 5000;
private static final int REVERSE_THRESHOLD = 18;
private static final int SHUFFLE_THRESHOLD = 5;
private static final int FILL_THRESHOLD = 25;
private static final int ROTATE_THRESHOLD = 100;
private static final int COPY_THRESHOLD = 10;
private static final int REPLACEALL_THRESHOLD = 11;
private static final int INDEXOFSUBLIST_THRESHOLD = 35;
静态方法
排序
排序算法方法体比较简单,根据形参是否带比较器 Comparator,有两种实现。不带比较器的 sort
方法由 public static <T extends Comparable<? super T>>
修饰,后面是一个泛型定义的语法。先看内层,? super T
是Comparable接口的泛型参数,其中 ? super
是下界通配符,保证Comparable接口能够使用T或T的超类作为类型参数。因此 sort 方法的泛型类型 T 必须是实现了Comparable接口,并且Comparable接口的类型参数是T或者T的父类。
Comparator和Comparable区别
- Comparable是一个内部比较器接口,它对实现这个接口的对象进行自然排序的方式提供了统一的规范,因此只有一个方法 compareTo()。
- Comparator是一个外部比较器接口,它允许在实现这个接口的类之外进行比较操作,因此有两个方法 compare() 和 equals()。
- 基本数据类型的装箱类型,也就是包装类,如Integer、Double等都实现了Comparable接口,它们可以进行自然排序,具体实现是通过实现Comparable
接口中的compareTo(T o)方法来实现的。当我们需要自定义实现排序规则时,可以使用Comparator来定义。
public final class Integer extends Number implements Comparable<Integer> {
// ...
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
// ...
}
我们回到sort,跟踪调用的 sort
源码,可以看到底层提供了 mergeSort
和 Timsort.sort
两种排序方法。在JDK8中,Arrays.sort()和Collections.sort()都是采用了Timsort算法(LegacyMergeSort.userRequested
默认为 false)。
jdk1.7之前的排序是归并排序,legacyMergeSort此方法就是1.7为了兼容之前版本的归并排序。
Timsort是一种排序算法,由Tim Peters在2002年为Python实现的。它是融合归并排序(Merge Sort)和插入排序(Insertion Sort)的一种排序算法,具有稳定性,且在最坏情况下的时间复杂度也能达到O(nlogn)。
二分查找
列表必须有序(根据Comparable升序),只有对于支持随机访问的列表可以达到 O(logn)
的查找性能。否则即便是 logn 次比较,也需要线性的遍历时间(迭代器获得中间节点)。
反转列表
这里 REVERSE_THRESHOLD
派上用场,当列表长度小于18,或者支持随机访问时,直接调用 swap 方法交换首尾元素。否则则用迭代器来进行赋值/设置(set方法)。
为什么不都用swap来交换呢?查看swap源码,有两种实现:
public static void swap(List<?> list, int i, int j) {
// instead of using a raw type here, it's possible to capture
// the wildcard but it will require a call to a supplementary
// private method
final List l = list;
l.set(i, l.set(j, l.get(i)));
}
private static void swap(Object[] arr, int i, int j) {
Object tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
对于列表的反转,会调用第一个方法,其中 set 方法赋值会创建新的对象并返回旧的对象引用,因此 list.set(i, list.set(j, list.get(i)))
代码产生了两个中间临时对象。在大规模数据操作中,这种频繁的对象创建和回收会影响系统性能。
因此,在列表长度大于等于 18 要进行反转时,Java提供了更加高效的 swap 方式,即仅通过一个对象引用来保存交换过程种的对象位置,然后通过迭代器设置新的位置。
// else 部分
ListIterator fwd = list.listIterator();
ListIterator rev = list.listIterator(size);
for (int i=0, mid=list.size()>>1; i<mid; i++) {
Object tmp = fwd.next();
fwd.set(rev.previous());
rev.set(tmp);
}
打乱 shuffle
打乱算法用到了洗牌算法,逆序将第 i 个元素与前 i 个的随机元素交换。与前面类似,当列表长度大于等于SHUFFLE_THRESHOLD
时,选择将 List转化为 对象数组 Object[] arr = list.toArray();
,然后执行交换过程,最后再通过迭代器完成赋值。
覆盖 fill / 拷贝 copy / 替换 replaceAll
fill方法将列表中所有元素替换为传入值,copy方法将源列表内容拷贝到目标列表(要求不小于源列表长度),replaceAll将所有原始值替换为新的值。同样,在大于等于对应的阈值 FILL_THRESHOLD
、 COPY_THRESHOLD
和 REPLACEALL_THRESHOLD
时,使用迭代器避免频繁的对象创建和销毁的开销。
旋转 rotate
这个方式实现了列表元素的循环移动(右移)。核心算法逻辑为三次就地反转,这避免了额外的内存开销。
// mid 为 size减去右移距离
reverse(list.subList(0, mid));
reverse(list.subList(mid, size));
reverse(list);
其他方法
- indexOfSubList:查找目标列表在源列表中作为子列表的第一次出现的位置
- lastIndexOfSubList:查找目标列表在源列表中作为子列表的最后一次出现的位置
内部类
按照源码的声明顺序包含有以下静态内部类:
- UnmodifiableCollection
- UnmodifiableSet
- UnmodifiableSortedSet
- UnmodifiableNavigableSet
- UnmodifiableList
- UnmodifiableRandomAccessList
- UnmodifiableMap
- UnmodifiableEntrySet:UnmodifiableMap的内部类
- UnmodifiableSortedMap
- UnmodifiableNavigableMap
- EmptyNavigableMap
- SynchronizedCollection
- SynchronizedSet
- SynchronizedSortedSet
- SynchronizedNavigableSet
- SynchronizedList
- SynchronizedRandomAccessList
- SynchronizedMap
- SynchronizedSortedMap
- SynchronizedNavigableMap
- CheckedCollection
- CheckedSet
- CheckedNavigableSet
- CheckedList
- CheckedRandomAccessList
- CheckedMap
- CheckedEntrySet
- CheckedSortedMap
- CheckedNavigableMap
- EmptyIterator
- EmptyListIterator
- EmptyEnumeration
- EmptySet
- EmptyList
- EmptyMap
- Spliterator
- SingletonSet
- SingletonList
- SingletonMap
- CopiesList:调用
nCopies
方法返回一个包含充分 n 个元素的不可变列表 - ReverseComparator:调用
reverseOrder
方法返回一个逆序比较器,其compare
方法体中,两个形参位置顺序与一般的方法相反 - SetFromMap:将Map作为Set使用,忽略原来Map的值,所有操作都在键集合 keySet() 上
- AsLIFOQueue:将双端队列(Deque)作为后进先出(LIFO)队列使用
根据类的命名,可以分为以下类别
-
Unmodifiable(不可变)类
Unmodifiable 类是用于创建不可更改的工具类。通过使用“包装器”(Wrapper)的方式,将一个已有的集合包装起来生成不可变的集合对象,该对象只支持读取操作,不支持添加、删除和修改集合中的元素。常用方法包括 unmodifiableList()、unmodifiableSet()、unmodifiableMap() 等(注意方法名开头字母为小写),用来返回对应的不可变类型。不可变类可以保护集合数据的不可变性,防止不应该更改的数据被修改,从而保证数据的安全性。 -
Synchronized(同步)类
Synchronized 类是一个用于创建同步化的工具类。通过使用“包装器”(Wrapper)的方式,将一个已有的集合包装起来,从而创建一个线程安全的集合对象。这些集合使用同步化机制来确保在多线程环境下对集合的并发访问时,集合中的数据不会被损坏。要得到Synchronized 类的的对象,通过相应的 synchronizedList()、synchronizedSet()、synchronizedMap() 返回。所有方法的同步操作通过对类中的mutex
对象加锁实现。 -
Checked(检查)类
Checked 类用于确保集合中只包含指定类型的元素,并在插入元素时进行检查。当元素类型不是指定类型时,将引发 ClassCastException 异常,从而发出警告。Checked 类用于增强程序的健壮性和安全,确保程序在运行时能够正确处理集合数据。通过相应的 checkedList()、checkedSet()、checkedMap() 等方法返回检查类。 -
Empty(空)类
以 Empty 开头的类是一组不可变、空(没有元素)的实现类,提供了一种容易使用的、不可变的、空集合的对象。Empty类是单例模式,只存在一个实例,可以省去创建大量的空集合对象的开销。另外,使用空集合对象作为默认值,在避免空指针异常的同时,也提高了代码的可读性。 -
Singleton(单例)类
Singleton 类用于创建值为单个元素的集合/列表/Map,其创建方式是通过 Collections.singletonXXX 方法实现的。该方法会返回一个只包含传入的单一对象或值的不可变对象。该对象生成后不能修改其内容,如果尝试添加或删除元素会导致 UnsupportedOperationException 异常。Singleton 可以代表一个单一的值或对象,并且充当一种方便提供对集合支持的方式。
(完)