CodeGym /课程 /JAVA 25 SELF /一维数组的基本操作

一维数组的基本操作

JAVA 25 SELF
第 7 级 , 课程 2
可用

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 设计了一个特殊常量——nullnull 是空引用。 当为对象类型创建变量时,它的初始值就是 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}

只有基本类型有“默认值”。其他所有类型默认值都是 nullString 的初始值不是空字符串。事实如此。

2. 内存中的 String 类型数组

图 1. String 对象在内存中的布局:

内存中的 String 类型数组

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

图 2. 整型数组在内存中的布局:

内存中的 int 数组

这张图你们之前也见过。

图 3. 字符串数组在内存中的布局:

String 数组在内存中的布局

左侧是一个 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-each 循环转换为下面的代码:

for (int i = 0; i < scores.length; i++)
{
    int score = scores[i];
    System.out.println("分数:" + score);
}
for-each 的“底层”是如何工作的

所以没有什么魔法。这也解释了为什么你不能通过变量 score 来修改数组元素。

再写一个示例:求所有分数之和。

int sum = 0;
for (int score : scores)
{
    sum += score;
}
System.out.println("所有分数之和:" + sum);
通过 for-each 计算数组元素之和

何时使用哪种循环?

  • 如果需要索引或需要修改元素——使用 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。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION