Java 册 · 配套教学 · 按《Java 2 实用教程》第 7 版第 4-15 章

Java 面向对象与 API 教学

基础语法之后的九课:类与对象、子类与继承、接口、内部类与异常、常用实用类与事件处理、输入输出流、JDBC、多线程与网络、泛型与集合。 每课结构:概念讲解 → 可运行示例 → 要点 → 课堂练习(可展开参考答案)。

9 课 · 含练习 面向对象 IO / JDBC 多线程 / 集合

第 1 课 类与对象

对应教材第 4 章。面向对象思维的第一步。

类是"图纸",对象是根据图纸造出来的"实体"。类中定义成员变量(状态)与成员方法(行为)。 对象创建时调用构造方法初始化;对象变量保存的是引用,而不是对象本身。 Java 只有值传递:基本类型传值的副本,引用类型传引用的副本——因此方法内修改对象状态会影响原对象。

public class Student {
    private String name;          // 成员变量(封装:private 隐藏细节)
    private int age;
    private static int count = 0; // 类变量:被所有对象共享

    public Student(String name, int age) {   // 构造方法:与类同名,无返回值类型
        this.name = name;                     // this 指代当前对象
        this.age = age;
        count++;
    }
    public void introduce() {                 // 成员方法
        System.out.println("我叫" + name + ",今年" + age + "岁");
    }
    public static int getCount() { return count; }   // 类方法:只能访问类成员
}

// 使用
Student s = new Student("张三", 20);   // new 创建对象,引用赋给 s
s.introduce();
System.out.println(Student.getCount());
要点
  • 一旦自定义了构造方法,系统不再提供默认无参构造方法;需要无参构造时必须显式写出。
  • static 成员属于类,被所有对象共享;static 方法中不能用 this,也不能直接访问实例成员。
  • 方法重载只看参数列表(个数/类型/顺序),与返回值无关。
  • 访问权限:private(本类) < 默认(同包) < protected(子类) < public(全部)。
课堂练习(4 题)
  1. 为什么自定义了构造方法后 new Student() 会报错? 答:系统只在没有定义任何构造方法时才提供默认无参构造方法;自定义后需显式写出无参版本。
  2. 方法中修改传入对象的内容,调用方的对象会变吗?为什么? 答:会变。传的是引用的副本,副本与原件指向同一个对象,因此方法内对该对象的修改对调用方可见。
  3. static 方法能否直接访问成员变量? 答:不能。static 方法属于类,没有 this,只能通过对象引用访问实例成员。
  4. 写方法重载的两个 add,分别求两个 int 与三个 int 的和。 答:int add(int a, int b) 与 int add(int a, int b, int c),参数个数不同构成重载。

第 2 课 子类与继承

对应教材第 5 章。代码复用与多态的基础。

继承让子类获得父类的成员(private 与构造方法除外),并可以重写父类方法。 上转型对象(父类引用指向子类对象)配合重写,产生动态绑定——运行期才决定调用哪个版本,这就是多态。 注意:成员变量没有"重写",只有隐藏,访问哪个取决于引用的声明类型。

class Animal {
    void sound() { System.out.println("..."); }
}
class Dog extends Animal {
    @Override void sound() { System.out.println("汪汪"); }   // 重写
    void fetch() { System.out.println("叼球"); }             // 子类新增
}

Animal a = new Dog();      // 上转型对象
a.sound();                 // 输出"汪汪"(动态绑定,执行子类版本)
// a.fetch();              // 编译错误:上转型对象不能调用子类新增方法
((Dog) a).fetch();         // 向下转型后可以调用
要点
  • 重写要求方法签名相同、返回类型兼容、访问权限不能降低;加 @Override 让编译器帮忙检查。
  • 子类构造方法默认先调用父类无参构造;父类没有无参构造时必须写 super(...),且必须是第一条语句。
  • final:类不可继承、方法不可重写、变量为常量。
  • 抽象类不能实例化,用于规定子类必须实现的行为(抽象方法)。
课堂练习(4 题)
  1. 重写与重载的区别? 答:重写发生在父子类之间,方法签名相同,体现多态;重载在同一个类中,方法名相同但参数列表不同。
  2. 父类只有带参构造方法,子类构造方法应如何写? 答:必须在子类构造方法第一行显式调用 super(参数)。
  3. 上转型对象访问被隐藏的成员变量时,取的是父类还是子类的? 答:取父类的。成员变量不具有多态性,访问哪个由引用的声明类型决定;方法才具有多态性。
  4. 为什么需要抽象类? 答:用于抽取共性、规定子类必须实现的行为,使面向抽象编程成为可能(依赖抽象而非具体实现)。

第 3 课 接口与面向接口编程

对应教材第 6 章。表达"具有某种能力"。

接口只规定"能做什么",不规定"怎么做"。一个类可以实现多个接口,弥补了单继承的限制。 接口回调是最常用的模式:接口变量存放实现类的引用,调用方法时执行实现类的版本。

interface Shape {
    double area();          // 默认 public abstract
}
class Circle implements Shape {
    private double r;
    Circle(double r) { this.r = r; }
    public double area() { return Math.PI * r * r; }
}
class Rect implements Shape {
    private double w, h;
    Rect(double w, double h) { this.w = w; this.h = h; }
    public double area() { return w * h; }
}

// 接口回调:统一处理不同的实现
Shape[] arr = { new Circle(1), new Rect(2, 3) };
double sum = 0;
for (Shape s : arr) sum += s.area();      // 调用各自的实现
要点
  • 接口中的方法默认 public abstract,变量默认 public static final。
  • 实现类必须重写接口中的全部抽象方法,否则该类必须声明为 abstract。
  • 接口与抽象类:接口强调"规范/能力",多实现;抽象类强调"共性/代码复用",单继承。
课堂练习(3 题)
  1. 接口变量能否直接 new 一个接口对象? 答:不能,接口不能被实例化;但可以声明接口类型的引用,指向实现类的对象。
  2. 用接口回调改写"计算一组图形面积之和"有什么好处? 答:新增图形只需实现 Shape 接口,遍历求和的代码无需改动,符合开闭原则。
  3. 一个类可以同时继承父类并实现接口吗? 答:可以,写法为 class A extends B implements C, D,继承在前、实现在后。

第 4 课 内部类与异常处理

对应教材第 7 章。让程序更健壮。

内部类定义在另一个类内部,可直接访问外部类的成员;匿名类常用于只需要用一次的场合(如事件监听器)。 异常是程序运行时出现的不正常情况。受检异常必须捕获或声明抛出,运行时异常可不处理。 try-catch-finally 中,finally 的代码总会执行,适合释放资源。

// 匿名类:实现接口的一次性写法
button.addActionListener(new ActionListener() {
    @Override public void actionPerformed(ActionEvent e) {
        System.out.println("按钮被点击");
    }
});

// 异常处理
try {
    int x = Integer.parseInt(sc.next());
    if (x < 0) throw new Exception("年龄不能为负");   // 主动抛出
} catch (NumberFormatException e) {                  // 子类异常在前
    System.out.println("请输入数字");
} catch (Exception e) {
    System.out.println(e.getMessage());
} finally {
    sc.close();                                       // 总会执行
}

// 自定义异常
class AgeException extends Exception {
    AgeException(String msg) { super(msg); }
}
要点
  • 多个 catch 必须按子类在前、父类在后排列。
  • 受检异常(IOException、SQLException 等)编译期强制处理;RuntimeException 及其子类可不处理。
  • throw 用于抛出,throws 用于声明该方法可能抛出。
  • finally 中的代码总会执行(除非调用 System.exit)。
课堂练习(3 题)
  1. try 中有 return,finally 还会执行吗? 答:会。finally 在 return 之前执行(先把返回值暂存,执行完 finally 再返回)。
  2. catch 顺序写反(父类在前)会怎样? 答:编译报错,因为后面的子类 catch 永远不可达。
  3. 受检异常与运行时异常处理要求有何不同? 答:受检异常必须捕获或声明抛出,否则编译不通过;运行时异常可不处理,通常由代码逻辑避免。

第 5 课 常用实用类与事件处理

对应教材第 8、9 章。

标准库提供了大量可直接使用的类:String / StringBuilder 处理文本、Math / BigInteger 做运算、 Date / Calendar 处理时间、Pattern / Matcher 做正则匹配。 图形界面遵循事件委托模型:组件(事件源)产生事件对象,注册在其上的监听器被调用。

// 字符串
String s = "Java,SQL,Java";
s.split(",");                    // ["Java","SQL","Java"](参数是正则)
String.valueOf(123);             // "123"
StringBuilder sb = new StringBuilder();   // 循环拼接用它
for (int i = 0; i < 1000; i++) sb.append(i);

// 数学与正则
Math.round(3.5);                 // 4
Pattern.matches("\\d{3}", "123");// true

// GUI 事件处理(Swing)
JFrame frame = new JFrame("示例");
JButton btn = new JButton("点我");
JLabel label = new JLabel("尚未点击");
btn.addActionListener(e -> label.setText("已点击"));   // Lambda 简写监听器
frame.add(btn, BorderLayout.NORTH);
frame.add(label, BorderLayout.CENTER);
frame.setSize(300, 200);
frame.setVisible(true);
要点
  • String 不可变,循环拼接用 StringBuilder;StringBuffer 线程安全但更慢。
  • 事件处理三要素:事件源、事件对象、监听器;监听器需注册到事件源上才生效。
  • 布局管理器(BorderLayout / FlowLayout / GridLayout)决定组件位置。
课堂练习(3 题)
  1. 为什么循环里 s += "a" 效率低? 答:String 不可变,每次 += 都新建对象,产生大量垃圾;应使用 StringBuilder.append。
  2. 按钮点击后没有任何反应,可能的原因有哪些? 答:监听器未注册到按钮;组件未添加到容器;处理方法签名写错;界面未 setVisible(true)。
  3. split(".") 为什么切不出结果? 答:split 的参数是正则表达式,"." 在正则中表示任意字符,需写成 split("\\.")。

第 6 课 输入输出流

对应教材第 10 章。程序与外部数据交换。

流是数据的有序序列。字节流(InputStream / OutputStream)以字节为单位,适合图片、音频等二进制数据; 字符流(Reader / Writer)以字符为单位,处理文本并涉及编码,中文必须用它(或指定编码)。 缓冲流套在节点流之上,减少实际读写次数——这是提升 IO 性能最简单的方法。

// 读文本文件(字符流 + 缓冲)
try (BufferedReader br = new BufferedReader(new FileReader("in.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}   // try-with-resources:自动关闭资源

// 写文本文件
try (BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"))) {
    bw.write("你好,Java");
    bw.newLine();
}

// 对象序列化:类需实现 Serializable
class Person implements Serializable { String name; int age; }
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("p.dat"))) {
    oos.writeObject(new Person());
}
要点
  • try-with-resources 会自动关闭实现了 AutoCloseable 的资源,推荐优先使用。
  • 文本用字符流,二进制用字节流;中文乱码多因读写两端编码不一致。
  • 序列化要求类实现 Serializable;transient 成员不参与序列化。
课堂练习(3 题)
  1. 读取文本出现中文乱码,如何处理? 答:明确指定编码,如 new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8),并确保文件本身编码一致。
  2. 为什么要套一层缓冲流? 答:缓冲流在内存中积累数据后批量读写,显著减少磁盘 IO 次数,提高性能。
  3. try-with-resources 相比 finally 关闭资源有什么好处? 答:代码简洁,且能自动关闭多个资源(按逆序),避免遗漏与异常覆盖问题。

第 7 课 JDBC 与 MySQL

对应教材第 11 章。Java 操作数据库的标准方式。

JDBC 是 Java 访问数据库的统一接口。固定流程六步:加载驱动 → 建立连接 → 创建语句 → 执行 → 处理结果集 → 关闭资源。 查询用 executeQuery 返回 ResultSet;增删改使用 executeUpdate 返回影响行数。 一律优先使用 PreparedStatement:预编译、效率高、能防 SQL 注入。

String url = "jdbc:mysql://localhost:3306/school?useSSL=false&serverTimezone=UTC";
try (Connection conn = DriverManager.getConnection(url, "root", "pwd")) {

    // 查询(PreparedStatement + 占位符)
    String sql = "SELECT Sname, Grade FROM Student s JOIN SC c ON s.Sno = c.Sno WHERE c.Cno = ?";
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setString(1, "2");
        try (ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {                    // 必须先 next()
                System.out.println(rs.getString("Sname") + " " + rs.getInt("Grade"));
            }
        }
    }

    // 事务:关闭自动提交,出错回滚
    conn.setAutoCommit(false);
    try {
        // 多条更新...
        conn.commit();
    } catch (SQLException e) {
        conn.rollback();
    }
}
要点
  • ResultSet 遍历前必须先调用 next();列可用名称或序号取值。
  • 资源关闭顺序与创建顺序相反:ResultSet → Statement → Connection。
  • 事务处理需先 setAutoCommit(false),异常时 rollback。
  • SQL 注入的根源是拼接字符串,PreparedStatement 通过占位符参数化避免。
课堂练习(3 题)
  1. 为什么必须用 PreparedStatement 而不是拼接 SQL? 答:拼接会引入 SQL 注入风险(如输入 ' or '1'='1),PreparedStatement 把参数与语句分离、预编译执行,既安全又高效。
  2. executeQuery 与 executeUpdate 的返回值分别是什么? 答:前者返回 ResultSet 结果集;后者返回 int,表示受影响的行数。
  3. 转账操作为什么要显式管理事务? 答:扣款与加款必须作为一个整体生效,任一步失败都要回滚,否则会出现数据不一致。

第 8 课 多线程与网络编程

对应教材第 12、13 章。

线程是程序内部的执行线索,多个线程共享进程资源。启动线程必须调用 start(), 直接调用 run() 只是普通方法调用。共享资源竞争用 synchronized 解决; 线程间协作用 wait / notify(必须在同步块中使用)。 网络方面,TCP 用 Socket 建立可靠连接,UDP 用 DatagramSocket 发送数据报。

// 创建线程(推荐实现 Runnable)
class MyTask implements Runnable {
    public void run() { System.out.println("子线程工作中"); }
}
Thread t = new Thread(new MyTask());
t.start();                       // 启动线程(不能写 t.run())

// 同步:保护共享资源
class Counter {
    private int n = 0;
    synchronized void inc() { n++; }     // 同一时刻只允许一个线程执行
}

// wait / notify 协作
synchronized (lock) {
    while (!ready) lock.wait();          // 释放锁并等待
    lock.notifyAll();                    // 唤醒等待方
}

// TCP 服务端片段
try (ServerSocket server = new ServerSocket(8888)) {
    Socket s = server.accept();          // 阻塞等待连接
    BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
    System.out.println(in.readLine());
}
要点
  • start() 才会启动新线程;run() 只是普通调用,不会并发。
  • wait 会释放锁,sleep 不释放锁;wait/notify 必须在同步块中调用。
  • 线程状态:新建 → 就绪 → 运行 → 阻塞 → 死亡。
  • TCP 面向连接可靠,UDP 无连接、可能丢包但开销小。
课堂练习(3 题)
  1. t.run() 与 t.start() 有何区别? 答:run() 在当前线程中顺序执行方法体,不产生新线程;start() 由 JVM 创建新线程并在其中执行 run()。
  2. 多线程同时对一个计数器加一,结果为何可能偏小? 答:n++ 不是原子操作(读—改—写三步),线程切换会覆盖彼此的结果;需加 synchronized 或使用原子类。
  3. wait 与 sleep 的区别? 答:wait 是 Object 的方法,会释放锁,需被 notify 唤醒,用于线程协作;sleep 是 Thread 的静态方法,不释放锁,到时自动恢复。

第 9 课 泛型与集合框架

对应教材第 15 章。日常开发使用频率最高。

集合框架分两大体系:Collection(单元素)与 Map(键值对)。 List 有序可重复、Set 无序不可重复、Map 按键取值。 泛型在编译期保证类型安全(运行时类型擦除),避免强制转换与类型错误。

List<String> list = new ArrayList<>();     // 有序可重复,随机访问快
Set<String> set  = new HashSet<>();       // 无序去重
Map<String, Integer> map = new HashMap<>();// 键值映射

list.add("Java"); list.add("SQL");
for (String s : list) System.out.println(s);
Collections.sort(list);                    // 排序

map.put("张三", 90);
map.getOrDefault("李四", 0);
for (Map.Entry<String, Integer> e : map.entrySet()) {
    System.out.println(e.getKey() + ":" + e.getValue());
}

// 遍历时删除必须用迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) if (it.next().isEmpty()) it.remove();
要点
  • ArrayList 随机访问快、中间插入慢;LinkedList 插入删除快、随机访问慢。
  • HashMap 的键必须正确实现 equals 与 hashCode,且二者一致;TreeMap 会按键排序。
  • 增强 for 遍历集合时删除元素会抛 ConcurrentModificationException,应使用 Iterator.remove()。
  • 泛型不支持基本类型(用包装类),运行时类型被擦除。
课堂练习(4 题)
  1. 统计一段文本中每个单词出现的次数,用什么结构最合适? 答:Map<String, Integer>(HashMap),用 getOrDefault 累加计数。
  2. 为什么自定义对象作为 HashMap 的键要重写 hashCode 与 equals? 答:HashMap 先按 hashCode 定位桶,再用 equals 判等;只重写其中一个会导致相同逻辑对象被当成不同键(或散列错误)。
  3. foreach 遍历集合时调用 list.remove() 会怎样? 答:抛 ConcurrentModificationException;应改用 Iterator 的 remove 方法。
  4. ArrayList 与 LinkedList 如何选择? 答:频繁按下标读取、追加用 ArrayList;频繁在中间插入删除用 LinkedList。