← yifan.website
🔍
Chapter 00 · Subject Introduction

课程速览

导论这一讲以课程行政信息为主,下面只保留与备考/学习方向相关的要点。

已收录:课程速览 · 1 Java 速览 · 2 数组与字符串 · 3 类与对象 · 4 继承与多态 · 5 工具与 Bagel · 6 输入输出 · 7 接口 · 8 泛型 · 9 集合与映射 · 10 类关系与 UML · 11 异常 · 12 软件设计 · 13 设计模式 · 14 软件测试 · 15 事件驱动编程 · 16 进阶 Java
注:① 第 5、6 章在时间线上位于第 4 章之前;② 第 7 章「接口 Interfaces」在课程时间线上紧跟第 4 章「继承与多态」之后。

0学习目标 · 评分 · 工具

◆ 学习目标 Learning Outcomes

学完本课你应当能够:

  • Java 写面向对象程序(Write Object Oriented programs in Java
  • 使用 IDEIntegrated Development Environment,集成开发环境)开发软件
  • 用面向对象的方式抽象一个问题规格(Abstract a problem specification
  • 设计一个软件方案,而不只是把它写出来(Design a solution, not just write it)——评分会看你的结构、设计与背后的理由
  • 把这些技术 / 框架 / 约定迁移到其他面向对象语言

评分结构(Assessment)

项目占比备注
Project 110%实现一个游戏(用提供的游戏库),重点在面向对象设计决策,不只是实现
Project 2A8%
Project 2B12%
Mid-semester Test10%第 7 周,40 分钟,机考,范围 Week 1–6
Tutorials5%11 次取最好 8 次
In-lecture Quizzes5%11 次取最好 8 次,必须 10/10 全对才得分
Final Exam50%机考,范围 Week 1–12(讲座+tutorial+project)
● Hurdle 及格线

总分 ≥ 50%,且 project 部分 ≥ 15/30,且「期中测验 + 期末笔试」合计 ≥ 30/60,三者都满足才算通过本课。

教材与工具

  • 教材Absolute Java,作者 Walter Savitch(第 4 版或更高,非必须购买)。本批内容对应 Ch 1 & 3
  • IntelliJ:从第 2 周起使用的 IDE,Project 必须用 IDE。
  • GitHub(学校提供):用于 Project 版本管理。
  • ED Lessons:配套练习材料,自定进度,不计分。
Chapter 01 · A Quick Tour of Java

Java 速览

第 1 周快速过一遍 Java 语言基础。课程假设你已有至少一门语言(Python / C)的经验,所以这里是"对照式"的语法巡览,重点放在容易踩坑的地方。

1.1Java 是什么 / 历史

  • Sun Microsystems 开发,项目 1991 年启动,团队由 James Gosling 领导。
  • 最初目标是为"下一波计算浪潮"——数字消费电子设备(嵌入式系统)——设计一门语言,当时取名 Oak(橡树)。
  • 1993 年消费电子方向遇冷,团队转向新的"下一波"——互联网,做了一种嵌入网页浏览器的语言,叫 applet(小应用)。
⚠ 易错点

JavaScript 和 Java 没有任何关系。名字相似只是历史巧合,二者是完全不同的语言。

1.2三大特性

课件列出 Java 的三个关键特性:编译且解释(Compiled and Interpreted)平台无关且可移植(Platform-Independent & Portable)面向对象(Object Oriented)

① 既编译又解释

先看纯编译型语言(以 C 为例)的流程,再对比 Java:

编译型语言(C)Java
源码.c 文件.java 文件
编译器gcc → 机器码javac → 字节码
产物可执行文件 a.out字节码 .class
运行直接由 CPU 执行由解释器(虚拟机)java 执行
◆ 核心概念:字节码 Bytecode

Java 编译器把 .java 源码编译成 字节码.class 文件)。字节码是一种中间形式(intermediate form),比源码更接近机器表示,但还不是某台具体机器的机器码。

任何目标平台上的解释器(虚拟机 / interpreter)负责把字节码翻译成该平台的机器指令来执行。

② 平台无关、可移植

平台无关示意图
图 1.2 · 一次编译,到处运行。JAVA COMPILER(编译器,翻译器)把源码翻译成 JAVA BYTE CODE(字节码,对所有平台都相同);每个平台各有一个 JAVA INTERPRETER(解释器),负责在 Windows / Mac / Ubuntu / Android 上运行同一份字节码。
★ 记住

字节码对所有平台相同;要把 Java 系统移植到新平台,只需为该平台写一个解释器,解释器会负责找出等价的、依赖具体机器的代码来运行。这就是"一次编译,到处运行"。

③ 面向对象

Java 是面向对象编程(Object Oriented Programming, OOP)语言。常见的程序构件有:类(Classes)对象(Objects)方法(Methods) 等——这些会在后续章节深入。

1.3Application vs Applet

Java 程序有两种类型:

Application(应用程序)Applet(小程序)
形态独立(stand-alone)程序嵌入网页的程序
main 方法 main没有 main
运行方式命令行用 Java 解释器调用由支持 Java 的浏览器运行
● 范围说明

本课只学 Application。Applet 是较过时的技术,如今几乎不再使用。

1.4Hello World 逐行拆解

HelloWorld.java
// HelloWorld.java: Display "Hello World!" on the screen

import java.lang.*;

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

注释(Comments)

Java 支持 3 种注释:

  • /* */:通常用于多行注释(与 C 相同)
  • //:单行注释
  • /** */文档注释(documentation comments),后续会学

import java.lang.*;

  • 作用类似 C 的 #include,用来导入额外的类(类似 C 的库)。
  • Java 中类被分组到包(package)里。不同人写的包可能有同名的类和方法,靠包名区分(如 ibm.mathlib.*microsoft.mathlib.*)。
⚠ 易错点

Java 默认就会导入 java.lang.*,所以这一行其实是可省略的

public class HelloWorld { }

  • 类定义——Java 中所有东西都定义在类里
⚠ 易错点:类名必须等于文件名

类的名字必须和 Java 文件名一致HelloWorld 类必须保存在 HelloWorld.java 文件中。

public static void main(String args[])

  • main 方法的定义,和 C 很像。
  • 一个独立 Java 程序必须有一个 main 方法
  • 一个类只能有一个 main() 方法
  • String args[]:定义命令行参数,类似 C。
  • publicstatic 关键字后续详解。

System.out.println("Hello World!");

  • 作用类似 C 的 printf,但 println 在 Java 里叫方法(method),而 C 的 printf函数(function)
  • System.outoutSystem 类里的一个对象System 类定义在 java.lang 包中。

1.5编译运行 + 命令行参数

编译与运行

  1. 用文本编辑器写程序,存为 HelloWorld.java
  2. 确认机器装好了 Java 构建与运行环境:javac -versionjava -version
  3. 编译:javac HelloWorld.java —— 成功后生成 HelloWorld.class
  4. 运行:java HelloWorld —— 输出 Hello World!
★ 记住命令搭配

javac 文件名.java(编译,带 c,吃 .java)→ 生成 .classjava 类名(运行,不带扩展名)。

命令行参数(Command Line Arguments)

运行 java HelloWorld Australia England France 时,main(String args[]) 里的 args[] 会装上这些参数:

args[0] -> Australia
args[1] -> England
args[2] -> France

args.length 得到参数个数。课件示例输出:Number of arguments = 3

1.6Java vs C 的区别

◆ 对照记忆
  • Java 是面向对象语言;C 是过程式(procedural)语言。
  • Java 没有 gotosizeoftypedef
  • Java 没有结构体 struct 和联合体 union
  • Java 没有显式的指针类型
  • Java 没有预处理器:没有 #define#include#ifdef
  • Java 安全且定义良好:内存由虚拟机管理,而不是由程序员管理。

1.7标识符 Identifiers

◆ 定义

标识符(Identifier):唯一标识某个程序元素(类、对象、变量、方法)的名字。

规则(必须遵守)

  • 不能以数字开头。
  • 所有字符必须是字母、数字或下划线 _
  • 理论上可以任意长。
  • 大小写敏感RaterateRATE 是三个不同的变量。

约定(惯例,不是强制)

  • 变量 / 方法 / 对象:小写字母开头,用大写字母标记"单词"边界(小驼峰),如 topSpeedbankRatetimeOfArrival
  • :大写字母开头(大驼峰),如 PrintDemoHelloWorld
⚠ 关键字 vs 预定义标识符

关键字 / 保留字(如 public class void static绝对不能用作标识符。

预定义标识符(如 System String println可以被重新定义,但这样会改变它们的标准含义,既危险又容易造成混淆。

1.8数据类型 Data Types

Java 数据类型分类树
图 1.8a · 数据类型分类。原始类型 Primitive(Intrinsic,内在的)分数值(Numeric:Integer 整数、Floating-point 浮点)与非数值(Non-numeric:Characters 字符、Boolean 布尔);非原始类型 Non-primitive(Derived,派生的)包括 Class、Interface、String、Array、Object。
整数与浮点类型的字节数
图 1.8b · 各数值类型占用字节数。整数:byte 1、short 2、int 4、long 8 字节;浮点:float 4、double 8 字节。
◆ 浮点与布尔
  • float单精度double双精度;双精度在表示小数时精度更高。
  • 浮点数默认按 double 处理
  • boolean 只能取 truefalse,如 boolean x = true;
⚠ 易错点:float 要加 f / F

想强制单精度,必须给数字加后缀 fFfloat a = 2.3F;。而 double b = 6.7; 不用后缀(默认就是 double)。

1.9变量 Variables + 类型转换

◆ 定义

变量(Variable):存在程序内存中、可被改变的信息;一个变量有一个内存位置和一个标识符。变量必须先声明并初始化再使用

声明语法 & 赋值
<type> <variable name> = <initial value>;

int count = 1;
float length = 2.3F;
double height = 6.7;
boolean status = true;

int countX = 1, countY = 2;
countX = countY + countX;
countX += 3;   // 等价于 countX = countX + 3;

类型转换规则(考试高频陷阱)

★ 拓宽方向(可自动转换)

下面这条链上,右边的类型可以接收左边的值(自动拓宽):

byte → short → int → long → float → double

char → int

⚠ 窄化必须显式强制转换 + 会截断
  • 把值赋给链上更左的类型(如 double→int)需要显式强制类型转换(cast)
  • int x = 2.99; ❌ 非法。
  • int y = (int)2.99; ✅ 合法,但 y2 不是 3——是截断,不是四舍五入!
  • int intVariable = 2.99; ❌;double doubleVariable = 2; ✅(int→double 自动拓宽)。
  • intboolean 互相不能赋值

变量的三种分类

  • 实例变量(instance variables)
  • 静态 / 类变量(static / class variables)
  • 局部变量(local variables):定义在方法内部的变量
● 提示

实例变量和静态变量会在后续"类与对象"章节展开。本讲只要记住三类的名字与"局部变量在方法内"即可。

1.10常量 Constants

◆ 定义

常量(Constant):程序执行期间不会改变的值,也叫"只读(READ only)"值。用关键字 final 声明。

final int MAX_LENGTH = 420;
final double PI = 3.1428;
final char CHAR_CONSTANT = 'Z';
final boolean BOOL_CONSTANT = true;
final String STRING_CONSTANT = "Welcome to Java";
  • 约定用全大写字母命名常量。
  • 定义常量时必须显式写出数据类型(这点 C 不要求)。

1.11运算符 Operators

Java 运算符可分为:算术、关系、逻辑、赋值、自增自减、条件、位运算、特殊运算符等。

算术运算符

运算符含义
+加 / 一元正号
-减 / 一元负号
*
/
%取模(modulo)
● 混合模式算术 Mixed-mode

当一个操作数是实数、另一个是整数时,叫混合模式算术表达式:只要有一个操作数是实数类型,另一个也会被转成实数,然后做实数运算。

关系运算符

< <= > >= ==(等于) !=(不等于)。

★ 记住

关系运算符的结果类型是 boolean

逻辑运算符 + 真值表

&& 逻辑与、|| 逻辑或、! 逻辑非。

op1op2op1 && op2op1 || op2!op1
truetruetruetruefalse
truefalsefalsetruefalse
falsetruefalsetruetrue
falsefalsefalsefalsetrue

位运算符 Bitwise

& 按位与、| 按位或、^ 按位异或、~ 取反(one's complement)、<< 左移、>> 右移、>>> 无符号右移(补零)。

自增自减 + 条件(三元)运算符

  • 自增自减:++--
  • 条件运算符:exp1 ? exp2 : exp3。例如 x = (a>b) ? a : b; 等价于"若 a>b 则 x=a 否则 x=b"。

数学函数 Math 类

Java 用 Math 类(定义在 java.lang 包)提供 cossinlogsqrt 等函数,用法 Math.方法名()

double y = Math.sqrt(x);
double z = Math.cos(y);

1.12流程控制 Flow of Control

◆ 定义

流程控制(Flow of Control):Java 中的分支(branching)循环(looping)机制。大多数分支和循环由 boolean 表达式控制——它只会求值为 truefalse

分支 Branching

四种:if-else、多路 if-else(multi-way)、switch、两路决策表达式(即三元 ?:)。

switch 结构
switch (Controlling_Expression) {
    case Case_Label_1:
        Statement_Sequence_1
        break;
    case Case_Label_2:
        Statement_Sequence_2
        break;
    default:
        Default_Statement_Sequence
        break;
}

循环 Loops

  • 循环体(body):被重复执行的代码;每执行一次叫一次迭代(iteration);循环可嵌套。
  • 三种:whiledo-whilefor
⚠ while vs do-while

while 先判断条件再执行;do-while 把条件放在末尾} while(...)),所以循环体至少执行一次

for 语句的三段
for (Initialize_Expressions; Terminate_Expression; Update_Expressions) {
    ...
}
  • Initialize:第一次迭代前,初始化/声明控制变量。
  • Terminate:每次迭代求值的 boolean 表达式,决定何时结束。
  • Update:每次循环体执行更新控制变量。

break 与 continue

★ 区别
  • break退出整个循环(while / do / for)。可配合标签(label)使用,如 break loop1; 直接跳出指定的外层循环。
  • continue跳过循环体内剩下的语句,直接进入下一次迭代。
Chapter 02 · Arrays and Strings

数组与字符串

这一讲两个主题:数组(Array)与字符串(String)。重点不在语法本身(和 Python/C 类似),而在几个反直觉的坑:数组是引用、字符串不可变、==.equals 的区别。这些是考试与 project 的高频出错点。

2.1数组基础:声明 / 初始化 / 默认值

◆ 定义

数组(Array):在内存中按顺序排列的、相同类型的一串元素(a sequence of elements of the same type arranged in order in memory)。

动机:存 1 个整数用 int x;,存 n 个就需要数组 int[] intArray;

声明(两种写法)

basetype[] varName;   // 写法一(推荐)
basetype   varName[]; // 写法二

int[] intArray;       // 声明了数组,但还没有任何元素
⚠ 易错点:声明 ≠ 初始化

光声明 int[] intArray; 后直接 int x = intArray[0];编译报错
variable intArray might not have been initialized

数组和普通变量一样,必须先初始化才能用

初始化的三种方式

写法元素个数各元素的值
int[] a = {0,1,2,3,4};5就是花括号里写的字面值
int[] a = new int[100];100全部为默认值 0
int[] a2 = a1;与 a1 相同与 a1 共享同一份数据(见 2.2)

2.2数组是引用(别名陷阱)

数组引用框图
图 2.2 · int[] array = {1,2,3,4,5}; 中,变量 array 并不"装着"这五个数,而是一个引用(reference),指向内存里那一排格子。
◆ 核心
  • 数组是一种数据类型,类似你自己定义类得到的类型。
  • 数组是引用(references)!
  • 操作一个引用,会影响所有指向同一数组的引用
自测:下面输出什么?
int[] intArray_1 = {10, 20, 30, 40};
int[] intArray_2 = intArray_1;        // 不是复制!两个名字指向同一数组
System.out.println(intArray_2[0]);    // 10
intArray_1[0] = 15;                   // 通过 _1 改,_2 也变
System.out.println(intArray_2[0]);    // 15
⚠ 别名(aliasing)

intArray_2 = intArray_1; 只是让两个变量指向同一块数据不是拷贝。所以通过任意一个名字修改,另一个看到的也变了。输出是 10 然后 15

2.3多维数组

  • Java 允许"多维数组",技术上是"数组的数组"(array of arrays)
  • 声明方式和一维类似。
int[][] nums = new int[10][10];  // 方形数组(square)
int[][] nums = new int[10][];    // 不规则数组(irregular / jagged)

2D 数组初始化略复杂——不规则数组要逐行 new:

for (int i = 0; i < nums.length; i++) {
    nums[i] = new int[<length_of_subarray>];
}
三角形(jagged)数组示例
int HEIGHT = 5;
int[][] triangleArray = new int[HEIGHT][];     // 每行长度不同
for (int i = 0; i < HEIGHT; i++) {
    triangleArray[i] = new int[HEIGHT - i];    // 第 i 行有 HEIGHT-i 个元素
    for (int j = 0; j < HEIGHT - i; j++) {
        triangleArray[i][j] = i + j + 1;
    }
}
// 得到 {1,2,3,4,5},{2,3,4,5},{3,4,5},{4,5},{5}

2.4对象数组

数组也能存对象,但要分两步:

Circle[] circleArray;              // ① 声明数组
circleArray = new Circle[25];      // ② 分配存储
⚠ 易错点:数组建好 ≠ 对象建好

new Circle[25] 只创建了一个"能存 25 个 Circle 引用"的数组——Circle 对象本身还没被创建。你必须自己 new Circle(...) 再放进去:

Circle[] circleArray = new Circle[3];
for (int i = 0; i < circleArray.length; i++) {
    circleArray[i] = new Circle(i, i, i + 2);   // 逐个创建并存入
}

2.5数组常用方法

操作写法说明
索引intArray[0][10][-1] 会抛 out of bounds 越界错误
长度intArray.length属性,没有括号(对比 String 的 length() 有括号)
相等Arrays.equals(n1, n2)import java.util.Arrays;;元素值都相同才 true
调整大小arr = new int[arr.length+3]数组定长,"扩容"只能新建数组
排序Arrays.sort(n1)升序
打印Arrays.toString(n1)输出形如 [1, 2, 3]
⚠ 数组 length 无括号

数组用 arr.length(属性),字符串用 str.length()(方法)。两者很容易写混。

2.6For-Each 循环

for (<type> varName : <iterable object>) {
    <block of code>
}

for (Circle c : circleArray) {
    System.out.println(c.getRadius());
}
★ 何时用
  • 更方便的遍历方式,不需要索引
  • 当你关心的是数据本身、而不是数组下标时最适合。

2.7字符串基础 + 转义

◆ String 到底是什么?

String 是一个 Java 类(class),由一串字符组成(a Java class made up of a sequence of characters)。

课件特别提醒:说它是"对象"在技术上不够准确——准确说法是。它用来表示消息、错误、名字等"字符相关"的属性,对输入输出非常有用。

String s1 = "This is a String";
String s2 = "This is " + "also a String";
String s3 = "10";   // 即使内容是数字,它仍是字符串
● 与 Python 对比

Java 字符串和 Python 几乎一样,但不能用单引号(单引号在 Java 里是 char 字符)。

转义 Escaping

有些字符(如 ")是保留的,对 Java 有特殊含义;要按字面使用它们,需要用 \ 转义。

⚠ 引号要转义

直接写 System.out.println("... was "good".");Error(编译器以为字符串在 "good" 前就结束了)。正确:

System.out.println("Game of Thrones season 8 was \"good\".");

常见转义:\n 换行、\t 制表符、\" 引号。

2.8字符串拼接陷阱

  • ++= 拼接字符串。
  • + 很"聪明":只要有一个操作数是字符串,就会把另一个也转成字符串。
⚠ 经典陷阱:从左到右求值

表达式从左往右算,所以先发生的是字符串拼接:

System.out.println("1 + 1 = " + 1 + 1);
// 输出 "1 + 1 = 11"  ← 先 "1 + 1 = "+1 拼成字符串, 再 +1 继续拼

System.out.println("1 + 1 = " + (1 + 1));
// 输出 "1 + 1 = 2"   ← 括号让 1+1 先做整数加法

想让数字先算,用括号把算式括起来。

2.9字符串常用方法

功能PythonJava
长度len("Hello")"Hello".length()
大写s.upper()s.toUpperCase()
分割s.split()s.split(" ")
是否包含子串"Hell" in ss.contains("Hell")
查找子串位置s.find("Hell")s.indexOf("Hell")
取子串s[2:7]s.substring(2, 7)

2.10不可变性 Immutability

◆ 核心
  • 字符串是不可变的(immutable):一旦创建就不能被修改,只能被替换
  • 这意味着每个 String 操作都返回一个新的 String,而不会改动原来的。
⚠ 不接收返回值 = 白做

下面这段,每个方法都返回了新字符串,但没人接住,所以 s 根本没变:

String s = "Hello World";
s.toUpperCase();      // 返回新串, 但被丢弃
s.replace("e", "i");  // 同上
s.substring(0, 2);    // 同上
s += " FIVE";         // 这一行才真的改了 s(替换为新串)
System.out.println(s);
// 输出 "Hello World FIVE"  ← 前三行全部无效

正确写法是把返回值赋回去

String s = "Hello World";
s = s.toUpperCase();   // "HELLO WORLD"
s = s.replace("e","i");// 此时无 e, 不变
s = s.substring(0, 2); // "HE"
s += " FIVE";
System.out.println(s);
// 输出 "HE FIVE"

2.11== vs .equals + 字符串常量池

这是 Java 最容易出错的点之一。先看课件的几个自测,全部围绕"== 比的到底是什么"。

代码结果原因
"Hello" == "Hello"true字面量都在字符串常量池(String Constant Pool),是同一个
String s="Hello";
s == "Hello"
true同样来自常量池
String s="Hello";
String s2="Hello";
s == s2
true两个字面量指向常量池里同一个 "Hello"
String s="Hello";
String s2=new String("Hello");
s == s2
falsenew String(...)堆内存(heap)里造了一个新对象,引用不同
◆ 两块内存
  • 字符串常量池:字符串字面量(如 "Hello")放这里,相同内容共享同一个。
  • 堆内存 heapnew String("Hello") 每次都新建一个对象。
⚠ == 比引用,不比内容

== 比较的是两个引用是不是指向同一个对象不是比较内容。所以它对字符串字面量"碰巧"为 true,遇到 new String 就翻车。

★ 正确做法:用 .equals

要判断两个对象内容是否相等,必须用 .equals 方法(这是每个类都应该有的标准方法):

String s  = "Hello";
String s2 = new String("Hello");
System.out.println(s == s2);        // false(引用不同)
System.out.println(s.equals(s2));   // true (内容相同)

.equals 定义:用于检查两个对象是否相等的方法。比较内容用 .equals,不要用 ==

Chapter 03 · Classes and Objects

类与对象

全课最核心的一章(原为三讲)。围绕一个贯穿示例——画图板里的 Circle 类——讲清楚类怎么定义、对象怎么用、以及 getter/setter、构造函数、static、可见性、委托、包装类这一整套面向对象基础。

3.1类 vs 对象 + 四大编程概念 + 抽象

◆ 所有语言都支持的四个基本概念
  • 计算 Calculation:常量、变量、运算符、表达式
  • 选择 Selection:if-else、switch、?:
  • 迭代 Iteration:while、do、for
  • 抽象 Abstraction:创建自包含的软件单元,使解决方案可被参数化、因而更通用

抽象正是区分过程式语言(如 C)与面向对象语言(如 Java、C++)的根本概念。

  • 过程式语言的抽象靠函数 / 过程:函数对外部数据执行操作。
  • OO 语言的抽象靠抽象数据类型(ADT, Abstract Data Type):它把数据 + 操作数据的函数装在一起。
  • 在 Java 中,类(Class)就是抽象数据类型的一种实现。
◆ 关键定义
  • 类 Class:OOP 中抽象的基本单元,代表问题中的一个"实体"。它是真实/问题世界实体的"泛化",是有共同属性的事物的模板,包含属性方法,并定义一个新数据类型
  • 对象 Object:类的一个具体、实在的例子。
  • 实例 Instance:存在于你代码中的一个对象。

"X is of type A"、"X is an object of class A"、"X is an instance of class A" 三种说法是等价的

贯穿示例(画图板 Drawing Pad):以 Circle 类为例——属性有 Centre(中心)、Radius(半径)、Fill Colour/Type;方法有 computeCircumference(算周长)、computeArea(算面积)、move、resize。

3.2OO 六大特性总览

面向对象设计范式的关键特性:数据抽象、封装、信息隐藏、委托、继承、多态。本章覆盖前四个,继承与多态在后续章节。

◆ 数据抽象 Data Abstraction

通过定义新类来创建适合应用的新数据类型的技术。类是一种特殊的、程序员自定义的数据类型,类似 C 的 struct,但多了属性和方法。类的定义决定了对象能含哪些数据(属性)以及能执行哪些动作(方法)。

◆ 封装 Encapsulation

通过定义类,把数据(属性)操作这些数据的方法组合到单一实体的能力。这种封装是 OO 独有的,过程式范式不提供。

3.3定义类:属性 + 方法

类的语法 / 最简类
<visibility modifier> class <ClassName> {
    <attribute declarations>
    <method declarations>
}

public class Circle { }   // 一个"裸"类

加属性(attributes,又叫 data / fields)

public class Circle {
    public double centreX;   // 中心 x 坐标
    public double centreY;   // 中心 y 坐标
    public double radius;    // 半径
}
◆ 实例变量 Instance Variable

上面这些属性叫实例变量:每个类的实例(对象)各自独有的属性。给 centreX/centreY/radius 赋不同的值,就定义出大小和位置不同的 Circle 对象——它们维护对象的状态

加方法

<visibility modifier> <void 或 返回类型> myMethod(paramList) {
    变量声明
    语句
}
  • 方法有返回值就要在定义里写明返回类型,并且方法体里要有 return 语句;否则写 void
  • 方法内部声明的变量叫局部变量(local variables)——在方法内部,区别于在方法外部声明的实例变量。
public double computeCircumference() {
    return 2 * Math.PI * radius;
}
public double computeArea() {
    return Math.PI * radius * radius;
}
public void resize(double factor) {
    radius = radius * factor;
}

3.4引用 vs 对象 / null / new / 垃圾回收

编译 Circle.java 后,Circle 成为一个派生数据类型(Derived Data Type),区别于 intfloat 这种原始数据类型(Primitive)

⚠ 声明 ≠ 创建对象
Circle aCircle;   // 没有创建对象!
Circle bCircle;

这两行只是声明了两个引用(reference),它们目前指向 nothing,即 null 引用

◆ null / new
  • null:Java 关键字,意思是"这里没有对象"。null 对象不能被访问来取变量或方法,也不能以任何方式使用。
  • new:指示 JVM 为对象分配内存,即实例化(instantiate)它。对象在被实例化之前都是 null。
Circle aCircle, bCircle;
aCircle = new Circle();   // aCircle 现在指向一个对象
bCircle = new Circle();   // bCircle 现在指向另一个对象
bCircle = aCircle;        // 赋类引用:两个名字指向同一对象
★ 垃圾回收 Garbage Collection

上面 bCircle = aCircle; 之后,bCircle 原来指向的对象就没有有效引用了,无法再被使用——它成为 Java 自动垃圾回收的候选。Java 会定期自动回收、释放无用对象的内存供未来使用,你不需要在程序里显式处理

3.5访问成员(点语法)

<objectName>.<variableName>;
<objectName>.<methodName>(<arguments>);

语法和 C 访问 struct 里的数据类似。

Circle aCircle = new Circle();
aCircle.centreX = 2.0;
aCircle.radius  = 1.0;
double area = aCircle.computeArea();  // 调用方法 = 向对象"发消息"
aCircle.resize(2.0);
● 关于 main

Java 程序就是一个有 main 方法的类。运行程序时,运行时系统会调用 main。main 是 void 方法,签名 public static void main(String[] args)(其中 static 见 3.9)。

3.6Getter / Setter

  • 初始化 / 更新 / 访问实例变量,通常用专门的方法来做。
  • 这些方法叫访问器 / 修改器(Accessor / Mutator),俗称 Getter / Setter
  • Setter(mutator)aCircle.setX(10.0); —— 初始化/更新实例变量
  • Getter(accessor)double x = aCircle.getX(); —— 访问实例变量
  • IntelliJ、Eclipse 等 IDE 支持自动生成 getter/setter。
public double getRadius() {
    return radius;
}
public void setRadius(double radius) {
    this.radius = radius;   // this 见 3.8
}
● 为什么要这么做

用 getter/setter 的真正理由要到信息隐藏 / 可见性控制(3.12)才看得清楚——先记住写法,耐心往下。

3.7构造函数 Constructor

Circle aCircle = new Circle(); 右边其实是在调用类的构造函数

◆ 构造函数 Constructor

定义:用于创建并初始化对象的方法。它的特点:

  • 是一种方法,用来初始化对象
  • 名字必须和类同名
  • 不能返回值
  • 一个类可以有一个或多个构造函数,每个参数集不同(这就是重载,见 3.8)。
public <ClassName>(<arguments>) {
    <block of code>
}

public Circle() {              // 无参构造,设默认值
    centreX = 10.0;
    centreY = 10.0;
    radius  = 5.0;
}
public Circle(double newX, double newY, double newR) {  // 带参构造
    centreX = newX;  centreY = newY;  radius = newR;
}
● 默认值

对象创建时,实例变量会先被设为基于数据类型的默认值(如 double 默认 0.0)。如果有无参构造,就会用构造里写的值覆盖。

3.8方法重载 Method Overloading + this

◆ 方法重载 Method Overloading

方法同名,靠签名(signature)区分:参数的个数 / 类型 / 位置。任何方法都能重载(构造函数或普通方法)。

重载是多态(polymorphism)的一种形式——同一个方法名、不同行为。

多态定义:根据对象的数据类型/类,不同地处理对象的能力。

⚠ 别和"方法重写 Overriding"混

重载 Overloading(同名不同签名,本节)≠ 重写 Overriding(继承里子类覆盖父类方法,后续章节)。考试常考区别。

构造函数陷阱 → this 关键字

⚠ 参数名和实例变量同名
public Circle(double centreX, double centreY, double radius) {
    centreX = centreX;   // ❌ 把参数赋给自己, 实例变量没被改
    centreY = centreY;
    radius  = radius;
}

当参数名和实例变量同名,centreX = centreX; 操作的都是那个参数,实例变量根本没被赋值。

★ this 关键字

this:指向调用对象(拥有 / 正在执行该方法的那个对象)的引用。用 this. 明确指代实例变量,解决同名问题:

public Circle(double centreX, double centreY, double radius) {
    this.centreX = centreX;   // this.centreX 是实例变量, centreX 是参数
    this.centreY = centreY;
    this.radius  = radius;
}

3.9static 静态成员 + 实例 vs 静态

问题:怎么统计一共创建了多少个 Circle 对象?答案是用静态变量

public class Circle {
    public static int numCircles = 0;   // 静态(类)变量, 整个类只有一份
    public double centreX, centreY, radius;

    public Circle(double x, double y, double r) {
        centreX = x; centreY = y; radius = r;
        numCircles++;                    // 每创建一个对象就 +1
    }
    public static void printNumCircles() {       // 静态方法
        System.out.println("Number of circles = " + numCircles);
    }
}

用类名(不是对象名)访问:Circle.numCirclesCircle.printNumCircles();

实例变量与静态变量内存图
图 3.9 · 实例变量(centreX/centreY/radius)每个对象一份——circleA 和 circleB 各有自己的一套;静态变量 numCircles 整个类只有一份,被所有对象共享。
⚠ 静态方法的限制(高频考点)
  • 静态方法只能调用其他静态方法
  • 静态方法只能访问静态数据
  • 静态方法不能用 thissuper——因为它们关联的是对象(类的实例),而静态方法不依赖具体对象。
  • 不要把所有方法和属性都设成 static!否则你只是在用 Java 写过程式程序,作业和考试会因此扣分。设 static 前先想清楚:这是类级成员还是实例级成员。
◆ 定义
  • 静态成员 Static Members:不特定于类的任何对象的方法和属性。
  • 静态变量 Static Variable:被该类所有对象共享的变量,只有单一一份,用类名访问。
  • 静态方法 Static Method:不依赖(不访问/修改)任何实例变量的方法,用类名调用。
  • 提示:main 就是一个静态方法。

3.10标准方法 equals / toString / 拷贝构造

有些常用方法几乎每个类都该有,叫标准方法(Standard Methods)。看三个:

① equals

public boolean equals(<ClassName> var) {
    return <boolean expression>;
}
★ 为什么需要 equals(呼应第 2 章)
  • == 做相等判断只会检查引用是否相等,不会检查对象内容是否相等。
  • "两个对象怎样才算相等"由你决定——用对象的一个或多个属性来判断。
public boolean equals(Circle circle) {
    return Double.compare(circle.centreX, centreX) == 0 &&
           Double.compare(circle.centreY, centreY) == 0 &&
           Double.compare(circle.radius,  radius)  == 0;
}

② toString

  • toString 返回对象的一个 String 表示
  • 当对象被要求"表现得像字符串"时自动被调用,例如 System.out.println(c_1);(c_1 是 Circle 引用)。
public String toString() {
    return "I am a Circle with {centreX=" + centreX +
           ", centreY=" + centreY + ", radius=" + radius + '}';
}
// System.out.println(aCircle);
// 输出: I am a Circle with {centreX=5.0, centreY=5.0, radius=40.0}

③ 拷贝构造函数 Copy Constructor

public <ClassName>(<ClassName> var) {
    <block of code>
}
  • 是一个参数为同类型的单参数构造函数
  • 为传入的对象创建一个独立的副本:是一个独立对象,但实例变量被设成和参数对象完全一致。
◆ 深拷贝 Deep Copy

如果某些实例变量是对其他对象的引用,必须用那个对象自己的拷贝构造函数新建一个状态相同的对象——这叫深拷贝(而不是只复制引用)。

⚠ 引用复制 vs 拷贝构造
Circle c1 = new Circle(10.0, 10.0, 5.0);
Circle c2 = c1;             // 引用复制:c2 和 c1 是同一个对象
Circle c3 = new Circle(c1); // 拷贝构造:c3 是新对象, 状态同 c1

c2 = c1 改 c2 会影响 c1;c3 = new Circle(c1) 改 c3 不影响 c1。

3.11包 Packages

◆ 定义

包 Package:把类和接口(后续介绍)分组成一个个 bundle,用约定的命名方式统一处理。

为什么分包

  • 类似 C 的库:可以开发、打包、被其他 Java 程序导入使用。
  • 支持复用:导入现有类,而不是重写。
  • 防命名冲突:同名的类可以共存,靠所属包名唯一区分。
  • 支持访问控制(见信息隐藏)。
  • 另一层封装

创建包 / 使用包

// 类文件的第一条语句必须是 package 语句
package utilities.shapes;     // shapes 是 utilities 的子目录
public class Circle { ... }
// Circle.class 必须放在 utilities/shapes 目录下

// 使用包
import utilities.shapes.*;          // 导入包里所有类
import utilities.shapes.Circle;     // 导入特定类
● CLASSPATH 与默认包
  • 类所在的父目录必须在 CLASSPATH 环境变量中(类似 PATH)。
  • 当前目录下没写 package 语句的类,属于一个无名的默认包(default package);只要当前目录 . 在 CLASSPATH 中,这些类就自动可用。

3.12信息隐藏 + 可见性修饰符

◆ 定义
  • 信息隐藏 Information Hiding:对外界"隐藏"类的细节的能力,也叫可见性控制(Visibility Control)
  • 访问控制 Access Control:阻止外部类以不期望的方式操作另一个类的属性。

对象的动作通过类的方法执行——这些方法就是类的接口(interface);有些属性和方法可以对用户隐藏。

三个可见性修饰符

  • public:用于类/方法/属性时,处处可见(类内、类外都可见)。
  • private:用于方法/属性时,只在该类内可见。private 成员在子类中不可见,也不被继承
  • protected:用于方法/属性时,在该类、子类、以及同包的所有类中可见;其他包里的子类也能看到。
★ 可见性对照表(务必记牢)
修饰符同类 Class同包 Package子类 Subclass外部 Outside
public
protected
default(不写)
private
◆ 标准做法 + 好处
  • 属性设为 private,通过 public 的 getter/setter 访问;其他类不需要调用的方法设为 private
  • 好处:把数据安全封进类的"胶囊"里;防止程序员依赖实现细节;防意外/错误使用;代码更优雅干净、易维护;通过干净的接口提供访问。

3.13可变 vs 不可变类

◆ 定义
  • 可变类 Mutable:含有 public 修改器、或其他能改变实例变量的 public 方法的类;其对象是可变对象。
  • 不可变类 Immutable:除构造函数外,不含任何会改变实例变量的方法的类;其对象是不可变对象。
public class ImmutableCircle {
    private final double centreX, centreY, radius;  // final + 无 setter
    public ImmutableCircle(double x, double y, double r) { ... }
    public double getCentreX() { ... }   // 只有 getter, 没有 setter
    // ...
}

做法:实例变量设为 private final,且不提供 setter

3.14关联与委托 Delegation

◆ 委托 Delegation
  • 一个类可以把它的职责委托给其他类。
  • 一个对象可以通过包含关系(containership)调用另一个对象里的方法。
  • 这是类之间的关联(Association)关系(后续详讲)。
例:Circle 包含一个 Point 对象,把取坐标委托给它
public class Point {
    private double xCoord, yCoord;
    public double getXCoord() { return xCoord; }
    public double getYCoord() { return yCoord; }
}

public class Circle {
    private Point centre;     // Circle 里包含一个 Point 对象
    private double radius;
    public Circle(Point centre, double radius) {
        this.centre = centre;
        this.radius = radius;
    }
    public double getX() {
        return centre.getXCoord();  // 委托给 centre 这个 Point 对象
    }
    public double getY() {
        return centre.getYCoord();
    }
}

Point 对象被包含在 Circle 对象里;Circle 的方法通过引用 centre 调用 Point 的方法。

3.15包装类 + 装箱拆箱 + parse

◆ 原始类型的局限

intdouble 这类原始类型(Primitive):只含数据,没有属性和方法,不能"执行动作"(比如 parse 解析)。

Primitive 定义:只含数据、无属性无方法的信息单元。

◆ 包装类 Wrapper Class
  • Java 为原始类型提供"包装"类。
  • 让原始类型能被"打包"/"装箱(boxed)"成对象。
  • 让原始类型"假装"是类(这一点在后面很重要)。
  • 为原始类型提供额外功能。

Wrapper 定义:给 int 等原始类型额外功能、让它们能像对象一样行动的类。

PrimitiveWrapper ClassPrimitiveWrapper Class
booleanBooleanintInteger
byteBytefloatFloat
charCharacterdoubleDouble
shortShortlongLong
⚠ 容易记错的两个:char / int

char 的包装类是 Character(不是 Char),int 的是 Integer(不是 Int)。其余基本是首字母大写。

parse(解析)

◆ 定义

Parsing:把一种数据类型处理成另一种。每个包装类都有一个 parse 函数:XXX var = XXX.parseXXX(<string>);

int i     = Integer.parseInt("1");
double d  = Double.parseDouble("1");
boolean b = Boolean.parseBoolean("TruE");
// Integer 类还有: Integer.reverse(10), Integer.signum(-10) 等
● 自动装箱 / 拆箱 Boxing / Unboxing

把原始类型与它对应的包装类互相转换的过程。Java 可以自动完成,例如 Integer x = Integer.parseInt("20"); int y = x;

Chapter 04 · Inheritance and Polymorphism

继承与多态

用国际象棋的棋子做贯穿示例:所有棋子(Pawn/Rook/Knight…)有大量共同属性和方法。继承让你把共性提到父类,子类只写差异;多态让你用一个父类引用统一操作各种子类对象。这章是考试重头戏,重点在重写、动态绑定、抽象类。

4.1为什么需要继承(动机)

象棋里 Pawn、Rook、Knight、Bishop、King、Queen 每个棋子都有 isAliveisWhitecurrentRowcurrentColumn 这些属性和 moveisValidMove 方法。如果每个棋子写一个独立类:

⚠ 差设计的代价
  • 大量重复代码/功能,难调试;
  • 无法体现棋子之间的"相似性"/关系;
  • 实现工作量大;
  • 难以扩展。

例如 Board 里要 move(Pawn)move(Rook)move(Knight)… 而这些方法里的代码几乎完全一样。这不是你水平差,只是还没学会用继承

4.2继承基础:superclass / subclass / extends / is-a

◆ 定义
  • 继承 Inheritance:一种抽象形式,允许把多个类相似的属性/方法"泛化"出来(类比把基因传给孩子)。
  • 父类 Superclass:继承关系里的"父/基"类,向子类提供通用信息。
  • 子类 Subclass:继承关系里的"子/派生"类,从父类继承公共属性和方法。
  • 在父类定义公共属性和方法;
  • 子类自动拥有父类所有 public/protected 实例变量和方法;
  • 子类可以再定义额外的方法和实例变量;
  • 继承实现代码复用;子类应是父类的"更具体"版本。
public class Piece {                 // 父类:放共性
    private int currentRow, currentColumn;
    public int getCurrentRow() { return currentRow; }
    public void setCurrentRow(int r) { this.currentRow = r; }
    // ... getCurrentColumn / setCurrentColumn
}

public class Rook extends Piece {    // 子类:只写差异
    public void move(int toRow, int toColumn) { ... }
    public boolean isValidMove(int toRow, int toColumn) { ... }
}
public class Knight extends Piece { ... }
◆ extends 与 "Is A" 关系

extends:表示一个类继承自另一个类。继承表达的是 "Is A"(是一个)关系:

  • 所有 Rook 对象都 Piece;所有 Dog 都是 Animal;所有 Husky 都是 Dog。
  • 只在 "Is A" 关系成立时才用继承。
⚠ Is-A 是单向的
Rook rook1 = new Rook();      // ✅
Piece rook2 = new Rook();     // ✅ Rook "is a" Piece(向上)
Rook rook3 = new Piece();     // ❌ Piece "is not a" Rook(不能向下)

4.3super 调用父类构造函数

子类构造函数不必复制粘贴父类构造代码,用 super 调用父类构造函数。

public class Piece {
    private int currentRow, currentColumn;
    public Piece(int currentRow, int currentColumn) {
        this.currentRow = currentRow;
        this.currentColumn = currentColumn;
    }
}
public class Rook extends Piece {
    public Rook(int currentRow, int currentColumn) {
        super(currentRow, currentColumn);   // 调用父类构造函数
        // 其他代码
    }
}
★ super 构造函数三条规则
  • 只能在子类构造函数内使用;
  • 如果用,必须是子类构造函数的第一条语句
  • 传给 super 的参数类型必须匹配父类的某个构造函数。

4.4方法重写 Overriding(vs 重载)

★ 关键区分:方法只在父类 vs 父子都有
  • 方法只在父类定义(如 move):不管创建的对象是什么类型,都调用父类那个。
  • 父类和子类都定义了同签名方法(如 isValidMove):调用哪个纯粹取决于对象的类型,而不是引用的类型——这就是子类重写(Override)了父类方法。
public class Rook extends Piece {
    @Override   // 注解,表明这是在重写父类方法(可选, 但推荐)
    public boolean isValidMove(int toRow, int toColumn) {
        System.out.println("Rook class: isValidMove() method");
        return isValid;
    }
}
◆ 重写 vs 重载(必背区别)
  • 重写 Overriding:在子类里再次声明父类已有的、相同签名的方法。只能被子类重写。
  • 重载 Overloading:声明多个同名但签名不同的方法。父类方法可以在子类被重载。
● 为什么要重写

子类可以扩展或改变父类功能;用父类类型的引用时也能用到子类行为;在父类定义通用"接口",具体行为由子类实现——这让你能用一个父类引用无缝访问各子类的方法。

4.5super.method() 扩展父类方法

isValidMove 的逻辑有两部分:所有棋子共同的(检查是否在棋盘内)和各自特有的(具体走法)。把通用部分放父类,子类调 super 复用:

public class Piece {
    final static int BOARD_SIZE = 8;
    public boolean isValidMove(int toRow, int toColumn) {
        return toRow >= 0 && toRow < BOARD_SIZE &&
               toColumn >= 0 && toColumn < BOARD_SIZE;   // 通用边界检查
    }
}
public class Rook extends Piece {
    public boolean isValidMove(int toRow, int toColumn) {
        if (!super.isValidMove(toRow, toColumn))   // 先调父类做边界检查
            return false;
        // 再加 Rook 特有的走法逻辑
        return isValid;
    }
}
◆ super 关键字

super:对一个对象父类的引用。就像 this 指向自身,super 指向父类的属性和方法。

● 更好的封装

进一步把 isValidMove 的调用收进 move 方法内部,使用者不必自己显式检查合法性:move 内部先 if (!isValidMove(...)) return false; 再移动。

4.6重写的规则与陷阱

⚠ 重写不能改返回类型

重写方法不能改变返回类型——除非改成原返回类型的子类型(协变返回)。

⚠ private 方法不能被重写

父类 private 方法无法被重写。子类里写一个同名 private 方法是合法的,但它不是重写——不会被父类引用调用,也不能用 super 调到父类那个。给它加 @Override 会编译报错。

⚠ 不能缩小可见性

重写时子类不能进一步限制被重写方法的可见性:

  • 父类 public 方法 → 子类必须保持 public
  • 父类 protected 方法 → 子类可保持 protected 或放宽为 public
  • 父类 private 方法 → 无法重写。
★ final 禁止重写

final:表示属性/方法/类只能被赋值/声明/定义一次。final 方法不能被子类重写,例如把 move 声明为 public final boolean move(...) 就锁死了它。

4.7继承中的访问控制 + 隐私泄露

  • 子类不能调用父类的 private 方法,不能访问父类 private 属性。
  • 子类可以调用父类的 protected 方法,可以访问父类 protected 属性。
⚠ 隐私泄露 Privacy Leaks

把属性设成 protected 虽然能让子类直接改它们,但应避免,因为会造成隐私泄露:

父类对属性的更新本应带有检查(如确认新位置合法)。一旦属性是 protected,子类就能绕过这些检查直接改,导致对象进入无效状态。父类属性应通过父类的 public/protected 方法来访问。

★ 经验法则

只被子类使用的父类方法,应定义为 protected(如 isValidMove:子类要用它,但它逻辑不完整、不应被外部直接调用)。

4.8遮蔽 Shadowing + 动态绑定

这是本章最容易混的点。先看课件例子:父类 PieceS 和子类 RookS 声明了 public currentRow(子类变量"遮蔽"了父类的同名变量)。

public class PieceS {
    public int currentRow, currentColumn;
    public PieceS(int currentRow, int currentColumn) {
        this.currentRow = currentRow;   // 设的是 PieceS 的 currentRow
        this.currentColumn = currentColumn;
    }
    public int getCurrentRow() { return this.currentRow; }
}
public class RookS extends PieceS {
    public int currentRow, currentColumn;   // 遮蔽父类的同名变量
    public RookS(int currentRow, int currentColumn) {
        super(currentRow, currentColumn);   // 只设了父类的 currentRow, RookS 自己的还是 0
    }
    public int getCurrentRow() { return this.currentRow; }  // 返回 RookS 的 currentRow
}
测试输出
RookS  r1 = new RookS(4, 3);
r1.getCurrentRow()  // → 0
r1.currentRow       // → 0

PieceS r2 = new RookS(4, 3);   // 引用类型 PieceS, 对象类型 RookS
r2.getCurrentRow()  // → 0   方法跟【对象类型 RookS】, 返回 RookS.currentRow = 0
r2.currentRow       // → 4   字段跟【引用类型 PieceS】, 访问 PieceS.currentRow = 4
★ 动态绑定核心规则(必背)
访问的东西看谁来决定记法
方法调用对象的真实类型(运行时)方法跟对象
字段访问引用的声明类型(编译期)字段跟引用

这就是为什么 r2.getCurrentRow()(方法)走子类返回 0,而 r2.currentRow(字段)走引用类型 PieceS 返回 4。

⚠ 遮蔽 Shadowing:Don't. Do. It.

遮蔽定义:在重叠的作用域(如子类和父类)里用相同名字声明了两个或多个变量。访问哪个变量取决于引用类型而非对象。课件原话:"Don't. Do. It." ——公共变量在父类定义一次就够了,不要在子类重复声明。

4.9Object 类(toString / equals / hashCode)

◆ 万物皆 Object
  • Java 中每个类都隐式继承自 Object 类
  • 所有类都是 Object 类型;所有类都有 toString 方法、equals 方法(以及其他次要的东西)。
  • 继承来的 toString 没用:默认打印类似 Rook@1540e19d(类名@哈希),所以要重写它,返回有意义的字符串。
  • 继承来的 equals 也没用:默认返回 false(只比引用),所以要重写。
⚠ equals 的签名必须是 Object

public boolean equals(Piece otherPiece) 能编译,但它没有真正重写 Object 的 equals——因为 Object 里的签名是 public boolean equals(Object otherObject)。参数类型不同 = 重载而非重写。

IntelliJ 自动生成的正确 equals(推荐用这种)
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null) return false;
    if (getClass() != o.getClass()) return false;
    Piece piece = (Piece) o;            // 安全的向下转型
    return currentRow == piece.currentRow &&
           currentColumn == piece.currentColumn;
}
@Override
public int hashCode() {
    return Objects.hash(currentRow, currentColumn);
}

这个版本才真正重写了 Object 的 equals,比第 3 章里那个简化版更标准。比较逻辑可按需替换。

4.10instanceof / 向上向下转型

◆ 两个工具
  • getClass:返回一个 Class 类型对象,表示调用对象所属类的细节。
  • instanceof:运算符,当对象 A 是 B 的同类、或继承自 B 的类的实例时返回 true。
new Rook()  instanceof Piece   // true
new Piece() instanceof Rook    // false
◆ 向上 / 向下转型
  • Upcasting 向上转型:把子类对象赋给祖先类变量。总是安全
    Piece p = new Rook(2, 3);
  • Downcasting 向下转型:把祖先类变量赋给子类变量。只有当底层对象确实是那个类时才有意义,需要显式 cast。
    Piece robot = new WingedRobot();
    WingedRobot plane = (WingedRobot) robot;
⚠ 向下转型有风险

如果底层对象其实不是目标子类,向下转型会出错。转型前常用 instanceof 先确认类型。

4.11多态 Polymorphism

◆ 定义

多态 Polymorphism:以多种不同方式使用对象或方法的能力,字面意思大致是"多种形态(multiple forms)"。课件列出四种:

形式机制类别
重载 Overloading同名方法,按签名区分多种形态Ad Hoc 多态
重写 Overriding同名方法,按类不同而行为不同子类型多态 Subtype
替换 Substitution用子类替代父类的位置子类型多态 Subtype
泛型 Generics定义参数化的方法/类(后续章节)参数化多态 Parametric
★ 多态最常见的用法
Shape[] s = new Shape[3];
s[0] = new Circle(0, 0, 1);
s[1] = new Square(0, 0, 2);
for (Shape shape : s) {
    System.out.println(shape.computeArea());  // 各自调自己的 computeArea
}

用父类类型的数组装不同子类对象,循环里调同一个方法名,每个对象按自己真实类型执行对应实现——这就是子类型多态 + 动态绑定的威力。

4.12抽象类与抽象方法

问题:Piece 这个类本身有意义吗?new Piece() 是什么东西?凭空一个"棋子"——它不对应任何真实棋子(兵、车…)。

◆ 定义
  • 抽象类 Abstract Class:表示其子类的公共属性和方法、但缺少某些子类特定信息的类。不能被实例化
  • 具体类 Concrete Class:任何非抽象的类,对它能做的所有动作都有明确、具体的实现。
  • abstract(类):定义一个不完整的类,是"通用概念"而非完全实现的细节。
  • abstract(方法):定义一个对所有子类公共、但没有实现的父类方法;每个子类通过重写提供自己的实现。
public abstract class Shape {
    private double centreX, centreY;
    public Shape(double centreX, double centreY) {
        this.centreX = centreX;  this.centreY = centreY;
    }
    public abstract double computeArea();   // 抽象方法, 无方法体, 以分号结尾
}
public class Circle extends Shape {
    private double radius;
    public Circle(double x, double y, double radius) {
        super(x, y);  this.radius = radius;
    }
    @Override
    public double computeArea() { return Math.PI * Math.pow(radius, 2); }
}
⚠ 抽象类 vs 具体类的规则
  • 只有抽象类可以有抽象方法;具体类不能有抽象方法。
  • 只要类里有抽象方法,这个类就必须声明为 abstract。
  • 抽象类不能被实例化Piece p = new Piece(3,2); ❌ 非法。
  • 抽象方法不能有任何实现,全部逻辑必须由子类实现。

4.13继承的类型 + Java 不支持多继承

继承可以有多级,例如 Shape → Circle → GraphicCircle。更一般地,继承有几种形式:

  • 单继承 Single:只有一个父类
  • 多继承 Multiple:多个父类
  • 层次继承 Hierarchical:一个父类,多个子类
  • 多级继承 Multi-Level:从一个派生类再派生
  • 混合继承 Hybrid:多于两种类型的组合
  • 多路继承 Multi-path:从两个来源继承某些属性
★ 重要结论
  • Java 不支持多继承(以及涉及多继承的形式,如多路继承)。
  • Java 通过接口(Interfaces,下一章)"算是"支持多继承,但并不完全等同于多继承。
Chapter 05 · Software Tools and Bagel

工具与 Bagel

工具课:Git 版本控制、Maven 构建、IntelliJ 调试器、Bagel 游戏框架、开源项目。考试主要考概念与术语(不会让你背完整操作流程),所以这里聚焦定义、命令清单和 Bagel 的核心 API。

5.1Git 版本控制(模型 + 命令)

◆ 版本控制 + Git
  • 软件版本控制:系统化地管理软件制品(文档、源码、数据等)的并发版本。个人能回退到旧版本;团队能在本地写测后再合入代码库。
  • Git:最常用的现代版本控制系统(约 70%),由 Linus Torvalds 创建(也是 Linux 作者)。
★ Git 的四个区 / 核心术语
  • Repository 仓库:被版本系统存储的一组文件 + 追踪用的元数据。Remote(远程,如 GitHub/GitLab)/Local(本地,.git 子目录)。
  • Index / Staging Area 暂存区:暂存改动的概念区域(在 .git 子目录里)。
  • Working directory 工作目录:本地保存和修改文件的目录。
  • Master:当前"已知良好"的代码库;Branch 分支:正在开发的某个版本;Commit:分支的一个快照。
  • Merging 合并:把一个分支的改动并入另一个;Merge request(= pull request):请求把完成的功能分支合入 master。
◆ 常用命令
命令作用
git clone [URL]把远程仓库复制一份到本地(首次用 clone,不是 pull)
git add . / git add [file]暂存全部 / 指定文件
git commit -m "message"把暂存的改动提交到本地仓库(message 要有意义)
git push / git push -u origin master把本地改动推到远程
git status显示当前状态:哪些文件改了、哪些已暂存
git log显示仓库历史(之前的 commit)
git branch显示当前所在分支
git reset --hard回退到上一个已提交版本(误删/改坏时救命)

5.2Maven 构建管理

◆ 定义
  • 构建管理/自动化工具:自动化构建(编译)软件的过程(如 make、Apache Ant)。
  • Maven:流行的构建管理工具,通过自动从外部仓库导入所需的库和依赖来简化构建。
  • Maven 用一个 XML 文件 pom.xml(POM = Project Object Model,项目对象模型)声明项目结构和内容。
● 本课要求

本课不要求深入懂 Maven 细节——会提供目录结构、pom.xml 和构建说明,你把它作为 Maven 项目导入 IntelliJ 即可开始开发。

5.3IntelliJ 调试器

调试 Debugging:查找并修复软件中的 bug。调试器让你逐步走查程序、在运行时检查程序状态——比"乱改试试"或"printf 调试"高效得多。

★ 调试器核心功能(考点术语)
  • Breakpoint 断点:预先设定的一行,代码运行到这里会暂停,让你检查状态(变量内容、活动方法等)。
  • Watch Expression 监视表达式:你想随时看到其值的一个表达式。
  • Step over 跨过:跳到下一行,"跨过"本行里的任何方法调用(不进入)。
  • Step into / Step out of 步入 / 步出:进入本行那个方法的代码 / 跳出当前方法调用。

5.4Bagel 游戏框架

◆ 什么是 Bagel

Bagel(Basic Academic Graphical Engine Library):为 SWEN20003 定制的 Java 图形库(基于 LWJGL),Project 必须使用。

★ Bagel 程序骨架
import bagel.*;
public class BagelTest extends AbstractGame {
    private Image smiley;
    private float x = 100, y = 100;

    public BagelTest() {
        super(800, 600, "Bagel Demo");      // 窗口宽, 高, 标题
        smiley = new Image("res/smiley.png");
    }
    public static void main(String[] args) {
        BagelTest game = new BagelTest();
        game.run();                          // 启动游戏
    }
    @Override
    public void update(Input input) {        // 游戏主循环, 约 60 次/秒
        float speed = 0.5f;
        if (input.isDown(Keys.LEFT))  { x -= speed; }
        if (input.isDown(Keys.RIGHT)) { x += speed; }
        if (input.wasPressed(Keys.ESCAPE)) { Window.close(); }
        smiley.draw(x, y);
    }
}
◆ 核心类与机制
  • AbstractGame:游戏基类,自己的游戏类 extends 它;run() 启动。
  • update(Input input):约每秒 60 次,Bagel 会:① 清空窗口为统一底色 ② 检查键鼠输入 ③ 调用 update。你把更新状态的逻辑写在这里。
  • Imagenew Image("res/x.png") 创建,draw(x, y) 绘制,draw 要在 update 里调用
  • Input:作为参数传给 update,用 isDown(Keys.X)(按住)、wasPressed(Keys.X)(刚按下)检查键鼠。
  • Window:含操作显示窗口的静态方法,如 Window.getWidth()Window.close()

5.5开源项目

◆ 定义

开源 Open-Source:原始源代码免费公开、可被再分发和修改的软件。任何人都能贡献,只要遵守项目的"规则"。

课件观点:为开源项目做贡献是展示就业能力的最佳方式之一——因为它要求高质量代码、理解复杂应用、学习陌生技能、在分布式团队协作、遵守项目规范。核心 take-away:学会用 Git、熟悉若干工具、善用调试器、会用 Bagel、学会 Google。

Chapter 06 · Input and Output

输入输出

三种输入(命令行参数、键盘、文件)和两种输出(终端、文件)。考试会要求你会用 Scanner、Reader(FileReader/BufferedReader)、Writer(FileWriter/PrintWriter)以及命令行参数。最容易丢分的是 Scanner 的两个陷阱。

6.1命令行参数

◆ 定义

命令行参数:程序执行时提供给它的信息/数据,通过 main(String[] args) 里的 args 变量访问。args 是一个 String 数组

java MyProg Hello World 10
// args = {"Hello", "World", "10"}   ← 三个元素

java MyProg "Hello World" 10
// args = {"Hello World", "10"}      ← 多词字符串用引号, 算一个元素
⚠ "10" 是 String 不是 int

命令行参数全是字符串。要当数字用必须转换:

int age = Integer.parseInt(args[0]);
double height = Double.parseDouble(args[1]);
String name = args[2];
● 何时用

缺点是没有交互性,通常用于程序配置。课件原话:考试里"只在题目明确要求时才用,基本上几乎不会用"。IntelliJ 里要通过设置 "run configuration" 提供命令行参数。

6.2Scanner 键盘输入

Java 提供比 C/Python 更强大的输入方式:Scanner

import java.util.Scanner;                       // ① 先导入
Scanner scanner = new Scanner(System.in);      // ② 创建
⚠ 每个程序只创建一个 Scanner

每个程序只创建一个 Scanner,否则会出问题(bad things happen)。

◆ System.in 与读取方法

System.in:代表标准输入流(命令行/终端)的对象。Scanner 的读取方法:

方法读取内容
nextLine()读一整行文本,直到回车/换行符
next()读一个 token(单词)
nextInt() / nextDouble() / nextBoolean() / nextFloat()读一个与方法名匹配类型的值
hasNext() / hasNextXXX()是否还有输入 / 下一个 token 是否匹配 XXX 类型

6.3Scanner 两大陷阱

⚠ 陷阱一:nextXXX 不自动向下转型

Scanner 不会自动把类型向下转换(如 float→int)。用 nextXXX 时输入必须严格匹配代码期望的类型,否则报错。

double d = scanner.nextDouble();
float  f = scanner.nextFloat();
int    i = scanner.nextInt();
// 输入 "5 6.7 7.2" → Error!因为 nextInt 读到 7.2 不是整数
⚠ 陷阱二:nextXXX 与 nextLine 混用(高频踩坑)

nextLine 是唯一会"吃掉"换行符的方法。nextInt/nextDouble 等读完数字后,那个换行符还留在输入里,紧接着的 nextLine 会读到这个空残留。

double d  = scanner.nextDouble();   // 读 5
String s1 = scanner.nextLine();     // 读到 5 后面剩下的空(换行前的空白)→ ""
String s2 = scanner.nextLine();     // 读下一行 → "6.7"
// 输入:
//   5
//   6.7
//   7.2
// 输出: 5.00 , (空) , 6.7

解决:在 nextXXX 之后、读整行之前,补一个 scanner.nextLine() 把残留换行吃掉:

double rating = scanner.nextDouble();
scanner.nextLine();                 // 吃掉残留换行
String comment = scanner.nextLine();// 现在能正确读到下一行

6.4读文件

方式一:BufferedReader + FileReader
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;

try (BufferedReader br =
         new BufferedReader(new FileReader("test.txt"))) {
    String text;
    while ((text = br.readLine()) != null) {   // 逐行读, 读到 null 为止
        System.out.println(text);
    }
} catch (Exception e) {
    e.printStackTrace();
}
◆ 两层对象 + 逐行读的写法
  • FileReader:低级文件类,只能简单读字符
  • BufferedReader:更高级,能读字符串(行),包在 FileReader 外面。
  • br.readLine() 读一行;text = 把这行赋给变量;!= null 判断是否真的读到了内容(到文件末尾返回 null)。
  • try(...) 是 try-with-resources,会自动关闭文件;异常处理见"异常"章节。
方式二:Scanner + FileReader
try (Scanner file = new Scanner(new FileReader("test.txt"))) {
    while (file.hasNextLine()) {
        System.out.println(file.nextLine());
    }
} catch (Exception e) { e.printStackTrace(); }
● 两种方式对比

Scanner 方式和 BufferedReader 一样能读,但还能解析(parse)文本;缺点是缓冲区(内部内存)更小、更慢,适合较小的文件。

6.5CSV 文件

◆ 定义(Project 必备)
  • CSV = Comma Separated Value(逗号分隔值),约等于一个电子表格。
  • 通常有一个 header 行解释各列。
// recipe.csv:
// Ingredient,Cost,Quantity
// Bananas,9.2,4
// Eggs,1,6

while ((text = br.readLine()) != null) {
    String cells[] = text.split(",");          // 按逗号拆成各列
    String ingredient = cells[0];
    double cost = Double.parseDouble(cells[1]);
    int quantity = Integer.parseInt(cells[2]);
}
★ 套路

读 CSV 的标准套路:逐行 readLine()split(",") 拆列 → 用 parseInt/parseDouble 把需要的列转成数字。(常常要先 nextLine()/读掉 header 行。)

6.6写文件

import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.IOException;

try (PrintWriter pw =
         new PrintWriter(new FileWriter("testOut.txt"))) {
    pw.println("Hello World");
    pw.format("Device is %s, price $%d", "iPhone", 100000);
} catch (IOException e) {
    e.printStackTrace();
}
◆ 两层对象 + 方法
  • FileWriter:低级文件类,简单字符输出,用来创建……
  • PrintWriter:更高级,支持更复杂的格式化(方法和 System.out 一样)。
  • pw.print(s) 输出字符串;pw.println(s) 输出并换行;pw.format(...) 支持格式说明符。
  • try(...) 用完会自动关闭文件。

6.7读写结合 + append 模式

try (BufferedReader br = new BufferedReader(new FileReader("input.txt"));
     PrintWriter pw = new PrintWriter(new FileWriter("output.txt"))) {
    String text;
    while ((text = br.readLine()) != null) {
        pw.println(text.toLowerCase());   // 边读边写
    }
} catch (Exception e) { e.printStackTrace(); }
★ append 追加模式

new FileWriter("file.txt", true) 的第二个参数 true 表示追加模式:每次运行会往文件追加内容,而不是覆盖。

两个典型应用:① 用文件在计算过程中存储/恢复中间数据;② 用 Java 解析/操作/转换文件(读入→处理→写出)。

6.8考试要求

★ 课件明确的考试范围
  • 会用 Scanner
  • 会用 Reader 类(FileReader、BufferedReader)。
  • 会用 Writer 类(FileWriter、PrintWriter)。
  • 会写从文件和向文件的代码,熟悉讲座/workshop/Grok 里介绍的方法。
  • 会用命令行参数
Chapter 07 · Interfaces

接口 Interfaces

接口(interface)是比抽象类更"纯"的一种抽象:它只规定"一个对象会做哪些事",完全不管"怎么做"。它解决了 Java 一个大限制——一个类只能继承一个父类,但可以实现多个接口。这一章讲清楚接口是什么、那些容易踩的隐含规则、和抽象类怎么区分,以及你早就用过的 Comparable 接口。
(课程时间线上,这一讲紧跟在第 4 章「继承与多态」之后。)

7.1接口是什么

★ 定义

接口 Interface:声明一组常量和/或方法,用来定义一个对象的行为(behaviour)

◆ 接口是"抽象类的远房亲戚"
  • 定义了一个"抽象"的实体——不能被实例化(不能 new)。
  • (传统上)只能包含常量和抽象方法
  • 定义了一组跨多个类共有的行为/动作
  • 一个类可以选择"实现(implement)"某个接口。
● 类比

接口像一份"岗位职责清单":上面写着"能打印""能比较大小""能上色",但不写具体怎么做。任何类只要愿意"签下这份清单"(implement),就承诺自己会提供这些能力的具体做法。清单本身招不来人干活(不能实例化),它只是个约定。

7.2定义接口的隐含规则

◆ 一个接口长这样
public interface Printable {
    int MAXIMUM_PIXEL_DENSITY = 1000;   // 一个常量
    void print();                        // 一个抽象方法
}
★ 四条"隐含"规则(不用写出来,但 Java 默认就是)
  • 方法永远没有方法体(没有 { ... } 代码)。
  • 所有方法都隐含为 abstract(不用、也不必写 abstract 关键字)。
  • 所有属性都隐含为 static final——也就是常量。所以 MAXIMUM_PIXEL_DENSITY 自动是常量。
  • 所有方法和属性都隐含为 public
⚠ 常见考点

因为属性隐含 static final,你不能在接口里放一个"普通的、可变的成员变量"——写进去的字段一律是常量,且必须当场赋值。这点常被拿来出判断题。

7.3实现接口 implements

◆ 用 implements 关键字
public class Image implements Printable {
    public void print() {
        // 具体怎么打印图片
    }
}
public class Spreadsheet implements Printable {
    public void print() {
        // 具体怎么打印表格
    }
}
★ 两条规则
  • 具体类(concrete class)实现一个接口时,必须实现接口里定义的所有方法
  • 如果一个类没有实现全部方法,那它必须是抽象类(abstract)——把没实现的留给它的子类去完成。
● 这正是多态的来源

ImageSpreadsheet 都是 Printable,所以你可以用统一类型 Printable 来装它们、调用 print(),各自表现各自的行为——这就是第 4 章和第 12 章反复出现的多态与开闭原则的基础。

7.4default 方法

★ 定义

default:给接口里的方法指定一个标准实现;如果某个实现类觉得这个默认行为不符合自己的预期,可以重写(override)它

◆ 例子
public interface Printable {
    default void print() {
        System.out.println(this.toString());
    }
}

有了 default,实现 Printable 的类就算不写 print() 也能用(用默认那套);想要不一样的行为时再重写。它打破了"接口方法一定没有方法体"的老规矩,是现代 Java 的补充。

● 和第 16 章呼应

还记得函数式接口吗?它"只能有一个抽象方法,但可以有多个 defaultstatic 方法"——正是因为 default 方法不算抽象方法,所以加再多也不影响它当函数式接口、配 lambda 使用。

7.5一个类可以实现多个接口

★ 最重要的一条

一个类只能继承(extends)一个父类,但可以实现(implements)多个接口。

◆ 例子:又继承又实现一堆接口
public class Spreadsheet extends Document
        implements Printable, Colourable, Filterable, Comparable<Spreadsheet> {
    public void print() {
        // ...
    }
    // 还要实现 Colourable / Filterable / compareTo 里的方法
}

语法上:先 extends 唯一的父类,再 implements 一串用逗号隔开的接口。

● 为什么这很重要

Java 故意不允许多重类继承(避免"两个父类有同名冲突方法"的著名难题)。接口绕开了这个限制:一个类可以同时"承诺"多种能力。课件原话:继承和接口一起配合,能搭出非常强大的抽象,让写解决方案变得容易得多。

7.6接口 vs 抽象类

这是接口这一讲最高频的辨析考点。两者都"不能实例化、都用来做抽象",但定位不同。先回顾抽象类:

◆ 抽象类 Abstract Class 回顾

代表其子类共有的属性和方法,但缺少某些子类特有的信息,不能被实例化。它表示一个"不完整的概念",而不是问题里一个具体的"东西"。Piece p = new Piece(3,2); 这种写法是非法的。

★ 核心对照表
抽象类 Abstract Class接口 Interface
能否实例化不能不能
能有的成员普通字段、构造器、有实现的方法、抽象方法都行(传统上)只有常量 + 抽象方法;后来加了 default/static 方法
字段可有普通可变成员变量字段一律 public static final(常量)
一个类能要几个只能 extends 一个implements 多个
表达的语义is-a("是一种…",共享实现)can-do("能做…",行为契约)
关键字extendsimplements
● 一句话选择

共享一部分代码实现、且是"同一族东西"→ 用抽象类;只想规定"必须具备某种能力"、还希望一个类能同时具备好几种能力 → 用接口。课件把接口形容为抽象类"vague, distant 的远房亲戚"——更抽象、更松、更灵活。

7.7Comparable 接口

◆ 你其实早就用过接口

第 9 章排序时用到的 Comparable 就是一个接口。实现 Comparable<ClassName> 的类:能和同类对象比较大小、必须实现 public int compareTo(ClassName object)、因此能被自动排序(如 Collections.sort)。它定义了一个类的默认 / 自然顺序

★ compareTo 的返回值约定

比较"当前对象 this(A)"和"参数对象(B)",返回:

  • 负数:A "小于" B(A 排在前)
  • :A 与 B 相等
  • 正数:A "大于" B(A 排在后)
◆ 完整例子
public class RandomNumber implements Comparable<RandomNumber> {
    public final int number;
    public RandomNumber() {
        this.number = new Random().nextInt(100);
    }
    public int compareTo(RandomNumber other) {
        return this.number - other.number;   // 差值天然满足"负/零/正"约定
    }
    public String toString() {
        return Integer.toString(this.number);
    }
}

this.number - other.number 这个小技巧很常见:自己小就得负数、相等就是 0、自己大就是正数,正好符合约定。(注意 Comparable<T> 里的 T 是泛型,详见第 8 章。)

● 本章小结

记牢:接口是"行为契约"、不能实例化;四条隐含规则(方法无体、隐含 abstract、字段隐含 static final、成员隐含 public);具体类必须实现所有方法、否则得是抽象类;default 方法给默认实现;单继承、多实现;接口 vs 抽象类(can-do vs is-a、多实现 vs 单继承);以及 ComparablecompareTo 负/零/正约定。

Chapter 08 · Generics

泛型 Generics

泛型让你写一次逻辑、适用于任意类型,同时保留编译期类型检查。先学怎么用泛型类(Comparable、ArrayList),再学怎么自己定义泛型类和泛型方法。

8.1泛型是什么 + 类型参数 T

◆ 定义

Java 允许类、接口或方法的定义里带参数类型,这种定义叫泛型(Generics)。好处:能写出适用于任何类型的通用逻辑、实现代码复用。

public interface Comparable<T> {
    public int compareTo(T other);
}
◆ 类型参数 Type Parameter
  • T类型参数(type parameter / type variable,类型变量)。
  • T 被赋予一个值(类型)时,所有占位符 T 都被替换。
  • T 的值就是一个类型(类/接口):Integer、String、Robot…
  • 实现接口的人负责提供这个类型:public class Dog implements Comparable<Dog> {...}

8.2Comparable<T>:泛型 vs 非泛型

对比两种写法,体会泛型的好处。非泛型版用 Object + instanceof + 强制转型:

public class Circle implements Comparable {     // 非泛型
    @Override
    public int compareTo(Object o) {
        if (o instanceof Circle) {
            Circle c = (Circle) o;              // 必须手动转型
            // ... 比较 radius, 返回 1 / -1 / 0
        } else {
            return -2;                          // 类型不对只能返回一个"无效"值
        }
    }
}
// c1.compareTo(s) 拿 Circle 比 Square → 返回 -2, 程序员还得处理 -2
★ 泛型版的优势:编译期就拦住错误
public class CircleT implements Comparable<CircleT> {  // 泛型
    @Override
    public int compareTo(CircleT c) {           // 参数直接是 CircleT, 不用转型
        // ... 比较 radius
    }
}
// c1.compareTo(s)  ← Circle 比 Square, 直接编译错误!

泛型版不必检查 -2 这种无效返回,类型不匹配在编译期就被发现,更安全、更优雅。

8.3ArrayList 入门

ArrayList 是一个常用的泛型类,弥补了数组的局限(定长、手动 resize、增删费力)。

import java.util.ArrayList;
ArrayList<Circle> circles = new ArrayList<Circle>();
circles.add(new Circle(0.0, 0.0, 5));
for (Circle c : circles) { ... }    // 可像数组一样 for-each
◆ ArrayList 给你什么
  • 能像数组一样 for-each 遍历;自动处理扩容;任意位置增/删/取/改;自带 toString()
  • 不能用 [] 索引(用 get(i))。
  • 本质:ArrayList 是一个以数组为实例变量的类
⚠ ArrayList 的两个限制
  • 自动增长但不会自动收缩,可能占用多于所需的内存——需调 trimToSize() 释放多余内存。
  • 不能存原始类型(int、float 等)——要用包装类(Integer、Float)。

8.4定义泛型类(单 / 多类型)

◆ 定义

泛型类 Generic Class:字段、参数或返回类型使用任意类型来定义的类。类型参数写在类名后的尖括号里;可插入任何引用类型(类类型);传统用单个大写字母,但任何非关键字标识符都行;像普通类一样编译。

public class Sample<T> {           // 单类型参数
    private T data;
    public void setData(T data) { this.data = data; }
    public T getData() { return data; }
}

public class TwoTypePair<T1, T2> { // 多类型参数
    private T1 first;
    private T2 second;
    public void setFirst(T1 first) { this.first = first; }
    public void setSecond(T2 second) { this.second = second; }
}
// 使用:
TwoTypePair<String, Integer> rating =
    new TwoTypePair<String, Integer>("The Car Guys", 8);

8.5有界类型参数 Bounded Type Parameters

有时需要保证类型参数具备某种行为,就给它加上界(bound),用 extends

public class Generic<T extends Comparable<T>> { }   // T 必须可比较
public class Generic<T extends Robot> { }            // T 必须是 Robot 或其子类
public class Generic<T extends Robot
                     & Comparable<T> & List<T>> { }  // 多重界限, 用 & 连接
● 注意

有界类型参数里 extends 既可以表示"继承自某个类",也可以表示"实现某个接口",多个界限用 & 连接。

8.6泛型方法 Generic Method

◆ 定义

泛型方法 Generic Method:接受任意类型参数、或返回任意类型对象的方法。类型参数(如 T局部于该方法,可定义在任何类里。类型参数写在返回类型前面

public <T> int genericMethod(T arg);          // 泛型参数
public <T> T   genericMethod(String name);    // 泛型返回值
public <T> T   genericMethod(T arg);          // 两者都有
public <T,S> T genericMethod(S arg);          // 两个类型参数

// 例: 统计 item 在 array 中出现次数
public static <T> int countOccurrences(T[] array, T item) {
    int count = 0;
    for (T arrayItem : array) {
        count = item.equals(arrayItem) ? count + 1 : count;
    }
    return count;
}

8.7泛型的限制

⚠ 泛型不能做的两件事
  • 不能实例化参数化对象:T item = new T();
  • 不能创建参数化对象的数组:T[] elements = new T[];

除此之外,大多数操作都可以。

Chapter 09 · Collections and Maps

集合与映射

Java 两大框架:Collections(存一组对象——list/set/queue)和 Maps(存键值对)。重点在选对数据结构、排序(Comparable vs Comparator)、泛型不变性,以及 HashMap/TreeMap 的使用。

9.1Collections vs Maps 框架概览

◆ 定义
  • Collections:用于存储、访问、操作一组对象(如 list、set、queue)的框架。
  • Maps:用于存储、访问、操作键值对(key-value pairs)的框架。
集合与映射框架层次图
图 9.1 · Java 集合与映射框架层次(椭圆=接口,蓝框=抽象类,紫框=类;实线=extends,虚线=implements)。红框是本课重点:PriorityQueueLinkedListArrayListTreeSetHashSetTreeMapHashMap。注意 Map 不属于 Collection 体系,自成一支。

9.2选哪种集合

◆ List vs Set 的本质区别
  • List:保持元素顺序允许重复;支持按下标访问(如 get(0))。
  • Set:只存唯一元素;适合需要忽略重复的场景。
特点何时用
ArrayList用可变数组存,按下标访问快大多数 list 场景的默认选择
LinkedList用链接节点存在开头增删、或遍历中增删更好
HashSet唯一元素,查找快,不排序需要去重、快速查找
TreeSet唯一元素,按序排列,通常比 HashSet 慢需要有序的唯一集合
PriorityQueue可按非平凡方式排序元素需要优先级排序
● 默认建议

一般情况下,list 用 ArrayList、map 用 HashMap 通常就是更好的选择。

9.3Collection 常用操作

来源接口操作方法
Collection<E>长度int size()
是否包含boolean contains(Object e)
添加boolean add(E element)
删除boolean remove(Object e)
Iterable<E>取迭代器Iterator<E> iterator()
遍历for (E e : collection)
List<E>按下标取E get(int index)Set 没有 get(index)
⚠ contains 用的是 equals

contains()equals() 判断是否匹配。对自定义类,通常必须重写 equals();否则 Java 用默认的 Object.equals(),它只检查两个引用是否指向同一个对象(呼应第 4 章)。

9.4ArrayList 进阶 + Iterator

ArrayList 可存"同一基类"的不同子类对象,实现多态:

ArrayList<Shape> shapes = new ArrayList<Shape>();
shapes.add(new Circle(0.0, 0.0, 5));   // Circle extends Shape
shapes.add(new Square(0.0, 0.0, 7));   // Square extends Shape
for (Shape s : shapes) {
    System.out.println(s.getArea());   // 各自调自己的 getArea(), 无缝多态
}
◆ Iterator 迭代器

Iterator 是一个能让我们一次一个元素遍历集合的对象,是 for-each 的另一种写法:

Iterator<Circle> it = circles.iterator();
while (it.hasNext()) {        // hasNext(): 还有下一个吗
    Circle c = it.next();     // next(): 返回下一个
    System.out.println(c.getRadius());
}

普通遍历用 for-each 更简单;需要更多控制(如遍历中安全删除元素)时用显式 Iterator。

9.5泛型不变性 Invariance

★ 泛型是"不变的"(invariant)

可以往 ArrayList<Shape> 里加 Shape 的子类对象:

ArrayList<Shape> shapes = new ArrayList<>();
shapes.add(new Circle(...));   // OK
shapes.add(new Square(...));   // OK

ArrayList<Circle> 不是 ArrayList<Shape>

ArrayList<Shape> list1 = new ArrayList<Circle>();  // ❌ 不合法
ArrayList<Shape> list2 = new ArrayList<Square>();  // ❌ 不合法
⚠ 数组却是"协变的"——别混!
Shape[] shapes1 = new Shape[10];
Shape[] shapes2 = new Square[10];   // ✅ 数组允许这样(协变 covariant)

关键对比:泛型集合类型不变(invariant)——List<Circle>List<Shape> 无父子关系;而数组协变(covariant)——Square[] 可当 Shape[] 用。这是考试爱考的反直觉点。

9.6排序:Comparable<T>

◆ Comparable<T> = 默认/自然顺序

实现 Comparable<T> 为类定义默认(自然)排序compareTo(T other) 把当前对象和另一个同类型对象比较:

  • 返回负数:this 排在 other 前面
  • 返回:两者相等
  • 返回正数:this 排在 other 后面
public class Movie implements Comparable<Movie> {
    private int year;
    public int compareTo(Movie m) {
        return this.year - m.year;   // 按年份升序
    }
}
// 排序: Collections.sort(list);  ← 用类的默认顺序(compareTo)
//      list 会被直接修改

9.7排序:Comparator<T> + Collections.sort

◆ Comparator<T> = 另一套排序规则

Comparator<T> 为 T 定义一套独立于类默认顺序的排序规则。compare(T o1, T o2):负数→o1 在前;零→相等;正数→o1 在后。

public class RatingComparator implements Comparator<Movie> {
    public int compare(Movie m1, Movie m2) {
        if (m1.getRating() < m2.getRating()) return -1;
        if (m1.getRating() > m2.getRating()) return 1;
        return 0;
    }
}
★ Collections.sort 两个版本
写法用什么排序
Collections.sort(list)类的默认顺序(要求元素类实现 Comparable<T>,用 compareTo)
Collections.sort(list, comparator)用传入的 Comparator<T> 对象(自定义顺序)

口诀:自然/默认顺序用 Comparable,替代顺序用 Comparator。 注意:没有 Maps.sort()Collections.sort(map)

9.8匿名内部类 Anonymous Inner Class

给每种排序都建一个 Comparator 类有点小题大做。匿名内部类是更轻的方案。

◆ 定义

匿名内部类:"即时"创建的类,没有单独文件、没有类名,只为它创建单个对象

Collections.sort(list, new Comparator<Movie>() {
    @Override
    public int compare(Movie m1, Movie m2) {
        if (m1.getRating() < m2.getRating()) return -1;
        if (m1.getRating() > m2.getRating()) return 1;
        return 0;
    }
});   // 直接在这里定义并实例化, 不用单独写一个 RatingComparator 文件

9.9Maps:HashMap / TreeMap

◆ HashMap vs TreeMap
  • HashMap:存键值对,按键快速查找不保持键有序。大多数 map 场景默认用它。
  • TreeMap:键按序排列,通常比 HashMap 慢。
操作方法(来自 Map<K,V>
长度int size()
是否含键 / 值boolean containsKey(Object k) / containsValue(Object v)
添加/替换V put(K key, V value)
删除V remove(Object key)(返回被删的值,或 null)
取值V get(Object key)
遍历键Set<K> keySet()
遍历条目Set<Map.Entry<K,V>> entrySet()
HashMap<String, Book> library = new HashMap<>();
library.put(b1.getAuthor(), b1);

// 遍历方式一: 按 keySet
for (String author : library.keySet()) {
    Book b = library.get(author);
}
// 遍历方式二: 按 entrySet(拿到键和值)
for (Map.Entry<String, Book> entry : library.entrySet()) {
    String author = entry.getKey();
    Book b = entry.getValue();
}

9.10泛型在框架中的价值 + 实战提示

◆ 为什么集合/映射要用泛型

没有泛型的话,要么把一切都当 Object(用时再转型),要么为每种类型重写代码。泛型让你写一次、复用于任意类型,同时让对象保留自己的类型(不退化成 Object),还能让编译器提前发现错误,避免运行时错误。

⚠ 实战提示:电话号码别用 Integer

做电话簿 HashMap 时,若把国家码 61 和号码 136352 都存成 Integer 再相加,会被当成整数加法得到 136413,而不是想要的拼接 61136352。号码含 + ( ) 等符号也只能用 String。所以:HashMap<String, String> phonebook

● 选键/值类型的思路

设计 Map 时,键选什么取决于你主要想"用谁查谁"。如宠物追踪:键为 petID 适合"按宠物找主人"(动物救助中心);键为 owner 适合"按主人找宠物"(宠物登记的地方议会)。

Chapter 10 · Modelling Classes and Relationships

类关系与 UML 建模

从问题描述识别类、理清关系、用 UML 画类图。Project 2 的设计基础,也是考点(关联/聚合/组合区别明确"可考")。

10.1设计流程:识别类与关系

★ 先设计,后实现

写代码前先设计系统,再按设计写代码。四步:

  1. 识别类:名词提取(Noun Extraction),从描述里找名词当候选类。
  2. 识别类关系:找 has-a、is-a、can-do。
  3. 精炼类与关系:合并、调整。
  4. 画类图:用 UML(业界广泛接受的记号)。

10.2UML 是什么

◆ 定义

统一建模语言(UML):图形化建模语言,表示面向对象的分析、设计和实现。你学的是类建模,UML 只是其中一种记号。

10.3UML 表示一个类

一个类用三栏框表示:类名 / 属性 / 方法

UML 类表示
图 10.3 · numObjects/getNumObjects()下划线=static;属性 可见性 名称: 类型 = 初值;方法 可见性 名称(参数: 类型): 返回类型
★ 可见性符号(必背)
符号含义
+public
#protected
~package-private(default)
-private
◆ 属性与方法的组成
  • 属性:名称、类型(: int)、初值(= 0)、可见性、多重性、static。
  • 多重性写法[10] 固定、[1..10] 已知范围、[1..*] 未知上限、[*] 零或多。
  • 方法:名称、可见性、返回类型、参数。static 成员用下划线

10.4四种类关系总览

★ 四种关系 + UML 符号
关系含义UML 画法
关联 Associationhas-a,含另一个类作属性实线
 ↳ 聚合 Aggregation"有",但两者独立存在实线 + 空心菱形
 ↳ 组合 Composition"有",部分离开整体无意义实线 + 实心菱形
泛化 Generalizationis-a(继承)实线 + 空心三角 △
实现 Realization实现接口虚线 + 空心三角 △
依赖 Dependency弱关系,改动可能互相影响虚线箭头

10.5关联 Association

◆ 定义

关联:一条链接,表示一个类含有"本身也是类"的属性,是 has-a 关系,让对象把任务委托给别的对象,用实线连接。最基本的关系形式,常用于较弱的、对象可独立存在的关系(如 Customer 与 Order)。

⚠ 关联不要画成变量

一个类包含另一个类(对象)时用关联线,不要写成属性变量;只有原始数据类型才画成属性。

● 箭头方向(补充材料)
  • 无箭头:双向可导航,不强调对称(更常用、更简单)。
  • 单箭头:单向,有箭头一端能访问另一端,反之不行(Student→School)。
  • 双箭头:双向,强调对称。SWEN20003 多数情况可用无箭头代替双箭头;不确定就用无箭头。

10.6多重性 Multiplicity

◆ 定义

多重性:指定关联两端类的实例之间能存在多少条链接,可写在关系线任一端。

写法含义
1恰好一个
0..1零或一
*0..*多(零或多个)
1..*一或多个
3..4 / 6确切区间 / 确切数
0..1, 3..4, 6..*除 2 和 5 以外的任意个数
● 例 + 自关联

"Student 选 1..5 门 Course,一门 Course 有 10..400 个 Student"。自关联:每个 Student 有个学生代表(也是 Student),即类和自己关联。

10.7聚合 vs 组合

聚合和组合都是关联的子集,区别在"部分"能否脱离"整体"独立存在。

组合与聚合 UML 对比
图 10.7 · 实心菱形◆(University—Department)=组合,Department 离开 University 无意义;空心菱形◇(Department—Professor)=聚合,Professor 可去别的 University。菱形画在"整体/容器"一端。
★ 核心判据
聚合 ◇组合 ◆
菱形空心实心
部分能否独立存在不能(依赖整体)
整体被销毁时部分仍存在部分也失去意义
Pond◇Duck;Library◇BookUniversity◆Department;Person◆Arm
⚠ 不要在一个关系里混聚合和组合

一条关系线不能两端各放一个菱形(一空心一实心)。聚合和组合不能在同一关系里混用。

10.8泛化 / 实现 / 依赖

◆ 泛化 Generalization(继承)

表示 is-a,用实线 + 空心三角箭头指向父类。UML 里斜体类名/方法名表示 abstract 抽象

◆ 实现 Realization(接口)

一个类实现某接口,用虚线 + 空心三角箭头指向接口(如 Attacker 接口含 target()attack())。

◆ 依赖 Dependency

类之间的弱关系,一个类改动可能影响另一个,用虚线箭头。典型:方法把另一个类作输入参数,如 Player.move(p: Position) 依赖 Position

10.9UML 工具

课件推荐:draw.io(SWEN30006 首选)、LucidChart(学生邮箱注册解锁完整功能)、StarUML、带插件的 IDE(可把 UML 转代码)等。

Chapter 11 · Exceptions

异常 Exceptions

运行时出错时如何优雅地捕获、恢复或终止,而不是崩溃。重点:try/catch/finally、throw vs throws、自定义异常、checked vs unchecked。

11.1三类错误

◆ 定义
  • 语法错误 Syntax:写的不是合法代码,编辑器/编译器发现。
  • 语义错误 Semantic:能跑完但结果不对,靠软件测试发现。
  • 运行时错误 Runtime:导致程序提前结束(崩溃),靠执行发现。
● 常见运行时错误

除以零、数组越界、存不兼容数据、用负数当数组大小、把 "abc" 转 int、文件错误(读不存在的文件、写只读文件)等。

11.2处理运行时错误的三种方案

◆ 以"除法 n1/n2,n2 可能为 0"为例
  • 方案一:什么都不做,听天由命——显然不理想,n2=0 时抛 ArithmeticException 程序崩溃。
  • 方案二:防御式编程——用 if 显式守卫每个危险条件。缺点:要为每个可能错误都写防护、有些错误没有"备用路径"、代码不好读、抽象差(臃肿)。
  • 方案三:用异常——捕获错误状态,然后恢复或优雅结束程序。

11.3try / catch / finally

◆ 异常是什么
  • Exception 异常:代码运行时错误造成的错误状态;也是 Java 创建的、表示该错误的对象(是 Exception 类层次里某个类的实例)。
  • 异常处理 Exception Handling:在异常发生时主动保护程序的代码。
try {
    // 可能抛异常的代码
} catch (ExceptionClass e) {
    // 从异常中恢复, 或结束程序
} finally {
    // 无论是否发生异常都会执行
}
★ 三个关键字
  • try:尝试执行可能出错的代码。
  • catch:处理异常——恢复(让用户重输、调整索引)或失败(输出错误信息并退出)。
  • finally:做清理(如关文件),只要没强制退出就会执行。

11.4为什么需要 finally

◆ 不能直接把清理代码放在 try-catch 之后吗?

不能总依赖放在 try-catch 之后的代码运行——因为如果 try 或 catch 里抛了异常、或执行了 return,执行可能永远到不了后面那行。

finally保证清理代码(关文件、释放数据库连接、释放资源)无论 try 如何退出都会执行,防止资源泄漏。

11.5多 catch 块

try {
    ...
} catch (FileNotFoundException e) {   // 最具体的(子类)在前
    e.printStackTrace();
} catch (IOException e) {             // 更宽泛的(父类)在后
    e.printStackTrace();
}
⚠ 顺序:具体在前,宽泛在后

可以链接多个 catch 分别处理不同异常。最具体的异常(子类)写在前面,更宽泛的(父类)写在后面。顺序反了会编译错或永远命中不到具体分支。

11.6throw vs throws

★ 一个字母之差,作用不同
  • throw(动词,抛):响应错误状态,创建并抛出一个异常对象(已有的或自定义的)。throw new NullPointerException("...");
  • throws(声明):写在方法签名上,声明这个方法可能产生某异常,"我不处理 / 处理方式因应用而异",交给调用者。public Circle(...) throws InvalidRadiusException
public Person(int age, String name) {
    if (name == null) {
        throw new NullPointerException("Creating person with null name");
    }
    this.age = age; this.name = name;
}

11.7自定义异常

◆ 异常就是类

发现新"类型"的问题时,可以自定义异常:异常都是;大多数异常继承自 Exception 类;自定义异常常含两个构造函数(默认消息 + 自定义消息)。

// 第1步: 写异常类
public class InvalidRadiusException extends Exception {
    public InvalidRadiusException() {
        super("Radius is not valid");
    }
    public InvalidRadiusException(double radius) {
        super("Radius [" + radius + "] is not valid");
    }
}
// 第2步: 在可能出错处 throw, 并在方法签名 throws
public Circle(double cx, double cy, double radius)
        throws InvalidRadiusException {
    if (radius <= 0) throw new InvalidRadiusException(radius);
    ...
}
// 第3步: 调用处 try-catch, 用 e.getMessage() 取消息

11.8Checked vs Unchecked

★ 核心区别(必背)
Unchecked 非受检Checked 受检
是否必须处理不需要显式 catch 或 throws必须用 try-catch 处理或 throws 声明
继承自RuntimeExceptionException不是 RuntimeException
例子NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException、NumberFormatException、ArithmeticExceptionIOException、FileNotFoundException、ClassNotFoundException、ParseException
◆ 异常层次(Throwable 树)
  • Throwable(顶层)分两支:ExceptionError
  • Error:unchecked(如系统级严重错误)。
  • Exception:本身是 checked,但其子类 RuntimeExceptionunchecked;Exception 的其他子类是 checked。

11.9Catch-or-Declare + try-with-resources

★ Catch or Declare(针对 checked 异常)
  • 所有 checked 异常必须二选一处理:① 用 try-catch 包住可能抛异常的代码;或 ② 用 throws 声明方法可能抛出。
  • 同一方法里两种方式可对不同异常分别使用。
  • 异常应留给"无法用其他方式轻松处理的、不寻常或意外的情况"。
◆ try-with-resources(自动关闭资源)

用于"用完需要关闭"的对象(如文件)。资源在 try(...) 的括号里创建,Java 会在 try 块结束时自动关闭它——无论正常结束还是抛了异常。常用于文件 IO(如 BufferedReader)。

try (BufferedReader reader = ...) {
    ...
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
Chapter 12 · Software Design

软件设计 Software Design

前面学的封装、继承、多态、UML、设计模式都是"工具"。这一章退后一步问一个更大的问题:到底怎样组织代码,才能让它易懂、易改、易测、易复用?分两半:上半场讲"怎么把代码写漂亮"(编码规范、注释、JavaDoc),下半场讲八条"设计原则"。这章基本全是文字 + 判断题式考点,没有难算的东西,重在理解每个词回答的是哪个问题。

12.1这一章在讲什么

◆ 两条主线
  • 写更好的代码(Write better code):编码规范 Coding Standards、注释 Commenting、JavaDoc。——这是"句子层面"的整洁。
  • 设计更好的软件(Design better software):模块化、内聚、耦合、抽象、封装、多态、委托、开闭原则。——这是"结构层面"的整洁。
● 一个生活类比

把写程序想成写一本书。编码规范和注释 = 排版、标点、章节标题(让读者读得顺);设计原则 = 这本书该怎么分章、每章只讲一件事、章与章之间别互相缠死(让整本书结构合理)。排版再漂亮,结构乱了照样难读;反过来也一样。两条主线缺一不可。

12.2编码规范 Coding Standards

◆ 什么是编码规范

写代码很大程度是"主观"的(同一个功能十个人能写十种样子),但程序员之间有大量共识性的约定(conventions)。一些组织会把这些约定写成正式的编码规范(coding standards),团队统一遵守。

★ 常见规范条目
  • 统一的布局:缩进、空白要一致。
  • 避免过长的行(80 字符是个"历史遗留"的经典上限)。
  • 小心 tab(不同编辑器里 tab 宽度不一样,容易让缩进错乱)。
  • 注释和代码排版整齐
  • 变量、方法、类命名要有意义(呼应第 1 章的标识符约定:变量/方法小驼峰,类大驼峰)。
  • 不要复制粘贴 / 重复代码(重复是 bug 的温床,改一处要改十处)。
  • 用注释解释每一代码在做什么。
● 为什么重要

这些都不是"语法要求"——不守规范程序照样能跑。它们是为了:让你的队友(和三个月后的你自己)能读懂。考试里这部分常以"下面哪个不是好的编码习惯"之类的判断题出现。

12.3注释 Comments + 放置位置

◆ 注释写给谁、起什么作用
  • 注释主要写给你自己和一起写代码的开发者看(区别于下一节的 JavaDoc,那是给"使用你代码的人"看的)。
  • 理想状态:代码本身要自解释(self-documenting)——光读代码、不看注释也能看懂。
  • 注释负责"讲故事":如果把所有代码删掉、只留注释,应该还能据此"拼凑出"整个算法的思路。
  • 注释应附着在一段段代码上,每段大致对应算法的一个步骤。
⚠ 放置位置:注释写在代码"上方",不是下方
// 不好:注释写在代码下面
<一行代码>
// This is a comment below my code

// 好:注释写在代码上面
// This is a comment above my code
<一行代码>

放在代码前面的注释像一段"开场白 / 序言":先告诉读者这段代码要干嘛,读者再去消化下面的实现。放后面就成了"马后炮"。

12.4JavaDoc 文档注释

◆ JavaDoc 是什么
  • javadoc 是 JDK(Java 开发工具包)自带的一个命令行工具,能从一种特殊注释里自动生成文档
  • JavaDoc 注释以 /** 开头、*/ 结尾(普通注释是 /* *///,多一个星号就变成文档注释)。
  • 可以被编译成 HTML 网页
  • 用来给包、类、方法、属性等写文档。
  • 有各种 @ 标签,如 @param(参数)、@return(返回值)、@author@version
★ JavaDoc ≠ 注释(重点区分)
  • 注释(comments):写给"和你一起写代码的人",解释内部是怎么实现的。
  • JavaDoc:主要写给"使用你程序的开发者",说明怎么用你的类和方法(输入什么、得到什么),不关心内部实现。
  • 课件原话:写 JavaDoc 不等于给代码写注释(两者目的、读者都不同,不能互相替代)。
◆ JavaDoc 示例
/** 这个例子演示如何用各种标签写 javadoc 注释
 * @author Andrew Valentine
 * @version 1.0
 */
public class JavaDocExample {
    /** 把两个整数相加
     * @param a addNum 的第一个参数
     * @param b addNum 的第二个参数
     * @return int 返回 a 与 b 的和
     */
    public int addNum(int a, int b) {
        return a + b;
    }

    /** 主方法, 调用 addNum
     */
    public static void main(String[] args) {
        JavaDocExample jd = new JavaDocExample();
        System.out.println("Sum of numbers is: " + jd.addNum(1, 2));
    }
}

12.5设计原则总览

◆ 这些原则从哪来

到目前为止你已经学了一堆面向对象技术:封装与信息隐藏、继承/接口/多态、类关系建模(UML)、设计模式。课件强调:它们不是孤立的知识点,而是同一批"更宏观的设计原则"的不同应用方式

而所有原则都在回答同一个核心问题

★ 核心问题

怎样组织代码,让它更易理解、易修改、易测试、易复用

● 本章会讲的八条原则

模块化 Modularity、内聚 Cohesion、耦合 Coupling、抽象 Abstraction、封装 Encapsulation、多态 Polymorphism、委托 Delegation、开闭原则 Open-Closed。下面逐个看,重点放在前三条和最后一条(高频考点)。

12.6五个易混概念对照表

这几个词长得像、经常一起出现,但各自回答不同的问题。这张表是本章最容易考"概念辨析"的地方,建议直接背下来。

★ 它们各自回答什么问题
概念它回答的问题
模块化 Modularity怎么把程序拆成几个独立的部分?
内聚 Cohesion每个部分是不是只有一个清晰的目的?
耦合 Coupling各部分之间互相依赖得有多深?
委托 Delegation这件事该由哪个对象去做?
封装 Encapsulation怎么保护一个对象的内部细节?
◆ 用一个 Player 类把五个词串起来
  • Player 是一个模块
  • 如果它只管玩家的状态和行为 → 高内聚
  • 如果它让 GameRenderer 去画自己 → 这是委托
  • 如果它的字段都是 private → 这是封装
  • 如果它只依赖小而简单的接口 → 低耦合

12.7模块化 Modularity

◆ 定义

模块化:把问题分解成一个个单元(模块),让每个单元都易于理解、管理和复用。

  • 在面向对象里,类就是最基本的模块
  • 每个类应该有一个清晰的目的
  • 再通过"类关系"把这些类组合起来,解决更大的问题。
public class Player { ... }
public class Enemy  { ... }
public class Level  { ... }
public class Game   { ... }
● 类比

像搭乐高:与其造一整块没法拆的塑料,不如做成一颗颗标准积木——每颗职责单一,还能反复拼到不同作品里。

12.8内聚 Cohesion

★ 定义(关键词)

内聚 Cohesion:模块应当被设计成解决清晰、聚焦的问题。好的设计应该高内聚(high cohesion)

低内聚 = 一个类承担了太多不相关的职责。

⚠ 低内聚的反例
public class Player {
    private int health;
    public void move()        { ... }   // 玩家行为 ✓
    public void attack()      { ... }   // 玩家行为 ✓
    public void drawOnScreen(){ ... }   // 画图 —— 不该归玩家管
    public void saveToFile()  { ... }   // 存档 —— 也不该归玩家管
}

这个 Player 又管自己、又管画图、又管存档,三件不相关的事塞一个类,就是低内聚。

◆ 改成高内聚:一个类一个职责
public class Player {
    private int health;
    public void move()   { ... }
    public void attack() { ... }
}
public class GameRenderer {
    public void drawPlayer(Player player) { ... }
}
public class SaveManager {
    public void savePlayer(Player player) { ... }
}

把"画图"交给 GameRenderer、"存档"交给 SaveManager,每个类目的更清晰。

12.9耦合 Coupling

★ 定义(关键词)

耦合 Coupling:模块之间的相互作用程度应尽量降低。好的设计应该低耦合(low coupling)

⚠ 高耦合:Game 直接依赖某个具体控制器
public class Game {
    private KeyboardController controller = new KeyboardController();
}

这里 Game 把自己钉死在 KeyboardController 上——想换成 AI 控制器或测试用的假控制器,就得改 Game 内部代码。

◆ 低耦合:Game 依赖更通用的接口
public class Game {
    private Controller controller;
    public Game(Controller controller) {   // 从外面传进来
        this.controller = controller;
    }
}

Game 只认 Controller 这个"通用类型",不在乎具体是谁。这样无论是键盘控制器、AI 控制器还是测试控制器,都能直接塞进去用——这正是第 10 章"依赖 Dependency"和构造注入的设计动机

● 一句话记牢内聚 vs 耦合

内聚要高(一个类只干一类事),耦合要低(类之间别缠太死)。两者方向相反,常一起考。

12.10抽象 / 封装 / 多态 / 委托

◆ 抽象 Abstraction

抽象:通过给真实概念建立简化模型来解决问题。比如 Player 类不会去模拟一个真人的所有细节,只建模"游戏需要的那部分"。

public class Player {
    private int health;
    private int x, y;
    public void move(int dx, int dy)   { ... }
    public void takeDamage(int amount) { ... }
}
◆ 多态 Polymorphism

多态:通过一个共享类型来使用不同的对象类型(呼应第 4 章)。下面的循环不需要知道具体是哪种敌人,每个对象自己决定怎么 update

ArrayList<Enemy> enemies = new ArrayList<>();
enemies.add(new Bug());
enemies.add(new Robot());
for (Enemy enemy : enemies) {
    enemy.update();   // 各自表现各自的行为
}
● 封装与委托(前面已讲过,这里作为设计原则再点名)
  • 封装 Encapsulation:把字段设为 private,保护对象内部细节(见 3.12)。
  • 委托 Delegation:把某件工作交给"更合适的那个对象"去做,而不是自己硬扛(见 3.14)。例:PlayerGameRenderer 来画自己。

12.11开闭原则 Open-Closed

★ 定义(关键词)

开闭原则 Open-Closed Principle:模块应当对扩展开放,对修改关闭

意思是:尽量通过写新代码来添加新行为,而不是去改已经能正常工作的旧代码

◆ 在 Java 里通常怎么做
  • 用一个共享类型(接口 interface 或父类 superclass)。
  • 把不同的行为放进不同的类
  • 多态,让主代码可以统一处理多种对象类型。
⚠ 重点:开闭原则 ≠ 继承

课件特别强调:开闭原则不等于继承。继承只是实现它的一种可能工具;开闭原则是设计目标。用接口同样能达到(下面的例子就是用接口)。

⚠ 违反开闭:加一种狗就要改这个方法
public void makeDogSound(String dogType) {
    if (dogType.equals("Poodle")) {
        System.out.println("yap");
    } else if (dogType.equals("Bulldog")) {
        System.out.println("woof");
    }
}   // 每加一种狗, 就得回来改这串 if-else
◆ 符合开闭:每种狗自带自己的行为
public interface Dog {
    void makeSound();
}
public class Poodle implements Dog {
    public void makeSound() { System.out.println("yap"); }
}
public class Bulldog implements Dog {
    public void makeSound() { System.out.println("woof"); }
}

主代码只认共享类型 Dog,不认每个具体的狗类:

ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Poodle());
dogs.add(new Bulldog());
for (Dog dog : dogs) {
    dog.makeSound();
}

要加一种新狗,只需新建一个类,循环一行都不用改:

public class Husky implements Dog {
    public void makeSound() { System.out.println("awoo"); }
}
● 收尾:设计是"有理由的选择"

课程补充课件提醒:软件设计往往没有唯一正确答案,同一个需求可以有好几种合理设计。关键不是背"标准答案",而是能用本章这些原则(高内聚、低耦合、开闭……)为自己的设计讲出合理的理由。这一点也是可考的

Chapter 13 · Design Patterns

设计模式 Design Patterns

设计模式是前人把"反复出现的设计问题"的好解法记录下来,让你直接复用,而不用每次从零造轮子。这一章讲清楚:设计模式是什么、怎么"读懂"一个模式的规格说明,以及课程要求掌握的五个具体模式——单例、工厂方法、模板方法、策略、观察者。这正是你 Project 2b(Shadow Aliens)里实打实用过的五个,所以这章可以边对照自己的代码边看。

13.1设计模式是什么 + 怎样成为好设计师

◆ 成为好设计师的三步
  • 第一步:学编程基础(编程语言、算法与数据结构)。
  • 第二步:学设计范式与原则(结构化设计、面向对象设计——就是第 12 章那些)。
  • 第三步研究并模仿有经验的设计者的设计。好的设计者会复用解法,而不是每样东西都从头设计。
★ 定义

设计模式(Design Pattern):把反复出现的设计解法系统地记录下来,以便能被反复复用。今天已经存在成千上万个设计模式,本章只挑课程要求的五个常见模式来讲。

● 类比

像做菜的"菜谱"。前人把"番茄炒蛋"的步骤写下来,你照着做就行,不用每次自己摸索鸡蛋该先下还是后下。模式不是现成代码,而是解决某类问题的套路——你还得按自己的食材(具体需求)去落地。

13.2模式规格说明:怎么读懂一个模式

课程明确要求你能"分析并理解设计模式的规格说明(specification)"。每个模式发布时通常用下面这套固定栏目来描述。考试可能给你一个模式让你按这些栏目分析,建议记住每栏在回答什么。

★ 模式说明的九个栏目
栏目回答什么
Intent 意图这个模式的目标、它为什么存在
Motivation 动机一个能体现"需要它"的具体场景
Applicability 适用性一般在什么情况下可以用它
Structure 结构图形表示,通常是 UML 类图
Participants 参与者模式里有哪些类/对象,各扮演什么角色
Collaboration 协作这些对象之间怎么交互
Consequences 后果使用后的结果、副作用、权衡(tradeoffs)
Implementation 实现用这个模式"解决一个问题"的示例
Known Uses 已知用途真实世界里用到它的具体例子

13.3三大类模式总览

模式按"解决哪一类问题"分成三大类。先有这张地图,下面五个模式各属于哪类就清楚了。

★ 三大类
类别解决什么问题例子
创建型 Creational对象的创建有关的解法SingletonFactory Method
结构型 Structural类的结构和关系有关Adapter、Bridge
行为型 Behavioural类之间的交互有关StrategyTemplate MethodObserver
● 本章五个模式的归属

单例、工厂方法 → 创建型;策略、模板方法、观察者 → 行为型。(Adapter、Bridge 这两个结构型只是举例,本章不展开。)

13.4单例 Singleton

★ Intent 意图

保证一个类只有一个实例,并提供一个全局访问点。当全程只想要"某样东西的唯一一份"时用它。

● 类比

"维多利亚州州长(Premier)同时有几个?"——永远只有一个。再比如打印机的任务队列管理器、缓存管理器,全系统只该有一份。

◆ 实现:三件套(私有构造 + 静态实例 + getInstance)
public class Singleton {
    private static Singleton instance = null;   // 唯一实例, 静态
    private String name;

    private Singleton() {        // 构造函数私有! 外部无法 new
        name = "George";
    }

    // 保证全程只有 1 个实例
    public static Singleton getInstance() {
        if (instance == null) {              // 第一次调用才创建
            instance = new Singleton();
        }
        return instance;                     // 之后都返回同一个
    }

    public String getName() { return name; }
}
  • 构造函数设为 private——这是关键,挡住外部用 new 乱造。
  • 静态字段 instance 存唯一那一份。
  • getInstance() 是全局访问点:没有就造,有就直接返回旧的。
◆ Collaboration 协作:别人怎么用它
x = Singleton.getInstance();
y = Singleton.getInstance();
System.out.println(x == y);   // true —— x 和 y 指向同一个对象

无论调用几次 getInstance(),拿到的都是同一个对象,所以 x == ytrue(呼应第 2/3 章:== 比的是"是不是同一个对象")。

● 对照你的项目(GameConfig)

Shadow Aliens 里的 GameConfig 就是单例:私有构造 + 静态 instance + getInstance(),任何地方都能拿到同一份游戏配置。它还加了一个 init() 做一次性初始化,没初始化就调用会抛 IllegalStateException 提醒开发者——这是工程上的常见加固。

⚠ Consequences:谨慎使用

课件提醒:单例要小心用,滥用会导致糟糕的设计(它本质是个"全局变量",用多了会让代码到处隐式依赖它,反而提高耦合)。

13.5工厂方法 Factory Method

★ 一句话理解

不要在通用类里直接用 new 创建对象;而是调用一个专门负责"造出正确对象"的方法

// 不这样:
players.add(new RPGPlayer());
// 而是这样:
players.add(createPlayer());   // 由子类决定 createPlayer() 造哪种 Player
◆ 三个参与者(关键词)
  • Factory 工厂:制造(创建)对象的通用技术。
  • Product 产品:一个抽象类,泛化"工厂要造出来的那些对象"。
  • Creator 创造者:一个抽象类,泛化"会消费/生产产品的那些对象";通常有个操作(如构造函数)会去调用工厂方法。
◆ 例子(结构)

抽象 Game 声明 createPlayer(): PlayerRPGGameShooterGame 各自重写它,分别造出 RPGPlayerShooterPlayerGame 的通用代码只认抽象的 Player,不在乎具体造的是哪种。

abstract class Game {
    public abstract Player createPlayer();   // 工厂方法
    public void operation() {
        Player p = createPlayer();            // 用产品, 但不关心是哪种
        ...
    }
}
class RPGGame extends Game {
    public Player createPlayer() { return new RPGPlayer(); }
}
class ShooterGame extends Game {
    public Player createPlayer() { return new ShooterPlayer(); }
}
● 为什么要这么绕(动机)
  • 直接到处 new 具体类会造成大量重复代码、让类变得僵硬脆弱,而且"决定造哪个对象"本来就不该是大多数类的主要职责。
  • 工厂方法的好处:把对象创建(及"造哪个"的决策)下放给子类;用工厂方法抽象了创建过程;让父类里的创建逻辑和"具体需要哪个对象"解耦

13.6模板方法 Template Method

★ Intent 意图

在一个操作里定义算法的骨架,把其中某些步骤推迟到子类去实现。它是"用继承做复用"的典型例子。

◆ 思路
  • 标准/不变的算法放在基类里。
  • 会变化的具体实现留给子类(声明为抽象方法)。
◆ 例子:算价格(折扣会变,加 GST 不变)
public abstract class PriceCalculator {
    public double finalPrice(double price) {   // 算法骨架(不变)
        double discounted = applyDiscount(price);
        return addGST(discounted);
    }
    public abstract double applyDiscount(double price);  // 留给子类(变)
    public double addGST(double price) {        // 共享步骤(不变)
        return price * 1.10;
    }
}

finalPrice() 固定了"先打折、再加 GST"的流程;具体怎么打折由各子类去填 applyDiscount()

⚠ Consequences / 权衡

用继承的代价是子类对基类有强依赖(呼应第 12 章:继承耦合较紧)。如果想避免这种强依赖,策略模式(Strategy)是它的替代方案——下一节登场。

13.7策略 Strategy

★ 思路

把"会变化的行为"抽成一个独立对象,通过委托(delegation)来用它:把一个"代表某种行为的对象"传进来,主类把这部分行为委托给它去做。这是"用组合做复用"。

◆ 结构(参与者)
  • Strategy:一个接口,声明可替换的算法,如 algorithm()
  • ConcreteStrategyA / B:实现该接口的不同算法。
  • Context:持有一个 Strategy 引用,在 contextMethod() 里把活儿交给它。
public interface DiscountStrategy {
    double applyDiscount(double price);
}
// 不同折扣 = 不同实现类, 都实现这个接口
// Context 持有一个 DiscountStrategy, 需要打折时就调它
● 对照你的项目(MovementStrategy)+ 类比

Shadow Aliens 里敌人的移动就是策略模式:MovementStrategy 是接口,StraightDownMovementStrafingMovement 是不同算法,敌人持有一个 MovementStrategy 并把"怎么动"委托给它,想换移动方式只要换传进来的策略对象。
类比:去餐厅点餐,你(Context)不自己做菜,而是把"做菜"这件事委托给厨师(Strategy);想换口味就换个厨师,你自己一行不用改。

13.8模板方法 vs 策略:继承 vs 组合

这两个是行为型模式里最容易混的一对,因为它们解决的是同一类问题(让"会变的行为"可替换、可复用),只是用了两种不同机制。这是高频考点,务必分清。

★ 核心对照
模板方法 Template Method策略 Strategy
复用机制继承(inheritance)组合 / 委托(composition/delegation)
变化的部分放在子类重写父类的抽象方法一个独立的策略对象里
关系子类 is-a 父类Context has-a Strategy
换行为的方式写一个新子类换一个传入的策略对象(可运行时切换)
代价子类与父类强耦合耦合更松,但类更多一点
● 一句话记牢

模板方法是"父类定骨架、子类填空"(靠继承);策略是"把行为塞进一个对象、需要时委托给它"(靠组合)。课件原话:模板方法用继承复用,策略是它避免强依赖的替代方案

13.9观察者 Observer

◆ 两个参与者(关键词)
  • Subject 主题:一个"重要"的对象,它的状态(或状态变化)决定了其他类要做什么。
  • Observer 观察者:监视 Subject 的对象,以便对它的状态及任何变化做出响应。
● 类比:YouTube 订阅

UP 主(Subject)发了新视频,所有订阅者(Observer)就自动收到通知,各自做各自的反应(点开看 / 点赞 / 忽略)。UP 主不需要知道每个订阅者是谁、要干嘛,只管"广播"一声。观察者模式就是这个"发布—订阅"机制。

◆ Java 内置版(java.util)示例
public class Player extends Observable {       // Subject
    public Player(String name, ArrayList<Observer> observers) {
        for (Observer o : observers) addObserver(o);  // 登记观察者
        setChanged();                                  // 标记"我变了"
        notifyObservers("Player created: " + name);    // 广播通知
    }
}
public abstract class Enemy implements Observer {   // Observer
    public abstract void update(Observable o, Object arg);
}

每个观察者实现 update(...),被通知时执行各自的响应。课件也指出:很多时候让 Subject 自己写成一个接口更合适,而不是用 Java 自带的 Observable(它已被官方弃用)。

◆ 对照你的项目(ScoreManager / ScoreObserver)—— 自己写接口的版本
// Observer 接口: 谁想在分数变化时被通知, 就实现它
public interface ScoreObserver {
    void onScoreChanged(int newScore);
}
// Subject: 持有分数, 一旦变化就通知所有登记的观察者
public class ScoreManager {
    private int score = 0;
    private final ArrayList<ScoreObserver> observers = new ArrayList<>();

    public void addObserver(ScoreObserver o) { observers.add(o); }

    public void add(int points) {
        score += points;
        if (score < 0) score = 0;
        notifyObservers();                 // 状态变了 → 广播
    }
    private void notifyObservers() {
        for (ScoreObserver o : observers) o.onScoreChanged(score);
    }
}

这就是课件建议的"自己写 Subject 接口"的做法:ScoreManager 是 Subject,任何想在分数变化时反应的类(如 UI 显示、成就系统)实现 ScoreObserver 注册进去即可——典型的低耦合,新增观察者不用改 ScoreManager(呼应开闭原则)。

● 收尾

这五个模式(单例、工厂方法、模板方法、策略、观察者)就是 Project 2b 要求的五个。考试既可能考概念(属于哪一类、解决什么问题、参与者是谁),也可能给段代码让你判断用了哪个模式、或按 13.2 的栏目分析它。把每个模式的"意图 + 一句话机制 + 一个例子"记牢就够应付。

Chapter 14 · Software Testing

软件测试 Software Testing

写完代码怎么知道它真的对?靠"运行一下看着没报错"是不够的。这一章讲单元测试(unit testing):把程序拆成一个个小单元,给每个单元喂各种输入、检查输出对不对。课程用的工具是 JUnit5。重点是三件事:三个 unit 关键词的区分、怎么设计测试用例(有效/无效/边界)、以及 JUnit5 的断言方法和写法。

14.1三个关键词:Unit / Unit Test / Unit Testing

★ 三个长得像但不同的词
关键词含义
Unit 单元软件系统里一个小而明确的组件,只有一个(或少数几个)职责。通常就是一个方法或一个类。
Unit Test 单元测试(一个)通过测一个单一用例(输入/输出)来验证一个单元的行为,抱着"让它失败"的心态去测。
Unit Testing 单元测试(活动)通过给每个单元都套上一整套测试来找出软件里的 bug。
● 为什么是"抱着让它失败的心态"

测试的目的不是"证明我的代码是对的",而是"努力找出它哪里错"。如果你只挑那些肯定会通过的输入来测,等于没测。好的测试者会主动去想"什么输入能把它搞崩"。

● 类比

像验收一台新车:unit 是"刹车系统"这个部件;一个 unit test 是"踩一脚看能不能停"这一次具体测试;unit testing 是把刹车、转向、灯光、空调……每个部件都测一遍的整个验收流程。

14.2测试用例:有效 / 无效 / 边界输入

★ 一个方法至少要测两类输入
  • 有效输入(valid input):正常、合法的输入,验证它给出正确结果。
  • 无效输入(invalid input):非法、越界、异常的输入,验证它能正确地拒绝或处理。

光说"测有效和无效"还不够具体——真正要覆盖的是各种边界情况(edge cases)

◆ 测试套件要多少个测试?
  • 一个全面(comprehensive)的测试套件,测试数量是"需要多少就写多少",没有固定数字
  • 有些方法可能需要 8 个以上的单元测试,才能覆盖各种边界情况。
⚠ 常见误区

不要以为"每个方法写一个测试"就够了。一个 isValidMove 方法,光"越界"就有上、下、左、右至少四种边界(见 14.7),每种都该单独测。考试若问"这个测试套件够不够全面",要往边界和无效输入上想。

14.3手动测试 vs 自动测试

★ 两种方式对照
手动测试 Manual自动测试 Automated
怎么测人工、临时(ad-hoc)地测用专门写好的测试软件来测
边界覆盖很难覆盖到所有边界情况容易反复跑、覆盖全
可扩展性大项目里不可扩展更快、更可靠、更少依赖人
● 为什么要自动化

手动测试每改一次代码就要人重新点一遍,几百个用例根本测不过来,还容易漏、容易累出错。自动化测试写一次、随时一键全跑,这正是 JUnit 这类框架存在的意义。

14.4JUnit5 入门:导入 + @Test + 命名

◆ JUnit 是什么
  • JUnit 是 Java 最流行的测试框架,主要用于单元测试——隔离地测试单个单元/组件。
  • 它提供一套标准化的方式来写和执行测试,让开发者更容易确认代码行为符合预期。
  • SWEN20003 用的是 JUnit5(2017 年发布)。
★ 两个必背导入 + 注解
import org.junit.jupiter.api.Test;              // 提供 @Test 注解
import static org.junit.jupiter.api.Assertions.*;  // 提供 assertXxx 方法 (静态导入)

每个测试方法上面加 @Test 注解,JUnit 就会把它当成一个测试自动运行。注意第二行是 import static——这样才能直接写 assertEquals(...) 而不用写 Assertions.assertEquals(...)

★ 两个关键概念
  • assert(断言):一个真/假的陈述,用来指示一个测试用例是成功还是失败。
  • TestCase 类:专门测试某一个单元的类。命名约定:被测类名 + Test。例如测 Board 类,测试类就叫 BoardTest

14.5断言方法 Assertion Methods

JUnit5 提供很多断言方法,还有接收不同参数类型/数量的变体。下面这几个是必须记住的核心:

★ 五个核心断言
方法断言什么
assertEquals(expected, actual)expected 与 actual 相等
assertNotEquals(expected, actual)两者不相等
assertTrue(condition)condition 为 true
assertFalse(condition)condition 为 false
assertArrayEquals(expected, actual)两个数组相等
⚠ 参数顺序别写反

assertEquals(expected, actual)——第一个是"期望值",第二个才是"实际跑出来的值"。写反了虽然通过/失败的结果一样,但失败信息里的 expected/actual 会颠倒,排查时容易被误导。

14.6完整示例:BoardTest

◆ 一个测试类长什么样
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

public class BoardTest {
    @Test
    public void testBoard() {
        Board board = new Board();
        Move move = new Move(0, 0);
        assertEquals(true, board.cellIsEmpty(move));   // 新棋盘格子应为空
    }

    @Test
    public void testValidMove() {
        Board board = new Board();
        Move move = new Move(0, 0);
        assertEquals(true, board.isValidMove(move));   // (0,0) 是合法落子
    }

    @Test
    public void testMakeMove() {
        Board board = new Board();
        Player player = new HumanPlayer("R");
        Move move = new Move(0, 0);
        board.makeMove(player, move);
        assertEquals("R", board.getBoard()[move.row][move.col]);
    }
}

每个方法是一个独立测试:建好对象 → 执行操作 → 用断言检查结果。三个方法分别测"格子是否为空""落子是否合法""落子后棋盘内容对不对"。

14.7测试自己也会有 bug + 边界用例

⚠ 重点:测试代码本身也可能写错

上面的 testMakeMove 其实埋了个坑。课件最初写的是:

assertEquals("r", board.getBoard()[move.row][move.col]);  // bug! 应是 "R"

玩家是用 "R"(大写)建的,但断言里期望值写成了小写 "r",于是测试会失败——不是程序错了,是测试本身写错了

课件两句关键结论:
  • 自动化测试既能测你的程序,也同样能"测出你的测试套件"本身的问题
  • 单元测试必须准确地编写,这是开发者的责任
◆ 边界用例:落子落到棋盘外

题目:写一个测试,验证当落子落在棋盘外isValidMove 返回 false。课件指出这至少有四个测试用例(行/列越界的上下左右各一种),这里是其中一个:

@Test
public void testValidMove2() {
    Board board = new Board();
    Move move = new Move(-1, 0);        // 行 = -1, 越出上边界
    assertEquals(false, board.isValidMove(move));
}

这正好印证 14.2:一个方法往往需要多个测试覆盖各个边界,绝不是"一个方法一个测试"。

● 本章小结

记牢三个 unit 关键词的区分、"valid/invalid/边界"用例设计、JUnit5 的两行 import + @Test + TestCase 命名约定、五个断言方法及 assertEquals(expected, actual) 的参数顺序,以及"测试本身也会有 bug、写准确是开发者责任"这个考点,就覆盖了本章主要内容。

Chapter 15 · Event-Driven Programming

事件驱动编程 Event-Driven Programming

到目前为止你写的程序基本都是"从上往下一行行按顺序执行",遇到 if、循环、方法调用就按你写的逻辑决定走向。事件驱动编程是另一种完全不同的风格:程序不再自己决定"接下来做什么",而是等着各种事件发生(鼠标点击、按键、触摸……),事件来了再触发对应的响应。这一章讲清楚它的核心概念,并对照你 Shadow Aliens 里 Bagel 的写法。

15.1什么是事件驱动编程

★ 定义

事件驱动编程(Event-Driven Programming):用事件(events)回调(callbacks),根据程序状态的变化来控制程序执行的流程。

◆ 和你以前写的程序有什么不同
  • 以前:程序是一串按顺序执行的语句;要不要重复(循环)、要不要跳转、要不要调用某个方法,都由程序自身的逻辑控制。
  • 现在:接下来发生什么,取决于下一个事件。程序不再决定事情发生的顺序,而是由事件来决定顺序。
● 它是"信号—响应"式、本质异步的

这种风格也叫"信号与响应(signal-and-response)"。它本质是异步(asynchronous)的——程序员要处理的是"在未知时刻冒出来的事件",你不知道用户什么时候会点按钮、按哪个键。这种抽象通常由开发框架提供,比如 Java 的 GUI 框架 JavaFX、各种 Web 开发框架。

● 类比

普通程序像"念剧本":从第一句念到最后一句,顺序固定。事件驱动像"前台接待":你坐在那儿什么都不主动做,谁来按铃(事件)你才起身处理谁,铃响的顺序你说了不算。

15.2三个关键词:State / Event / Callback

★ 三个核心定义
关键词含义
State 状态定义一个对象或设备的属性;例如它是否处于"激活"状态。
Event 事件当一个对象/设备等的状态被改变时产生。
Callback / Listener 回调 / 监听器由事件触发的一个方法。
◆ 关于监听器的几条规则
  • 一个组件可以有任意多个监听器。
  • 每个监听器可以响应不同种类的事件;也可以多个监听器响应同一个事件。
  • 监听器必须事先向事件生成者注册(register in advance)——不注册就收不到通知。

15.3事件的流动 + 控制反转

◆ 事件怎么走
  • 发出一个事件叫"触发事件(firing the event)"。
  • 事件处理器(event handler)是监听器里的一个方法,规定"收到某类事件时该做什么"。
  • 整体流程:能触发事件的对象被创建出来;能对事件做出反应的监听器对象也被创建出来;事件一来,对应的 handler 自动执行。
★ 最反直觉的一点:控制反转

在事件驱动里,有些方法你写了,但你自己的代码里从来不会去显式调用它们。相反,当某个事件发出"该调用这个方法了"的信号时,它会被自动调用

● 为什么这叫"控制反转"

平时是"你的代码调用别人的方法";现在反过来,是"框架在合适的时机回头调用你写的方法"。所以这些方法叫"回调(callback)"——框架"回过头来呼叫"你。这也是为什么你常常只需要"写好 handler、注册上去",剩下的交给框架。

15.4为什么用它(优点)

★ 事件驱动的好处
  • 更好地封装类——把类的行为隐藏起来。
  • 不用显式传递输入信息——这些信息会作为回调的一部分自动传进来(比如点击事件里自带"点了哪儿")。
  • 很容易给类增删行为
  • 很容易增删额外的响应(多挂一个监听器就多一个反应,互不干扰)。
● 真实世界的例子

图形界面(GUI)、Web 开发 / JavaScript、嵌入式系统 / 硬件。事件驱动是一项很强大的技术,通常通过软件框架来支持(如 JavaFX)。

15.5例子:JavaFX 按钮

● 说明

课程用一个简单的 JavaFX GUI 来演示事件驱动这个范式。你不需要会写 JavaFX,这个例子只是用来展示"事件驱动"长什么样。需求:显示一个窗口和一个 "Stop" 按钮,点击它就在控制台打印 I am the stop button.

◆ 第一步:写事件处理器(handler)
import javafx.event.ActionEvent;
import javafx.event.EventHandler;

public class StopButtonHandler implements EventHandler<ActionEvent> {
    @Override
    public void handle(ActionEvent actionEvent) {
        System.out.println("I am the stop button.");
    }
}

注意:handle() 这个方法你自己永远不会去调用它。它就是个 callback——按钮被点时框架自动调它。这正是 15.3 说的控制反转。

◆ 第二步:创建按钮并"注册"处理器
public class DisplayButton extends Application {
    private Button stopButton;
    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("Button Demo");

        stopButton = new Button();
        stopButton.setText("Stop");
        StackPane stackPane = new StackPane();
        stackPane.getChildren().add(stopButton);

        stopButton.setOnAction(new StopButtonHandler());  // ★ 注册监听器

        primaryStage.setScene(new Scene(stackPane, 300, 275));
        primaryStage.show();
        System.out.println("Displaying window");
    }
    public static void main(String[] args) { launch(args); }
}

关键就是 setOnAction(new StopButtonHandler()) 这一行——把 handler注册到按钮上(呼应 15.2"监听器必须事先注册")。注册完,主程序就 show() 完事了;之后什么时候打印那句话,完全由用户点不点决定

15.6对照你的项目:Bagel 的 update 轮询

◆ Bagel 怎么处理输入

你做 Shadow Aliens 用的 Bagel 框架,处理输入的方式和 JavaFX 略有不同,但思想一脉相承。Bagel 每秒约 60 次自动做三件事:清屏 → 检查键盘/鼠标输入 → 调用你的 update(Input input) 方法。

@Override
public void update(Input input) {
    if (input.isDown(Keys.DOWN)) {        // 持续按住
        y += speed;
    }
    if (input.wasPressed(Keys.ESCAPE)) {  // 这一帧刚按下
        Window.close();
    }
}
● 两种风格:回调 vs 轮询
  • JavaFX = 纯回调(callback):你只写 handler 并注册,框架在事件发生时回调你。
  • Bagel = 轮询(polling):框架每帧把 Input 传给你的 update(),你主动去问"这一帧某个键按了吗"。

共同点都是事件驱动的核心:你的 update() 不是你自己调的,是框架每帧自动回调的——同样是控制反转。你项目里 handleBattleState 里那些 input.wasPressed(Keys.ESCAPE) 转去暂停、Keys.R 重开,就是在 update 这个被回调的方法里,根据按键事件改变游戏状态(state)。

Chapter 16 · Advanced Java & OOP

进阶 Java:枚举 / Lambda / 流 Advanced Java

最后一章是几样让 Java 代码更简洁、更安全的"进阶工具":枚举(enum,固定取值的类型)、可变参数、函数式接口、Lambda 表达式、方法引用,以及流(Stream,对集合做流水线式处理)。这些不是必须用的语法糖,但用好了能让代码短一大截、读起来更清楚。考试会考概念和读代码,所以重点放在"它是什么、解决什么问题、一段典型代码怎么读"。

16.1枚举 enum

★ 定义

enum(枚举):一个由有限个常量组成的类。任何时候你需要表示一组固定的取值,就用它。

  • 必须列出所有可能的值
  • 除此之外,它和普通的类一样——可以有方法和属性!
● 为什么不用普通常量 / 字符串

如果用字符串表示扑克牌点数,"ACE" 拼错成 "AEC" 编译器不会报错,运行时才出 bug。用 enum,写错 Rank.AEC当场编译报错,而且取值被限死在你列的那几个里,更安全。你项目里的 GameState(START / BATTLE / PAUSE / END)就是个 enum。

◆ 声明一个枚举
public enum Rank {
    ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN,
    EIGHT, NINE, TEN, JACK, QUEEN, KING
}
◆ 怎么用:静态访问
Rank rank = Rank.ACE;
Card card = new Card(Rank.FOUR, ...);

枚举的值通过静态方式访问Rank.ACE),因为它们是常量。枚举对象当成普通对象用即可。

★ 枚举自带四样东西
  • 默认构造函数
  • toString()(打印出常量名字,如 ACE
  • compareTo()(可比较,能直接排序)
  • ordinal()(返回该常量在声明列表里的序号,从 0 开始)

枚举也是类,所以你能给它加或重写任何方法/属性:

public boolean isFaceCard() {
    return this.ordinal() > Rank.TEN.ordinal();  // 比 TEN 靠后的是花牌
}
⚠ 自然顺序 = 声明顺序

Java 里枚举的"自然顺序"由它们在定义里出现的先后决定(也就是 ordinal() 的大小)。所以排序结果跟着声明顺序走:

ranks = [TEN, FOUR, EIGHT, THREE, ACE];
Collections.sort(ranks);
// → [ACE, THREE, FOUR, EIGHT, TEN]   按声明顺序 ACE..KING 排

考点:排序后的顺序不是字母序,而是你在 enum 里写的那个顺序。

16.2可变参数 Variadic Parameters

★ 定义

可变参数方法(Variadic Method):一个能接收不确定个数参数的方法。它会隐式地把传进来的参数转成一个数组

◆ 语法:类型后加 ...
public int sum(int... numbers) {   // numbers 实际是 int[]
    int total = 0;
    for (int n : numbers) total += n;
    return total;
}
sum(1, 2);          // 传几个都行
sum(1, 2, 3, 4, 5); // 不用为每种个数写一个重载

没有它的话,你得为"2 个参数""3 个参数"……分别写一堆重载方法,很蠢。可变参数一行搞定。main(String[] args) 你早就见过类似的"一组参数"思想。

16.3函数式接口 Functional Interface

★ 定义

函数式接口(Functional Interface)只含一个抽象方法的接口,也叫"单一抽象方法接口(Single Abstract Method, SAM)"。

◆ 例子 + 规则
@FunctionalInterface
public interface Attackable {
    void attack();          // 唯一的抽象方法
}
  • 只能有一个非静态(抽象)方法,再加一个就报错。
  • 可以有多个 default 或 static 方法,但抽象方法只能有一个。
  • 接口里的方法默认就是 abstract不用显式写 abstract,Java 自动推断。
  • @FunctionalInterface 注解是个"保险":万一你不小心加了第二个抽象方法,编译器会报错提醒。
● 它有什么用 + 已学过的例子

函数式接口定义了一个清晰的"单一动作"契约,特别适合"把行为当参数传"的场景:① 回调机制(第 15 章);② 策略模式(第 13 章,每个策略就一个方法);③ Stream API(下面会用到的 PredicateFunctionConsumer)。常见的内置函数式接口有 RunnableCallableComparator(第 9 章排序用过!)。

16.4Lambda 表达式

★ 定义

Lambda 表达式:一种把"代码当成数据"、能像"对象"一样使用的技术;比如让我们不用写实现类就能"实例化"一个接口

◆ 语法
(参数1, 参数2, ...) -> (对这些参数做的操作)

它接收零个或多个参数,对它们施加一个操作。例子:

Predicate<Integer> p = (i) -> (i > 0);

这里 Predicate<Integer> 是个函数式接口(它的抽象方法是"测试一个值返回 true/false")。这一行就地造出一个对象,实现了"测试整数是否大于 0"的功能——没有单独写一个 implements Predicate 的类

★ Lambda 和函数式接口的关系(重点)

Lambda 表达式就是函数式接口的实例。正因为函数式接口"只有一个抽象方法",Java 才知道这个 lambda 要去实现哪个方法——这就是为什么 lambda 只能配函数式接口用。它让代码更简洁、更易读

● 类比

以前要"实现一个接口",你得郑重其事新建一个类、写 @Override、起名字……像为了寄一句话专门盖栋邮局。Lambda 就是"一张便利贴":把那句话(那段逻辑)直接写下来递过去,不用建邮局。

16.5方法引用 Method References

★ 定义

方法引用(Method Reference):一个"存着某个方法"的对象;当一个 lambda 只是用来调用单独一个方法时,可以用方法引用代替它,写起来更短。

◆ 语法:类名::方法名
// 这两行完全等价:
UnaryOperator<String> op = s -> s.toLowerCase();   // lambda
UnaryOperator<String> op = String::toLowerCase;    // 方法引用(更简洁)

names.replaceAll(String::toUpperCase);  // 把列表里每个字符串转大写

判断能不能用方法引用的标准很简单:这个 lambda 是不是"只干一件事——调用某个已有方法"?是的话就能换成 :: 形式。

16.6流 Streams

★ 定义

流(Stream):一种强大的 Java 技术,让你对一组数据(集合)依次施加一连串操作

● 类比:流水线

把集合想成传送带上的一堆零件,Stream 就是一条流水线:第一个工位筛掉不合格的(filter),第二个工位给每个零件喷漆(map),最后一个工位把成品装箱(collect)。数据一站一站流过,每站做一件事。

★ 常见操作
操作作用
map把输入转换成输出(每个元素变个样)
filter条件挑选元素
limit最多迭代多少个就停
collect把所有元素收集成 list / array / String…
reduce把整条流聚合成一个值
◆ 完整示例:挑出 18–40 岁的人,名字转大写,拼成一句
List<Person> people = Arrays.asList(
    new Person("Peter Parker", 18),
    new Person("Black Widow", 34),
    new Person("Thor", 1500),
    new Person("Nick Fury", 67),
    new Person("Iron Man", 49)
);
String output = people.stream()
    .filter(p -> p.getAge() >= 18)     // 留下 ≥18 岁
    .filter(p -> p.getAge() <= 40)     // 再留下 ≤40 岁
    .map(Person::getName)              // 取出名字(方法引用)
    .map(String::toUpperCase)          // 名字转大写(方法引用)
    .collect(Collectors.joining(", ")); // 用 ", " 拼成一个 String
System.out.println(output);
// 输出: PETER PARKER, BLACK WIDOW
◆ 怎么"读"这段链式代码

从上往下,把每一行当流水线的一站:遍历列表 → 选出年龄 ≥18 → 再选出 ≤40(Thor 1500、Nick 67、Iron Man 49 在这两步被筛掉)→ 把人换成名字 → 名字换成大写 → 收集拼接。剩下 Peter(18) 和 Black Widow(34),所以结果是 PETER PARKER, BLACK WIDOW

● 把这一章串起来

看那段 stream 代码:filter 收的 p -> p.getAge() >= 18lambda,它是函数式接口 Predicate 的实例;Person::getName方法引用。所以"函数式接口 → lambda → 方法引用 → stream"是层层叠上来的一套组合拳:函数式接口提供"单方法契约",lambda/方法引用提供"简洁地写出这个方法",stream 提供"把这些操作串成流水线"。

● 本章小结

记牢:enum 是"有限常量的类"(自然顺序=声明顺序、自带 ordinal());可变参数 type... 收成数组;函数式接口=只有一个抽象方法(SAM);lambda 是函数式接口的实例、写法 (args) -> (op);方法引用 类名::方法 替代"只调一个方法"的 lambda;stream 的 map/filter/collect/reduce 及如何逐行读懂链式调用。