第三单元 · 数组应用、方法、综合 · 第 8 / 12 节
方法:把一段逻辑装起来
定义无参/带参/带返回值的方法,区分 void 与 return,用参数传数组、用返回值带出结果,浅讲值传递,按需写重载。
左边网页看步骤,右边终端敲命令。bash 块复制到终端($ 会被复制按钮去掉),java 块写进块头标明的文件,output 块只读、用来对照,input 块只读、本节没有。
上一节所有逻辑都挤在 main 里,打印数组的循环写了两遍。这一节学方法:把一段有名字、可重复使用的逻辑从 main 里搬出来,需要时按名字调用。方法仍然是写在类里面的,不涉及“对象”;创建对象是下一单元的事。
先讲清楚一个你马上会看到的 static:
本节所有辅助方法都写成
static,因为main自己是static,同一个类里被main直接调用的方法也必须先写成static。这只是“现阶段从main直接调用”的写法,**不等于所有方法都必须static**。等下一单元有了对象,方法通常不带static,通过对象名调用。static的完整含义(静态变量、静态代码块、工具类)本阶段不展开,这里先照写。
$ 提示符;输出和输入示例没有复制按钮。已完成 0 / 7 步
第 1 步:把重复的两行输出抽成无参方法
先看没有方法时的重复:想打印两次分隔线,就得把同一条 println 写两次;想改分隔线样式,得改两处。把这条语句搬进一个方法,起个名字,以后调用名字就行。
方法的定义写在类里面、main 外面,不能写在 main 的大括号里面。
$ source ~/java-practice/env.sh
$ java -version
$ mkdir -p ~/java-practice/lesson08
$ cd ~/java-practice/lesson08
$ if [ -e MethodBasics.java ]; then mv MethodBasics.java "MethodBasics.java.bak.$(date +%s)"; fi
if [ -e ... ] 这行仍然是“文件已存在就先改名备份”,不会覆盖你上次写的内容。
$ nano MethodBasics.java
public class MethodBasics {
public static void main(String[] args) {
printHeader();
System.out.println("第 1 行成绩:72");
printHeader();
System.out.println("第 2 行成绩:88");
}
static void printHeader() {
System.out.println("---- 成绩单 ----");
}
}
$ javac -encoding UTF-8 MethodBasics.java
$ java MethodBasics
---- 成绩单 ----
第 1 行成绩:72
---- 成绩单 ----
第 2 行成绩:88
拆开看方法定义的第一行 static void printHeader():printHeader 是方法名,括号里是参数列表(空表示不需要外部给数据),void 表示“执行完就回来,不带回结果”。调用写 printHeader();,名字后面必须有括号,语句末尾有分号。
程序执行到 printHeader(); 会跳到方法体,打印完分隔线再回到调用处的下一行。方法体只有一行,看不出威力;等它有三五条语句、被调用十次,你只改方法体就能全部生效。
改一改:在 main 里加第三行成绩,并在它前面再调用一次 printHeader()。需要改方法体吗?
参考答案 对答案
不需要。只加一行 printHeader(); 和一行 System.out.println("第 3 行成绩:95"); 即可,输出会多出一组“分隔线 + 成绩”。方法体不变,这正是复用的意义。
第 2 步:给方法加参数:把变化的部分传进去
printHeader 每次都打印同一句话,因为它没有参数。如果每行成绩的科目和分数都不同,就得把“变化的部分”作为参数传进去。
参数写在方法名后面的括号里,每个参数都有类型和名字;调用时按位置和类型把实参传进去。方法里的参数就是几个局部变量,方法开始时由调用者赋值。
$ cd ~/java-practice/lesson08
$ if [ -e MethodParams.java ]; then mv MethodParams.java "MethodParams.java.bak.$(date +%s)"; fi
$ nano MethodParams.java
public class MethodParams {
public static void main(String[] args) {
printScore("语文", 72);
printScore("数学", 88);
printScore("英语", 95);
}
static void printScore(String subject, int score) {
System.out.println(subject + ":" + score + " 分");
}
}
$ javac -encoding UTF-8 MethodParams.java
$ java MethodParams
语文:72 分
数学:88 分
英语:95 分
定义里的 subject、score 叫形参,调用里的 "语文"、72 叫实参。它们按位置一一对应:第一个实参必须是 String,第二个必须是 int。方法体里直接用形参名,就像使用普通变量。
同一个方法被调用三次,每次形参拿到不同的值,所以输出三行不同内容。方法体只写了一次。
改一改:如果把第一次调用写成 printScore(72, "语文"),会发生什么?
参考答案 对答案
编译失败,提示类型不匹配(类似 incompatible types: int cannot be converted to String)。参数按位置匹配,第一个位置要 String,你给了 int,顺序不能凭感觉调换。
第 3 步:用返回值把结果带出来
前面的方法都是 void:执行完就回来,不带结果。如果方法算出一个数,调用者还要拿它继续算,就需要返回值:把定义里的 void 换成结果类型,方法体里用 return 把值交回去。
return 有两个作用:交出这个值,并立刻结束方法。return 后面的语句不会执行。
$ cd ~/java-practice/lesson08
$ if [ -e MethodReturn.java ]; then mv MethodReturn.java "MethodReturn.java.bak.$(date +%s)"; fi
$ nano MethodReturn.java
public class MethodReturn {
public static void main(String[] args) {
int total = add(72, 88);
double avg = average(total, 2);
System.out.println("两人总分:" + total);
System.out.println("两人平均分:" + avg);
}
static int add(int a, int b) {
return a + b;
}
static double average(int total, int count) {
return (double) total / count;
}
}
$ javac -encoding UTF-8 MethodReturn.java
$ java MethodReturn
两人总分:160
两人平均分:80.0
add 的返回类型是 int,所以 int total = add(72, 88); 合法:调用表达式本身就是一个 int 值。average 返回 double,所以用 double 变量接。返回类型不是装饰,它决定调用者能拿这个结果做什么。
规则要记牢:声明了非 void 返回类型的方法,所有可能的执行路径都必须 return 一个同类型的值;void 方法可以写 return; 提前结束,但不能 return 一个值。
改一改:把 average 的方法体改成 return total / count;,再把调用改成 average(total, 3),输出会是多少?
参考答案 对答案
160 / 3 是两个 int 相除,得到 53,再自动转成 double 返回,输出 两人平均分:53.0。正确的小数结果需要先转换:return (double) total / count;,此时是 53.333333333333336。返回类型是 double 不代表除法会自动按小数算。
第 4 步:把整个数组交给方法
参数类型可以写 int[]。调用时传数组名,方法里就能用 length 和下标遍历整个数组。这样求和、求最值的逻辑只写一次,换个数组还能继续用。
$ cd ~/java-practice/lesson08
$ if [ -e ArrayParam.java ]; then mv ArrayParam.java "ArrayParam.java.bak.$(date +%s)"; fi
$ nano ArrayParam.java
public class ArrayParam {
public static void main(String[] args) {
int[] scores = {72, 88, 95, 61, 78, 88, 54, 100};
int[] quiz = {10, 20, 30};
System.out.println("总分:" + sum(scores));
System.out.println("最高分:" + max(scores));
System.out.println("小测总分:" + sum(quiz));
System.out.println("调用后数组长度:" + scores.length);
}
static int sum(int[] scores) {
int total = 0;
for (int i = 0; i < scores.length; i++) {
total += scores[i];
}
return total;
}
static int max(int[] scores) {
int result = scores[0];
for (int i = 1; i < scores.length; i++) {
if (scores[i] > result) {
result = scores[i];
}
}
return result;
}
}
$ javac -encoding UTF-8 ArrayParam.java
$ java ArrayParam
总分:636
最高分:100
小测总分:60
调用后数组长度:8
sum 和 max 各写一次,scores 和 quiz 两个长度不同的数组都能用。方法里的形参名也叫 scores,它和 main 里的 scores 是两个不同的局部变量,只是恰好同名;方法只认自己收到的那一个数组。
max 假设数组至少有一个元素,因为第一行就访问 scores[0]。上一节学过:调用前要么保证数组非空,要么在方法里先判断 length。这一节先保持“调用者保证非空”,把注意力放在方法本身。
改一改:如果调用 max(new int[0]),会先在哪一行出问题?
参考答案 对答案
在 int result = scores[0]; 这一行,空数组没有下标 0,会抛 ArrayIndexOutOfBoundsException。要支持空数组,方法开头得先判断 if (scores.length == 0) 并返回一个约定值,或者让调用者先挡住空数组。
第 5 步:值传递:基本类型改不动,数组元素改得动
Java 调用方法时,传的都是值的副本,这叫值传递。区别在于“值”是什么:
- 基本类型(
int、double、boolean…)的值就是数字本身,方法拿到的是数字的副本,改副本不影响main。 - 数组变量的值是一个“指向数组的引用”,方法拿到的是引用的副本;副本和原变量指向同一个数组,所以通过副本改元素,
main看得见。 - 但如果在方法里让参数指向一个新数组,只是让副本改了指向,
main的变量还指着原来的数组。
$ cd ~/java-practice/lesson08
$ if [ -e ValuePass.java ]; then mv ValuePass.java "ValuePass.java.bak.$(date +%s)"; fi
$ nano ValuePass.java
public class ValuePass {
public static void main(String[] args) {
int x = 10;
changeNumber(x);
System.out.println("调用 changeNumber 后,main 的 x:" + x);
int[] scores = {72, 88, 95};
changeFirst(scores);
System.out.println("调用 changeFirst 后,scores[0]:" + scores[0]);
reassign(scores);
System.out.println("调用 reassign 后,scores[0]:" + scores[0]);
}
static void changeNumber(int n) {
n = 99;
System.out.println("方法里的 n:" + n);
}
static void changeFirst(int[] arr) {
arr[0] = 99;
System.out.println("方法里的 arr[0]:" + arr[0]);
}
static void reassign(int[] arr) {
int[] fresh = {0, 0, 0};
arr = fresh;
System.out.println("方法里重新赋值后 arr[0]:" + arr[0]);
}
}
$ javac -encoding UTF-8 ValuePass.java
$ java ValuePass
方法里的 n:99
调用 changeNumber 后,main 的 x:10
方法里的 arr[0]:99
调用 changeFirst 后,scores[0]:99
方法里重新赋值后 arr[0]:0
调用 reassign 后,scores[0]:99
changeNumber 里把 n 改成 99,main 的 x 仍是 10:改的是副本。changeFirst 里改 arr[0],main 的 scores[0] 变成 99:两个变量指向同一个数组。reassign 里先建一个新数组 fresh,再执行 arr = fresh;,只是让形参 arr 改指这个新数组,main 的 scores 没变,所以最后仍打印 99。
不要把它记成“基本类型传值、数组传引用”两句口诀——Java 只有值传递;数组特殊在“值是引用”,复制的是引用。下一单元学对象时也是同一套规则,现在把这一层看清楚就够了。
改一改:为什么 changeFirst 能改 main 的数组,reassign 不能?
参考答案 对答案
两个方法拿到的都是引用的副本,副本都指向 main 的同一个数组。changeFirst 通过副本修改了数组里的元素,原数组真的变了;reassign 把副本重新指向一个新数组,只是副本自己换了目标,main 的 scores 仍指向原数组。
第 6 步:方法重载:同名方法,参数列表不同
同一件事对不同数量的参数,可以起同一个名字,这叫方法重载。编译器在调用处看实参的个数和类型,决定进哪个方法;返回类型不参与这个决定。
$ cd ~/java-practice/lesson08
$ if [ -e Overload.java ]; then mv Overload.java "Overload.java.bak.$(date +%s)"; fi
$ nano Overload.java
public class Overload {
public static void main(String[] args) {
System.out.println("两个数较大值:" + max(72, 88));
System.out.println("三个数较大值:" + max(72, 88, 95));
}
static int max(int a, int b) {
if (a > b) {
return a;
}
return b;
}
static int max(int a, int b, int c) {
int result = max(a, b);
if (c > result) {
result = c;
}
return result;
}
}
$ javac -encoding UTF-8 Overload.java
$ java Overload
两个数较大值:88
三个数较大值:95
两个 max 的参数列表不同(两个 int vs 三个 int),所以能共存。调用 max(72, 88) 时编译器数到两个实参,进第一个;max(72, 88, 95) 进第二个。三个参数的版本还调用了两个参数的版本,避免重复写比较逻辑。
重载看的是“方法名 + 参数列表”,不看返回类型。下面两个方法不能共存:
static int max(int a, int b) { ... }
static double max(int a, int b) { ... }
参数列表一模一样,只有返回类型不同,编译器会报“方法已定义”(duplicate method)。仅返回值不同不是重载。
改一改:static void show(int a) 和 static void show(double a) 能构成重载吗?调用 show(5) 会进哪个?
参考答案 对答案
能,参数类型不同就是重载。show(5) 的实参是 int,优先精确匹配 show(int);如果只写 show(5.0),进 show(double)。类型不同、个数不同都算参数列表不同。
第 7 步:把上一节的统计代码拆成三个方法
现在把上一节(lesson07)的“总分、平均分、最高分”从一段 main 拆成三个方法。main 只负责准备数据和打印,计算交给方法;average 还可以调用 total,避免把求和循环再写一遍。
$ cd ~/java-practice/lesson08
$ if [ -e MethodRefactor.java ]; then mv MethodRefactor.java "MethodRefactor.java.bak.$(date +%s)"; fi
$ nano MethodRefactor.java
public class MethodRefactor {
public static void main(String[] args) {
int[] scores = {72, 88, 95, 61, 78, 88, 54, 100};
System.out.println("总分:" + total(scores));
System.out.println("平均分:" + average(scores));
System.out.println("最高分:" + max(scores));
}
static int total(int[] scores) {
int sum = 0;
for (int i = 0; i < scores.length; i++) {
sum += scores[i];
}
return sum;
}
static double average(int[] scores) {
return (double) total(scores) / scores.length;
}
static int max(int[] scores) {
int result = scores[0];
for (int i = 1; i < scores.length; i++) {
if (scores[i] > result) {
result = scores[i];
}
}
return result;
}
}
$ javac -encoding UTF-8 MethodRefactor.java
$ java MethodRefactor
总分:636
平均分:79.5
最高分:100
average 里调用 total(scores):方法可以调用同一个类里的其他 static 方法,调用规则和 main 里一样。main 现在只剩“准备数组 + 三次打印”,每段计算都有名字,改求和逻辑不用碰打印。
方法不是越多越好。一段只在两个地方出现、只有一行的逻辑,硬抽成方法反而增加跳转;判断标准是“这段逻辑有没有独立的名字和职责,会不会被重复使用”。下一节的成绩统计会按“输入、统计、输出”三类职责来拆。
改一改:给这个类加一个 static int min(int[] scores),并在 main 里打印最低分。先自己写,再对答案。
参考答案 对答案
static int min(int[] scores) {
int result = scores[0];
for (int i = 1; i < scores.length; i++) {
if (scores[i] < result) {
result = scores[i];
}
}
return result;
}
在 main 里加一行 System.out.println("最低分:" + min(scores));,输出 最低分:54。结构跟 max 一样,只把 > 换成 <。
常见错误
下面按“现象 → 常见原因 → 先检查什么”列出;报错文字会随 JDK 补丁和系统语言略有差异,认关键词和原因即可,不必逐字一致。
- **
non-static method printHeader() cannot be referenced from a static context**:辅助方法没写static,却被static的main直接调用。现象是编译报“无法从静态上下文引用非静态方法”。本阶段在方法定义前加static;这不是“所有方法都必须 static”,只是还没有对象可用来调用它。 - **
missing return statement**:方法声明了int/double等返回类型,但某条执行路径没有return。先看if的两个分支是否都返回,再看方法末尾有没有兜底的return。 - **
incompatible types: unexpected return value**(旧版 javac 也可能显示cannot return a value from method whose result type is void):void方法里写了return 值;。要么把返回类型改成值的类型,要么把return 值;改成return;或删掉。 - **
illegal start of expression,方法写在main里面**:Java 不允许方法嵌套定义。检查大括号层级,方法必须和main平级,写在类里面、main外面。 - **
incompatible types,实参和形参对不上**:调用时参数顺序或类型错了,例如printScore(72, "语文")。按方法定义的顺序逐个核对,String对String、int对int。 - **
method max(int,int) is already defined**:两个方法参数列表相同,只有返回类型不同。重载只认参数个数和类型;改参数列表,或改方法名。 - **在方法里改了
int参数,main的值没变**:这是值传递的正常结果,不是 bug。基本类型传的是副本;要带回结果,用返回值,而不是指望改形参。 - **数组参数一改,
main的数组也变了,以为出了错**:数组变量存的是引用,方法拿到引用的副本,副本和原变量指向同一个数组。改元素会互相看见;不想让方法改原数组,就在方法里新建一个数组再操作。
练习
- 写一个
static void printStars(int n),在一行里打印n个*,然后换行。在main里调用printStars(3)和printStars(5)。 - 写一个
static boolean isPass(int score),分数大于等于 60 返回true,否则返回false;在main里打印isPass(59)和isPass(60)。 - 判断下面两组方法能不能构成重载: A.
static int f(int a)与static int f(int a, int b)B.static int g(int a)与static double g(int a)
先自己写、自己判断,再展开答案。
参考答案 对答案
static void printStars(int n) {
String line = "";
for (int i = 0; i < n; i++) {
line = line + "*";
}
System.out.println(line);
}
printStars(3) 输出 ***,printStars(5) 输出 *****。循环次数由参数 n 决定,方法体不用改。
static boolean isPass(int score) {
if (score >= 60) {
return true;
}
return false;
}
也可以写成 return score >= 60;,比较表达式本身就是 boolean。isPass(59) 是 false,isPass(60) 是 true,边界包含 60。
- A 能,参数个数不同;B 不能,参数列表完全相同,只有返回类型不同,编译器会报重复定义。
做到这里
做到这里今天可以停。你已经会定义无参、带参、带返回值的方法,能区分 void 和 return,能把数组作为参数传进去,能用返回值把结果带出来,也知道了基本类型和数组引用在值传递上的差别。
现在你写的方法都带 static,因为都从 static 的 main 直接调用。下一节用方法把成绩统计拆成“输入、统计、输出”,仍然不创建对象;类和对象在第四单元才出现。
来源 来源与改编说明(非官方)
主题参考:黑马程序员 Java 入门到起飞(BV17F411T7Ao,2022-04-27)P063–P073 的方法定义与调用、参数、返回值、方法重载、基本类型与引用类型的值传递主题。本站为独立原创讲解,非官方站点/教材,未获授权,不逐字复刻视频或讲义;示例、错误清单与练习为原创补充。详见 source/SOURCE.md。
说明:本站是个人学习整理,不是官方教材;不包含原课程的书本、视频或受版权保护的整本内容。原课程与商标归其权利人。