☰
Java基础入门课后习题答案:从对答案到跑通代码
2026/10/29 18:38:22 网站建设 项目流程

简介:《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 章填空题里是并列出现的,但职责完全不同,混在一起记是后面一系列「明明编译过了却跑不起来」的根源。

path(Windows 里叫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.outputFile=cp.txt java -cp "target/classes:$(cat cp.txt)" com.demo.Main

2.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 能识别的字节码.class;java启动 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 里没有,或者静态初始化块里抛了异常导致类初始化失败。它是Error,catch 它属于兜底手段,正常应该修 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 而不是 0,show()打印出来是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 与 equals:HashSet 判重的真实顺序

这是整份答案里我认为最需要动手改的一题。答案的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) { HashSet<Person> 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)实现键的降序。这里如果返回 0,TreeMap会认为两个键相同,后面的值覆盖前面的值,跟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\n,Linux 下是\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?useSSL=false&serverTimezone=Asia/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 '1'='1这种输入直接沦陷,占位符会把参数强制当数据处理,不参与 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(...)组合,比手写稳。写完跑一遍「两个相同对象加进 set,size 应该是 1」的验证,别靠肉眼判断。

这里还有一个不那么显眼但很常见的:Swing 里给JFrame注册MouseListener,点按钮时坐标不更新。原因是鼠标事件被子组件消费了,没冒泡到窗口。解决办法是把监听器加到具体组件上,或者用getContentPane()上的监听器配合坐标转换。GUI 那章的代码量最大,但真正跑起来调事件的机会不多,知道有这么回事就行。

6. 把答案变成回归测试:一份可复用的自检脚本

前面几章都是单点验证,最后说一个我自己常用的做法:把这份答案里的编程题抽成方法,写一个统一的main跑断言。这样每次改环境、换 JDK 版本、调整某段实现,跑一遍就知道有没有退化,不用一题一题手工对输出。

关键是把「期望值」写死。下面

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询