有 C 或 FORTRAN 等语言编程经验的人都熟悉数组的概念。 它们基本上是连续的内存块,其中每个位置都是某种类型:整数、浮点数或任何其他类型。
Java 中的情况类似,但有一些额外的细节。
一个示例数组
让我们在 Java 中创建一个包含 10 个整数的数组
int[] ia = new int[10];
上面的代码片段发生了什么? 从左到右
-
最左边的 int[] 将变量的类型声明为 int 数组(用 [] 表示)。
-
右边是变量的名称,在本例中为 ia。
-
接下来,= 告诉我们左侧定义的变量设置为右侧的内容。
-
在 = 的右侧,我们看到单词 new,它在 Java 中表示正在初始化一个对象,这意味着分配存储空间并调用其构造函数(有关更多信息,请参见此处)。
-
接下来,我们看到 int[10],它告诉我们正在初始化的特定对象是一个包含 10 个整数的数组。
由于 Java 是强类型语言,因此变量 ia 的类型必须与 = 右侧表达式的类型兼容。
初始化示例数组
让我们将这个简单的数组放入一段代码中并试用一下。 将以下内容保存在名为 Test1.java 的文件中,使用 javac 编译它,并使用 java 运行它(当然是在终端中)
import java.lang.*;
public class Test1 {
public static void main(String[] args) {
int[] ia = new int[10]; // See note 1 below
System.out.println("ia is " + ia.getClass()); // See note 2 below
for (int i = 0; i < ia.length; i++) // See note 3 below
System.out.println("ia[" + i + "] = " + ia[i]); // See note 4 below
}
}
让我们了解最重要的部分。
- 我们对包含 10 个整数的数组 ia 的声明和初始化很容易发现。
- 在紧随其后的行中,我们看到表达式 ia.getClass()。 是的,ia 是一个属于类的对象,此代码将让我们知道该类是什么。
- 在后面的下一行中,我们看到循环的开始 for (int i = 0; i < ia.length; i++),它定义了一个循环索引变量 i,该变量运行一个从零到小于 ia.length 的序列,这是一个告诉我们数组 ia 中定义了多少个元素的表达式。
- 接下来,循环的主体打印出 ia 的每个元素的值。
当编译并运行此程序时,它会产生以下结果
me@mydesktop:~/Java$ javac Test1.java
me@mydesktop:~/Java$ java Test1
ia is class [I
ia[0] = 0
ia[1] = 0
ia[2] = 0
ia[3] = 0
ia[4] = 0
ia[5] = 0
ia[6] = 0
ia[7] = 0
ia[8] = 0
ia[9] = 0
me@mydesktop:~/Java$
ia.getClass() 的输出的字符串表示形式为 [I,它是“整数数组”的简写。 与 C 编程语言类似,Java 数组从元素零开始,一直延伸到元素 <数组大小> – 1。 我们可以看到,ia 的每个元素都设置为零(似乎是由数组构造函数设置的)。
那么,就是这样吗? 我们声明类型,使用适当的初始化程序,然后我们就完成了?
好吧,不是。 在 Java 中还有许多其他初始化数组的方法。
无论如何,我为什么要初始化数组?
这个问题的答案,就像所有好问题一样,是“视情况而定”。 在这种情况下,答案取决于我们希望在初始化数组后如何使用它。
在某些情况下,数组自然而然地成为一种累加器。 例如,假设我们正在编写代码来计算小型办公室中一组电话分机接听和拨打的电话数量。 有八个分机,编号为一到八,以及操作员的分机,编号为零。 因此,我们可能会声明两个数组
int[] callsMade;
int[] callsReceived;
然后,每当我们开始一个新的通话统计信息累积周期时,我们将每个数组初始化为
callsMade = new int[9];
callsReceived = new int[9];
在每个通话统计信息累积周期结束时,我们可以打印出统计信息。 粗略地说,我们可能会看到
import java.lang.*;
import java.io.*;
public class Test2 {
public static void main(String[] args) {
int[] callsMade;
int[] callsReceived;
// initialize call counters
callsMade = new int[9];
callsReceived = new int[9];
// process calls...
// an extension makes a call: callsMade[ext]++
// an extension receives a call: callsReceived[ext]++
// summarize call statistics
System.out.printf("%3s%25s%25s\n","ext"," calls made",
"calls received");
for (int ext = 0; ext < callsMade.length; ext++)
System.out.printf("%3d%25d%25d\n",ext,
callsMade[ext],callsReceived[ext]);
}
}
这将产生类似以下的输出
me@mydesktop:~/Java$ javac Test2.java
me@mydesktop:~/Java$ java Test2
ext calls made calls received
0 0 0
1 0 0
2 0 0
3 0 0
4 0 0
5 0 0
6 0 0
7 0 0
8 0 0
me@mydesktop:~/Java$
呼叫中心里非常清闲的一天。
在上面累加器的示例中,我们看到数组初始化程序设置的零作为起始值对于我们的需求来说是令人满意的。 但在其他情况下,此起始值可能不是正确的选择。
例如,在某些类型的几何计算中,我们可能需要将二维数组初始化为单位矩阵(除主对角线上的 1 之外,所有元素均为零)。 我们可以选择这样做
double[][] m = new double[3][3];
for (int d = 0; d < 3; d++)
m[d][d] = 1.0;
在这种情况下,我们依靠数组初始化程序 new double[3][3] 将数组设置为零,然后使用循环将对角线元素设置为一。 在这种简单的情况下,我们可以使用 Java 提供的快捷方式
double[][] m = {
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0}};
这种类型的可视结构在此类应用程序中特别适用,它可以是查看数组实际布局的有用双重检查。 但在行数和列数仅在运行时确定的情况下,我们可能会看到类似这样的情况
int nrc;
// some code determines the number of rows & columns = nrc
double[][] m = new double[nrc][nrc];
for (int d = 0; d < nrc; d++)
m[d][d] = 1.0;
值得一提的是,Java 中的二维数组实际上是一个数组的数组,没有什么能阻止大胆的程序员让每个二级数组的长度都不同。 也就是说,像这样的东西是完全合法的
int [][] differentLengthRows = {
{ 1, 2, 3, 4, 5},
{ 6, 7, 8, 9},
{10,11,12},
{13,14},
{15}};
有各种线性代数应用涉及不规则形状的矩阵,可以在其中应用这种类型的结构(有关更多信息,请参见 此 Wikipedia 文章 作为起点)。 除此之外,既然我们了解了二维数组实际上是一个数组的数组,那么
differentLengthRows.length
告诉我们二维数组 differentLengthRows 中的行数,并且
differentLengthRows[i].length
告诉我们 differentLengthRows 的第 i 行中的列数。
进一步了解数组
考虑到这种在运行时确定数组大小的想法,我们看到数组仍然要求我们在实例化它们之前知道该大小。 但是,如果我们在处理完所有数据之前都不知道大小该怎么办? 这是否意味着我们必须先处理一次数据以确定数组的大小,然后再处理一次数据? 这可能很难做到,尤其是在我们只有一次机会使用数据的情况下。
Java 集合框架 以一种很好的方式解决了这个问题。 其中提供的一种类是 ArrayList,它类似于数组但可以动态扩展。 为了演示 ArrayList 的工作原理,让我们创建一个并将其初始化为前 20 个 斐波那契数
import java.lang.*;
import java.util.*;
public class Test3 {
public static void main(String[] args) {
ArrayList<Integer> fibos = new ArrayList<Integer>();
fibos.add(0);
fibos.add(1);
for (int i = 2; i < 20; i++)
fibos.add(fibos.get(i-1) + fibos.get(i-2));
for (int i = 0; i < fibos.size(); i++)
System.out.println("fibonacci " + i +
" = " + fibos.get(i));
}
}
在上面,我们看到
- 用于存储 Integer 的 ArrayList 的声明和实例化。
- 使用 add() 追加到 ArrayList 实例。
- 使用 get() 按索引号检索元素。
- 使用 size() 确定 ArrayList 实例中已经有多少个元素。
未显示的是 put() 方法,该方法将值放置在给定的索引号处。
此程序的输出为
fibonacci 0 = 0
fibonacci 1 = 1
fibonacci 2 = 1
fibonacci 3 = 2
fibonacci 4 = 3
fibonacci 5 = 5
fibonacci 6 = 8
fibonacci 7 = 13
fibonacci 8 = 21
fibonacci 9 = 34
fibonacci 10 = 55
fibonacci 11 = 89
fibonacci 12 = 144
fibonacci 13 = 233
fibonacci 14 = 377
fibonacci 15 = 610
fibonacci 16 = 987
fibonacci 17 = 1597
fibonacci 18 = 2584
fibonacci 19 = 4181
也可以通过其他技术初始化 ArrayList 实例。 例如,可以将数组提供给 ArrayList 构造函数,或者当在编译时已知初始元素时,可以使用 List.of() 和 Arrays.asList() 方法。 我发现自己不经常使用这些选项,因为我对 ArrayList 的主要用例是当我只想读取一次数据时。
此外,ArrayList 实例可以使用其 toArray() 方法转换为数组,对于那些喜欢在加载数据后使用数组的人;或者,回到当前主题,一旦初始化 ArrayList 实例。
Java 集合框架提供了另一种类似数组的数据结构,称为 Map。 我所说的“类似数组”是指 Map 定义了一个对象集合,可以通过键设置或检索其值,但与数组(或 ArrayList)不同,此键不必是整数; 它可以是 String 或任何其他复杂对象。
例如,我们可以创建一个键是 String 且值是 Integer 的 Map,如下所示
Map<String,Integer> stoi = new Map<String,Integer>();
然后我们可以按如下方式初始化此 Map
stoi.set("one",1);
stoi.set("two",2);
stoi.set("three",3);
等等。 稍后,当我们想知道 "three" 的数值时,我们可以将其检索为
stoi.get("three");
在我的世界中,Map 可用于将第三方数据集中的字符串转换为我的数据集中的一致代码值。 作为 数据转换管道 的一部分,我经常构建一个小型的独立程序来清理数据,然后再进行处理; 为此,我几乎总是使用一个或多个 Map。
值得一提的是,拥有 ArrayList 的 ArrayLists 和 Map 的 Map 是完全可能的,有时也是合理的。 例如,假设我们正在研究树木,并且我们有兴趣累积按树种和年龄段划分的树木数量的计数。 假设年龄段定义是一组字符串值(“young”、“mid”、“mature”和“old”),并且物种是诸如“Douglas fir”、“western red cedar”等字符串值,那么我们可以定义一个 Map 的 Map,如下所示
Map<String,Map<String,Integer>> counter =
new Map<String,Map<String,Integer>>();
这里需要注意的一点是,以上代码仅为 Map 的行创建了存储空间。因此,我们的累积代码可能如下所示:
// assume at this point we have figured out the species
// and age range
if (!counter.containsKey(species))
counter.put(species,new Map<String,Integer>());
if (!counter.get(species).containsKey(ageRange))
counter.get(species).put(ageRange,0);
此时,我们可以开始累积,如下所示:
counter.get(species).put(ageRange,
counter.get(species).get(ageRange) + 1);
最后,值得一提的是,Java 8 中新增的 Streams 工具也可以用于初始化数组、ArrayList 实例和 Map 实例。关于此功能的一个不错的讨论可以在这里和这里找到。
评论已关闭。