Collections源码分析(一)

本文详细分析了Java Collections类中List排序方法的实现原理,包括使用归并排序和TimSort算法的过程,以及对小数组进行插入排序的优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

由于Collections类的源码量是比较大的,所以我打算分期来分析它的各个部分的代码。


本类做了2个方面的事情:

1. 提供一些操作集合的静态方法

2. 将某些集合作为参数,进行包装,变成如线程安全的集合

private Collections() {
}复制代码

上面是它的私有化的构造方法,防止用户对它进行实例化操作。

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;复制代码

上面是针对用到的算法的准备的一些协调作用的参数。他们都是一些阈值性质的静态常量,如第一个参数是二分法搜索的范围。

public static <T extends Comparable<? super T>> void sort(List<T> list) {
    list.sort(null);
}
复制代码

上面是对List类型的集合进行排序,而且这个集合里面的元素是可以相互比较的(这体现在T extends Comparable)。这个排序方法延期到一个叫sort(Comparator c)的方法来进行实现,并且参数为null,也就是不指定具体的比较器。


这个被调用的排序方法就是List 接口中的排序方法:

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}
复制代码

从上面分析看,排序的步骤如下:

1. 把要排序的集合转成一个Object类型的数组

2. 调用Arrays的排序方法

3. 利用该集合的迭代器来遍历得到的数组,将数组中的每个元素放入到原来的集合中,完成对集合的排序。


接下来我们重点看Arrays里面的这个排序方法:

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);
    }
}
复制代码

在这个方法里面调用了3类排序的方法。而在这里我们实际上只会去调用sort(Object[] a)方法,因为我们前面指定Comparator为null。

public static void sort(Object[] a) {
    if (LegacyMergeSort.userRequested)
        legacyMergeSort(a);
    else
        ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}复制代码

上面是这个排序方法的内容。这类排序方法属于mergesort。归并排序可以合并2个或者多个已经排好序的数组。

private static void legacyMergeSort(Object[] a) {
    Object[] aux = a.clone();
    mergeSort(aux, a, 0, a.length, 0);
}复制代码

上面是核心的实现。它先对要排序的数组copy一个副本,然后对副本进行归并排序。

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++];
    }
}复制代码

我们来分析归并方法的实现思路:

low是dest这个数组的开始索引位置,high是dest数组的结束索引位置。off是为src院数组服务的一个参数,用来产生src的low以及high2个参数。


需要强调是的dest就是我们要排序的数组a,而src是a的一个副本。

当数组的长度小于7的时候,用插入排序。那插入排序的思想是什么呢?

对于数组中2个相邻的元素,若干前一个元素的值大于后面这个元素的值的时候,就会把这2个元素的位置进行交换。

private static void swap(Object[] x, int a, int b) {
    Object t = x[a];
    x[a] = x[b];
    x[b] = t;
}复制代码

上面是交换方法swap的实现。它利用的是中间变量来替换2个元素的值。


这里遇到了一个不常用的操作,也是一个写代码优化的点:>>> 1.下面给出一些例子就清楚他的作用了。

System.out.println(8 >>> 1);// return 4

System.out.println(8 >>> 2);// return 2

System.out.println(8 >>> 3);//return 1

复制代码

可以看出,当符号右边的值是1的时候,他的作用就是最左边的值除以2,当右边的值是2的时候,就是对左边的值除以2个2,以此类推。

mergeSort(dest, src, low, mid, -off);
mergeSort(dest, src, mid, high, -off);复制代码

在经过上面方法的循环调用之后,被分成2部分的数组就在src这个数组来说已经都是一个升序的数组了。但是在进行合并之前,要保证得到一个升序排列的数组,需要对左半边数组的最后一个值,也就是左半边数组里面的最大值,和右半边最小的值进行一次比较,若干前者小于后者,那么合并之后才是一个升序的数组。在这种情况下我们就直接用底层的数组复制方法就可以得到升序的数组了。

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++];
}复制代码

当不是这种情况的时候,就表示左半边是较大的数组,右半边是较小的数组,在上面这个循环中destLow的初始值位0,low也是0,q的初始值仍是原来的mid。在一开始q>high是不成立的,所以dest[i] = src[q++]就是把较小的数组放在dest左半边,而当q达到high的时候,就把较大的数组放在dest的右半边,完成排序。


需要注意的是这个路线的思路其实不是最优的,当你用debug调试的时候,并不是走的在这条路线,而是走的sort(Object[] a)的另一条路线。


--------------------华丽的分割线---------------------------

ComparableTimSort.sort(a, fromIndex, toIndex, null, 0, 0)复制代码

上面走的就是优化路线。


static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
    assert a != null && lo >= 0 && lo <= hi && hi <= a.length;

    int nRemaining  = hi - lo;
    if (nRemaining < 2)
        return;  // Arrays of size 0 and 1 are always sorted

    // If array is small, do a "mini-TimSort" with no merges
    if (nRemaining < MIN_MERGE) {
        int initRunLen = countRunAndMakeAscending(a, lo, hi);
        binarySort(a, lo, hi, lo + initRunLen);
        return;
    }

    /**
     * March over the array once, left to right, finding natural runs,
     * extending short natural runs to minRun elements, and merging runs
     * to maintain stack invariant.
     */
    ComparableTimSort ts = new ComparableTimSort(a, work, workBase, workLen);
    int minRun = minRunLength(nRemaining);
    do {
        // Identify next run
        int runLen = countRunAndMakeAscending(a, lo, hi);

        // If run is short, extend to min(minRun, nRemaining)
        if (runLen < minRun) {
            int force = nRemaining <= minRun ? nRemaining : minRun;
            binarySort(a, lo, lo + force, lo + runLen);
            runLen = force;
        }

        // Push run onto pending-run stack, and maybe merge
        ts.pushRun(lo, runLen);
        ts.mergeCollapse();

        // Advance to find next run
        lo += runLen;
        nRemaining -= runLen;
    } while (nRemaining != 0);

    // Merge all remaining runs to complete sort
    assert lo == hi;
    ts.mergeForceCollapse();
    assert ts.stackSize == 1;
}
复制代码

上面就是实现的细节。

他再哪些地方进行了优化呢?让我们来一步一步的看看:

第一句代码是在判断数组不能为空和判断范围的正确性。


接下来看到他对只有2个元素的数组的优化,只有2个元素的数组不需要进行排序。


当数组的元素个数小于32的时候,昨天进行迷你的TimSort.

private static int countRunAndMakeAscending(Object[] a, int lo, int hi) {
    assert lo < hi;
    int runHi = lo + 1;
    if (runHi == hi)
        return 1;

    // Find end of run, and reverse range if descending
    if (((Comparable) a[runHi++]).compareTo(a[lo]) < 0) { // Descending
        while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0)
            runHi++;
        reverseRange(a, lo, runHi);
    } else {                              // Ascending
        while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0)
            runHi++;
    }

    return runHi - lo;
}
复制代码

上面这个方法是干什么的呢?在while循环里面做的是:对于2个相邻的元素,如果大的元素在左边,小的在右边,即是一种降序的时候,对这些元素进行升序的排列。

private static void reverseRange(Object[] a, int lo, int hi) {
    hi--;
    while (lo < hi) {
        Object t = a[lo];
        a[lo++] = a[hi];
        a[hi--] = t;
    }
}复制代码

上面是反转降序位升序的方法。其实这就是一个swap方法。

其实count这个方法就是让要排序的数组进行升序排列。而while具有一种连续性,while循环结束了我们就得到1个或多个连续都是升序的元素。到此为止,只是对连续降序的数组进行了升序的排列。

private static void binarySort(Object[] a, int lo, int hi, int start) {
    assert lo <= start && start <= hi;
    if (start == lo)
        start++;
    for ( ; start < hi; start++) {
        Comparable pivot = (Comparable) a[start];

        // Set left (and right) to the index where a[start] (pivot) belongs
        int left = lo;
        int right = start;
        assert left <= right;
        /*
         * Invariants:
         *   pivot >= all in [lo, left).
         *   pivot <  all in [right, start).
         */
        while (left < right) {
            int mid = (left + right) >>> 1;
            if (pivot.compareTo(a[mid]) < 0)
                right = mid;
            else
                left = mid + 1;
        }
        assert left == right;

        /*
         * The invariants still hold: pivot >= all in [lo, left) and
         * pivot < all in [left, start), so pivot belongs at left.  Note
         * that if there are elements equal to pivot, left points to the
         * first slot after them -- that's why this sort is stable.
         * Slide elements over to make room for pivot.
         */
        int n = start - left;  // The number of elements to move
        // Switch is just an optimization for arraycopy in default case
        switch (n) {
            case 2:  a[left + 2] = a[left + 1];
            case 1:  a[left + 1] = a[left];
                     break;
            default: System.arraycopy(a, left, a, left + 1, n);
        }
        a[left] = pivot;
    }
}
复制代码

上面是binarySort方法。它是从不是升序的那个元素开始,用这个元素和前面已经是升序的数组中最中间的那个数进行比较,然后以此类推,目的是为了计算出需要移动的元素的个数,最后通过插入排序。


接下来就是TimSort了。

首先创建了一个ComparableTimSort的对象。

private static int minRunLength(int n) {
    assert n >= 0;
    int r = 0;      // Becomes 1 if any 1 bits are shifted off
    while (n >= MIN_MERGE) {
        r |= (n & 1);
        n >>= 1;
    }
    return n + r;
}复制代码

上面是:如果n也就是数组的长度大于等于32,那么如果n是2的N次幂,那么返回16,否则逐位向右位移(即除以2),直到找到介于16和32间的一个数。


这里需要认识ComparableTimSort这个类的一些属性:

1.  stackSize: 分区的个数

2. runBase:第一个分区的数组

3. runLength: 每个分区的数组的长度


private void mergeCollapse() {
    while (stackSize > 1) {
        int n = stackSize - 2;
        if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) {
            if (runLen[n - 1] < runLen[n + 1])
                n--;
            mergeAt(n);
        } else if (runLen[n] <= runLen[n + 1]) {
            mergeAt(n);
        } else {
            break; // Invariant is established
        }
    }
}复制代码

上面是主要的方法:相邻的三个分区,若第一分区的长度小于后面2个分区的长度的和,并且第一个分区的长度也小于第三个分区的长度,那么第一个分区和中间第二个分区合并。








转载于:https://juejin.im/post/5ccd1393f265da03b0516e45

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值