主题
界面
主题
方法是最小的可复用单元。到目前为止,代码都写在 main 里;从本节开始,代码会被拆进一个个方法。
本节有一个必须吃透的知识点:Java 只有值传递。这个结论看起来简单,但"为什么传数组进去改内容会生效,重新赋值却不生效",是初学者最容易糊涂的地方,也是面试高频题。
完成本节后,你应该能够:
void 方法;第一,方法让代码可读。 一屏 200 行的 main 没人愿意读;拆成 validateOrder()、calculateTotal()、saveOrder() 之后,主流程五行就能看懂。
第二,方法让代码可测。 后面测试专项里的单元测试,测的就是"一个方法在给定输入下的输出"。main 里的代码没法测——这也是为什么业务逻辑要放进方法。
第三,值传递的概念决定了你会不会写出"改了参数却没生效"的 bug。 这类 bug 不报错,只是结果不对,排查起来很费时间。
public static long totalInCents(long unitPriceInCents, int quantity) {
return unitPriceInCents * quantity;
}
// ↓ ↓ ↓ ↓
// 修饰符 返回类型 方法名 参数列表| 部分 | 说明 |
|---|---|
| 修饰符 | public / private / static / final 等 |
| 返回类型 | 值的类型,不返回值写 void |
| 方法名 | 动词开头,如 calculateTotal、findById |
| 参数列表 | 类型 + 参数名,可以没有 |
方法签名只包括"方法名 + 参数列表"——返回类型不算签名的一部分,这也是重载能成立的前提。
int add(int a, int b) { return a + b; } // 有返回值
void print(String text) { System.out.println(text); } // 无返回值规则:
return(编译器会检查);void 方法可以只写 return; 用来提前结束;int 的方法是 long 也能接收)。结论:Java 里所有参数传递都是"值传递"——传递的是变量的副本。
理解的关键是分两种情况:
情况一:基本类型参数——复制的是值本身,方法内改动不影响调用方。
static void changePrimitive(int value) {
value = 999; // 改的是副本
}
int number = 10;
changePrimitive(number);
System.out.println(number); // 10,没变情况二:引用类型参数——复制的是引用(地址),于是:
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]用图理解:
调用方的 data ─────────────▶ [1, 2, 3]
方法内的 values ─────────────▶ (同一个数组)
changeContent 改的是数组内容 → 调用方看得见
reassign 让 values 指向新数组 → 调用方的 data 仍然指向老数组所以准确的说法是:Java 传递的是"引用的副本",不是"引用本身"——这仍然属于值传递。
同一个类里可以定义同名但参数列表不同的方法:
static String describe(int value) { ... }
static String describe(int first, int second) { ... }
static String describe(String text) { ... }编译器根据参数的个数与类型决定调用哪个,这叫"编译期多态"。
重载的判断依据只有两个:参数个数与参数类型。以下不算重载:
static int calc(int a) { return a; }
static long calc(int a) { return a; } // 编译错误:仅返回类型不同与重写的区别(后面 《继承与重写》 会详细讲):
| 重载 Overload | 重写 Override | |
|---|---|---|
| 发生位置 | 同一个类内 | 子类对父类方法 |
| 判断依据 | 参数列表不同 | 签名相同,方法体不同 |
| 绑定时机 | 编译期 | 运行期(多态) |
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(可以直接传数组)三条规则:
sum(null) 会编译通过但运行时 NPE。static long factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}递归方法必须有两个部分:
n <= 1 时直接返回,不再递归;n - 1)。缺任何一个都会导致 StackOverflowError——调用栈被无限压入直到溢出。
递归不是"更高级的循环",它的价值在于表达自相似结构(树、目录、JSON)。能用简单循环解决时,优先用循环。
static int max(int a, int b) {
return a > b ? a : b;
}
System.out.println(max(3, 7)); // 7public 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),这是后面测试专项的基础。
static void reset(String[] names) {
names = new String[]{"默认"}; // 只改了局部引用
}
String[] data = {"A", "B"};
reset(data);
System.out.println(data[0]); // 还是 A想真正修改调用方的数据,有两种正确做法:
// 方案一:改内容而不是换引用
static void reset(String[] names) {
for (int i = 0; i < names.length; i++) {
names[i] = "默认";
}
}
// 方案二:返回新对象,由调用方接收
static String[] createDefault() {
return new String[]{"默认"};
}
// 调用方:data = createDefault();推荐方案二:不改传入的参数,返回新对象——副作用更少,代码更好测。
文件:labs/java-syntax/src/main/java/cn/kb/learn/syntax/MethodsLesson.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);
}
}运行命令:
cd labs
./mvnw -pl java-syntax compile
java -cp java-syntax/target/classes cn.kb.learn.syntax.MethodsLesson预期输出:
方法外的 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 又没生效(换引用没用)。
changePrimitive(number); 处按 F7 进入方法,观察参数 value 是 10;执行 value = 999 后,Frames 面板里调用方的 number 仍然是 10——两个变量各有一份;changeContent(data); 处 F7 进入,展开 values 与调用方的 data,看 id 相同(同一个数组对象);reassign(data); 里 F7 逐步执行,在 values = new int[]{7, 7, 7} 之后,看 values 的 id 变了,而调用方的 data 还是老 id;factorial(5) 练调用栈:在方法首行下断点,连续 F7,观察 Frames 面板堆到 5 层后逐层返回(《Java 程序的运行与调试》 讲过同样的方法)。见 6.3 节。修复:改内容而不是换引用,或返回新对象让调用方接收。
static int count(int n) {
return count(n - 1); // 永远不返回
}Exception in thread "main" java.lang.StackOverflowError修复:先写出口条件,再写递归调用。写完后问自己:"什么情况下它不再调用自己?"
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 这种容易被隐式转换搞混的组合。
static int sum(int... numbers) {
return numbers.length; // numbers 可能是 null
}
sum(null); // 编译通过,运行时 NPE修复:方法内先判空,或在文档注释里写明"不要传 null"。
目标:把一段 main 里的逻辑重构为多个可测试的方法。
步骤:
average、max、min、passRate;IllegalArgumentException);main 里依次调用它们,输出结果;void fillZero(int[] values) 把数组全部置零,确认调用方的数组被修改;再写一个 int[] createZero(int size) 返回新数组,确认两种方式的差别。验收方式:能解释第 5 步两个方法为什么一个"改了原数组"、一个"没改"。
扩展任务:写一个递归方法 long countFiles(String path) 统计某个目录下的所有文件数(含子目录),体会递归在"树形结构"上的表达力。(提示:用 java.io.File 的 listFiles(),注意判空)
基础题
代码阅读题
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));b 没变?编程题
isPrime(int n) 判断质数,要求: isPrime(long n)(提示:注意循环变量的类型选择)。综合任务
NumberUtils: int max(int... numbers):返回最大值,参数为空时抛异常;double average(int... numbers):返回平均值,参数为空时返回 0;long factorial(int n):递归实现,n < 0 时抛异常;boolean isPalindrome(int n):判断回文数字;提示 1(方向):第 4 题的关键是 other = new int[]{9, 9} 改的是方法内的局部引用。
提示 2(关键点):第 5 题的试除上界是 i <= Math.sqrt(n),或者写成 i * i <= n(避免浮点误差,但要注意 i * i 溢出)。
提示 3(接近答案):第 6 题的 isPalindrome 可以把数字反转后比较,或用字符串双指针;注意负数一般不算回文。
[100, 2]
[3, 4]arr[0] = 100:改的是数组内容,arr 与调用方的 a 指向同一个数组,所以 a 变了;other = new int[]{9, 9}:改的是方法内局部引用,让它指向一个新数组,与调用方的 b 再无关系,所以 b 不变。这一题把"改内容生效、换引用不生效"同时演示了一遍,是理解值传递最好的一道题。
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 溢出。
预期输出:
false
true
true
false
truepackage 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 会溢出(《变量与数据类型》 的溢出问题)。
预期输出:
9
2.5
0.0
120
true
falsemain 抽成方法,为写测试做准备。语法基础到这里告一段落。《类与对象》:从"写方法"进入"设计对象",开始真正的面向对象。