内容


历史上的 Collection 类 ― 数组

Comments

数组是在 Java 编程语言中定义的唯一的 Collection 支持。它们是按照 索引可访问的顺序或位置次序来存储一组元素的对象。它们是 Object 类的子类,实现了 SerializableCloneable 两种接口。这里没有 .java 源文件让您了解内部是如何工作的。基本上,您要创建一个有特定大小和元素类型的数组,然后填充它。

注意:因为数组是 Object 的子类,所以您可以同步数组变量并调用它的 wait()notify() 方法。

让我们看一下使用数组对象能够做些什么 ― 从基本用法和声明开始,然后到复制和克隆。我们还将了解数组赋值、等同性检查以及反射。

数组基础知识

在讨论声明、创建、初始化以及复制数组的细节问题之前,让我们先复习一个简单的数组示例。当创建一个 Java 应用程序时, main() 方法有个唯一的字符串数组参数: public static void main(String args []) 。编译器并不在意您用什么参数名,只在意它是不是一个 String 对象的数组。

假设现在我们有个作为 String 对象数组的应用程序的命令行参数,我们可以观察每个元素并打印它。在 Java 中,数组知道它们的大小,而且它们总是从位置零开始建立索引。因此,我们可以通过观察数组专用的实例变量:length 来询问这个数组有多大。下面的代码展示了如何做到这一点:

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]);
    }
  }
}

注意:数组索引不能是 long 类型,因为只有非负整数才可以被用作索引,因此通过从 0 到 2 31-1 的索引范围有效地将数组元素的数量限制在 2,147,483,648 或 2 31个。

因为在遍历循环时数组大小不变,这就没有必要在每次测试条件中查看 length 了,例如: for (int i=0; i<args.length; i++) 。事实上,在大多数情况下,不用递增而用递减遍历循环,并用零作测试条件通常会更快: for (int i=args.length-1; i>=0; i -) 。虽然在 JDK 1.1 和 1.2 发行版中,递减计数与递增计数相比只有相对较小的性能差异,但这个时间差异在 1.3 发行版中却更为显著。为在您的平台上演示这种速度差异,请尝试运行清单 2-1 中的程序,测试循环“max int”(int 类型的最大值,即 2 的 31 次方减 1)次所需要的时间:

清单 2-1. 对循环性能计时
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));
  }
}

这个测试程序实际上是对 for 循环进行计时而不是对数组存取进行计时,因为这里没有数组存取。

注意:在大多数情况下,在我的 400 MHz Windows NT 操作系统环境的 JDK 1.1 和 1.2 下,计数值低于 11,000 秒内。但在 JDK1.3 下,并用 -classic 选项(没有 JIT),计数值增加到 250,000 左右。甚至用 HotSpot VM with 1.3,计数值也只介于 19,000 和 30,000 之间。

如果你试图访问在数组头部前面或者尾部以后的内容,一个 ArrayIndexOutOfBoundsException 异常将被抛出。作为 IndexOutOfBoundsException 类的子类, ArrayIndexOutOfBoundsException 是一个运行时异常,如图 2-1 所示。感到高兴的是:这意味着您不必将数组存取放入 try-catch 代码块了。此外,因为查看数组上下界之外的内容是一个运行时异常,所以您的程序会编译得很好。而程序将只在我们试图存取时抛出异常。

图 2-1. ArrayIndexOutOfBoundsException 的类层次结构
ArrayIndexOutOfBoundsException 的类层次结构

注意:您无法关闭数组的边界检查。这是 Java 运行时安全体系的一部分,它确保无效内存空间永远不被存取。

下面的代码演示了一种不正确的读取命令行数组元素的方法:

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 示例相同,但对控制流使用异常处理是一个不好的编程习惯。异常处理应该为异常情况保留。

声明和创建数组

请记住,数组是按照可通过索引可访问的顺序来存储一组元素的对象。这些元素可以是基本数据类型如 intfloat ,也可以是 Object 的任意类型。要声明一个特殊类型的数组,只要向声明添加方括号([])就可以了:

int[] variable;

对于数组声明来说,方括号可以处于三个位置中的其中一个:int[] variable、int []variable 以及 int variable[]。第一个位置说明变量是 int[] 类型的。后两个说明变量是一个数组,该数组是 int 类型的。

注意:听起来我们似乎是在讨论语义学。但是,在声明多个变量时,根据所使用的格式,会存在差异。格式 int[] var1, var2; 将会声明两个 int 类型的数组变量,而 int []var1,var2;int var1[], var2; 将声明一个 int 类型的数组和另一个只是 int 类型的变量。

一旦声明了一个数组,就可以创建这个数组并保存一个对它的引用。 new 操作符用来创建数组。在创建一个数组时,必须指定它的长度。长度一旦设定,就不能再更改。象下面的代码所演示的那样,长度可以被指定为常量或表达式。

int variable[] = new int [10];

或者

int[] createArray(int size) {
  return new int[size];
}

注意:如果您试图建立一个长度为负值的数组,将会抛出 NegativeArraySizeException 运行时异常。但零长度的数组是有效的。

您可以将数组的声明和创建合并为一个步骤:

int variable[] = new int[10];

警告:如果数组的创建导致了 OutOfMemoryError 错误的抛出,所有表示数组大小的表达式都已被求出了值。重要的是能否在指定数组大小的地方完成一次赋值。例如,如果表达式 int variable[] = new int[var1 = var2*var2] 会导致 OutOfMemoryError 抛出,那么在错误被抛出前要设置变量 var1

一旦数组被创建,您就可以填充该数组。这通常用一个 for 循环或者分别的赋值语句实现。例如,下面的代码将创建并填充一个三元素的 names 数组:

String names = new String[3];
names [0] = "Leonardo";
names [1] = "da";
names [2] = "Vinci";

基本数据类型数组 当您创建了一个基本数据类型元素的数组,数组为那些元素保留了实际值。例如,图 2-2 所示的是一个被变量 life 引用的包含六个整型元素的数组 (1452, 1472, 1483, 1495, 1503, 1519) 从栈和堆内存的角度看,会是什么样子。

图 2-2. 基本数据类型数组的栈和堆内存
基本数据类型数组的栈和堆内存

对象数组
与基本数据类型数组不同,当您创建了一个对象数组,它们不是存储在一个实际的数组里。数组只是存储了对实际对象的引用,除非被明确地初始化,否则最初每个引用都为空。(简言之,更多的是依靠初始化。)图 2-3 展示了 Object 元素组成的数组,其元素如下所示:

  • Leonardo da Vinci 出生的国家,意大利
  • 他的一张油画图像 The Baptism of Christ
  • 他关于直升机和降落伞的理论(画图)
  • 一张 Mona Lisa的图像
  • 他逝世的国家,法国

图 2-3 中要注意的关键是对象并不在数组中;在数组中的只是对对象的 引用

图 2-3. 对象数组的栈和堆内存
对象数组的栈和堆内存
对象数组的栈和堆内存

多维数组
因为数组通过引用处理,这就没有什么能阻止您让一个数组元素引用另一个数组。当一个数组引用另一个时,就得到一个 多维数组。向声明行每添加一个维数就需要一组额外的方括号。例如,如果您想定义一个矩形,即二维数组,您可以用以下代码行:

int coordinates[][];

对一维数组来说,如果数组类型是基本数据类型的其中之一,一旦创建了数组就可以立即往里面存储数值。对于多维数组,光声明是不够的。例如,下面两行代码会导致一个编译时错误,因为数组变量从未被初始化:

int coordinates[][];
coordinates[0][0] = 2;

但如果您在这两句源代码行之间创建一个数组(像 coordinates = new int [3][4]; ;),最后一行就会生效。

对于对象数组,创建一个多维数组会产生一个充满空对象引用的数组。您还是需要创建对象以存储到数组中。

因为多维数组最外层数组的每个元素都是对象引用,这就没有必要让数组成为一个矩形(或者一个为实现三维数组创建的立方体)。每个内部数组可以有自己的大小。例如,下面的代码演示了如何创建一个 float 类型的二维数组,其中内部数组元素排列像一组保龄球瓶 — 第一排一个元素,第二排两个,第三排三个,第四排四个:

float bowling[][] = new float[4][];
for (int i=0; i<4; i++) {
  bowling[i] = new float[i+1];
}

为帮助您形象地认识最终的数组,请参阅图 2-4。

图 2-4. 一个三角形的、像保龄球瓶的数组
一个三角形的、像保龄球瓶的数组
一个三角形的、像保龄球瓶的数组

当对一个多维数组进行存取时,在检查右侧的下一维表达式之前每一维表达式都已被完全求值。知道在存取数组时是否出现异常是很重要的。

注意:对于一维数组,虽然在语法上您可以将一个方括号放在数组变量的前面或后面( [index]name 或者 name[index] ),但对于多维数组,必须将方括号放在数组变量的后面,如 name[index1][index2] 。按照语法来看, [index1][index2]name[index1]name[index2] 如果在您的代码中出现,是不合法的,将会导致一个编译时错误。但对于声明来说,将方括号放在变量名前面( type [][]name )、后面( type name[][] )以及两边( type []name[] )是完全合法的。

请记住计算机内存是线性的 ― 当您对多维数组进行存取时,实际上是对内存中的一维数组进行存取。如果您可以按照内存存储的次序对它进行存取,就会最有效率。通常,如果所有的东西都在内存中安排好了,就不会有什么关系,因为计算机内存中跳跃存取也是很快的。但是,当使用大型的数据结构时,线性存取执行情况最好而且避免了不必要的交换。此外,您可以通过将它们包装成一维数组来模拟多维数组。对图像经常进行这样处理。有两种方法包装一维数组,一种是以行为主的次序,数组一次填充一行;还有一种是以列为主的次序,往数组里放的是列。图 2-5 显示了这两者的不同之处。

图 2-5. 以行为主与以列为主的次序的比较
以行为主与以列为主的次序的比较
以行为主与以列为主的次序的比较

注意:在许多图像处理例程中,像 ImageFilter 类里的 setPixels() 方法,您会发现二维的图像数组转换成了以行为主次序的一维数组,其中 Pixel(m, n) 转化成了一维的下标 n * scansize + m。按照自上而下、自左至右的顺序读取整个图像数据。

初始化数组当数组首次被创建时,运行时环境会确保数组内容被自动初始化为某些已知的(相对于未定义来说)值。对于未初始化的实例和类变量,数组内容被初始化为:等价于 0 的数字、等价于 \u0000 的字符、布尔值 false 或者用于对象数组的 null。如表 2-1 所示。

表 2-1. 数组初始值

缺省值数组
byte short int long
0.0float double
\u0000char
falseboolean
nullObject

当您声明一个数组时,您可以指定元素的初始值。这可通过在声明位置等号后面的花括号 {} 之间提供由逗号分隔的数据列来完成。

例如,下面的代码将会创建一个三元素的 names 数组:

String names[] = {"Leonardo", "da", "Vinci"};

注意:如果提供了数组的初始化程序,则不必指定长度。数组长度会根据逗号分隔的数据列中元素的数量被自动地设置。

注意:Java 语言的语法允许在数组初始化程序块中的最后一个元素后带一个结尾逗号,如 {"Leonardo", "da", "Vinci",}。这并没有将数组的长度变成 4,而仍旧保持为 3 。这种灵活性主要是出于对代码生成器的考虑。

对于多维数组,您只要为每个新添加的维数使用一组额外的圆括号就可以了。例如,下面的代码创建了一个 6 X 2 的关于年份和事件的数组。因为数组被声明为 Object 元素的数组,就必须使用 Integer 包装类将每个 int 基本数据类型的值存储在里面。数组中的所有元素都必须是数组声明的类型或这种类型的一个子类,在本例中就是 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")}
};

注意:如果数组的类型是接口,则数组中的所有元素都必须实现这个接口。

从 Java 的第二个 dot-point 发行版(Java 1.1)开始,引入了 匿名数组的概念。虽然当数组被声明以后可以很容易初始化,但以后不能再用逗号分隔的数据列对数组进行重新初始化,除非声明另一个变量来将新的数组存储进去。这就是我们引入匿名数组的原因。有了匿名数组,您可以将数组重新初始化为一组新值,或者在您不想定义本地变量来存储上述数组时将未命名的数组传递到方法中。

匿名数组的声明与常规数组类似。但是,您要将包含在花括号里的用逗号分隔的一列值放在方括号的后面,而不是在方括号里指定一个长度。如下所示:

new type[] {comma-delimited-list}

为了演示,以下几行展示了如何调用方法并将匿名的 String 对象数组传递给它:

method(new String[] {"Leonardo", "da", "Vinci"});

您会发现代码生成器频繁的使用匿名数组。

传递数组参数并返回值当数组作为参数被传递到方法,对数组的引用就被传递了。这允许您修改数组的内容,并在方法返回的时候让调用例程看到数组的变化。此外,因为引用被传递,您还可以返回在方法中创建的数组,而且不必担心在方法完成时垃圾收集器会释放数组内存。

复制和克隆数组

使用数组可以做很多事。如果数组的初始大小已无法满足您的需要,您就需要创建一个新的更大的数组,然后将原始元素复制到更大数组的相同位置。但是,如果您不需要将数组变大,而只希望在使原始数组保持原样的基础上修改数组元素,您必须创建数组的一个副本或克隆版本。

System 类中的 arraycopy() 方法允许您将元素从一个数组复制到另一个数组。当进行这种复制时,目标数组可以大些;但如果目标数组比源数组小,就会在运行时抛出一个 ArrayIndexOutOfBoundsException 异常。 arraycopy() 方法用到 5 个参数(两个用于数组,两个用作数组的起始位置,还有一个表示复制元素的数量): public static void arraycopy (Object sourceArray,int sourceOffset,Object destinationArray,int destinationOffset,int numberOfElementsToCopy) 。 除了类型的兼容性,这里唯一的必要条件是已经为目标数组分配了内存。

警告:当在不同的数组之间复制元素时,如果源参数或目标参数不是数组,或者它们的类型不兼容,就会抛出 ArrayStoreException 异常。不兼容的数组比如一个是基本数据类型数组而另一个是对象数组;或基本数据类型不同;或对象类型不可赋值。

为了演示,清单 2-2 采用一个整型数组并创建一个两倍大的新数组。下面示例中的 doubleArray() 方法为我们做到了这一点:

清单 2-2. 将数组大小加倍
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;
  }
}

在获得源数组的长度以后,先创建适当大小的新数组,再把原始元素复制到新数组中相同的位置。在您了解数组的反射以后,您可以总结出将任意类型数组的大小加倍的方法。

执行程序,生成如下输出:

Original size: 5
New size: 10
Original size: 9
New size: 18

注意:当用 arraycopy() 进行复制时,如果您要将数组的子集复制到该数组内的另一个区域,源数组和目标数组可以是同一个。即使有些地方发生重叠也会生效。

因为数组实现了 Cloneable 接口,除了复制数组的区域以外,还可以克隆它们。 克隆包括创建一个同样大小和类型的数组,并将所有的原始元素复制到新数组里。这和复制不同,复制要求您自己创建目标数组并限定其大小。对基本数据类型的元素来说,新数组包含原始元素的副本,所以一个数组中元素的变化不会反映到它的副本中。但对于对象引用的情况,复制的只是引用。因而,数组的两个副本将指向同一个对象。对该对象的更改将反映到两个数组中。这叫做 浅拷贝或者 浅克隆

为了演示,下面的方法采用了一个整型数组并返回上述数组的一个副本。

static int[] cloneArray(int original[]) {
  return (int[])original.clone();
}

数组克隆用一个实际起作用的 public 方法覆盖了被保护起来的 Object 方法,后者通常会抛出 CloneNotSupportedException 异常。

数组不变性

如果您不希望方法的调用程序修改底层数组结构,从方法返回数组克隆是非常有用的。您可以声明数组为 final ,像下面的示例那样:

final static int array[] = {1, 2, 3, 4, 5};

不过声明一个对象引用为 final (特别是这里的数组引用)并不限制您修改对象。它只限制您改变 final 变量引用的内容。下面的代码行导致了一个编译错误:

array = new int[] {6, 7, 8, 9};

不过改变个别的元素完全合法:

array[3] = 6;

提示:另一种从方法“返回”不变数组的方法是将 Enumeration 或者 Iterator 返回到数组,这比返回实际数组要好。任一接口都提供对单个元素的存取而不需要改变整个数组或要求制作整个数组的副本。您会在 Java Collections的后续章节学到更多的有关这些接口的知识。

数组赋值

数组赋值和变量赋值的操作一样。如果变量 x 是对 y 数组的引用,如果 z 类型的变量可以赋值给 y,那么 x 就可以成为对 z 的引用。例如,设想 y 是个 AWT Component 类,而 z 是个 AWT Button 类。因为 Button 变量可以被赋给 Component 变量,则 Button 数组可以被赋给 Component 数组:

Button buttons[] = {
  new Button("One"),
  new Button("Two"),
  new Button("Three")};
Component components[] = buttons;

当进行这样的赋值时,两个变量的 bottons 和 componets 引用的是内存的同一个堆空间。如图 2-6 所示。改变一个数组中的元素将会造成两个数组的元素都改变。

图 2-6. 数组赋值后的共享内存
数组赋值后的共享内存

如果,将一个数组变量赋值给一个超类数组变量之后(正如在前一个示例中将 botton 数组赋值给 component 数组变量一样),您接着试图将一个不同的子类实例放入数组,一个 ArrayStoreException 异常就会被抛出。继续前一个示例,如果您试图将一个 Canvas 对象放入 components 数组,一个 ArrayStoreException 异常就会被抛出。即使 components 数组被声明为 Component 对象数组也是如此,因为 components 数组是对 Button 对象数组的特别引用, Canvas 对象无法被存到数组中。这是个运行时异常,因为从类型安全编译器看,实际赋值是合法的。

检查数组等同性

检查两个数组之间的等同性可以用两种方式中的一种,这取决于您想要查找的等同性类型。是数组变量指向内存的同一位置因而指向同一数组?或是两数组中的元素相等?

检查对同一内存空间的两个引用可用由两个等号组成的运算符(==)来实现。例如,前面的 components 和 buttons 变量在这种情况下将是相等的,因为其中一个是另一个的引用:

components == buttons  //true

但是,如果您将数组和它的克隆版本比较,那么用 ==,它们是不会相等的。因为这两个数组虽然有相同的元素,但处于不同的内存空间,它们是不同的。为了让一个数组的克隆版本与原数组相等,您必须使用 java.util.Arrays 类中的 equals() 方法。

String[] clone = (String[]) strarray.clone();
boolean b1 = Arrays.equals(strarray, clone);   //Yes,they're equal

这就会检查每个元素的等同性。在参数是对象数组的情况下,每个对象的 equals() 方法会被用来检查等同性。 Arrays.equals() 方法也为非克隆的数组服务。

数组反射

如果因为某种原因,您并不确定参数或对象是不是数组,您可以检索对象的 Class 对象并询问它。 Class 类的 isArray() 方法将会告诉您。一旦您知道拥有了一个数组,您可以询问 ClassgetComponentType() 方法,您实际拥有的是什么类型的数组。如果 isArray() 方法返回 false,那么 getComponentType() 方法返回空。否则返回元素的 Class 类型。如果数组是多维的,您可以递归调用 isArray() 。它将仍只包含一个 component 类型。此外,您可以用在 java.lang.reflect 包里找到的 Array 类的 getLength() 方法获取数组的长度。

为了演示,清单 2-3 显示了传递给 main() 方法的参数是 java.lang.String 对象的数组,其中数组长度由命令行参数的个数确定:

清单 2-3. 使用反射检查数组类型和长度
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() 用于前面定义的 buttonscomponents 变量调用,每个都会表明数组是 java.awt.Button 类型。

如果不使用 isArray()getComponentType() 方法,而且试图打印数组的 Class 类型,您将获得一个包含 [ ,后面跟着一个字母和类名(如果是个基本数据类型就没有类名)的字符串。例如,如果您试图打印出上述 printType() 方法中的类型变量,您将获得 class [Ljava.lang.String; 作为输出。

除了询问一个对象是不是数组以及是什么类型的数组之外,您还可以在运行时用 java.lang.reflect.Array class 创建数组。这对于创建一般实用例程非常有用,这些例程执行数组任务,比如将大小加倍。(我们会立即回到那一点。)

要创建一个新数组,使用 ArraynewInstance() 方法,它有两种变化形式。对于一维数组您通常将使用较简单版本,它的执行方式如语句 new type [length] 所示,并作为对象返回数组: public static Object newInstance(Class type, int length) 。例如,下面的代码创建一个五个整数空间大小的数组:

int array[] = (int[])Array.newInstance(int.class, 5);

注意:要为基本数据类型指定 Class 对象,只要在基本数据类型名末尾添加 .class 就可以了。您还可以使用包装类中的 TYPE 变量,如 Integer.TYPE。

newInstance() 方法中的第二种变化形式要求维数被指定为整型数组: public static Object newInstance(Class type,int dimensions []) 。在创建一个一维数组的最简单的情况下,您将创建只有一个元素的数组。换句话说,如果您要创建包含五个整数的相同数组,您需要创建一个单个元素 5 的数组并传递到 newInstance() 方法,而不是传递整数值 5。

int dimensions[] = {5};
int array[] = (int[])Array.newInstance(int.class, dimensions);

在您只需要创建一个矩形数组的时候,您就可以将每个数组长度填充到这个 dimensions 数组中。例如,下面的代码与创建一个 3 X 4 的整数数组等价。

int dimensions[] = {3, 4};
int array[][] = (int[][])Array.newInstance(int.class, dimensions);

但是,如果您需要创建一个非矩形数组,您将需要多次调用 newInstance() 方法。第一次调用将定义外部数组的长度,并获得一个看上去很古怪的类参数([].class 适用于元素为 float 类型的数组)。每个后续调用将定义每个内部数组的长度。例如,下面演示了如何创建一个元素为 float 类型的数组,其内部数组的大小设置像一组保龄球瓶:第一排一个元素,第二排两个,第三排三个,第四排四个。为了帮您将这种情况形象化,让我们回顾早先在图 2-4 展示的三角形数组。

float bowling[][] = (float[][])Array.newInstance(float[].class, 4);
for (int i=0; i<4; i++) {
  bowling[i] = (float[])Array.newInstance(float.class, i+1);
}

一旦在运行时创建了数组,您还可以获取和设置数组元素。不过通常不会这样做,除非键盘上的方括号键失灵或者您在动态的编程环境(程序被创建时数组名未知)中工作。 如表 2-2 所示, Array 类有一系列的 getter 和 setter 方法用来获取和设置数组元素。使用什么方法取决于您处理的数组类型。

表 2-2. 数组 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() 方法。如果数组是一个基本数据类型数组, get() 方法的返回值或 set() 方法的值参数将被包装到用于基本数据类型的包装类中,像装着一个 int 数组的 Integer 类那样。

清单 2-4 提供了一个如何创建、填充以及显示数组信息的完整示例。方括号只在 main() 方法的声明中使用。

清单 2-4. 使用反射创建、填充和显示数组
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);
    }
  }
}

运行时,输出将如下所示(尽管随机数会不同):

Array of: int
Array size: 3
Position: 0, value: -54541791
Position: 1, value: -972349058
Position: 2, value: 1224789416

让我们返回到早先的,创建一个将数组大小加倍的方法的示例。既然您知道如何获取数组的类型,您可以创建一种方法用来将任意类型数组的大小加倍。这个方法确保我们能在获取它的长度和类型之前得到数组。然后在复制原来的那组元素之前,它将新数组的大小加倍。

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;
}

字符数组

在总结我们对 Java 数组的讨论之前还要提到最后一件事:与 C 和 C++ 不同的是,字符数组在 Java 语言中不是字符串。虽然使用 String 构造器(采用 char 类型的对象数组)和 StringtoCharArray() 方法能很容易的在 Stringchar[] 之间转换,但它们的确不同。

尽管字节数组是另一种情况,但它们也不是字符串,试图在 byte[]String 之间转换要做一些工作,因为 Java 语言中的字符串是基于 Unicode 的,并且有 16 位的宽度。您需要告诉字符串构造器编码模式。表 2-3 演示了 1.3 平台提供的基本编码模式。如需其它扩展的编码模式的清单,请参阅 http://java.sun.com/j2se/1.3/docs/guide/intl/encoding.doc.html上的在线清单。它们因 JDK 版本的不同而不同。

表 2-3. 字节到字符的基本编码模式
名称描述
ASCII美国信息交换标准码
Cp1252Windows Latin-1
ISO8859_1ISO 8859-1,拉丁字母表一号
UnicodeBig16 位统一码转换格式,大尾数字节顺序,带字节顺序记号
UnicodeBigUnmarked16 位统一码转换格式,大尾数字节顺序
UnicodeLittle16 位统一码转换格式,小尾数字节顺序,带字节顺序记号
UnicodeLittleUnmarked16 位统一码转换格式,小尾数字节顺序
UTF1616 位统一码转换格式,字节顺序由强制的初始字节顺序字节记号指定
UTF88 位统一码转换格式

如果您的确指定了编码模式,您必须把对 String 构造器的调用放置在 try-catch 代码块中,因为如果指定的编码模式无效,一个 UnsupportedEncodingException 异常就会被抛出。

如果您只是用 ASCII 码字符,您的确不用为此过多担心。把 byte [] 传递到 String 构造器而不使用任何编码模式的参数,这样就可以使用平台缺省的编码模式,这已经足够了。当然为保险起见,您可以总是只传递 ASCII 作为编码模式。

注意:为检查您平台上的缺省值,请察看 _file.encoding_ 系统属性。

总结

操作 Java 中的数组似乎非常容易,不过要充分利用它们的功能,还要知道很多事情。虽然基础的数组声明和用法非常简单,但当操作基本数据类型的数组、对象数组以及多维数组时还需要有不同的考虑。数组的初始化不需要很复杂,但是引入了匿名数组,事件就复杂了。

一旦有了数组,您需要多花点心思想想如何把它用到最好。当传递数组到方法时您需要特别小心,因为它们是通过引用传递的。如果数组的初始大小已无法满足您的需要,您需要构造一个带有额外空间的副本。数组克隆让您顺利的复制而无需担心 final 关键字的特性。当把数组赋给其它变量时,请当心不要遇到 ArrayStoreException 异常,因为这个异常在运行时很难处理。数组的等同性检查既包括对于相同内存空间的检查,又包括对已赋值的元素的等同性的检查。通过神奇的 Java 反射,您可以处理正好是数组的对象。在这一章里您了解的最后一件事是如何在字节数组和字符串之间转换,以及字节数组如何像在其它语言中一样缺省情况下不是字符串。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=52964
ArticleTitle=历史上的 Collection 类 ― 数组
publish-date=05152001