
简介《Java基础入门》课后习题答案面向Java零基础学习者与高校课程复习人群用于核对教材练习、梳理章节重点和补足编程题思路。内容按Java开发入门、语言基础、程序设计与面向对象编程等模块组织覆盖填空、判断、选择、简答及编程题参考答案并包含HelloWorld、奇数和、数组排序、Student类等示例代码便于对照理解语法、方法重载、封装继承多态等考点。资源包共1个doc文件约214KB结构简洁适合按章查阅和打印复习。已有5694人学习下载可作为课后自测、期末复习和上机练习的答案参照。1. 从对答案到跑通代码这套 Java 基础入门课后习题答案的真正用法翻《Java 基础入门》课后习题答案的人十有八九是卡在某一章的编程题上填空题对完就合书第二天照样写不出能跑的程序。这份答案覆盖 9 章从 Java 开发入门一路排到 JDBC除了填空、判断、选择的参考答案每章末尾还留了简答题和完整可编译的编程题代码。我把它当最小回归用例集用——每道编程题都能单独跑输出能对齐比对着答案点头有用得多。它适合两类人刚学完语法、想找参照实现的新手和带新人时需要一个现成检查清单的后端工程师。它不教你 Java但它把「写得对不对」这件事变成可验证的动作。2. 环境链路JDK、JRE 和 path/classpath 三个词到底管什么第 1 章的填空题看着最没技术含量实际是最容易埋雷的地方。答案里「JDK 中已经包含了 JRE装了 JDK 就不必再装 JRE」这句话本身没错但落到具体机器上很多人根本说不清自己的 java 命令到底来自哪个目录。环境链路这一层如果没打通后面 8 章的代码全是复制粘贴跑不通也不知道从哪查。2.1 JDK 与 JRE 的边界看目录结构比背定义可靠答案对 JRE 的描述是「相当于操作系统部分提供运行所需的基本条件和基础类」对 JDK 的描述是「包含编译工具、解释工具、文档制作工具、打包工具」。这段文字没问题但验证方式更直接去看安装根目录。# 打印 JDK 根目录没配 JAVA_HOME 就先手动 cd 到安装路径 echo $JAVA_HOME # 列出关键子目录 ls -1 $JAVA_HOME # 典型输出JDK 8 及以前 # bin javac、java、javadoc、jar 都在这里 # jre JDK 自带的运行时独立安装的 JRE 是同一套东西 # lib tools.jar、rt.jar 等基础类库这里有个版本边界值得记一下JDK 9 之后模块化改造安装目录下不再单独出现jre子目录运行时被拆成jmods和模块镜像。所以按老教材的描述去找jre文件夹在 JDK 17 上会扑空这不是装错了是版本差异。判断标准换一条就行javac和java两个可执行文件都在bin下那就是 JDK只有java没有javac那是 JRE。# 两个版本必须对得上否则会出现编译通过、运行报错 javac -version java -version参数说明这两个命令读的是PATH里第一个匹配项不是JAVA_HOME。机器上装过多版本 JDK 时JAVA_HOME指向 17、PATH里排前面的是 8就会出现「javac是 17、java是 8」的错配运行时抛UnsupportedClassVersionError报错信息里会写明 class file version 是多少、当前 JVM 支持到多少。看到这个错先别怀疑代码去核对这两个版本号。2.2 path 与 classpath一个给系统找命令一个给 JVM 找类这两个词在第 1 章填空题里是并列出现的但职责完全不同混在一起记是后面一系列「明明编译过了却跑不起来」的根源。pathWindows 里叫PATH是操作系统级的作用是让终端在任意目录下敲javac时能找到那个可执行文件。配错了的症状很直接javac 不是内部或外部命令或者command not found。classpath是 JVM 级的作用是让类加载器在启动时知道去哪些目录和 jar 包里找.class文件。配错了的症状是NoClassDefFoundError或者ClassNotFoundException——注意这两个不是一回事后面 2.4 会拆。# Linux / macOS把 JDK 的 bin 前置到 PATH只在当前终端会话生效 export PATH$JAVA_HOME/bin:$PATH # 运行期临时指定类路径当前目录下的 out 加一个驱动 jar java -cp ./out:./lib/mysql-connector-java-8.0.33.jar com.demo.Main参数说明-cp后面多个路径之间的分隔符Windows 用分号;Linux 和 macOS 用冒号:。这一条在跨平台时翻车率极高同一个脚本从 Windows 挪到服务器上直接报找不到主类先查这个。我一般会养成一个习惯不在系统环境变量里长期挂着CLASSPATH。全局CLASSPATH一旦设了就会覆盖-cp的默认值默认是当前目录.后续每个项目都得手动补当前目录时间一长全是玄学问题。需要什么就在命令行里用-cp显式写或者交给构建工具生成。# 用 Maven 直接把依赖树打成一条 classpath 字符串避免手写出错 mvn -q dependency:build-classpath -Dmdep.outputFilecp.txt java -cp target/classes:$(cat cp.txt) com.demo.Main2.3 用 HelloWorld 把编译链路完整走一遍答案第 1 章的编程题就是标准 HelloWorld。这段代码本身没什么可讲的价值在于它是一个最小可验证单元——环境对不对跑一遍就知道。public class HelloWorld { public static void main(String[] args) { // 类名必须与文件名严格一致包括大小写 System.out.println(这是第一个 Java 程序!); } }保存为HelloWorld.java然后# -d 指定字节码输出目录目录不存在会自动创建 javac -d out HelloWorld.java # 编译成功后 out 目录下会多出 HelloWorld.class ls out # -cp 告诉 JVM 去哪找 HelloWorld.class后面跟的是类名不带 .class 后缀 java -cp out HelloWorld # 想看编译器到底生成了什么反汇编一下 javap -c -p out/HelloWorld.class逻辑说明javac做的是把.java源文件翻译成 JVM 能识别的字节码.classjava启动 JVM通过类加载器按-cp指定的路径找到HelloWorld.class校验、准备、解析之后执行main方法。答案里说的「先编译生成 .class再由 Java 虚拟机解释执行」就是这个过程只不过现代 JVM 不是纯解释热点代码会被 JIT 编译成本地机器码。javap -c -p的参数含义-c反汇编出字节码指令-p显示所有成员包括 private。第一次看会看到一堆aload_0、invokespecial、getstatic不用急着看懂但至少能确认一件事System.out.println在字节码层面是getstatic java/lang/System.out加invokevirtual java/io/PrintStream.println。知道这层后面理解为什么System.out是静态字段、为什么println是实例方法会比死记硬背快。2.4 编译期错误和运行期错误要分开查新手最容易把这两类混着查结果方向完全跑偏。ClassNotFoundException是运行期异常出现在显式加载类的场景典型是Class.forName(com.mysql.cj.jdbc.Driver)找不到驱动或者反射里写错了全限定名。它是Exception的子类可以被 catch。NoClassDefFoundError是Error出现在 JVM 已经知道要用某个类、但实际加载失败的时候。常见原因是编译时依赖在、运行时 classpath 里没有或者静态初始化块里抛了异常导致类初始化失败。它是Errorcatch 它属于兜底手段正常应该修 classpath。区分方法很简单报错栈里如果带着Caused by: java.lang.ClassNotFoundException顺着往下看是哪一行触发的如果直接是NoClassDefFoundError且没有 cause先查-cp里有没有那个 jar再查那个类的静态块是不是炸了。3. 语法与对象短路、重载、构造、重写四个必验点第 2 章到第 4 章是整份答案密度最高的部分基本类型、运算符、方法重载、封装继承多态、抽象类接口几乎每道简答题背后都藏着一个「背下来会答、写出来会错」的点。挑四个必须动手验的。3.1 与 答案里那道除零题有个笔误答案对和的解释是对的两边都参与计算左边为 false 时右边不执行。但给的例子写成了boolean b x y x / 2 0和boolean b x y x / 2 0两行一模一样而且x / 2根本除不出零。显然是排版时把y敲成了2。按原意复原一下public class ShortCircuitAnd { public static void main(String[] args) { int x 2, y 0; // 非短路与左右两边都要算x / y 直接抛 ArithmeticException: / by zero boolean b1 (x y) (x / y 0); System.out.println(b1); } }public class ShortCircuitOr { public static void main(String[] args) { int x 2, y 0; // 短路与左边 x y 已经是 false右边整段被跳过 boolean b2 (x y) (x / y 0); System.out.println(b2); // 输出 false一行异常都不会有 } }逻辑说明用在 boolean 表达式之间时是逻辑与但它是非短路的两个操作数一定都会被求值才具备短路能力。这个差别在业务代码里的实际意义是obj ! null obj.getValue() 0这种写法能防 NPE换成就防不住。参数说明x / y是整数除法y为 0 时抛ArithmeticException如果换成double类型除以 0 得到的是Infinity而不是异常那时候这段代码不会崩但也验不出短路效果。练习题里用 int 是对的。3.2 方法重载签名只看方法名加参数列表答案第 2 章简答题第 3 问的标准表述是「多个同名方法参数个数或类型不一样JVM 根据实参绑定不同方法」。这句话要补一个边界返回值类型不参与重载判定。public class OverloadDemo { public int calc(int a) { return a; } // 1 个 int 参数 public int calc(int a, int b) { return a b; } // 参数个数不同构成重载 public double calc(double a) { return a * 2; } // 参数类型不同构成重载 public int calc(int a, long b) { return a; } // 顺序与类型组合不同构成重载 // 下面这行取消注释会直接编译失败method calc(int) is already defined // public String calc(int a) { return ; } }逻辑说明编译期在同一个类里收集所有同名方法按参数数量、类型、顺序做匹配返回值不进入这张表。所以「只改返回值」不构成重载编译器认为你重复定义了同一个方法。参数说明calc(1, 2)会被匹配到calc(int, int)因为 int 字面量不需要转换calc(1, 2L)匹配到calc(int, long)。如果同时存在calc(long, int)和calc(int, long)传入calc(1, 2)就会因为两个都能通过自动类型提升而变得不明确直接编译报错。答案没提这一层但实际写重载时很容易撞上。3.3 构造方法调用时机和 this 那一行的必要性答案第 3 章简答题第 1 问把构造方法和普通方法的区别写得很清楚实例化时自动调用、方法名与类名相同、没有返回值类型声明、不能 return 值。真正容易写错的是this那一行。public class Student { private String name; private double grade; // 无参构造给反射、框架、手工 new 留个口子 public Student() {} public Student(String name, double grade) { this.name name; // 左边的 this.name 是字段右边的 name 是参数 this.grade grade; } public void show() { System.out.println(name / grade); } public static void main(String[] args) { new Student(zhangsan, 99).show(); // 输出 zhangsan / 99.0 new Student().show(); // 输出 null / 0.0 } }逻辑说明形参名和字段名相同时作用域里形参把字段遮蔽了。this.name name中this.name明确指向当前对象的字段右边不带this的name指向形参。如果把this.去掉写成name name两边都指向形参字段永远保持默认值null和0.0。参数说明double字段不赋值时默认 0.0 而不是 0show()打印出来是99.0也印证了这一点。答案里stu1.setGrade(99)传的是 int 字面量靠自动类型提升转成 double这种隐式转换在参数是 double 时是安全的反过来会丢精度。顺带说下答案里UnderGraduate的写法它直接访问父类的public String name。教材为了简化用了 public 字段实际项目里字段应该是 private子类要用 getter 或者用protected。把 public 改成 private 之后UnderGraduate.show()立刻编译失败这个改动本身就是一个很好的练习。3.4 重写与多态编译看左边运行看右边答案第 4 章的Student/UnderGraduate是讲多态的标准例子。这里的关键不是记住「父类引用指向子类对象」这句话而是搞清楚调用到底走的哪个方法。class Person { protected String name; public Person(String name) { this.name name; } public void show() { System.out.println(Person: name); } } class UnderGraduate extends Person { private String degree; public UnderGraduate(String name, String degree) { super(name); // 父类没有无参构造这里必须显式调用否则编译失败 this.degree degree; } Override public void show() { System.out.println(本科: name / degree); } } public class PolyDemo { public static void main(String[] args) { Person p new UnderGraduate(lisi, bachelor); p.show(); // 输出「本科: lisi / bachelor」走子类重写后的实现 } }逻辑说明p的静态类型是Person编译器只认Person里有show()这个方法所以能通过编译运行时p指向的是UnderGraduate实例JVM 通过虚方法表找到子类重写后的show()执行。这就是「编译看左边运行看右边」。参数说明Override不是必需的但它能让编译器帮你检查——如果方法签名写错比如把show()写成show(String)编译器会直接报「method does not override」不至于静默变成一个重载方法。IDE 里这个注解我基本每次都加。还有一个边界static方法、final方法、private方法不能被重写。private方法在子类里写一个同名同参的方法编译器认为那是新方法而不是重写加Override会直接报错。答案没展开这一层但面试和实际调试都绕不开。3.5 抽象类与接口Shape 那道题为什么用接口答案第 4 章编程题第 2 问用interface Shape定义area(double)Square和Circle分别实现。选择理由要讲清楚不然下次遇到类似需求还是会犹豫。interface Shape { double area(double v); // 接口方法隐式 public abstract } class Square implements Shape { Override public double area(double side) { return side * side; } } class Circle implements Shape { Override public double area(double r) { return Math.PI * r * r; } } public class ShapeDemo { public static void main(String[] args) { Shape[] shapes { new Square(), new Circle() }; double[] params { 2, 3 }; for (int i 0; i shapes.length; i) { System.out.println(shapes[i].area(params[i])); } } }逻辑说明接口只描述「能算面积」这个能力正方形和圆之间没有任何共享状态用接口比用抽象类更轻。如果两个图形都需要共享一个color字段或者一段公共的日志逻辑那就该用抽象类因为抽象类可以有字段和已实现的方法。参数说明area(double v)里的v在不同实现里含义不同边长、半径这是接口定义的一个妥协——统一签名牺牲了语义清晰度。更严谨的做法是定义两个方法或者用泛型但教材层面这样写没问题知道这个边界就行。4. 常用类与集合String 不可变、hashCode 与字节流的三个实验第 5 章到第 7 章是答案里代码量最大的部分也是「照抄能过、改一点就崩」的高发区。抽三个必须亲手跑一遍的实验。4.1 String 与 StringBuffer不可变类和可变缓冲区的差别答案对String和StringBuffer的定义是准确的String是不可变类StringBuffer是可变字符串缓冲区。光看定义没感觉跑一遍反转大小写的题就清楚了。public class ReverseCase { public static void main(String[] args) { String str HelloWorld; char[] ch str.toCharArray(); // String 转 char 数组逐位处理 StringBuffer buffer new StringBuffer(); for (int i ch.length - 1; i 0; i--) { // 从后往前遍历天然实现反转 if (ch[i] A ch[i] Z) { buffer.append(Character.toLowerCase(ch[i])); // 大写转小写 } else if (ch[i] a ch[i] z) { buffer.append(Character.toUpperCase(ch[i])); // 小写转大写 } else { buffer.append(ch[i]); // 非字母原样保留 } } System.out.println(buffer.toString()); // 输出 DLROwOLLEh } }逻辑说明toCharArray()把字符串拆成字符数组遍历时从末尾开始等于做完反转再做大小写翻转。答案里只处理了大写和小写两个分支没处理数字和符号如果输入是Hello2024那四个数字会被直接丢掉——补一个else分支就完整了。参数说明buffer.append(char)有重载版本传 char 不会做额外装箱buffer.toString()在最后调用一次避免在循环里反复转换。如果循环里用String s ; s ch[i];每次拼接都会新建对象长度到几千时差异很明显。4.2 hashCode 与 equalsHashSet 判重的真实顺序这是整份答案里我认为最需要动手改的一题。答案的Person类重写了hashCode()返回name.hashCode()重写了equals()但只比较了name、没有比较age也没有做类型判断。结果就是Jack 25和Jack 27会被判定为同一个对象。import java.util.HashSet; class Person { String name; int age; Person(String name, int age) { this.name name; this.age age; } Override public int hashCode() { return name.hashCode(); // 只按 name 算哈希同名对象落在同一个桶 } Override public boolean equals(Object obj) { if (this obj) return true; // 同一引用直接返回 true if (obj null || getClass() ! obj.getClass()) return false; // 类型判断不能省 Person p (Person) obj; return age p.age name.equals(p.name); // 补齐 age 比较 } } public class HashSetDemo { public static void main(String[] args) { HashSetPerson set new HashSet(); set.add(new Person(Jack, 25)); set.add(new Person(Rose, 23)); set.add(new Person(Jack, 27)); System.out.println(set.size()); // 补齐 equals 后是 3答案原版是 2 } }逻辑说明HashSet添加元素分两步。先用hashCode()算出桶位置桶里没有元素就直接放进去桶里已有元素才逐个调用equals()比较。所以hashCode决定「落在哪个桶」equals决定「桶里是不是真的同一个」。两个方法必须成对重写只重写一个会出现「明明 equals 相等却加进去两个」或者反过来的情况。参数说明getClass() ! obj.getClass()比instanceof更严格父子类之间不认为是相等如果业务上允许子类对象与父类对象相等改成instanceof。实际项目里我一般直接让 IDE 生成用Objects.hash(name, age)和Objects.equals组合少踩空指针的坑。顺带说下答案里TreeMap那道题自定义Comparator返回ele2.compareTo(ele1)实现键的降序。这里如果返回 0TreeMap会认为两个键相同后面的值覆盖前面的值跟HashMap的行为完全不同。这个差异在数据去重场景里是血泪经验。4.3 字节流与字符流两种拷贝方式的差异在哪答案第 7 章编程题第 1 问把字节流拷贝和字符流拷贝放在一个类里这个对比很有价值但里面有几个参数不改就会出问题。import java.io.*; public class CopyDemo { public static void main(String[] args) throws IOException { // 字节流图片、压缩包、视频这类二进制内容 try (FileInputStream in new FileInputStream(src.bin); FileOutputStream out new FileOutputStream(des1.bin)) { byte[] buf new byte[8192]; // 缓冲区 8 KB小文件 1024 也够 int len; while ((len in.read(buf)) ! -1) { out.write(buf, 0, len); // 必须写 len不能写 buf.length } } // 字符流纯文本按平台默认编码解码成字符 try (BufferedReader br new BufferedReader(new FileReader(src.txt)); BufferedWriter bw new BufferedWriter(new FileWriter(des2.txt))) { String line; while ((line br.readLine()) ! null) { bw.write(line); bw.newLine(); // readLine 会吃掉换行必须手动补 } } } }逻辑说明read(buf)返回的是本次实际读到的字节数最后一次读取通常小于buf.length。如果写成out.write(buf)缓冲区里上一轮的残留数据会被一起写出去文件末尾多出一段垃圾。这是字节流拷贝最经典的一个坑答案里写的是out.write(buf, 0, len)是对的。参数说明readLine()按行读取返回的字符串里不含换行符所以写回时必须调用newLine()补上否则所有行会粘成一行。newLine()用的是平台换行符Windows 下是\r\nLinux 下是\n跨平台传输文本时要注意这一点。答案第 7 章简答题还提到 NIO 的 Buffer、Channel、Selector 三件套。这部分教材只讲概念实际要验证的话最直观的是用FileChannel做一次内存映射读取对比一下传统流在大文件上的表现差异。不过既然题目只要求概念描述先把字节流和字符流跑通更划算。4.4 JDBC 六步连接、语句、结果集、关闭的顺序不能乱答案第 9 章简答题第 2 问把 JDBC 流程归纳成六步加载驱动、获取连接、获取 Statement、执行 SQL、操作 ResultSet、关闭资源。这个顺序背下来容易写的时候最容易错的是第 3 步到底用Statement还是PreparedStatement。import java.sql.*; public class JdbcDemo { private static Connection getConn() throws Exception { // 步骤 1加载驱动。JDBC 4.0 之后可省略写出来便于排查问题 Class.forName(com.mysql.cj.jdbc.Driver); // 8.x 驱动必须带时区参数否则连接直接失败 String url jdbc:mysql://127.0.0.1:3306/javatest?useSSLfalseserverTimezoneAsia/Shanghai; return DriverManager.getConnection(url, root, 1234); // 步骤 2获取连接 } public static void main(String[] args) throws Exception { String sql select id, name, email from tb_user where sex ?; try (Connection conn getConn(); PreparedStatement ps conn.prepareStatement(sql)) { // 步骤 3预编译语句 ps.setString(1, 男); // 步骤 4占位符下标从 1 开始 try (ResultSet rs ps.executeQuery()) { // 步骤 5执行查询 while (rs.next()) { // 游标初始在第一行之前 System.out.println(rs.getInt(id) \t rs.getString(name)); } } } // try-with-resources 自动关闭顺序是 rs → ps → conn } }逻辑说明PreparedStatement的 SQL 在创建时就发给数据库预编译参数用setXxx填进去。答案简答题列了三条好处可读性、预编译带来的性能、防止 SQL 注入。第三条最实际——拼接字符串的写法遇到 or 11这种输入直接沦陷占位符会把参数强制当数据处理不参与 SQL 语法解析。参数说明setString(1, 男)里的 1 是占位符序号从 1 开始而不是 0这一点跟数组完全相反写惯了 Java 的人第一次很容易写setString(0, ...)然后报Parameter index out of range。rs.next()每次调用把游标下移一行并返回是否有数据初始位置在第一行之前所以必须先调用一次才能读第一行。关闭顺序必须是 ResultSet → Statement → Connection反过来关会报错或者留下未释放的游标。try-with-resources 会自动按声明的逆序关闭跟手写 finally 块相比少了一大段样板代码。还有一处版本边界答案里驱动类名写的是com.mysql.jdbc.Driver那是 5.x 的类名。8.x 驱动换成了com.mysql.cj.jdbc.Driver旧类名虽然还能用但会打一行弃用警告。这一点在避坑章会展开。5. 避坑抄这份答案时最容易撞上的五个坑教材答案有一个共同特征它在作者的机器上跑通过但换一台机器、换一个版本就可能挂。下面五条是照抄这份答案时出现频率最高的每条按现象、原因、解决三段走。5.1 路径与分隔符类的坑坑一E:/src.txt在 Linux 上直接FileNotFoundException。现象第 7 章的文件拷贝代码一字未改运行报java.io.FileNotFoundException: E:/src.txt (No such file or directory)。原因答案用的是 Windows 盘符路径Linux 和 macOS 没有E:这个概念即便在 Windows 上如果E盘没有src.txt一样报错。解决换成相对路径src.txt相对当前工作目录即启动 JVM 的目录或者用Paths.get(System.getProperty(user.dir), src.txt)拼出来。更稳的做法是把路径作为main的参数传入代码里不出现硬编码路径。测试时先用new File(path).exists()打一行确认比事后猜快得多。坑二classpath 里用了分号Linux 上找不到主类。现象本地 Windows 上java -cp out;lib/a.jar Main跑得好好的扔到服务器上改成java -cp out;lib/a.jar Main报Error: Could not find or load main class Main。原因Windows 的 classpath 分隔符是分号Linux 和 macOS 是冒号。服务器上分号被当成路径的一部分整个字符串变成一个不存在的目录。解决Linux 下写java -cp out:lib/a.jar Main。跨平台脚本里可以用${path.separator}但更省事的办法是交给 Maven 或 Gradle 生成 classpath 文件别手写。坑三public class的名字和文件名不一致javac直接拒绝编译。现象把答案里的Test01改成Test02保存但忘了改文件名编译报class Test02 is public, should be declared in a file named Test02.java。原因Java 规定一个源文件里最多有一个public类且文件名必须与这个类名完全一致包括大小写。解决改文件名或者把public去掉。另外要注意从网页复制答案时经常带着全角分号、全角括号编译报illegal character: \uff1b就是这个IDE 里打开「显示空白字符」能一眼看出来。5.2 类型与依赖类的坑坑四驱动类名还是 5.x 的老名字ClassNotFoundException。现象第 9 章的getConn()原样照抄Class.forName(com.mysql.jdbc.Driver)抛ClassNotFoundException但 jar 包明明已经放进 classpath 了。原因MySQL 8.x 的驱动包把类名改成了com.mysql.cj.jdbc.Driver旧类名在部分版本里还保留做兼容但换一个驱动版本就没了。解决改成com.mysql.cj.jdbc.Driver连接串补上serverTimezone否则 8.x 驱动会抛时区相关的异常。如果只是学习语法不想装数据库可以先用 H2 的内存模式跑通六步流程jdbc:h2:mem:test不需要任何外部服务。坑五HashSet去重失灵同名对象加进去两个。现象第 6 章那道集合题两个Person对象的name和age都一样set.size()却是 2。原因只重写了equals()没重写hashCode()两个对象算出的哈希值不同落在不同的桶里HashSet根本不会去调用equals比较。解决两个方法成对重写字段保持一致——hashCode里用到的字段equals里也要用到反之亦然。用 IDE 生成或者Objects.hash(...)加Objects.equals(...)组合比手写稳。写完跑一遍「两个相同对象加进 setsize 应该是 1」的验证别靠肉眼判断。这里还有一个不那么显眼但很常见的Swing 里给JFrame注册MouseListener点按钮时坐标不更新。原因是鼠标事件被子组件消费了没冒泡到窗口。解决办法是把监听器加到具体组件上或者用getContentPane()上的监听器配合坐标转换。GUI 那章的代码量最大但真正跑起来调事件的机会不多知道有这么回事就行。6. 把答案变成回归测试一份可复用的自检脚本前面几章都是单点验证最后说一个我自己常用的做法把这份答案里的编程题抽成方法写一个统一的main跑断言。这样每次改环境、换 JDK 版本、调整某段实现跑一遍就知道有没有退化不用一题一题手工对输出。关键是把「期望值」写死。下面本文还有配套的精品资源点击获取