第五章 數組
數組是一個基礎的數據結構,它用來存儲一組相同類型的元素的集合。數組非常有用,例如Java提供的集合類ArrayList、HashMap等都是基于數組來實現的。
數組是一種容器,用于存儲數據。一旦定義了數組元素的類型,那么這個數組里面就只能存儲這個類型的元素。需要記住的是,數組中的元素是從0開始索引。
本章我們介紹Java中的數組,主要內容包括:
數組的創建與初始化
數組元素訪問
數組的常用操作
多維數組等。
5.1 數組的聲明
一維數組的聲明語法格式有兩種,分別是
Type varName[]; // (1)
或
Type[] varName; // (2)
這里的Type類型可以是基本類型或任意的引用類型。
通常我們使用第(2)種方式,因為它把類型跟變量名分開,語義更加清晰。
例如,我們聲明一個包含10個數字的 int 數組變量
int[] numbers;
但是,僅僅是上面的聲明語句,我們還不能使用numbers變量。
在 Java 中,需要對聲明的數組變量進行初始化才能進行相關的操作。
java> int[] numbers = null;
int[] numbers = null
這里的 null 是引用類型的默認值。這個 null 值在 Java 中是一個非常特殊的值,我們將會在后面的章節中探討。上面的代碼會在棧內存中存儲一個關于numbers數組變量的信息,我們可以用下面的圖來表示
此時的numbers變量里已經存儲了數組的類型信息了。
java> numbers instanceof Object
java.lang.Boolean res2 = false
上面的數組對象的聲明其實跟普通類的對象聲明是一樣的
java> class Person{}
Created type Person
java> Person p = null;
java.lang.Object p = null
java> p instanceof Person
java.lang.Boolean res12 = false
5.2 數組對象實例創建與初始化
數組在Java中其實也是一個對象,數組實例同樣是使用new操作符創建的。只不過數組的聲明語法比較特殊,它使用的是元素的類型加中括號 Type[] varName
的方式, 而普通的類型聲明只需要使用 Type varName
即可。
5.2.1 數組對象的創建
我們使用 new 關鍵字來創建一個數組對象實例。格式為:
數組元素類型[] 數組名 = new 數組元素類型[length];
這個new 的過程會在堆空間中給我們的數組開辟內存空間。其中,length是數組的容量大小。數組是一個固定長度的數據結構,一旦聲明了,那么在這個數組的生命周期內就不能改變這個數組的長度了。如果我們想動態擴容,就需要對數組進行拷貝來實現。ArrayList 的動態擴容就是使用的Arrays.copyOf方法。Arrays.copyOf 方法又使用了System.arraycopy 這個 native 本地方法。我們會在下面的小節中介紹。感興趣的同學還可以閱讀一下java.util.ArrayList類的代碼。
數組是一種非常快的數據結構,如果已經知道元素的長度,那么就應該使用數組而非ArrayList等數據結構。
例如:
java> numbers = new int[10]
int[] numbers = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
這個過程圖示如下
因為數組是引用類型,它的元素相當于類的成員變量,因此數組分配空間后,每個元素也被按照成員變量的規則被隱式初始化。例如,沒有初始化的整型數組元素都將默認值為0,沒有初始化的boolean值是false, String對象數組是null。
java> boolean[] barray = new boolean[2]
boolean[] barray = [false, false]
數組的內置屬性length指定了數組長度
java> numbers.length
java.lang.Integer res13 = 10
java> barray.length
java.lang.Integer res17 = 2
5.2.2 數組的初始化
我們既可以選擇在創建數組的時候初始化數組,也可以以后初始化。
如果我們想在創建數組的同時就初始化元素,使用下面的方式
java> int[] numbers = new int[]{0,1,2,3,4,5,6,7,8,9}
int[] numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
我們還可以省去 new int[], 直接使用花括號
java> int[] numbers = {0,1,2,3,4,5,6,7,8,9}
int[] numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
這種極簡單的形式,我們叫做數組字面量(Array Literals)。
需要注意的是, 如果我們使用一個未作初始化的數組對象,會導致空指針異常
java> int[] x = null;
int[] x = null
java> x[0]
java.lang.NullPointerException
我們也可以把數組定義以及分配內存空間的操作和賦值的操作分開進行,例如:
java> String[] s = new String[3];
java.lang.String[] s = [null, null, null]
java> s[0] = "abc";
java.lang.String res23 = "abc"
java> s[1]="xyz";
java.lang.String res24 = "xyz"
java> s[2]="opq";
java.lang.String res25 = "opq"
java> s
java.lang.String[] s = ["abc", "xyz", "opq"]
通常我們會使用 for 循環來初始化數組的元素, 例如:
java> int[] numbers = new int[10];
int[] numbers = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
java> for(int i = 0; i < 10; i++){
numbers[i] = i * i;
}
java> numbers
int[] numbers = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
5.3 數組元素的訪問
我們使用數組索引(下標)來訪問數組的元素。另外,值得注意的是Java中的數組的邊界檢查,如果程序訪問無效的數組索引,Java會拋出 ArrayIndexOutOfBoundException
異常。例如
java> String[] s = new String[3];
java.lang.String[] s = [null, null, null]
java> s[-1]
java.lang.ArrayIndexOutOfBoundsException: -1
java> s[3]
java.lang.ArrayIndexOutOfBoundsException: 3
java> s[4]
java.lang.ArrayIndexOutOfBoundsException: 4
我們可以看出,負數索引在Java中是無效的,會拋出ArrayIndexOutOfBoundException 。如果我們用大于等于數組長度的無效的索引來訪問數組元素時也會拋出異常。
5.3.1 數組的索引
Java 的數組索引起始于0,[0]返回第一個元素,[length-1]返回最后一個元素。代碼示例如下
java> int[] x = {1,2,3,4,5}
int[] x = [1, 2, 3, 4, 5]
java> x[0]
java.lang.Integer res26 = 1
java> x[x.length-1]
java.lang.Integer res27 = 5
我們可以看出,數組的索引index可以是整型常量或整型表達式。
需要注意的是,只有當聲明定義了數組,并用運算符new為之分配空間或者把這個數組引用變量指向一個數組對象空間,才可以訪問(引用)數組中的每個元素。
需要特別注意的是,這里的length是一個屬性,不是方法,沒有加括號(),我們這里特別說明是為了和String的length()方法做區別。
5.3.2 數組的存儲
數組存儲在Java堆的連續內存空間。如果沒有足夠的堆空間,創建數組的時候會拋出 OutofMemoryError
:
java> int[] xLargeArray = new int[10000000*1000000000]
java.lang.OutOfMemoryError: Java heap space
不同類型的數組有不同的類型,例如下面例子,intArray.getClass()不同于floatArray.getClass()
java> int[] intArray = new int[10]
int[] intArray = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
java> float[] floatArray = new float[10]
float[] floatArray = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
java> intArray.getClass()
java.lang.Class res5 = class [I
java> floatArray.getClass()
java.lang.Class res7 = class [F
我們不能存儲double值在int數組中,否則導致編譯錯誤。
java> intArray[5]=1.2
ERROR: incompatible types: possible lossy conversion from double to int
intArray[5]=1.2;
^
但是反過來是可以的
java> floatArray[5]=1
java.lang.Float res8 = 1.0
因為 Java 有類型默認轉換的機制。
5.3.3 遍歷數組元素
for循環是一種迭代整個數組便捷方法。我們可以使用for循環初始化整個數組、訪問的每個索引或更新、獲取數組元素。
int[] numbers = new int[]{10, 20, 30, 40, 50};
for (int i = 0; i < numbers.length; i++) {
System.out.println("element at index " + i + ": " + numbers[i]);
}
輸出
element at index 0: 10
element at index 1: 20
element at index 2: 30
element at index 3: 40
element at index 4: 50
Java5中開始提供for each循環,使用for each循環可以避免ArrayIndexOutOfBoundException。這里是一個for each循環迭代的例子:
for(int i: numbers){
System.out.println(i);
}
輸出:
10
20
30
40
50
正如你看到的,for each循環不需要檢查數組索引,如果你想逐個地訪問所有的元素這是一種很好的方法。
但是同時因為我們不能訪問索引,所以就不能修改數組元素的值了。
5.4 數組操作常用API
本節我們介紹數組的常用操作,包括Arrays 類 API、拷貝數組等。
Java API中提供了一些便捷方法通過java.utils.Arrays類去操作數組,通過使用Arrays類提供的豐富的方法,我們可以對數組進行排序,還可以快速二分查找數組元素等。
Arrays類的常用方法如下表所示:
方法 | 功能說明 |
---|---|
toString() | 將數組的元素以[1, 2, 3, 4, 5] 這樣的字符串形式返回 |
asList | 數組轉List |
copyOf() | 將一個數組拷貝到一個新的數組中 |
sort() | 將數組中的元素按照升序排列 |
binarySearch() | 二分查找方法:在數組中查找指定元素,返回元素的索引。如果沒有找到返回-1。 注意:使用二分查找的時候,數組要先排好序。 |
Arrays.toString : 將數組轉化成字符串
如果我們直接對一個數組調用 Object對象的 默認toString 方法,我們會得到如下輸出
java> x
int[] x = [1, 2, 3, 4, 5]
java> x.toString()
java.lang.String res33 = "[I@1ddcf61f"
這樣的信息,通常不是我們想要的。Arrays.toString()方法提供了一個更加有用的輸出
java> Arrays.toString(x)
java.lang.String res34 = "[1, 2, 3, 4, 5]"
Arrays.toString針對基本類型提供了如下8個簽名的方法
toString(boolean[] a)
toString(byte[] a)
toString(char[] a)
toString(double[] a)
toString(float[] a)
toString(int[] a)
toString(long[] a)
toString(short[] a)
對于引用類型,則提供了 toString(Object[] a) 方法。下面是 toString 傳入一個引用類型參數的例子。
Person[] persons = new Person[2];
Person jack = new Person();
jack.name = "Jack";
jack.age = 18;
persons[0] = jack;
persons[1] = new Person();
println(Arrays.toString(persons));
輸出:
[Person{name='Jack', age=18}, Person{name='null', age=0}]
其中,Person 類的代碼如下
class Person {
String name;
int age;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Arrays.asList: 數組轉List
Java中數組可以輕易的轉換成ArrayList。ArrayList是一個使用頻率非常高的集合類。ArrayList的優點是可以改變容量大小,ArrayList的動態擴容實現是通過創建一個容量更大的數組,然后拷貝當前數組的元素到這個新的數組來實現。
代碼示例
Integer[] bigX = {1,2,3};
List<Integer> bigXlist = Arrays.asList(bigX);
println("bigXlist size: " + bigXlist.size());
println(JSON.toJSONString(bigXlist));
String[] s = {"a","b","c"};
List slist = Arrays.asList(s);
println("slist size: " + slist.size());
println(JSON.toJSONString(slist));
輸出:
bigXlist size: 3
[1,2,3]
slist size: 3
["a","b","c"]
通過把數組轉成 List,我們就可以方便地使用集合類的常用工具類方法了。例如,我們想要檢查一個數組是否包含某個值,就可以如下實現
String[] s = {"a","b","c"};
List slist = Arrays.asList(s);
boolean b = slist.contains("a");
System.out.println(b);
// true
需要注意的是,如果我們在使用基本類型來聲明的數組上面調用Arrays.asList方法,結果可能并不是我們想要的
int[] x = {1,2,3};
List<int[]> xlist = Arrays.asList(x);
println("xlist size: " + xlist.size());
println(JSON.toJSONString(xlist));
輸出
xlist size: 1
[[1,2,3]]
這個 xlist 的 size 居然是 1 ?! 好奇怪。而且 int[] elementOfXList = xlist.get(0) 。這跟沒調用 asList 的效果一樣,我們拿到的仍然是個數組。
其實,這跟Arrays.asList的實現本身有關。
當使用 int[] 類型聲明數組時, ArrayList 構造函數這里的array 參數類型是
int[1][] ,如下圖所示
而我們使用 Integer 類型聲明數組時,ArrayList 構造函數這里的array 參數類型是Integer[3] ,如下圖所示
所以,我們不要使用Arrays.asList 方法來轉換基本類型聲明的數組時。如果要轉換一定要使用基本類型的包裝類型,這樣才能得到你想要的結果。
Arrays.copyOf:拷貝數組
java.lang.System類提供了一個 native方法來拷貝元素到另一個數組。arraycopy方法簽名如下
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
我們可以通過srcPos參數指定源數組 src 的拷貝下標位置,dest是目標數組,destPos是目標數組的拷貝下標位置, length參數來指定拷貝長度。。代碼示例:
我們先創建源數組
java> String[] src = {"Java","Kotlin","Scala","JS"};
java.lang.String[] src = ["Java", "Kotlin", "Scala", "JS"]
目標數組
java> String[] dest = new String[7]
java.lang.String[] dest = [null, null, null, null, null, null, null]
從下標0開始拷貝,src 元素全部拷貝到 dest 中
System.arraycopy(src,0,dest,0,src.length)
結果
java> dest
java.lang.String[] dest = ["Java", "Kotlin", "Scala", "JS", null, null, null]
如果源數據數目超過目標數組邊界會拋出IndexOutOfBoundsException異常
java> System.arraycopy(src,0,dest,0, 10)
java.lang.ArrayIndexOutOfBoundsException
我們可以看到,使用 System.arraycopy 方法,我們還要創建一個 dest 數組。有點費事。不用擔心,Arrays 類中已經為我們準備好了 copyOf 方法。我們可以直接調用 copyOf 方法對數組進行擴容。函數定義如下
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
其中方法實現里面調用的 copyOf 實現如下
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
我們可以看出其內部實現也是調用了System.arraycopy方法。相當于是對System.arraycopy方法的再高一層次的抽象。在程序設計中,進行向上一層的抽象是最本質也是最實用的方法論之一。
代碼示例:
java> s = Arrays.copyOf(s, s.length * 2)
java.lang.String[] s = ["Java", "Kotlin", "Scala", "JS", null, null, null, null]
Arrays.sort:數組元素排序
對數組元素進行升序排序。代碼示例
java> Integer[] x = {10,2,3,4,5}
java.lang.Integer[] x = [10, 2, 3, 4, 5]
java> Arrays.sort(x)
java> x
java.lang.Integer[] x = [2, 3, 4, 5, 10]
java> String[] s = {"abc", "cba", "bca"}
java.lang.String[] s = ["abc", "cba", "bca"]
java> Arrays.sort(s)
java> s
java.lang.String[] s = ["abc", "bca", "cba"]
需要注意的是,調用 sort 方法時,傳入的數組中的元素不能有 null 值,否則會報空指針異常
String[] s = {"JS", "Java", "Kotlin", "Scala", null, null, null, null}
java.lang.String[] s = ["JS", "Java", "Kotlin", "Scala", null, null, null, null]
java> Arrays.sort(s)
java.lang.NullPointerException
Arrays.binarySearch: 在傳入的數組中二分查找指定的元素
我們首先使用簡單的代碼示例來看一下這個方法的使用
java> Integer[] x = {2,3,4,5,10}
java.lang.Integer[] x = [2, 3, 4, 5,10]
java> Arrays.binarySearch(x, 3)
java.lang.Integer res40 = 1
java> Arrays.binarySearch(x, 10)
java.lang.Integer res41 = 4
java> Arrays.binarySearch(x, 0)
java.lang.Integer res42 = -1
如果找到元素,返回其下標; 如果沒找到,返回 -1 。
這個binarySearch方法定義如下
public static int binarySearch(int[] a, int key) {
return binarySearch0(a, 0, a.length, key);
}
其中,binarySearch0則是標準的二分查找算法的實現
private static int binarySearch0(int[] a, int fromIndex, int toIndex,
int key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
int midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
二分查找算法要求待查找的數組必須是有序的。如果是無序的查找,我們通常只能遍歷所有下標來搜索了。代碼如下
public int search(int[] nums, int target) {
// 遍歷每個元素
for (int i=0; i<nums.length; i++) {
if (nums[i] == target) {
return i; // 找到元素,返回其下標
}
}
// 如果沒找到target
return -1;
}
5.5 多維數組
我們首先來創建一個2行3列的多維數組:
java> int[][] multiArray = new int[2][3]
int[][] multiArray = [[0, 0, 0], [0, 0, 0]]
這是一個長度是2的數組,它的每個元素 ( 例如 [0, 0, 0] )里保存的是長度為3的數組。 多維數組其實也可以叫嵌套數組。下面是初始化多維數組的例子:
java> int[][] multiArray = {{1,2,3},{10,20,30}}
int[][] multiArray = [[1, 2, 3], [10, 20, 30]]
java> multiArray[0]
int[] res44 = [1, 2, 3]
java> multiArray[1]
int[] res45 = [10, 20, 30]
我們可以使用下面的圖來形象地說明多維數組的含義
多維數組就是以數組為元素的數組。上面的二維數組就是一個特殊的一維數組,其每一個元素都是一個一維數組。
我們可以先聲明多維數組的第1維的長度,第2維的長度可以單獨在初始化的時候再聲明。例如:
我們首先聲明一個2行的數組,這里我們并沒有指定每一列的元素長度。代碼如下
java> String[][] s = new String[2][]
java.lang.String[][] s = [null, null]
圖示如下
我們來為每一行元素賦值,我們要的賦給每一行的值也是一個 String 數組
java> s[0] = new String[2]
java.lang.String[] res46 = [null, null]
java> s[1] = new String[3]
java.lang.String[] res47 = [null, null, null]
java> s
java.lang.String[][] s = [[null, null], [null, null, null]]
其中,s[0]=new String[2] 和 s[1]=new String[3] 是限制第2維各個數組的長度。
如下圖所示
這個時候,我們已經基本看到了這個多維數組的結構了 [[null, null], [null, null, null]] 。 第1行是一個有2個元素的數組,第2行是一個有3個元素的數組。
然后,我們對每行每列的元素進行賦值
java> s[0][0] = new String("Java");
java.lang.String res49 = "Java"
java> s[0][1] = new String("Scala");
java.lang.String res50 = "Scala"
java> s[1][0] = new String("Kotlin");
java.lang.String res51 = "Kotlin"
java> s[1][1] = new String("SpringBoot");
java.lang.String res52 = "SpringBoot"
java> s[1][2] = new String("JS");
java.lang.String res53 = "JS"
最終,我們的數組被初始化為
java> s
java.lang.String[][] s = [["Java", "Scala"], ["Kotlin", "SpringBoot", "JS"]]
二維數組中的元素引用方式為 arrayName[index1][index2]。 代碼示例如下
java> s[0][1]
java.lang.String res54 = "Scala"
java> s[1][0]
java.lang.String res55 = "Kotlin"
訪問不存在的元素,同樣拋出ArrayIndexOutOfBoundsException 異常
java> s[0][2]
java.lang.ArrayIndexOutOfBoundsException: 2