第08部分:数组

数组是一种特殊的对象,保存零个或多个基本类型或引用类型的值。这些值是数组的元素,是通过所在位置或索引引用的无名变量。数组的类型通过元素的类型表示,数组中的所有元素必须都属于这个类型。数组元素的编号从零开始,有效的索引范围是零到元素数量减一。例如,索引为 1 的元素,是数组中的第二个元素。数组中的元素数量是数组的长度。数组的长度在创建时指定,从此就不能改变。

数组中元素的类型可以是任何有效的 Java 类型,包括数组类型。也就是说,Java 支持由数组组成的数组,实现多维数组。Java 不支持其他语言中的矩阵式多维数组。

数组的类型

数组的类型和类一样,也是引用类型。数组的实例和类的实例一样,也是对象。和类不同的是,数组的类型不用定义,只需在元素类型后面加上一对中括号即可。例如,下述代码声明了三种不同类型的数组:

byte b; // byte是基本类型

byte[] arrayOfBytes; // byte[]是由byte类型的值组成的数组

byte[][] arrayOfArrayOfBytes; // byte[][]是由byte[]类型的值组成的数组

String[] points; // String[]是由字符串组成的数组

数组的长度不是数组类型的一部分。例如,声明一个方法,并且期望传入恰好由四个 int类型的值组成的数组,是不可能的。如果方法的参数类型是 int[],调用时传入的数组可以包含任意个元素(包括零个)。


数组类型不是类,但数组实例是对象。这意味着,数组从 java.lang.Object 类继承了方法。数组实现了 Cloneable 接口,而且覆盖了 clone() 方法,确保数组始终能被复制,而且 clone() 方法从不抛出 CloneNotSupportedException 异常。数组还实现了 Serializable 接口,所以只要数组中元素的类型能被序列化,数组就能被序列化。而且,所有数组都有一个名为 length 的字段,这个字段的修饰符是 public final int,表示数组中元素的数量。


因为数组扩展自 Object 类,而且实现了 Cloneable 和 Serializable 接口,所以任何数组类型都能放大转换成这三种类型中的任何一种。而且,特定的数组类型还能放大转换成其他数组类型。如果数组中的元素类型是引用类型 T,而且 T 能指定给类型 S,那么数组类型T[] 就能指定给数组类型 S[]。注意,基本类型的数组不能放大转换。例如,下述代码展示了合法的数组放大转换:

String[] arrayOfStrings; // 创建字符串数组

int[][] arrayOfArraysOfInt; // 创建int二维数组

// String可以指定给Object,

// 因此String[]可以指定给Object[]

Object[] oa = arrayOfStrings;

// String实现了Comparable接口

// 因此String[]可以视作Comparable[]

Comparable[] ca = arrayOfStrings;

// int[]是Object类的对象,因此int[][]可以指定给Object[]

Object[] oa2 = arrayOfArraysOfInt;

//所有数组都是可以复制和序列化的对象

Object o = arrayOfStrings;

Cloneable c = arrayOfArraysOfInt;

Serializable s = arrayOfArraysOfInt[0];

因为数组类型可以放大转换成另一种数组类型,所以编译时和运行时数组的类型并不总是一样。这种放大转换叫作“数组协变”(array covariance)。

现代标准认为这是历史遗留的不合理功能,因为编译时和运行时得出的类型不一致。


把引用类型的值存储在数组元素中之前,编译器通常必须插入运行时检查,确保运行时这个值的类型和数组元素的类型匹配。如果运行时检查失败,会抛出 ArrayStoreException异常。

与C语言兼容的句法

如前所示,指定数组类型的方法是在元素类型后加上一对中括号。为了兼容 C 和 C++,Java 还支持一种声明变量的句法:中括号放在变量名后面,元素类型后面可以放也可以不放中括号。这种句法可用于局部变量,字段和方法的参数。例如:

// 这行代码声明类型为int,int[]和int[][]的局部变量

int justOne, arrayOfThem[], arrayOfArrays[][];

// 这三行代码声明的字段属于同一种数组类型

public String[][] aas1;   // 推荐使用的Java句法

public String aas2[][];   // C语言的句法

public String[] aas3[];   // 令人困惑的混用句法

// 这个方法签名包含两个类型相同的参数

public static double dotProduct(double[] x, double y[]) { ... }

注意:这种兼容句法极其少见,不要使用。


创建和初始化数组

在 Java 中,使用 new 关键字创建数组,就像创建对象一样。数组类型没有构造方法,但创建数组时要指定长度,在中括号里使用非负整数指定所需的数组大小:

// 创建一个能保存1024个byte类型数据的新数组

byte[] buffer = new byte[1024];

//创建一个能保存50个字符串引用的数组

String[] lines = new String[50];

使用这种句法创建的数组,每个元素都会自动初始化,初始值和类中的字段默认值相同:boolean 类型元素的初始值是 false,char 类型元素的初始值是 \u0000,整数元素的初始值是 0,浮点数元素的初始值是 0.0,引用类型元素的初始值是 null。

创建数组的表达式也能用来创建和初始化多维数组。这种句法稍微复杂一些。


数组初始化程序

若想在一个表达式中创建数组并初始化其中的元素,不要指定数组的长度,在方括号后面跟着一对花括号,在花括号里写入一些逗号分隔的表达式。当然了,每个表达式的返回值类型必须能指定给数组元素的类型。创建的数组长度和表达式的数量相等。这组表达式的最后一个后面可以加上逗号,但没必要这么做。例如:

String[] greetings = new String[] { "Hello", "Hi", "Howdy" };

int[] smallPrimes = new int[] { 2, 3, 5, 7, 11, 13, 17, 19, };

注意,这种句法无需把数组赋值给变量就能创建、初始化和使用数组。某种意义上,这种创建数组的表达式相当于匿名数组字面量。下面是几个示例:

// 调用一个方法,传入一个包含两个字符串的匿名数组字面量

String response = askQuestion("Do you want to quit?",

                                                   new String[] {"Yes", "No"});

// 调用另一个方法,传入匿名对象组成的匿名数组

double d = computeAreaOfTriangle(new Point[] { new Point(1,2),

                                                                              new Point(3,4),

                                                                              new Point(3,2) });

如果数组初始化程序是变量声明的一部分,可以省略 new 关键字和元素类型,在花括号里列出所需的元素:

String[] greetings = { "Hello", "Hi", "Howdy" };

int[] powersOfTwo = {1, 2, 4, 8, 16, 32, 64, 128};

数组字面量在程序运行时,而不是程序编译时,创建和初始化。例如下述数组字面量:

int[] perfectNumbers = {6, 28};

编译得到的 Java 字节码和下面的代码相同:

int[] perfectNumbers = new int[2];

perfectNumbers[0] = 6;

perfectNumbers[1] = 28;


Java 在运行时初始化数组有个重要的推论:数组初始化程序中的表达式可能会在运行时计算,而且不一定非要使用编译时常量。例如:

Point[] points = { circle1.getCenterPoint(), circle2.getCenterPoint() };






使用数组

创建数组后就可以开始使用了。下面说明访问元素的基本方法,以及常见的数组用法,例如迭代数组中的元素,复制数组或数组的一部分。

访问数组中的元素

数组中的元素是变量。如果元素出现在表达式中,其计算结果是这个元素中保存的值。如果元素出现在赋值运算符的左边,会把一个新值保存到这个元素中。不过,元素和普通的变量不同,它没有名字,只有编号。数组中的元素使用方括号访问。假如 a 是一个表达式,其计算结果为一个数组引用,那么可以使用 a[i] 索引数组,并引用某个元素。其中,i 是整数字面量或计算结果为 int 类型值的表达式。例如:

// 创建一个由两个字符串组成的数组

String[] responses = new String[2];

responses[0] = "Yes"; // 设定数组的第一个元素

responses[1] = "No"; // 设定数组的第二个元素

// 读取这个数组中的元素

System.out.println(question + " (" + responses[0] + "/" +

                        responses[1] + " ): ");

// 数组引用和数组索引都可以是复杂的表达式

double datum = data.getMatrix()[data.row() * data.numColumns() +

                        data.column()];

数组的索引表达式必须是 int 类型,或能放大转换成 int 的类型:byte、short,甚至是char。数组的索引显然不能是 boolean、float 或 double 类型。还记得吗,数组的 length字段是 int 类型,所以数组中的元素数量不能超过 Integer.MAX_VALUE。如果使用 long 类型的表达式索引数组,即便运行时表达式的返回值在 int 类型的取值范围内,也会导致编译出错。


数组的边界

数组 a 的第一个元素是 a[0],第二个元素是 a[1],最后一个元素是 a[a.length-1]。

使用数组时常见的错误是索引太小(负数)或太大(大于或等于数组的长度)。在 C 或C++ 等语言中,如果访问起始索引之前或结尾索引之后的元素,会导致无法预料的行为,而且在不同的调用和不同的平台中有所不同。这种问题不一定会被捕获,如果没捕获,可能过一段时间才会发现。因为在 Java 中容易编写错误的索引代码,所以运行时每次访问数组都会做检查,确保得到能预料的结果。如果数组的索引太小或太大,Java 会立即抛出ArrayIndexOutOfBoundsException 异常。


迭代数组

为了在数组上执行某种操作,经常要编写循环,迭代数组中的每个元素。这种操作通常使用 for 循环完成。例如,下述代码计算整数数组中的元素之和:

int[] primes = { 2, 3, 5, 7, 11, 13, 17, 19, 23 };

int sumOfPrimes = 0;

for(int i = 0; i < primes.length; i++)

                sumOfPrimes += primes[i];

这种 for 循环结构很有特色,会经常见到。Java 还支持遍历句法,前面已经介绍过。上述求和代码可以改写成下述简洁的代码:

for(int p : primes) 

                sumOfPrimes += p;


复制数组

所有数组类型都实现了 Cloneable 接口,任何数组都能调用 clone() 方法复制自己。注意,返回值必须校正成适当的数组类型。不过,在数组上调用 clone() 方法不会抛出CloneNotSupportedException 异常:

int[] data = { 1, 2, 3 };

int[] copy = (int[]) data.clone();

clone() 方法执行的是浅复制。如果数组的元素是引用类型,那么只复制引用,而不复制引用的对象。因为这种复制是浅复制,所以任何数组都能被复制,就算元素类型没有实现Cloneable 接口也行。

不过,有时只想把一个现有数组中的元素复制到另一个现有数组中。System.arraycopy()方法的目的就是高效完成这种操作。你可以假定 Java 虚拟机实现会在底层硬件中使用高速块复制操作执行这个方法。

arraycopy() 方法的作用简单明了,但使用起来有些难度,因为要记住五个参数。第一个参数是想从中复制元素的源数组;第二个参数是源数组中起始元素的索引;第三个参数是目标数组;第四个参数是目标索引;第五个参数是要复制的元素数量。


就算重叠复制同一个数组,arraycopy() 方法也能正确运行。例如,把数组 a 中索引为 0 的元素删除后,想把索引为 1 到 n 的元素向左移,把索引变成 0 到 n-1,可以这么做:

System.arraycopy(a, 1, a, 0, n);


数组的实用方法

java.util.Arrays 类中包含很多处理数组的静态实用方法。这些方法中大多数都高度重载,有针对各种基本类型数组的版本,也有针对对象数组的版本。排序和搜索数组时,sort()和binarySearch() 方法特别有用。equals() 方法用于比较两个数组的内容。如果想把数组的内容转换成一个字符串,例如用于调试或记录日志,Arrays.toString() 方法很有用。

Arrays 类中还包含能正确处理多维数组的方法,例如 deepEquals()、deepHashCode() 和deepToString()。





多维数组

前面已经见过,数组类型的写法是在元素类型后面加一对方括号。char 类型元素组成的数组是 char[] 类型,由 char[] 类型元素组成的数组是 char[][] 类型。如果数组的元素也是数组,我们说这个数组是多维数组。要想使用多维数组,需要了解一些其他细节。

假如想使用多维数组表示乘法表:

int[][] products; // 乘法表

每对方括号表示一个维度,所以这是个二维数组。若想访问这个二维数组中的某个 int元素,必须指定两个索引值,一个维度一个。假设这个数组确实被初始化成一个乘法表,那么元素中存储的 int 值就是两个索引的乘积。也就是说,products[2][4] 的值是 8,products[3][7] 的值是 21。

创建多维数组要使用 new 关键字,而且要指定每个维度中数组的大小。例如:

int[][] products = new int[10][10];

在某些语言中,会把这样的数组创建成包含 100 个 int 值的数组,但 Java 不会这样处理。这行代码会做三件事。

1    声明一个名为 products 的变量,保存一个由 int[] 类型数组组成的数组。

2    创建一个有 10 个元素的数组,保存 10 个 int[] 类型的数组。

3    再创建 10 个数组,每个都由 10 个 int 类型的元素组成。然后把这 10 个新数组指定为

前一步创建的数组的元素。这 10 个新数组中的每一个 int 类型元素的默认值都是 0。换种方式说,前面的单行代码等效于下述代码:

int[][] products = new int[10][]; // 保存10个int[]类型值的数组

for(int i = 0; i < 10; i++) // 循环10次......

                products[i] = new int[10]; // ......创建10个数组

new 关键字会自动执行这些额外的初始化操作。超过两个维度的数组也是一样:

float[][][] globalTemperatureData = new float[360][180][100];

使用 new 关键字创建多维数组时,无需指定所有维度的大小,只要为最左边的几个维度指定大小就行。例如,下面两行代码都是合法的:

float[][][] globalTemperatureData = new float[360][][];

float[][][] globalTemperatureData = new float[360][180][];

第一行代码创建一个一维数组,元素是 float[][] 类型。第二行代码创建一个二维数组,元素是 float[] 类型。不过,如果只为数组的部分维度指定大小,这些维度必须位于最左边。下述代码是不合法的:

float[][][] globalTemperatureData = new float[360][][100]; // 错误!

float[][][] globalTemperatureData = new float[][180][100]; // 错误!

和一维数组一样,多维数组也能使用数组初始化程序初始化,使用嵌套的花括号把数组嵌套在数组中即可。例如,可以像下面这样声明、创建并初始化一个 5×5 乘法表:

int[][] products = { {0, 0, 0, 0, 0},

                           {0, 1, 2, 3, 4},

                           {0, 2, 4, 6, 8},

                          {0, 3, 6, 9, 12},

                          {0, 4, 8, 12, 16} };

如果不想声明变量就使用多维数组,可以使用匿名初始化程序句法:

boolean response = bilingualQuestion(question, new String[][] {

                                                                                { "Yes", "No" },

                                                                                { "Oui", "Non" }});

使用 new 关键字创建多维数组时,往往最好只使用矩形数组,即每个维度的数组大小相同。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • 一、Java 简介 Java是由Sun Microsystems公司于1995年5月推出的Java面向对象程序设计...
    子非鱼_t_阅读 4,128评论 1 44
  • 数组类型和数组引用变量详解 数组类型为什么要用数组?Java数组的两大特征:定义数组时,不能指定数组的长度变量分为...
    Ansaxnsy阅读 2,851评论 2 3
  • 每日复盘 Objective 你对今天学的记得什么? 小龙老师讲的五大说服术! Reflective 一句话形容今...
    她念阅读 107评论 0 0
  • 01 前文中我提到的同事的嫂嫂,因为电车的交通事故意外身亡。 本应该在家里办丧事的一家人,却…… 两车相撞,一死一...
    米米的精神小屋阅读 425评论 0 3
  • MobileVLCKit:基于FFmpeg,Live555提供完整的媒体播放库,支持CocoaPods导入库,开发...
    Bryan5137阅读 6,038评论 13 4