读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 sphereList =
      new ArrayList();
    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 intList = new ArrayList(
      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。

你可能感兴趣的:(读thinging,in,java)