LW Leon 学习

Java 入门第一步

给从没写过程序的人准备的八节小课:编译和运行是什么,变量,if 和 for,方法,数组,字符串,第一个类,以及怎样读懂报错信息。学完以后,再读 Java 课程的第一章就能跟上。

开始前需要什么

  • 一台允许你安装软件的电脑:Windows 10 或 11、macOS 或 Linux。只有“准备你的电脑”这一节才用得到它。每个例子也能直接在本页运行,什么都不用安装。
  • 不需要任何编程经验。你需要会新建并保存纯文本文件,并且照着示例一个字符一个字符地敲。Java 区分大小写,少一个括号就可能整个程序都不能用。
  • 第一次准备环境大约 30 分钟,之后每节课 15 到 25 分钟。每个例子都自己敲一遍,不要只看。
  • 一个较新的浏览器来使用“运行”按钮(Chrome 或 Edge 119、Firefox 120、Safari 18.2,或更新的版本),再准备纸和笔。手写小程序是学会它们的好办法。

准备你的电脑

一开始可以跳过:下面的例子在这个页面上就能运行。

JDK(Java Development Kit,Java 开发工具包)是一套免费的工具,用来在你自己的电脑上编译并运行 Java 程序。里面有两个你会天天用到的程序:javac 是编译器,java 负责运行你的代码。

这一节现在可以先跳过。点开本页任意一个例子下面的“运行”,代码就在浏览器里运行。但要分清楚:运行按钮用的是精简过的 Java 标准库,够这些课使用,不过有些程序在那里的表现不一样,有些根本跑不起来。所以还是值得装一个真正的 JDK,你的课程也会要求你用真正的 JDK。

我们用 Adoptium 提供的 Eclipse Temurin,它是开源 OpenJDK 的一个免费、广泛使用的版本。请选择最新的 LTS 版本。LTS 是长期支持版:这个版本会连续多年得到更新。2026 年 11 月最新的 LTS 是 Java 25;Java 21 和 Java 17 也是 LTS,本指南里的所有内容都能用。当前的版本号请到 adoptium.net 上确认。

下面会遇到两个词。终端是你用文字输入命令的窗口,Windows 上叫“命令提示符”(Command Prompt)或 PowerShell,macOS 上叫“终端”(Terminal)。PATH 是一份文件夹清单:你在终端里输入一个命令名时,系统会到清单里的文件夹去找这个命令。如果存放 java 和 javac 的文件夹不在清单里,终端就会说找不到它们。

如果你的电脑设置的是别的语言,Java 可能用那种语言显示报错信息。想看英文报错,在 javac 后面加上 -J-Duser.language=en,例如:javac -J-Duser.language=en Main.java

Windows
  1. 在浏览器里打开 https://adoptium.net/temurin/releases/ 。

  2. 这个页面让你选择版本、包类型、操作系统和架构。选标有 LTS 的版本(写作时是 25),类型选 JDK,操作系统选 Windows,架构选 x64。下载以 .msi 结尾的文件。大多数 Windows 电脑是 x64。如果你的电脑用的是 Arm 处理器(在“设置”“系统”“关于”的“系统类型”里查看),请改在 WSL 里按 Linux 的步骤来做。

  3. 双击这个 .msi 文件,接受许可协议。在“Custom Setup”(自定义安装)界面,确认“Add to PATH”处于开启状态,它默认就是开启的。“Set JAVA_HOME”是可选项,本指南用不到。

  4. 点“Next”,再点“Install”。出现权限提示时选择允许。最后点“Finish”。

  5. 打开一个新的命令提示符:按 Windows 键,输入 cmd,按回车。安装时就已经开着的窗口看不到这次改动,所以一定要开新窗口。

  6. 喜欢用命令的人有个捷径:不做第 1 到 4 步,直接在终端里运行 winget install EclipseAdoptium.Temurin.25.JDK,然后从第 5 步接着做。

检查是否成功

java -version
javac -version

你应该看到 两条命令,一共四行。java -version 输出三行:第一行以 openjdk version 和你安装的版本号开头,后两行里有 Temurin。javac -version 输出一行,例如 javac 25.0.1。具体数字会和这里不同。

macOS
  1. 先弄清你的 Mac 用的是哪种芯片:打开苹果菜单,选“关于本机”。看到 Apple M1、M2、M3 之类的名字,就是 Apple 芯片;名字里有 Intel,就是 Intel 的 Mac。

  2. 打开 https://adoptium.net/temurin/releases/ 。选标有 LTS 的版本,类型选 JDK,操作系统选 macOS,架构:Apple 芯片选 aarch64,Intel 选 x64。下载以 .pkg 结尾的文件。

  3. 打开这个 .pkg 文件,点“继续”,接受许可协议,点“安装”,需要时输入 Mac 的登录密码。JDK 会被放进 /Library/Java/JavaVirtualMachines/ 文件夹。

  4. 打开“终端”:按 Command 加空格,输入 Terminal,按回车。如果终端窗口在安装前就开着,请再开一个新的。macOS 上不用自己改 PATH,因为 java 和 javac 这两个命令会自动去找已安装的 JDK。

  5. 用 Homebrew 的人有个捷径:不做第 2、3 步,直接运行 brew install --cask temurin@25。

检查是否成功

java -version
javac -version

你应该看到 和 Windows 一样,一共四行:java -version 输出三行,以 openjdk version 开头并提到 Temurin;javac -version 输出一行,例如 javac 25.0.1。

Linux(Debian、Ubuntu、Linux Mint)
  1. 打开终端。在很多桌面系统里,快捷键是 Ctrl+Alt+T。

  2. 先安装接下来的命令要用到的几个小工具:
    sudo apt install -y wget apt-transport-https gpg

  3. 下载 Adoptium 的签名密钥,apt 要靠它检查软件包:
    wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/adoptium.gpg > /dev/null

  4. 添加 Adoptium 的软件源(这是很长的一行):
    echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
    如果用的是 Linux Mint,把 VERSION_CODENAME 换成 UBUNTU_CODENAME。

  5. 更新软件包列表,再安装 JDK:
    sudo apt update
    sudo apt install temurin-25-jdk
    如果最新的 LTS 已经不是 25,就换成新的数字。要装 jdk 包,不要装 jre 包,因为 jre 包里没有 javac。

  6. 其他系统:Fedora、RHEL、CentOS 用 dnf 加上 Adoptium 自己的软件源,做法写在 adoptium.net/installation/linux。在 Windows 上也可以用 WSL(Windows 的 Linux 子系统):在管理员身份的 PowerShell 里运行 wsl --install,重启后在 Ubuntu 里照上面的步骤做。

检查是否成功

java -version
javac -version

你应该看到 一共四行:java -version 输出三行,以 openjdk version 开头并提到 Temurin;javac -version 输出一行,例如 javac 25.0.1。

如果看到报错

  • 'javac' is not recognized as an internal or external command, operable program or batch file.

    Windows 找不到 javac。按顺序检查三个原因。第一,窗口是在安装完成之前打开的:关掉它,再开一个新的。第二,Java 没有被加进 PATH:打开“设置”,进入“应用”,找到 Eclipse Temurin JDK,选“修改”,把“Add to PATH”打开。第三,你装的是 JRE,它能运行程序但没有 javac:请安装 JDK。PowerShell 的说法不同:The term 'javac' is not recognized as the name of a cmdlet, function, script file, or operable program。意思是一样的。

  • javac: command not found

    终端找不到 javac。macOS 的 zsh 会写成 zsh: command not found: javac,macOS 也可能改说 Unable to locate a Java Runtime。请按上面的步骤安装 JDK。如果刚装完,先关掉终端再开一个新的。如果 java -version 能用而 javac 不能用,说明你装成了 JRE 而不是 JDK。在 Linux 上请装 temurin-25-jdk,不要装 temurin-25-jre。Ubuntu 可能会打印 Command 'javac' not found, but can be installed with: 和一串软件包。你可以不理那一串,改按上面的 Temurin 步骤来做。

  • javac: file not found: Main.java

    javac 在终端当前所在的文件夹里看不到这个名字的文件。用 dir(Windows)或 ls(macOS 和 Linux)列出文件,再用 cd 进入存放文件的文件夹,例如 cd Desktop。仔细核对文件名。Windows 可能把扩展名隐藏起来,所以你命名为 Main.java 的文件,实际上可能是 Main.java.txt。

  • error: class Hello is public, should be declared in a file named Hello.java

    public 的类必须放在同名文件里,大小写也要一样。这里的类叫 Hello,所以文件必须叫 Hello.java。要么给文件改名,要么把类名改成和文件一致。在设置为中文的电脑上,这条信息可能显示为:错误: 类Hello是公共的, 应在名为 Hello.java 的文件中声明。

  • Error: Could not find or load main class Main

    java 命令找不到 Main.class。检查这几个原因。还没有编译:先运行 javac Main.java。你所在的文件夹和 Main.class 不在一起:用 cd 进入正确的文件夹。你输入了 java Main.class:去掉 .class,输入 java Main。大小写不一致:java main 不等于 java Main。改过文件之后要重新编译。后面可能还跟着第二行 Caused by: java.lang.ClassNotFoundException: Main。在中文系统上,第一行可能显示为:错误: 找不到或无法加载主类 Main。

  • error: reached end of file while parsing

    Java 读到文件末尾时,还在等待某样东西,几乎总是一个右花括号 }。每个 { 都要有自己的 }。看看文件最后几行,把代码缩进整齐,再数一数花括号。很多编辑器在光标紧挨着花括号时,会高亮和它配对的那一个。在中文系统上,这条信息可能显示为:错误: 解析时已到达文件结尾。

  • error: cannot find symbol

    Java 不认识你用到的某个名字。信息下面几行会告诉你是哪个名字(symbol,符号),以及 Java 在哪里找过(location,位置)。常见原因:拼写错误(把 total 写成 totl)、大小写不对(把 String 写成 string,把 System 写成 system)、在变量被创建的花括号之外使用它,或者漏写了库类(例如 Arrays)的 import 行。第 8 课会教你怎样读这些信息。在中文系统上,这条信息可能显示为:错误: 找不到符号。

01

源代码、编译、运行:第一个程序

学完你能 你能说清源代码、编译和运行各是什么,并能自己写出、编译、运行一个打印文字的程序。

程序就是写给计算机的一串指令。你把这串指令写成文字,存在一个文件里。这些文字叫源代码。Java 源文件的名字以 .java 结尾。

计算机不能直接照着 Java 文字去做,要先由一个叫编译器的程序把它翻译一遍。Java 的编译器叫 javac。它读取 Main.java,写出一个新文件 Main.class,里面装的是字节码。字节码是一串紧凑的指令,专为 Java 自己的虚拟机准备。这个翻译的步骤叫编译。

接着,Java 虚拟机(Java Virtual Machine,简称 JVM)读取字节码,并执行里面的指令。启动它就叫运行程序。你用 java 这个命令来启动它。

所以永远是两步。你在终端里输入它们,终端就是用来输入命令的窗口(见“准备你的电脑”):

javac Main.java
java Main

第一条命令编译,第二条命令运行。注意第二条只写 Main,没有 .java,也没有 .class,因为你给出的是类的名字,不是文件的名字。较新的 JDK(11 及以上)也接受 java Main.java,一次完成两步。本指南仍然分成两步,让你能看清每一步。

如果你改了源文件,旧的 Main.class 不会跟着变。要重新编译,再重新运行。忘了这一点,是第一天最常见的错误。

下面看程序的基本形状。Java 把所有代码都放在类里面。类是一个有名字的代码容器。public 的类必须和它的文件同名,所以叫 Main 的类放在 Main.java 里。类的里面写一个叫 main 的方法。方法是一段有名字、只做一件事的代码。运行程序时,JVM 会去找 main,并从那里开始。

在方法里,每一条指令叫一条语句。大多数语句以分号结尾。花括号 { } 标出类或方法从哪里开始、到哪里结束。语句 System.out.println 会打印文字,然后换到新的一行。System.out.print 做同样的事,但不换行。文字必须放在双引号里面。

Java 25 还允许一种短得多的程序写法。你的课程用的是这里的经典写法,它在所有版本上都能用。要在本页运行一个例子,点它下面的“运行”。你也可以点“修改代码”,改一点东西,再点一次“运行”。

新词

源代码
人写出来的程序文字,存放在以 .java 结尾的文件里。
编译器(javac)
把源代码翻译成字节码,并报告它发现的错误的程序。
字节码
保存在 .class 文件里的紧凑指令,是为 JVM 准备的。
JVM(Java 虚拟机)
用 java 命令启动的程序,负责执行字节码。
类和方法
类是有名字的代码容器;方法是类里面一段有名字、只做一件事的代码。
语句
一条指令。大多数语句以分号结尾。

完整例子

Java 8+
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
        System.out.println("My name is Leon.");
        System.out.println(2 + 3);
    }
}
先预测输出,再看结果

预期输出

Hello, Java!
My name is Leon.
5

逐步讲解

  1. public class Main { 开始定义类。它叫 Main,所以文件必须叫 Main.java。这个花括号打开类,文件的最后一行把它关上。

  2. public static void main(String[] args) { 开始定义 JVM 最先运行的方法。请原样照抄。现在可以这样理解这几个词:public 表示允许 JVM 调用它;static 表示它属于类本身;void 表示它不返回任何东西;String[] args 是运行程序时写在程序名后面的一串词,我们暂时用不到。

  3. System.out.println("Hello, Java!"); 打印双引号之间的文字,引号本身不会被打印。println 随后换到新的一行。

  4. 下一条 println 打印第二行。每条语句都以分号结尾。

  5. System.out.println(2 + 3); 没有引号,所以 Java 会先算出 2 + 3,再打印答案 5。有引号表示文字,没有引号表示让 Java 去计算。

  6. 最后两个右花括号先结束 main,再结束类。每个 { 都要有自己的 }。

常见错误

错误写法
Main.java.txt

文本编辑器可能会在文件名后面加上 .txt,而 Windows 又会隐藏扩展名,所以你看不到它。这时 javac 会报告:javac: file not found: Main.java。在文件资源管理器里,打开“查看”菜单,勾选“文件扩展名”,就能看到真实的文件名。在记事本的“另存为”窗口里,先把“保存类型”改成“所有文件”,再输入 Main.java。在 Mac 上,“文本编辑”默认保存为富文本:保存之前先选“格式”菜单里的“制作纯文本”,或者改用 VS Code 之类的编辑器。

正确写法
Main.java
错误写法
java Main    (typed after editing Main.java, without compiling again)

java 运行的是 Main.class,也就是旧的编译结果。你改的内容不在里面,所以程序的表现和以前一样,你会奇怪为什么没有变化。要先重新编译。

正确写法
javac Main.java
java Main
错误写法
system.out.println("Hi");

Java 把大写字母和小写字母当作不同的字母,所以 system 和 System 是两个不同的名字。javac 会报:error: package system does not exist。这个类叫 System,S 要大写。

正确写法
System.out.println("Hi");

检查自己

  1. 写出两条命令,先编译、再运行一个 public 类叫 Greeting 的程序。

    先自己做,再看答案

    javac Greeting.java,然后 java Greeting。文件必须叫 Greeting.java,运行命令里不带扩展名。

  2. 修改这个例子,让它的第一行打印你自己的名字,第二行打印 4 * 5 的结果。

    先自己做,再看答案

    例如:System.out.println("My name is Noah."); 再加上 System.out.println(4 * 5); 第二行会打印 20,因为没有引号,Java 会去计算。

  3. 预测 System.out.print("A"); System.out.print("B"); System.out.println("C"); 会打印什么。

    先自己做,再看答案

    在同一行打印 ABC。print 不换行,所以字母连在一起。只有 println 才会结束这一行。

02

变量和类型

学完你能 你能声明 int、double、boolean、char 和 String 类型的变量,修改它们的值,并预测除法和 + 的结果。

变量是计算机内存里一个有名字的位置,里面存放一个值。名字由你来起。Java 还需要知道这个变量允许存放哪一种值,这一种就叫变量的类型。

最先要学的五种类型是:
int:整数,例如 7 或 -3。
double:带小数部分的数,例如 1.5。
boolean:只有 true 或 false 两个值。
char:一个字符,写在单引号里,例如 'A'。
String:文字,写在双引号里,例如 "Mia"。String 的 S 要大写。

声明变量时,依次写出类型、名字和第一个值:int apples = 7; 其中的 = 号表示“把右边的值存进左边的名字里”。它不表示数学里的“等于”。之后你可以存入新的值:apples = apples + 1; 读作“取出 apples 现在的值,加 1,再把结果存回 apples”。这时不用再写类型,因为变量已经存在了。

Java 在程序运行之前就检查类型。它不会把 2.5 存进 int,因为 .5 会丢掉。它还要求每个变量在被使用之前先有一个值。

算术运算有 + - * / 和 %,其中 % 求除法的余数。如果 / 两边都是 int,结果就是 int,小数部分被直接去掉:7 / 2 是 3,不是 3.5。只要有一边是 double,结果就是 double:7 / 2.0 是 3.5。Java 先算 * 和 /,再算 + 和 -,其余情况按从左到右的顺序计算。

+ 号还能把文字连起来。把文字连起来叫拼接。"Mia" + " is " + 7 得到文字 Mia is 7。因为 Java 从左到右计算,所以顺序很重要,下面的常见错误会演示这一点。

新词

变量
内存中一个有名字的位置,里面存放一个值。
类型
变量允许存放的值的种类,例如 int 或 String。
声明和赋值
声明是带着类型创建一个变量;赋值是用 = 把一个值存进去。
整数除法
int 除以 int。结果是 int,小数部分被舍去。
拼接
用 + 把几段文字连成一段。

完整例子

Java 8+
public class Main {
    public static void main(String[] args) {
        String name = "Mia";
        int apples = 7;
        int people = 2;
        double price = 1.5;
        boolean cheap = price < 2.0;
        char grade = 'A';

        System.out.println(name + " has grade " + grade);
        System.out.println("whole share: " + apples / people);
        System.out.println("left over: " + apples % people);
        System.out.println("exact share: " + apples / 2.0);
        System.out.println("total cost: " + apples * price);
        System.out.println("cheap? " + cheap);

        apples = apples + 1;
        System.out.println("apples now: " + apples);
    }
}
先预测输出,再看结果

预期输出

Mia has grade A
whole share: 3
left over: 1
exact share: 3.5
total cost: 10.5
cheap? true
apples now: 8

逐步讲解

  1. String name = "Mia"; 创建了一个叫 name 的 String 变量,并存入文字 Mia。

  2. int apples = 7; 和 int people = 2; 存入整数。double price = 1.5; 存入带小数部分的数。

  3. boolean cheap = price < 2.0; 先计算比较 price < 2.0,结果为 true,再把这个结果存起来。char grade = 'A'; 存入一个字符,要写在单引号里。

  4. 在 "whole share: " + apples / people 里,Java 先做除法,因为 / 排在 + 前面。两个 int 相除,7 / 2 得到 3,.5 被去掉。

  5. apples % people 是 7 除以 2 的余数,等于 1。

  6. apples / 2.0 的右边是 double,所以结果是 double 类型的 3.5。apples * price 是 int 乘 double,结果是 double 类型的 10.5。

  7. apples = apples + 1; 把 8 存进 apples。前面没有类型名,因为这个变量已经存在了。

常见错误

错误写法
int half = 7 / 2.0;

7 / 2.0 是 double 类型的 3.5。int 存不下 .5,所以 javac 拒绝编译:error: incompatible types: possible lossy conversion from double to int。应该选一个装得下这个值的类型。

正确写法
double half = 7 / 2.0;
错误写法
System.out.println("Sum: " + 2 + 3);

Java 从左到右计算。"Sum: " + 2 先得到文字 Sum: 2,接着 + 3 又接上一个字符。这一行打印的是 Sum: 23。要把算术部分放进括号里。

正确写法
System.out.println("Sum: " + (2 + 3));    // prints Sum: 5
错误写法
int total;
System.out.println(total);

total 还没有值。javac 会报:error: variable total might not have been initialized。声明变量的时候就给它一个值。

正确写法
int total = 0;
System.out.println(total);
错误写法
char grade = "A";

双引号得到的是 String,单引号得到的才是 char。javac 会报:error: incompatible types: String cannot be converted to char。

正确写法
char grade = 'A';

检查自己

  1. 预测下面代码打印的三行:int a = 9; System.out.println(a / 4); System.out.println(a % 4); System.out.println(a / 4.0);

    先自己做,再看答案

    依次是 2、1、2.25。前两行只用到 int,所以 9 / 4 去掉小数,9 % 4 是余数。第三行里有 double,所以小数保留下来。

  2. 写两行代码:一行声明 double 变量 temperature,值为 21.5;另一行声明 String 变量 owner,值为 Emma。

    先自己做,再看答案

    double temperature = 21.5;
    String owner = "Emma";

  3. 预测下面两行打印的内容:System.out.println("Total: " + 4 + 1); 和 System.out.println(4 + 1 + " total");

    先自己做,再看答案

    依次是 Total: 41 和 5 total。第一行里文字在最前面,所以每个数字都被当作文字接上去。第二行先算 4 + 1 得 5,然后才接上文字。

03

判断与重复:if 和 for

学完你能 你能用 if 和 else 让程序在两条路径中选择,并用 for 让一段工作重复固定的次数。

通常 Java 把语句从上到下各运行一次。有两个工具可以改变这一点:if 语句决定要不要运行某些语句,for 语句让某些语句重复运行。

if 语句在括号里放一个条件,后面跟一个代码块。代码块是花括号里的一组语句。条件必须是 boolean,也就是 true 或 false。如果为 true,Java 运行这个代码块;如果为 false,Java 跳过它,有 else 的话就运行 else 后面的代码块。else 部分是可选的。要依次测试好几种情况,可以在 else 后面再写一个 if。Java 会选中第一个条件为 true 的代码块,并跳过其余的。

条件通常来自比较:<(小于)、<=(小于或等于)、>(大于)、>=(大于或等于)、==(等于)和 !=(不等于)。条件还可以组合:&& 表示并且,|| 表示或者,! 表示取反。注意区别:两个等号 == 是比较两个值,一个等号 = 是存入一个值。

for 循环让一个代码块重复运行。循环就是让代码块一遍又一遍运行的语句。for 的括号里有三部分,用分号隔开,例如 for (int i = 1; i <= 5; i++)。起点 int i = 1 创建一个叫 i 的计数器并设为 1。测试 i <= 5 在每一轮开始前检查,变成 false 时循环结束。步进 i++ 在每一轮结束后让 i 加 1,它是 i = i + 1 的简写。同样,i-- 表示减 1。

跟踪的意思是用手把程序走一遍,每一步之后写下每个变量的值。这样不用运行代码也能知道它做了什么,这是值得在纸上多练习的一项技能。

新词

条件
答案为 true 或 false 的一个判断。if 和 for 都要用到它。
代码块
花括号里的一组语句。
循环
让代码块一遍又一遍运行的语句。
跟踪
用手把程序走一遍,在每一步之后写下每个变量的值。

完整例子

Java 8+
public class Main {
    public static void main(String[] args) {
        int total = 0;
        for (int i = 1; i <= 5; i++) {
            total = total + i;
            if (i % 2 == 0) {
                System.out.println(i + " is even, total " + total);
            } else {
                System.out.println(i + " is odd, total " + total);
            }
        }
        System.out.println("final total " + total);
    }
}
先预测输出,再看结果

预期输出

1 is odd, total 1
2 is even, total 3
3 is odd, total 6
4 is even, total 10
5 is odd, total 15
final total 15

逐步讲解

  1. int total = 0; 在循环开始之前创建累计的总和。

  2. for (int i = 1; i <= 5; i++) 开始循环。i 从 1 开始,只要 i <= 5,代码块就继续运行。

  3. 第 1 轮:i 是 1,所以 total 变成 0 + 1 = 1。余数 1 % 2 是 1,不是 0,所以条件 i % 2 == 0 为 false,运行 else 代码块,打印 1 is odd, total 1。

  4. 第 2 轮:i 是 2,total 变成 3。2 % 2 是 0,条件为 true,运行第一个代码块,打印 2 is even, total 3。

  5. 第 3、4、5 轮分别加上 3、4、5,总和依次是 6、10、15。

  6. 第 5 轮之后,i++ 让 i 变成 6。测试 6 <= 5 为 false,循环结束。最后一条 println 在循环的花括号之外,所以只在循环结束后运行一次。

常见错误

错误写法
if (score = 50) {

一个等号是存入一个值,不是比较。javac 会报:error: incompatible types: int cannot be converted to boolean,因为条件必须是 true 或 false。要比较,请用两个等号。

正确写法
if (score == 50) {
错误写法
for (int i = 0; i < 3; i++);
{
    System.out.println("hi");
}

括号后面紧跟的分号是一条空语句,它就是整个循环体,所以循环把“什么也不做”重复了三遍。下面的代码块不属于循环,只运行一次。程序只打印一次 hi,而不是三次。

正确写法
for (int i = 0; i < 3; i++) {
    System.out.println("hi");
}
错误写法
for (int i = 1; i < 5; i++) {
    System.out.println(i);
}    // meant to print 1 to 5

i 到 5 时测试 i < 5 变成 false,所以循环在打印 5 之前就停了,只打印 1、2、3、4。要先想清楚最后一个值要不要包括进来。要包括的话,就用 <=。

正确写法
for (int i = 1; i <= 5; i++) {
    System.out.println(i);
}
错误写法
if (x > 0)
    System.out.println("positive");
    System.out.println("done");

不写花括号的话,if 后面只有第一条语句属于它。打印 done 的那一行在 if 之外,总是会运行,即使 x 不是正数也一样,尽管缩进看上去不是这样。Java 不看缩进。请总是写上花括号。

正确写法
if (x > 0) {
    System.out.println("positive");
}
System.out.println("done");

检查自己

  1. 写一个 for 循环,打印 10、20、30、40、50,每个数占一行。

    先自己做,再看答案

    for (int i = 1; i <= 5; i++) {
    System.out.println(i * 10);
    }

  2. 预测下面的代码打印什么:for (int i = 3; i > 0; i--) { System.out.println(i); }

    先自己做,再看答案

    依次打印 3、2、1,每个数占一行。i 从 3 开始,每一轮减 1。i 变成 0 时测试 i > 0 为 false,所以不会打印 0。

  3. 写一个条件:当 age 至少为 18 且小于 65 时为 true。

    先自己做,再看答案

    age >= 18 && age < 65。两部分必须同时为 true,所以用 && 连接。像数学那样写成 18 <= age < 65,在 Java 里是不行的。

04

方法:给一段工作起名字

学完你能 你能写出带形参和返回值的方法,并在 main 里调用它。

方法是一段有名字的代码。你只写一遍,却可以用任意多次。用它叫作调用它。有了方法,就不必把同样的几行代码复制一遍又一遍,而一个好名字还能告诉读者这几行代码是做什么的。

方法有四个部分。以 static int square(int n) { return n * n; } 为例:返回类型 int 说明返回的是哪一种值;名字是 square;形参列表 (int n) 声明输入,形参就是接收一个输入的变量;花括号里的方法体是运行的代码。现在必须写 static,因为 main 是 static 的,眼下先保留它。第 7 课会讲不带 static 的方法。

调用方法时,写出它的名字,并在括号里放上要交给它的值:square(6)。这些值叫实参。Java 把每个实参复制给对应的形参。return 语句把一个值送回去,并结束这个方法。于是 square(6) 就代表值 36,凡是能放值的地方都能用它,例如放在 println 里面,或者放在 = 号的右边。

有些方法什么也不返回。它们的返回类型是 void,也不需要 return 语句。例子中的 greet 就是这样。

在方法里创建的变量(包括形参)都是局部变量。它只在方法运行期间存在,别的方法看不到它。因为实参是被复制过去的,所以在方法里修改形参,不会改变调用它的代码(称为调用者)里的变量。例子里用 n 演示了这一点。

新词

方法和调用
方法是有名字的一段代码。调用它,就是写出名字和括号来运行它。
形参和实参
形参是方法头里接收输入的变量;实参是调用时传进去的值。
返回值
方法用 return 送回的值。它的种类是返回类型;void 表示什么也不返回。
局部变量
只在某个方法运行期间、只在这个方法里面存在的变量。

完整例子

Java 8+
public class Main {
    static void greet(String name) {
        System.out.println("Hello, " + name + "!");
    }

    static int square(int n) {
        return n * n;
    }

    static int addOne(int n) {
        n = n + 1;
        return n;
    }

    public static void main(String[] args) {
        greet("Iris");
        int result = square(6);
        System.out.println("6 squared is " + result);
        System.out.println(square(square(2)));

        int n = 5;
        int bigger = addOne(n);
        System.out.println("n is " + n + ", bigger is " + bigger);
    }
}
先预测输出,再看结果

预期输出

Hello, Iris!
6 squared is 36
16
n is 5, bigger is 6

逐步讲解

  1. greet 是 void 方法。它只打印,不返回任何东西。它的形参 name 接收传给它的文字。

  2. square 接收一个 int,返回一个 int。return n * n; 把乘积送回给调用者。

  3. addOne 修改的是它自己的那份 n,然后把它返回。

  4. 在 main 里,greet("Iris") 把实参 "Iris" 复制给 name,于是打印出 Hello, Iris!。

  5. square(6) 在 n 等于 6 的情况下运行方法体,返回 36,并存进 result。

  6. 在 square(square(2)) 里,里面那次调用先运行,得到 4;外面那次调用再把 4 平方,得到 16。

  7. main 有自己的 n,值是 5。addOne(n) 把 5 复制进 addOne 里另一个独立的 n,并把那份副本改成 6。main 里的 n 仍然是 5。返回的 6 被存进 bigger。

常见错误

错误写法
static int square(int n) {
    int result = n * n;
}

方法承诺要返回一个 int,却从来没有返回。javac 会报:error: missing return statement。凡是有返回类型的方法,每一条执行路径都必须以 return 结束。

正确写法
static int square(int n) {
    int result = n * n;
    return result;
}
错误写法
int n = 5;
addOne(n);
System.out.println(n);    // expecting 6

这次调用算出了 6 并把它返回,但没有任何东西把答案存起来。形参只是一份副本,所以 main 里的 n 仍然是 5,程序打印 5。要把返回值存起来。

正确写法
int n = 5;
n = addOne(n);
System.out.println(n);    // prints 6
错误写法
int square(int n) {
    return n * n;
}

main 是 static 的,没有对象时,它只能调用同一个类里同样是 static 的方法。javac 会报:error: non-static method square(int) cannot be referenced from a static context。眼下请加上 static。第 7 课会讲不带 static 的方法。

正确写法
static int square(int n) {
    return n * n;
}

检查自己

  1. 写一个方法 max2,接收两个 int,返回其中较大的那个。

    先自己做,再看答案

    static int max2(int a, int b) {
    if (a > b) {
    return a;
    }
    return b;
    }
    如果 a 更大,就立刻返回 a;否则执行到最后一行,返回 b。

  2. 预测打印的内容:static int twice(int x) { x = x * 2; return x; },然后在 main 里 int a = 4; twice(a); System.out.println(a);

    先自己做,再看答案

    打印 4。形参 x 是 a 的副本。把 x 翻倍不会影响 a,而返回的 8 根本没有被存起来。

  3. 修正这个方法:static void add(int a, int b) { return a + b; }

    先自己做,再看答案

    javac 会报:error: incompatible types: unexpected return value。void 方法不能返回值。把 void 改成 int:static int add(int a, int b) { return a + b; }

05

数组:排成一行的多个值

学完你能 你能创建数组,按下标读取和修改它的元素,用循环遍历它,并把它打印出来。

数组存放固定数量的同类型的值,它们排成一行。想象一排有编号的格子。每个格子是一个元素,格子的编号叫下标。下标从 0 开始,不是从 1 开始。有 3 个元素的数组,下标是 0、1、2。

创建数组时可以直接给出值:int[] scores = {80, 95, 70}; 类型 int[] 表示“int 的数组”。也可以只指定大小:int[] blanks = new int[3]; 这时每个元素都有一个默认值:数字是 0,boolean 是 false,String 是 null。null 表示“没有对象”。

用 scores[0] 读取一个元素,用 scores[1] = 100; 修改一个元素。数组的大小是 scores.length。注意 length 后面没有括号。数组一旦创建,大小就不能再改变。

最后一个有效下标是 length - 1。下标超出 0 到 length - 1 的范围,不会造成编译错误,而是程序运行到那一行时停下来,报 ArrayIndexOutOfBoundsException。异常是 Java 报告“程序运行时出了问题”的方式。第 8 课会教你怎样读懂它。

要访问每一个元素,可以按下标循环:for (int i = 0; i < scores.length; i++)。还有一种更短的写法,叫 for-each 循环:for (int s : scores) { ... } 读作“对 scores 里的每个 int s”。变量 s 依次取到每一个值。不需要下标的时候用它。

直接打印数组,显示的是类似 [I@6d06d69c 的代号,而不是里面的值。想看到值,就在类的上方写 import java.util.Arrays;,然后打印 Arrays.toString(scores)。import 这一行告诉 Java 你指的是哪个库里的类。库是 Java 自带的、大量现成的类的集合。

新词

数组
固定数量、同一类型的值,排成一行存放。
元素和下标
元素是数组里的一个值;它的下标是它的位置,从 0 开始数。
异常
程序运行时出了问题的一种报告。
import(导入)
写在文件开头的一行,指明你要使用的库里的类。

完整例子

Java 8+
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] scores = {80, 95, 70};
        System.out.println("first = " + scores[0]);
        System.out.println("length = " + scores.length);

        scores[1] = 100;
        System.out.println(Arrays.toString(scores));

        int total = 0;
        for (int i = 0; i < scores.length; i++) {
            total = total + scores[i];
        }
        System.out.println("total = " + total);

        for (int s : scores) {
            System.out.println("score: " + s);
        }

        int[] blanks = new int[3];
        System.out.println(Arrays.toString(blanks));
    }
}
先预测输出,再看结果

预期输出

first = 80
length = 3
[80, 100, 70]
total = 250
score: 80
score: 100
score: 70
[0, 0, 0]

逐步讲解

  1. import java.util.Arrays; 让库里的 Arrays 类可以使用。它写在类的上方。

  2. int[] scores = {80, 95, 70}; 创建了一个有三个 int 的数组。scores[0] 是 80,scores[1] 是 95,scores[2] 是 70。scores.length 是 3。

  3. scores[1] = 100; 替换下标 1 的元素。Arrays.toString 把数组变成可读的文字:[80, 100, 70]。

  4. 第一个循环用 i 作下标,从 0 到 2。i 变成 3 时,测试 i < scores.length 为 false,所以下标 3 永远不会被碰到。它把 80、100、70 加起来,得到 250。

  5. for-each 循环按顺序访问这三个值,s 每次拿到其中一个。

  6. new int[3] 创建三个元素,它们一开始都是 0。

常见错误

错误写法
for (int i = 0; i <= scores.length; i++) {
    System.out.println(scores[i]);
}

数组有 3 个元素,最后一个下标是 2,而 i 会取到 3。程序先打印出三个分数,然后停下来,报:Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3。它下面一行会写出你的文件名和行号。Java 8 在冒号后只会打印数字 3。和 length 比较时请用 <。

正确写法
for (int i = 0; i < scores.length; i++) {
    System.out.println(scores[i]);
}
错误写法
int[] copy = scores;
copy[0] = 0;

这并没有复制格子,只是给同一个数组起了第二个名字。修改 copy[0] 同时也改了 scores[0],所以现在打印 scores 会显示 [0, 95, 70]。要得到真正的副本,请用 Arrays.copyOf。

正确写法
int[] copy = Arrays.copyOf(scores, scores.length);
copy[0] = 0;    // scores is unchanged
错误写法
System.out.println(scores.length());

对数组来说,length 是一个值,不是方法,所以不带括号。javac 会报:error: cannot find symbol,后面跟着 symbol: method length()。字符串则不同,第 6 课会讲到:它要带括号。

正确写法
System.out.println(scores.length);

检查自己

  1. 预测打印的内容:int[] a = {5, 6, 7}; a[0] = a[2]; System.out.println(Arrays.toString(a));

    先自己做,再看答案

    [7, 6, 7]。这条语句把下标 2 的值复制到下标 0,原来的 5 被覆盖了。

  2. 写一个循环,从最后一个到第一个,依次打印 scores 的元素。

    先自己做,再看答案

    for (int i = scores.length - 1; i >= 0; i--) {
    System.out.println(scores[i]);
    }
    循环从最后一个下标开始,倒数到 0。

  3. new int[5] 有几个元素?最大的有效下标是多少?

    先自己做,再看答案

    它有 5 个元素,全是 0。有效下标是 0 到 4,最大的是 4。使用下标 5 会让程序停下来,并报 ArrayIndexOutOfBoundsException。

06

字符串,以及 == 与 equals 的区别

学完你能 你能使用常用的 String 方法,并正确地比较两个字符串。

String 是一段文字,由按顺序排列的字符组成。字符串的值写在双引号里。String 不是 int 那样的简单类型,它是一个类,每个字符串值都是一个对象。对象是由类创建出来的值,它存放在内存的某个地方。String 类型的变量里存放的不是文字本身,而是一个引用,说明对象在哪里。可以把引用想成指向对象的一张标签。

用点号来使用对象的方法,例如 word.length()。下面这些很有用:length() 给出字符的个数。charAt(i) 给出下标 i 处的字符,下标像数组一样从 0 开始数。substring(a, b) 给出从下标 a 到下标 b 之前的字符,不包括下标 b。indexOf("l") 给出第一次匹配的下标,没有匹配就给出 -1。toUpperCase() 和 toLowerCase() 改变大小写。注意,String 的 length() 带括号,这一点和数组的 length 不同。

String 一旦创建就永远不能改变。toUpperCase() 这样的方法不会改变原来的字符串,而是返回一个新的 String。想保留结果,就要把它存起来:word = word.toUpperCase();

下面讲 == 和 equals。对引用来说,== 问的是:这两个名字指向的是不是同一个对象?它不问文字是否一样。两个不同的对象可以装着相同的字母。equals 方法问的是:这两个字符串包含的字符是否相同?这几乎总是你真正想问的。所以比较字符串要用 a.equals(b),永远不要用 a == b。要忽略大小写,用 equalsIgnoreCase。

== 有时候看上去能用。Java 对相同的字符串字面量(直接写在代码里的文字)只存一份,所以文字相同的两个字面量就是同一个对象。但是一旦文字来自别处,例如键盘、文件或者某次计算,== 就会得到 false。在测试里正常、之后才出错的 bug 是最糟糕的一种。所以请永远使用 equals。

新词

字符串(String)
按顺序排列的字符组成的文字。在 Java 里它是一个类,每个值都是一个对象。
对象和引用
对象是由类创建出来的值;引用说明对象在哪里。
字面量
直接写在代码里的值,例如 "hello" 或 42。
不可变
创建之后不能再改变。String 是不可变的。

完整例子

Java 8+
public class Main {
    public static void main(String[] args) {
        String word = "hello";
        String typed = new String("hello");    // pretend this text came from the keyboard

        System.out.println("same text? " + word.equals(typed));
        System.out.println("same object? " + (word == typed));

        String shout = word.toUpperCase();
        System.out.println(shout + " " + word);
        System.out.println(word.length());
        System.out.println(word.charAt(1));
        System.out.println(word.substring(1, 3));
        System.out.println(word.indexOf("l"));
        System.out.println("ignoring case? " + word.equalsIgnoreCase("HELLO"));
    }
}
先预测输出,再看结果

预期输出

same text? true
same object? false
HELLO hello
5
e
el
2
ignoring case? true

逐步讲解

  1. word 是一个字面量。new String("hello") 另外创建了一个独立的对象,里面的字母一样。我们假装这段文字来自键盘。

  2. equals 比较字母,所以得到 true。

  3. == 问的是 word 和 typed 是不是同一个对象。它们是两个对象,所以得到 false。word == typed 外面的括号是必需的,否则会先做 + 运算。

  4. toUpperCase() 返回一个新字符串 HELLO。word 本身没有变,同一行的输出就能看出来。

  5. length() 是 5。charAt(1) 是 e,因为从 0 开始数,h 的下标是 0。

  6. substring(1, 3) 取下标 1 和 2 的字符,不包括 3,结果是 el。

  7. indexOf("l") 找到第一个 l,下标是 2。equalsIgnoreCase 把 hello 和 HELLO 当作相同。

常见错误

错误写法
if (answer == "yes") {

这是在问 answer 和这个字面量是不是同一个对象。如果 answer 里的文字是键盘输入或拼出来的,即使内容是 yes,结果也是 false。请用 equals。把字面量写在前面,"yes".equals(answer),还能避免 answer 是 null(表示“根本没有对象”)时程序崩溃,因为对 null 调用方法会让程序停止。

正确写法
if ("yes".equals(answer)) {
错误写法
name.toUpperCase();
System.out.println(name);

toUpperCase 返回一个新的 String,name 本身不变。没有任何东西把这个新 String 存起来,所以 name 没有改变,大写的结果也丢掉了。

正确写法
name = name.toUpperCase();
System.out.println(name);
错误写法
String part = "hello".substring(1, 3);    // expecting "ell"

结束下标是不包括的。substring(1, 3) 取下标 1 和 2,得到 el。想包括下标 3,结束下标就要写 4。

正确写法
String part = "hello".substring(1, 4);    // ell

检查自己

  1. 预测打印的内容:String s = "Java"; System.out.println(s.substring(1, 3)); System.out.println(s.charAt(0)); System.out.println(s.length());

    先自己做,再看答案

    依次是 av、J、4。substring(1, 3) 取下标 1 和 2 的字母,也就是 a 和 v。charAt(0) 是第一个字母。这个字符串有 4 个字符。

  2. 修正这一行,让它比较文字内容:if (command == "stop") {

    先自己做,再看答案

    if ("stop".equals(command)) { 或者 if (command.equals("stop")) { 第一种写法更安全,因为 command 为 null 时它也能正常工作。

  3. 写一行代码,当 String 变量 name 忽略大小写后等于 Leon 时,打印 true。

    先自己做,再看答案

    System.out.println(name.equalsIgnoreCase("Leon"));

07

第一个类,以及构造方法

学完你能 你能写一个带字段、构造方法和方法的小类,用 new 创建它的对象,并调用对象的方法。

到目前为止,你用的都是 Java 自带的类型。有了类,你就能做出自己的类型。类是一张蓝图:它说明每个对象有哪些数据、每个对象能做什么。对象是按蓝图造出来的一个具体的东西。一个类(例如 Player)可以造出许多对象,每个对象都有自己的数据。

这些数据叫字段。字段是属于某个对象的变量。你在类里面、所有方法外面声明它。我们把字段标成 private。private 表示只有同一个类里面的代码才能使用这个字段。这样数据就受到了保护,其他代码必须通过这个类的方法来使用它。

不带 static 的方法是针对某一个具体的对象工作的。对 Leon 的对象调用 addPoints(5),改变的是 Leon 的分数,不会影响别人。在类的内部,这样的方法直接用名字使用字段。

构造方法是一种特殊的方法,用 new 创建对象时它会运行。它的名字和类名相同,没有返回类型,连 void 也不写。它的工作是给字段赋上最初的值。new Player("Leon") 会创建对象,以 "Leon" 作为实参运行构造方法,并把指向新对象的引用交给你。

在构造方法里,形参和字段可能都叫 name。写 this.name 表示“这个对象的 name 字段”,单独写 name 表示形参。所以 this.name = name; 的作用是把实参存进字段。

类放在哪里呢?在真正的项目里,每个类都有自己的文件,文件以类命名。为了让这个例子留在同一个文件里,多出来的类要写在 public class Main 的上方。在这种写法下,只有叫 Main 的类可以是 public 的。

新词

类和对象
类是蓝图;对象是用 new 按蓝图造出来的一个具体的东西。
字段
属于某个对象的变量,声明在类里面、所有方法外面。
构造方法
一种特殊的方法,名字与类相同,没有返回类型。new 创建对象时它会运行,负责设置字段。
this
指代正在运行其方法的那个对象。this.name 就是这个对象的 name 字段。
private
只有同一个类里面的代码才能使用它。

完整例子

Java 8+
class Player {
    private String name;
    private int score;

    Player(String name) {
        this.name = name;
        this.score = 0;
    }

    void addPoints(int points) {
        score = score + points;
    }

    String describe() {
        return name + " has " + score + " points";
    }
}

public class Main {
    public static void main(String[] args) {
        Player leon = new Player("Leon");
        Player mia = new Player("Mia");

        leon.addPoints(5);
        leon.addPoints(3);
        mia.addPoints(10);

        System.out.println(leon.describe());
        System.out.println(mia.describe());
    }
}
先预测输出,再看结果

预期输出

Leon has 8 points
Mia has 10 points

逐步讲解

  1. class Player { 开始定义这张蓝图。它不是 public 的,所以同一个文件里还可以放 public class Main。

  2. private String name; 和 private int score; 是字段。每个 Player 对象都有自己的一对。

  3. Player(String name) 是构造方法:名字是类名,没有返回类型。this.name = name; 把实参存进字段。this.score = 0; 让分数从零开始。

  4. addPoints(int points) 给这个对象的分数加分。它不是 static 的,所以对哪个对象调用,它就为哪个对象工作。

  5. describe() 用字段拼出一个 String 并返回。

  6. 在 main 里,new Player("Leon") 创建一个对象,new Player("Mia") 创建另一个。leon 和 mia 是引用它们的变量。

  7. leon.addPoints(5) 只改变 Leon 的对象。加了 5 再加 3 之后,Leon 是 8 分。Mia 的对象没有受影响,她自己得到 10 分。describe() 对每个对象各调用一次,所以各自打印自己的数据。

常见错误

错误写法
Player leon = Player("Leon");

没有 new,Java 就去找一个叫 Player 的普通方法,找不到。javac 会报:error: cannot find symbol,并附带 symbol: method Player(String)。创建对象要用 new。

正确写法
Player leon = new Player("Leon");
错误写法
void Player(String name) {
    this.name = name;
}

加上返回类型(哪怕是 void),它就变成了一个碰巧叫 Player 的普通方法。这样类里就没有接收 String 的构造方法了,new Player("Leon") 会失败:error: constructor Player in class Player cannot be applied to given types;,并附带 required: no arguments 和 found: String。

正确写法
Player(String name) {
    this.name = name;
}
错误写法
Player(String name) {
    name = name;
    this.score = 0;
}

两边都是形参,所以这是把形参赋给它自己。字段 name 从来没有被设置,保持默认值 null。不会出现任何报错,describe() 打印出来的是 null has 0 points。要在字段前面写上 this.。

正确写法
Player(String name) {
    this.name = name;
    this.score = 0;
}

检查自己

  1. 给 Player 加一个方法 getScore(),把分数作为 int 返回。再写出 main 里打印 Mia 分数的那一行。

    先自己做,再看答案

    在 Player 里面:
    int getScore() {
    return score;
    }
    在 main 里:
    System.out.println(mia.getScore());
    它会打印 10。

  2. 在 main 的末尾加上下面两行后,预测打印什么:Player noah = new Player("Noah"); System.out.println(noah.describe());

    先自己做,再看答案

    打印 Noah has 0 points。构造方法把 score 设成了 0,而且从未对这个对象调用过 addPoints。

  3. 如果 main 里有 leon.score = 100;,javac 会说什么?为什么?

    先自己做,再看答案

    error: score has private access in Player。这个字段是 private 的,Player 之外的代码不能使用它。应该改成通过 addPoints 这样的方法来改变分数。

08

读懂编译错误和堆栈跟踪

学完你能 你能在 javac 的报错信息和异常的堆栈跟踪里,找到出错的行和原因。

程序出问题有三种方式。编译错误:javac 拒绝构建程序,什么都不会运行。运行时错误:程序开始运行,随后 Java 在某一行把它停掉,并打印堆栈跟踪。答案错误:程序一直运行到结束,但打印出的内容不对,Java 什么也不说。前两种会带着一条信息,这一课就是教你读这些信息。下面的常见错误里有真实的例子。

读编译错误要从上往下读。第一行有三部分:文件名和行号(例如 Main.java:4)、单词 error,以及问题是什么(例如 cannot find symbol)。下一行重复那一行的代码,并在 Java 卡住的位置下面用插入符 ^ 标出来。有些错误还带详细信息,例如 symbol: variable totl 和 location: class Main,说明 Java 不认识哪个名字,以及它在哪里找过。最后一行统计错误的个数。

有三个习惯很有帮助。先修第一个错误,因为后面的错误常常是它引起的,少一个花括号就可能带来十几条报错。每改一处就重新编译一次。其次,报出的行号是 Java 发现问题的地方,真正的错误可能在上面一行。最后,把信息当作提示,而不是诊断结论。cannot find symbol 只说明 Java 不认识某个名字。

异常没有被处理时,就会出现堆栈跟踪。异常是一种事件,表示程序运行时出了问题,例如除以零,或者使用了数组范围之外的下标。程序会在那一刻停止。堆栈跟踪的第一行写着异常的名字和一条消息。接下来是若干以 at 开头的行,每一行是一个当时正在运行的方法,括号里是文件名和行号。最上面的 at 行是异常发生的地方,它下面一行是调用它的方法,依次往下,一直到 main。找到第一行写着你自己文件名(例如 Main.java)的 at 行,打开那个文件,跳到那一行。提到 java.base 或 java.util 的行是 Java 自己的代码。原因几乎总是在你自己的代码里。

你也可以处理异常,而不是任由它让程序停下来。把有风险的语句放进 try 代码块。如果这里发生了指定种类的异常,Java 就跳过 try 代码块里剩下的部分,转而运行 catch 代码块,之后程序在 catch 后面继续运行。例子就是这样工作的。

新词

编译错误
javac 在程序运行之前发现的错误。不修好就什么都不能运行。
运行时错误
程序运行过程中出现、并让程序停下来的问题。
异常
在运行时表示出了问题的事件,例如 ArithmeticException。
堆栈跟踪
异常发生时正在运行的方法的清单,最新的在最前,每个方法都带文件名和行号。
try 和 catch
try 里放可能抛出异常的语句;如果发生了指定种类的异常,就运行 catch。

完整例子

Java 8+
public class Main {
    static int readAge(String text) {
        return Integer.parseInt(text);
    }

    public static void main(String[] args) {
        System.out.println("start");
        try {
            System.out.println("age is " + readAge("12"));
            System.out.println("age is " + readAge("twelve"));
            System.out.println("this line is skipped");
        } catch (NumberFormatException e) {
            System.out.println("not a whole number");
        }
        System.out.println("done");
    }
}
先预测输出,再看结果

预期输出

start
age is 12
not a whole number
done

逐步讲解

  1. readAge 调用 Integer.parseInt,这是 Java 的一个方法,把 12 这样的文字变成 int 值 12。文字不是整数时,它会抛出 NumberFormatException。

  2. main 先打印 start,然后进入 try 代码块。

  3. readAge("12") 正常工作,得到 12,于是打印 age is 12。

  4. readAge("twelve") 抛出 NumberFormatException。Java 立刻离开 try 代码块,所以 this line is skipped 那一行永远不会运行。

  5. catch (NumberFormatException e) 代码块与这种异常匹配,于是运行并打印 not a whole number。

  6. 程序在 catch 之后继续运行,打印 done。如果没有 try 和 catch,异常会在第 4 步就让程序停下来。Java 会打印 Exception in thread "main" java.lang.NumberFormatException: For input string: "twelve",再跟着几行 at,而 done 永远不会出现。

常见错误

错误写法
public class Main {
    public static void main(String[] args) {
        int total = 5
        System.out.println(total);
    }
}

javac 会打印 Main.java:3: error: ';' expected,接着是 int total = 5 这一行,插入符就在 5 的后面。一条语句漏掉了分号。行号指向缺分号的那条语句。在插入符所在的位置补上分号。

正确写法
public class Main {
    public static void main(String[] args) {
        int total = 5;
        System.out.println(total);
    }
}
错误写法
public class Main {
    public static void main(String[] args) {
        int total = 5;
        System.out.println(totl);
    }
}

javac 会打印四行:Main.java:4: error: cannot find symbol,然后是代码行,接着是 symbol: variable totl,最后是 location: class Main。这样读:错误在 Main.java 的第 4 行;不认识的名字是 totl,一个变量;Java 在 Main 类里找过,没有找到。这个变量声明时叫 total,所以这是拼写错误。

正确写法
public class Main {
    public static void main(String[] args) {
        int total = 5;
        System.out.println(total);
    }
}
错误写法
public class Main {
    static int average(int total, int count) {
        return total / count;
    }

    public static void main(String[] args) {
        System.out.println(average(10, 0));
    }
}

这段代码能通过编译,但运行时 Java 会打印:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.average(Main.java:3)
at Main.main(Main.java:7)
从最上面读起。第一行说明发生了什么:一个整数被零除。第一条 at 行说明在哪里:average 方法,Main.java 的第 3 行,也就是 return total / count; 这一行。下一条 at 行说明是谁调用的:main,在第 7 行。所以除法在第 3 行,而那个 0 来自第 7 行的调用。要决定修哪一处。这里应该让方法检查 count 是不是零。

正确写法
static int average(int total, int count) {
    if (count == 0) {
        return 0;
    }
    return total / count;
}

检查自己

  1. javac 打印:Main.java:12: error: cannot find symbol,然后是 symbol: variable cnt,再是 location: class Main。你该打开哪一行?你怀疑是什么问题?

    先自己做,再看答案

    打开 Main.java 的第 12 行。Java 在那里不认识叫 cnt 的变量。可以怀疑是拼写错误(也许变量其实叫 count)、变量根本没有声明,或者变量声明在别的花括号里面,所以在第 12 行够不着它。

  2. 一个程序停下来时的输出是:Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3,然后是 at Main.last(Main.java:9),再是 at Main.main(Main.java:15)。哪个方法出了问题,在哪一行?这条消息告诉了你什么?

    先自己做,再看答案

    出问题的是 last 方法,在 Main.java 的第 9 行。main 是在第 15 行调用它的。消息说明在长度为 3 的数组上使用了下标 3,而有效下标是 0 到 2。打开第 9 行,看看那个下标。很可能是写成了 length 而不是 length - 1,或者循环里用了 <= 而不是 <。

  3. 写出 try 和 catch 的代码:当 Integer.parseInt(text) 失败时,打印 bad number,而不是让程序停下来。

    先自己做,再看答案

    try {
    int n = Integer.parseInt(text);
    System.out.println(n);
    } catch (NumberFormatException e) {
    System.out.println("bad number");
    }

接下来

继续学习 Java 课程(INFO1113) →

独立整理的学习笔记,不是官方课程材料。