大问题1 | Array最全源码分析(一)内部工具和数组排序
本节的前置问题:小问题6 | Java中如何实现深拷贝?实现深拷贝的三种方法,因为其规模,不再视为是小问题。
版本:JDK8,首先声明一下,Arrays中方法为了支持基本参数类型以及引用参数类型,会有许多方法重载,大同小异,为省略篇幅,下面可能只写出其中一个。
内部工具
1. 默认比较器
NaturalOrder 类是一个静态内部类,实现了 Comparator 接口,并重写了 compare 方法来实现自然排序。 NaturalOrder 类还定义了一个静态的 INSTANCE 常量,可以在需要自然排序比较器的地方使用。
在 Arrays 类的实现中,如果没有提供比较器,就会使用 NaturalOrder.INSTANCE 进行排序。
static final class NaturalOrder implements Comparator<Object> {
@SuppressWarnings("unchecked")
public int compare(Object first, Object second) {
return ((Comparable<Object>)first).compareTo(second);
}
static final NaturalOrder INSTANCE = new NaturalOrder();
}
2.传入参数范围检验方法
这段代码用于检查指定的索引范围是否在数组的有效范围内。如果不在范围内,就会抛出相应的异常。
rangeCheck 方法有三个参数:arrayLength 表示数组的长度,fromIndex 表示要访问的起始索引,toIndex 表示要访问的结束索引
(注: **结束索引不包含在内。 **如果要访问从 fromIndex 开始到 toIndex 结束的一段数组元素,只需要访问索引从 fromIndex 到 toIndex-1 的元素)
首先,该方法会检查 fromIndex 是否大于 toIndex,如果是,就会抛出 IllegalArgumentException 异常,表示起始索引不能大于结束索引。
然后,该方法会检查 fromIndex 是否小于 0,如果是,就会抛出 ArrayIndexOutOfBoundsException 异常,表示起始索引不能为负数。
最后,该方法会检查 toIndex 是否大于数组的长度,如果是,就会抛出 ArrayIndexOutOfBoundsException 异常,表示结束索引不能大于数组的长度。
private static void rangeCheck(int arrayLength, int fromIndex, int toIndex) {
if (fromIndex > toIndex) {
throw new IllegalArgumentException(
"fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")");
}
if (fromIndex < 0) {
throw new ArrayIndexOutOfBoundsException(fromIndex);
}
if (toIndex > arrayLength) {
throw new ArrayIndexOutOfBoundsException(toIndex);
}
}
提供给外部的工具
1. 基本类型数组排序(以int数组为例)
第一个 sort 方法用于对整个数组进行排序,它调用了 DualPivotQuicksort 类中的 sort 方法,并传入了整个数组的起始索引和结束索引。
第二个 sort 方法用于对指定范围内的数组元素进行排序,它调用了 DualPivotQuicksort 类中的 sort 方法,并传入了指定范围的起始索引和结束索引。和第一种方法的区别是需要对起始索引和结束索引进行检查,确保它们在数组的有效范围内。
注:这两个 sort 方法都使用了 Dual-Pivot Quicksort 算法,这是一种由 Vladimir Yaroslavskiy、Jon Bentley 和 Joshua Bloch 发明的快速排序算法。它的时间复杂度为 O(n log(n)),在许多数据集上表现优异,尤其是那些会导致其他快速排序算法性能下降的数据集。
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
public static void sort(int[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
DualPivotQuicksort.sort(a, fromIndex, toIndex - 1, null, 0, 0);
}
2. 基本类型数组并行排序(以int数组为例)(Java8新增)
并行排序算法,基于Fork/Join 框架,将数组拆分,排序再组合。
第一个 parallelSort 方法用于对整个数组进行排序,它使用了 Fork/Join 框架,将数组分成若干个子数组,并在多个线程上同时对这些子数组进行排序,最后再将这些子数组归并到一起,形成一个有序数组。
如果数组长度小于并行排序的最小值(MIN_ARRAY_SORT_GRAN),则使用快速排序算法进行排序,不会进一步划分排序任务。
第二个 parallelSort 方法用于对指定范围内的数组元素进行排序,和第一种方法的区别是需要对起始索引和结束索引进行检查,确保它们在数组的有效范围内。
(注:
-
此处的并行排序的最小值(MIN_ARRAY_SORT_GRAN),在代码中被置为 1 << 13,其中
<<
是位移运算符,表示将二进制数向左移动 13 位,相当于乘以 2^13,即 2 的 13 次方,结果为 8192。这个值通常是根据实际应用中的数据量和硬件环境来确定的,过小的值会导致内存争用,从而影响并行排序的效率和性能。 -
ArraysParallelSortHelpers
,包含了用于每种基本类型和对象类型的排序器和合并器实现。这些排序器和合并器的实现基本算法如下:-
如果数组长度很小,直接使用顺序快速排序(通过
Arrays.sort
方法)。 -
否则,将数组分成两半,对每一半进行以下操作:
a. 将这一半再分成两半(即四分之一)。
b. 对这四个子数组进行排序。
c. 将这四个子数组合并。
-
将这两半合并。)
-
private static final int MIN_ARRAY_SORT_GRAN = 1 << 13;
public static void parallelSort(int[] a) {
int n = a.length, p, g;
if (n <= MIN_ARRAY_SORT_GRAN ||
(p = ForkJoinPool.getCommonPoolParallelism()) == 1)
DualPivotQuicksort.sort(a, 0, n - 1, null, 0, 0);
else
new ArraysParallelSortHelpers.FJInt.Sorter
(null, a, new int[n], 0, n, 0,
((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
MIN_ARRAY_SORT_GRAN : g).invoke();
}
public static void parallelSort(int[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
int n = toIndex - fromIndex, p, g;
if (n <= MIN_ARRAY_SORT_GRAN ||
(p = ForkJoinPool.getCommonPoolParallelism()) == 1)
DualPivotQuicksort.sort(a, fromIndex, toIndex - 1, null, 0, 0);
else
new ArraysParallelSortHelpers.FJInt.Sorter
(null, a, new int[n], fromIndex, n, 0,
((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
MIN_ARRAY_SORT_GRAN : g).invoke();
}
3. 对象数组并行排序
将指定的对象数组按照升序排序,根据其元素Comparable的自然顺序。数组中的所有元素都必须实现Comparable接口。此外,数组中的所有元素必须是相互可比较的(即,对于数组中的任何元素e1和e2,e1.compareTo(e2)不应抛出ClassCastException)。
这种排序是 稳定的 :相等的元素在排序结果中的顺序不会改变。
和上面的两种一样,第二个方法支持按范围排序,同时也需要调用rangeCheck(a.length, fromIndex, toIndex)来检查范围是否合法。
第三个方法支持指定比较器,如果没有提供比较器,那么将使用元素的自然顺序进行排序,第四个方法支持指定比较器的按范围排序。
@SuppressWarnings("unchecked")
public static <T extends Comparable<? super T>> void parallelSort(T[] a) {
int n = a.length, p, g;
// 如果数组长度小于最小排序粒度或者公共池的并行级别为1,则使用TimSort进行排序
if (n <= MIN_ARRAY_SORT_GRAN ||
(p = ForkJoinPool.getCommonPoolParallelism()) == 1)
TimSort.sort(a, 0, n, NaturalOrder.INSTANCE, null, 0, 0);
else
// 否则,使用ForkJoin框架进行并行排序
new ArraysParallelSortHelpers.FJObject.Sorter<T>
(null, a,
(T[])Array.newInstance(a.getClass().getComponentType(), n),
0, n, 0, ((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
MIN_ARRAY_SORT_GRAN : g, NaturalOrder.INSTANCE).invoke();
}
@SuppressWarnings("unchecked")
public static <T extends Comparable<? super T>>
void parallelSort(T[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
int n = toIndex - fromIndex, p, g;
if (n <= MIN_ARRAY_SORT_GRAN ||
(p = ForkJoinPool.getCommonPoolParallelism()) == 1)
TimSort.sort(a, fromIndex, toIndex, NaturalOrder.INSTANCE, null, 0, 0);
else
new ArraysParallelSortHelpers.FJObject.Sorter<T>
(null, a,
(T[])Array.newInstance(a.getClass().getComponentType(), n),
fromIndex, n, 0, ((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
MIN_ARRAY_SORT_GRAN : g, NaturalOrder.INSTANCE).invoke();
}
@SuppressWarnings("unchecked")
public static <T> void parallelSort(T[] a, int fromIndex, int toIndex,
Comparator<? super T> cmp) {
rangeCheck(a.length, fromIndex, toIndex);
if (cmp == null)
cmp = NaturalOrder.INSTANCE;
int n = toIndex - fromIndex, p, g;
if (n <= MIN_ARRAY_SORT_GRAN ||
(p = ForkJoinPool.getCommonPoolParallelism()) == 1)
TimSort.sort(a, fromIndex, toIndex, cmp, null, 0, 0);
else
new ArraysParallelSortHelpers.FJObject.Sorter<T>
(null, a,
(T[])Array.newInstance(a.getClass().getComponentType(), n),
fromIndex, n, 0, ((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
MIN_ARRAY_SORT_GRAN : g, cmp).invoke();
}
4. 对象数组排序
对于非并行的对象数组排序,代码提供了两种排序实现:新的TimSort(ComparableTimSort.sort
)和一个旧的归并排序(legacyMergeSort
)。使用哪种实现由系统属性控制。排序是稳定的,相等的元素保持它们的相对顺序。
下面对代码进行解释:
-
LegacyMergeSort 静态内部类:
- 这个类包含一个静态布尔字段
userRequested
,用来决定是否应该使用旧的归并排序实现。 userRequested
的值是通过读取一个名为"java.util.Arrays.useLegacyMergeSort"
的系统属性来设置的,使用AccessController.doPrivileged
方法可以在必要的安全权限下读取该属性。- 注释指出,这个旧的归并排序是为了兼容性而保留的,应该在将来的版本中移除。
- 这个类包含一个静态布尔字段
-
sort(Object[] a) 方法:
- 这是主要的排序方法,它接受一个对象数组
a
作为参数。 - 方法首先检查
userRequested
标志是否为真,这将表明应该使用旧的归并排序实现。如果是这样,它会调用legacyMergeSort
方法。 - 如果
userRequested
为假,它使用ComparableTimSort.sort
方法对数组进行排序。
- 这是主要的排序方法,它接受一个对象数组
-
legacyMergeSort方法:
- 这是一个私有方法,实现了旧的归并排序算法。
- 它将原始数组
a
克隆到一个新的数组aux
中,然后调用mergeSort
方法,传递辅助数组、原始数组和要排序的索引范围。 - 注释表明这个方法已经被弃用,并将在未来的版本中移除。
-
mergeSort方法
-
mergeSort
方法的参数:Object[] src
: 源数组,即要排序的数组。Object[] dest
: 目标数组,用于存放排序的结果,可能比源数组大,并且可能有偏移量。int low
: 目标数组中开始排序的索引。int high
: 目标数组中结束排序的索引。int off
: 偏移量,用于在源数组中生成对应的low
和high
索引。length
变量计算需要排序的元素数量。 -
如果数组的长度小于
INSERTIONSORT_THRESHOLD
(阈值,定义为7),则使用插入排序而不是归并排序。插入排序在小数组上效率较高。 -
如果数组长度大于等于阈值,则递归地对数组的两半进行排序:
-
计算中点
mid
。 -
对
dest
数组的前半部分进行排序,结果存放在src
数组中。 -
对
dest
数组的后半部分进行排序,结果也存放在src
数组中。 -
检查是否需要合并:
如果
src
数组中间的两个元素已经有序(src[mid-1] <= src[mid]
),则不需要合并,直接将src
的这部分复制到dest
数组中。如果不是有序的,则需要将
src
数组中已排序的两半合并到dest
数组中。 -
合并过程:
使用两个指针
p
和q
分别指向src
数组中两个有序部分的开始。遍历
dest
数组,比较src
中的元素,并将较小的元素复制到dest
数组中,直到所有元素都被合并。注:
swap
方法用于交换数组中两个元素的位置。
-
同时,和之前的几种排序一样,对对象的sort方法也支持对指定范围和指定比较器的排序。
下附源码:
static final class LegacyMergeSort {
private static final boolean userRequested =
java.security.AccessController.doPrivileged(
new sun.security.action.GetBooleanAction(
"java.util.Arrays.useLegacyMergeSort")).booleanValue();
}
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
/** To be removed in a future release. */
private static void legacyMergeSort(Object[] a) {
Object[] aux = a.clone();
mergeSort(aux, a, 0, a.length, 0);
}
public static void sort(Object[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, fromIndex, toIndex);
else
ComparableTimSort.sort(a, fromIndex, toIndex, null, 0, 0);
}
/** To be removed in a future release. */
private static void legacyMergeSort(Object[] a,
int fromIndex, int toIndex) {
Object[] aux = copyOfRange(a, fromIndex, toIndex);
mergeSort(aux, a, fromIndex, toIndex, -fromIndex);
}
/**
* Tuning parameter: list size at or below which insertion sort will be
* used in preference to mergesort.
* To be removed in a future release.
*/
private static final int INSERTIONSORT_THRESHOLD = 7;
@SuppressWarnings({"unchecked", "rawtypes"})
private static void mergeSort(Object[] src,
Object[] dest,
int low,
int high,
int off) {
int length = high - low;
// Insertion sort on smallest arrays
if (length < INSERTIONSORT_THRESHOLD) {
for (int i=low; i<high; i++)
for (int j=i; j>low &&
((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
swap(dest, j, j-1);
return;
}
// Recursively sort halves of dest into src
int destLow = low;
int destHigh = high;
low += off;
high += off;
int mid = (low + high) >>> 1;
mergeSort(dest, src, low, mid, -off);
mergeSort(dest, src, mid, high, -off);
// If list is already sorted, just copy from src to dest. This is an
// optimization that results in faster sorts for nearly ordered lists.
if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {
System.arraycopy(src, low, dest, destLow, length);
return;
}
// Merge sorted halves (now in src) into dest
for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)
dest[i] = src[p++];
else
dest[i] = src[q++];
}
}
/**
* Swaps x[a] with x[b].
*/
private static void swap(Object[] x, int a, int b) {
Object t = x[a];
x[a] = x[b];
x[b] = t;
}
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
/** To be removed in a future release. */
private static <T> void legacyMergeSort(T[] a, Comparator<? super T> c) {
T[] aux = a.clone();
if (c==null)
mergeSort(aux, a, 0, a.length, 0);
else
mergeSort(aux, a, 0, a.length, 0, c);
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static void mergeSort(Object[] src,
Object[] dest,
int low, int high, int off,
Comparator c) {
int length = high - low;
// Insertion sort on smallest arrays
if (length < INSERTIONSORT_THRESHOLD) {
for (int i=low; i<high; i++)
for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--)
swap(dest, j, j-1);
return;
}
// Recursively sort halves of dest into src
int destLow = low;
int destHigh = high;
low += off;
high += off;
int mid = (low + high) >>> 1;
mergeSort(dest, src, low, mid, -off, c);
mergeSort(dest, src, mid, high, -off, c);
// If list is already sorted, just copy from src to dest. This is an
// optimization that results in faster sorts for nearly ordered lists.
if (c.compare(src[mid-1], src[mid]) <= 0) {
System.arraycopy(src, low, dest, destLow, length);
return;
}
// Merge sorted halves (now in src) into dest
for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
if (q >= high || p < mid && c.compare(src[p], src[q]) <= 0)
dest[i] = src[p++];
else
dest[i] = src[q++];
}
}
读完本文的朋友可能留下很多问题:
TimSort和Dual-Pivot Quicksort 算法是什么?实现原理和区别是什么?
Fork/Join 框架是什么?
Java 中的移位运算符有什么用?
AccessController和背后的Java安全框架是什么?
关于Array其他源码的介绍和上述的哪些问题,我们继续看。
————————————————————
本专栏是【小问题】系列,旨在每篇解决一个小问题,并秉持着刨根问底的态度解决这个问题可能带出的一系列问题。