Day16-51.Array initialization

数组初始化


数组只是相同类型的,用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。

数组是通过方括号下表操作符[]来定义和使用的。

要定义一个数组,只需在类型名后加上一对方括号即可:

int[] a1;

方括号也可以置于标识符后面:

int a1[];

两种格式的含义是一样的,后一种格式符合C和C++程序员 的习惯。

不过,前一种格式或许更合理,毕竟它表明类型是“一个int型数组”。本书将采用这种形式。

编译器不允许制定数组的大小。

这就又把我们带回到有关“引用”的问题上。

现在拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),而且也没给数组对象本身分配任何空间。

为了给数组创建相应的存储空间,必须写初始化表达式。

对于数组,初始化动作可以出现在代码的任何地方,但也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。

这种特殊的初始化是由一对花括号括起来的值组成的。

在这种情况下,存储空间的分配(等价于使用new)将由编译器负责。例如:

int [] a1={1,2,3,4,5};

那么为甚还要在没有数组的时候定义一个数组引用呢?

int [] a2;

在Java中可以将一个数组赋值给另一个数组,所以可以这样:

a2=a1;

其实真正做的只是复制了一个引用,就像下面演示的那样:

//: initialization/ArraysOfPrimitives.java
import static net.mindview.util.Print.*;

public class ArraysOfPrimitives {
  public static void main(String[] args) {
    int[] a1 = { 1, 2, 3, 4, 5 };
    int[] a2;
    a2 = a1;
    for(int i = 0; i < a2.length; i++)
      a2[i] = a2[i] + 1;
    for(int i = 0; i < a1.length; i++)
      print("a1[" + i + "] = " + a1[i]);
  }
} /* Output:
a1[0] = 2
a1[1] = 3
a1[2] = 4
a1[3] = 5
a1[4] = 6
*///:~



可以看到代码中给出了a1的初始值,但a2却没有;在本例中,a2是在后面被赋给另一个数组的。

由于a2和a1是相同数组的别名,因此通过a2所做的修改在a1中可以看到。

所有数组(无论它们的元素是对象还是基本类型)都有一个固定成员,可以通过它获知数组内包含了多少个元素,但不能对其修改。

这个成员就是length。

与C和C++类似,Java数组技术也是从第0个元素开始,所以能使用的最大下标数是length-1.

要是超出这个边界,C和C++会默默地接受,并允许你访问所有内存,许多声名狼藉的程序错误由此而生。

Java则能保护你免受这一问题的困扰,一旦访问下标过界,就会出现运行时错误(即异常)。

如果在编写程序时,并不能确定在数组里需要多少个元素,那么该怎么办呢?

可以直接用new在数组里创建元素。

尽管创建的是基本类型数组,new仍然可以工作(不能用new创建单个的基本类型数据)。

//: initialization/ArrayNew.java
// Creating arrays with new.
import java.util.*;
import static net.mindview.util.Print.*;

public class ArrayNew {
  public static void main(String[] args) {
    int[] a;
    Random rand = new Random(47);
    a = new int[rand.nextInt(20)];
    print("length of a = " + a.length);
    print(Arrays.toString(a));
  }
} /* Output:
length of a = 18
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
*///:~


数组的大小是通过Random.nextInt()方法随机决定的,这个方法会返回0到输入参数之间的一个值。

这表明数组的创建确实是在运行时刻进行的。

此外程序输出表明:数组元素中的基本数据类型值会自动初始化成空值(对于数字和字符,就是0;对于布尔型,是false)。

Array,toString()方法属于java.util标准类库,它将产生一维数组的可打印版本。

当然,在本例中,数组也可以在定义的同时进行初始化:

int [] a =new int [rand.nextInt(20)];

如果可能的话,应该尽量这么做、

如果你创建了一个非基本类型的数组,那么你就创建了一个引用数组。

以整型的包装器类Integer为例,它是一个类而不是基本类型。


//: initialization/ArrayClassObj.java
// Creating an array of nonprimitive objects.
import java.util.*;
import static net.mindview.util.Print.*;

public class ArrayClassObj {
  public static void main(String[] args) {
    Random rand = new Random(47);
    Integer[] a = new Integer[rand.nextInt(20)];
    print("length of a = " + a.length);
    for(int i = 0; i < a.length; i++)
      a[i] = rand.nextInt(500); // Autoboxing
    print(Arrays.toString(a));
  }
} /* Output: (Sample)
length of a = 18
[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]
*///:~


这里,几遍使用new创建数组之后:

    Integer[] a = new Integer[rand.nextInt(20)];

它还只是一个引用数组,并且直到通过创建新的Integer对象(在本例中是通过自动包装机制创建的),并把对象赋值给引用,初始化进程才算结束:

a[i]=rand.nextInt(500);

如果忘记了创建对象,并且试图使用数组中的空引用,就会在运行时产生异常。

也可以用花括号括起来的列表来初始化对象数组。有两种形式:

//: initialization/ArrayInit.java
// Array initialization.
import java.util.*;

public class ArrayInit {
  public static void main(String[] args) {
    Integer[] a = {
      new Integer(1),
      new Integer(2),
      3, // Autoboxing
    };
    Integer[] b = new Integer[]{
      new Integer(1),
      new Integer(2),
      3, // Autoboxing
    };
    System.out.println(Arrays.toString(a));
    System.out.println(Arrays.toString(b));
  }
} /* Output:
[1, 2, 3]
[1, 2, 3]
*///:~


在这两种形式中,初始化列表的最后一个逗号都是可选的(这一特性使维护长列表变得更容易)。

尽管第一种形式很有用,但是它也更加受限,因为它只能用于数组被定义之处。

你可以在任何地方使用第二种和第三种形式,甚至是在方法调用的内部。

例如,你可以创建一个String对象数组,将其传递给另一个main()方法,以提供参数,用来替换传递给该main()方法的命令行参数。

//: initialization/DynamicArray.java
// Array initialization.

public class DynamicArray {
  public static void main(String[] args) {
    Other.main(new String[]{ "fiddle", "de", "dum" });
  }
}

class Other {
  public static void main(String[] args) {
    for(String s : args)
      System.out.print(s + " ");
  }
} /* Output:
fiddle de dum
*///:~



为 Other.main()的参数而创建的数组是在方法调用处创建的,因此你甚至可以在调用时提供可替换的参数。


Variable argument lists 

可变参数列表


第二种形式提供了一种方便的语法来创建对象并调用方法,以获得与C的可变参数列表variable argument lists (C通常把它简称为varargs)一样的效果。

这可以应用于参数个数或类型未知的场合。

由于所有的类都直接或间接集成与Object类,所以可以创建以Object数组为参数的方法,并像下面这样调用:

//: initialization/VarArgs.java
// Using array syntax to create variable argument lists.

class A {}

public class VarArgs {
  static void printArray(Object[] args) {
    for(Object obj : args)
      System.out.print(obj + " ");
    System.out.println();
  }
  public static void main(String[] args) {
    printArray(new Object[]{
      new Integer(47), new Float(3.14), new Double(11.11)
    });
    printArray(new Object[]{"one", "two", "three" });
    printArray(new Object[]{new A(), new A(), new A()});
  }
} /* Output: (Sample)
47 3.14 11.11
one two three
A@1a46e30 A@3e25a5 A@19821f
*///:~



可以看到print()方法使用Object数组作为参数,然后使用foreach语法遍历数组,打印每个对象。

标准Java库中的类能输出有意义的内容,但这里建立的类的对象,打印出的内容只是类的名称以及后面紧跟着的一个@符号以及多个十六进制数字。

于是,默认行为就是打印类的名字和对象的地址。

你可能看到过像上面这样编写的Java SE5之前的代码,它们可以产生可变的参数列表。

然而,在Java SE5 中,这种盼望已久的特性终于添加了进来,因此你现在可以使用省略号(ellipsis)来定义可变参数列表了,就像在printArray()中看到的那样:

//: initialization/NewVarArgs.java
// Using array syntax to create variable argument lists.

public class NewVarArgs {
  static void printArray(Object... args) {
    for(Object obj : args)
      System.out.print(obj + " ");
    System.out.println();
  }
  public static void main(String[] args) {
    // Can take individual elements:
    printArray(new Integer(47), new Float(3.14),
      new Double(11.11));
    printArray(47, 3.14F, 11.11);
    printArray("one", "two", "three");
    printArray(new A(), new A(), new A());
    // Or an array:
    printArray((Object[])new Integer[]{ 1, 2, 3, 4 });
    printArray(); // Empty list is OK
  }
} /* Output: (75% match)
47 3.14 11.11
47 3.14 11.11
one two three
A@1bab50a A@c3c749 A@150bd4d
1 2 3 4
*///:~


有了可变参数,就再也不用显式地编写数组语法了,当你制定参数时,编译器实际上会为你去填充数组。

你获取的仍旧是一个数组,这就是为什么print()可以使用foreach来迭代该数组的原因。

但是,这不仅仅只是从元素列表到数组的自动转换。

请注意程序中倒数第二行,一个Integer数组(通过使用自动包装而创建的)被转型为一个Object数组(以便移除编译器警告信息),并且传递给了printArray().

很明显,编译器会发现它已经是一个数组了,所以不会再其上执行任何转换。

因此,如果你有一组item,可以把他们当做list传递,而如果你已经有了一个数组,该方法可以把它们当作可变参数列表来接受。

该程序的最后一行表明将0个参数传递给可变参数列表是可行的,当具有可选的尾随参数(optional trailing arguments)时,这一特性就会很有用:

//: initialization/OptionalTrailingArguments.java

public class OptionalTrailingArguments {
  static void f(int required, String... trailing) {
    System.out.print("required: " + required + " ");
    for(String s : trailing)
      System.out.print(s + " ");
     System.out.println();
  }
  public static void main(String[] args) {
    f(1, "one");
    f(2, "two", "three");
    f(0);
  }
} /* Output:
required: 1 one
required: 2 two three
required: 0
*///:~


这个程序还展示了你可以如何使用具有Object之外类型的可变参数列表。

这里所有的可变参数都必须是String对象。

在可变参数列表中可以使用任何类型的参数,包括基本类型。

下面的例子也展示了可变参数列表变为数组的情形,并且如果在该列表中没有任何元素那么转变成长度为0的:

//: initialization/VarargType.java

public class VarargType {
  static void f(Character... args) {
    System.out.print(args.getClass());
    System.out.println(" length " + args.length);
  }
  static void g(int... args) {
    System.out.print(args.getClass());
    System.out.println(" length " + args.length);
  }
  public static void main(String[] args) {
    f('a');
    f();
    g(1);
    g();
    System.out.println("int[]: " + new int[0].getClass());
  }
} /* Output:
class [Ljava.lang.Character; length 1
class [Ljava.lang.Character; length 0
class [I length 1
class [I length 0
int[]: class [I
*///:~


getClass()方法属于Object的一部分,我们将在第十四章中做全面介绍。

它将产生对象的类,并且在打印该类时,可以看到表示该类类型的编码字符串。

前导的“[”表示这是一个后面紧随的类型的数组,而紧随的“I”表示基本类型int。

为了进行双重检查,我在最后一行创建了一个int数组,并打印了其类型这样也就验证了使用可变参数列表不依赖于自动包装机制,而实际上使用的是基本类型。

然而,可变参数列表与自动包装机制可以和谐共处,例如:

//: initialization/AutoboxingVarargs.java

public class AutoboxingVarargs {
  public static void f(Integer... args) {
    for(Integer i : args)
      System.out.print(i + " ");
     System.out.println();
  }
  public static void main(String[] args) {
    f(new Integer(1), new Integer(2));
    f(4, 5, 6, 7, 8, 9);
    f(10, new Integer(11), 12);
  }
} /* Output:
1 2
4 5 6 7 8 9
10 11 12
*///:~


请注意,你可以在单一的参数列表中将类型混合在一起,而自动包装机制将有选择的将int参数提升为Integer。

可变参数列表使得重载过程变得复杂了,尽管乍一看会显得足够安全:

//: initialization/OverloadingVarargs.java

public class OverloadingVarargs {
  static void f(Character... args) {
    System.out.print("first");
    for(Character c : args)
      System.out.print(" " + c);
    System.out.println();
  }
  static void f(Integer... args) {
    System.out.print("second");
    for(Integer i : args)
      System.out.print(" " + i);
    System.out.println();
  }
  static void f(Long... args) {
    System.out.println("third");
  }
  public static void main(String[] args) {
    f('a', 'b', 'c');
    f(1);
    f(2, 1);
    f(0);
    f(0L);
    //! f(); // Won't compile -- ambiguous
  }
} /* Output:
first a b c
second 1
second 2 1
second 0
third
*///:~


在每一种情况中,编译器都会使用自动包装机制来匹配重载的方法,然后调用最明确匹配的方法。

但是在不适用参数调用f()时,编译器就无法知道该调用哪一个方法了。

尽管这个错误可以弄清楚,但是它可能会使客户端程序员大感意外。

你可能会通过在某个方法中增加一个非可变参数来解决该问题:

//: initialization/OverloadingVarargs2.java
// {CompileTimeError} (Won't compile)

public class OverloadingVarargs2 {
  static void f(float i, Character... args) {
    System.out.println("first");
  }
  static void f(Character... args) {
    System.out.print("second");
  }
  public static void main(String[] args) {
    f(1, 'a');
    f('a', 'b');
  }
} ///:~


{CompileTimeError}注释标签把该文件排除在了本书的Ant构建之外。

如果你手动编译它,就会得到下面的错误消息:

reference to  f is ambiguous,both method f(float,java,lang.Character…)

in OverloadingVarargs2 and method f(java.lang.Character…) in

OverloadingVarargs2 match


如果你给这两个方法都添加一个非可变参数,就可以解决问题了:

//: initialization/OverloadingVarargs3.java

public class OverloadingVarargs3 {
  static void f(float i, Character... args) {
    System.out.println("first");
  }
  static void f(char c, Character... args) {
    System.out.println("second");
  }
  public static void main(String[] args) {
    f(1, 'a');
    f('a', 'b');
  }
} /* Output:
first
second
*///:~


你应该总是只在重载方法的一个版本上使用可变参数列表,或者压根就不是用它。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值