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 中是特殊型別:
- 在內部可實作為 0
- 可以其他型別宣告變數並賦 null 值,例如
int num = null;
、String str = null;
- 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;
- 印出所有元素
Arrays.toString(Object[])
int[] arr = { 37, 64, 21, 93, 12 }; System.out.println(Arrays.toString(arr)); // 結果:[37, 64, 21, 93, 12]
- 填滿陣列元素
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]
- 排序元素
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));
搜尋元素
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));
判斷陣列元素
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));
回傳 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,以及常用的操作。