2001 年 5 月 15 日
Java Collections (由 Apress 出版社出版) 一書由 John Zukowski 撰寫,介紹了 Java 2 平臺提供的 Collection 庫的詳細信息。其中包含關(guān)于歷史上的 Collection 類、Collection 框架以及可選的 Collection 庫等章節(jié)。這段節(jié)選(“歷史上的 Collection 類”部分的第一章)涵蓋有關(guān)數(shù)組的詳細信息 ― 用它們可以做些什么以及如何在使用數(shù)組的時候避免缺陷。
數(shù)組是在 Java 編程語言中定義的唯一的 Collection 支持。它們是按照 索引可訪問的順序或位置次序來存儲一組元素的對象。它們是 Object 類的子類,實現(xiàn)了 Serializable 和 Cloneable 兩種接口。這里沒有 .java 源文件讓您了解內(nèi)部是如何工作的?;旧?,您要創(chuàng)建一個有特定大小和元素類型的數(shù)組,然后填充它。
注意:因為數(shù)組是 Object 的子類,所以您可以同步數(shù)組變量并調(diào)用它的 wait() 和 notify() 方法。
讓我們看一下使用數(shù)組對象能夠做些什么 ― 從基本用法和聲明開始,然后到復(fù)制和克隆。我們還將了解數(shù)組賦值、等同性檢查以及反射。
數(shù)組基礎(chǔ)知識
在討論聲明、創(chuàng)建、初始化以及復(fù)制數(shù)組的細節(jié)問題之前,讓我們先復(fù)習(xí)一個簡單的數(shù)組示例。當(dāng)創(chuàng)建一個 Java 應(yīng)用程序時, main() 方法有個唯一的字符串?dāng)?shù)組參數(shù): public static void main(String args []) 。編譯器并不在意您用什么參數(shù)名,只在意它是不是一個 String 對象的數(shù)組。
假設(shè)現(xiàn)在我們有個作為 String 對象數(shù)組的應(yīng)用程序的命令行參數(shù),我們可以觀察每個元素并打印它。在 Java 中,數(shù)組知道它們的大小,而且它們總是從位置零開始建立索引。因此,我們可以通過觀察數(shù)組專用的實例變量:length 來詢問這個數(shù)組有多大。下面的代碼展示了如何做到這一點:
public class ArrayArgs {
public static void main (String args[]) {
for (int i=0, n=args.length; i<n; i++) {
System.out.println("Arg " + i +":" + args[i]);
}
}
}
|
注意:數(shù)組索引不能是 long 類型,因為只有非負整數(shù)才可以被用作索引,因此通過從 0 到 2 31-1 的索引范圍有效地將數(shù)組元素的數(shù)量限制在 2,147,483,648 或 2 31個。
因為在遍歷循環(huán)時數(shù)組大小不變,這就沒有必要在每次測試條件中查看 length 了,例如: for (int i=0; i<args.length; i++) 。事實上,在大多數(shù)情況下,不用遞增而用遞減遍歷循環(huán),并用零作測試條件通常會更快: for (int i=args.length-1; i>=0; i -) 。雖然在 JDK 1.1 和 1.2 發(fā)行版中,遞減計數(shù)與遞增計數(shù)相比只有相對較小的性能差異,但這個時間差異在 1.3 發(fā)行版中卻更為顯著。為在您的平臺上演示這種速度差異,請嘗試運行清單 2-1 中的程序,測試循環(huán)“max int”(int 類型的最大值,即 2 的 31 次方減 1)次所需要的時間:
清單 2-1. 對循環(huán)性能計時
public class TimeArray {
public static void main (String args[]) {
int something = 2;
long startTime = System.currentTimeMillis();
for (int i=0, n=Integer.MAX_VALUE; i<n; i++) {
something =- something;
}
long midTime = System.currentTimeMillis();
for (int i=Integer.MAX_VALUE-1; i>=0; i-) {
something = -something;
}
long endTime = System.currentTimeMillis();
System.out.println("Increasing Delta: " + (midTime - startTime));
System.out.println("Decreasing Delta: " + (endTime - midTime));
}
}
|
這個測試程序?qū)嶋H上是對 for 循環(huán)進行計時而不是對數(shù)組存取進行計時,因為這里沒有數(shù)組存取。
注意:在大多數(shù)情況下,在我的 400 MHz Windows NT 操作系統(tǒng)環(huán)境的 JDK 1.1 和 1.2 下,計數(shù)值低于 11,000 秒內(nèi)。但在 JDK1.3 下,并用 -classic 選項(沒有 JIT),計數(shù)值增加到 250,000 左右。甚至用 HotSpot VM with 1.3,計數(shù)值也只介于 19,000 和 30,000 之間。
如果你試圖訪問在數(shù)組頭部前面或者尾部以后的內(nèi)容,一個 ArrayIndexOutOfBoundsException 異常將被拋出。作為 IndexOutOfBoundsException 類的子類, ArrayIndexOutOfBoundsException 是一個運行時異常,如圖 2-1 所示。感到高興的是:這意味著您不必將數(shù)組存取放入 try-catch 代碼塊了。此外,因為查看數(shù)組上下界之外的內(nèi)容是一個運行時異常,所以您的程序會編譯得很好。而程序?qū)⒅辉谖覀冊噲D存取時拋出異常。
圖 2-1. ArrayIndexOutOfBoundsException 的類層次結(jié)構(gòu)
注意:您無法關(guān)閉數(shù)組的邊界檢查。這是 Java 運行時安全體系的一部分,它確保無效內(nèi)存空間永遠不被存取。
下面的代碼演示了一種不正確的讀取命令行數(shù)組元素的方法:
public class ArrayArgs2 {
public static void main (String args[]) {
try {
int i=0;
do {
System.out.println("Arg " + i + ": " + args[i++]);
} while (true);
} catch (ArrayIndexOutOfBoundsException ignored) {
}
}
}
|
雖然在功能上與前一個 ArrayArgs 示例相同,但對控制流使用異常處理是一個不好的編程習(xí)慣。異常處理應(yīng)該為異常情況保留。
聲明和創(chuàng)建數(shù)組
請記住,數(shù)組是按照可通過索引可訪問的順序來存儲一組元素的對象。這些元素可以是基本數(shù)據(jù)類型如 int 或 float ,也可以是 Object 的任意類型。要聲明一個特殊類型的數(shù)組,只要向聲明添加方括號([])就可以了:
對于數(shù)組聲明來說,方括號可以處于三個位置中的其中一個:int[] variable、int []variable 以及 int variable[]。第一個位置說明變量是 int[] 類型的。后兩個說明變量是一個數(shù)組,該數(shù)組是 int 類型的。
注意:聽起來我們似乎是在討論語義學(xué)。但是,在聲明多個變量時,根據(jù)所使用的格式,會存在差異。格式 int[] var1, var2; 將會聲明兩個 int 類型的數(shù)組變量,而 int []var1,var2; 或 int var1[], var2; 將聲明一個 int 類型的數(shù)組和另一個只是 int 類型的變量。
一旦聲明了一個數(shù)組,就可以創(chuàng)建這個數(shù)組并保存一個對它的引用。 new 操作符用來創(chuàng)建數(shù)組。在創(chuàng)建一個數(shù)組時,必須指定它的長度。長度一旦設(shè)定,就不能再更改。象下面的代碼所演示的那樣,長度可以被指定為常量或表達式。
int variable[] = new int [10];
|
或者
int[] createArray(int size) {
return new int[size];
}
|
注意:如果您試圖建立一個長度為負值的數(shù)組,將會拋出 NegativeArraySizeException 運行時異常。但零長度的數(shù)組是有效的。
您可以將數(shù)組的聲明和創(chuàng)建合并為一個步驟:
int variable[] = new int[10];
|
警告:如果數(shù)組的創(chuàng)建導(dǎo)致了 OutOfMemoryError 錯誤的拋出,所有表示數(shù)組大小的表達式都已被求出了值。重要的是能否在指定數(shù)組大小的地方完成一次賦值。例如,如果表達式 int variable[] = new int[var1 = var2*var2] 會導(dǎo)致 OutOfMemoryError 拋出,那么在錯誤被拋出前要設(shè)置變量 var1 。
一旦數(shù)組被創(chuàng)建,您就可以填充該數(shù)組。這通常用一個 for 循環(huán)或者分別的賦值語句實現(xiàn)。例如,下面的代碼將創(chuàng)建并填充一個三元素的 names 數(shù)組:
String names = new String[3];
names [0] = "Leonardo";
names [1] = "da";
names [2] = "Vinci";
|
基本數(shù)據(jù)類型數(shù)組 當(dāng)您創(chuàng)建了一個基本數(shù)據(jù)類型元素的數(shù)組,數(shù)組為那些元素保留了實際值。例如,圖 2-2 所示的是一個被變量 life 引用的包含六個整型元素的數(shù)組 (1452, 1472, 1483, 1495, 1503, 1519) 從棧和堆內(nèi)存的角度看,會是什么樣子。
圖 2-2. 基本數(shù)據(jù)類型數(shù)組的棧和堆內(nèi)存
對象數(shù)組 與基本數(shù)據(jù)類型數(shù)組不同,當(dāng)您創(chuàng)建了一個對象數(shù)組,它們不是存儲在一個實際的數(shù)組里。數(shù)組只是存儲了對實際對象的引用,除非被明確地初始化,否則最初每個引用都為空。(簡言之,更多的是依靠初始化。)圖 2-3 展示了 Object 元素組成的數(shù)組,其元素如下所示:
- Leonardo da Vinci 出生的國家,意大利
- 他的一張油畫圖像 The Baptism of Christ
- 他關(guān)于直升機和降落傘的理論(畫圖)
- 一張 Mona Lisa的圖像
- 他逝世的國家,法國
圖 2-3 中要注意的關(guān)鍵是對象并不在數(shù)組中;在數(shù)組中的只是對對象的 引用。
圖 2-3. 對象數(shù)組的棧和堆內(nèi)存
多維數(shù)組 因為數(shù)組通過引用處理,這就沒有什么能阻止您讓一個數(shù)組元素引用另一個數(shù)組。當(dāng)一個數(shù)組引用另一個時,就得到一個 多維數(shù)組。向聲明行每添加一個維數(shù)就需要一組額外的方括號。例如,如果您想定義一個矩形,即二維數(shù)組,您可以用以下代碼行:
對一維數(shù)組來說,如果數(shù)組類型是基本數(shù)據(jù)類型的其中之一,一旦創(chuàng)建了數(shù)組就可以立即往里面存儲數(shù)值。對于多維數(shù)組,光聲明是不夠的。例如,下面兩行代碼會導(dǎo)致一個編譯時錯誤,因為數(shù)組變量從未被初始化:
int coordinates[][];
coordinates[0][0] = 2;
|
但如果您在這兩句源代碼行之間創(chuàng)建一個數(shù)組(像 coordinates = new int [3][4]; ;),最后一行就會生效。
對于對象數(shù)組,創(chuàng)建一個多維數(shù)組會產(chǎn)生一個充滿空對象引用的數(shù)組。您還是需要創(chuàng)建對象以存儲到數(shù)組中。
因為多維數(shù)組最外層數(shù)組的每個元素都是對象引用,這就沒有必要讓數(shù)組成為一個矩形(或者一個為實現(xiàn)三維數(shù)組創(chuàng)建的立方體)。每個內(nèi)部數(shù)組可以有自己的大小。例如,下面的代碼演示了如何創(chuàng)建一個 float 類型的二維數(shù)組,其中內(nèi)部數(shù)組元素排列像一組保齡球瓶 — 第一排一個元素,第二排兩個,第三排三個,第四排四個:
float bowling[][] = new float[4][];
for (int i=0; i<4; i++) {
bowling[i] = new float[i+1];
}
|
為幫助您形象地認識最終的數(shù)組,請參閱圖 2-4。
圖 2-4. 一個三角形的、像保齡球瓶的數(shù)組
當(dāng)對一個多維數(shù)組進行存取時,在檢查右側(cè)的下一維表達式之前每一維表達式都已被完全求值。知道在存取數(shù)組時是否出現(xiàn)異常是很重要的。
注意:對于一維數(shù)組,雖然在語法上您可以將一個方括號放在數(shù)組變量的前面或后面( [index]name 或者 name[index] ),但對于多維數(shù)組,必須將方括號放在數(shù)組變量的后面,如 name[index1][index2] 。按照語法來看, [index1][index2]name 和 [index1]name[index2] 如果在您的代碼中出現(xiàn),是不合法的,將會導(dǎo)致一個編譯時錯誤。但對于聲明來說,將方括號放在變量名前面( type [][]name )、后面( type name[][] )以及兩邊( type []name[] )是完全合法的。
請記住計算機內(nèi)存是線性的 ― 當(dāng)您對多維數(shù)組進行存取時,實際上是對內(nèi)存中的一維數(shù)組進行存取。如果您可以按照內(nèi)存存儲的次序?qū)λM行存取,就會最有效率。通常,如果所有的東西都在內(nèi)存中安排好了,就不會有什么關(guān)系,因為計算機內(nèi)存中跳躍存取也是很快的。但是,當(dāng)使用大型的數(shù)據(jù)結(jié)構(gòu)時,線性存取執(zhí)行情況最好而且避免了不必要的交換。此外,您可以通過將它們包裝成一維數(shù)組來模擬多維數(shù)組。對圖像經(jīng)常進行這樣處理。有兩種方法包裝一維數(shù)組,一種是以行為主的次序,數(shù)組一次填充一行;還有一種是以列為主的次序,往數(shù)組里放的是列。圖 2-5 顯示了這兩者的不同之處。
圖 2-5. 以行為主與以列為主的次序的比較
注意:在許多圖像處理例程中,像 ImageFilter 類里的 setPixels() 方法,您會發(fā)現(xiàn)二維的圖像數(shù)組轉(zhuǎn)換成了以行為主次序的一維數(shù)組,其中 Pixel(m, n) 轉(zhuǎn)化成了一維的下標(biāo) n * scansize + m。按照自上而下、自左至右的順序讀取整個圖像數(shù)據(jù)。
初始化數(shù)組當(dāng)數(shù)組首次被創(chuàng)建時,運行時環(huán)境會確保數(shù)組內(nèi)容被自動初始化為某些已知的(相對于未定義來說)值。對于未初始化的實例和類變量,數(shù)組內(nèi)容被初始化為:等價于 0 的數(shù)字、等價于 \u0000 的字符、布爾值 false 或者用于對象數(shù)組的 null。如表 2-1 所示。
表 2-1. 數(shù)組初始值
缺省值 |
數(shù)組 |
| byte short int long |
0.0 |
float double |
\u0000 |
char |
false |
boolean |
null |
Object |
當(dāng)您聲明一個數(shù)組時,您可以指定元素的初始值。這可通過在聲明位置等號后面的花括號 {} 之間提供由逗號分隔的數(shù)據(jù)列來完成。
例如,下面的代碼將會創(chuàng)建一個三元素的 names 數(shù)組:
String names[] = {"Leonardo", "da", "Vinci"};
|
注意:如果提供了數(shù)組的初始化程序,則不必指定長度。數(shù)組長度會根據(jù)逗號分隔的數(shù)據(jù)列中元素的數(shù)量被自動地設(shè)置。
注意:Java 語言的語法允許在數(shù)組初始化程序塊中的最后一個元素后帶一個結(jié)尾逗號,如 {"Leonardo", "da", "Vinci",}。這并沒有將數(shù)組的長度變成 4,而仍舊保持為 3 。這種靈活性主要是出于對代碼生成器的考慮。
對于多維數(shù)組,您只要為每個新添加的維數(shù)使用一組額外的圓括號就可以了。例如,下面的代碼創(chuàng)建了一個 6 X 2 的關(guān)于年份和事件的數(shù)組。因為數(shù)組被聲明為 Object 元素的數(shù)組,就必須使用 Integer 包裝類將每個 int 基本數(shù)據(jù)類型的值存儲在里面。數(shù)組中的所有元素都必須是數(shù)組聲明的類型或這種類型的一個子類,在本例中就是 Object ,即使所有的元素都是子類也是如此。
Object events [][] = {
{new Integer(1452), new Birth("Italy")},
{new Integer(1472), new Painting("baptismOfChrist.jpg")},
{new Integer(1483), new Theory("Helicopter")},
{new Integer(1495), new Theory("Parachute")},
{new Integer(1503), new Painting("monaLisa.jpg")},
{new Integer(1519), new Death("France")}
};
|
注意:如果數(shù)組的類型是接口,則數(shù)組中的所有元素都必須實現(xiàn)這個接口。
從 Java 的第二個 dot-point 發(fā)行版(Java 1.1)開始,引入了 匿名數(shù)組的概念。雖然當(dāng)數(shù)組被聲明以后可以很容易初始化,但以后不能再用逗號分隔的數(shù)據(jù)列對數(shù)組進行重新初始化,除非聲明另一個變量來將新的數(shù)組存儲進去。這就是我們引入匿名數(shù)組的原因。有了匿名數(shù)組,您可以將數(shù)組重新初始化為一組新值,或者在您不想定義本地變量來存儲上述數(shù)組時將未命名的數(shù)組傳遞到方法中。
匿名數(shù)組的聲明與常規(guī)數(shù)組類似。但是,您要將包含在花括號里的用逗號分隔的一列值放在方括號的后面,而不是在方括號里指定一個長度。如下所示:
new type[] {comma-delimited-list}
|
為了演示,以下幾行展示了如何調(diào)用方法并將匿名的 String 對象數(shù)組傳遞給它:
method(new String[] {"Leonardo", "da", "Vinci"});
|
您會發(fā)現(xiàn)代碼生成器頻繁的使用匿名數(shù)組。
傳遞數(shù)組參數(shù)并返回值當(dāng)數(shù)組作為參數(shù)被傳遞到方法,對數(shù)組的引用就被傳遞了。這允許您修改數(shù)組的內(nèi)容,并在方法返回的時候讓調(diào)用例程看到數(shù)組的變化。此外,因為引用被傳遞,您還可以返回在方法中創(chuàng)建的數(shù)組,而且不必擔(dān)心在方法完成時垃圾收集器會釋放數(shù)組內(nèi)存。
使用數(shù)組可以做很多事。如果數(shù)組的初始大小已無法滿足您的需要,您就需要創(chuàng)建一個新的更大的數(shù)組,然后將原始元素復(fù)制到更大數(shù)組的相同位置。但是,如果您不需要將數(shù)組變大,而只希望在使原始數(shù)組保持原樣的基礎(chǔ)上修改數(shù)組元素,您必須創(chuàng)建數(shù)組的一個副本或克隆版本。
System 類中的 arraycopy() 方法允許您將元素從一個數(shù)組復(fù)制到另一個數(shù)組。當(dāng)進行這種復(fù)制時,目標(biāo)數(shù)組可以大些;但如果目標(biāo)數(shù)組比源數(shù)組小,就會在運行時拋出一個 ArrayIndexOutOfBoundsException 異常。 arraycopy() 方法用到 5 個參數(shù)(兩個用于數(shù)組,兩個用作數(shù)組的起始位置,還有一個表示復(fù)制元素的數(shù)量): public static void arraycopy (Object sourceArray,int sourceOffset,Object destinationArray,int destinationOffset,int numberOfElementsToCopy) 。 除了類型的兼容性,這里唯一的必要條件是已經(jīng)為目標(biāo)數(shù)組分配了內(nèi)存。
警告:當(dāng)在不同的數(shù)組之間復(fù)制元素時,如果源參數(shù)或目標(biāo)參數(shù)不是數(shù)組,或者它們的類型不兼容,就會拋出 ArrayStoreException 異常。不兼容的數(shù)組比如一個是基本數(shù)據(jù)類型數(shù)組而另一個是對象數(shù)組;或基本數(shù)據(jù)類型不同;或?qū)ο箢愋筒豢少x值。
為了演示,清單 2-2 采用一個整型數(shù)組并創(chuàng)建一個兩倍大的新數(shù)組。下面示例中的 doubleArray() 方法為我們做到了這一點:
清單 2-2. 將數(shù)組大小加倍
public class DoubleArray {
public static void main (String args ]) {
int array1[] = {1, 2, 3, 4, 5};
int array2[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
System.out.println("Original size: " + array1.length);
System.out.println("New size: " + doubleArray(array1).length);
System.out.println("Original size: " + array2.length);
System.out.println("New size: " + doubleArray(array2).length);
}
static int[] doubleArray(int original[]) {
int length = original.length;
int newArray[] = new int[length*2];
System.arraycopy(original, 0, newArray, 0, length);
return newArray;
}
}
|
在獲得源數(shù)組的長度以后,先創(chuàng)建適當(dāng)大小的新數(shù)組,再把原始元素復(fù)制到新數(shù)組中相同的位置。在您了解數(shù)組的反射以后,您可以總結(jié)出將任意類型數(shù)組的大小加倍的方法。
執(zhí)行程序,生成如下輸出:
Original size: 5
New size: 10
Original size: 9
New size: 18
|
注意:當(dāng)用 arraycopy() 進行復(fù)制時,如果您要將數(shù)組的子集復(fù)制到該數(shù)組內(nèi)的另一個區(qū)域,源數(shù)組和目標(biāo)數(shù)組可以是同一個。即使有些地方發(fā)生重疊也會生效。
因為數(shù)組實現(xiàn)了 Cloneable 接口,除了復(fù)制數(shù)組的區(qū)域以外,還可以克隆它們。 克隆包括創(chuàng)建一個同樣大小和類型的數(shù)組,并將所有的原始元素復(fù)制到新數(shù)組里。這和復(fù)制不同,復(fù)制要求您自己創(chuàng)建目標(biāo)數(shù)組并限定其大小。對基本數(shù)據(jù)類型的元素來說,新數(shù)組包含原始元素的副本,所以一個數(shù)組中元素的變化不會反映到它的副本中。但對于對象引用的情況,復(fù)制的只是引用。因而,數(shù)組的兩個副本將指向同一個對象。對該對象的更改將反映到兩個數(shù)組中。這叫做 淺拷貝或者 淺克隆。
為了演示,下面的方法采用了一個整型數(shù)組并返回上述數(shù)組的一個副本。
static int[] cloneArray(int original[]) {
return (int[])original.clone();
}
|
數(shù)組克隆用一個實際起作用的 public 方法覆蓋了被保護起來的 Object 方法,后者通常會拋出 CloneNotSupportedException 異常。
如果您不希望方法的調(diào)用程序修改底層數(shù)組結(jié)構(gòu),從方法返回數(shù)組克隆是非常有用的。您可以聲明數(shù)組為 final ,像下面的示例那樣:
final static int array[] = {1, 2, 3, 4, 5};
|
不過聲明一個對象引用為 final (特別是這里的數(shù)組引用)并不限制您修改對象。它只限制您改變 final 變量引用的內(nèi)容。下面的代碼行導(dǎo)致了一個編譯錯誤:
array = new int[] {6, 7, 8, 9};
|
不過改變個別的元素完全合法:
提示:另一種從方法“返回”不變數(shù)組的方法是將 Enumeration 或者 Iterator 返回到數(shù)組,這比返回實際數(shù)組要好。任一接口都提供對單個元素的存取而不需要改變整個數(shù)組或要求制作整個數(shù)組的副本。您會在 Java Collections的后續(xù)章節(jié)學(xué)到更多的有關(guān)這些接口的知識。
數(shù)組賦值
數(shù)組賦值和變量賦值的操作一樣。如果變量 x 是對 y 數(shù)組的引用,如果 z 類型的變量可以賦值給 y,那么 x 就可以成為對 z 的引用。例如,設(shè)想 y 是個 AWT Component 類,而 z 是個 AWT Button 類。因為 Button 變量可以被賦給 Component 變量,則 Button 數(shù)組可以被賦給 Component 數(shù)組:
Button buttons[] = {
new Button("One"),
new Button("Two"),
new Button("Three")};
Component components[] = buttons;
|
當(dāng)進行這樣的賦值時,兩個變量的 bottons 和 componets 引用的是內(nèi)存的同一個堆空間。如圖 2-6 所示。改變一個數(shù)組中的元素將會造成兩個數(shù)組的元素都改變。
圖 2-6. 數(shù)組賦值后的共享內(nèi)存
如果,將一個數(shù)組變量賦值給一個超類數(shù)組變量之后(正如在前一個示例中將 botton 數(shù)組賦值給 component 數(shù)組變量一樣),您接著試圖將一個不同的子類實例放入數(shù)組,一個 ArrayStoreException 異常就會被拋出。繼續(xù)前一個示例,如果您試圖將一個 Canvas 對象放入 components 數(shù)組,一個 ArrayStoreException 異常就會被拋出。即使 components 數(shù)組被聲明為 Component 對象數(shù)組也是如此,因為 components 數(shù)組是對 Button 對象數(shù)組的特別引用, Canvas 對象無法被存到數(shù)組中。這是個運行時異常,因為從類型安全編譯器看,實際賦值是合法的。
檢查數(shù)組等同性
檢查兩個數(shù)組之間的等同性可以用兩種方式中的一種,這取決于您想要查找的等同性類型。是數(shù)組變量指向內(nèi)存的同一位置因而指向同一數(shù)組?或是兩數(shù)組中的元素相等?
檢查對同一內(nèi)存空間的兩個引用可用由兩個等號組成的運算符(==)來實現(xiàn)。例如,前面的 components 和 buttons 變量在這種情況下將是相等的,因為其中一個是另一個的引用:
components == buttons //true
|
但是,如果您將數(shù)組和它的克隆版本比較,那么用 ==,它們是不會相等的。因為這兩個數(shù)組雖然有相同的元素,但處于不同的內(nèi)存空間,它們是不同的。為了讓一個數(shù)組的克隆版本與原數(shù)組相等,您必須使用 java.util.Arrays 類中的 equals() 方法。
String[] clone = (String[]) strarray.clone();
boolean b1 = Arrays.equals(strarray, clone); //Yes,they're equal
|
這就會檢查每個元素的等同性。在參數(shù)是對象數(shù)組的情況下,每個對象的 equals() 方法會被用來檢查等同性。 Arrays.equals() 方法也為非克隆的數(shù)組服務(wù)。
數(shù)組反射
如果因為某種原因,您并不確定參數(shù)或?qū)ο笫遣皇菙?shù)組,您可以檢索對象的 Class 對象并詢問它。 Class 類的 isArray() 方法將會告訴您。一旦您知道擁有了一個數(shù)組,您可以詢問 Class 的 getComponentType() 方法,您實際擁有的是什么類型的數(shù)組。如果 isArray() 方法返回 false,那么 getComponentType() 方法返回空。否則返回元素的 Class 類型。如果數(shù)組是多維的,您可以遞歸調(diào)用 isArray() 。它將仍只包含一個 component 類型。此外,您可以用在 java.lang.reflect 包里找到的 Array 類的 getLength() 方法獲取數(shù)組的長度。
為了演示,清單 2-3 顯示了傳遞給 main() 方法的參數(shù)是 java.lang.String 對象的數(shù)組,其中數(shù)組長度由命令行參數(shù)的個數(shù)確定:
清單 2-3. 使用反射檢查數(shù)組類型和長度
public class ArrayReflection {
public static void main (String args[]) {
printType(args);
}
private static void printType (Object object) {
Class type = object.getClass();
if (type.isArray()) {
Class elementType = type.getComponentType();
System.out.println("Array of: " + elementType);
System.out.println(" Length: " + Array.getLength(object));
}
}
}
|
注意:如果 printType() 用于前面定義的 buttons 和 components 變量調(diào)用,每個都會表明數(shù)組是 java.awt.Button 類型。
如果不使用 isArray() 和 getComponentType() 方法,而且試圖打印數(shù)組的 Class 類型,您將獲得一個包含 [ ,后面跟著一個字母和類名(如果是個基本數(shù)據(jù)類型就沒有類名)的字符串。例如,如果您試圖打印出上述 printType() 方法中的類型變量,您將獲得 class [Ljava.lang.String; 作為輸出。
除了詢問一個對象是不是數(shù)組以及是什么類型的數(shù)組之外,您還可以在運行時用 java.lang.reflect.Array class 創(chuàng)建數(shù)組。這對于創(chuàng)建一般實用例程非常有用,這些例程執(zhí)行數(shù)組任務(wù),比如將大小加倍。(我們會立即回到那一點。)
要創(chuàng)建一個新數(shù)組,使用 Array 的 newInstance() 方法,它有兩種變化形式。對于一維數(shù)組您通常將使用較簡單版本,它的執(zhí)行方式如語句 new type [length] 所示,并作為對象返回數(shù)組: public static Object newInstance(Class type, int length) 。例如,下面的代碼創(chuàng)建一個五個整數(shù)空間大小的數(shù)組:
int array[] = (int[])Array.newInstance(int.class, 5);
|
注意:要為基本數(shù)據(jù)類型指定 Class 對象,只要在基本數(shù)據(jù)類型名末尾添加 .class 就可以了。您還可以使用包裝類中的 TYPE 變量,如 Integer.TYPE。
newInstance() 方法中的第二種變化形式要求維數(shù)被指定為整型數(shù)組: public static Object newInstance(Class type,int dimensions []) 。在創(chuàng)建一個一維數(shù)組的最簡單的情況下,您將創(chuàng)建只有一個元素的數(shù)組。換句話說,如果您要創(chuàng)建包含五個整數(shù)的相同數(shù)組,您需要創(chuàng)建一個單個元素 5 的數(shù)組并傳遞到 newInstance() 方法,而不是傳遞整數(shù)值 5。
int dimensions[] = {5};
int array[] = (int[])Array.newInstance(int.class, dimensions);
|
在您只需要創(chuàng)建一個矩形數(shù)組的時候,您就可以將每個數(shù)組長度填充到這個 dimensions 數(shù)組中。例如,下面的代碼與創(chuàng)建一個 3 X 4 的整數(shù)數(shù)組等價。
int dimensions[] = {3, 4};
int array[][] = (int[][])Array.newInstance(int.class, dimensions);
|
但是,如果您需要創(chuàng)建一個非矩形數(shù)組,您將需要多次調(diào)用 newInstance() 方法。第一次調(diào)用將定義外部數(shù)組的長度,并獲得一個看上去很古怪的類參數(shù)([].class 適用于元素為 float 類型的數(shù)組)。每個后續(xù)調(diào)用將定義每個內(nèi)部數(shù)組的長度。例如,下面演示了如何創(chuàng)建一個元素為 float 類型的數(shù)組,其內(nèi)部數(shù)組的大小設(shè)置像一組保齡球瓶:第一排一個元素,第二排兩個,第三排三個,第四排四個。為了幫您將這種情況形象化,讓我們回顧早先在圖 2-4 展示的三角形數(shù)組。
float bowling[][] = (float[][])Array.newInstance(float[].class, 4);
for (int i=0; i<4; i++) {
bowling[i] = (float[])Array.newInstance(float.class, i+1);
}
|
一旦在運行時創(chuàng)建了數(shù)組,您還可以獲取和設(shè)置數(shù)組元素。不過通常不會這樣做,除非鍵盤上的方括號鍵失靈或者您在動態(tài)的編程環(huán)境(程序被創(chuàng)建時數(shù)組名未知)中工作。 如表 2-2 所示, Array 類有一系列的 getter 和 setter 方法用來獲取和設(shè)置數(shù)組元素。使用什么方法取決于您處理的數(shù)組類型。
表 2-2. 數(shù)組 getter 和 setter 方法
Getter 方法 |
Setter 方法 |
get(Object array, int index) |
set(Object array, int index, Object value) |
getBoolean(Object array, int index) |
setBoolean(Object array, int index, boolean value) |
getByte(Object array, int index) |
setByte(Object array, int index, byte value) |
getChar(Object array, int index) |
setChar(Object array, int index, char value) |
getDouble(Object array, int index) |
setDouble(Object array, int index, double value) |
getFloat(Object array, int index) |
setFloat(Object array, int index, float value) |
getInt(Object array, int index) |
setInt(Object array, int index, int value) |
getLong(Object array, int index) |
setLong(Object array, int index, long value) |
getShort(Object array, int index) |
setShort(Object array, int index, short value) |
注意:您可以一直使用 get() 和 set() 方法。如果數(shù)組是一個基本數(shù)據(jù)類型數(shù)組, get() 方法的返回值或 set() 方法的值參數(shù)將被包裝到用于基本數(shù)據(jù)類型的包裝類中,像裝著一個 int 數(shù)組的 Integer 類那樣。
清單 2-4 提供了一個如何創(chuàng)建、填充以及顯示數(shù)組信息的完整示例。方括號只在 main() 方法的聲明中使用。
清單 2-4. 使用反射創(chuàng)建、填充和顯示數(shù)組
import java.lang.reflect.Array;
import java.util.Random;
public class ArrayCreate {
public static void main (String args[]) {
Object array = Array.newInstance(int.class, 3);
printType(array);
fillArray(array);
displayArray(array);
}
private static void printType (Object object) {
Class type = object.getClass();
if (type.isArray()) {
Class elementType = type.getComponentType();
System.out.println("Array of: " + elementType);
System.out.println("Array size: " + Array.getLength(object));
}
}
private static void fillArray(Object array) {
int length = Array.getLength(array);
Random generator = new Random(System.currentTimeMillis());
for (int i=0; i<length; i++) {
int random = generator.nextInt();
Array.setInt(array, i, random);
}
}
private static void displayArray(Object array) {
int length = Array.getLength(array);
for (int i=0; i<length; i++) {
int value = Array.getInt(array, i);
System.out.println("Position: " + i +", value: " + value);
}
}
}
|
運行時,輸出將如下所示(盡管隨機數(shù)會不同):
Array of: int
Array size: 3
Position: 0, value: -54541791
Position: 1, value: -972349058
Position: 2, value: 1224789416
|
讓我們返回到早先的,創(chuàng)建一個將數(shù)組大小加倍的方法的示例。既然您知道如何獲取數(shù)組的類型,您可以創(chuàng)建一種方法用來將任意類型數(shù)組的大小加倍。這個方法確保我們能在獲取它的長度和類型之前得到數(shù)組。然后在復(fù)制原來的那組元素之前,它將新數(shù)組的大小加倍。
static Object doubleArray(Object original) {
Object returnValue = null;
Class type = original.getClass();
if (type.isArray()) {
int length = Array.getLength(original);
Class elementType = type.getComponentType();
returnValue = Array.newInstance(elementType, length*2);
System.arraycopy(original, 0, returnValue, 0, length);
}
return returnValue;
}
|
字符數(shù)組
在總結(jié)我們對 Java 數(shù)組的討論之前還要提到最后一件事:與 C 和 C++ 不同的是,字符數(shù)組在 Java 語言中不是字符串。雖然使用 String 構(gòu)造器(采用 char 類型的對象數(shù)組)和 String 的 toCharArray() 方法能很容易的在 String 和 char[] 之間轉(zhuǎn)換,但它們的確不同。
盡管字節(jié)數(shù)組是另一種情況,但它們也不是字符串,試圖在 byte[] 與 String 之間轉(zhuǎn)換要做一些工作,因為 Java 語言中的字符串是基于 Unicode 的,并且有 16 位的寬度。您需要告訴字符串構(gòu)造器編碼模式。表 2-3 演示了 1.3 平臺提供的基本編碼模式。如需其它擴展的編碼模式的清單,請參閱 http://java./j2se/1.3/docs/guide/intl/encoding.doc.html上的在線清單。它們因 JDK 版本的不同而不同。
表 2-3. 字節(jié)到字符的基本編碼模式
名稱 |
描述 |
ASCII |
美國信息交換標(biāo)準(zhǔn)碼 |
Cp1252 |
Windows Latin-1 |
ISO8859_1 |
ISO 8859-1,拉丁字母表一號 |
UnicodeBig |
16 位統(tǒng)一碼轉(zhuǎn)換格式,大尾數(shù)字節(jié)順序,帶字節(jié)順序記號 |
UnicodeBigUnmarked |
16 位統(tǒng)一碼轉(zhuǎn)換格式,大尾數(shù)字節(jié)順序 |
UnicodeLittle |
16 位統(tǒng)一碼轉(zhuǎn)換格式,小尾數(shù)字節(jié)順序,帶字節(jié)順序記號 |
UnicodeLittleUnmarked |
16 位統(tǒng)一碼轉(zhuǎn)換格式,小尾數(shù)字節(jié)順序 |
UTF16 |
16 位統(tǒng)一碼轉(zhuǎn)換格式,字節(jié)順序由強制的初始字節(jié)順序字節(jié)記號指定 |
UTF8 |
8 位統(tǒng)一碼轉(zhuǎn)換格式 |
如果您的確指定了編碼模式,您必須把對 String 構(gòu)造器的調(diào)用放置在 try-catch 代碼塊中,因為如果指定的編碼模式無效,一個 UnsupportedEncodingException 異常就會被拋出。
如果您只是用 ASCII 碼字符,您的確不用為此過多擔(dān)心。把 byte [] 傳遞到 String 構(gòu)造器而不使用任何編碼模式的參數(shù),這樣就可以使用平臺缺省的編碼模式,這已經(jīng)足夠了。當(dāng)然為保險起見,您可以總是只傳遞 ASCII 作為編碼模式。
注意:為檢查您平臺上的缺省值,請察看 _file.encoding_ 系統(tǒng)屬性。
總結(jié)
操作 Java 中的數(shù)組似乎非常容易,不過要充分利用它們的功能,還要知道很多事情。雖然基礎(chǔ)的數(shù)組聲明和用法非常簡單,但當(dāng)操作基本數(shù)據(jù)類型的數(shù)組、對象數(shù)組以及多維數(shù)組時還需要有不同的考慮。數(shù)組的初始化不需要很復(fù)雜,但是引入了匿名數(shù)組,事件就復(fù)雜了。
一旦有了數(shù)組,您需要多花點心思想想如何把它用到最好。當(dāng)傳遞數(shù)組到方法時您需要特別小心,因為它們是通過引用傳遞的。如果數(shù)組的初始大小已無法滿足您的需要,您需要構(gòu)造一個帶有額外空間的副本。數(shù)組克隆讓您順利的復(fù)制而無需擔(dān)心 final 關(guān)鍵字的特性。當(dāng)把數(shù)組賦給其它變量時,請當(dāng)心不要遇到 ArrayStoreException 異常,因為這個異常在運行時很難處理。數(shù)組的等同性檢查既包括對于相同內(nèi)存空間的檢查,又包括對已賦值的元素的等同性的檢查。通過神奇的 Java 反射,您可以處理正好是數(shù)組的對象。在這一章里您了解的最后一件事是如何在字節(jié)數(shù)組和字符串之間轉(zhuǎn)換,以及字節(jié)數(shù)組如何像在其它語言中一樣缺省情況下不是字符串。
參考資料
關(guān)于作者
|
|
|
John Zukowski 和 JZ Ventures從事戰(zhàn)略性的 Java 咨詢,提供指導(dǎo)、結(jié)構(gòu)框架建議、設(shè)計、調(diào)試以及代碼評審以及開發(fā)定制的解決方案。他曾獲得東北大學(xué)計算機系和數(shù)學(xué)系的學(xué)士學(xué)位,并獲得了 Johns Hopkins 大學(xué)計算機系的碩士學(xué)位。請通過 jaz@zukowski.net與他聯(lián)系。
|
|