Java 學習筆記 13 – 陣列物件


Posted by vickyh1315 on 2024-06-05

Java 將陣列視為物件,宣告這樣一個陣列,

int[] arr = { 1, 2, 3 };

相當於下列語法:

int[] arr = new int[3]; // 這裡 int[] 可用 var 代替
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;

類別裡面 main 方法的引數,就是字串陣列(String args[])

class 類別 {
    public static void main(String args[]) {
        // ...
    }
}

可變引數(Variable Arguments)也是利用陣列特性,做到可放入不固定引數。

// 可變引數只能放最後面
void print(int argA, double argB, String ...values)
{
    System.out.println(Array.toString(values))
}

編譯器會以元素個數跟值去開記憶體空間、建立物件,並回傳參考位址給變數 arr,所以宣告陣列當下就需要指定元素個數(編譯器預設指向 null,可以後面再賦值)。

備註:var 是從 Java 10 開始的特性,改變以往宣告變數需先指定型別的寫法,又稱作 Local Variable Type Inference(類型推斷)。
由於會被作為識別字,儘管 Java 本身不禁止將它設為變數名稱,但基於除錯與開發,實務上建議別這麼做,須注意。

遍歷陣列元素,如果存取超過儲存範圍,會拋出 ArrayIndexOutOfBoundsException 錯誤。

// 方法一
for(int i=0; i<arr.length; i++)
{
    for(int j=0; j<arr[i].length, j++)
    {
        System.out.printf("%2d", arr[i][j]);
    }
    System.out.println();
}

// 方法二(Enhanced for loop)
for(int[] row : arr)
{
    for(int value : row)
    {
        System.out.printf("%2d", value);
    }
    System.out.println();
}

Stack 放的是參考位址,對應到 Heap 的記憶體空間,編譯器先以元素個數跟值去開記憶體空間,預設指向 null

null 在 Java 中是特殊型別:

  1. 在內部可實作為 0
  2. 可以其他型別宣告變數並賦 null 值,例如 int num = null;String str = null;
  3. null 只等於 null 自己

int[] arr = new int[3]; 初始化一維陣列的記憶體配置(位址為說明意示用,非實際):

arr[0] = 1;arr[1] = 2;arr[2] = 3; 陣列賦值後的記憶體配置:

更多維度的陣列也一樣在陣列元素中放入參考位址,以作用來說可以看成分頁的狀態(二維一頁,三維兩頁,四維三頁…)。

陣列包含物件(參考型別元素)時,同樣以一維陣列舉例,程式碼執行時,會在 Heap 開設記憶體空間,給該陣列元素(存物件參考位址)跟物件。

class Box {
    int id;
}

Item[] arrItemsA = { new Box() { id = 1; } };

陣列複製

上面提到以 = 賦值只是讓其他變數存取陣列的記憶體位址,並不是複製陣列。

int[] array1 = { 1, 1, 1 };
int[] array2 = array1;

根據用途,Java 原生 API 提供多種方法做到複製陣列的功能,前面提到陣列本身就是物件,而物件在複製時分成淺層複製與深層複製。

看下面內容前,先有一個觀念,淺層複製陣列可能是片面複製,深層複製則完全複製出獨立個體。

淺層複製

物件複製會按照來源物件,建立一個新物件,再依來源物件的資料成員進行複製。

進行淺層複製時,如果資料成員是基本型別,複製的就是基本型別的值;如果是參考型別(物件),複製的就是參考位址。

乍聽之下有點模糊,直接觀察方法一:

方法一:System.arraycopy()

System 類別中,有 arraycopy() 靜態方法,能複製相同內容的陣列物件。

// 語法
System.arraycopy(來源陣列, 來源陣列起始索引, 目標陣列, 目標陣列起始索引, 複製元素個數);
int[] arrayA = { 10, 20, 30 };
var arrayB = new int[arrayA.length];

System.arraycopy(arrayA, 0, arrayB, 0, arrayA.length);

arrayA[0] = 1;
System.out.println("arrayA[0]: " + arrayA[0]); // 1
System.out.println("arrayB[0]: " + arrayB[0]); // 10

對於基本型別陣列(如 int),由於基本型別不是引用參考位址,每個值存在不同記憶體空間,是獨立的,使用 System.arraycopy(),可以複製出獨立的基本型別陣列物件(內容相同,但位於不同記憶體)。

參考型別

遇到參考型別陣列,編譯器執行淺層複製,看到物件就會偷懶,只會存取參考位址,因此如果來源改變了位址,就會影響到複製物件。

class Item {
    int id;
}
Item[] arrItemsA = { new Item() { {id = 1;} } };
var arrItemsB = new Item[arrItemsA.length];
System.arraycopy(arrItemsA, 0, arrItemsB, 0, arrItemsA.length);

System.out.println("改 id 前:" + arrItemsA[0]);

arrItemsA[0].id = 2;

System.out.println("改 id 後 arrItemsA:" + arrItemsA[0].id); // 2
System.out.println("改 id 後 arrItemsB:" + arrItemsB[0].id); // 2

String 雖然也是參考型別,但編譯器對它的處理機制又特殊一些(稍後會談到)。

方法二:Arrays.copyOfRange()
Arrays 類別自 Java 6 所新增的 copyOfRange() 方法,可指定複製起始索引與結束索引範圍。

// 語法
Arrays.copyOfRange(來源陣列, 起始索引, 結束索引);

基本型別陣列

int[] arrayA = { 23, 43, 55, 12, 65, 88, 92 };
var arrayB = Arrays.copyOfRange(arrayA, 1, 4);

參考型別陣列

class Item {
    int id;
}
Item[] arrItemsA = { new Item() { {id = 1;} },
                     new Item() { {id = 2;} }
};
var arrItemsB = Arrays.copyOfRange(arrItemsA, arrItemsA.length);

方法三:Object.clone()

.clone() 是繼承 Object 類別的方法,陣列物件可通過此方法,複製完整陣列。

基本型別陣列

int[] arrayA = { 37, 64, 21, 93, 12 };
var arrayB = Arrays.copyOfRange(arrayA, 1, 4);

參考型別陣列

class Item implements Cloneable {
    private int id;

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return this.id;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        super.clone();
        Item item = new Item();
        item.setId(this.id);
        return item;
    }
}
Item[] arrItemsA = { new Item() { {} },
                     new Item() { {} }
};
var arrItemsB = arrItemsA.clone();
arrItemsA[0].setId(arrItemsA[0].getId() + 10);
System.out.println(arrItemsA[0].getId());
System.out.println(arrItemsB[0].getId());

方法四:Stream API

String[] strArray = {"hello", "hello"};
String[] copiedArray = Arrays.stream(strArray).toArray(String[]::new);

使用 Stream API 複製陣列,相同地會對參考型別類型進行淺層複製。

String 陣列屬於參考型別陣列,然而字串是個特殊的類別,由於字串池的機制,當執行上面範例程式,記憶體配置會是這樣:

深層複製

進行深層複製,完整複製出獨立陣列也有蠻多方法,其中兩種有原生的 for 方法跟額外套件庫 Apache Commons:

方法一:for

class Item {
    int id;
    // ... getters and setters
}

Item[] arrItemsA = { new Item() { {id = 1;} },
                     new Item() { {id = 2;} }
};
var arrayB = new Item[arrItemsA.length];

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

方法二:額外套件庫
Apache Commons 提供的複製方法,可以使用深層複製。

引入 Maven dependency

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>
public class Employee implements Serializable {
    // 分別引入以下內容
    // fields
    // getters and setters
}

Item[] arrItemsA = { new Item() { {id = 1;} },
                     new Item() { {id = 2;} }
};
var arrayB = SerializationUtils.clone(arrItemsA);

常用陣列方法

SE API 的 Arrays 類別還有些常用的陣列方法:

import java.util.Arrays;
  1. 印出所有元素 Arrays.toString(Object[])
    int[] arr = { 37, 64, 21, 93, 12 };
    System.out.println(Arrays.toString(arr)); // 結果:[37, 64, 21, 93, 12]
    
  2. 填滿陣列元素 Arrays.fill(Object[],value)
    int[] arr = new int[3];
    System.out.println(Arrays.toString(arr)); // 結果:[0, 0, 0]
    Arrays.fill(arr, -1);
    System.out.println(Arrays.toString(arr)); // 結果:[-1, -1, -1]
    
  3. 排序元素 Arrays.sort(Object[]);
    (用到亂數先引入)
    import java.util.Random;
    
    int arr = new int[5];
    for(int i=0; i<array.length; i++) {
     arr[i] = (int) Math.random() * 10;
    }
    System.out.println(Arrays.toString(arr));
    
  4. 搜尋元素 Arrays.binarySearch(Object[], key)
    搜尋陣列目標的索引值,如果找不到,會回傳小於 0 的整數,需注意的是供搜尋的陣列必須是遞增排序好的狀態。

    int[] arr = { 41, 0, -10, 57 };
    Arrays.sort(arr);
    System.out.println(Arrays.binarySearch(arr, -10));
    System.out.println(Arrays.binarySearch(array, 33));
    
  5. 判斷陣列元素 Arrays.equals(Object[],Object[])
    比較兩個陣列的元素值是否全部相等,並將結果以 boolean 回傳

    int[] arr = { 37, 64, 21, 93, 12 };
    int[] arr2 = { 1, 5, 21, 39, 21 };
    System.out.println(Arrays.equals(arr, arr2));
    
  6. 回傳 List(Collection 底下介面) Arrays.asList(Object[])
    List 是 Collection API 底下其中一種介面,通過 List 實作的類別,可以更方便做到想要的操作。

    import java.util.Arrays;
    import java.util.List;
    
    String[] arr = { "a", "b", "c", "d", "e" };
    Arrays.asList(arr);
    

ArrayList

因為 Array 的特性宣告長度後無法改變,Java 提供 ArrayList 類別,讓可以加入、移除陣列元素。

先引入套件

import java.util.ArrayList;

加入、移除元素

// ArrayList 可同時加入不同資料型別,但不建議
Array arraylist = new ArrayList();

// 加入元素:預設最後索引
arrayList.add(1);
arrayList.add(1.0);
arrayList.add(1.0F);
arrayList.add('1');
arrayList.add("1.0");

System.out.println(arrayList.toString());

// 刪除元素:指定值,編譯器會從頭找,刪除第一符合元素
arrayList.remove("1.0");

System.out.println(arrayList.toString());

也能通過索引操作

// 指定在索引 3 位置插入元素
arrayList.add(3, "here");

// 指定刪除索引 4 元素
arrayList.remove(4);

本篇學習陣列分別在基本型別、參考型別的記憶體配置,如何複製(淺層與深層),以及常用的方法,下篇會討論 Collection API,以及常用的操作。


#java







Related Posts

關於 Fetch API

關於 Fetch API

滲透測試基本技術 第二章(中篇)

滲透測試基本技術 第二章(中篇)

JavaScript 捉摸不定的 This

JavaScript 捉摸不定的 This


Comments