方法与参数
这一课你将能做到
- 按规范定义方法(返回值、参数、命名),把重复逻辑抽成可复用单元
- 用一句话说清「Java 只有值传递」——并用代码验证基本类型和对象参数的区别
- 会用方法重载、可变参数和递归,写出与 Spring Service 同风格的方法签名
为什么要学
方法是代码组织的最小复用单元。本站后端一个 Service 方法通常长这样:
public Order createOrder(Long userId, Long productId, int quantity) { ... }
控制器调它,事务包着它,单元测试测它。方法设计得好,每个方法只干一件事、名字见名知意,别人读你的代码像读文章;设计得差,一个方法三百行,改一处崩全局。
这一课还有一个必考点:参数传递机制。面试官一句「Java 是值传递还是引用传递」能筛掉一半人——答案将在步骤 3 用代码证实。
先建立图像
方法的解剖图:
修饰符 返回类型 方法名(参数列表) { 方法体 }
public int add(int a, int b) { return a + b; }
│ │ │ │
谁能调 交回什么 叫什么 收什么
调用像「寄快递 + 等回执」:
main: int total = add(3, 5); ──寄出 3 和 5──▶ add 方法里 a=3, b=5
│ 计算 8
main: total = 8 ◀──寄回 8────────── return a + b;
跟着做
步骤 1:定义并调用第一个方法
新建 MethodDemo.java:
public class MethodDemo {
public static void main(String[] args) {
int total = add(3, 5); // 调用:实参 3、5 传给形参 a、b
System.out.println("3 + 5 = " + total);
greet("tom"); // 无返回值方法,单独成语句调用
printLine('=', 20);
double avg = average(88, 92, 75);
System.out.printf("平均分: %.1f%n", avg);
}
static int add(int a, int b) { // 有返回值:必须 return
return a + b;
}
static void greet(String name) { // void:不返回东西
System.out.println("你好, " + name);
}
static void printLine(char ch, int count) {
for (int i = 0; i < count; i++) {
System.out.print(ch);
}
System.out.println();
}
static double average(int... scores) { // 可变参数,步骤 5 详解
if (scores.length == 0) return 0;
int sum = 0;
for (int s : scores) sum += s;
return (double) sum / scores.length;
}
}
输出:
3 + 5 = 8
你好, tom
====================
平均分: 85.0
步骤 2:方法返回值规则
public class ReturnDemo {
public static void main(String[] args) {
System.out.println(findUserStatus(-1));
System.out.println(findUserStatus(1001));
}
static String findUserStatus(long userId) {
if (userId <= 0) {
return "非法用户 ID"; // 提前返回:卫语句风格,本站后端标准写法
}
// ... 这里假装查了数据库
return "在线";
}
static double divide(int a, int b) {
if (b == 0) {
System.out.println("除数不能为 0");
return Double.NaN; // 所有分支都必须有返回值,编译器强制检查
}
return (double) a / b;
}
}
规则:返回类型不是 void 的方法,每条执行路径都必须 return;return 一执行,方法立即结束。
步骤 3:值传递实证(本课最重要实验)
public class PassByValue {
public static void main(String[] args) {
int n = 10;
changeInt(n);
System.out.println("基本类型 after: " + n); // 还是 10!
int[] arr = {1, 2, 3};
changeArray(arr);
System.out.println("数组 after: " + java.util.Arrays.toString(arr)); // [99, 2, 3]
int[] arr2 = {1, 2, 3};
reassignArray(arr2);
System.out.println("数组 after2: " + java.util.Arrays.toString(arr2)); // [1, 2, 3]
}
static void changeInt(int x) {
x = 999; // 改的是「复印件」,外面的 n 毫发无损
}
static void changeArray(int[] a) {
a[0] = 99; // 复印的是「地址」,顺着地址改到了同一个对象 → 生效
}
static void reassignArray(int[] a) {
a = new int[]{7, 7, 7}; // 改的是复印件本身(指向了新对象)→ 外面看不见
}
}
基本类型 after: 10
数组 after: [99, 2, 3]
数组 after2: [1, 2, 3]
结论:传进去的永远是变量的复印件——基本类型复印「值」,引用类型复印「地址」。顺着地址改对象内容有效,把复印件改指向新对象则无效。这就是「Java 只有值传递」的完整含义。
步骤 4:方法重载(Overload)
public class OverloadDemo {
public static void main(String[] args) {
System.out.println(format(100)); // 调 int 版本
System.out.println(format(3.14159)); // 调 double 版本
System.out.println(format("ORDER-1001")); // 调 String 版本
}
// 同名不同参:参数个数、类型或顺序不同即构成重载
static String format(int n) { return "整数: " + n; }
static String format(double d) { return "小数: " + d; }
static String format(String s) { return "字符串: " + s; }
}
注意:只有返回类型不同不构成重载,编译直接报错。JVM/编译器靠「参数列表」决定调谁。
步骤 5:可变参数(varargs)
public class VarargsDemo {
public static void main(String[] args) {
System.out.println(join("-")); // 空
System.out.println(join("-", "a", "b", "c")); // a-b-c
System.out.println(join(",", "mysql", "redis")); // mysql,redis
}
// String... 本质是 String[],调用方想传几个传几个
static String join(String sep, String... parts) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
if (i > 0) sb.append(sep);
sb.append(parts[i]);
}
return sb.toString();
}
}
规则:可变参数必须是参数列表的最后一个;一个方法最多一个可变参数。
步骤 6:递归入门
public class RecursionDemo {
public static void main(String[] args) {
System.out.println(factorial(5)); // 120
System.out.println(fib(10)); // 55
}
// 阶乘:5! = 5 × 4! = ... = 5 × 4 × 3 × 2 × 1
static long factorial(int n) {
if (n <= 1) return 1; // 递归出口,没有它必栈溢出
return n * factorial(n - 1);
}
// 斐波那契(演示用;实际有大量重复计算,学习 dsa-java 时会优化)
static long fib(int n) {
if (n <= 2) return 1;
return fib(n - 1) + fib(n - 2);
}
}
递归两要素:出口(终止条件)+ 递推(缩小问题规模)。缺出口就是 StackOverflowError。
讲清楚要点
- 方法必须属于类:Java 没有「独立函数」,哪怕是工具方法也要挂在某个类里(配合 static)。
- 形参是复印件,实参不受影响:除非你顺着引用改对象内部状态。想「真的改变」外部变量,用返回值接回来,或传可变容器。
- 重载解决「同一个动作、不同入参」:如
System.out.println 本身就有几十个重载版本。
- void 方法也能提前
return;:用于「条件不满足就结束」,不带任何值。
- static 方法属于类:调用写
类名.方法名(),内部不能直接访问非 static 成员(因为非 static 成员需要对象才存在)。
- 方法命名用动词开头小驼峰:
getUserById、createOrder、isValid。本站后端还有惯例:查询用 get/find/query,判断用 is/has,避免一个方法既查询又修改。
你可能会懵的地方
-
Java 到底是值传递还是引用传递?
只有值传递。引用类型的「值」是对象地址,复印件也指向同一对象,所以看起来像「引用传递」。判断标准:方法内让参数指向新对象,外部是否受影响——不受影响,就证明是值传递。
-
方法重载和重写是一回事吗?
不是。重载(overload):同一个类里同名不同参,编译期决定;重写(override):子类覆盖父类同签名方法,运行期决定(下一课细讲)。中文相似,英文和机制都不同。
-
可变参数和数组参数什么关系?
String... parts 完全等价于 String[] parts,只是调用端语法糖。已有 String[] 变量时可直接传入。
-
为什么工具方法都加 static?
工具方法不依赖对象状态(无实例字段),加 static 可以直接 Math.max(1, 2) 这样调用,不必 new 对象。Spring 的 Bean 方法反而是实例方法——因为它们依赖注入的字段状态。
-
递归会不会一直占内存?
每层调用在虚拟机栈里压一个栈帧。深度过大(如 factorial(100000))触发 StackOverflowError。能用循环就用循环,递归适合「天然树形/分形」问题。
错法对照
| 错法 | 问题 | 正解 |
|---|
static int add(int a, int b) { System.out.println(a+b); } 声明返回 int 却没 return | 编译错误 | 补 return a + b; 或改返回 void |
期望 changeInt(n) 改变外部 n | 值传递,外面不变 | 用返回值:n = change(n) |
| 重载只改返回类型 | 不构成重载,编译错 | 参数列表必须不同 |
static void join(String... parts, String sep) | 可变参数必须在最后 | 调整为 join(String sep, String... parts) |
| 递归没写出口 | StackOverflowError | 第一步先写终止条件 |
方法名叫 do1、process2 | 无法从名字看懂用途 | 动词+名词小驼峰,见名知意 |
卡点急救
- 编译报「缺少返回语句」:if 分支 return 了、if 外没写兜底 return。编译器按「所有路径」检查,补全 else 或方法末尾的 return。
- 改了参数外面没变化:回看步骤 3,值传递是语言机制不是 bug。需要改变量就 return 新值接住。
- 调用方法报「无法从静态上下文引用非静态方法」:main 是 static,直接调了非 static 方法。给该方法加 static,或先创建对象再调用。
- 重载调用歧义:如
format(null) 时 String 版本和 char[] 版本都匹配报错,强制转型 format((String) null) 明确意图——实际代码应避免这种设计。
面试常问
-
Java 是值传递还是引用传递?(必考)
只有值传递。基本类型复制值,引用类型复制地址;方法内重新赋值参数不影响外部,顺着地址修改对象内容才影响。标准论证代码即本课步骤 3。
-
重载和重写的区别?
重载:同类中同名不同参,编译期静态绑定;重写:父子类间同签名方法,运行期动态绑定,依赖 @Override 保证签名一致。
-
构造器能重载吗?
能,而且非常常见(如 new StringBuilder() 与 new StringBuilder(32)),构造器本质就是「类名做方法名的特殊方法」。
-
可变参数会带来什么坑?
与固定参数重载共存时可能歧义;null 直接传入会被当成 null 数组导致 NPE;反射场景 varargs 处理麻烦。团队规范通常优先固定参数重载。
-
什么是栈帧?
方法调用时 JVM 在虚拟机栈中分配的帧,存局部变量表、操作数栈、返回地址。方法结束栈帧出栈——这也是「局部变量在栈上」说法的由来。
自测三问
- 用一段代码(不加解释)向面试官证明 Java 只有值传递。
- 下面两个方法构成重载吗:
int f(int a) 与 double f(int a)?为什么?
- 递归计算 1+2+...+n,出口和递推分别是什么?n 很大时会发生什么?
小结一句话
方法 = 有名字的可复用代码块,靠参数列表区分重载;Java 只传「复印件」(值或地址),改对象内部有效、换指向无效;递归先写出口,可变参数必须排最后。
延伸
- Oracle:定义方法
- 下一课:数组——批量数据的容器,
Arrays 工具类全家桶,以及数组与集合的爱恨情仇。