Skip to content

方法、参数与重载 ​

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

1. 本节定位 ​

方法是最小的可复用单元。到目前为止,代码都写在 main 里;从本节开始,代码会被拆进一个个方法。

本节有一个必须吃透的知识点:Java 只有值传递。这个结论看起来简单,但"为什么传数组进去改内容会生效,重新赋值却不生效",是初学者最容易糊涂的地方,也是面试高频题。

2. 学习目标 ​

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

  • 说清方法签名的组成部分,并写出返回值与 void 方法;
  • 用"值传递"解释基本类型与引用类型参数的行为差异;
  • 解释为什么"方法内改数组内容会影响调用方,重新赋值不会";
  • 写出方法重载,并说清它与重写的区别;
  • 正确使用可变参数;
  • 写出带出口条件与收敛条件的递归方法。

3. 前置知识检查 ​

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

第一,方法让代码可读。 一屏 200 行的 main 没人愿意读;拆成 validateOrder()、calculateTotal()、saveOrder() 之后,主流程五行就能看懂。

第二,方法让代码可测。 后面测试专项里的单元测试,测的就是"一个方法在给定输入下的输出"。main 里的代码没法测——这也是为什么业务逻辑要放进方法。

第三,值传递的概念决定了你会不会写出"改了参数却没生效"的 bug。 这类 bug 不报错,只是结果不对,排查起来很费时间。

5. 核心概念 ​

5.1 方法签名 ​

java
public static long totalInCents(long unitPriceInCents, int quantity) {
    return unitPriceInCents * quantity;
}
//  ↓       ↓      ↓            ↓
// 修饰符   返回类型  方法名        参数列表
部分说明
修饰符public / private / static / final 等
返回类型值的类型,不返回值写 void
方法名动词开头,如 calculateTotal、findById
参数列表类型 + 参数名,可以没有

方法签名只包括"方法名 + 参数列表"——返回类型不算签名的一部分,这也是重载能成立的前提。

5.2 返回值 ​

java
int add(int a, int b) { return a + b; }        // 有返回值
void print(String text) { System.out.println(text); }   // 无返回值

规则:

  • 声明了返回类型,所有分支都必须有 return(编译器会检查);
  • void 方法可以只写 return; 用来提前结束;
  • 返回类型可以隐式提升(返回 int 的方法是 long 也能接收)。

5.3 值传递(本节核心) ​

结论:Java 里所有参数传递都是"值传递"——传递的是变量的副本。

理解的关键是分两种情况:

情况一:基本类型参数——复制的是值本身,方法内改动不影响调用方。

java
static void changePrimitive(int value) {
    value = 999;              // 改的是副本
}
int number = 10;
changePrimitive(number);
System.out.println(number);   // 10,没变

情况二:引用类型参数——复制的是引用(地址),于是:

  • 通过这个引用修改对象内容 → 影响调用方(因为指向同一个对象);
  • 给参数重新赋值(换一个对象)→ 不影响调用方(改的是副本引用)。
java
static void changeContent(int[] values) {
    values[0] = 999;          // 改了对象内容 → 生效
}

static void reassign(int[] values) {
    values = new int[]{7, 7, 7};   // 换了对象 → 不生效
}

int[] data = {1, 2, 3};
changeContent(data);          // data 变成 [999, 2, 3]
reassign(data);               // data 还是 [999, 2, 3]

用图理解:

text
调用方的 data ─────────────▶ [1, 2, 3]
方法内的 values ─────────────▶ (同一个数组)

changeContent 改的是数组内容 → 调用方看得见
reassign 让 values 指向新数组 → 调用方的 data 仍然指向老数组

所以准确的说法是:Java 传递的是"引用的副本",不是"引用本身"——这仍然属于值传递。

5.4 重载(Overload) ​

同一个类里可以定义同名但参数列表不同的方法:

java
static String describe(int value) { ... }
static String describe(int first, int second) { ... }
static String describe(String text) { ... }

编译器根据参数的个数与类型决定调用哪个,这叫"编译期多态"。

重载的判断依据只有两个:参数个数与参数类型。以下不算重载:

java
static int calc(int a) { return a; }
static long calc(int a) { return a; }     // 编译错误:仅返回类型不同

与重写的区别(后面 《继承与重写》 会详细讲):

重载 Overload重写 Override
发生位置同一个类内子类对父类方法
判断依据参数列表不同签名相同,方法体不同
绑定时机编译期运行期(多态)

5.5 可变参数 ​

java
static int sum(int... numbers) {
    int total = 0;
    for (int number : numbers) {
        total += number;
    }
    return total;
}

sum(1, 2, 3);                  // 6
sum();                         // 0
sum(new int[]{2, 4});          // 6(可以直接传数组)

三条规则:

  1. 可变参数必须是最后一个参数;
  2. 一个方法最多有一个可变参数;
  3. 它本质是数组,所以调用方传数组也合法——这带来一个坑:sum(null) 会编译通过但运行时 NPE。

5.6 递归:出口 + 收敛 ​

java
static long factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

递归方法必须有两个部分:

  • 出口条件(base case):n <= 1 时直接返回,不再递归;
  • 收敛(progress):每次调用让问题规模变小(n - 1)。

缺任何一个都会导致 StackOverflowError——调用栈被无限压入直到溢出。

递归不是"更高级的循环",它的价值在于表达自相似结构(树、目录、JSON)。能用简单循环解决时,优先用循环。

6. 递进示例 ​

6.1 最小示例:方法与返回 ​

java
static int max(int a, int b) {
    return a > b ? a : b;
}
System.out.println(max(3, 7));    // 7

6.2 业务示例:拆方法让主流程可读 ​

java
public static void main(String[] args) {
    long[] prices = {1990L, 2500L, 999L};
    System.out.println("总价(分): " + totalInCents(prices));
    System.out.println("最贵(分): " + maxPriceInCents(prices));
}

static long totalInCents(long[] prices) {
    long total = 0;
    for (long price : prices) {
        total += price;
    }
    return total;
}

static long maxPriceInCents(long[] prices) {
    if (prices == null || prices.length == 0) {
        throw new IllegalArgumentException("价格列表不能为空");
    }
    long max = prices[0];
    for (long price : prices) {
        if (price > max) {
            max = price;
        }
    }
    return max;
}

最直接的好处:每个方法都能单独写测试(totalInCents(new long[]{1,2}) 返回 3),这是后面测试专项的基础。

6.3 反例:以为改了参数就生效 ​

java
static void reset(String[] names) {
    names = new String[]{"默认"};      // 只改了局部引用
}

String[] data = {"A", "B"};
reset(data);
System.out.println(data[0]);           // 还是 A

想真正修改调用方的数据,有两种正确做法:

java
// 方案一:改内容而不是换引用
static void reset(String[] names) {
    for (int i = 0; i < names.length; i++) {
        names[i] = "默认";
    }
}

// 方案二:返回新对象,由调用方接收
static String[] createDefault() {
    return new String[]{"默认"};
}
// 调用方:data = createDefault();

推荐方案二:不改传入的参数,返回新对象——副作用更少,代码更好测。

7. 完整可运行代码 ​

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

java
package cn.kb.learn.syntax;

import java.util.Arrays;

public class MethodsLesson {
    public static void main(String[] args) {
        // 1. 基本类型参数:值传递,方法内修改不影响调用方
        int number = 10;
        changePrimitive(number);
        System.out.println("方法外的 number = " + number);

        // 2. 引用类型参数:传递的是引用的副本,改内容会影响调用方
        int[] data = {1, 2, 3};
        changeContent(data);
        System.out.println("改内容后 data = " + Arrays.toString(data));

        // 3. 重新赋值引用:不影响调用方
        reassign(data);
        System.out.println("重新赋值后 data = " + Arrays.toString(data));

        // 4. 重载:同名方法按参数列表区分
        System.out.println(describe(1));
        System.out.println(describe(1, 2));
        System.out.println(describe("文本"));

        // 5. 可变参数
        System.out.println("sum(1..5) = " + sum(1, 2, 3, 4, 5));
        System.out.println("sum() = " + sum());
        System.out.println("sum(new int[]{2, 4}) = " + sum(new int[]{2, 4}));

        // 6. 递归
        System.out.println("5! = " + factorial(5));
        System.out.println("斐波那契第 10 项 = " + fibonacci(10));
    }

    static void changePrimitive(int value) {
        value = 999;
    }

    static void changeContent(int[] values) {
        values[0] = 999;
    }

    static void reassign(int[] values) {
        values = new int[]{7, 7, 7};
    }

    static String describe(int value) {
        return "一个 int 参数: " + value;
    }

    static String describe(int first, int second) {
        return "两个 int 参数: " + first + ", " + second;
    }

    static String describe(String text) {
        return "一个 String 参数: " + text;
    }

    static int sum(int... numbers) {
        int total = 0;
        for (int number : numbers) {
            total += number;
        }
        return total;
    }

    static long factorial(int n) {
        return n <= 1 ? 1 : n * factorial(n - 1);
    }

    static long fibonacci(int position) {
        if (position <= 2) {
            return 1;
        }
        return fibonacci(position - 1) + fibonacci(position - 2);
    }
}

运行命令:

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

预期输出:

text
方法外的 number = 10
改内容后 data = [999, 2, 3]
重新赋值后 data = [999, 2, 3]
一个 int 参数: 1
两个 int 参数: 1, 2
一个 String 参数: 文本
sum(1..5) = 15
sum() = 0
sum(new int[]{2, 4}) = 6
5! = 120
斐波那契第 10 项 = 55

前两行是本节的核心证据:changePrimitive 没生效(基本类型传值),changeContent 生效了(引用类型改内容),reassign 又没生效(换引用没用)。

8. Debug 观察点 ​

  1. 在 changePrimitive(number); 处按 F7 进入方法,观察参数 value 是 10;执行 value = 999 后,Frames 面板里调用方的 number 仍然是 10——两个变量各有一份;
  2. 在 changeContent(data); 处 F7 进入,展开 values 与调用方的 data,看 id 相同(同一个数组对象);
  3. 在 reassign(data); 里 F7 逐步执行,在 values = new int[]{7, 7, 7} 之后,看 values 的 id 变了,而调用方的 data 还是老 id;
  4. 用 factorial(5) 练调用栈:在方法首行下断点,连续 F7,观察 Frames 面板堆到 5 层后逐层返回(《Java 程序的运行与调试》 讲过同样的方法)。

9. 常见错误 ​

错误 1:以为改了参数就生效 ​

见 6.3 节。修复:改内容而不是换引用,或返回新对象让调用方接收。

错误 2:递归没有出口 ​

java
static int count(int n) {
    return count(n - 1);      // 永远不返回
}
text
Exception in thread "main" java.lang.StackOverflowError

修复:先写出口条件,再写递归调用。写完后问自己:"什么情况下它不再调用自己?"

错误 3:重载时的类型歧义 ​

java
static void print(int value) { }
static void print(long value) { }

print(1);        // 调用 int 版本
print(1L);       // 调用 long 版本
print('a');      // char 会提升为 int(走了 int 版本)

修复:重载参数类型差异要明显(如 int 与 String),避免 int / long / char 这种容易被隐式转换搞混的组合。

错误 4:可变参数传 null ​

java
static int sum(int... numbers) {
    return numbers.length;      // numbers 可能是 null
}
sum(null);                      // 编译通过,运行时 NPE

修复:方法内先判空,或在文档注释里写明"不要传 null"。

10. 动手实验 ​

目标:把一段 main 里的逻辑重构为多个可测试的方法。

步骤:

  1. 运行第 7 节代码,核对输出(重点是前三行);
  2. 把 《数组》 综合任务里的"成绩分析"逻辑拆成四个方法:average、max、min、passRate;
  3. 每个方法都写"入参校验"(数组为 null 或为空时抛 IllegalArgumentException);
  4. 在 main 里依次调用它们,输出结果;
  5. 验证"值传递":写一个方法 void fillZero(int[] values) 把数组全部置零,确认调用方的数组被修改;再写一个 int[] createZero(int size) 返回新数组,确认两种方式的差别。

验收方式:能解释第 5 步两个方法为什么一个"改了原数组"、一个"没改"。

扩展任务:写一个递归方法 long countFiles(String path) 统计某个目录下的所有文件数(含子目录),体会递归在"树形结构"上的表达力。(提示:用 java.io.File 的 listFiles(),注意判空)

11. 练习题 ​

基础题

  1. 判断:Java 的参数传递对引用类型来说是"引用传递"。(对 / 错)
  2. 填空:方法签名由 ______ 和 ______ 组成。
  3. 简答:重载与重写的区别是什么?

代码阅读题

java
static void modify(int[] arr, int[] other) {
    arr[0] = 100;
    other = new int[]{9, 9};
}

int[] a = {1, 2};
int[] b = {3, 4};
modify(a, b);
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
  1. 输出是什么?为什么 b 没变?

编程题

  1. 写出方法 isPrime(int n) 判断质数,要求:
    • 小于 2 的数返回 false;
    • 只需试除到平方根;
    • 写出它的重载版本 isPrime(long n)(提示:注意循环变量的类型选择)。

综合任务

  1. 实现一个工具类 NumberUtils:
    • int max(int... numbers):返回最大值,参数为空时抛异常;
    • double average(int... numbers):返回平均值,参数为空时返回 0;
    • long factorial(int n):递归实现,n < 0 时抛异常;
    • boolean isPalindrome(int n):判断回文数字;
    • 每个方法都写清参数校验与返回值说明(用 Javadoc 注释)。
分级提示

提示 1(方向):第 4 题的关键是 other = new int[]{9, 9} 改的是方法内的局部引用。

提示 2(关键点):第 5 题的试除上界是 i <= Math.sqrt(n),或者写成 i * i <= n(避免浮点误差,但要注意 i * i 溢出)。

提示 3(接近答案):第 6 题的 isPalindrome 可以把数字反转后比较,或用字符串双指针;注意负数一般不算回文。

12. 参考答案 ​

基础题
  1. 错。Java 只有值传递。引用类型传递的是"引用的副本",所以能改对象内容,但不能让调用方的引用指向新对象——这仍然是值传递。
  2. 方法名、参数列表(返回类型不算签名)。
  3. 重载发生在同一个类内,靠参数列表不同区分,编译期决定调用哪个;重写发生在子类中,方法签名相同、实现不同,运行期按实际对象类型决定调用哪个(多态)。
代码阅读题(第 4 题)
text
[100, 2]
[3, 4]
  • arr[0] = 100:改的是数组内容,arr 与调用方的 a 指向同一个数组,所以 a 变了;
  • other = new int[]{9, 9}:改的是方法内局部引用,让它指向一个新数组,与调用方的 b 再无关系,所以 b 不变。

这一题把"改内容生效、换引用不生效"同时演示了一遍,是理解值传递最好的一道题。

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

public class PrimeChecker {
    static boolean isPrime(int n) {
        if (n < 2) {
            return false;
        }
        for (int i = 2; (long) i * i <= n; i++) {
            if (n % i == 0) {
                return false;
            }
        }
        return true;
    }

    static boolean isPrime(long n) {
        if (n < 2) {
            return false;
        }
        for (long i = 2; i * i <= n; i++) {
            if (n % i == 0) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        System.out.println(isPrime(1));
        System.out.println(isPrime(2));
        System.out.println(isPrime(97));
        System.out.println(isPrime(100));
        System.out.println(isPrime(2147483647L));
    }
}

要点:试除到平方根即可——若 n = a × b 且 a ≤ b,则 a ≤ √n。int 版本里用 (long) i * i 是为了避免 i * i 溢出。

预期输出:

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

public class NumberUtils {
    /**
     * 返回一组整数中的最大值。
     *
     * @param numbers 待比较的数字,至少一个
     * @return 最大值
     * @throws IllegalArgumentException 参数为空时抛出
     */
    public static int max(int... numbers) {
        if (numbers == null || numbers.length == 0) {
            throw new IllegalArgumentException("至少需要一个数字");
        }
        int result = numbers[0];
        for (int number : numbers) {
            if (number > result) {
                result = number;
            }
        }
        return result;
    }

    /**
     * 返回一组整数的平均值,空参数返回 0。
     */
    public static double average(int... numbers) {
        if (numbers == null || numbers.length == 0) {
            return 0;
        }
        long total = 0;
        for (int number : numbers) {
            total += number;
        }
        return (double) total / numbers.length;
    }

    /**
     * 递归计算阶乘。
     *
     * @throws IllegalArgumentException n 为负数时抛出
     */
    public static long factorial(int n) {
        if (n < 0) {
            throw new IllegalArgumentException("n 不能为负");
        }
        return n <= 1 ? 1 : n * factorial(n - 1);
    }

    /**
     * 判断一个非负整数是否为回文数(如 121、1221)。
     */
    public static boolean isPalindrome(int n) {
        if (n < 0) {
            return false;
        }
        int original = n;
        long reversed = 0;
        while (n > 0) {
            reversed = reversed * 10 + n % 10;
            n /= 10;
        }
        return reversed == original;
    }

    public static void main(String[] args) {
        System.out.println(max(3, 9, 1));
        System.out.println(average(1, 2, 3, 4));
        System.out.println(average());
        System.out.println(factorial(5));
        System.out.println(isPalindrome(1221));
        System.out.println(isPalindrome(1234));
    }
}

注意 average 里累加用 long:数组很长时 int 会溢出(《变量与数据类型》 的溢出问题)。

预期输出:

text
9
2.5
0.0
120
true
false

13. 掌握标准 ​

  • [ ] 能写出带参数校验与返回值的方法;
  • [ ] 能用"值传递"解释参数修改为什么生效或不生效;
  • [ ] 能写出重载方法并说清它与重写的区别;
  • [ ] 会写带出口条件的递归方法;
  • [ ] 能主动把业务逻辑从 main 抽成方法,为写测试做准备。

14. 扩展资源 ​

15. 下一步学习 ​

语法基础到这里告一段落。《类与对象》:从"写方法"进入"设计对象",开始真正的面向对象。

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

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