Java从环境到类与对象 · SSH 实操

第三单元 · 数组应用、方法、综合 · 第 8 / 12 节

方法:把一段逻辑装起来

定义无参/带参/带返回值的方法,区分 void 与 return,用参数传数组、用返回值带出结果,浅讲值传递,按需写重载。

约 35 分钟 · 7 个步骤 · 进度只存本机

左边网页看步骤,右边终端敲命令。bash 块复制到终端($ 会被复制按钮去掉),java 块写进块头标明的文件,output 块只读、用来对照,input 块只读、本节没有。

上一节所有逻辑都挤在 main 里,打印数组的循环写了两遍。这一节学方法:把一段有名字、可重复使用的逻辑从 main 里搬出来,需要时按名字调用。方法仍然是写在类里面的,不涉及“对象”;创建对象是下一单元的事。

先讲清楚一个你马上会看到的 static:

本节所有辅助方法都写成 static,因为 main 自己是 static,同一个类里被 main 直接调用的方法也必须先写成 static。这只是“现阶段从 main 直接调用”的写法,**不等于所有方法都必须 static**。等下一单元有了对象,方法通常不带 static,通过对象名调用。static 的完整含义(静态变量、静态代码块、工具类)本阶段不展开,这里先照写。

SSH 服务器终端Java 源码程序交互输入输出示例(只读)复制 Java 源码不带文件名;复制命令不带 $ 提示符;输出和输入示例没有复制按钮。
这是教程网页,不是在线运行环境。Java 代码和 SSH 命令都要在你的服务器上执行;网页不会执行代码、不会连接服务器、也不会接收密码。

已完成 0 / 7 步

第 1 步:把重复的两行输出抽成无参方法

先看没有方法时的重复:想打印两次分隔线,就得把同一条 println 写两次;想改分隔线样式,得改两处。把这条语句搬进一个方法,起个名字,以后调用名字就行。

方法的定义写在类里面、main 外面,不能写在 main 的大括号里面。

SSH 服务器终端bash
$ 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 ... ] 这行仍然是“文件已存在就先改名备份”,不会覆盖你上次写的内容。

SSH 服务器终端bash
$ nano MethodBasics.java
Java 源码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("---- 成绩单 ----");
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 MethodBasics.java
$ java MethodBasics
输出示例(只读)output不可复制
---- 成绩单 ----
第 1 行成绩:72
---- 成绩单 ----
第 2 行成绩:88

拆开看方法定义的第一行 static void printHeader():printHeader 是方法名,括号里是参数列表(空表示不需要外部给数据),void 表示“执行完就回来,不带回结果”。调用写 printHeader();,名字后面必须有括号,语句末尾有分号。

程序执行到 printHeader(); 会跳到方法体,打印完分隔线再回到调用处的下一行。方法体只有一行,看不出威力;等它有三五条语句、被调用十次,你只改方法体就能全部生效。

改一改:在 main 里加第三行成绩,并在它前面再调用一次 printHeader()。需要改方法体吗?

参考答案 对答案

不需要。只加一行 printHeader(); 和一行 System.out.println("第 3 行成绩:95"); 即可,输出会多出一组“分隔线 + 成绩”。方法体不变,这正是复用的意义。

第 2 步:给方法加参数:把变化的部分传进去

printHeader 每次都打印同一句话,因为它没有参数。如果每行成绩的科目和分数都不同,就得把“变化的部分”作为参数传进去。

参数写在方法名后面的括号里,每个参数都有类型和名字;调用时按位置和类型把实参传进去。方法里的参数就是几个局部变量,方法开始时由调用者赋值。

SSH 服务器终端bash
$ cd ~/java-practice/lesson08
$ if [ -e MethodParams.java ]; then mv MethodParams.java "MethodParams.java.bak.$(date +%s)"; fi
SSH 服务器终端bash
$ nano MethodParams.java
Java 源码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 + " 分");
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 MethodParams.java
$ java MethodParams
输出示例(只读)output不可复制
语文: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 后面的语句不会执行。

SSH 服务器终端bash
$ cd ~/java-practice/lesson08
$ if [ -e MethodReturn.java ]; then mv MethodReturn.java "MethodReturn.java.bak.$(date +%s)"; fi
SSH 服务器终端bash
$ nano MethodReturn.java
Java 源码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;
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 MethodReturn.java
$ java MethodReturn
输出示例(只读)output不可复制
两人总分: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 和下标遍历整个数组。这样求和、求最值的逻辑只写一次,换个数组还能继续用。

SSH 服务器终端bash
$ cd ~/java-practice/lesson08
$ if [ -e ArrayParam.java ]; then mv ArrayParam.java "ArrayParam.java.bak.$(date +%s)"; fi
SSH 服务器终端bash
$ nano ArrayParam.java
Java 源码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;
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 ArrayParam.java
$ java ArrayParam
输出示例(只读)output不可复制
总分: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 的变量还指着原来的数组。
SSH 服务器终端bash
$ cd ~/java-practice/lesson08
$ if [ -e ValuePass.java ]; then mv ValuePass.java "ValuePass.java.bak.$(date +%s)"; fi
SSH 服务器终端bash
$ nano ValuePass.java
Java 源码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]);
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 ValuePass.java
$ java ValuePass
输出示例(只读)output不可复制
方法里的 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 步:方法重载:同名方法,参数列表不同

同一件事对不同数量的参数,可以起同一个名字,这叫方法重载。编译器在调用处看实参的个数和类型,决定进哪个方法;返回类型不参与这个决定。

SSH 服务器终端bash
$ cd ~/java-practice/lesson08
$ if [ -e Overload.java ]; then mv Overload.java "Overload.java.bak.$(date +%s)"; fi
SSH 服务器终端bash
$ nano Overload.java
Java 源码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;
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 Overload.java
$ java Overload
输出示例(只读)output不可复制
两个数较大值:88
三个数较大值:95

两个 max 的参数列表不同(两个 int vs 三个 int),所以能共存。调用 max(72, 88) 时编译器数到两个实参,进第一个;max(72, 88, 95) 进第二个。三个参数的版本还调用了两个参数的版本,避免重复写比较逻辑。

重载看的是“方法名 + 参数列表”,不看返回类型。下面两个方法不能共存:

输出示例(只读)text不可复制
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,避免把求和循环再写一遍。

SSH 服务器终端bash
$ cd ~/java-practice/lesson08
$ if [ -e MethodRefactor.java ]; then mv MethodRefactor.java "MethodRefactor.java.bak.$(date +%s)"; fi
SSH 服务器终端bash
$ nano MethodRefactor.java
Java 源码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;
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 MethodRefactor.java
$ java MethodRefactor
输出示例(只读)output不可复制
总分:636
平均分:79.5
最高分:100

average 里调用 total(scores):方法可以调用同一个类里的其他 static 方法,调用规则和 main 里一样。main 现在只剩“准备数组 + 三次打印”,每段计算都有名字,改求和逻辑不用碰打印。

方法不是越多越好。一段只在两个地方出现、只有一行的逻辑,硬抽成方法反而增加跳转;判断标准是“这段逻辑有没有独立的名字和职责,会不会被重复使用”。下一节的成绩统计会按“输入、统计、输出”三类职责来拆。

改一改:给这个类加一个 static int min(int[] scores),并在 main 里打印最低分。先自己写,再对答案。

参考答案 对答案
输出示例(只读)text不可复制
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 的数组也变了,以为出了错**:数组变量存的是引用,方法拿到引用的副本,副本和原变量指向同一个数组。改元素会互相看见;不想让方法改原数组,就在方法里新建一个数组再操作。

练习

  1. 写一个 static void printStars(int n),在一行里打印 n 个 *,然后换行。在 main 里调用 printStars(3) 和 printStars(5)。
  2. 写一个 static boolean isPass(int score),分数大于等于 60 返回 true,否则返回 false;在 main 里打印 isPass(59) 和 isPass(60)。
  3. 判断下面两组方法能不能构成重载: A. static int f(int a) 与 static int f(int a, int b) B. static int g(int a) 与 static double g(int a)

先自己写、自己判断,再展开答案。

参考答案 对答案
输出示例(只读)text不可复制
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 决定,方法体不用改。

输出示例(只读)text不可复制
static boolean isPass(int score) {
    if (score >= 60) {
        return true;
    }
    return false;
}

也可以写成 return score >= 60;,比较表达式本身就是 boolean。isPass(59) 是 false,isPass(60) 是 true,边界包含 60。

  1. A 能,参数个数不同;B 不能,参数列表完全相同,只有返回类型不同,编译器会报重复定义。

做到这里

做到这里今天可以停。你已经会定义无参、带参、带返回值的方法,能区分 void 和 return,能把数组作为参数传进去,能用返回值把结果带出来,也知道了基本类型和数组引用在值传递上的差别。

现在你写的方法都带 static,因为都从 static 的 main 直接调用。下一节用方法把成绩统计拆成“输入、统计、输出”,仍然不创建对象;类和对象在第四单元才出现。

来源 来源与改编说明(非官方)

主题参考:黑马程序员 Java 入门到起飞(BV17F411T7Ao,2022-04-27)P063–P073 的方法定义与调用、参数、返回值、方法重载、基本类型与引用类型的值传递主题。本站为独立原创讲解,非官方站点/教材,未获授权,不逐字复刻视频或讲义;示例、错误清单与练习为原创补充。详见 source/SOURCE.md。

说明:本站是个人学习整理,不是官方教材;不包含原课程的书本、视频或受版权保护的整本内容。原课程与商标归其权利人。