java中的Arrays這個工具類你真的會用嗎

chenweicool 發表於 2020-06-29

Java原始碼系列三-工具類Arrays

​ 今天分享java的原始碼的第三彈,Arrays這個工具類的原始碼。因為近期在複習資料結構,瞭解到Arrays裡面的排序演算法和二分查詢等的實現,收益匪淺,決定研讀一下Arrays這個類的原始碼。不足之處,歡迎在評論區交流和指正。

1.認識Arrays這個類:

​ 首先它在java的utils包下,屬於Java Collections Framework中的一員。它的初衷就是一個工具類,封裝了操縱陣列的各種方法,比如排序,二分查詢,陣列的拷貝等等。滿足了我們日常對陣列操做的基本需求,瞭解它的底層實現,不僅能幫助我們更好的使用它,而且還能培養我們更好的程式碼的思維。

2.構造方法

​ 因為是一個工具類,所以它的構造方法定義為私有的,且所有的實現方法都是靜態方法。也就是說這個類不能被例項化,通俗的講,就是不能new。只能通過類名來直接呼叫方法(反射除外)。這樣做的目的是強化該類不可實列化的能力,突出該類作為工具類的根本職能。原始碼如下:

 // Suppresses default constructor, ensuring non-instantiability.
    private Arrays() {}

3.常用方法的解析

3.1快速插入集合元素的方法asList(T... a):

基本使用:

  /**
     * 陣列轉化為集合
     */
    @Test
    public void toArrayTest(){
        List<Integer> list = Arrays.asList(2,4,5,6,6);
        for (Integer integer : list) {
            System.out.print(integer+" ");
        }
    }

輸出結果:

2 4 5 6 6 

看一下原始碼:

 @SafeVarargs
 @SuppressWarnings("varargs")
 public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

// ArrayList的構造方法和屬性
      private final E[] a;
        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }

​ 這個方法的實現比較簡單,就是呼叫ArrayList的構造方法,並且引數是一個陣列,也就是將我們要構造的數傳入到ArrayList的構造方法中去,進行例項化。

3.2.二分查詢的方法

Arrays類中的二分查詢八種基本型別都有涉及,但都是方法的過載。其實現原理都是一樣,這裡以int型別為例,進行說明。

基本使用:

    @Test
    public void binarySearchTest(){
        int[] arrays = {1,4,6,7,9,3};
        // 查詢元素為7的下標值
        int result = Arrays.binarySearch(arrays,7);
        System.out.println(result);
    }

結果:

3

這個方法主要涉及的一下三個方法:

// 我們常用的方法
public static int binarySearch(int[] a, int key) {
        return binarySearch0(a, 0, a.length, key);
    }

/*
   引數說明如下: a  待查詢的陣列
    fromIndex    查詢的開始位置
    toIndex      查詢的結束位置
    key          查詢的目標值
 */
public static int binarySearch(int[] a, int fromIndex, int toIndex,
                                   int key) {
        // 進行異常檢查
        rangeCheck(a.length, fromIndex, toIndex);
        return binarySearch0(a, fromIndex, toIndex, key);
    }

    // Like public version, but without range checks.
    private static int binarySearch0(int[] a, int fromIndex, int toIndex,
                                     int key) {
        int low = fromIndex;
        int high = toIndex - 1;

        while (low <= high) {
              
            // 找出查詢範圍的中間值
            int mid = (low + high) >>> 1;
            int midVal = a[mid];
           
            // 進行比較
            if (midVal < key)
                low = mid + 1;
            else if (midVal > key)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found.
    }

當然實現的核心方法還是上述私有方法binarySearch0()這個方法,實現的邏輯也不復雜。

第一步就是宣告兩個變數儲存查詢區域的開始和結束。

第二步 迴圈,比較,不斷的縮小比較的範圍,直到找到陣列中的值和目標值相同,返回下標,如果沒有找到就返回一個負數也就是下面的這l兩行程式碼:

return mid; // key found
return -(low + 1);  // key not found.

我認為:這個二分法實現的亮點就在於求中間值的移位運算:

int mid = (low + high) >>> 1;

有人就納悶了,為什麼還要使用移位運算,除法不行嗎?主要還是為了效能考量。因為移位運算佔兩個機器週期,而乘除法佔四個運算週期,所以移位運算的速度肯定比乘除法的運算速度快很多,計算量小了可能區別不大,但是計算量很大,就區別很明顯了。

3.3 陣列的拷貝

    @Test
    public void testCopyArrange(){
          // 原陣列
        int [] srcArray = {11,2,244,5,6,54};
        // 拷貝原陣列長度為3的部分
        int[] descArray = Arrays.copyOf(srcArray,3);
        
        System.out.println(Arrays.toString(descArray));
    }

輸出結果:

[11, 2, 244]

原始碼分析:

/* 引數說明:
   original   原陣列
   newLength   拷貝的陣列長度 
 */
public static int[] copyOf(int[] original, int newLength) {
         // 宣告一個新陣列的長度,儲存拷貝後的陣列
        int[] copy = new int[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

分析: 主要還是呼叫了本地的方法arraycopy完成陣列的指定長度拷貝,可以看到原始碼並沒有對陣列的長度進行檢查,主要是arraycopy()這個方法時使了Math.min()方法,保證了你宣告的長度在一個安全的範圍之內,如果你拷貝的長度超出了陣列的長度,就預設拷貝整個陣列。至於native修飾的方法的使用,可以看看這裡

 System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));

當然如果需要拷貝陣列指定的區間 ,可以使用Arrays的 copyOfRange(int[] original, int from, int to) 實現原理和arraycopy()方法的原理類似:

     @Test
    public void testCopy(){
        int [] srcArray = {11,2,244,5,6,54};
         // 拷貝指定範圍的陣列
        int[] descArray = Arrays.copyOfRange(srcArray,0,3);
        System.out.println(Arrays.toString(descArray));
    }

輸出結果:

[11, 2, 244]

注: copyOfRange(int[] original, int from, int to)中的引數to是不包含在拷貝的結果中的,上述的例子,就只能拷貝到索引為2的元素,不包含索引為3的元素,這點需要注意。

3.4 equals方法

主要重寫了Object類的equals方法,用來比較兩個陣列內容是否相等,也就是他們中的元素是否相等。

基本用法:

   @Test
    public void equalTest(){
        int[] array ={1,2,3,4};
        int[] result ={1,2,3,4};
        System.out.println(Arrays.equals(array,result)); 
        System.out.println(array == result);
    }

結果:

true
false

看原始碼之前,有必要講一下重寫了equals方法之後,兩個物件比較的是值,也就是他們的內容,這點非常的重要。重寫equals方法的注意事項可以移步這裡

原始碼如下:

 public static boolean equals(int[] a, int[] a2) {
       // 基於地址的比較
     if (a==a2)
            return true;
        if (a==null || a2==null)
            return false;

        int length = a.length;
        //  基於長度的比較
        if (a2.length != length)
            return false;
       
        // 比較每個元素是否相等
        for (int i=0; i<length; i++)
            if (a[i] != a2[i])
                return false;

        return true;
    }

原始碼說明如下:

原始碼判斷了四次,分別是首地址比較,是否為空,以及長度的比較,最後對於陣列的各個元素進行比較。

有必要說明下第一個判斷,也就是首地址的比較。當我們宣告一個陣列變數時,這個變數就代表陣列的首地址,看下面這個程式碼:

    @Test
    public void equalTest(){
        int[] array ={1,2,3,4};
        System.out.println(array);
      
    }

結果:

[[email protected]     // [代表陣列 I代表整數 @分隔符 後邊記憶體地址十六進位制 

​ 這表示的是一個地址。還是因為在宣告一個陣列時,會在堆裡面建立一塊記憶體區域,但是這塊記憶體區域相對於堆來說可能很小,不好找。為了方便查詢,所以將陣列記憶體中的首地址表示出來。虛擬機器將地址傳給變數名array。這也是引用型別,傳的是地址,也就是理解成array指向記憶體地址(類似於家庭的地址),每次執行可能地址都不一樣,因為虛擬機器開闢的記憶體空間可能不一樣。

理解了這個,那麼a==a2就好理解了,如果兩個陣列記憶體地址都相同,那麼兩個陣列的肯定是相等的。

還有我認為程式寫的比較好的地方就是原始碼中對陣列每個元素的比較,也就是下面這段程式碼;

  for (int i=0; i<length; i++)
            if (a[i] != a2[i])
                return false;

        return true;

使用a[i] != a2[i] 作為判斷條件,就可以減少比較次數,提高了效能。試想一下如果這裡是相等的比較,那每次都要遍歷整個陣列,如果資料量大了,無疑在效能上會慢很多。又一次感嘆到原始碼的魅力。

3.5 排序相關的方法sort()和parallelSort()

Arrays 這個類中主要涉及了兩種型別的排序方法序列 sort()和並行parallelSort()這兩個方法,當然物件的排序和基本型別的排序也不太一樣。這裡還是以int[]型別的為例。進行說明。

首先比較兩個方法的效能:

     
    public final int UPPER_LIMIT = 0xffffff;
    final int ROUNDS = 10;
    final int INCREMENT = 5;
    final int INIT_SIZE = 1000;

    @Test
    public void sortAndParallelSortTest(){
        
        // 構造不同容量的集合
        for (int capacity = INIT_SIZE; capacity < UPPER_LIMIT ; capacity*= INCREMENT) {
            ArrayList<Integer> list = new ArrayList<>(capacity);
            for (int j = 0; j < capacity; j++) {
                list.add((int) (Math.random()*capacity));
            }
            
            double avgTimeOfParallelSort = 0;
            double avgTimeOfSort = 0;

            for (int j = 0; j <= ROUNDS ; j++) {
                // 每次排序都打亂順序
                Collections.shuffle(list);

                Integer[] arr1 = list.toArray(new Integer[capacity]);
                Integer[] arr2 = arr1.clone();
                 
                avgTimeOfParallelSort += counter(arr1,true);
                avgTimeOfSort += counter(arr2, false);
            }
            // 輸出結果
            output(capacity,avgTimeOfParallelSort/ROUNDS,avgTimeOfSort/ROUNDS);
        }
    }

    private void output(int capacity, double v, double v1) {
        System.out.println("=======================測試排序的時間=========");
        System.out.println("Capacity"+capacity);
        System.out.println("ParallelSort"+v);
        System.out.println("Sort"+v1);
        System.out.println("比較快的排序是:"+(v < v1 ? "ParallelSort":"Sort"));
    }

    // 計算消耗的時間
    private double counter(Integer[] arr1, boolean b) {
        long begin,end;
        begin = System.nanoTime();
        if(b){
            Arrays.parallelSort(arr1);
        }else{
            Arrays.parallelSort(arr1);
        }
        end = System.nanoTime();
        return BigDecimal.valueOf(end-begin,9).doubleValue();
    }

部分的測試的結果:

=======================測試排序的時間=========
Capacity1000
ParallelSort6.284099999999999E-4
Sort5.599599999999999E-4
比較快的排序是:Sort
=======================測試排序的時間=========
Capacity5000
ParallelSort0.00163599
Sort0.0018313699999999995
比較快的排序是:ParallelSort

可以看到在資料量比較小的情況下,使用sort()方法更快,一旦過了一個閾值,就是ParallelSort()這個方法效能好。這個閾值是多少呢。

我們先看一下parallelSort的原始碼:

 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();
    }

可以看到當陣列的長度小於MIN_ARRAY_SORT_GRAN或者p = ForkJoinPool.getCommonPoolParallelism()) == 1 (在單執行緒下)的時候,呼叫sort()排序的底層實現的DualPivotQuicksort.sort(a, 0, n - 1, null, 0, 0);Arrays的開頭定義的常量如下:

 private static final int MIN_ARRAY_SORT_GRAN = 1 << 13;    // 這個值是8192

對比兩者,也就是在陣列的長度比較大或者是多執行緒的情況下,優先考慮並行排序,否則使用序列排序。

兩個排序的核心思想:

  • sort()方法的核心還是快排和優化後的歸併排序, 快速排序主要是對哪些基本型別資料(int,short,long等)排序, 而合併排序用於對物件型別進行排序。

  • parallelSort()它使用並行排序-合併排序演算法。它將陣列分成子陣列,這些子陣列本身先進行排序然後合併。

由於並行排序和序列排序的底層比較複雜,且篇幅有限,想要詳細瞭解底層實現的話,可以移步到序列排序並行排序

3.6 toString方法

基本用法:

 @Test
    public void toStringTest(){
         int[] array = {1,3,2,5};
        System.out.println(Arrays.toString(array));
    }

結果:

[1, 3, 2, 5]

原始碼分析如下:

 public static String toString(int[] a) {
        // 1.判斷陣列的大小
        if (a == null)
            return "null";
        int iMax = a.length - 1;
        if (iMax == -1)
            return "[]";
        
       // 2.使用StringBuilder進行追加
        StringBuilder b = new StringBuilder();
        b.append('[');
        for (int i = 0; ; i++) {
            b.append(a[i]);
            if (i == iMax)
                return b.append(']').toString();
            b.append(", ");
        }
    }

具體的實現,已在原始碼的註釋中進行了說明。這個方法對於基本資料型別來說,很方便的遍歷陣列。

追本溯源,方能闊步前行。

參考資料

https://blog.csdn.net/ExcellentYuXiao/article/details/52344628

sakuraTears的部落格

javaSE的官方問檔。