← 返回 Java 后端知识路线
阶段 01Java 基础

数组、方法与值传递:看清输入和返回值的边界

用成绩统计工具理解数组索引、方法参数、返回值与 Java 的值传递,避免把引用传递误记成另一种机制。

第 04 / 35 篇
数组方法值传递重载

先看这一课值不值得学

学完后,你手里多了哪些代码积木

把一批同类型数据交给方法处理,并看清参数和返回值。

本课正式新增

语法 / API / 命令你必须会到什么程度
int[] scores = new int[5]创建定长数组
scores[index]按从 0 开始的下标读写元素
返回类型 方法名(参数列表)声明可复用方法
return value结束方法并把结果交回调用方

本课只借用,先别硬背

  • Arrays 工具类先会调用,集合与算法阶段再比较实现

学完必须能独立写

  • 写数组最大值、平均值和查找方法
  • 用调用前后变量解释 Java 的值传递
本课目录
  1. 1. 现实问题:统计工具为什么越写越难改
  2. 2. 最小可运行示例:输入数组,返回统计结果
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:让方法成为业务单元
  6. 6. 易错排查
  7. 7. 一页复习

1. 现实问题:统计工具为什么越写越难改

成绩、库存或接口结果经常以一批数据出现。把求和、平均值、最高分都塞进 main,很快就会出现重复循环、变量互相覆盖和边界不一致。方法的价值不是让文件变长,而是把“输入是什么、返回什么、是否会修改输入”说清楚。

另一个高频误解是“Java 传引用”。Java 只有值传递:传基本类型时复制数值,传对象时复制引用这个值。复制的引用仍然指向同一个数组对象,所以方法可以改数组元素,但不能把调用方的变量改成指向另一个数组。

2. 最小可运行示例:输入数组,返回统计结果

public class ScoreSummary {
    public static void main(String[] args) {
        int[] scores = {88, 72, 96, 81};
        System.out.println("平均分=" + average(scores));
        normalizeFirst(scores);
        System.out.println("首项被方法改为=" + scores[0]);
        replaceArray(scores);
        System.out.println("调用方数组首项仍是=" + scores[0]);
    }

    static double average(int[] scores) {
        if (scores == null || scores.length == 0) {
            throw new IllegalArgumentException("成绩不能为空");
        }
        int total = 0;
        for (int score : scores) total += score;
        return (double) total / scores.length;
    }

    static void normalizeFirst(int[] scores) {
        scores[0] = Math.max(0, Math.min(100, scores[0]));
    }

    static void replaceArray(int[] scores) {
        scores = new int[]{0};
    }
}

average 只读数组并返回一个 doublenormalizeFirst 修改对象内容,replaceArray 只修改方法内部的局部引用。三个方法的行为不同,方法名和返回类型应让调用者不需要猜测副作用。真实项目更适合返回不可变的 ScoreSummary 记录,而不是让调用方传一个可被到处修改的累加器。

3. 调用链与对象变化

main 创建数组对象,变量 scores 保存指向它的引用值。调用 average(scores) 时,参数槽得到一份相同的引用副本;循环通过该引用读取元素,返回计算后的基本类型值。调用 normalizeFirst 时,副本仍然指向原数组,所以写入下标 0 会被调用方观察到。

调用 replaceArray 后,方法内部的 scores 指向新数组,但调用方的 scores 槽没有改变,仍指向旧对象。这是“值传递”最好的实验。方法返回值是另一条边界:如果需要让调用方使用新对象,就显式 return newArray,不要期待给参数重新赋值能产生魔法效果。

4. 为什么这样设计

方法参数是契约,返回值是结果,副作用是额外成本。纯计算方法更容易测试、缓存和并行;修改共享数组虽然省一次复制,却把调用顺序变成隐含前提。小程序可以接受直接修改,但 Service 层和领域对象应尽量限制可变范围。

数组访问是连续内存语义的抽象,索引从 0 开始,length 是容量而不是最后一个索引。方法重载允许同一意图接受不同参数类型,但重载过多会让调用选择和装箱变得隐蔽;优先让方法名表达不同业务,而不是用大量重载模拟动态语言。

5. 项目落点:让方法成为业务单元

在博客系统里,可以将文章列表的标题提取、分页边界和标签汇总拆成独立方法,每个方法只接受它需要的数据。分页方法应返回 PageResult<T>,包含列表、页码和总数;不要返回一个裸数组再让 Controller 重新猜“空数组是没有结果还是查询失败”。

练习:实现 findIndex(int[] values, int target)countEven(int[] values)copySorted(int[] values)。先写空数组、null、没有命中和重复值测试,再决定是否返回 -1OptionalInt 或抛异常。不同选择要在项目文档里写清楚。

6. 易错排查

  • NullPointerException:数组引用可能是 null,先决定 null 是非法输入还是空集合语义。
  • ArrayIndexOutOfBoundsException:最后一个合法索引是 length - 1,循环条件不要写成 <= length
  • 修改参数后调用方数据变化:检查传入的是数组/对象还是基本值,并记录方法的副作用契约。
  • 方法返回值被忽略:编译器不一定警告,调用处要确认是否遗漏了新对象或计算结果。

7. 一页复习

方法看三件事:参数值是什么、返回值是什么、对象是否被修改。Java 传的是值;对象参数传的是“引用值的副本”,所以能改内容,不能改调用方变量的指向。数组适合固定顺序批量数据,复杂业务结果应升级为有名字的对象和清晰的返回契约。

可以用一个别名实验把值传递说清楚:int[] original = {1, 2}; int[] alias = original; 之后通过 alias 写下标 0,original 读到 9,因为两个变量保存的是同一个数组引用值;int[] copy = original.clone(); 之后改 copy,original 不变,因为堆上有两个数组对象。方法参数只是再复制一份 alias,并没有创造第三种传递机制。

方法契约最好写成输入、返回和副作用三行。average 的输入是非空成绩数组,返回 double,不修改数组;normalizeFirst 输入是可写数组,无返回值,会修改第一个元素;copySorted 返回新数组,不修改输入。这样在项目中的 domain/statistics 包里,每个方法都能对应一组测试,调用方无需打开实现文件猜副作用。

错误排查也要按调用链:平均值为整数时先查强制转换发生在除法之前还是之后;修改后调用方没变化时检查方法是否接住了返回的新数组;结果长度错误时核对空输入和过滤条件;数组越界时把索引、length 和目标元素同时打印。不要通过把异常 catch 成空数组来掩盖输入契约被破坏。

练习:为 copySorted 规定 null、空数组、重复值和负数的行为,并用 Arrays.copyOf 保证输入不被修改。再写一个 PageResult<T>,保存 itemspagesizetotal,用 List.copyOf 固定返回快照。比较裸数组、List 和结果对象在 Controller 输出中的可读性,记录为什么最终选择其中一种。

数组方法的输入输出表还应记录容量和别名:average 读四个元素返回 84.25;空数组没有平均值,应返回错误而不是 0;normalizeFirst 改变原数组第一个位置;replaceArray 只改变方法内部引用。对象变化可用 System.identityHashCode 辅助观察,但它只是身份线索,不能代替 equals 的业务判等。

当数据量和操作变复杂时,数组不一定是最终容器。分页结果、标签集合和批量 SQL 参数更适合 List;固定长度的算法输入才保留数组。项目文件里将算法方法放在 domain/statistics,将数组/列表转换放在 adapter,可以避免数据库结果直接泄漏到计算代码。

错误排查要区分“结果错”和“输入被改”:先复制输入做快照,再调用方法并比较快照;若结果错,打印总和、长度和强转位置;若别名污染,查是否把内部数组直接返回;若 null 被静默转成空数组,确认调用方是否失去了区分“没有数据”和“数据读取失败”的能力。练习是给每个方法写前置条件和后置条件。

方法练习完成后,把每个方法的契约写在测试名称中,例如“空数组拒绝”“复制排序不修改输入”“修改元素会被调用方观察”。这些名称本身就是调用链边界,能防止后来为了省一次复制而悄悄改变副作用。

验证清单:给 findIndex 依次传入空数组、单元素命中、重复目标和 null,核对返回值以及异常/错误结果是否符合约定;给 copySorted 保存调用前快照,调用后同时检查排序结果和原数组顺序。再用一个方法把数组参数重新指向新数组,确认调用方的引用没有被替换。复盘时画出栈上的参数变量和堆上的数组对象,标出哪些箭头共享同一对象,才能把“传值”从口号变成可观察的调用链。

进阶附录:可变参数、递归与栈

int... values 在调用端表现得像多个参数,方法内部实际收到一个数组;它适合少量可选项,不适合替代集合。递归每次调用都会创建新的栈帧,必须有能到达的基线条件;深度过大可能栈溢出。后端分页和树遍历中,先估算深度,再决定递归还是显式栈。

本课按「Java 21 数组、方法调用与参数语义」的学习范围组织,正文与示例均为本站原创整理。