1. String 类型的数组
下面简单介绍一下 String 类型的数组。
正如我们已经说过的,数组可以是任何类型。因此也可以创建 String 类型的数组。下面是一个程序示例:它“从键盘输入10行字符串,并以相反的顺序将它们输出到屏幕”。
String[] array = new String[10]; // 创建一个包含 10 个元素的数组对象
for (int i = 0; i < 10; i++) // 从 0 到 9 的循环
{
array[i] = console.nextLine(); // 从键盘读取一行并保存到数组的对应单元
}
for (int i = 9; i >= 0; i--) // 从 9 到 0 的循环
{
System.out.println(array[i]); // 将当前数组单元输出到屏幕
}
代码几乎没有变化!只是把创建数组时的类型从 int 换成了 String。其余部分完全一样!
认识 null
来个小谜题——这些数组在刚创建后,它们的各个单元里分别是什么?
int[] numbers = new int[10];
String[] strings = new String[10];
User[] users = new User[10];
想一想,很可能 numbers 的单元里是 0。没错。
照此逻辑,strings 的单元应该填充为空字符串——""。姑且算对。
那么 users 数组的单元里又是什么?空的 User 吗?
关键就在这里,并不是每种数据类型都有“默认值”。于是 Java 设计了一个特殊常量——null。null 是空引用。 当为对象类型创建变量时,它的初始值就是 null:不指向任何对象,即没有引用。
String name; // name 中是 null
name = "Alex"; // name 保存指向对象/字符串 "Alex" 的引用
name = null; // name 中是 null
处理 null
当引用为 null 时,不能对对象调用方法——因为对象并不存在!如下代码会以错误结束:
String name; // name 中是 null
String upperName = name.toUpperCase(); // 将抛出 NullPointerException:name == null
回到刚才的“谜题”,答案如下:
int[] numbers = new int[10]; // {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
String[] strings = new String[10]; // {null, null, null, null, null, null, null, null, null, null}
User[] users = new User[10]; // {null, null, null, null, null, null, null, null, null, null}
只有基本类型有“默认值”。其他所有类型默认值都是 null。String 的初始值不是空字符串。事实如此。
2. 内存中的 String 类型数组
图 1. String 对象在内存中的布局:

请注意,字符串文本并不是直接存放在变量里:它有自己单独的内存块。而在 String 类型的变量中,存放的是指向该文本对象的地址(引用)。
图 2. 整型数组在内存中的布局:

这张图你们之前也见过。
图 3. 字符串数组在内存中的布局:

左侧是一个 String[] 变量(保存数组对象的地址)。
中间是数组对象(String 类型的数组)。
右侧是字符串对象,它们存放具体的文本。
在 String 类型的数组对象的各个单元中,存放的不是字符串(文本)本身,而是它们的地址(引用)。这与 String 类型变量里存放字符串(文本)的地址是一样的。
3. Java 中的数组快速初始化
数组非常有用,所以 Java 设计者尽量让对数组的操作更便捷。首先,他们简化了数组的初始化,也就是填入初始值。
很多时候,除了程序从某处读取的数据之外,它还需要自己的内部数据。例如,我们需要在数组中保存每个月的天数。代码可能是这样:
int[] months = new int[12];
months[0] = 31; // 一月
months[1] = 28; // 二月
months[2] = 31; // 三月
months[3] = 30; // 四月
months[4] = 31; // 五月
months[5] = 30; // 六月
months[6] = 31; // 七月
months[7] = 31; // 八月
months[8] = 30; // 九月
months[9] = 31; // 十月
months[10] = 30; // 十一月
months[11] = 31; // 十二月
但有一种更简洁的写法——感谢 Java 的设计者:
// 一年中每个月的天数
int[] months = new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
你可以直接用逗号列出数组的所有值!
编译器可以根据数组变量的类型推断出容器(数组对象)的类型;而数组长度则可以通过统计花括号中元素的数量来确定。
因此,这段代码还能写得更短:
// 一年中每个月的天数
int[] months = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
是不是很优雅?🙂 这种写法称为“数组的快速初始化”。顺便说一句,它不仅适用于 int 类型……
// 一年中各月份的名称
String[] months = { "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月" };
4. for-each 循环
遍历数组的所有元素是非常常见的操作,因此专门有一种特殊循环——for-each。它的写法如下:
for (int score : scores)
{
System.out.println("分数:" + score);
}
这里循环会自动遍历元素——无需手动索引。但不能直接修改元素(循环变量里只有值的副本),不过用于简单地“走一遍”数组非常方便。
编译器会把 for-each 循环转换为下面的代码:
for (int i = 0; i < scores.length; i++)
{
int score = scores[i];
System.out.println("分数:" + score);
}
所以没有什么魔法。这也解释了为什么你不能通过变量 score 来修改数组元素。
再写一个示例:求所有分数之和。
int sum = 0;
for (int score : scores)
{
sum += score;
}
System.out.println("所有分数之和:" + sum);
何时使用哪种循环?
- 如果需要索引或需要修改元素——使用 for 循环。
- 如果只是遍历取值——for-each 更快也更简洁。
5. 修改数组元素
如果我们需要把每个分数都增加 1 分(比如因为表现良好),这时就需要带索引的经典 for,只有这样才能修改数组中的值。
示例:将每个元素加 1
for (int i = 0; i < grades.length; i++) {
grades[i] = grades[i] + 1;
}
执行完这段代码后,数组 grades 中将是新的数值。
为什么不能用 for-each 这么做?
for (int grade : grades) {
grade = grade + 1; // 不起作用!
}
这段代码不会修改数组,因为 grade 是数组中值的副本,而不是对元素本身的引用。
6. 基于数组的计算
数组经常用于各种计算:求和、平均值、最大值、最小值等。
示例:求数组所有元素之和
int sum = 0;
for (int i = 0; i < grades.length; i++) {
sum += grades[i]; // 等价于 sum = sum + grades[i];
}
System.out.println("分数总和:" + sum);
示例:寻找最大值
int max = grades[0]; // 从第一个元素开始
for (int i = 1; i < grades.length; i++) {
if (grades[i] > max) {
max = grades[i];
}
}
System.out.println("最高分:" + max);
示例:寻找最小值
int min = grades[0]; // 从第一个元素开始
for (int i = 1; i < grades.length; i++) {
if (grades[i] < min) {
min = grades[i];
}
}
System.out.println("最低分:" + min);
示例:计算算术平均值
int sum = 0;
for (int i = 0; i < grades.length; i++) {
sum += grades[i];
}
double average = (double) sum / grades.length; // 必须强制转换为 double!
System.out.println("平均分:" + average);
注意:为了得到小数结果,需要在除法前将总和显式转换为 double 类型。
7. 实战练习
从键盘读取数组
很多时候需要用用户输入来填充数组。这时使用 Scanner 和循环即可。
Scanner console = new Scanner(System.in);
int n = 5; // 数组大小
int[] numbers = new int[n];
System.out.println("请输入 " + n + " 个数字:");
for (int i = 0; i < n; i++) {
numbers[i] = console.nextInt();
}
System.out.println("你输入的是:");
for (int i = 0; i < n; i++) {
System.out.println(numbers[i]);
}
逆序输出数组
有时需要从末尾开始输出数组元素(例如想知道谁最后走进了房间)。
for (int i = grades.length - 1; i >= 0; i--) {
System.out.println("第" + (i + 1) + "个分数:" + grades[i]);
}
8. 一维数组操作中的常见错误
错误 1:越界访问数组
最常见的问题是尝试访问不存在的元素。在 Java 中这会导致抛出 ArrayIndexOutOfBoundsException 异常。记住:最后一个合法索引是 arr.length - 1。
int[] arr = new int[5];
System.out.println(arr[5]); // 错误!有效索引为 0 到 4。
错误 2:忘记初始化数组
只声明了变量,却没有创建数组:
int[] arr;
arr[0] = 5; // 错误!数组尚未创建。
必须使用 new 来创建数组:
arr = new int[10];
错误 3:试图通过 for-each 修改数组元素
在 for-each 循环中,循环变量是值的副本,而不是元素的引用:
for (int x : arr) {
x = 100; // 不会修改数组!
}
要修改值,请使用带索引的普通 for 循环。
错误 4:错误使用数组长度
有时会把数组长度与最后一个索引混淆:
for (int i = 0; i <= arr.length; i++) { // 错误!应该写 i < arr.length
// ...
}
这样的代码会导致越界。
错误 5:隐式类型转换不当
如果数组类型是 int,就不能直接给它赋一个 double 类型的值:
int[] arr = new int[3];
arr[0] = 3.14; // 错误!3.14 是 double。
GO TO FULL VERSION