要想陣列用的 6,怎能不懂 java.util.Arrays

沉默王二發表於2020-07-04

java.util.Arrays 類就是為陣列而生的專用工具類,基本上常見的對陣列的操作,Arrays 類都考慮到了,這讓我由衷地覺得,是時候給該類的作者 Josh Bloch、Neal Gafter、John Rose 點個讚了。

(我是怎麼知道作者名的?看原始碼就可以,小夥伴們,裝逼吧)

Arrays 都可以幹嘛呢?常見的有:

  • 建立陣列
  • 比較陣列
  • 陣列排序
  • 陣列檢索
  • 陣列轉流
  • 列印陣列
  • 陣列轉 List
  • setAll(沒想好中文名)
  • parallelPrefix(沒想好中文名)

那接下來,小夥伴們是不是已經迫不及待想要和二哥一起來打怪進階了。走你。

01、建立陣列

使用 Arrays 類建立陣列可以通過以下三個方法:

  • copyOf,複製指定的陣列,擷取或用 null 填充
  • copyOfRange,複製指定範圍內的陣列到一個新的陣列
  • fill,對陣列進行填充

1)copyOf,直接來看例子:

String[] intro = new String[] { "沉""默""王""二" };
String[] revised = Arrays.copyOf(intro, 3);
String[] expanded = Arrays.copyOf(intro, 5);
System.out.println(Arrays.toString(revised));
System.out.println(Arrays.toString(expanded));

revised 和 expanded 是複製後的新陣列,長度分別是 3 和 5,指定的陣列長度是 4。來看一下輸出結果:

[沉, 默, 王]
[沉, 默, 王, 二, null]

看到沒?revised 擷取了最後一位,因為長度是 3 嘛;expanded 用 null 填充了一位,因為長度是 5。

2)copyOfRange,直接來看例子:

String[] intro = new String[] { "沉""默""王""二" };
String[] abridgement = Arrays.copyOfRange(intro, 03);
System.out.println(Arrays.toString(abridgement));

copyOfRange() 方法需要三個引數,第一個是指定的陣列,第二個是起始位置(包含),第三個是截止位置(不包含)。來看一下輸出結果:

[沉, 默, 王]

0 的位置是“沉”,3 的位置是“二”,也就是說擷取了從 0 位(包含)到 3 位(不包含)的陣列元素。那假如說下標超出了陣列的長度,會發生什麼呢?

String[] abridgementExpanded = Arrays.copyOfRange(intro, 06);
System.out.println(Arrays.toString(abridgementExpanded));

結束位置此時為 6,超出了指定陣列的長度 4,來看一下輸出結果:

[沉, 默, 王, 二, nullnull]

仍然使用了 null 進行填充。為什麼要這麼做呢?小夥伴們思考一下,我想是作者考慮到了陣列越界的問題,不然每次呼叫 Arrays 類就要先判斷很多次長度,很麻煩。

3)fill,直接來看例子:

String[] stutter = new String[4];
Arrays.fill(stutter, "沉默王二");
System.out.println(Arrays.toString(stutter));

使用 new 關鍵字建立了一個長度為 4 的陣列,然後使用 fill() 方法將 4 個位置填充為“沉默王二”,來看一下輸出結果:

[沉默王二, 沉默王二, 沉默王二, 沉默王二]

如果想要一個元素完全相同的陣列時, fill() 方法就派上用場了。

02、比較陣列

Arrays 類的 equals() 方法用來判斷兩個陣列是否相等,來看下面這個例子:

String[] intro = new String[] { "沉""默""王""二" };
boolean result = Arrays.equals(new String[] { "沉""默""王""二" }, intro);
System.out.println(result);
boolean result1 = Arrays.equals(new String[] { "沉""默""王""三" }, intro);
System.out.println(result1);

輸出結果如下所示:

true
false

指定的陣列為沉默王二四個字,比較的陣列一個是沉默王二,一個是沉默王三,所以 result 為 true,result1 為 false。

簡單看一下 equals() 方法的原始碼:

public static boolean equals(Object[] a, Object[] 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 (!Objects.equals(a[i], a2[i]))
            return false;
    }

    return true;
}

因為陣列是一個物件,所以先使用“==”操作符進行判斷,如果不相等,再判斷是否為 null,兩個都為 null,返回 false;緊接著判斷 length,不等的話,返回 false;否則的話,依次呼叫 Objects.equals() 比較相同位置上的元素是否相等。

感覺非常嚴謹,這也就是學習原始碼的意義,鑑賞的同時,學習。

除了 equals() 方法,還有另外一個訣竅可以判斷兩個陣列是否相等,儘管可能會出現誤差(概率非常小)。那就是 Arrays.hashCode() 方法,先來看一下該方法的原始碼:

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

雜湊演算法本身是非常嚴謹的,所以如果兩個陣列的雜湊值相等,那幾乎可以判斷兩個陣列是相等的。

String[] intro = new String[] { "沉""默""王""二" };

System.out.println(Arrays.hashCode(intro));
System.out.println(Arrays.hashCode(new String[] { "沉""默""王""二" }));

來看一下輸出結果:

868681617
868681617

兩個陣列的雜湊值相等,畢竟元素是一樣的。但這樣確實不夠嚴謹,優先使用 Objects.equals() 方法。

03、陣列排序

Arrays 類的 sort() 方法用來判斷兩個陣列是否相等,來看下面這個例子:

String[] intro1 = new String[] { "chen""mo""wang""er" };
String[] sorted = Arrays.copyOf(intro1, 4);
Arrays.sort(sorted);
System.out.println(Arrays.toString(sorted));

由於排序會改變原有的陣列,所以我們使用了 copyOf() 方法重新複製了一份。來看一下輸出結果:

[chen, er, mo, wang]

可以看得出,按照的是首字母的升序進行排列的。基本資料型別是按照雙軸快速排序的,引用資料型別是按照 TimSort 排序的,使用了 Peter McIlroy 的“樂觀排序和資訊理論複雜性”中的技術。

04、陣列檢索

陣列排序後就可以使用 Arrays 類的 binarySearch() 方法進行二分查詢了。否則的話,只能線性檢索,效率就會低很多。

String[] intro1 = new String[] { "chen""mo""wang""er" };
String[] sorted = Arrays.copyOf(intro1, 4);
Arrays.sort(sorted);
int exact = Arrays.binarySearch(sorted, "wang");
System.out.println(exact);
int caseInsensitive = Arrays.binarySearch(sorted, "Wang", String::compareToIgnoreCase);
System.out.println(caseInsensitive);

binarySearch() 方法既可以精確檢索,也可以模糊檢索,比如說忽略大小寫。來看一下輸出結果:

3
3

排序後的結果是 [chen, er, mo, wang],所以檢索出來的下標是 3。

05、陣列轉流

Stream 流非常強大,需要入門的小夥伴可以檢視我之前寫的一篇文章:

一文帶你入門Java Stream流,太強了

Arrays 類的 stream() 方法可以將陣列轉換成流:

String[] intro = new String[] { "沉""默""王""二" };
System.out.println(Arrays.stream(intro).count());

還可以為 stream() 方法指定起始下標和結束下標:

System.out.println(Arrays.stream(intro, 12).count());

如果下標的範圍有誤的時候,比如說從 2 到 1 結束,則程式會丟擲 ArrayIndexOutOfBoundsException 異常:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: origin(2) > fence(1)
    at java.base/java.util.Spliterators.checkFromToBounds(Spliterators.java:387)

06、列印陣列

關於陣列的列印方式,我之前單獨寫過一篇文章:

列印Java陣列最優雅的方式是什麼?

裡面談了很多種陣列列印的方式,因為陣列是一個物件,直接 System.out.println 的話,結果是這樣的:

[Ljava.lang.String;@3d075dc0

那最優雅的方式,其實文章裡面已經出現過很多次了,就是 Arrays.toString()

public static String toString(Object[] a) {
    if (a == null)
        return "null";

    int iMax = a.length - 1;
    if (iMax == -1)
        return "[]";

    StringBuilder b = new StringBuilder();
    b.append('[');
    for (int i = 0; ; i++) {
        b.append(String.valueOf(a[i]));
        if (i == iMax)
            return b.append(']').toString();
        b.append(", ");
    }
}

小夥伴可以好好欣賞一下這段原始碼,感覺考慮得非常周到。

07、陣列轉 List

儘管陣列非常強大,但它自身可以操作的工具方法很少,比如說判斷陣列中是否包含某個值。轉成 List 的話,就簡便多了。

String[] intro = new String[] { "沉""默""王""二" };
List<String> rets = Arrays.asList(intro);
System.out.println(rets.contains("二"));

不過需要注意的是,Arrays.asList() 返回的是 java.util.Arrays.ArrayList,並不是 java.util.ArrayList,它的長度是固定的,無法進行元素的刪除或者新增。

rets.add("三");
rets.remove("二");

執行這兩個方法的時候,會丟擲異常:

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.base/java.util.AbstractList.add(AbstractList.java:153)
    at java.base/java.util.AbstractList.add(AbstractList.java:111)

要想操作元素的話,需要多一步轉化:

List<String> rets1 = new ArrayList<>(Arrays.asList(intro));
rets1.add("三");
rets1.remove("二");

08、setAll

Java 8 新增了 setAll() 方法,它提供了一個函數語言程式設計的入口,可以對陣列的元素進行填充:

int[] array = new int[10];
Arrays.setAll(array, i -> i * 10);
System.out.println(Arrays.toString(array));

這段程式碼什麼意思呢?i 就相當於是陣列的下標,值從 0 開始,到 9 結束,那麼 i * 10 就意味著 0 * 10 開始,到 9 * 10 結束,來看一下輸出結果:

[0102030405060708090]

比之前的 fill() 方法強大多了,對吧?不再填充的是相同的元素。

09、parallelPrefix

parallelPrefix() 方法和 setAll() 方法一樣,也是 Java 8 之後提供的,提供了一個函數語言程式設計的入口,通過遍歷陣列中的元素,將當前下標位置上的元素與它之前下標的元素進行操作,然後將操作後的結果覆蓋當前下標位置上的元素。

int[] arr = new int[] { 1234};
Arrays.parallelPrefix(arr, (left, right) -> left + right);
System.out.println(Arrays.toString(arr));

上面程式碼中有一個 Lambda 表示式((left, right) -> left + right),需要入門的小夥伴可以檢視我之前寫的一篇文章:

Lambda 表示式入門,看這篇就夠了

那為了讓小夥伴看得更明白一些,我們把上面這段程式碼稍微改造一下:

int[] arr = new int[]{1234};
Arrays.parallelPrefix(arr, (left, right) -> {
    System.out.println(left + "," + right);
    return left + right;
});
System.out.println(Arrays.toString(arr));

先來看一下輸出結果:

1,2
3,3
6,4
[1, 3, 6, 10]

也就是說, Lambda 表示式執行了三次:

  • 第一次是 1 和 2 相加,結果是 3,替換下標為 1 的位置
  • 第二次是 3 和 3 相加,結果是 6,也就是第一次的結果和下標為 2 的元素相加的結果
  • 第三次是 6 和 4 相加,結果是 10,也就是第二次的結果和下標為 3 的元素相加的結果

有點強大,對吧?

10、總結

好了,我親愛的小夥伴們,以上就是本文的全部內容了,能看到這裡的都是最優秀的程式設計師,二哥必須要為你點個贊。

我是沉默王二,一枚有趣的程式設計師。如果覺得文章對你有點幫助,請微信搜尋「 沉默王二 」第一時間閱讀,回覆【666】更有我為你精心準備的 500G 高清教學視訊(已分門別類)。

本文 GitHub 已經收錄,有大廠面試完整考點,歡迎 Star。

原創不易,莫要白票,請你為本文點個贊吧,這將是我寫作更多優質文章的最強動力。

相關文章