1. 程式人生 > >讀thinking in java筆記(十五):陣列

讀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。