Java學習之路07—陣列


架構圖

創建陣列

作為參考數據類型的一員,陣列(array)在java中其實就是一個物件,因此後續處理陣列時需要將物件的概念套用上,對於學過C/C++的人可能需要一點思想上的轉換

首先我們來看看在java當中,是如何創建一個陣列的。java宣告時可將[]置於變數名前方或後方,但為了與C/C++做出區別,官方推薦將[]放到變數前方:

int[] arr1; // 推薦

int arr2[]; // 也可以

不過宣告完陣列變數arr以後,陣列事實上並不真正存在

對於一個陣列來說,他具備一個物件的特性,也就代表目前的arr只是一個陣列物件的參考名而已。這個階段編譯器只知道這個整數陣列可能會指向一個陣列物件,所以在宣告陣列變數後還需要使用new在Heap當中生成一個真正的整數陣列,然後再指定給arr

下面幾種方法均可以生成一個陣列物件:

/* 方法1 */
int[] arr1;
arr1 = new int[10];

/* 方法2 */
int[] arr2 = new int[10];

/* 方法3 */
int[] arr3 = new int[]{1,2,3,4,5,6,7,8,9,10}; 

/* 方法4 */
int[] arr3 = {1,2,3,4,5,6,7,8,9,10}; // 可以省略掉 int[]

初始化需要注意的幾個問題

  • 陣列一旦建立,長度就固定了,若需要更改只能重新建立一個長度更長的陣列
  • 數據類型為必填項
  • 若是使用直接賦值法(例如方法3、4)可以忽略陣列長度
  • 在java當中所有陣列都是動態分配的

陣列初始值
使用new建立陣列後,有別於其他變數類型,每個陣列元素都會自動分配一個預設值(物件特性)

  • 對於引用資料類型或任何物件(ex. String或一個陣列物件) → null
  • 對於byte/short/int/long → 0
  • 對於float/double → 0.0
  • 對於字元 → null字元\u0000
  • 對於布林 → false

越界
當陣列index小於0或者等於或大於當前陣列長度時就會發生越界,例如我們故意將index自增到與陣列常相等(a.length為陣列長度)

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

        for(int i=0; i<=arr.length; i++){
            System.out.println("arr["+i+"] = " + arr[i]); // i=7時發生異常
        }
    }
}

輸出結果:

arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
arr[5] = 6
arr[6] = 7
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 7 out of bounds for length 7
        at Main.main(Main.java:15)

我們可以看到程式拋出例外資訊ArrayIndexOutOfBoundsException。不像C/C++一樣,編譯器不會事先檢查陣列是否越界,而是在執行過程中處理意外(Runtime Exception)

有幾種方法可以解決例外的產生,分別是撰寫例外處理程式或者使用For-each loop迴圈

使用例外處理
我們把越界問題調整一下,將上述程式的起始值設定成-1,終值設定為<=arr.length+1,產生三次越界。與此同時我們也已經知道越界異常退跳出ArrayIndexOutOfBoundsException,所以可以利用try catch去捕捉這個異常:

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

        for(int i=-1; i<=arr.length+1; i++){
            try {
                System.out.println("arr["+i+"] = " + arr[i]);
            } catch(ArrayIndexOutOfBoundsException e){ // 捕捉越界例外發生
                System.out.println("發生越界!"); // 通知例外發生
                continue; // 直接進入下一次循環
            }
        }
    }
}

輸出結果:

發生越界!
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
arr[5] = 6
arr[6] = 7
發生越界!
發生越界!

如此一來程式執行時發生越界問題時能夠即時通報,並繼續運行值到跳出迴圈。下一小節將會繼續探討避免越界例外發生的另一個解決方法: For-each loop

For-each loop

For-each loop又稱為增強型for迴圈,它是java迴圈的一種語法糖,具備以下幾種好處:

  1. 書寫較為簡潔
  2. 方便遍歷搜尋
  3. 避免越界例外產生

不過若是有特殊需求,例如指定index範圍或是程式邏輯涉及到陣列index時還是建議使用原本的for loop迴圈。基本的增強型for迴圈邏輯結構如下所示:

class Main {
    public static void main(String[] args){
        int[] arr = {1,2,3,4,5,0,0,0};

        for(int n:arr){
            System.out.println(n);  
        }
    }
}

輸出結果:

1
2
3
4
5
0
0
0

增強型for迴圈會自動判斷陣列的長度,並將陣列元素賦值給區域變數(n),直到所有陣列元素接訪問為止。所以上述程式碼其實相當於:

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

        for(int i=0; i<len; i++){ // 有效避免越界發生
            int n = arr2[i];
            System.out.println(n);  
        }
    }
}

多維陣列

二維陣列

二維陣列其實是一種特殊的一維陣列,差別在於二維陣列的變數參考名參考一個陣列物件,該陣列物件的元素是一個陣列的參考名。舉例來說:

int[][] arr;
arr = new int[4][3]{{1,2,3},{4,5,6},{7,8,9},{10,11,12}};

上述程式碼的架構可以表示成下圖,差別在於一個物件的陣列元素是陣列參考,一個是整數值:

其實你可以把int[]看成一種類別,隨便假設它是一個新的變數類型mytype好了,因此我們其實可以把二維陣列看成mytype[]陣列,而arr就是指向這個陣列物件的參考名

如同我們在一維陣列的創建時介紹的一樣,二維陣列也支援多種不同的宣告與動態分配格式:

int[][] arr1;
float arr2[][];
double []arr3[];

arr1 = new int[3][3];
arr2 = new float[3][3];
arr3 = new double[3][3];

/* Direct Method of Declaration */
long[][] num = {{1,2,3},{4,5,6},{7,8,9}};
long[][] num1 = {{78,98},{65,75,63},{98}};

需要特別注意多維陣列在宣告的時候最少需要填上row:

int[][]arr = new int[2][]; // row不能省略
arr[0] = new int[]{1,2,3};
arr[1] = new int[]{4,5,6};

錯誤創建方式如下:

char[][] ch = new char[][] ;
char[][] ch = new char[][5];

二維形式的增強型for迴圈

public class Main
{
    public static void main(String[] args) {
        int[][] arr = { {1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}};

        for(int[] f: arr){
            for(int s: f){
                System.out.print(s + " ");
            }
            System.out.println();
        }
    }
}

輸出結果:

1 2 3 
4 5 6 
7 8 9 
10 11 12

jagged array

其實多維陣列不一定要是方陣。舉二維陣列來說,陣列物件中的參考指向的陣列長度可以不等長,這種陣列我們稱之為jagged array(不規則陣列),例如:

int[][] arr= {{78,98},{65,75,63},{98}};

int len0 = arr[0].length; // 2
int len1 = arr[1].length; // 3
int len2 = arr[2].length; // 1

創建物件時也可以分配不同長度的陣列:

class Main {
    public static void main(String[] args){
        int[][]arr = new int[2][]; // row不能省略
        arr[0] = new int[4];
        arr[1] = new int[5];

        int count = 0;
        for (int i = 0; i < arr.length; i++){
            for (int j = 0; j < arr[i].length; j++){
                arr[i][j] = count++;
            }
        }

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

輸出結果:

0 1 2 3 
4 5 6 7 8 

另外假如一個二維方陣陣列已經被創建,那麼可以將它的改變成一個不規則陣列嗎?答案當然是可以的,我們只需要將參考名參考到新的陣列物件即可:

class Main {
    public static void main(String[] args){
        int[][]arr = new int[2][];
        int []arr2 = {1,2,3,4,5,6,7,8};

        arr[0] = new int[4]; // 參考一個長度為4的陣列
        arr[1] = new int[4]; // 參考一個長度為4的陣列

        System.out.print("arr[0] before: ");
        for(int f: arr[0])
            System.out.print(f + " ");

        System.out.println();

        arr[0] = arr2; // 重新參考到arr2物件

        System.out.print("arr[0] after: ");
        for(int f: arr[0])
            System.out.print(f + " ");
    }
}

例如上述程式中,arr[0]原本參考一個長度為4並且預設值為0的陣列物件,但經過指定操作,將arr[0]參考到與arr2參考相同的物件(好拗口🤔),這時的arr就變成一個不規則陣列

輸出結果:

arr[0] before: 0 0 0 0 
arr[0] after: 1 2 3 4 5 6 7 8 

再舉一個例子,我們先宣告一個二維陣列,它的row會讓使用者決定,而column的長度則會依據當前index去調整,並且陣列元素的值會利用一個整數變數去累加:

import java.util.Scanner;

class Main {
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        System.out.print("input array's row: ");
        int row = sc.nextInt();
        int element = 1;

        int arr[][] = new int[row][];

        for (int i = 0; i < arr.length; i++){
            arr[i] = new int[i + 1];
        }

        for (int i = 0; i < arr.length; i++){
            for (int j = 0; j < arr[i].length; j++){
                arr[i][j] = element++;
            }
        }

        System.out.println("-------output-------");
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr[i].length; j++)
                System.out.print(arr[i][j] + " ");
            System.out.println();
        }
    }
}

輸入20查看其輸出結果:

input array's row: 20
-------output-------
1 
2 3 
4 5 6 
7 8 9 10 
11 12 13 14 15 
16 17 18 19 20 21 
22 23 24 25 26 27 28 
29 30 31 32 33 34 35 36 
37 38 39 40 41 42 43 44 45 
46 47 48 49 50 51 52 53 54 55 
56 57 58 59 60 61 62 63 64 65 66 
67 68 69 70 71 72 73 74 75 76 77 78 
79 80 81 82 83 84 85 86 87 88 89 90 91 
92 93 94 95 96 97 98 99 100 101 102 103 104 105 
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 

三維陣列

三維陣列跟二維陣列類似,不過在宣告時需要增加維度,除此之外創建陣列的方法是相同的:

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

建立三維陣列時除了第一個row以外其他陣列數都可以省略,這部分只要在後續的動態分配中決定就可以了

結合之前所學,我們試著建立一個三維的不規則陣列,最後透過增強型for迴圈將值打印出來:

public class Main
{
    public static void main(String[] args) {
        int[][][] arr = new int[4][][];
        int element=0;

        for(int i=0; i<arr.length; i++){
            arr[i] = new int[i+1][];
            for(int j=0; j<arr[i].length; j++){
                arr[i][j] = new int[j+1];
                for(int k=0; k<arr[i][j].length; k++){
                    arr[i][j][k] = element;
                    element++;
                }        
            }
        }

        for(int [][]f:arr){
            for(int []s:f){
                for(int t:s){
                    System.out.print(t + " ");
                }
                System.out.println();
            }
            System.out.println("--------------");
        }
    }
}

輸出結果:

0 
--------------
1 
2 3 
--------------
4 
5 6 
7 8 9 
--------------
10 
11 12 
13 14 15 
16 17 18 19 
--------------

不建議使用三維以上陣列進行程式撰寫,不易閱讀加上編寫困難會增加開發上的難度,可以思考有無其他資料結構可以代替

互相參考

由於陣列的物件特性,我們可以將變數參考到別的物件上,因為這種特性,陣列物件元素值的改變,是可以透過任何參考它的參考名稱來產生,舉例來說:

int[] arr1 = {1,2,3,4,5};
int[] arr2 = arr1;

System.out.println("arr1[2] = " + arr1[2] + ", arr2[2] = " + arr2[2]);

arr2[2] = 9;

System.out.println("arr1[2] = " + arr1[2] + ", arr2[2] = " + arr2[2]);

輸出結果:

arr1[2] = 3, arr2[2] = 3
arr1[2] = 9, arr2[2] = 9

因為兩個變數名稱接參考同一個物件,所以當arr1改變陣列元素時,arr2[2]也會跟著發生變化,利如下圖所示,因此在編寫程式時需要特別注意這種關聯性

複製陣列

從上述問題我們了解到,如果想要複製陣列值又想同時避免互相參考的特性,最好的辦法就是再創建一個新的陣列物件,然後將陣列元素值一個一個複製過來

比如說最簡單的for迴圈方式:

int[] arr1 = {1,2,3,4,5};
int[] arr2 = new int[5];

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

System.out.println("arr1[2] = " + arr1[2] + ", arr2[2] = " + arr2[2]);

arr2[2] = 9;

System.out.println("arr1[2] = " + arr1[2] + ", arr2[2] = " + arr2[2]);

輸出結果:

arr1[2] = 3, arr2[2] = 3
arr1[2] = 3, arr2[2] = 9

會產生這種差別的原因在於arr2是參考一個不同於arr1的物件。是否使用new就是關鍵點,其背後邏輯如下圖所示

不過其實我們可以借助System、陣列或物件提供的方法來處理陣列複製問題,不需要每次都使用for迴圈來賦值,以下介紹三種常見的陣列複製方法:

System.arraycopy()

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

        System.arraycopy(arr1, 0, arr2, 0, arr2.length); // 複製arr1的前5個陣列元素

        for(int f:arr2){
            System.out.print(f + " ");
        }
    }
}

輸出結果:

1 2 3 4 5 

參數列表:

  1. 源陣列參考名
  2. 源陣列起始index
  3. 目標陣列參考名
  4. 目標陣列起始index
  5. 複製長度

Arrays.copyOf()

import java.util.Arrays; // 需要import

class Main {
    public static void main(String[] args){
        int[] arr1 = {1,2,3,4,5,6,7,8,9,10};
        int[] arr2 = Arrays.copyOf(arr1, 5); // 返回一個物件,它的長度為5,陣列元素為arr1[0]~arr1[4]

        for(int f:arr2){
            System.out.print(f + " ");
        }
    }
}

輸出結果:

1 2 3 4 5 

參數列表:

  1. 源陣列參考名
  2. 複製長度

arr.clone()

class Main {
    public static void main(String[] args){
        int[] arr1 = {1,2,3,4,5,6,7,8,9,10};
        int[] arr2 = arr1.clone(); // 繼承陣列物件arr1

        System.out.print("arr2: ");

        for(int f:arr2){
            System.out.print(f + " ");
        }

        arr2[0] = 100; // 測試是否為互相參考

        System.out.println();
        System.out.print("arr1: ");

        for(int f:arr1){
            System.out.print(f + " ");
        }
    }
}

輸出結果:

arr2: 1 2 3 4 5 6 7 8 9 10 
arr1: 1 2 3 4 5 6 7 8 9 10 

如果想要更詳細的說明可以參考這篇文章,裡面對陣列的複製方法有詳盡的介紹

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

%d 位部落客按了讚: