Java17 入门基础知识(十一)
十九、数组
在本章中,您将学习:
-
如何声明数组类型的变量
-
如何创建数组
-
如何访问数组的元素
-
如何使用一个
for循环和一个for-each循环来遍历一个数组的元素 -
如何将一个数组的元素复制到另一个数组
-
如何复制基元和引用类型的数组
-
如何使用多维数组
-
需要变长数组时如何使用
ArrayList -
如何将一个
ArrayList的元素转换成一个数组,反之亦然 -
如何执行与数组相关的操作,如对数组元素排序、比较两个数组、在数组中执行二分搜索法、用值填充数组、获取数组的字符串表示等。
本章中的所有示例程序都是清单 19-1 中声明的jdojo.array模块的成员。
// module-info.java
module jdojo.array {
exports com.jdojo.array;
}
Listing 19-1The Declaration of a jdojo.array Module
什么是数组?
数组是一种固定长度的数据结构,用于保存同一数据类型的多个值。让我们考虑一个例子,它将解释为什么我们需要数组。假设您被要求声明变量来保存三个雇员的雇员 id。员工 id 将是整数。保存三个整数值的变量声明如下所示:
int empId1, empId2, empId3;
如果员工人数增加到五人,你会怎么做?您可以修改变量声明,如下所示:
int empId1, empId2, empId3, empId4, empId5;
如果员工人数增加到 1000 人,你会怎么做?你肯定不想声明 1000 个int变量,比如empId1、empId2...empId1000。即使你这样做了,产生的代码也是难以管理和笨拙的。在这种情况下,数组可以帮助您。使用数组,您可以声明一个类型的变量,该变量可以包含任意多的该类型的值。事实上,Java 对数组可以容纳的值的数量有限制。一个数组最多可以容纳 2,147,483,647 个值,这是int数据类型的最大值。
什么使变量成为数组?在变量声明中,将[](空括号)放在数据类型之后或变量名之后,使变量成为数组。例如,
int empId;
是一个简单的变量声明。这里,int是数据类型,empId是变量名。这个声明意味着empId变量可以保存一个整数值。将[]放在前面声明中的数据类型之后,如
int[] empId;
使empId成为数组变量。前面的声明读作“empId是一个int的数组。”您也可以通过在变量名称后放置[]来使empId变量成为一个数组,如下所示:
int empId[];
这两个声明都是有效的。本书使用第一个约定来声明数组。我们从一个保存三个雇员 id 的变量声明的例子开始讨论。到目前为止,您已经为在一个变量中保存多个值做好了准备。也就是说,声明为一个int数组的empId变量能够保存多个int值。你的empId数组变量可以容纳多少个值?答案是你还不知道。在声明数组时,不能指定数组可以容纳的值的数量。随后的部分解释了如何指定一个数组可以容纳的值的数量。您可以声明基元类型的数组以及引用类型的数组。以下是数组声明的更多示例:
// salary can hold multiple float values
float[] salary;
// name can hold multiple references to String objects
String[] name;
// emp can hold multiple references to Employee objects
Employee[] emp;
Tip
数组是固定长度的数据结构,用于存储相同类型的数据项。数组的所有元素都连续存储在内存中。
数组是对象
Java 中的数组是一个对象。Java 中的每个对象都属于一个类;每个数组对象也是如此。您可以使用new操作符创建一个数组对象。您已经使用了带有构造器的new操作符来创建一个类的对象。构造器的名称与类名相同。数组对象的类的名字是什么?这个问题的答案不是那么明显。我们将在本章后面回答这个问题。
现在,让我们专注于如何创建一个特定类型的数组对象。数组创建表达式的一般语法如下:
new <array-data-type>[<array-length>];
数组对象创建表达式以new操作符开始,后跟要存储在数组中的值的数据类型,再跟一个用[](括号)括起来的整数,这是数组中元素的数目。例如,您现在可以创建一个数组来存储五个int值,如下所示:
new int[5];
在这个表达式中,5是数组的长度(也称为数组的维数)。“维度”一词也用于另一个上下文中。您可以拥有一维、二维、三维或多维数组。具有一个以上维度的数组称为多维数组。我们将在本章后面讨论多维数组。在本书中,我把前面表达式中的5称为数组的长度,而不是数组的维数。
注意,前面的表达式在内存中创建了一个数组对象,它分配内存来存储五个整数。new操作符返回内存中新对象的引用。如果要在代码中稍后使用该对象,必须将该引用存储在对象引用变量中。引用变量类型必须匹配由new操作符返回的对象引用的类型。在前一种情况下,new操作符将返回一个int数组类型的对象引用。您已经看到了如何声明一个int数组类型的引用变量。现声明如下:
int[] empIds;
要在empId中存储数组对象引用,您可以这样写:
empIds = new int[5];
您也可以将数组的声明及其创建合并到一条语句中,如下所示:
int[] empIds = new int[5];
由于数组的类型可以从初始化中解释,如果这是一个局部变量,您可以使用局部类型推断来避免重复,如下所示:
var empIds = new int[5];
如何创建一个数组来存储 252 个员工 id?您可以这样做:
var empIds = new int[252];
创建数组时,也可以使用表达式来指定数组的长度:
int total = 23;
int[] array1 = new int[total]; // array1 has 23 elements
int[] array2 = new int[total * 3]; // array2 has 69 elements
因为所有数组都是对象,所以它们的引用可以赋给一个Object类型的引用变量,例如:
int[] empId = new int[5]; // Create an array object
Object obj = empId; // A valid assignment
但是,如果您在一个Object类型的引用变量中引用了一个数组,那么在将它赋给一个数组引用变量或通过索引访问元素之前,您需要将它转换为适当的数组类型。记住每个数组都是一个对象。然而,并不是每个对象都一定是数组:
// Assume that obj is a reference of the Object type that holds a reference of int[]
int[] tempIds = (int[]) obj;
访问数组元素
一旦使用new操作符创建了一个数组对象,就可以使用括号中的元素索引来引用数组中的每个元素。第一个元素的索引是 0,第二个元素是 1,第三个元素是 2,依此类推。这被称为从零开始的索引。数组最后一个元素的索引是数组长度减 1。如果您有一个长度为 5 的数组,数组元素的索引将是 0、1、2、3 和 4。考虑以下语句:
int[] empId = new int[5];
empId数组的长度为5;其要素可分为empId[0]、empId[1]、empId[2]、empId[3]、empId[4]。
如果引用数组中不存在的元素,将会导致运行时错误。例如,在您的代码中使用empId[5]会抛出异常,因为empId的长度为5,而empId[5]引用的是不存在的第六个元素。可以为数组元素赋值,如下所示:
empId[0] = 10; // Assign 10 to the first element of empId
empId[1] = 20; // Assign 20 to the second element of empId
empId[2] = 30; // Assign 30 to the third element of empId
empId[3] = 40; // Assign 40 to the fourth element of empId
empId[4] = 50; // Assign 50 to the fifth element of empId
表 19-1 显示了一个数组的详细信息。它显示执行语句后数组元素的索引、值和引用。
表 19-1
empId 数组在内存中的数组元素
|元素的索引
|
0
|
1
|
2
|
3
|
4
|
| --- | --- | --- | --- | --- | --- |
| 元素的值 | 10 | 20 | 30 | 40 | 50 |
| 元素的引用 | empId[0] | empId[1] | empId[2] | empId[3] | empId[4] |
以下语句将empId数组的第三个元素的值赋给一个int变量temp:
int temp = empId[2]; // Assigns 30 to temp
数组长度
一个数组对象有一个名为length的public final实例变量,它包含数组中元素的数量:
int[] empId = new int[5]; // Create an array of length 5
int len = empId.length; // 5 will be assigned to len
注意length是你创建的数组对象的属性。在创建数组对象之前,不能使用它的length属性。以下代码片段说明了这一点:
// salary is a reference variable, which can refer to an array of int.
// At this point, it contains null. That is, it is not referencing a valid object.
int[] salary = null;
// A runtime error. salary is not referring to any array object yet.
int len = salary.length;
// Create an int array of length 1000 and assign its reference to salary
salary = new int[1000];
// Correct. len2 has a value 1000
int len2 = salary.length;
通常,使用循环来访问数组元素。如果您想对数组的所有元素进行任何处理,您可以执行一个从索引 0(零)到长度减 1 的循环。例如,要将值 10、20、30、40 和 50 赋给长度为5的empId数组的元素,可以执行如下所示的for循环:
for (int i = 0 ; i < empId.length; i++) {
empId[i] = (i + 1) * 10;
}
值得注意的是,在执行循环时,循环条件必须检查数组索引/下标是否小于数组长度,如"i < empId.length"所示,因为数组索引从 0 开始,而不是从 1 开始。程序员在使用for循环处理数组时犯的另一个常见错误是从 1 开始循环计数器,而不是从 0 开始。如果把前面代码中for循环的初始化部分从int i = 0改成int i = 1会怎么样?它不会给你任何错误。但是,第一个元素empId[0]不会被处理,也不会被赋值为 10。
数组创建后,不能更改其长度。您可能想修改length属性:
int[] roll = new int[5]; // Create an array of 5 elements
// A compile-time error. The length property of an array is final. You cannot modify it.
roll.length = 10;
你可以有一个零长度的数组。这样的数组称为空数组:
// Create an array of length zero
int[] emptyArray = new int[0];
// Will assign zero to len
int len = emptyArray.length;
Tip
数组使用从零开始的索引。也就是说,数组的第一个元素的索引为零。数组在运行时动态创建。创建数组后,不能修改数组的长度。如果需要修改数组的长度,必须创建一个新数组,并将旧数组中的元素复制到新数组中。数组的长度可以为零。
初始化数组元素
回想一下第七章中的内容,与类成员变量(实例和静态变量)不同,局部变量在默认情况下是不初始化的。除非给局部变量赋值,否则不能访问局部变量。同样的规则也适用于空白的最终变量。编译器使用明确赋值的规则来确保所有变量在程序中使用它们的值之前已经被初始化。
不管数组创建的范围是什么,数组元素总是被初始化。原始数据类型的数组元素被初始化为其数据类型的默认值。例如,数字数组元素被初始化为零,boolean元素被初始化为false,char 元素被初始化为'\u0000'。引用类型的数组元素初始化为null。以下代码片段说明了数组初始化:
// intArray[0], intArray[1] and intArray[2] are initialized to zero by default.
int[] intArray = new int[3];
// bArray[0] and bArray[1] are initialized to false.
boolean[] bArray = new boolean[2];
// An example of a reference type array. strArray[0] and strArray[1] are
// initialized to null.
String[] strArray = new String[2]
// Another example of a reference type array.
// All 100 elements of the person array are initialized to null.
Person[] person = new Person[100];
清单 19-2 展示了一个实例变量和一些局部变量的数组初始化。
// ArrayInit.java
package com.jdojo.array;
public class ArrayInit {
private final boolean[] bArray = new boolean[3]; // An instance variable
public ArrayInit() {
// Display the initial value for elements of the instance variable bArray
for (int i = 0; i < bArray.length; i++) {
System.out.println("bArray[" + i + "]:" + bArray[i]);
}
}
public static void main(String[] args) {
System.out.println("int array initialization:");
int[] empId = new int[3]; // A local array variable
for (int i = 0; i < empId.length; i++) {
System.out.println("empId[" + i + "]:" + empId[i]);
}
System.out.println("\nboolean array initialization:");
// Initial value for bArray elements are displayed inside the constructor
new ArrayInit();
System.out.println("\nReference type array initialization:");
String[] name = new String[3]; // A local array variable
for (int i = 0; i < name.length; i++) {
System.out.println("name[" + i + "]:" + name[i]);
}
}
}
int array initialization:
empId[0]:0
empId[1]:0
empId[2]:0
boolean array initialization:
bArray[0]:false
bArray[1]:false
bArray[2]:false
Reference type array initialization:
name[0]:null
name[1]:null
name[2]:null
Listing 19-2Default Initialization of Array Elements
当心引用类型的数组
基元类型的数组元素包含该基元类型的值,而引用类型的数组元素包含对对象的引用。假设您有一个int数组:
int[] empId = new int[5];
这里,empId[0]、empId[1]...empId[4]包含一个int值。假设你有一个String数组,像这样:
String[] name = new String[5];
这里,name[0], name[1]...name[4]可以包含对String对象的引用。注意,String对象,即name数组的元素,还没有被创建。正如上一节所讨论的,此时name数组的所有元素都包含null。您需要创建String对象并将它们的引用逐个分配给数组的元素,如下所示:
name[0] = "John";
name[1] = "Donna";
name[2] = "Wally";
name[3] = "Reddy";
name[4] = "Buddy";
一个常见的错误是,在创建数组之后,在为每个元素分配有效的对象引用之前,引用引用类型数组的元素。以下代码说明了这一常见错误:
// Create an array of String
String[] name = new String[5];
// A runtime error as name[0] is null
int len = name[0].length();
// Assign a valid string object to all elements of the array
name[0] = "John";
name[1] = "Donna";
name[2] = "Wally";
name[3] = "Reddy";
name[4] = "Buddy";
// Now you can get the length of the first element
int len2 = name[0].length(); // Correct. len2 has value 4
图 19-1 描述了String参考型数组的初始化概念。这个概念适用于所有引用类型。
图 19-1
引用类型数组初始化
数组的所有元素都连续存储在内存中。对于引用类型的数组,数组元素存储对象的引用。这些元素中的引用是连续存储的,而不是它们所引用的对象。对象存储在堆上;并且它们的位置通常不相邻。
显式数组初始化
当声明数组或使用new操作符创建数组对象时,可以显式初始化数组元素。元素的初始值由逗号分隔,并用大括号({})括起来:
// Initialize the array at the time of declaration
int[] empIds = {1, 2, 3, 4, 5};
这段代码创建一个长度为5的int数组,并将其元素初始化为1、2、3、4和5。请注意,在声明数组时指定数组初始化列表,并不指定数组的长度。数组的长度与数组初始化列表中指定的值的数量相同。这里,empId数组的长度将是5,因为您在初始化列表中传递了五个值{ 1、2、3、4、5}。初始化列表中的最后一个值后面可以跟一个逗号:
int[] empIds = {1, 2, 3, 4, 5, }; // A comma after the last value 5 is valid.
或者,您可以初始化数组的元素,如下所示:
int[] empIds = new int[]{1, 2, 3, 4, 5};
如果正在定义局部变量,并且类型包含在初始化中,则还可以使用“var ”,如下所示:
var empIds = new int[]{1, 2, 3, 4, 5};
请注意,如果指定了数组初始化列表,则不能指定数组的长度。数组的长度与初始化列表中指定的值的数量相同。使用空初始化列表创建空数组是有效的:
int[] emptyNumList = { };
对于引用类型数组,可以在初始化列表中指定对象列表。下面的代码片段说明了String和Account类型的数组初始化。假设Account类存在,并且它有一个构造器,该函数将一个账号作为参数:
// Create a String array with two Strings "Sara" and "Truman"
String[] names = {new String("Sara"), new String("Truman")};
// You can also use String literals
String[] names = {"Sara", "Truman"};
// Create an Account array with two Account objects
Account[] ac = new Account[]{new Account(1), new Account(2)};
Tip
当使用初始化列表初始化数组元素时,不能指定数组的长度。数组的长度被设置为初始化列表中值的数量。
使用数组的限制
Java 中的数组在创建后不能扩展或收缩。假设您有一个包含 100 个元素的数组,稍后,您只需要保留 15 个元素。你不能去掉剩下的 85 个元素。如果需要 135 个元素,就不能再追加 35 个元素。如果您的应用程序有足够的可用内存,您就可以处理第一个限制(内存不能释放给未使用的数组元素)。但是,如果需要向现有数组添加更多元素,就没有办法了。唯一的解决方案是创建另一个所需长度的数组,并将数组元素从原始数组复制到新数组中。有两种方法可以将数组元素从一个数组复制到另一个数组:
-
使用循环
-
使用
java.lang.System类的static arraycopy()方法 -
使用
java.util.Arrays类的copyOf()方法
假设您有一个长度为originalLength的int数组,您想将其长度修改为newLength。您可以应用复制数组的第一种方法,如下面的代码片段所示:
int originalLength = 100;
int newLength = 15;
int[] ids = new int[originalLength];
// Do some processing here...
// Create a temporary array of new length
int[] tempIds = new int[newLength];
// While copying array elements we have to check if the new length
// is less than or greater than original length
int elementsToCopy = originalLength > newLength ? newLength : originalLength;
// Copy the elements from the original array to the new array
for (int i = 0; i < elementsToCopy; i++){
tempIds[i] = ids[i];
}
// Finally assign the reference of new array to ids
ids = tempIds;
将一个数组的元素复制到另一个数组的另一种方法是使用System类的arraycopy()方法。arraycopy()的签名方法如下:
public static void arraycopy(Object sourceArray, int sourceStartPosition,
Object destinationArray,
int destinationStartPosition,
int lengthToBeCopied)
这里
-
sourceArray是对源数组的引用。 -
sourceStartPosition是源数组中开始复制元素的起始索引。 -
destinationArray是对目标数组的引用。 -
destinationStartPosition是目标数组中的起始索引,将从该处复制源数组中的新元素。 -
lengthToBeCopied是从源数组复制到目标数组的元素个数。
您可以用以下代码替换之前的for循环:
// Now copy array elements using the arraycopy() method
System.arraycopy (ids, 0, tempIds, 0, elementsToCopy);
也可以使用Arrays类的copyOf()静态方法。下面显示了一些copyOf()方法的声明:
-
boolean[] copyOf(boolean[] original, int newLength) -
byte[] copyOf(byte[] original, int newLength) -
char[] copyOf(char[] original, int newLength) -
double[] copyOf(double[] original, int newLength) -
float[] copyOf(float[] original, int newLength) -
int[] copyOf(int[] original, int newLength) -
short[] copyOf(long[] original, int newLength) -
<T> T[] copyOf(T[] original, int newLength)
copyOf()方法的第一个参数是源数组。第二个参数newLength是新数组中元素的数量。如果newLength小于源数组的长度,返回的数组将是源数组的截断副本。如果newLength大于源数组的长度,那么返回的数组将包含原始数组中的所有元素,多余的元素将根据数组的数据类型设置默认值。如果newLength等于源数组的长度,则返回的数组包含与源数组相同数量的元素。
Tip
Arrays类包含一个copyOfRange()方法,让您将一组元素从一个数组复制到另一个数组。它对 int 数组的声明是int[] copyOfRange(int[] original, int from, int to)。该方法对所有数据类型都是重载的。这里,from和to是要复制的源数组中元素的初始索引(含)和最终索引(不含)。这些索引必须在源数组的范围内,这意味着目标数组的长度最大可以等于源数组的长度。
两个类的对象java.util.ArrayList和java.util.Vector可以用来代替数组,其中数组的长度需要修改。你可以把这两个类的对象想象成变长数组。下一节将详细讨论这两个类。
清单 19-3 展示了如何使用for循环、System.arraycopy()方法、Arrays.copyOf()方法和Arrays.copyOfRange()方法复制一个数组。
// ArrayCopyTest.java
package com.jdojo.array;
import java.util.Arrays;
public class ArrayCopyTest {
public static void main(String[] args) {
// Have an array with 5 elements
int[] data = {1, 2, 3, 4, 5};
// Expand the data array to 7 elements
int[] eData = expandArray(data, 7);
// Truncate the data array to 3 elements
int[] tData = expandArray(data, 3);
System.out.println("Using for-loop...");
printArrays(data, eData, tData);
/* Using System.arraycopy() method */
// Copy data array to new arrays
eData = new int[7];
tData = new int[3];
System.arraycopy(data, 0, eData, 0, 5);
System.arraycopy(data, 0, tData, 0, 3);
System.out.println("\nUsing System.arraycopy() method...");
printArrays(data, eData, tData);
/* Using Arrays.copyOf() method */
// Copy data array to new arrays
eData = Arrays.copyOf(data, 7);
tData = Arrays.copyOf(data, 3);
System.out.println("\nUsing Arrays.copyOf() method...");
printArrays(data, eData, tData);
/* Using Arrays.copyOfRange() method */
// Copy data array to new arrays
int[] copy1 = Arrays.copyOfRange(data, 0, 3);
int[] copy2 = Arrays.copyOfRange(data, 2, 4);
System.out.println("\nUsing Arrays.copyOfRange() method...");
System.out.println("Original Array: " + Arrays.toString(data));
System.out.println("Copy1 (0, 3): " + Arrays.toString(copy1));
System.out.println("Copy2 (2, 4): " + Arrays.toString(copy2));
}
// Uses a for-loop to copy an array
public static int[] expandArray(int[] oldArray, int newLength) {
int originalLength = oldArray.length;
int[] newArray = new int[newLength];
int elementsToCopy = originalLength > newLength ? newLength : originalLength;
for (int i = 0; i < elementsToCopy; i++) {
newArray[i] = oldArray[i];
}
return newArray;
}
private static void printArrays(int[] original, int[] expanded, int[] truncated) {
System.out.println("Original Array: " + Arrays.toString(original));
System.out.println("Expanded Array: " + Arrays.toString(expanded));
System.out.println("Truncated Array: " + Arrays.toString(truncated));
}
}
Using for-loop...
Original Array: [1, 2, 3, 4, 5]
Expanded Array: [1, 2, 3, 4, 5, 0, 0]
Truncated Array: [1, 2, 3]
Using System.arraycopy() method...
Original Array: [1, 2, 3, 4, 5]
Expanded Array: [1, 2, 3, 4, 5, 0, 0]
Truncated Array: [1, 2, 3]
Using Arrays.copyOf() method...
Original Array: [1, 2, 3, 4, 5]
Expanded Array: [1, 2, 3, 4, 5, 0, 0]
Truncated Array: [1, 2, 3]
Using Arrays.copyOfRange() method...
Original Array: [1, 2, 3, 4, 5]
Copy1 (0, 3): [1, 2, 3]
Copy2 (2, 4): [3, 4]
Listing 19-3Copying an Array Using a for Loop and the System.arraycopy() Method
Arrays类在java.util包中。它包含了许多处理数组的方便方法。例如,它包含将数组转换为字符串格式、对数组排序等方法。您使用了Arrays.toString()静态方法来获取字符串格式的数组内容。该方法被重载;您可以使用它来获取字符串格式的任何类型数组的内容。在这个例子中,您使用了一个for循环和 S ystem.arraycopy()方法来复制数组。注意,使用arraycopy()方法比使用for循环要强大得多。例如,arraycopy()方法被设计用来处理将一个数组的元素从一个区域复制到同一个数组中的另一个区域。它会处理数组中源区域和目标区域的任何重叠。对于引用类型数组,您可以使用以下版本的copyOfRange()方法来更改返回数组的类型:
<T,U> T[] copyOfRange(U[] original, int from, int to, Class<? extends T[]> newType)
该方法采用一个U类型的数组,并返回一个T类型的数组。
模拟可变长度数组
你知道 Java 不提供变长数组。然而,Java 库提供了一些类,它们的对象可以用作变长数组。这些类提供了获取其元素的数组表示的方法。ArrayList和Vector是java.util包中的两个类,每当需要可变长度数组时都可以使用。LinkedList 是另一种类型的列表,可以存储任意数量的元素,但不利用数组,因此具有不同的性能特征。
ArrayList和Vector类的工作方式相同,只是Vector类中的方法是同步的,而ArrayList类中的方法不是同步的。Vector 是一个遗留类,应该避免使用。如果你的对象列表被多个线程同时访问和修改,你应该使用CopyOnWriteArrayList类,它会慢一些但是线程安全的。否则,你应该使用ArrayList类。在接下来的讨论中,我们将仅参考ArrayList。然而,这个讨论也适用于other List implementations。
数组和ArrayList类的一个很大的区别是,后者只处理对象,不处理原始数据类型。ArrayList类是一个泛型类,它将元素的类型作为类型参数。如果你想使用原始值,你需要声明一个包装类的ArrayList。例如,使用ArrayList<Integer>来处理int元素,你所有的int值都会自动装箱到Integer对象中。下面的代码片段说明了ArrayList类的用法:
import java.util.ArrayList;
...
// Create an ArrayList of Integer
ArrayList<Integer> ids = new ArrayList<>();
// Get the size of array list
int total = ids.size(); // total will be zero at this point
// Print the details of array list
System.out.println("ArrayList size is " + total);
System.out.println("ArrayList elements are " + ids);
// Add three ids 10, 20, 30 to the array list.
ids.add(new Integer(10)); // Adding an Integer object.
ids.add(20); // Adding an int. Autoboxing is at play.
ids.add(30); // Adding an int. Autoboxing is at play.
// Get the size of the array list
total = ids.size(); // total will be 3
// Print the details of array list
System.out.println("ArrayList size is " + total);
System.out.println("ArrayList elements are " + ids);
// Clear all elements from array list
ids.clear();
// Get the size of the array list
total = ids.size(); // total will be 0
// Print the details of array list
System.out.println("ArrayList size is " + total);
System.out.println("ArrayList elements are " + ids);
ArrayList size is 0
ArrayList elements are []
ArrayList size is 3
ArrayList elements are [10, 20, 30]
ArrayList size is 0
ArrayList elements are []
请注意该输出中的一个重要观察结果。您可以打印一个ArrayList中所有元素的列表,只需将它的引用传递给System.out.println()方法。ArrayList类的toString()方法返回一个字符串,该字符串是用括号([ ])括起来的元素的逗号分隔的字符串表示。
像数组一样,ArrayList使用从零开始的索引。即ArrayList的第一个元素的索引为零。您可以通过使用get(int index)方法获得存储在任何索引处的元素:
// Get the element at the index 0 (the first element)
Integer firstId = ids.get(0);
// Get the element at the index 1 (the second element)
int secondId = ids.get(1); // Auto-unboxing is at play
您可以使用其contains()方法检查ArrayList是否包含一个对象:
Integer id20 = 20;
Integer id50 = 50;
// Add three objects to the arraylist
ids.add(10);
ids.add(20);
ids.add(30);
// Check if the array list contains id20 and id50
boolean found20 = ids.contains(id20); // found20 will be true
boolean found50 = ids.contains(id50); // found50 will be false
您可以通过三种方式之一迭代一个ArrayList的元素:使用循环、使用迭代器或使用 forEach 方法。在这一章中,我们将讨论如何使用for循环和 forEach 来遍历ArrayList的元素。下面的代码片段展示了如何使用for循环来遍历ArrayList的元素:
// Get the size of the ArrayList
int total = ids.size();
// Iterate through all elements
for (int i = 0; i < total; i++) {
int temp = ids.get(i);
// Do some processing...
}
如果想遍历ArrayList的所有元素而不考虑它们的索引,可以使用如下所示的for - each循环:
// Iterate through all elements
for (int temp : ids) {
// Do some processing with temp...
}
要使用 forEach 遍历所有元素,需要提供一个方法引用或 lambda 表达式作为参数,例如
ids.forEach(id ->
//Do some processing with id
);
清单 19-4 展示了使用for循环和for-each循环来遍历ArrayList的元素。它还展示了如何使用remove()方法从ArrayList中删除一个元素。
// NameIterator.java
package com.jdojo.array;
import java.util.ArrayList;
public class NameIterator {
public static void main(String[] args) {
// Create an ArrayList of String
ArrayList<String> nameList = new ArrayList<>();
// Add some names
nameList.add("Chris");
nameList.add("Laynie");
nameList.add("Jessica");
// Get the count of names in the list
int count = nameList.size();
// Let us print the name list using a for loop
System.out.println("List of names...");
for(int i = 0; i < count; i++) {
String name = nameList.get(i);
System.out.println(name);
}
// Let us remove Jessica from the list
nameList.remove("Jessica");
// Get the count of names in the list again
count = nameList.size();
// Let us print the name list again using a for-each loop
System.out.println("\nAfter removing Jessica...");
for(String name : nameList) {
System.out.println(name);
}
}
}
List of names...
Chris
Laynie
Jessica
After removing Jessica...
Chris
Laynie
Listing 19-4Iterating Through Elements of an ArrayList
将数组作为参数传递
您可以将数组作为参数传递给方法或构造器。传递给方法的数组类型必须与形参类型的赋值兼容。方法的数组类型参数声明的语法与其他数据类型的语法相同。也就是说,参数声明应以数组类型开头,后跟空格和参数名,如下所示:
[modifiers] <return-type> <methodName>([<array-type> argumentName, ...])
以下是带有数组参数的方法声明的一些示例:
// The processSalary() method has two parameters:
// 1\. id is an array of int
// 2\. salary is an array of double
public static void processSalary(int[] id, double[] salary) {
// Code goes here...
}
// The setAka() method has two parameters:
// 1\. id is int (It is simply int type, not array of int)
// 2\. aka is an array of String
public static void setAka(int id, String[] aka) {
// Code goes here...
}
// The printStates() method has one parameter:
// 1\. stateNames is an array of String
public static void printStates(String[] stateNames) {
// Code goes here...
}
下面的代码片段模拟了ArrayList的toString()方法。它接受一个int数组,并返回用括号括起来的逗号分隔的值([]):
public static String arrayToString(int[] source) {
if (source == null) {
return null;
}
// Use StringBuilder to improve performance
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < source.length; i++) {
if (i == source.length - 1) {
result.append(source[i]);
} else {
result.append(source[i] + ",");
}
}
result.append("]");
return result.toString();
}
这种方法可以按如下方式调用:
int[] ids = {10, 15, 19};
String str = arrayToString(ids); // Pass ids int array to arrayToString() method
因为数组是一个对象,所以数组引用被传递给方法。接收数组参数的方法可以修改数组的元素。清单 19-5 展示了一个方法如何改变它的数组参数的元素;这个例子还展示了如何实现swap()方法来使用数组交换两个整数。
// Swap.java
package com.jdojo.array;
public class Swap {
public static void main(String[] args) {
int[] num = {17, 80};
System.out.println("Before swap");
System.out.println("#1: " + num[0]);
System.out.println("#2: " + num[1]);
// Call the swap() method passing the num array
swap(num);
System.out.println("After swap");
System.out.println("#1: " + num[0]);
System.out.println("#2: " + num[1]);
}
// The swap() method accepts an int array as an argument and swaps the values
// if array contains two values.
public static void swap (int[] source) {
if (source != null && source.length == 2) {
// Swap the first and the second elements
int temp = source[0];
source[0] = source[1];
source[1] = temp;
}
}
}
Before swap
#1: 17
#2: 80
After swap
#1: 80
#2: 17
Listing 19-5Passing an Array as a Method Parameter
回想一下,在第八章的中,我们无法实现一个方法来使用基本类型的参数交换两个整数。这是因为,对于基本类型,实际参数被复制到形式参数。这里,您能够在swap()方法中交换两个整数,因为您使用了一个数组作为参数。数组的引用被传递给方法,而不是数组元素的副本。
Tip
将数组传递给方法会有风险。该方法可以修改数组元素,这有时可能不是所期望或想要的。在这种情况下,您应该将数组的副本传递给方法,而不是原始数组;因此,如果该方法修改了数组,您的原始数组不会受到影响。
您可以使用数组的clone()方法快速复制数组。“快速复制”一词值得特别注意。对于基本类型,克隆的数组将拥有原始数组的真实副本。创建一个相同长度的新数组,并将原始数组中每个元素的值复制到克隆数组的相应元素中。但是,对于引用类型,存储在原始数组的每个元素中的对象的引用被复制到克隆数组的相应元素中。这称为浅层复制,而前一种类型(复制对象或值)称为深层复制。在浅层复制的情况下,原始数组和克隆数组的元素都引用内存中的同一个对象。您可以使用存储在原始数组和克隆数组中的引用来修改对象。在这种情况下,即使将原始数组的副本传递给方法,原始数组中引用的对象的状态也可以在方法内部修改。这个问题的解决方案是制作原始数组的深层副本,将其传递给方法。下面的代码片段演示了一个int数组和一个String数组的克隆。注意,clone()方法的返回类型是Object,您需要将返回值转换为适当的数组类型:
// Create an array of 3 integers 1, 2, and 3
int[] ids = {1, 2, 3};
// Declare an array of int named clonedIds.
int[] clonedIds;
// The clonedIds array has the same values as the ids array.
clonedIds = (int[]) ids.clone();
// Create an array of 3 strings.
String[] names = {"Lisa", "Pat", "Kathy"};
// Declare an array of String named clonedNames.
String[] clonedNames;
// The clonedNames array has the reference of the same three strings as the names array.
String[] clonedNames = (String[]) names.clone();
图 19-2 到 19-5 描述了前面代码片段中的原始数组ids和引用数组names的克隆过程。
图 19-5
names 数组克隆在 clonedNames 数组中
图 19-4
填充 names 数组,并声明 clonedNames 数组
图 19-3
ids 数组克隆在 clonedIds 数组中
图 19-2
填充 ids 数组,并声明 clonedIds 数组
注意,当克隆names数组时,clonedNames数组元素引用内存中相同的String对象。当您提到修改传递给它的数组参数的方法时,您可能指以下三种情况中的一种或全部:
-
数组参数引用
-
数组参数的元素
-
数组参数元素引用的对象
数组参数引用
因为数组是一个对象,所以它的引用的副本被传递给一个方法。如果方法更改数组参数,实际参数不受影响。清单 19-6 说明了这一点。main()方法将一个数组传递给tryArrayChange()方法,后者又将一个不同的数组引用分配给参数。输出显示,main()方法中的数组不受影响。
// ModifyArrayParam.java
package com.jdojo.array;
import java.util.Arrays;
public class ModifyArrayParam {
public static void main(String[] args) {
int[] origNum = {101, 307, 78};
System.out.println("Before method call: " + Arrays.toString(origNum));
// Pass the array to the method
tryArrayChange(origNum);
System.out.println("After method call: " + Arrays.toString(origNum));
}
public static void tryArrayChange(int[] num) {
System.out.println("Inside method-1: " + Arrays.toString(num));
// Create and store a new int array in num
num = new int[]{10, 20};
System.out.println("Inside method–2: " + Arrays.toString(num));
}
}
Before method call: [101, 307, 78]
Inside method-1: [101, 307, 78]
Inside method–2: [10, 20]
After method call: [101, 307, 78]
Listing 19-6Modifying an Array Parameter Inside a Method
如果不希望方法改变方法体内的数组引用,必须将方法参数声明为final,如下所示:
public static void tryArrayChange(final int[] num) {
// An error. num is final and cannot be changed
num = new int[]{10, 20};
}
数组参数的元素
存储在数组参数元素中的值总是可以在方法内部更改。清单 19-7 说明了这一点。
// ModifyArrayElements.java
package com.jdojo.array;
import java.util.Arrays;
public class ModifyArrayElements {
public static void main(String[] args) {
int[] origNum = {10, 89, 7};
String[] origNames = {"Mike", "John"};
System.out.println("Before method call, origNum: " + Arrays.toString(origNum));
System.out.println("Before method call, origNames: " + Arrays.toString(origNames));
// Call methods passing the arrays
tryElementChange(origNum);
tryElementChange(origNames);
System.out.println("After method call, origNum: " + Arrays.toString(origNum));
System.out.println("After method call, origNames: " + Arrays.toString(origNames));
}
public static void tryElementChange(int[] num) {
// If the array has at least one element, store 1116 in its first element.
if (num != null && num.length > 0) {
num[0] = 1116;
}
}
public static void tryElementChange(String[] names) {
// If the array has at least one element, store "Twinkle" in its first element
if (names != null && names.length > 0) {
names[0] = "Twinkle";
}
}
}
Before method call, origNum: [10, 89, 7]
Before method call, origNames: [Mike, John]
After method call, origNum: [1116, 89, 7]
After method call, origNames: [Twinkle, John]
Listing 19-7Modifying Elements of an Array Parameter Inside a Method
请注意,在方法调用后,数组的第一个元素发生了变化。您可以在方法内部更改数组参数的元素,即使数组参数声明为final。
数组参数元素引用的对象
本节仅适用于引用类型的数组参数。如果数组的引用类型是可变的,您可以更改存储在数组元素中的对象的状态。在上一节中,我讨论了用新的对象引用替换数组元素中存储的引用。本节讨论如何更改数组元素所引用的对象的状态。考虑一个Item类,如清单 19-8 所示。
// Item.java
package com.jdojo.array;
public class Item {
private double price;
private final String name;
public Item (String name, double price) {
this.name = name;
this.price = price;
}
public double getPrice() {
return this.price;
}
public void setPrice(double price ) {
this.price = price;
}
@Override
public String toString() {
return "[" + this.name + ", " + this.price + "]";
}
}
Listing 19-8An Item Class
清单 19-9 说明了这一点。main()方法创建一个Item数组。该数组被传递给tryStateChange()方法,该方法将数组中第一个元素的价格改为 10.38。输出显示在main()方法中创建的数组中的原始元素的价格发生了变化。
// ModifyArrayElementState.java
package com.jdojo.array;
public class ModifyArrayElementState {
public static void main(String[] args) {
Item[] myItems = {new Item("Pen", 25.11), new Item("Pencil", 0.10)};
System.out.println("Before method call #1:" + myItems[0]);
System.out.println("Before method call #2:" + myItems[1]);
// Call the method passing the array of Item
tryStateChange(myItems);
System.out.println("After method call #1:" + myItems[0]);
System.out.println("After method call #2:" + myItems[1]);
}
public static void tryStateChange(Item[] allItems) {
if (allItems != null && allItems.length > 0) {
// Change the price of the first item to 10.38
allItems[0].setPrice(10.38);
}
}
}
Before method call #1:[Pen, 25.11]
Before method call #2:[Pencil, 0.1]
After method call #1:[Pen, 10.38]
After method call #2:[Pencil, 0.1]
Listing 19-9Modifying the States of Array Elements of an Array Parameter Inside a Method
Tip
方法可以用来克隆一个数组。对于引用数组,clone()方法执行浅层复制。应该小心地将数组传递给方法并从方法返回。如果一个方法可能会修改它的数组参数,而您不希望实际的数组参数受到该方法调用的影响,则必须将数组的深层副本传递给该方法。
如果将对象的状态存储在数组实例变量中,则在从类的任何方法返回该数组的引用之前,应该仔细考虑。该方法的调用方将获得数组实例变量的句柄,并且能够在类外部更改该类的对象的状态。以下示例说明了这种情况:
public class MagicNumber {
// Magic numbers are not supposed to be changed. They can be looked up though.
private int[] magicNumbers = {5, 11, 21, 51, 101};
// Other code goes here...
public int[] getMagicNumbers () {
/* Never do the following. If you do this, callers of this
method will be able to change the magic numbers.
*/
// return this.magicNumbers;
/* Do the following instead. In case of reference arrays, make a deep copy, and
return that copy. For primitive arrays you can use the clone() method.
*/
return (int[]) magicNumbers.clone();
}
}
您也可以创建一个数组并将其传递给一个方法,而不将数组引用存储在变量中。假设有一个名为setNumbers(int[] nums)的方法,它以一个int数组作为参数。您可以调用此方法,如下所示:
setNumbers(new int[]{10, 20, 30});
注意,在这种情况下,您必须使用new操作符。以下方法调用将不起作用:
// A compile-time error. The array initialization list is supported only
// in an array declaration statement
setNumbers({10, 20, 30});
命令行参数
可以从命令提示符(Windows 中的命令提示符和 UNIX 中的 shell 提示符)启动 Java 应用程序。它也可以从 Java 开发环境工具中启动,如 NetBeans、Eclipse、JDeveloper 等。Java 应用程序在命令行运行,如下所示:
java --module-path <module-path> --module <module-name/<class-name>
java --module-path <module-path> --module <module-name/<class-name> <list-of-command-line arguments>
参数列表中的每个参数由空格分隔。例如,以下命令运行com.jdojo.array.Test类并传递三个名称作为命令行参数:
C:\JavaFun>java --module-path build\modules\jdojo.array --module jdojo.array/com.jdojo.array.Test Cat Dog Rat
当运行Test类时,这三个命令行参数会发生什么变化?操作系统将参数列表传递给 JVM。有时,操作系统可以通过解释参数的含义来扩展参数列表,并且可以将修改后的参数列表传递给 JVM。JVM 使用空格作为分隔符来解析参数列表。它创建了一个String数组,其长度与列表中参数的数量相同。它按顺序用参数列表中的项目填充String数组。最后,JVM 将这个String数组传递给正在运行的Test类的main()方法。这是您使用传递给main()方法的String数组参数的时候。如果没有命令行参数,JVM 将创建一个零长度的String数组,并将其传递给main()方法。如果要将空格分隔的单词作为一个参数传递,可以用双引号将它们括起来。您还可以通过将特殊字符用双引号括起来来避免操作系统对它们的解释。让我们创建一个名为CommandLine的类,如清单 19-10 所示。
// CommandLine.java
package com.jdojo.array;
public class CommandLine {
public static void main(String[] args) {
// args contains all command-line arguments
System.out.println("Total Arguments: " + args.length);
// Display all arguments
for (int i = 0; i < args.length; i++) {
System.out.println("Argument #" + (i + 1) + ": " + args[i]);
}
}
}
Listing 19-10Processing Command-line Arguments Inside the main() Method
以下是向CommandLine类传递命令行参数的几个例子:
C:\JavaFun>java --module-path build\modules\jdojo.array --module jdojo.array/com.jdojo.array.CommandLine
Total Arguments: 0
C:\JavaFun>java --module-path build\modules\jdojo.array --module jdojo.array/com.jdojo.array.CommandLine Cat Dog Rat
Total Arguments: 3
Argument #1: Cat
Argument #2: Dog
Argument #3: Rat
C:\JavaFun>java --module-path build\modules\jdojo.array --module jdojo.array/com.jdojo.array.CommandLine "Cat Dog Rat"
Total Arguments: 1
Argument #1: Cat Dog Rat
C:\JavaFun>java --module-path build\modules\jdojo.array --module jdojo.array/com.jdojo.array.CommandLine 29 Dogs
Total Arguments: 2
Argument #1: 29
Argument #2: Dogs
命令行参数有什么用?它们允许你改变程序的行为,而不需要重新编译。例如,您可能希望按升序或降序对文件内容进行排序。您可以传递指定排序顺序的命令行参数。如果命令行上没有指定排序顺序,默认情况下可以采用升序。如果您调用排序类com.jdojo.array.SortFile,您可以通过以下方式运行它:
// To sort employee.txt file in ascending order
C:\JavaFun>java --module-path build\modules\jdojo.array --module jdojo.array/com.jdojo.array.SortFile names.txt asc
// To sort department.txt file in descending order
C:\JavaFun>java --module-path build\modules\jdojo.array --module jdojo.array/com.jdojo.array.SortFile names.txt desc
// To sort salary.txt in ascending order
C:\JavaFun>java --module-path build\modules\jdojo.array --module jdojo.array/com.jdojo.array.SortFile names.txt
根据传递给SortFile类的main()方法的String数组的第二个元素(如果有的话),可以对文件进行不同的排序。
注意,所有命令行参数都作为String传递给main()方法。如果你想传递一个数字参数,你需要把参数转换成一个数字。为了说明这种数值参数转换,让我们开发一个迷你计算器类,它将一个表达式作为命令行参数并打印结果。迷你计算器只支持四种基本运算:加、减、乘、除。见清单 19-11 。
// Calc.java
package com.jdojo.array;
import java.util.Arrays;
public class Calc {
public static void main(String[] args) {
// Print the list of commandline argument
System.out.println(Arrays.toString(args));
// Make sure we received three arguments and the
// the second argument has only one character to indicate operation.
if (!(args.length == 3 && args[1].length() == 1)) {
printUsage();
return; // Stop the program here
}
// Parse the two number operands. Place the parsing code inside a try-catch,
// so we will handle the error in case both operands are not numbers.
double n1;
double n2;
try {
n1 = Double.parseDouble(args[0]);
n2 = Double.parseDouble(args[2]);
} catch (NumberFormatException e) {
System.out.println("Both operands must be a number");
printUsage();
return; // Stop the program here
}
String operation = args[1];
double result = compute(n1, n2, operation);
// Print the result
System.out.println(args[0] + args[1] + args[2] + " = " + result);
}
public static double compute(double n1, double n2, String operation) {
// Initialize the result with not-a-number
double result = Double.NaN;
switch (operation) {
case "+":
result = n1 + n2;
break;
case "-":
result = n1 - n2;
break;
case "*":
result = n1 * n2;
break;
case "/":
result = n1 / n2;
break;
default:
System.out.println("Invalid operation:" + operation);
}
return result;
}
public static void printUsage() {
System.out.println("Usage: java com.jdojo.array.Calc expr");
System.out.println("Where expr could be:");
System.out.println("n1 + n1");
System.out.println("n1 - n2");
System.out.println("n1 * n2");
System.out.println("n1 / n2");
System.out.println("n1 and n2 are two numbers");
}
}
Listing 19-11A Mini Command-Line Calculator
以下是使用Calc类执行基本算术运算的几种方法:
C:\JavaFun>java --module-path build\modules\jdojo.array --module jdojo.array/com.jdojo.array.Calc 3 + 7
[3, +, 7]
3+7 = 10.0
C:\JavaFun>java --module-path build\modules\jdojo.array --module jdojo.array/com.jdojo.array.Calc 78.9 * 98.5
[78.9, *, 98.5]
78.9*98.5 = 7771.650000000001
当您尝试使用*(星号)作为两个数字相乘的运算时,可能会出现错误。操作系统可能会将其解释为当前目录中的所有文件名。为了避免这种错误,可以用双引号或操作系统提供的转义符将运算符括起来,如下所示:
C:\JavaFun>java --module-path build\modules\jdojo.array --module jdojo.array/com.jdojo.array.Calc 78.9 "*" 98.5
Tip
如果你的程序使用命令行参数,它就不是一个 100%的 Java 程序。这是因为该程序不符合“编写一次,到处运行”的类别有些操作系统没有命令提示符,因此,您可能无法使用命令行参数功能。此外,操作系统可能会以不同的方式解释命令行参数中的元字符。
多维数组
如果列表中的数据元素使用多个维度来标识,则可以使用多维数组来表示列表。例如,表中的数据元素由行和列两个维度来标识。您可以将表格数据存储在二维数组中。您可以通过在数组声明中为每个维度使用一对括号([])来声明多维数组。例如,您可以声明一个二维数组int,如下所示:
int[][] table;
这里,table是一个引用变量,可以保存对二维数组int的引用。在声明的时候,内存只分配给引用变量table,不分配给任何数组元素。该代码执行后的存储状态如图 19-6 所示。
图 19-6
二维数组声明后的内存状态
可以创建一个三行两列的二维数组int,如下所示:
table = new int[3][2];
该代码执行后的存储器状态如图 19-7 所示。所有元素的值都显示为零,因为默认情况下,数值数组的所有元素都被初始化为零。正如本章前面所讨论的,多维数组的数组元素的默认初始化规则与一维数组的规则相同。
图 19-7
创建二维数组后的内存状态
多维数组中每个维度的索引是从零开始的。table数组的每个元素都可以作为table[rowNumber][columnNumber]来访问。行号和列号总是从零开始。例如,您可以为table数组中的第一行和第二列赋值,如下所示:
table[0][1] = 32;
您可以为第三行和第一列赋值 71,如下所示:
table[2][0] = 71;
两次分配后的存储器状态如图 19-8 所示。
图 19-8
二维数组元素两次赋值后的内存状态
Java 不支持真正意义上的多维数组。相反,它支持数组的数组。使用数组的数组,可以实现与多维数组相同的功能。创建二维数组时,第一个数组的元素属于数组类型,可以引用一维数组。每个一维数组的大小不必相同。考虑到table二维数组的数组概念,你可以描绘数组创建和两个值赋值后的内存状态,如图 19-9 所示。
图 19-9
数组的数组
二维数组的名字table,指的是三个元素的数组。数组的每个元素都是一维数组int。table[0]、table[1]、table[2]的数据类型是一个int数组。table[0]、table[1]和table[2]的长度各为 2。
创建多维数组时,必须至少指定第一级数组的维度。例如,创建二维数组时,必须至少指定第一维,即行数。您可以获得与前面的代码片段相同的结果,如下所示:
table = new int[3][];
该语句仅创建数组的第一级。此时只存在table[0]、table[1]、table[2]。他们指的是null。此时,table.length的值为3。由于table[0]、table[1]和table[2]引用的是null,所以不能访问它们的length属性。也就是说,您在一个表中创建了三行,但是您不知道每行将包含多少列。由于table[0]、table[1]和table[2]是int的数组,可以给它们赋值如下:
table[0] = new int[2]; // Create 2 columns for row 1
table[1] = new int[2]; // Create 2 columns for row 2
table[2] = new int[2]; // Create 2 columns for row 3
您已经完成了二维数组的创建,该数组有三行,每行有两列。您可以将这些值分配给一些单元格,如下所示:
table[0][1] = 32;
table[2][0] = 71;
也可以创建一个每行有不同列数的二维数组。这样的数组称为参差数组。清单 19-12 展示了如何使用一个参差不齐的数组。
// RaggedArray.java
package com.jdojo.array;
public class RaggedArray {
public static void main(String[] args) {
// Create a two-dimensional array of 3 rows
int[][] raggedArr = new int[3][];
// Add 2 columns to the first row
raggedArr[0] = new int[2];
// Add 1 column to the second row
raggedArr[1] = new int[1];
// Add 3 columns to the third row
raggedArr[2] = new int[3];
// Assign values to all elements of raggedArr
raggedArr[0][0] = 1;
raggedArr[0][1] = 2;
raggedArr[1][0] = 3;
raggedArr[2][0] = 4;
raggedArr[2][1] = 5;
raggedArr[2][2] = 6;
// Print all elements. One row at one line
System.out.println(raggedArr[0][0] + "\t" + raggedArr[0][1]);
System.out.println(raggedArr[1][0]);
System.out.println(raggedArr[2][0] + "\t" + raggedArr[2][1] + "\t" + raggedArr[2][2]);
}
}
1 2
3
4 5 6
Listing 19-12An Example of a Ragged Array
Tip
Java 支持数组的数组,可以用来实现多维数组提供的功能。多维数组广泛应用于科学和工程领域。如果您在业务应用程序中使用二维以上的数组,您可能需要重新考虑选择多维数组作为您的数据结构。
访问多维数组的元素
通常,使用嵌套的for循环来填充多维数组。用于填充多维数组的for循环的数量等于数组中的维数。例如,两个for循环用于填充一个二维数组。通常,循环用于访问多维数组的元素。清单 19-13 展示了如何填充和访问一个二维数组的元素。
// MDAccess.java
package com.jdojo.array;
public class MDAccess {
public static void main(String[] args){
int[][] ra = new int[3][];
ra[0] = new int[2];
ra[1] = new int[1];
ra[2] = new int[3];
// Populate the ragged array using for loops
for(int i = 0; i < ra.length; i++) {
for(int j = 0; j < ra[i].length; j++){
ra[i][j] = i + j;
}
}
// Print the array using for loops
for(int i = 0; i < ra.length; i++) {
for (int j = 0; j < ra[i].length; j++){
System.out.print(ra[i][j] + "\t");
}
// Add a new line after each row is printed
System.out.println();
}
}
}
0 1
1
2 3 4
Listing 19-13Accessing Elements of a Multidimensional Array
初始化多维数组
您可以通过在声明或创建多维数组时提供值列表来初始化多维数组的元素。如果用值列表初始化数组,则不能指定任何维度的长度。每个维度的初始值的数量将决定数组中每个维度的长度。由于多维数组中涉及许多维度,因此某个级别的值列表用大括号括起来。对于二维数组,每行的值列表用一对大括号括起来,如下所示:
int[][] arr = {{10, 20, 30}, {11, 22}, {222, 333, 444, 555}};
该语句创建一个包含三行的二维数组。第一行包含值为 10、20 和 30 的三列。第二行包含值为 11 和 22 的两列。第三行包含值为 222、333、444 和 555 的四列。可以创建一个 0 行 0 列的二维数组,如下所示:
int[][] empty2D = { };
引用类型的多维数组的初始化遵循相同的规则。您可以像这样初始化一个二维的String数组:
String[][] acronymList = {{"JMF", "Java Media Framework"},
{"JSP", "Java Server Pages"},
{"JMS", "Java Message Service"}};
您可以在创建多维数组时初始化它的元素,如下所示:
int[][] arr = new int[][]{{1, 2}, {3,4,5}};
阵列的增强 for 循环
Java 有一个增强的for循环,可以让你以一种更简洁的方式遍历数组的元素。增强型for环路也被称为for-each环路。语法如下:
for(DataType e : array) {
// Loop body goes here...
// e contains one element of the array at a time
}
for - each循环使用与基本for循环相同的for关键字。它的主体被执行的次数与array中的元素数一样多。DataType e是变量声明,其中e是变量名,DataType是其数据类型。变量e的数据类型应该与array的类型赋值兼容。变量声明后面跟一个冒号(:),冒号后面跟一个要循环的数组的引用。for - each循环将数组元素的值赋给变量e,您可以在循环体中使用该变量。下面的代码片段使用了一个for - each循环来打印一个int数组的所有元素:
int[] numList = {1, 2, 3};
for(int num : numList) {
System.out.println(num);
}
1
2
3
您可以使用基本的for循环完成同样的任务,如下所示:
int[] numList = {1, 2, 3};
for(int i = 0; i < numList.length; i++) {
int num = numList[i];
System.out.println(num);
}
1
2
3
注意,for - each循环提供了一种遍历数组元素的方法,这比基本的for循环更简洁。然而,它不能代替基本的for循环,因为您不能在所有情况下使用它。例如,您不能访问数组元素的索引,也不能修改循环内元素的值,因为您没有该元素的索引。
数组声明语法
您可以通过在数组的数据类型之后或数组引用变量的名称之后放置一对括号([])来声明数组。例如,下面的声明
int[] empIds;
int[][] points2D;
int[][][] points3D;
Person[] persons;
相当于
int empIds[];
int points2D[][];
int points3D[][][];
Person persons[];
Java 还允许混合两种语法。在同一个数组声明中,可以在数据类型后放置一些括号,在变量名后放置一些括号。例如,您可以如下声明一个二维数组int:
int[] points2D[];
您可以在一个声明语句中声明一个二维数组和一个三维数组int,如下所示:
int[] points2D[], points3D[][];
或者
int[][] points2D, points3D[];
运行时数组边界检查
在运行时,Java 会检查对数组元素的每次访问的数组边界。如果超出了数组界限,就会抛出一个java.lang.ArrayIndexOutOfBoundsException。编译时对数组索引值的唯一要求是它们必须是整数。Java 编译器不会检查数组索引的值是小于零还是超出其长度。这个检查必须在运行时执行,在每次允许访问数组元素之前。运行时数组边界检查会降低程序的执行速度,原因有两个:
-
第一个原因是约束支票本身的成本。要检查数组边界,数组的长度必须加载到内存中,并且必须执行两次比较(一次小于零,一次大于或等于其长度)。
-
第二个原因是,当超出数组界限时,必须抛出异常。Java 必须做一些内务处理,并准备好在超出数组界限时抛出异常。
清单 19-14 展示了超出数组边界时抛出的异常。程序创建一个名为test的数组int,长度为 3。程序无法访问第四个元素(test[3]),因为它不存在。当进行这样的尝试时,抛出一个ArrayIndexOutOfBoundsException。
// ArrayBounds.java
package com.jdojo.array;
public class ArrayBounds {
public static void main(String[] args) {
int[] test = new int[3];
System.out.println("Assigning 12 to the first element");
test[0] = 12; // OK. Index 0 is between 0 and 2.
System.out.println("Assigning 79 to the fourth element");
// Index 3 is not between 0 and 2\. At runtime, an exception is thrown.
test[3] = 79;
System.out.println("We will not get here");
}
}
Assigning 12 to the first element
Assigning 79 to the fourth element
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at com.jdojo.array.ArrayBounds.main(ArrayBounds.java:14)
Listing 19-14Array Bounds Checks
在访问数组元素之前检查数组长度是一个好习惯。数组边界违规引发异常这一事实可能会被误用,如下面的代码片段所示,该代码片段打印存储在数组中的值:
/* Do not use this code, even if it works.*/
// Create an array
int[] arr = new int[10];
// Populate the array here...
// Print the array. Wrong way
try {
// Start an infinite loop. When we are done with all elements an exception is
// thrown and we will be in catch block and hence out of the loop.
int counter = 0;
while (true) {
System.out.println(arr[counter++]);
}
} catch (ArrayIndexOutOfBoundsException e) {
// We are done with printing array elements
}
// Do some processing here...
前面的代码片段使用一个无限的while循环来打印数组元素的值,并依靠异常抛出机制来检查数组边界。正确的方法是使用一个for循环,并使用数组的length属性检查数组索引值。
数组对象的类是什么?
数组是对象。因为每个对象都有一个类,所以每个数组都必须有一个类。Object类的所有方法都可以在数组上使用。因为Object类的getClass()方法为 Java 中的任何对象提供了该类的引用,所以您将使用该方法获取所有数组的类名。清单 19-15 展示了如何获得一个数组的类名。
// ArrayClass.java
package com.jdojo.array;
public class ArrayClass {
public static void main (String[] args){
int[] iArr = new int[2];
int[][] iiArr = new int[2][2];
int[][][] iiiArr = new int[2][2][2];
String[] sArr = {"A", "B"} ;
String[][] ssArr = {{"AA"}, {"BB"}} ;
String[][][] sssArr = {} ; // A 3D empty array of string
// Print the class name for all arrays
System.out.println("int[]: " + getClassName(iArr));
System.out.println("int[][]: " + getClassName(iiArr));
System.out.println("int[][][]: " + getClassName(iiiArr));
System.out.println("String[]: " + getClassName(sArr));
System.out.println("String[][]: " + getClassName(ssArr));
System.out.println("String[][][]: " + getClassName(sssArr));
}
// Any Java object can be passed to getClassName() method.
// Since every array is an object, we can also pass an array to this method.
public static String getClassName(Object obj) {
// Get the reference of its class
Class<?> c = obj.getClass();
// Get the name of the class
String className = c.getName();
return className;
}
}
int[]: [I
int[][]: [[I
int[][][]: [[[I
String[]: [Ljava.lang.String;
String[][]: [[Ljava.lang.String;
String[][][]: [[Ljava.lang.String;
Listing 19-15Knowing the Class of an Array
数组的类名以左括号([)开始。左括号的数量等于数组的维数。对于一个int数组,左括号后面是一个字符I。对于引用类型数组,左括号后面是字符L,后面是类名,后面是分号。表 [19-2 中显示了一维原始数组和引用类型的类名。
表 19-2
数组的类名
|数组类型
|
类别名
|
| --- | --- |
| byte[] | [B |
| short[] | [S |
| int[] | [I |
| long[] | [J |
| char[] | [C |
| float[] | [F |
| double[] | [D |
| boolean[] | [Z |
| com.jdojo.array.Person[] | [Lcom.jdojo.array.Person; |
数组的类名在编译时不可用于声明或创建它们。您必须使用本章中描述的语法来创建数组。也就是说,您不能编写以下代码来创建一个int数组:
[I myIntArray;
相反,您必须编写以下代码来创建一个int数组:
int[] myIntArray;
数组赋值兼容性
数组中每个元素的数据类型与数组的数据类型相同。例如,int[]数组的每个元素都是一个int;一个String[]数组的每个元素都是一个String。赋给数组元素的值必须与其数据类型的赋值兼容。例如,允许将一个byte值赋给一个int数组的元素,因为byte与int的赋值是兼容的。但是,不允许将float值赋给int数组的元素,因为float与int的赋值不兼容:
int[] sequence = new int[10];
sequence[0] = 10; // OK. Assigning an int 10 to an int
sequence[1] = 19.4f; // A compile-time error. Assigning a float to an int
在处理引用类型数组时,必须遵循相同的规则。如果有一个类型为T的引用类型数组,当且仅当S与T的赋值兼容时,它的元素才能被赋值为类型为S的对象引用。子类对象引用总是与 Java 中所有类的超类赋值兼容。你可以使用一个Object类的数组来存储任何类的对象,例如:
Object[] genericArray = new Object[4];
genericArray[0] = new String("Hello"); // OK
genericArray[1] = new Person("Daniel"); // OK. Assuming Person class exists
genericArray[2] = new Account(189); // OK. Assuming Account class exist
genericArray[3] = null; // Ok. null can be assigned to any reference type
您需要在从数组中读回对象时执行强制转换,如下所示:
/* The compiler will flag an error for the following statement. genericArray is of Object
type and an Object reference cannot be assigned to a String reference variable. Even
though genericArray[0] contains a String object reference, we need to cast it to String
as we do in next statement.
*/
String s = genericArray[0]; // A compile-time error
String str = (String) genericArray[0]; // OK
Person p = (Person) genericArray[1]; // OK
Account a = (Account) genericArray[2]; // OK
如果您试图将数组元素强制转换为一种类型,而这种类型的实际类型与新类型的赋值不兼容,则会抛出java.lang.ClassCastException。例如,下面的语句将在运行时抛出一个ClassCastException:
String str = (String) genericArray[1]; // Person cannot be cast to String
不能将超类的对象引用存储在子类的数组中。以下代码片段说明了这一点:
String[] names = new String[3];
names[0] = new Object(); // A compile-time error. Object is superclass of String
names[1] = new Person(); // A compile-time error. Person is not subclass of String
names[2] = null; // OK.
最后,如果前一种类型与后一种类型在赋值上兼容,则可以将数组引用赋给另一种类型的数组引用:
Object[] obj = new Object[3];
String[] str = new String[2];
Account[] a = new Account[5];
obj = str; // OK
str = (String[]) obj; // OK because obj has String array reference
obj = a;
// A ClassCastException will be thrown. obj has the reference of an Account array and
// an Account cannot be converted to a String
str = (String[]) obj;
a = (Account[]) obj; // OK
将列表转换为数组
当列表中元素的数量不精确时,可以使用一个ArrayList。一旦列表中的元素数量固定,您可能想要将一个ArrayList转换成一个数组。您可以出于以下原因之一执行此操作:
-
程序语义可能要求您使用数组,而不是
ArrayList。例如,您可能需要将一个数组传递给一个方法,但是您将数据存储在一个ArrayList中。 -
您可能希望将用户输入存储在一个数组中。但是,您不知道用户将输入多少个值。在这种情况下,您可以在接受用户输入的同时将值存储在一个
ArrayList中。最后,您可以将ArrayList转换成一个数组。 -
访问数组元素比访问
ArrayList元素快。如果您有一个ArrayList并且想要多次访问元素,您可能想要将ArrayList转换为一个数组以获得更好的性能。
ArrayList类有一个名为toArray()的重载方法:
-
Object[] toArray() -
<T> T[] toArray(T[] a)
第一种方法将ArrayList的元素作为Object的数组返回。第二种方法接受任意类型的数组作为参数。如果有足够的空间,所有的ArrayList元素都被复制到传递的数组中,并返回相同的数组。如果传递的数组中没有足够的空间,则创建一个新的数组。新数组的类型与传递的数组相同。新数组的长度等于ArrayList的大小。清单 19-16 展示了如何将一个ArrayList转换成一个数组。
// ArrayListToArray.java
package com.jdojo.array;
import java.util.ArrayList;
import java.util.Arrays;
public class ArrayListToArray {
public static void main(String[] args) {
ArrayList<String> al = new ArrayList<>();
al.add("cat");
al.add("dog");
al.add("rat");
// Print the content of the ArrayList
System.out.println("ArrayList: " + al);
// Create an array of the same length as the ArrayList
String[] s1 = new String[al.size()];
// Copy the ArrayList elements to the array
String[] s2 = al.toArray(s1);
// s1 has enough space to copy all ArrayList elements.
// al.toArray(s1) returns s1 itself
System.out.println("s1 == s2: " + (s1 == s2));
System.out.println("s1: " + Arrays.toString(s1));
System.out.println("s2: " + Arrays.toString(s2));
// Create an array of string with 1 element.
s1 = new String[1];
s1[0] = "hello"; // Store hello in first element
// Copy ArrayList to the array s1
s2 = al.toArray(s1);
/* Since s1 doesn't have sufficient space to copy all ArrayList elements,
al.toArray(s1) creates a new String array with 3 elements in it. All
elements of arraylist are copied to the new array. Finally, the new array is
returned. Here, s1 == s2 is false. s1 will be untouched by the method call.
*/
System.out.println("s1 == s2: " + (s1 == s2));
System.out.println("s1: " + Arrays.toString(s1));
System.out.println("s2: " + Arrays.toString(s2));
}
}
ArrayList: [cat, dog, rat]
s1 == s2: true
s1: [cat, dog, rat]
s2: [cat, dog, rat]
s1 == s2: false
s1: [hello]
s2: [cat, dog, rat]
Listing 19-16An ArrayList to an Array Conversion
执行数组操作
在日常编程中,您需要执行一些例行的数组操作,如排序、搜索、比较和复制。java.util.Arrays类是一个实用类,包含 150 多个静态便利方法来执行这种类型的数组操作。在您推出自己的代码来执行数组操作之前,请参考Arrays类 API 文档,您可能会找到实现相同功能的方法。
不要被这个类中的大量方法吓倒。它不支持超过 150 种类型的数组操作。在Arrays类中有大量方法的原因是为了支持对所有原始类型和引用类型的数组的相同操作。大多数方法至少有九个重载版本——一个用于八个基元类型数组,一个用于引用类型数组。有时,可以在整个数组或一系列元素上执行操作,这使得一个数组操作的最小方法数加倍,至少达到 18 个。不可能详细介绍每一种方法并提供示例。那将会超过这本书的 100 页。我们根据它们执行的操作类型将所有方法分为不同的类别,并提供了几个例子。表 19-3 列出了这些类别以及在这些类别中执行数组操作的方法的名称。
表 19-3
Arrays 类中的方法及其类别和说明
|种类
|
方法名称
|
描述
|
| --- | --- | --- |
| 转换 | asList() | 返回一个由指定数组支持的固定大小的列表。这个方法只有一个版本。 |
| | stream() | 为所有元素或某个范围的元素返回一个数组的序列流。 |
| | toString() | 返回指定数组内容的字符串表示形式。 |
| | deepToString() | 返回数组“深层内容”的字符串表示形式。适合用于多维数组。 |
| 搜索 | binarySearch() | 允许您使用二分搜索法算法搜索排序后的数组。在传递给此方法之前,必须对数组进行排序;否则,结果不确定。允许在整个数组或数组中的某个元素范围内进行搜索。 |
| 比较 | compare() | 按字典顺序比较两个数组。如果第一个和第二个数组相等并且包含相同顺序的相同元素,则返回 0;如果第一个数组在字典序上小于第二个数组,则返回小于 0 的值;如果第一个数组在字典序上大于第二个数组,则返回大于 0 的值。该方法是在 Java 9 中添加的。 |
| | compareUnsigned() | 工作原理与compare()方法相同,在数字上将元素视为无符号元素。该方法是在 Java 9 中添加的。 |
| | deepEquals() | 如果两个指定数组完全相等,则返回true。 |
| | equals() | 如果两个指定的整数数组彼此相等,则返回true。您可以比较整个数组或数组中某个范围的元素是否相等。该方法是在 Java 9 中添加的。 |
| | mismatch() | 查找并返回两个数组之间第一个不匹配项的索引;否则,如果没有发现不匹配,则返回–1。可以比较两个数组的全部内容或它们的元素范围是否不匹配。该方法是在 Java 9 中添加的。 |
| 复制 | copyOf() | 将一个数组复制到另一个数组。指定新数组的长度。新数组可能小于或大于源数组。如果它更大,则用数组的数据类型的默认值填充附加元素。 |
| | copyOfRange() | 将一系列元素从一个数组复制到另一个数组。 |
| 填充物 | fill() | 允许您为数组中的所有元素或某个范围的元素分配相同的值。 |
| | setAll() | 允许您为数组中的所有元素或某个范围的元素赋值。这些值由生成器函数生成。 |
| 计算哈希代码 | deepHashCode() | 基于数组的“深层内容”返回哈希代码。对于多维数组,所有维度中的内容都包括在哈希代码的计算中。 |
| | hashCode() | 基于指定数组的内容返回哈希代码。 |
| 并行更新 | parallelPrefix() | 使用提供的函数,并行累积数组中的每个元素。 |
| | parallelSetAll() | 使用生成器函数计算每个元素,并行设置指定数组的所有元素。 |
| 整理 | parallelSort() | 使用并行排序对数组中的所有元素或某个范围的元素进行排序。 |
| | sort() | 对数组中的所有元素或某个范围的元素进行排序。 |
| 获取拆分器 | spliterator() | 返回一个包含数组中所有或一系列元素的Spliterator。 |
以下部分向您展示了如何使用其中的一些方法。您需要从java.util包中导入几个类和接口来运行示例代码片段。以下导入将完成这项工作:
import java.util.*;
将数组转换为其他类型
在这个类别中,Arrays类中的方法让您从数组中获得一个List、Stream和String。asList(T... a)方法接受类型为T的 var-args 参数,并返回一个List<T>。以下是几个例子:
// Create a String array
String[] animals = {"rat", "dog", "cat"};
// Convert the array to a List
List<String> animalList = Arrays.asList(animals);
System.out.println("As a List: " + animalList);
// Convert the array to a String
String str = Arrays.toString(animals);
System.out.println("As a String: " + str);
// Get a sorted Stream of the array and print its elements
System.out.println("Sorted Stream of Animals: ");
Arrays.stream(animals)
.sorted()
.forEach(System.out::println);
As a List: [rat, dog, cat]
As a String: [rat, dog, cat]
Sorted Stream of Animals:
cat
dog
rat
搜索数组
使用binarySearch()方法在数组中搜索一个键。必须对数组进行排序,此方法才能起作用。如果搜索关键字包含在数组中,则方法返回该关键字的索引。否则,它返回一个负数,等于
(-(insertion point) - 1)
这里,插入点被定义为索引,在这个索引处,键将被插入到数组中。这保证了如果键不在数组中,返回值是负整数。下面是一个例子:
// Create an array to work with
int[] num = {2, 4, 3, 1};
System.out.println("Original Array: " + Arrays.toString(num));
// Sort the array before using the binary search
Arrays.sort(num);
System.out.println("Array After Sorting: " + Arrays.toString(num)); // Array After Sorting: [1, 2, 3, 4]
int index = Arrays.binarySearch(num, 3);
System.out.println("Found index of 3: " + index); //Found index of 3: 2
index = Arrays.binarySearch(num, 200);
System.out.println("Found index of 200: " + index); // Found index of 200: -5
由于 200 应该放在最后,在索引 4 处,binarySearch 方法返回–4–1,即–5。
比较数组
equals()方法让你比较两个数组是否相等。如果数组或切片中的元素数量相同,并且数组或切片中所有对应的元素对都相等,则认为两个数组相等。
compare()和compareUnsigned()方法按字典顺序比较数组或数组切片中的元素。compareUnsigned()方法将整数值视为无符号的。一个null数组在字典序上小于一个非null数组。两个null数组相等。
mismatch()方法比较两个数组或数组片。The方法返回第一个不匹配的索引。如果没有不匹配,它返回-1。如果任一数组是null,它抛出一个NullPointerException。清单 19-17 包含了一个比较两个数组及其切片的完整程序。该程序使用int数组进行比较。
// ArrayComparison.java
package com.jdojo.array;
import java.util.Arrays;
public class ArrayComparison {
public static void main(String[] args) {
int[] a1 = {1, 2, 3, 4, 5};
int[] a2 = {1, 2, 7, 4, 5};
int[] a3 = {1, 2, 3, 4, 5};
// Print original arrays
System.out.println("Three arrays:");
System.out.println("a1: " + Arrays.toString(a1));
System.out.println("a2: " + Arrays.toString(a2));
System.out.println("a3: " + Arrays.toString(a3));
// Compare arrays for equality
System.out.println("\nComparing arrays using equals() method:");
System.out.println("Arrays.equals(a1, a2): " + Arrays.equals(a1, a2));
System.out.println("Arrays.equals(a1, a3): " + Arrays.equals(a1, a3));
System.out.println("Arrays.equals(a1, 0, 2, a2, 0, 2): "
+ Arrays.equals(a1, 0, 2, a2, 0, 2));
// Compare arrays lexicographically
System.out.println("\nComparing arrays using compare() method:");
System.out.println("Arrays.compare(a1, a2): " + Arrays.compare(a1, a2));
System.out.println("Arrays.compare(a2, a1): " + Arrays.compare(a2, a1));
System.out.println("Arrays.compare(a1, a3): " + Arrays.compare(a1, a3));
System.out.println("Arrays.compare(a1, 0, 2, a2, 0, 2): "
+ Arrays.compare(a1, 0, 2, a2, 0, 2));
// Find the mismatched index in arrays
System.out.println("\nFinding mismatch using the mismatch() method:");
System.out.println("Arrays.mismatch(a1, a2): " + Arrays.mismatch(a1, a2));
System.out.println("Arrays.mismatch(a1, a3): " + Arrays.mismatch(a1, a3));
System.out.println("Arrays.mismatch(a1, 0, 5, a2, 0, 1): "
+ Arrays.mismatch(a1, 0, 5, a2, 0, 1));
}
}
Three arrays:
a1: [1, 2, 3, 4, 5]
a2: [1, 2, 7, 4, 5]
a3: [1, 2, 3, 4, 5]
Comparing arrays using equals() method:
Arrays.equals(a1, a2): false
Arrays.equals(a1, a3): true
Arrays.equals(a1, 0, 2, a2, 0, 2): true
Comparing arrays using compare() method:
Arrays.compare(a1, a2): -1
Arrays.compare(a2, a1): 1
Arrays.compare(a1, a3): 0
Arrays.compare(a1, 0, 2, a2, 0, 2): 0
Finding mismatch using the mismatch() method:
Arrays.mismatch(a1, a2): 2
Arrays.mismatch(a1, a3): -1
Arrays.mismatch(a1, 0, 5, a2, 0, 1): 1
Listing 19-17Comparing Arrays and Array Slices Using the Arrays Class Methods
复制数组
copyOf()方法允许您通过为新数组指定新长度来复制原始数组的元素。如果新数组的长度大于原始数组的长度,则根据数组的类型为附加元素分配一个默认值。copyOfRange()方法允许您将一个数组的一部分复制到另一个数组中。这里有一个例子:
// Create an array to work with
int[] nums = {2, 4, 3, 1};
System.out.println("Original Array: " + Arrays.toString(nums));
// Copy of the truncated num to 2 elements
int[] numCopy1 = Arrays.copyOf(nums, 2);
System.out.println("Truncated Copy: " + Arrays.toString(numCopy1));
// Copy of the extended num to 6 elements
int[] numCopy2 = Arrays.copyOf(nums, 6);
System.out.println("Extended Copy: " + Arrays.toString(numCopy2));
// Copy of the range index 2 (inclusive) to 4 (exclusive)
int[] numCopy3 = Arrays.copyOfRange(nums, 2, 4);
System.out.println("Range Copy: " + Arrays.toString(numCopy3));
Original Array: [2, 4, 3, 1]
Truncated Copy: [2, 4]
Extended Copy: [2, 4, 3, 1, 0, 0]
Range Copy: [3, 1]
填充数组
您可以使用fill()方法用相同的值填充数组的所有元素或元素范围。setAll()方法允许您使用函数为数组中的所有元素设置值。向该函数传递数组的索引,它返回该索引处元素的值。以下是使用这两种方法的示例:
// Create an array to work with
int[] num = {2, 4, 3, 1};
System.out.println("Original Array: " + Arrays.toString(num));
// Fill elements of the array with 10
Arrays.fill(num, 10);
System.out.println("Array filled with 10: " + Arrays.toString(num));
// Fill elements of the array with a value (index + 1) * 10
Arrays.setAll(num, index -> (index + 1) * 10);
System.out.println("Array filled with a function: " + Arrays.toString(num));
Original Array: [2, 4, 3, 1]
Array filled with 10: [10, 10, 10, 10]
Array filled with a function: [10, 20, 30, 40]
计算哈希代码
使用hashCode()方法根据数组元素的值计算数组的散列码。如果传入的数组是null,方法返回 0。对于任意两个阵列a1和a2使得Arrays.equals (a1, a2),对于Arrays.hashCode(a1) == Arrays.hashCode(a2)也是如此。这里有一个例子:
// Create an array to work with
int[] num = {2, 4, 3, 1};
System.out.println("Array: " + Arrays.toString(num));
// Compute the hash code of the array
int hashCode = Arrays.hashCode(num);
System.out.println("Hash Code: " + hashCode);
Array: [2, 4, 3, 1]
Hash Code: 987041
执行并行累加
int[] num = {2, 4, 3, 1};
System.out.println("Before: " + Arrays.toString(num));
Arrays.parallelPrefix(num, (n1, n2) -> n1 * n2);
System.out.println("After: " + Arrays.toString(num));
Before: [2, 4, 3, 1]
After: [2, 8, 24, 24]
排序数组
使用sort()和parallelSort()方法对数组元素进行排序。前者适用于较小的阵列,后者适用于较大的阵列。这里有几个例子:
// Create an array to work with
int[] num1 = {2, 4, 3, 1};
System.out.println("Original Array: " + Arrays.toString(num1));
// Sort the array
Arrays.sort(num1);
System.out.println("Using sort(): " + Arrays.toString(num1));
// Create an array to work with
int[] num2 = {2, 4, 3, 1};
System.out.println("Original Array: " + Arrays.toString(num2));
// Sort the array
Arrays.parallelSort(num2);
System.out.println("Using parallelSort(): " + Arrays.toString(num2));
Original Array: [2, 4, 3, 1]
Using sort(): [1, 2, 3, 4]
Original Array: [2, 4, 3, 1]
Using parallelSort(): [1, 2, 3, 4]
摘要
数组是存储同一类型的多个数据值的数据结构。所有数组元素在内存中都被分配了连续的空间。使用数组元素的索引来访问数组元素。数组使用从零开始的索引。第一个元素的索引为零。每个数组都有一个名为length的属性,它包含数组中元素的数量。数组的长度可以为零。
Java 中的数组是对象。Java 支持定长数组。也就是说,一旦创建了数组,它的长度就不能更改。如果你需要一个可变长度的数组,使用一个ArrayList。ArrayList类提供了一个toArray()方法来将其元素转换成数组。Java 支持数组的数组形式的多维数组。您可以使用clone()方法克隆一个数组。该方法对引用数组执行浅层克隆。
java.util包中的Arrays类包含了几个静态的便利方法,让你可以对一个数组执行许多不同类型的操作,比如搜索、排序、比较、填充等等。
EXERCISES
-
什么是数组?命名数组的属性,该属性给出数组中元素的数目。
-
数组第一个元素的索引是什么?
-
编写代码以两种方式初始化一个
int数组。该数组包含元素10、20和30: -
您必须在数组中存储值,但是您事先不知道元素的数量。在这种情况下,你将如何编码,以便最终得到一个数组中的所有元素?
-
完成以下代码片段,打印数组对象的类名:
String[] names = {"Corky", "Bryce", "Paul", "Tony"}; String className = names./* Your code goes here */; System.out.println("Class Name: " + className); -
考虑下面这个名为
test()的方法声明,它采用一个int[]数组作为参数:public static void test(int[] num) { if(num.length > 0) { num[0] = 100; } num = new int[]{1000, 2000}; }在执行下面的代码时写出输出:
int[] num = {2, 4, 3, 1}; System.out.println("num[0] = " + num[0]); test(num); System.out.println("num[0] = " + num[0]); -
下面哪个语句声明了一个二维
int数组?int[][] y; int z[][]; int[] x[]; int[] x = {2, 2}; -
声明一个名为
table的三行三列的二维数组。演示如何在声明和使用for循环的过程中用值10初始化数组的所有元素。 -
Consider the following declaration for an array:
int[] x = {10, 20, 30, 40};编写一个
for循环和一个for-each循环,在标准输出的一行中打印数组中每个元素的值。 -
Consider the following declaration for an array:
```java
int[] x = {10, 20, 30, 40};
System.out.println(x[5]);
```
当这段代码被执行时会发生什么?
11. 你将使用Arrays类的什么方法对一个大数组进行排序:sort()还是parallelSort()?
-
在
Arrays类中命名将数组转换为其字符串表示的方法。 -
Arrays类包含一个binarySearch()方法,允许您在数组中搜索一个值。在使用binarySearch()方法之前,阵列必须满足什么条件? -
编写并解释以下代码片段的输出:
```java
int[][] table1 = {{1, 2, 3}, {10, 20, 30}};
int[][] table2 = {{1, 2, 3}, {10, 20, 30}};
boolean equal1 = Arrays.equals(table1, table2);
boolean equal2 = Arrays.deepEquals(table1, table2);
System.out.println(equal1);
System.out.println(equal2);
```
15. 考虑下面的代码片段,它将一个名为table1的二维数组的内容复制到另一个名为table2的二维数组中。帮助这段代码的作者完成缺失的逻辑。您需要编写两行代码:
```java
int[][] table1 = {{1, 2, 3}, {10, 20, 30}};
int[][] table2 = new int[table1.length][];
// Complete missing logic
for(int i = 0; i < table1.length; i++) {
/* Your one line code goes here */
for(int j = 0; j < table1[i].length; j++) {
/* Your one line code goes here */
}
}
boolean equal = Arrays.deepEquals(table1, table2);
System.out.println(equal);
System.out.println(Arrays.deepToString(table1));
System.out.println(Arrays.deepToString(table2));
```
这段代码应该有以下输出:
```java
true
[[1, 2, 3], [10, 20, 30]]
[[1, 2, 3], [10, 20, 30]]
```