Java SE 6 技術手冊

5.2 進階陣列觀念

陣列本身若作為物件來操作的話,會有許多特性值得討論,這個小節中將討論一些 Java 中更進階的陣列觀念,並且我也將介紹 J2SE 5.0 中對 Arrays 類別所作的功能加強(JDK6 對於 Arrays 的加強,請查看第 21 章),以及如何使用 J2SE 5.0 新增的 foreach 語法來更簡便的循序存取陣列元素。

5.2.1 進階的陣列操作

藉由對陣列物件的進一步探討,您可以稍微瞭解 Java 對物件處理的一些作法,首先來看看一維陣列的參考名稱之宣告:

int[] arr = null;

在這個宣告中,arr 表示一個可以參考至 int 一維陣列物件的參考名稱,但是目前您將這個名稱參考至 null,表示這個名稱參考還沒有參考至實際的物件,在 Java 中,'=' 運算用於基本資料型態時,是將值複製給變數,但當它用於物件時,則是將物件指定給參考名稱來參考,您也可以將同一個物件指定給兩個參考名稱,當物件的值藉由其中一個參考名稱進行操作而變更時,另一個參考名稱所參考到的值也會更動,來看看範例 5.8 的示範。

範例 5.8 AdvancedArray.java

public class AdvancedArray { 
    public static void main(String[] args) { 
        int[] arr1 = {1, 2, 3, 4, 5}; 
        int[] tmp1 = arr1; 
        int[] tmp2 = arr1; 

        System.out.print("透過tmp1取出陣列值:");
        for(int i = 0; i < tmp1.length; i++) 
            System.out.print(tmp1[i] + " "); 

        System.out.print("\n透過tmp2取出陣列值:"); 
        for(int i = 0; i < tmp2.length; i++) 
            System.out.print(tmp2[i] + " "); 

        tmp1[2] = 9; 
        System.out.print("\n\n透過tmp1取出陣列值:");
        for(int i = 0; i < tmp1.length; i++) 
            System.out.print(tmp1[i] + " "); 

        System.out.print("\n透過tmp2取出陣列值:"); 
        for(int i = 0; i < tmp2.length; i++) 
            System.out.print(tmp2[i] + " "); 
        System.out.println();
    } 
}

執行結果:

透過tmp1取出陣列值:1 2 3 4 5
透過tmp2取出陣列值:1 2 3 4 5

透過tmp1取出陣列值:1 2 9 4 5
透過tmp2取出陣列值:1 2 9 4 5

在這個範例中,您藉由 tmp1 名稱改變了索引 2 的元素值,由於 tmp2 也參考至同一陣列物件,所以 tmp2 取出索引 2 的元素值是改變後的值,事實上在範例 5.8 中,有三個參考名稱參考至同一個陣列物件,也就是 arr1、tmp1 與 tmp2,如下圖所示:

三個名稱參考至同一物件

圖 5.3 三個名稱參考至同一物件

所以您應該知道,如果取出 arr1 索引 2 的元素,元素值也會是 9。

在宣告 int[] arr 之後,arr 是一個一維陣列物件的參考名稱,所以它可以參考至任何長度的一維陣列物件,這邊使用範例 5.9 來作示範。

範例 5.9 AdvancedArray2.java

public class AdvancedArray2 { 
    public static void main(String[] args) { 
        int[] arr1 = {1, 2, 3, 4, 5}; 
        int[] arr2 = {5, 6, 7}; 
        int[] tmp = arr1;

        System.out.print("使用tmp取出arr1中的元素:");
        for(int i = 0; i < tmp.length; i++) 
            System.out.print(tmp[i] + " "); 

        tmp = arr2; 
        System.out.print("\n使用tmp取出arr2中的元素:");
        for(int i = 0; i < tmp.length; i++) 
            System.out.print(tmp[i] + " "); 
        System.out.println();
    } 
}

在範例 5.9 中,tmp 可以參考至擁有5個元素的一維陣列,也可以參考至擁有 3 個元素的一維陣列,執行結果如下:

使用tmp取出arr1中的元素:1 2 3 4 5
使用tmp取出arr2中的元素:5 6 7

您瞭解到在 Java 中陣列是一個物件,而使用 '=' 指定時是將物件指定給陣列名稱來參考,也就是相當於圖 5.3 中改變名稱所綁定的物件,而不是將陣列進行複製,如果您想將整個陣列的值複製給另一個陣列該如何作呢?您可以使用迴圈,將整個陣列的元素值走訪一遍,並指定給另一個陣列相對應的索引位置,範例 5.10 示範了進行陣列複製的方法。

範例 5.10 ArrayCopy.java

public class ArrayCopy { 
    public static void main(String[] args) { 
        int[] arr1 = {1, 2, 3, 4, 5}; 
        int[] arr2 = new int[5]; 

        for(int i = 0; i < arr1.length; i++) 
            arr2[i] = arr1[i];

        for(int i = 0; i < arr2.length; i++) 
            System.out.print(arr2[i] + " "); 
        System.out.println();
    } 
}

執行結果:

1 2 3 4 5

另一個進行陣列複製的方法是使用 System 類別所提供的 arraycopy() 方法,其語法如下:

System.arraycopy(來源, 起始索引, 目的, 起始索引, 複製長度);

範例 5.11 改寫了範例 5.10,使用 System.arraycopy() 進行陣列複製,執行結果與範例 5.10 是相同的。

範例 5.11 ArrayCopy2.java

public class ArrayCopy2 { 
    public static void main(String[] args) { 
        int[] arr1 = {1, 2, 3, 4, 5}; 
        int[] arr2 = new int[5];

        System.arraycopy(arr1, 0, arr2, 0, arr1.length);

        for(int i = 0; i < arr2.length; i++) 
            System.out.print(arr2[i] + " "); 
        System.out.println();
    } 
}

在 JDK6 中,也為 Arrays 類別新增了陣列複製的 copyOf() 方法,詳情請查看第 21 章。

5.2.2 Arrays 類別

對陣列的一些基本操作,像是排序、搜尋與比較等動作是很常見的,在 Java 中提供了 Arrays 類別可以協助您作這幾個動作,Arrays 類別位於 java.util 套件中,它提供了幾個方法可以直接呼叫使用。

表 5.2 Arrays 類別提供的幾個方法說明

名稱 說明
sort() 幫助您對指定的陣列排序,所使用的是快速排序法
binarySearch() 讓您對已排序的陣列進行二元搜尋,如果找到指定的值就傳回該值所在的索引,否則就傳回負值
fill() 當您配置一個陣列之後,會依資料型態來給定預設值,例如整數陣列就初始為 0,您可以使用Arrays.fill()方法來將所有的元素設定為指定的值
equals() 比較兩個陣列中的元素值是否全部相等,如果是將傳回true,否則傳回 false

範例 5.12 示範了使用 Arrays 來進行陣列的排序與搜尋。

範例 5.12 ArraysMethodDemo.java

import java.util.Scanner;
import java.util.Arrays;

public class ArraysMethodDemo {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int[] arr = {93, 5, 3, 55, 57, 7, 2 ,73, 41, 91};

        System.out.print("排序前: "); 
        for(int i = 0; i < arr.length; i++) 
            System.out.print(arr[i] + " "); 
        System.out.println(); 

        Arrays.sort(arr);

        System.out.print("排序後: "); 
        for(int i = 0; i < arr.length; i++) 
            System.out.print(arr[i] + " ");

        System.out.print("\n請輸入搜尋值: "); 
        int key = scanner.nextInt();
        int find = -1;
        if((find = Arrays.binarySearch(arr, key)) > -1) {
            System.out.println("找到值於索引 " + 
                                       find + " 位置"); 
        }
        else 
            System.out.println("找不到指定值"); 
    }
}

執行結果:

排序前: 93 5 3 55 57 7 2 73 41 91
排序後: 2 3 5 7 41 55 57 73 91 93
請輸入搜尋值: 7
找到值於索引 3 位置

範例 5.13 示範了使用 Arrays 來進行陣列的填充與比較。

範例 5.13 ArraysMethodDemo2.java

import java.util.Arrays;

public class ArraysMethodDemo2 {
    public static void main(String[] args) {
        int[] arr1 = new int[10]; 
        int[] arr2 = new int[10]; 
        int[] arr3 = new int[10]; 

        Arrays.fill(arr1, 5); 
        Arrays.fill(arr2, 5); 
        Arrays.fill(arr3, 10); 

        System.out.print("arr1: "); 
        for(int i = 0; i < arr1.length; i++) 
            System.out.print(arr1[i] + " "); 

        System.out.println("\narr1 = arr2 ? " + 
                         Arrays.equals(arr1, arr2)); 
        System.out.println("arr1 = arr3 ? " + 
                         Arrays.equals(arr1, arr3)); 
    }
}

執行結果:

arr1: 5 5 5 5 5 5 5 5 5 5
arr1 = arr2 ? true
arr1 = arr3 ? false

請注意到,您不可以用 '==' 來比較兩個陣列的元素值是否相等,'==' 使用於物件比對時,是用來測試兩個物件名稱是否參考至同一個物件,也就是測試兩個名稱是不是綁定至同一個物件,範例 5.14 是這個觀念的實際示範。

範例 5.14 TestArrayValue.java

public class TestArrayValue {
    public static void main(String[] args) { 
        int[] arr1 = {1, 2, 3, 4, 5};
        int[] arr2 = {1, 2, 3, 4, 5};

        int[] tmp = arr1;

        System.out.println(arr1 == tmp); 
        System.out.println(arr2 == tmp); 
    }    
}

在範例 5.14 中,雖然 arr1 與 arr2 中的元素值是相同的,但實際上 arr1 與 arr2 是參考至不同的兩個陣列物件,您將 arr1 指定給 tmp 來參考,由於 tmp 與 arr1 是參考同一陣列物件,如圖 5.4 所示:

arr1 與 tmp 是參考至同一物件

圖 5.4 arr1 與 tmp 是參考至同一物件

所以進行 'arr1==tmp' 比較時會顯示 true,而 tmp 與 arr2 是參考至不同陣列物件,所以進行 'arr2==tmp' 比較時會顯示 false,執行結果如下:

true
false

在 J2SE 5.0 中對對 Arrays 類別作了不少的修改與功能新增,由此可見陣列操作在程式中的重要性,這邊介紹 Arrays 中新增的兩個方法:deepEquals() 與 deepToString()。

表 5.3 Arrays 類別新增的兩個方法說明

名稱 說明
deepEquals() 對陣列作深層比較,簡單的說,您可以對二維仍至三維以上的陣列進行比較是否相等
deepToString() 將陣列值作深層輸出,簡單的說,您可以對二維仍至三維以上的陣列輸出其字串值

範例 5.15 是個簡單示範,它對三個二維陣列進行深層比較與深層輸出。

範例 5.15 NewArraysDemo.java

import java.util.Arrays; 

public class NewArraysDemo { 
    public static void main(String args[]) { 
        int[][] arr1 = {{1, 2, 3},
                             {4, 5, 6},
                            {7, 8, 9}};
        int[][] arr2 = {{1, 2, 3},
                             {4, 5, 6},
                             {7, 8, 9}};
        int[][] arr3 = {{0, 1, 3},
                             {4, 6, 4},
                             {7, 8, 9}};

        System.out.println("arr1 內容等於 arr2 ? " + 
                            Arrays.deepEquals(arr1, arr2));
        System.out.println("arr1 內容等於 arr3 ? " + 
                            Arrays.deepEquals(arr1, arr3));
        System.out.println("arr1 deepToString()\n\t" + 
                            Arrays.deepToString(arr1));
    } 
}

執行結果:

arr1 內容等於 arr2 ? true
arr1 內容等於 arr3 ? false
arr1 deepToString()
        [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

當然 Arrays 並不只有以上介紹的功能,總之,如果您之前對 Arrays 沒這麼的重視,現在您可以多關照它幾眼,如果您有陣列操作方面的相關需求,可以先查查 java.util.Arrays 的 API 文件說明,看看有沒有現成的方法可以使用。

5.2.3 foreach 與陣列

J2SE 5.0 新增了 foreach 的語法,又稱加強的 for 迴圈(Enhanced for Loop),其應用的對象之一是在陣列的循序存取上,foreach 語法如下:

for(type element : array) {
  System.out.println(element)....
}

直接以實例的方式來說明會更容易瞭解這個語法如何使用,在 J2SE 5.0 之前您可以使用以下的方式來循序存取陣列中的元素:

int[] arr = {1, 2, 3, 4, 5};
for(int i = 0; i < arr.length; i++)
    System.out.println(arr[i]);

在 J2SE 5.0 中可以使用新的 foreach 語法這麼寫:

int[] arr = {1, 2, 3, 4, 5};
for(int element : arr)
    System.out.println(element);

每一次從 arr 中取出的元素,會自動設定給 element,您不用自行判斷是否超出了陣列的長度,注意 element 的型態必須與陣列元素的元素的型態相同。

如果是物件的話,作法也是類似,例如存取字串陣列的話,可以如下撰寫:

String[] names = {"caterpillar", "momor", "bush"};
for(String name : names)
     System.out.println(name);

那麼二維陣列呢?基本上您要是瞭解陣列本身就是個物件,您自然就會知道如何存取,舉個例子:

int[][] arr = {{1, 2, 3},
               {4, 5, 6},
               {7, 8, 9}};
for(int[] row : arr) {
    for(int element : row) {
        System.out.println(element);
    }
}

三維以上的陣列使用 foreach 的方式來存取也可以依此類推。

5.2.4 物件陣列

如果使用類別型態來宣告陣列,有幾個常見的有趣問題,在這邊先一步一步來看,首先請問您,以下產生幾個物件:

int[] arr = new int[3];

這個是很基本的問題,答案是一個一維陣列物件,由於元素型態是 int,所以每個元素值初始值是 0。那麼以下的宣告產生幾個物件:

int[][] arr = new int[2][3];

答案是 3 個,理由可以看看先前圖 5.1 的圖解就可以明白。現在再請問,以下產生幾個物件:

Integer[] arr = new Integer[3];

有的人會以為這樣會產生 3 個 Integer 的實例,但事實上不是,以上產生的是 1 個一維陣列,由於元素型態是 Integer,所以元素值全部參考至 null,如圖 5.5 所示:

一維物件陣列示意圖

圖 5.5 一維物件陣列示意圖

最後一個問題,請問以下的宣告產生幾個物件?

Integer[][] arr = new Integer[2][3];

不好想像嗎?這時畫個圖就很清楚了,如圖 5.6 所示:

二維物件陣列示意圖

圖 5.6 二維物件陣列示意圖

當您搞不清楚物件之間的配置關係時,畫圖是很好的表示方式,在上圖中,我們可以看到有3個物件,而由於元素型態是 Integer,所以六個元素參考名稱預設都是參考至 null。