Java 语言入门教程

Java 语言入门教程

从 Hello World 到面向过程编程

cover

没有伴随着痛苦的教训是毫无意义的,因为人若不牺牲些什么,是什么都无法得到的。

但当忍受过这种痛楚,并将其克服的时候,人就会得到不输给任何事的坚韧的心。

没错,钢铁般坚韧的心。

——爱德华·艾尔利克 《钢之炼金术师》
cover2

Java程序基础

从这节课开始,我们就可以正式进入到Java语言的学习当中了。

Java是一门面向对象的语言,但在那之前,我们得先学会如何面向过程编程。

程序代码基本结构

正如所有的语言教学都是从Hello World开始一样,这里我们也来开始编写第一个Hello World!

首先,在IDEA中通过 file -> 新建 -> 项目 创建一个名为MyHelloWorldProject的项目。

新建项目步骤1

程序代码基本结构 (续)

新建项目步骤2

接着,在src/main/java目录右键,选择new -> class,命名为MainApp。IDEA会自动生成基础代码:

public class MainApp {

}

编写第一个程序:Hello World

MainApp的花括号中写入main方法,这是应用程序的入口。

public class MainApp {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

就像C语言的int main()一样,请先记住这个固定写法。

#include <stdio.h>
    int main() {
    printf("Hello, World!\n");
    return 0;
}

代码规范与错误

Java语言严格区分大小写。如果大小写错误,IDEA会用红色波浪线提示:

大小写错误

只要源代码中存在报错,就无法完成编译,会提示构建失败。

构建失败
在代码编辑器存在红色波浪提示的情况下不要点运行,这毫无意义!

语句和分号

Java用分号 ; 来分隔语句。如果没有分号,即使换行,编译器也会认为是同一句代码。

缺少分号

相反,如果添加了分号,即使写在同一行,也会被认为是两句独立的代码。

同一行多句代码

注释

注释是给程序员看的笔记,编译器会忽略它们。

单行注释

使用双斜杠 //,从斜杠开始到行尾都是注释。

// 这是一个单行注释
System.out.println("Hello World!");

多行注释

使用 /**/ 包围多行内容。

/*
    这里面的内容
    无论多少行
    都可以
*/

文档注释

使用 /***/ 包围,可以用来自动生成API文档。

/**
 * 这是一个文档注释
 * @author YourName
 */

变量与常量

变量,指值可以变的量。在Java中,我们需要声明一个变量才能使用它。

声明变量

[数据类型] [变量名称];

例如,声明一个整数变量a并初始化:

int a = 10;  // 声明一个整数类型变量a,并赋值为10
变量的盒子模型

变量命名:规则、约定与实践

变量命名是编程的基本功。一个好的名字能让代码更易读。Java的命名规范可分为三类:

必须遵守的硬性规则

如果违反,代码将无法编译。

  • ✅ 只能包含:字母、数字、下划线(_)、美元符($)
  • 不能以数字开头 (如 1stPlace)
  • 不能是Java关键字 (如 class, public)
  • ✅ 大小写敏感 (ageAge 是两个不同变量)

推荐遵循的社区约定

大家默认的“潜规则”,让代码风格统一。

  • 驼峰命名法 (CamelCase):首字母小写,后续单词首字母大写,如 myScore, userName
  • 见名知意:使用有意义的完整单词,如 studentName 而不是 sName
  • ❌ 避免使用下划线开头 (如 _tmp) 或美元符 (price$),虽然语法允许,但通常有特殊含义。

应该避免的不良实践

语法上可能正确,但会降低代码质量。

  • 拼音或中英混合:如 nianling, userXingming
  • 变量名过短或无意义:如 a, b, temp (除非在极小的作用域内)。
  • 使用中文:虽然Java支持 (如 String 姓名 = "张三";),但在团队协作和跨平台时可能引发问题,强烈不推荐。

使用变量

变量的值可以被修改,也可以参与运算。

修改变量值

int a = 666;
System.out.println(a);  // 输出 666
a = 777;
System.out.println(a);  // 输出 777

变量间的赋值

int a = 10;
int b = a;   // b的值现在也是10
System.out.println(b);

变量参与运算

int a = 9;
a = a + 1;   // a的新值是 9 + 1 = 10
System.out.println(a);

常量

有时候我们希望一个变量的值在初始化后不能被修改,这时可以使用 final 关键字将其声明为常量。

final int PI_APPROXIMATION = 3; // 常量名通常大写
// PI_APPROXIMATION = 4;  // 这行代码会报错,因为常量的值不能被修改

尝试修改一个 final 变量会导致编译错误:

修改常量错误

基本数据类型

程序中需要表示各种各样的数据,比如整数、小数、字符等。Java提供了八大基本数据类型来满足这些需求。

计算机中的二进制表示

计算机内部所有数据都以二进制(0和1)形式存储。

  • bit (位): 最小的数据单位,一个0或1。
  • Byte (字节): 8个bit组成一个字节。1 Byte = 8 bit

为了表示负数,计算机使用补码的方式。这解决了正负零问题,并简化了加减法运算。

编码方式演进

  • 原码:最高位为符号位(0正1负),其余为数值。简单但计算麻烦。
  • 反码:正数不变,负数符号位不变,其余位取反。解决了部分计算问题但存在+0和-0。
  • 补码:正数不变,负数反码+1。消除了-0,是现代计算机的标准。

补码示例 (以byte为例)

-5 的补码:

  1. 写出原码: 5 的二进制是 00000101。所以 -5 的原码是 10000101
  2. 计算反码: 符号位不变,其余位取反,得到 11111010
  3. 计算补码: 反码加1,得到 11111011

所以,-5 在计算机中就以 11111011 的形式存储。

整数类型

Java提供四种整数类型,用于表示不同范围的整数。

  • byte: 1字节 (-128 ~ 127)
  • short: 2字节 (-32,768 ~ 32,767)
  • int: 4字节 (约-21亿 ~ 21亿) - 最常用
  • long: 8字节 (非常大)

定义一个long类型的常量,需要在数字后面加上Ll

long bigNumber = 9223372036854775807L;
int population = 1_000_000; // 可以用下划线分隔,提高可读性

隐式类型转换 (整数)

当把一个表示范围小的类型赋值给一个表示范围大的类型时,Java会自动进行类型转换。

short a = 10;
int b = a;   // 合法,short可以自动转换为int

反之则会报错,因为可能导致数据丢失。

大类型转小类型错误

整数溢出

当一个整数超出了其类型的最大表示范围时,会发生“溢出”,结果会像时钟一样回绕。

int a = 2147483647;   // int类型的最大值
a = a + 1;
System.out.println(a);  // 输出结果会是 -2147483648 (int的最小值)

这是因为二进制的最高位(符号位)发生了变化。

整数溢出结果

浮点类型

用于表示小数,也叫浮点数。

  • float: 4字节 (单精度,约7位有效数字)
  • double: 8字节 (双精度,约15-17位有效数字) - 更常用

小数在计算机中采用 IEEE 754 标准存储,由符号位、指数位和尾数位组成。

IEEE 754
示例:float f = 9.625f;
  1. 符号位: 0 (正数)
  2. 转为二进制: 9.6251001.101
  3. 科学计数法: 1.001101 * 2^3
  4. 指数位: 偏移量为127,所以指数为 3 + 127 = 130,二进制为 10000010
  5. 尾数位: 小数部分 001101,补齐23位 → 00110100000000000000000
最终的32位表示:
0 10000010 00110100000000000000000
- -------- -----------------------
S   Exponent         Fraction

这个例子展示了浮点数如何被编码为二进制形式。理解这个过程有助于我们认识到为什么浮点数会有精度限制。

注意:浮点数运算存在精度误差,不适合用于金融等需要精确计算的场景。下面是一个经典的例子:

// 浮点数精度问题
System.out.println(0.1 + 0.2); // 输出 0.30000000000000004

// 在金融计算中,应使用 BigDecimal
java.math.BigDecimal bd1 = new java.math.BigDecimal("0.1");
java.math.BigDecimal bd2 = new java.math.BigDecimal("0.2");
System.out.println(bd1.add(bd2)); // 输出 0.3

使用浮点类型

小数常量默认为double类型。要定义float常量,需在数字后加Ff

double pi = 3.1415926535;
float price = 9.99F;

隐式类型转换规则:byte→short(char)→int→long→float→double

long可以隐式转换为float,因为float的表示范围更大,但可能会丢失精度。

long l = 21731371236768L;
float f = l;   // 编译通过,但f的值可能不完全精确
System.out.println(f);

字符类型 char

char类型用于表示单个字符,占2个字节,无符号,范围 0 ~ 65535。

字符常量需要用单引号 ' ' 括起来。

char c1 = 'A';
char c2 = 65; // 效果同上,65是'A'的ASCII码
System.out.println(c2); // 输出 A
ASCII Table

字符编码的演进:从ASCII到Unicode

  1. ASCII的局限: 计算机最初只在美国使用,所以设计了ASCII码,用1个字节(8位)中的7位表示128个字符(英文字母、数字、符号)。当计算机走向世界,ASCII无法表示中文、日文等成千上万的字符。
  2. Unicode的诞生: 为了解决这个问题,Unicode字符集应运而生。它像一本巨大的字典,为世界上每个字符分配了一个唯一的数字编号(码点/Code Point)。它只定义了“字符”与“数字”的对应关系,不关心如何存储。
  3. 编码的实现 (UTF-8/UTF-16/GBK): 如何将Unicode码点高效地存入计算机?这就是编码(Encoding)要解决的问题。
    • UTF-8: 变长编码,用1-4个字节表示字符。兼容ASCII,节省空间,是最广泛使用的编码。
    • UTF-16: Java运行时内部使用的编码,通常用2个或4个字节表示字符。
    • GBK: 中国国家标准,用于兼容早期的中文软件,使用1-2个字节。
示例:“你好”在不同编码下的样子

Unicode码点: 你 (U+4F60), 好 (U+597D)

编码方式 二进制表示 (十六进制)
UTF-8 E4 BD A0 E5 A5 BD
UTF-16 (BE) 4F 60 59 7D
GBK C4 E3 BA C3

结论: 同一个“字符”,在不同“编码”规则下,存储为完全不同的二进制内容。这就是乱码的根源。

字符串类型 String

String类型用于表示一串字符。它不是基本数据类型,而是一个类(对象)。

字符串常量需要用双引号 " " 括起来。

String greeting = "Hello World!";
String name = "张三";
String empty = ""; // 空字符串

由于Java强大的编码支持,变量名甚至可以是中文(但不推荐)。

Chinese Variable Name

布尔类型 boolean

boolean类型只有两个值:true (真) 和 false (假)。

它通常用作条件判断的结果。

boolean isStudent = true;
boolean hasPassed = false;

System.out.println(isStudent);

不能将整数(如0或1)赋值给boolean变量,这和C语言不同。

Boolean Assignment Error

与用户交互:从命令行读取输入

到目前为止,我们的程序都是“自说自话”。为了让程序能接收用户的输入,我们需要使用 Scanner 类。这能让我们的程序变得可交互,更加动态。

如何使用 Scanner

  1. 导入 Scanner 类: 在代码文件的最顶端,使用 import java.util.Scanner; 来告诉编译器我们要使用这个工具。
  2. 创建 Scanner 对象: 在 main 方法中,通过 Scanner scanner = new Scanner(System.in); 来创建一个实例。System.in 代表标准输入流,也就是我们的键盘。
  3. 调用方法读取数据: scanner 对象有很多方法来读取不同类型的数据,例如:
    • nextInt(): 读取一个整数。
    • nextDouble(): 读取一个双精度浮点数。
    • nextLine(): 读取一行文本(字符串)。

import java.util.Scanner; // 1. 导入工具

public class UserInputExample {
    public static void main(String[] args) {
        // 2. 创建对象并连接到键盘
        Scanner scanner = new Scanner(System.in);

        System.out.print("请输入您的名字: ");
        String name = scanner.nextLine(); // 3. 读取一行文字

        System.out.print("请输入您的年龄: ");
        int age = scanner.nextInt(); // 3. 读取一个整数

        System.out.println("\n你好, " + name + "! 你今年 " + age + " 岁了。");
    }
}
                

Scanner 常见陷阱:被“吃掉”的换行符

当混合使用 nextInt(), nextDouble() 等方法和 nextLine() 时,初学者会遇到一个非常普遍的“坑”。

打开交互动画:Scanner 换行符陷阱演示


// 问题复现代码
Scanner scanner = new Scanner(System.in);

System.out.print("请输入您的年龄: ");
int age = scanner.nextInt(); // 用户输入 25 然后敲回车

System.out.print("请输入您的姓名: ");
String name = scanner.nextLine(); // 这句代码会立刻执行,根本不给用户输入的机会!

System.out.println("年龄: " + age + ", 姓名: '" + name + "'");
// 输出结果:
// 年龄: 25, 姓名: ''
                

问题根源

这就像吃东西,nextInt() 只吃了“数字”这个正餐,但把“回车”这个餐盘留下了。当 nextLine() 来的时候,它看到有个现成的“餐盘”(回车符),就直接收走了,导致它认为已经读取了一行(一个空行)。

解决方案

  1. 多吃一口: 在 nextInt() 之后,再加一句 scanner.nextLine();,专门用来“吃掉”那个被剩下的回车符。
  2. 最佳实践(推荐): 永远只用 nextLine() 读取一整行。如果需要数字,再用 Integer.parseInt()Double.parseDouble() 来转换。这样最稳妥。

// 最佳实践代码
Scanner scanner = new Scanner(System.in);

System.out.print("请输入您的年龄: ");
int age = Integer.parseInt(scanner.nextLine()); // 读取整行,再转成整数

System.out.print("请输入您的姓名: ");
String name = scanner.nextLine();

System.out.println("年龄: " + age + ", 姓名: '" + name + "'");
                

Scanner 进阶:一次读取多个数据


import java.util.Scanner;

public class MultiInputExample {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("请输入两个整数 (用空格隔开): "); // 例如输入: 100 200
        int a = scanner.nextInt();
        int b = scanner.nextInt();

        System.out.println("第一个数是: " + a);
        System.out.println("第二个数是: " + b);
        System.out.println("它们的和是: " + (a + b));
        // 输出:
        // 第一个数是: 1
        // 第二个数是: 2
        // 它们的和是: 3
    }
}
                

为什么我们可以输入 1 2 然后按回车,就能给两个变量赋值?这揭示了 Scanner 更强大的工作原理。

核心概念:标记 (Token) 与分隔符 (Delimiter)

Scanner 并不只是按行读取,它实际上是在寻找“标记”。默认情况下,它使用任何空白字符(空格、Tab键、回车符)作为“分隔符”来切分用户的输入。

当你输入 1 2 并回车时,输入流中是 "1 2\n"Scanner 会看到两个有效的数字标记:"1" 和 "2"。

nextInt() 的工作方式

当你调用 scanner.nextInt() 时,它会:

  1. 跳过所有开头遇到的分隔符。
  2. 开始读取,直到再次遇到分隔符为止,然后将这部分内容解析为整数。

所以,第一次 nextInt() 读取了 "1",第二次 nextInt() 跳过了中间的空格,读取了 "2"。

这个特性非常方便,但也再次解释了为什么 nextInt() 会留下换行符——因为它只关心找到下一个数字标记,而把作为分隔符的换行符留在了原地。

运算符

有了数据类型,我们还需要运算符来对这些数据进行操作和计算。

算术运算符

用于进行基本的数学运算:+, -, *, /, % (取模)。

整数除法:结果会舍去小数部分。

System.out.println(8 / 5); // 输出 1

字符串拼接+号用于连接字符串。

System.out.println("分数: " + 95); // 输出 "分数: 95"

运算符有优先级,* / % 高于 + -,与数学一致。

括号与强制类型转换

括号 ()

用于提升运算优先级,与数学中用法一致。

System.out.println((10 + 2) * 5); // 输出 60

强制类型转换

用于将一个表示范围大的类型强制转换为小的类型。

int a = 8, b = 5;
double c = (double)a / b; // 将a转为double, 整个表达式结果为double
System.out.println(c);    // 输出 1.6

注意:(double)(a/b) 是错误的,因为它先计算了整数除法 a/b 得到1,再将1转为1.0。

自增/自减运算符 (++, --)

这是Java中用于快速将变量值加一或减一的快捷方式。

  • 前缀 (++a, --a): 先变,后用。先完成变量的自增/自减,然后将新值用于表达式的计算。
  • 后缀 (a++, a--): 先用,后变。先使用变量的当前值参与表达式计算,然后才对变量进行自增/自减。

// 前缀示例
int a = 5;
int b = ++a; // a先变成6, 然后b被赋值为6
// 结果: a = 6, b = 6

// 后缀示例
int x = 5;
int y = x++; // 先将x的值(5)赋给y, 然后x再变成6
// 结果: x = 6, y = 5

理解“先变后用”与“先用后变”是掌握它们的关键。

面试题陷阱:请远离“奇技淫巧”

有些代码利用了++/--的运算顺序,写出了极其晦涩的表达式。请看下面的例题:


int a = 1;
int b = a++ + ++a + a * 2;
// 问:b 的值是多少?

运算过程拆解:

  1. a++: 先用后变。表达式的这部分使用 a 的当前值 1。之后,a 变为 2
  2. ++a: 先变后用a 先从 2 变为 3。然后表达式的这部分使用新值 3
  3. a * 2: 此时 a 的值是 3,所以这部分是 3 * 2 = 6
  4. 最终计算: b = 1 + 3 + 6,所以 b 的结果是 10

结论:绝对不要写这样的代码! 它严重破坏了代码的可读性,是典型的“炫技”而非工程实践。在团队协作中,清晰易懂的代码远比所谓的“技巧”重要得多。

复合赋值运算符

复合赋值运算符是算术运算符和赋值运算符的简写,能让代码更简洁。


int a = 8;
a += 4;   // 等价于 a = a + 4;  // a 现在是 12
a -= 2;   // 等价于 a = a - 2;  // a 现在是 10
a *= 3;   // 等价于 a = a * 3;  // a 现在是 30
a /= 5;   // 等价于 a = a / 5;  // a 现在是 6
a %= 4;   // 等价于 a = a % 4;  // a 现在是 2

一个重要的特性:自动类型转换

复合赋值运算符会自动进行强制类型转换,这在处理不同数据类型时非常方便。


short s = 1;
// s = s + 1; // 编译错误!因为 s + 1 的结果是 int 类型,不能直接赋给 short
s += 1;       // 编译通过!等价于 s = (short)(s + 1);

使用复合赋值运算符不仅能简化代码,还能避免一些繁琐的类型转换问题。

关系与逻辑运算符

关系运算符

用于比较,结果为boolean>, <, >=, <=, ==, !=

逻辑运算符

用于连接布尔表达式:&& (与), || (或), ! (非)

短路特性&&中如果左边为false,则不计算右边;||中如果左边为true,则不计算右边。

int a = 10;
boolean b = a++ > 10 && ++a == 12; // a++ > 10 为 false
// 由于短路,&& 右边的 ++a 没有执行
System.out.println("a = "+a + ", b = "+b);
Short-circuit result

按位与移位:让你“看见二进制”

按位运算符直接对二进制位进行操作,常用于:权限位、掩码、性能敏感的低层逻辑、哈希与编码。

运算符 含义 例子(十进制)
& 按位与 6 & 3 == 2
| 按位或 6 | 3 == 7
^ 按位异或 6 ^ 3 == 5
~ 按位取反 ~0 == -1
<< 左移(补 0) 1 << 3 == 8
>> 算术右移(补符号位) -8 >> 1 == -4
>>> 逻辑右移(补 0) -1 >>> 1 变成一个很大的正数

最常见的坑:& 不是 &&

  • &&/|| 有短路;&/| 即使是 boolean 也会“左右都算”
  • 想检查“奇偶”:(x & 1) == 1(奇数)
int x = 6;             // 110
int y = 3;             // 011
System.out.println(x & y); // 010 -> 2
System.out.println(x | y); // 111 -> 7
System.out.println(x ^ y); // 101 -> 5

三元运算符

if-else语句的简化形式。

判断条件 ? 结果1 : 结果2

如果判断条件为true,则表达式的结果为“结果1”;否则为“结果2”。

int score = 50;
String result = score >= 60 ? "及格" : "不及格";
System.out.println(result); // 输出 "不及格"

流程控制

程序默认从上到下执行。流程控制语句可以改变代码的执行顺序,实现条件判断和循环。

代码块与作用域

用花括号 { } 包围起来的一段代码称为一个代码块。

变量的作用域(Scope)指的是变量能够被访问的范围。一个变量只在它被定义时所在的代码块及其子块中有效。

public static void main(String[] args) {
    int a = 10; // a的作用域是整个main方法
    {
        int b = 20; // b的作用域仅限于这个内部代码块
        System.out.println(a); // 可以访问a
        System.out.println(b); // 可以访问b
    }
    // System.out.println(b); // 错误!超出了b的作用域
}

选择结构:if-else-if

根据条件是否成立来决定执行哪段代码。

int score = 85;
if (score >= 90) {
    System.out.println("优秀");
} else if (score >= 80) {
    System.out.println("良好");
} else if (score >= 60) {
    System.out.println("及格");
} else {
    System.out.println("不及格");
}

选择结构:switch

适用于对一个变量进行多个“等值”判断的情况。

支持的类型: switch 支持的变量类型随JDK版本演进:

  • Java 5之前: 支持 byte, short, char, int
  • Java 5: 增加了对 enum (枚举) 类型的支持。
  • Java 7: 增加了对 String 类型的支持。

现代Java版本(12+)还引入了功能更强大的 "switch表达式",这是更高级的用法。

char level = 'B';
switch (level) {
    case 'A':
        System.out.println("优秀");
        break;  // break用于跳出switch, 否则会“穿透”到下一个case
    case 'B':
        System.out.println("良好");
        break;
    case 'C':
        System.out.println("及格");
        break;
    default:    // 如果以上都不匹配
        System.out.println("不及格");
        break;
}

switch 细节:fall-through 与 switch 表达式

fall-through(穿透)就是“没写 break,继续执行下一个 case”。有时是技巧,多数时候是坑。

int x = 1;
switch (x) {
    case 1:
        System.out.println("one");
    case 2:
        System.out.println("two"); // 会被打印(因为 case 1 没 break)
        break;
    default:
        System.out.println("other");
}

Java 12+ 引入 switch 表达式:可以直接得到一个值(不用手写临时变量)。

String level = "B";
int score = switch (level) {
    case "A" -> 95;
    case "B" -> 85;
    case "C" -> 70;
    default  -> 50;
};

建议:新代码优先使用“箭头”形式,减少 break 穿透带来的维护风险。

带标签 break/continue:跳出多层循环

普通的 break/continue 只能作用于最内层循环。带标签语法可以直接控制“跳到哪一层”。

public class LabeledLoopDemo {
    public static void main(String[] args) {
        int[][] grid = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
        };

        int target = 5;
        boolean found = false;

        outer:
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[i].length; j++) {
                if (grid[i][j] == target) {
                    found = true;
                    break outer;
                }
            }
        }

        System.out.println(found ? "找到了" : "没找到");
    }
}

建议:带标签控制流非常强,但也更“跳跃”。多数场景可以用“提取方法 + return”或“设置标志位”来替代,代码更容易维护。

循环结构:for 与调试器

System.out.println("准备就绪!"); }

学习使用调试器 (Debugger):在行号旁单击设置断点,然后点击像“甲虫”一样的调试按钮运行。程序会在断点处暂停,你可以观察变量的值。

Debug Button

核心技能:代码调试 (Debugging)

学会调试是程序员最重要的能力之一。

它让你拥有“全知之眼”,可以暂停、审查、控制程序的每一步执行,是定位和修复问题的最强武器。

忘掉 System.out.println 吧! 调试器提供了更强大、更高效的手段。

Debug View

调试第一步:设置断点 (Breakpoint)

断点是告诉调试器“当代码执行到这里时,请暂停”。

  1. 设置断点: 在代码编辑区的行号旁边,单击鼠标左键,会出现一个红点。
  2. 启动调试: 点击工具栏上的“甲虫”图标 (Debug),而不是“播放”图标 (Run)。

程序将正常运行,直到执行到你设置断点的那一行时,它会自动暂停,IDEA将切换到调试视图。

Set Breakpoint

认识调试器面板

当程序暂停时,调试器面板会显示所有关键信息:

Debug Panel
  • Frames (调用栈): 显示了程序是如何一步步调用到当前位置的。你可以点击不同的栈帧来查看该层级的变量状态。
  • Variables (变量): 显示当前作用域内所有变量的值。这是最有用的窗口,你可以实时看到每个变量的变化。
  • Watches (监视): 可以手动添加你想持续观察的变量或表达式。

核心操作:单步执行 (Stepping)

单步执行允许你逐行控制代码的运行,是调试的精髓。

    Debug Options
  • Step Over (F8): 最常用 。执行当前行,然后移动到下一行。如果当前行有方法调用,它会执行完整个方法,而不会进入方法内部。
  • Step Into (F7): 如果当前行有方法调用,此操作会进入该方法的内部,让你能逐行调试方法内的代码。
  • Step Out (Shift+F8): 如果你已经在一个方法内部,此操作会执行完该方法余下的所有代码,然后返回到调用它的地方。
  • Resume Program (F9): 继续执行程序,直到遇到下一个断点或程序结束。
Stepping Toolbar

把鼠标悬停在这些图标上,可以看到它们的名称和快捷键。

高级技巧:条件断点

有时候,你只想在特定条件下才暂停程序,例如在一个循环了1000次的for循环中,你只想在 `i == 999` 时暂停。

  1. 在断点(红点)上单击右键
  2. 在出现的输入框中,填写你的条件,例如 i > 50

这样,只有当这个布尔表达式为 true 时,程序才会在该断点处暂停。

Conditional Breakpoint

高级技巧:表达式求值

在调试暂停时,你不仅可以查看变量,还可以动态执行代码片段来验证逻辑。

  • 方法一:Watches窗口: 在Watches窗口中添加你想观察的表达式,例如 a + b > c
  • 方法二:Evaluate Expression (Alt+F8): 按下快捷键或右键菜单选择“Evaluate Expression”,会弹出一个窗口,你可以在里面输入任何合法的Java代码并立即看到结果。

这是验证复杂逻辑、测试边界条件、甚至在运行时“热修复”代码的强大工具。

循环结构:while 和 do-while

while 循环

只要循环条件为true,就一直执行循环体。适用于不确定循环次数的场景。

int i = 100;
while (i > 0) {
    System.out.println(i);
    i /= 2;
}

do-while 循环

while类似,但它保证循环体至少执行一次。

do {
    // 循环体
} while (循环条件);

循环控制:break 和 continue

break

立即终止并跳出当前循环。

continue

跳过本次循环的剩余部分,直接进入下一次循环。

带标签的 break

可以跳出指定的嵌套外层循环。

outer: for (int i = 1; i < 4; ++i) {
    for (int j = 1; j < 4; ++j) {
        if(i == j) break outer; // 当i==j时,直接终止外层循环
        System.out.println(i + ", " + j);
    }
}

带标签的 continue

可以跳过指定嵌套外层循环的本次迭代,直接进入外层循环的下一次迭代。

outer: for (int i = 1; i <= 3; ++i) {
    for (int j = 1; j <= 3; ++j) {
        if(i == 2) continue outer; // 当i==2时,跳过外层循环的本次迭代
        System.out.println(i + ", " + j);
    }
    System.out.println("外层循环结束,i = " + i);
}

调试练习:调试登录失败

这个练习更有挑战性。下面的代码模拟了一个登录程序,预设的密码是 "password123"。然而,无论你输入多么正确的密码,程序总是提示“登录失败”。

这段代码在逻辑上看起来没有问题,但它隐藏着一个Java初学者最常犯的经典错误。请运用调试器,找到原因


import java.util.Scanner;

public class LoginFailure {
    public static void main(String[] args) {
        String correctPassword = "password123";

        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入密码: ");
        String inputPassword = scanner.nextLine();

  
        if (inputPassword == correctPassword) {
            System.out.println("登录成功!");
        } else {
            System.out.println("登录失败!密码不正确。");
        }
    }
}
                

调试提示

  • if (inputPassword == correctPassword) 这一行设置断点。
  • 以调试模式运行程序,并在控制台输入正确的密码 "password123"。
  • 当程序在断点处暂停时,仔细观察 inputPasswordcorrectPassword 这两个变量。
  • 你会发现,尽管它们的(字符序列)看起来完全一样,但它们的ID(内存地址)却不同。
  • 核心问题:在Java中,== 用于比较对象的内存地址。对于字符串等对象,比较其内容是否相等应该使用 .equals() 方法。

这个练习告诉你:代码的“表面逻辑”和“底层实现”可能存在差异。调试器是看透这一切的“X光”。

调试练习:数组去重

给定一个有序数组,请原地删除重复元素,使每个元素只出现一次,并返回去重后的长度。

下面的代码“看起来”像是一个正确的双指针去重方法,但结果总是错。

请你用调试器找到 bug,并修复它。


public class RemoveDuplicatesDebug {
    public static void main(String[] args) {
        int[] nums = {1, 1, 2, 2, 3, 3, 4};
        int len = removeDuplicates(nums);

        System.out.println("新长度: " + len);
        System.out.print("去重后数组: ");
        for (int i = 0; i < len; i++) {
            System.out.print(nums[i] + " ");
        }
    }

    public static int removeDuplicates(int[] nums) {
        if (nums.length == 0) return 0;

        int slow = 0;
        for (int fast = 1; fast < nums.length; fast++) {
            if (nums[fast] == nums[slow]) {
                slow++;
                nums[slow] = nums[fast];
            }
        }
        return slow + 1;
    }
}
                

调试提示

  1. if (nums[fast] == nums[slow]) 这一行打断点,观察 fastslow 和数组内容。
  2. 用样例 {1,1,2,2,3,3,4} 单步执行,记录每次进入 if 的条件是否正确。
  3. 思考:双指针法里,什么时候才应该移动 slow 并写入新值?是“相等”还是“不相等”时?
  4. 修复后应输出:新长度: 4,前四个元素为 1 2 3 4

参考修复代码


public static int removeDuplicates(int[] nums) {
    if (nums.length == 0) return 0;

    int slow = 0;
    for (int fast = 1; fast < nums.length; fast++) {
        if (nums[fast] != nums[slow]) { // 只在遇到新值时前进 slow
            slow++;
            nums[slow] = nums[fast];
        }
    }
    return slow + 1;
}
                

这个练习的核心是循环不变式:任意时刻,区间 [0, slow] 都是已经去重完成的有序结果。调试时要盯住这个不变式是否被破坏。

实战练习

现在,让我们用所学的知识来解决一些经典问题吧!

练习一:寻找水仙花数

任务:找出所有1000以内的“水仙花数”。

定义:一个3位数,它的每个位上的数字的3次幂之和等于它本身。例如:153 = 1³ + 5³ + 3³

提示

  1. 使用for循环遍历所有3位数 (100到999)。
  2. 在循环中,获取每个数的个位、十位、百位。
    • 个位: num % 10
    • 十位: (num / 10) % 10
    • 百位: num / 100
  3. 判断立方和是否等于原数,如果是,则打印。

练习二:打印九九乘法表

任务:在控制台打印出标准的九九乘法表。

九九乘法表

提示

  1. 使用嵌套的for循环。
  2. 外层循环控制行数 (从1到9)。
  3. 内层循环控制列数,注意内层循环的结束条件与外层循环的变量有关 (j <= i)。
  4. 使用System.out.print()打印每个表达式,并用制表符\t分隔。
  5. 在外层循环的末尾,使用System.out.println()进行换行。

练习三:斐波那契数列

任务:获取斐波那契数列中第 n 位的值。

定义:一个数列,从第三项开始,每一项都等于前两项之和。数列:1, 1, 2, 3, 5, 8, 13, 21, ...

public static void main(String[] args) {
    int target = xxx;  // xx是从键盘读取的整数
    int result = 0;

    // 请在这里实现算法

    System.out.println(result); 
}

提示

  1. 处理特殊情况(如 target=1 或 target=2)。
  2. 定义两个变量,a=1, b=1,表示数列的前两项。
  3. 使用for循环,从第3项开始计算到第target项。
  4. 在循环中,计算sum = a + b,然后更新ab的值 (a = b; b = sum;)。
  5. 循环结束后,b的值即为所求结果。

面向过程部分学习完毕

下一章,我们将正式进入面向对象的世界!

1 / X