讀thinking in java筆記(十五):陣列
1. 陣列為什麼特殊
Java中有大量其他的方式可以持有物件,那麼,到底是什麼使陣列變得與眾不同呢?
陣列與其他種類的容器之間的區別有三方面:效率、型別和儲存基本型別的能力。在Java中陣列是一種效率最高的儲存和隨機訪問物件引用序列的方式。陣列就是一個簡單的線性序列,這使得元素訪問非常快速。但是為這種速度所付出的代價是陣列物件的大小被固定,並且在其生命週期中不可改變。你可能會建議使用ArrayList,它可以通過建立一個新例項,然後把舊例項中所有的引用移到新例項中,從而實現更多空間的自動分配。儘管通常應該首選ArrayList而不是陣列,但是這種彈性需要開銷,因此,ArrayList的效率比陣列低很多。
陣列和容器都可以保證你不能濫用它們。無論你是使用陣列還是容器,如果越界,都會得到一個表示程式設計師錯誤的RuntimeException異常。
在泛型之前,其他的容器類在處理物件時,都將它們視作沒有任何具體型別。也就是所,它們將這些物件都當作Java中所有類的根類Object處理。陣列之所以優於泛型之前的容器,就是因為你可以建立一個數組去持有某種具體型別。這意味著你可以通過編譯器檢查,來防止插入錯誤型別和抽取不當型別。當然,不論在編譯時還是在執行時,Java都會阻止你向物件傳送不恰當的訊息。所以,並不是說哪種方法更不安全,只是如果編譯時就能指出錯誤,會顯得更加優雅,也減少了程式的使用者被異常嚇到的可能性。
陣列可以持有基本型別,而泛型之前的容器則不能。但是有了泛型,容器就可以指定並檢查它們所持有物件的型別,並且有了自動包裝機制,容器看起來還能持有基本型別。
陣列與泛型容器進行比較的示例:
class BerylliumSphere {
private static long counter;
private final long id = counter++;
public String toString() { return "Sphere " + id; }
}
public class ContainerComparison {
public static void main(String[] args) {
BerylliumSphere[] spheres = new BerylliumSphere[10 ];
for(int i = 0; i < 5; i++)
spheres[i] = new BerylliumSphere();
print(Arrays.toString(spheres));
print(spheres[4]);
List<BerylliumSphere> sphereList =
new ArrayList<BerylliumSphere>();
for(int i = 0; i < 5; i++)
sphereList.add(new BerylliumSphere());
print(sphereList);
print(sphereList.get (4));
int[] integers = { 0, 1, 2, 3, 4, 5 };
print(Arrays.toString(integers));
print(integers[4]);
List<Integer> intList = new ArrayList<Integer>(
Arrays.asList(0, 1, 2, 3, 4, 5));
intList.add(97);
print(intList);
print(intList.get(4));
}
} /* Output:
[Sphere 0, Sphere 1, Sphere 2, Sphere 3, Sphere 4, null, null, null, null, null]
Sphere 4
[Sphere 5, Sphere 6, Sphere 7, Sphere 8, Sphere 9]
Sphere 9
[0, 1, 2, 3, 4, 5]
4
[0, 1, 2, 3, 4, 5, 97]
4
*/
這兩種持有物件的方式都是型別檢查型的,並且唯一明顯的差異就是陣列使用[]來訪問元素,而List使用的是add()和get()這樣的方法。
隨著自動包裝機制的出現,容器已經可以與陣列幾乎一樣的用於基本型別中了。陣列碩果僅存的優點就是效率。
2. 陣列是第一級物件
無論使用哪種型別的陣列,陣列識別符號其實只是一個引用,指向在堆中建立的一個真實物件,這個(陣列)物件用以儲存指向其他物件的引用。可以作為陣列初始化語法的一部分隱式的建立此物件,或者用new表示式顯式的建立。只讀成員length是陣列物件的一部分(事實上,這是唯一一個可以訪問的欄位或方法),表示此陣列物件可以儲存多少元素。“[]”語法是訪問陣列物件唯一的方式。
下例總結了初始化陣列的各種方式,以及如何對指向陣列的引用賦值,使之指向另一個數組物件。此例也說明,物件陣列和基本型別陣列在使用上幾乎是相同的;唯一的區別就是物件陣列儲存的數引用,基本型別陣列直接儲存基本型別的值。
public class ArrayOptions {
public static void main(String[] args) {
// Arrays of objects:
BerylliumSphere[] a; // Local uninitialized variable
BerylliumSphere[] b = new BerylliumSphere[5];
// The references inside the array are
// automatically initialized to null:
print("b: " + Arrays.toString(b));
BerylliumSphere[] c = new BerylliumSphere[4];
for(int i = 0; i < c.length; i++)
if(c[i] == null) // Can test for null reference
c[i] = new BerylliumSphere();
// Aggregate initialization:
BerylliumSphere[] d = { new BerylliumSphere(),new BerylliumSphere(), new BerylliumSphere()
};
// Dynamic aggregate initialization:
a = new BerylliumSphere[]{
new BerylliumSphere(), new BerylliumSphere(),
};
// (Trailing comma is optional in both cases)
print("a.length = " + a.length);
print("b.length = " + b.length);
print("c.length = " + c.length);
print("d.length = " + d.length);
a = d;
print("a.length = " + a.length);
// Arrays of primitives:
int[] e; // Null reference
int[] f = new int[5];
// The primitives inside the array are
// automatically initialized to zero:
print("f: " + Arrays.toString(f));
int[] g = new int[4];
for(int i = 0; i < g.length; i++)
g[i] = i*i;
int[] h = { 11, 47, 93 };
// Compile error: variable e not initialized:
//!print("e.length = " + e.length);
print("f.length = " + f.length);
print("g.length = " + g.length);
print("h.length = " + h.length);
e = h;
print("e.length = " + e.length);
e = new int[]{ 1, 2 };
print("e.length = " + e.length);
}
} /* Output:
b: [null, null, null, null, null]
a.length = 2
b.length = 5
c.length = 4
d.length = 3
a.length = 3
f: [0, 0, 0, 0, 0]
f.length = 5
g.length = 4
h.length = 3
e.length = 3
e.length = 2
*/
陣列a是一個尚未初始化的區域性變數,在你對它正確的初始化之前,編譯器不允許用此引用做任何事情。陣列b初始化為指向一個BerylliumSphere引用的陣列,但其他並沒有BerylliumSphere物件置於陣列中。然而,仍然可以詢問陣列的大小,因為b指向一個合法的物件。這樣做有一個小缺點:你無法知道在次陣列中確切的有多少元素,因為length只表示陣列能容納多少元素。也就是說,length是陣列的大小,而不是實際儲存的元素個數。新生成一個數組物件時,其中所有的引用被自動初始化為null;所以檢查其中的引用是否為null,即可知道陣列的某個位置是否存在物件。同樣,基本型別的陣列如果是數值型的,就被自動初始化為0;如果是字元型(char)的,就被自動初始化為 ;如果是boolean,就自動初始化為false。
陣列c表明,陣列物件在建立之後,隨即將陣列的各個位置都賦值為BerylliumSphere物件。陣列d表明使用“聚集初始化”語法建立陣列物件(隱式的使用new在堆中建立,就像陣列c一樣),並且以BerylliumSphere物件將其初始化的過程,這些操作只用了一條語句。下一個陣列初始化可以看作是“動態的聚集初始化”。陣列d採用的聚集初始化操作必須在定義d的位置使用,但若使用第二種語法,可以在任意位置建立和初始化陣列物件。例如,假設方法hide()需要一個BerylliumSphere物件的陣列作為輸入引數。可以如下呼叫:hide(d);
但也可以動態的建立將要作為引數傳遞的陣列:hide(new BerylliumSphere[]{new BerylliumSphere});
在許多情況下,此語法使得程式碼書寫變得更方便了。
表示式 a = d;
說明如何將指向某個陣列物件的引用賦給另一個數組物件,這與其他型別的物件引用沒什麼區別。現在a與d都指向堆中的同一個陣列物件。
3. 返回一個數組
演示:如何返回String型別陣列:
public class IceCream {
private static Random rand = new Random(47);
static final String[] FLAVORS = {
"Chocolate", "Strawberry", "Vanilla Fudge Swirl",
"Mint Chip", "Mocha Almond Fudge", "Rum Raisin",
"Praline Cream", "Mud Pie"
};
public static String[] flavorSet(int n) {
if(n > FLAVORS.length)
throw new IllegalArgumentException("Set too big");
String[] results = new String[n];
boolean[] picked = new boolean[FLAVORS.length];
for(int i = 0; i < n; i++) {
int t;
do
t = rand.nextInt(FLAVORS.length);
while(picked[t]);
results[i] = FLAVORS[t];
picked[t] = true;
}
return results;
}
public static void main(String[] args) {
for(int i = 0; i < 7; i++)
System.out.println(Arrays.toString(flavorSet(3)));
}
} /* Output:
[Rum Raisin, Mint Chip, Mocha Almond Fudge]
[Chocolate, Strawberry, Mocha Almond Fudge]
[Strawberry, Mint Chip, Mocha Almond Fudge]
[Rum Raisin, Vanilla Fudge Swirl, Mud Pie]
[Vanilla Fudge Swirl, Chocolate, Mocha Almond Fudge]
[Praline Cream, Strawberry, Mocha Almond Fudge]
[Mocha Almond Fudge, Strawberry, Mint Chip]
*/
方法flavorSet()建立了一個名為results的String陣列。此陣列容量為n,由傳入方法的引數決定。然後從陣列FLAVORS中隨機選擇元素,存入results陣列中,它是方法所最終返回的陣列。返回一個數組與返回任何其他物件(實質上是返回引用)沒什麼區別。
說句題外話,注意當flavorSet()隨機選擇各種陣列元素時,它確保不會重複選擇。由一個do迴圈不斷進行隨機選擇,直到找出一個在陣列picked中不存在的元素。(當然,還會比較String以檢查隨機選擇的元素是否已經在陣列results中。)如果成功,將此元素加入陣列,然後查詢下一個(i遞增)。
4. 多維陣列
建立多維陣列很方便。對於基本型別的多維陣列,可以通過使用花括號將每個向量分割開:
public class MultidimensionalPrimitiveArray {
public static void main(String[] args) {
int[][] a = {
{ 1, 2, 3, },
{ 4, 5, 6, },
};
System.out.println(Arrays.deepToString(a));
}
} /* Output:
[[1, 2, 3], [4, 5, 6]]
*/
每隊花括號括起來的集合都會把你帶到下一級陣列。下面的示例使用了JavaSE5的Arrays.deepToString()方法,它可以將多維陣列轉換為多個String,正如從輸出中所看到的那樣。還可以使用new來分配陣列,下面的三維陣列就是在new表示式中分配的:
public class ThreeDWithNew {
public static void main(String[] args) {
// 3-D array with fixed length:
int[][][] a = new int[2][2][4];
System.out.println(Arrays.deepToString(a));
}
} /* Output:
[[[0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0]]]
*/
可以看到基本型別陣列的值在不進行顯式初始化的情況下,會被自動初始化。物件陣列會被初始化為null。