ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java基础入门课后题答案:从HelloWorld到JDBC的代码级拆解

Java基础入门课后题答案:从HelloWorld到JDBC的代码级拆解

简介:这份文档面向Java初学者与高校学生,针对《Java基础入门》教材课后习题提供系统参考答案,帮助读者在学完每章后及时核对思路、巩固语法与面向对象概念。压缩包内共1个doc文件,约214KB,按章节组织填空、判断、选择、简答与编程题答案,覆盖Java开发入门、语言基础、程序设计及面向对象编程等模块,并附HelloWorld、奇数求和、数组排序、学生类封装等编程题参考代码。已有5695人学习下载,适合课堂配套练习、期末复习与自学自查,可对照简答要点理解JRE与JDK区别、短路运算符、方法重载、构造方法及封装继承多态等高频考点,也能借助编程题示例梳理编译运行流程与代码书写规范,快速定位薄弱环节并查漏补缺。

1. 一份课后题答案,为什么值得当成“可执行规格书”来读

带过几届新人之后我发现一个反直觉的现象:真正把 Java 基础打牢的人,往往不是刷题最多的,而是会把课后题答案逐行跑一遍、再故意改坏它看报错的那批人。这份《Java 基础入门》课后习题答案覆盖第 1 章到第 9 章,从 JDK/JRE 关系到八种基本数据类型、从&&短路到方法重载、从集合去重到 JDBC 六步走,题目和参考答案是成对出现的。它解决的不是“答案在哪”,而是“每个知识点在代码里长什么样”。适合正在自学 Java 后端、准备面试八股、或者带新人需要一份对照清单的从业者。下面我按“先跑通、再拆解、最后避坑”的顺序,把它当成一份可复现的规格书来拆。

2. 从 HelloWorld 到字节码:把第 1 章的编译链路跑通

第 1 章看着最水,其实藏着后面所有章节的地基。填空题里 Java EE / Java SE / Java ME 的三分法、JRE 与 JDK 的包含关系、javac与java两个命令的分工,简答题里“先编译成 .class 再由 JVM 解释执行”的两步流程,这些如果只是背下来,遇到NoClassDefFoundError或UnsupportedClassVersionError时照样抓瞎。我的做法是把这一章当成一次环境自检:不装 IDE,纯命令行走一遍,把每个报错都当成知识点来读。

2.1 用命令行复现编译与运行两步

先把参考答案里的 HelloWorld 原样落地,注意文件名必须和 public 类名一致,这是 Java 的硬约束,不是风格建议。

# 目录结构建议:每章一个文件夹,避免 classpath 混乱 mkdir -p ch01 && cd ch01 # 写入源文件,文件名必须与 public class 名完全一致 cat > HelloWorld.java <<'EOF' public class HelloWorld { public static void main(String[] args) { System.out.println("这是第一个 Java 程序!"); } } EOF # 第一步:编译,生成 HelloWorld.class 字节码 javac HelloWorld.java # 第二步:运行,注意这里写的是类名,不带 .class 后缀 java HelloWorld

javac负责把.java编译成.class,java负责启动 JVM 加载并解释执行字节码,这正是第 1 章简答题第 3 问的原文逻辑。参数上要留意两点:javac -d out HelloWorld.java可以把字节码输出到指定目录,配合java -cp out HelloWorld使用,这是后面多包结构项目的必备姿势;java -version和javac -version的版本号必须一致,不一致就是典型的“编译用高版本、运行用低版本”,会直接抛UnsupportedClassVersionError。

2.2 用 PATH 与 CLASSPATH 验证第 1 章填空题

填空题里bin目录、path、classpath三个空,很多人填对了但没真正理解。PATH决定系统去哪找javac.exe/java.exe这些可执行程序,CLASSPATH决定 JVM 去哪找.class文件。验证方法很直接:临时清空CLASSPATH,在非当前目录下运行,看它是否还能找到类。

# 查看当前 JAVA_HOME 与 PATH 是否指向同一个 JDK echo $JAVA_HOME which javac which java # 故意在别的目录运行,观察 classpath 行为 cd /tmp java -cp /path/to/ch01 HelloWorld # 显式指定 classpath,能跑通 java HelloWorld # 不指定,通常报 Could not find or load main class

这里的关键认知是:CLASSPATH不配也能跑,是因为现代 JDK 默认把当前目录.算进去;一旦项目有了包名(package com.xxx;),当前目录就不再是类的根路径,必须用-cp指到包的父目录。第 1 章那些看似死记硬背的填空,本质是在给你一张“出问题时先查哪里”的地图。

3. 基本类型、短路运算与重载:第 2 章的三个高频翻车点

第 2 章是整份答案里信息密度最高的一章。八种基本数据类型及字节数、&与&&的区别、方法重载的判定规则,这三块几乎每次面试都会碰到,而且都能用极短的代码复现出“看起来对、跑起来错”的效果。我一般会让新人先把这三段代码手敲一遍,不许复制,敲完再对照答案看差异。

3.1 八种基本类型的字节数与取值范围实测

简答题第 1 问列了 byte/short/int/long/float/double/char/boolean 的字节数,但“boolean 占一个字节”这个说法在不同 JVM 实现里其实有争议,规范只保证它能表示 true/false。与其纠结,不如用代码把边界值打出来,形成肌肉记忆。

public class PrimitiveBounds { public static void main(String[] args) { // 整型的边界,注意 int 字面量溢出会直接编译报错 System.out.println("byte: " + Byte.MIN_VALUE + " ~ " + Byte.MAX_VALUE); System.out.println("short: " + Short.MIN_VALUE + " ~ " + Short.MAX_VALUE); System.out.println("int: " + Integer.MIN_VALUE + " ~ " + Integer.MAX_VALUE); System.out.println("long: " + Long.MIN_VALUE + " ~ " + Long.MAX_VALUE); // 浮点型没有精确边界,用 MIN_VALUE/MAX_VALUE 看的是可表示的最小正数/最大数 System.out.println("float: " + Float.MIN_VALUE + " ~ " + Float.MAX_VALUE); System.out.println("double: " + Double.MIN_VALUE + " ~ " + Double.MAX_VALUE); // char 是无符号 16 位,范围 0 ~ 65535 System.out.println("char: " + (int) Character.MIN_VALUE + " ~ " + (int) Character.MAX_VALUE); // 经典陷阱:两个 int 相加溢出,结果变负数 int a = Integer.MAX_VALUE; System.out.println("溢出演示: " + (a + 1)); } }

这段代码的价值在于把“字节数”翻译成“边界值”。int是 4 字节、32 位,所以最大值是 2^31-1;long是 8 字节,赋值时字面量要加L后缀,否则long x = 9999999999;会编译失败,因为右边默认按 int 解析已经溢出。float只有 4 字节却要表示很大范围,代价是精度只有约 7 位有效数字,这也是为什么金额计算一律用BigDecimal而不是double——这条经验第 2 章没写,但它是从“字节数”这个知识点自然推出来的。

3.2&与&&的短路差异:用除零异常验证

简答题第 2 问讲得很清楚:&两边都算,&&左边为 false 就短路。原文给的例子是x < y & x / 2 > 0会触发除零异常。这里有个细节要注意,原文例子里写的是x / 2,但描述里说的是“被 0 除”,实际要触发异常得让除数是 0。我把它改成可运行的版本,顺便把|和||也一起验证了。

public class ShortCircuitDemo { public static void main(String[] args) { int x = 2, y = 0; // 非短路 &:右边 x / y 一定会执行,y 为 0 直接抛 ArithmeticException try { boolean b1 = x < y & x / y > 0; System.out.println("b1 = " + b1); } catch (ArithmeticException e) { System.out.println("& 触发了除零异常: " + e.getMessage()); } // 短路 &&:左边 x < y 为 false,右边根本不执行,安全 boolean b2 = x < y && x / y > 0; System.out.println("&& 结果 = " + b2 + ",没有异常"); // 同理验证 | 与 || try { boolean b3 = x > y | x / y > 0; System.out.println("b3 = " + b3); } catch (ArithmeticException e) { System.out.println("| 触发了除零异常: " + e.getMessage()); } boolean b4 = x > y || x / y > 0; System.out.println("|| 结果 = " + b4 + ",没有异常"); } }

运行结果会明确告诉你:&和|是位运算符兼逻辑运算符,用在 boolean 上不做短路;&&和||才是短路逻辑运算符。实际开发里判断对象非空再取属性,obj != null && obj.getName() != null这种写法能跑,换成&就可能 NPE,这就是“知道区别”和“用对区别”的差距。参数上没什么可调的,但要注意:短路顺序会影响副作用,如果右边表达式带赋值或方法调用,短路与否直接决定它执行不执行。

3.3 方法重载的判定:只有参数列表算数

简答题第 3 问说重载是“同名方法、参数个数或类型不同”。这句话漏了关键边界:返回值类型不同不构成重载,参数名不同也不构成重载,只有参数列表(个数、类型、顺序)不同才算。下面这段代码把合法与非法两种情况都摆出来。

public class OverloadDemo { // 合法重载:参数个数不同 public int sum(int a) { return a; } public int sum(int a, int b) { return a + b; } // 合法重载:参数类型不同 public double sum(double a, double b) { return a + b; } // 合法重载:参数顺序不同 public String sum(String s, int n) { return s + n; } public String sum(int n, String s) { return n + s; } // 非法:仅返回值不同,编译直接报错,下面这行放开就编译不过 // public double sum(int a, int b) { return a + b; } public static void main(String[] args) { OverloadDemo d = new OverloadDemo(); System.out.println(d.sum(1)); // 命中 sum(int) System.out.println(d.sum(1, 2)); // 命中 sum(int,int) System.out.println(d.sum(1.0, 2.0)); // 命中 sum(double,double) System.out.println(d.sum("x", 1)); // 命中 sum(String,int) } }

JVM 在编译期就根据实参的静态类型完成方法绑定,这叫静态分派,所以重载是“编译期多态”,和后面第 4 章的重写(运行期多态)是两回事。容易翻车的点是自动类型提升:d.sum(1, 2)如果只有sum(double,double)一个候选,int 会被提升成 double 去匹配,结果可能和预期不符;有精确匹配时优先精确匹配。这也是为什么重载设计要避免“只差一个类型宽度”的版本,否则调用方永远猜不到命中了哪个。

4. 面向对象两章:封装、继承、多态在代码里的真实边界

第 3、4 章合起来是面向对象的上下篇,答案里既有概念简答,也有 Student 类、图形接口这类编程题。这两章最容易被当成“背概念”,但真正拉开差距的是访问修饰符的可见范围、构造方法与普通方法的调用时机、重写与重载的区别、抽象类与接口的选型。我习惯把这两章的编程题当成一个小型重构练习:先按答案写,再故意破坏封装看编译器怎么拦。

4.1 四种访问修饰符的可见范围用代码验证

填空题第 3 问列了 private、default、protected、public 四个修饰符,简答题讲封装是“隐藏实现细节”。光背表格没用,得让编译器亲口告诉你边界在哪。下面用同包和跨包两个类来验证。

// 文件:pkg1/Base.java package pkg1; public class Base { private int a = 1; // 仅本类可见 int b = 2; // default,仅同包可见 protected int c = 3; // 同包 + 子类可见 public int d = 4; // 全局可见 private void privateMethod() {} protected void protectedMethod() {} } // 文件:pkg2/Sub.java package pkg2; import pkg1.Base; public class Sub extends Base { public void test() { // System.out.println(a); // 编译错误:private 不可见 // System.out.println(b); // 编译错误:default 跨包不可见 System.out.println(c); // 可以:protected 对子类可见 System.out.println(d); // 可以:public 全局可见 protectedMethod(); // 可以:继承来的 protected 方法 } }

这段代码把“封装”从口号变成了编译器的实际约束。private字段只能通过 getter/setter 访问,这正是第 3 章编程题里 Student 类写getName/setName的原因;protected的微妙之处在于它跨包时只对子类可见,同包内则等同于 default,很多人在写工具类时误用 protected 导致外部调不到。参数上没什么可调,但要注意:字段默认值(int 为 0、引用为 null)只在成员变量上成立,局部变量不初始化直接用会编译报错,这是第 2 章和第 3 章的交界知识点。

4.2 构造方法、this 与 super 的调用链

第 3 章简答题第 1、2 问反复强调构造方法“方法名与类名相同、无返回值声明、不能 return 值”。第 4 章编程题里UnderGraduate用super(name, age)调父类构造,这条链的执行顺序是新手最容易搞混的。写个带打印的版本,一眼看清顺序。

class Person { String name; Person(String name) { this.name = name; System.out.println("1. Person 构造执行, name=" + name); } } class Student extends Person { int age; Student(String name, int age) { super(name); // 必须第一行,否则编译报错 this.age = age; System.out.println("2. Student 构造执行, age=" + age); } } public class ConstructorOrder { public static void main(String[] args) { new Student("zhangsan", 20); // 输出顺序:先 Person 后 Student } }

super(...)必须是子类构造方法的第一条语句,这是硬规则;如果子类构造里不写,编译器会自动插入super()去调父类无参构造,而父类一旦只定义了有参构造、没写无参构造,子类就会编译失败——这是继承体系里最常见的翻车点之一。this(...)用于同类构造方法之间互相调用,同样必须在第一行,且不能和super(...)同时出现。理解这条调用链,后面看 Spring 里 Bean 的初始化顺序、看任何框架的继承结构都不会懵。

4.3 抽象类与接口的选型:第 4 章编程题给的答案

第 4 章编程题第 2 问用Shape接口 +Square/Circle实现类演示多态,简答题第 3 问讲了抽象类和接口的区别。选型上我的经验是:需要定义“是什么”且带共享状态/实现,用抽象类;需要定义“能做什么”的能力契约,用接口。Java 8 之后接口可以有 default 方法和 static 方法,边界模糊了一些,但“单继承多实现”这条没变。

interface Shape { double area(double v); // 抽象方法,实现类必须写 default String desc() { // 默认方法,实现类可不写 return "面积=" + area(1); } } abstract class AbstractShape implements Shape { protected String color = "red"; // 抽象类可以有成员变量 public abstract double area(double v); public String getColor() { return color; } } class Square extends AbstractShape { public double area(double side) { return side * side; } } public class ShapeDemo { public static void main(String[] args) { Shape s = new Square(); // 父类型引用指向子类对象 System.out.println(s.area(2)); System.out.println(s.desc()); } }

这里体现的是第 4 章简答题第 2 问说的“父类类型变量引用子类对象,根据被引用子类对象的特性得到不同运行效果”,也就是运行期多态,靠的是方法表在运行时动态绑定。和第 3 章的重载(编译期静态分派)对照着看,两个概念就不会混。接口里不能有实例字段,抽象类可以有;接口方法默认 public,抽象类方法可以任意访问修饰符——这些差异在写框架扩展点时直接决定你的设计能不能落地。

5. 集合、IO 与 JDBC:第 6 到 9 章的踩坑清单

第 5 到 9 章跨度大,但答案的组织方式很统一:每章都是填空、判断、选择、简答、编程五段式。真正有复现价值的是编程题,尤其是第 6 章 HashSet 去重、第 7 章字节流与字符流拷贝、第 9 章 JDBC 六步走。这三块恰好也是实际项目里 bug 最密集的地方。下面这份清单是我带人时反复遇到的,按“现象 → 原因 → 解决”整理,建议对照答案里的代码逐条验证。

5.1 集合去重失效:hashCode 与 equals 必须成对重写

第 6 章编程题第 1 问用 HashSet 存 Person,答案里重写了hashCode()返回name.hashCode()、equals()只比较 name。现象是:明明两个 Person 的 name 相同,HashSet 里却存了两条。原因在于 HashSet 先按 hashCode 定位桶,再用 equals 比较,只重写其中一个就会出现“hash 相同但 equals 判不等”或“equals 相等但 hash 不同落不同桶”的情况。

import java.util.*; class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } @Override public int hashCode() { return Objects.hash(name); } // 只按 name 算哈希 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person p = (Person) o; return Objects.equals(name, p.name); // 只按 name 判等 } } public class SetDedup { public static void main(String[] args) { Set<Person> set = new HashSet<>(); set.add(new Person("Jack", 25)); set.add(new Person("Jack", 27)); // name 相同,被视为重复 System.out.println(set.size()); // 输出 1 } }

解决方式就是成对重写,且参与 equals 的字段必须参与 hashCode 计算。答案里用name.hashCode()是简化写法,生产代码建议用Objects.hash(...)或 IDE 生成,避免手写漏字段。另一个坑是可变对象放进 HashSet 后修改了参与哈希的字段,导致再也查不到,这条答案没提但很致命。

5.2 IO 拷贝的编码与资源释放:字节流和字符流别混用

第 7 章编程题第 1 问同时给了字节流和字符流两种拷贝方式。现象是:用字符流拷贝图片或压缩包,文件损坏;或者拷贝中文文本后乱码。原因是字符流底层依赖字符编码,二进制文件经过解码再编码必然失真;乱码则是FileReader用了平台默认编码,源文件和运行环境编码不一致。

import java.io.*; public class CopyDemo { // 二进制文件:必须用字节流 public static void copyBinary(String src, String dest) throws IOException { try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dest)) { byte[] buf = new byte[8192]; int len; while ((len = in.read(buf)) != -1) { out.write(buf, 0, len); } } } // 文本文件:用字符流并显式指定编码 public static void copyText(String src, String dest) throws IOException { try (BufferedReader br = new BufferedReader( new InputStreamReader(new FileInputStream(src), "UTF-8")); BufferedWriter bw = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(dest), "UTF-8"))) { String line; while ((line = br.readLine()) != null) { bw.write(line); bw.newLine(); } } } }

解决要点有三个:二进制一律字节流,文本才用字符流;字符流务必显式指定编码,别依赖平台默认;用 try-with-resources 替代答案里手写的close(),避免异常路径下资源泄漏。答案里in.close()写在正常流程末尾,一旦read抛异常就关不掉,这是教学代码和工程代码的典型差距。

5.3 JDBC 六步走:驱动、连接串与 PreparedStatement

第 9 章简答题第 2 问把 JDBC 拆成加载驱动、获取连接、获取 Statement、执行 SQL、处理结果集、关闭连接六步,编程题给了 insert/delete/update/select 的骨架。现象是:ClassNotFoundException: com.mysql.jdbc.Driver,或者 SQL 拼接导致注入。前者是驱动包没进 classpath,后者是用了 Statement 拼字符串。

import java.sql.*; public class JdbcDemo { private static final String URL = "jdbc:mysql://localhost:3306/javatest?useSSL=false&serverTimezone=UTC"; private static final String USER = "root"; private static final String PWD = "1234"; public static void main(String[] args) throws Exception { Class.forName("com.mysql.cj.jdbc.Driver"); // 新版驱动类名带 cj try (Connection conn = DriverManager.getConnection(URL, USER, PWD); PreparedStatement ps = conn.prepareStatement( "insert into tb_user(name, sex, email) values(?, ?, ?)")) { ps.setString(1, "zhangsan"); ps.setString(2, "男"); ps.setString(3, "z@example.com"); int rows = ps.executeUpdate(); System.out.println("影响行数: " + rows); } } }

参数说明:useSSL=false和serverTimezone=UTC是 MySQL 8 驱动常见的连接串参数,缺了时区可能报The server time zone value is unrecognized;驱动类名从com.mysql.jdbc.Driver变成com.mysql.cj.jdbc.Driver,答案里用的是旧名,跑不通时先查这里。用PreparedStatement而不是Statement拼串,既预编译提性能,又能防注入,这正是简答题第 3 问强调的两点。连接、语句、结果集都要在 finally 或 try-with-resources 里关闭,顺序与打开相反。

6. 把答案变成自测脚本:一套可复用的验证习惯

答案类资源最大的风险是“看懂了但没跑过”,所以我最后补一个进阶用法:把每章编程题改造成带断言的迷你测试,跑一遍就知道自己是不是真会了。不需要引入 JUnit,用main方法加if断言就够,重点是让“预期结果”显式写出来,而不是靠肉眼比对控制台输出。

public class SelfCheck { // 第 2 章:1 到 100 的奇数和,答案给的循环是 i < 100,注意边界 static int oddSum() { int sum = 0; for (int i = 1; i < 100; i++) { if (i % 2 != 0) sum += i; } return sum; } // 第 2 章:冒泡排序,验证结果有序 static int[] bubbleSort(int[] arr) { for (int i = 0; i < arr.length - 1; i++) { for (int j = 0; j < arr.length - i - 1; j++) { if (arr[j] > arr[j + 1]) { int t = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = t; } } } return arr; } public static void main(String[] args) { // 断言 1:奇数和。1~99 共 50 个奇数,和为 2500 assertEq(2500, oddSum(), "奇数和"); // 断言 2:排序结果 int[] sorted = bubbleSort(new int[]{25, 24, 12, 76, 101, 96, 28}); int[] expect = {12, 24, 25, 28, 76, 96, 101}; for (int i = 0; i < expect.length; i++) { assertEq(expect[i], sorted[i], "排序第 " + i + " 位"); } System.out.println("全部通过"); } static void assertEq(int expect, int actual, String tag) { if (expect != actual) { throw new AssertionError(tag + " 期望 " + expect + " 实际 " + actual); } } }

这段自测的价值在于暴露答案里的边界细节:第 2 章奇数和用的是i < 100,所以 99 被算进去、100 不算,结果 2500;如果改成i <= 100结果不变(100 是偶数),但改成i < 99就少加 99,差 99。冒泡排序的内层边界arr.length - i - 1少减一次就会数组越界,这是手写排序最常见的错。把这些断言跑通,比对着答案点头有用得多。

验证项对应章节预期结果常见错误
奇数和第 2 章2500循环边界写成<=或< 99
冒泡排序第 2 章升序数组内层边界少减 i 导致越界
HashSet 去重第 6 章size=1只重写 equals 不重写 hashCode
文本拷贝编码第 7 章无乱码依赖平台默认编码
JDBC 插入第 9 章影响行数=1驱动类名或时区参数缺失

从那以后我每次拿到这类课后答案,都强制自己先把编程题改写成带断言的版本再往下看,因为“能跑通”和“知道为什么能跑通”之间隔着的就是这些边界值。希望这份拆解帮到你,把答案用成规格书,而不是用成背诵材料。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进