Skip to content

数组 ​

S1-003必学入门约 60 分钟JDK 21编程语言基础 · 语法基础

1. 本节定位 ​

数组是 Java 里第一个容器:把多个同类型的数据组织在一起,用下标访问。

它也是第一个引用类型容器——数组变量里存的不是数据本身,而是数据在内存中的地址。这一点决定了数组的赋值、传参行为,为后面学集合与对象打好基础。

2. 学习目标 ​

完成本节后,你应该能够:

  • 用三种方式声明并初始化数组,说清 length 与下标的关系;
  • 解释为什么 new int[3] 的元素是 0,而 new String[3] 的元素是 null;
  • 用增强 for 与下标 for 遍历数组,并知道各自适用场景;
  • 使用 Arrays 工具类完成排序、查找、复制、输出;
  • 解释 int[] alias = numbers; 之后修改 alias 为什么会影响 numbers;
  • 说出数组的最大局限,以及它被什么替代。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

只有一个成绩时用 int score 就够了;有 50 个成绩时:

  • 定义 50 个变量?无法循环处理,求和要写 50 行;
  • 用数组 int[] scores = new int[50]?一行解决,配合循环轻松求和。

数组带来的不只是"少写变量",而是可循环处理——这是从"写几句代码"到"处理数据"的分水岭。

同时,数组的局限(长度固定)也直接解释了为什么需要集合(《List、Set、Map》)。

5. 核心概念 ​

5.1 三种初始化方式 ​

java
int[] a = new int[3];              // 只定长度,元素为默认值 [0, 0, 0]
int[] b = {1, 2, 3};               // 声明时直接初始化
int[] c = new int[]{4, 5, 6};      // 完整写法,可用于重新赋值

推荐把 [] 写在类型后面(int[] a),而不是变量名后面(int a[])——前者一眼能看出"类型是 int 数组"。

5.2 长度、下标与默认值 ​

java
int[] numbers = {5, 2, 9, 1, 7};
// 下标:           0  1  2  3  4
// numbers.length == 5

三条必须记住的规则:

  1. 下标从 0 开始,最后一个元素的下标是 length - 1;
  2. length 是属性(不带括号),字符串是 length() 方法(带括号)——这两个经常被搞混;
  3. 访问越界会抛 ArrayIndexOutOfBoundsException,这是运行时异常,编译器不会提前发现。

5.3 默认值:为什么 new int[3] 里是 0 ​

java
int[] numbers = new int[3];        // [0, 0, 0]
String[] names = new String[2];    // [null, null]

数组在创建时,元素会被自动初始化为类型的默认值(基本类型为 0 / false,引用类型为 null)。这与成员变量的规则一致(《变量与数据类型》)。

实用推论:拿到一个 String[] 就直接调 .length() 会 NPE——因为元素可能是 null:

java
String[] names = new String[2];
System.out.println(names[0].length());   // NullPointerException

5.4 数组是引用类型 ​

text
栈                         堆
numbers ────────────────▶ [5, 2, 9, 1, 7]
alias   ────────────────▶ (同一个数组对象)
java
int[] numbers = {5, 2, 9, 1, 7};
int[] alias = numbers;      // 复制的是地址,不是数组
alias[0] = 100;             // 通过 alias 修改
System.out.println(numbers[0]);   // 100 —— numbers 也变了

这与 《类与对象》 里对象引用的行为完全一致。"变量里存的是地址"这个认知,是理解 Java 传参和对象共享的关键。

5.5 遍历:两种 for 的选择 ​

java
int[] numbers = {5, 2, 9, 1, 7};

// 增强 for:只读遍历,代码简洁
for (int value : numbers) {
    System.out.print(value + " ");
}

// 下标 for:需要下标或需要修改元素时
for (int i = 0; i < numbers.length; i++) {
    numbers[i] = numbers[i] * 2;
}

选择标准:只读用增强 for;要下标或要改元素用下标 for。

5.6 Arrays 工具类 ​

数组本身没有多少方法,常用功能都在 java.util.Arrays 里:

方法作用注意
Arrays.toString(arr)打印数组内容直接 println(arr) 只会打印地址
Arrays.sort(arr)排序就地修改,返回 void
Arrays.binarySearch(arr, key)二分查找必须先排序,返回下标或负数
Arrays.copyOf(arr, newLength)复制/扩容长度可大于原数组(补默认值)
Arrays.equals(a, b)比较内容数组的 == 比的是地址
Arrays.deepToString(arr)打印二维数组二维数组必须用它

binarySearch 找不到时的返回值是 -(插入点) - 1,所以可能是负数——这不是错误码,而是"如果插入应该放哪"的提示。

6. 递进示例 ​

6.1 最小示例:声明、访问、遍历 ​

java
int[] scores = {90, 85, 77};
int total = 0;
for (int score : scores) {
    total += score;
}
System.out.println("平均分: " + (double) total / scores.length);

6.2 业务示例:统计与查找 ​

java
int[] numbers = {5, 2, 9, 1, 7};
Arrays.sort(numbers);                                  // [1, 2, 5, 7, 9]
int index = Arrays.binarySearch(numbers, 9);           // 4
int missing = Arrays.binarySearch(numbers, 100);       // -6(不存在)

-6 的含义:如果要把 100 插入,应该放在下标 5 的位置,而返回值是 -(5) - 1 = -6。判断存在性时要用 >= 0,不能判断"是否等于 -1"。

6.3 反例:浅拷贝陷阱 ​

java
int[][] matrix = {{1, 2}, {3, 4}};
int[][] shallow = matrix.clone();      // 只复制了"外层数组",内层数组仍是同一个
shallow[0][0] = 99;
System.out.println(matrix[0][0]);      // 99 —— 原数组也变了

结论:二维数组的 clone() 与 Arrays.copyOf() 都是浅拷贝。需要真正独立时,要逐行复制:

java
int[][] deep = new int[matrix.length][];
for (int i = 0; i < matrix.length; i++) {
    deep[i] = Arrays.copyOf(matrix[i], matrix[i].length);
}

7. 完整可运行代码 ​

文件:labs/java-syntax/src/main/java/cn/kb/learn/syntax/ArraysLesson.java

java
package cn.kb.learn.syntax;

import java.util.Arrays;

public class ArraysLesson {
    public static void main(String[] args) {
        // 1. 声明、初始化与访问
        int[] numbers = {5, 2, 9, 1, 7};
        System.out.println("长度: " + numbers.length);
        System.out.println("首尾元素: " + numbers[0] + " / " + numbers[numbers.length - 1]);

        // 2. 排序与输出
        Arrays.sort(numbers);
        System.out.println("排序后: " + Arrays.toString(numbers));

        // 3. 二分查找(前提:数组已排序)
        System.out.println("9 的下标: " + Arrays.binarySearch(numbers, 9));
        System.out.println("不存在的值返回负数: " + Arrays.binarySearch(numbers, 100));

        // 4. 复制与扩容
        int[] firstThree = Arrays.copyOf(numbers, 3);
        System.out.println("前三个元素: " + Arrays.toString(firstThree));

        // 5. 聚合运算
        System.out.println("求和: " + sum(numbers));
        System.out.println("最大值: " + max(numbers));

        // 6. 二维数组
        int[][] grid = {{1, 2, 3}, {4, 5, 6}};
        System.out.println("二维数组: " + Arrays.deepToString(grid));
        System.out.println("grid[1][2] = " + grid[1][2]);

        // 7. 数组是引用类型:两个变量指向同一个对象
        int[] alias = numbers;
        alias[0] = 100;
        System.out.println("通过 alias 修改后 numbers[0] = " + numbers[0]);

        // 8. 数组的默认值
        int[] defaults = new int[3];
        System.out.println("new int[3] 的默认值: " + Arrays.toString(defaults));
        String[] strings = new String[2];
        System.out.println("new String[2] 的默认值: " + Arrays.toString(strings));
    }

    static int sum(int[] values) {
        int total = 0;
        for (int value : values) {
            total += value;
        }
        return total;
    }

    static int max(int[] values) {
        if (values == null || values.length == 0) {
            throw new IllegalArgumentException("数组不能为空");
        }
        int result = values[0];
        for (int value : values) {
            if (value > result) {
                result = value;
            }
        }
        return result;
    }
}

运行命令:

powershell
cd labs
./mvnw -pl java-syntax compile
java -cp java-syntax/target/classes cn.kb.learn.syntax.ArraysLesson

预期输出:

text
长度: 5
首尾元素: 5 / 7
排序后: [1, 2, 5, 7, 9]
9 的下标: 4
不存在的值返回负数: -6
前三个元素: [1, 2, 5]
求和: 24
最大值: 9
二维数组: [[1, 2, 3], [4, 5, 6]]
grid[1][2] = 6
通过 alias 修改后 numbers[0] = 100
new int[3] 的默认值: [0, 0, 0]
new String[2] 的默认值: [null, null]

8. Debug 观察点 ​

  1. 在 int[] alias = numbers; 处断点,展开 Variables 面板看 numbers 与 alias 的 id 是否相同——它们指向同一个数组对象;
  2. 在 alias[0] = 100; 执行后,观察 numbers[0] 也变成 100;
  3. 把 Arrays.sort(numbers) 的数组改成 {5, 2, 9, 1, 7} 后手动断点逐步执行,观察就地排序(原数组被修改,没有新数组产生);
  4. 故意写 numbers[5](越界),用异常断点(《Java 程序的运行与调试》)停在抛异常的位置。

9. 常见错误 ​

错误 1:length 与 length() ​

java
int[] arr = {1, 2};
System.out.println(arr.length);        // 对:属性
System.out.println("abc".length());    // 对:方法
System.out.println(arr.length());      // 编译错误

记忆:数组是"属性",字符串是"方法"。

错误 2:直接打印数组得到地址 ​

java
int[] arr = {1, 2, 3};
System.out.println(arr);               // [I@1b6d3586
System.out.println(Arrays.toString(arr));   // [1, 2, 3]

二维数组同理,要用 Arrays.deepToString。

错误 3:越界访问 ​

java
int[] arr = {1, 2, 3};
for (int i = 0; i <= arr.length; i++) {   // <= 导致越界
    System.out.println(arr[i]);
}

修复:数组遍历统一用 <;需要"最后一个元素"时写 arr[arr.length - 1]。

错误 4:以为 = 复制了数组 ​

java
int[] a = {1, 2, 3};
int[] b = a;             // 不是复制,是别名
b[0] = 99;
System.out.println(a[0]);   // 99

修复:要真正独立用 Arrays.copyOf(a, a.length)。

10. 动手实验 ​

目标:手写三个数组算法,并用 Arrays 工具类验证。

步骤:

  1. 运行第 7 节代码,核对输出(注意 -6 这个返回值);
  2. 手写 int[] reverse(int[] source):返回反转后的新数组(不修改原数组);
  3. 手写 int indexOf(int[] values, int target):返回目标值的下标,找不到返回 -1;
  4. 用 Arrays.sort + Arrays.binarySearch 重写第 3 步,对比两者的前提条件与性能差异(提示:二分查找要求有序);
  5. 造一个 100 个元素的随机数组,分别用两种方式查找同一个值,打印耗时对比。

验收方式:能说出"为什么 binarySearch 必须先排序",以及不排序时会得到什么结果(可能返回错误的负数)。

扩展任务:实现 int[][] transpose(int[][] matrix)(矩阵转置),注意二维数组的行列关系,并用 Arrays.deepToString 验证结果。

11. 练习题 ​

基础题

  1. 判断:int[] a = new int[5]; 之后 a[0] 是 0。(对 / 错)
  2. 填空:数组的长度属性是 ______,字符串的长度方法是 ______。
  3. 简答:为什么 println(arr) 打印出来的是 [I@1b6d3586 这样的内容?

代码阅读题

java
int[] a = {1, 2, 3};
int[] b = Arrays.copyOf(a, 5);
b[0] = 100;
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
  1. 两行输出分别是什么?b 的后两个元素是什么?

编程题

  1. 写一个方法 int secondMax(int[] values),返回数组中第二大的数(要求:不能用 Arrays.sort,数组长度至少为 2)。

综合任务

  1. 写一个"成绩分析"程序:
    • 输入:int[] scores(0-100 的整数);
    • 输出:平均分、最高分、最低分、及格率(≥60 的占比)、以及每个分数段的人数(0-59 / 60-69 / 70-79 / 80-89 / 90-100);
    • 要求:分数越界(<0 或 >100)时打印提示并跳过该分数;
    • 说明你如何用数组存储分数段统计。
分级提示

提示 1(方向):第 5 题维护两个变量 max 与 second,遍历时按"比 max 大"和"介于两者之间"两种情况更新。

提示 2(关键点):第 6 题的分数段统计可以用一个长度为 5 的 int[],用 score / 10 计算下标(注意 100 分要单独处理或取 Math.min(score / 10, 9))。

提示 3(接近答案):第 6 步的越界处理用 continue 跳过,但记得在跳过前打印提示。

12. 参考答案 ​

基础题
  1. 对。数值型数组的元素默认值就是 0。
  2. length、length()。
  3. 因为数组是引用类型,println 打印的是 toString() 的结果,而数组没有重写 toString(),于是打印"类型标记 + 哈希码"。[I 表示"int 数组",@ 后面是对象地址的十六进制表示。
代码阅读题(第 4 题)
text
[1, 2, 3]
[100, 2, 3, 0, 0]

Arrays.copyOf(a, 5) 会创建一个长度为 5 的新数组:前 3 个元素复制自 a,多出来的两个补默认值 0。修改 b[0] 不影响 a——这证明 copyOf 是真正的复制(与 int[] b = a 的别名行为不同)。

编程题(第 5 题)
java
package cn.kb.learn.syntax;

public class SecondMax {
    static int secondMax(int[] values) {
        if (values == null || values.length < 2) {
            throw new IllegalArgumentException("数组长度至少为 2");
        }
        int max = Integer.MIN_VALUE;
        int second = Integer.MIN_VALUE;
        for (int value : values) {
            if (value > max) {
                second = max;      // 原最大值降级为第二大
                max = value;
            } else if (value > second && value != max) {
                second = value;
            }
        }
        if (second == Integer.MIN_VALUE) {
            throw new IllegalArgumentException("不存在第二大的数(所有元素相同)");
        }
        return second;
    }

    public static void main(String[] args) {
        System.out.println(secondMax(new int[]{3, 9, 9, 5, 1}));
        System.out.println(secondMax(new int[]{1, 2}));
    }
}

预期输出:

text
5
1

注意 else if 里的 value != max:它保证"重复的最大值"不会被当成第二大。

综合任务(第 6 题)
java
package cn.kb.learn.syntax;

public class ScoreAnalysis {
    public static void main(String[] args) {
        int[] scores = {95, 88, 72, 60, 45, 100, -5, 89, 77, 58};

        int[] buckets = new int[5];      // 0-59, 60-69, 70-79, 80-89, 90-100
        int valid = 0;
        int passed = 0;
        int total = 0;
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;

        for (int score : scores) {
            if (score < 0 || score > 100) {
                System.out.println("跳过无效分数: " + score);
                continue;
            }
            valid++;
            total += score;
            if (score >= 60) {
                passed++;
            }
            if (score > max) {
                max = score;
            }
            if (score < min) {
                min = score;
            }
            buckets[Math.min(score / 10, 4)]++;
        }

        System.out.printf("有效人数: %d%n", valid);
        System.out.printf("平均分: %.1f%n", (double) total / valid);
        System.out.printf("最高 / 最低: %d / %d%n", max, min);
        System.out.printf("及格率: %.1f%%%n", passed * 100.0 / valid);
        System.out.printf("分数段: 0-59=%d 60-69=%d 70-79=%d 80-89=%d 90-100=%d%n",
                buckets[0], buckets[1], buckets[2], buckets[3], buckets[4]);
    }
}

数组是怎么用来统计的:分数段只有 5 个且连续,用 score / 10 当下标直接定位到对应格子,把"多分支 if-else"变成了"一次数组下标访问"。这是数组最典型的用法——用下标代替分支。

预期输出:

text
跳过无效分数: -5
有效人数: 9
平均分: 76.0
最高 / 最低: 100 / 45
及格率: 88.9%
分数段: 0-59=2 60-69=1 70-79=2 80-89=2 90-100=2

13. 掌握标准 ​

  • [ ] 能熟练声明、初始化、遍历数组;
  • [ ] 知道默认值规则与越界的后果;
  • [ ] 能解释"数组变量存的是地址",并预测别名修改的影响;
  • [ ] 会用 Arrays 的常用方法,尤其是 toString 与 binarySearch。

14. 扩展资源 ​

15. 下一步学习 ​

《方法、参数与重载》:把重复的代码封装成方法,并搞清 Java 的参数传递到底传了什么。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引