课程速览
导论这一讲以课程行政信息为主,下面只保留与备考/学习方向相关的要点。
注:① 第 5、6 章在时间线上位于第 4 章之前;② 第 7 章「接口 Interfaces」在课程时间线上紧跟第 4 章「继承与多态」之后。
0学习目标 · 评分 · 工具
学完本课你应当能够:
- 用 Java 写面向对象程序(Write Object Oriented programs in Java)
- 使用 IDE(Integrated Development Environment,集成开发环境)开发软件
- 用面向对象的方式抽象一个问题规格(Abstract a problem specification)
- 设计一个软件方案,而不只是把它写出来(Design a solution, not just write it)——评分会看你的结构、设计与背后的理由
- 把这些技术 / 框架 / 约定迁移到其他面向对象语言
评分结构(Assessment)
| 项目 | 占比 | 备注 |
|---|---|---|
| Project 1 | 10% | 实现一个游戏(用提供的游戏库),重点在面向对象设计决策,不只是实现 |
| Project 2A | 8% | |
| Project 2B | 12% | |
| Mid-semester Test | 10% | 第 7 周,40 分钟,机考,范围 Week 1–6 |
| Tutorials | 5% | 11 次取最好 8 次 |
| In-lecture Quizzes | 5% | 11 次取最好 8 次,必须 10/10 全对才得分 |
| Final Exam | 50% | 机考,范围 Week 1–12(讲座+tutorial+project) |
总分 ≥ 50%,且 project 部分 ≥ 15/30,且「期中测验 + 期末笔试」合计 ≥ 30/60,三者都满足才算通过本课。
教材与工具
- 教材:Absolute Java,作者 Walter Savitch(第 4 版或更高,非必须购买)。本批内容对应 Ch 1 & 3。
- IntelliJ:从第 2 周起使用的 IDE,Project 必须用 IDE。
- GitHub(学校提供):用于 Project 版本管理。
- ED Lessons:配套练习材料,自定进度,不计分。
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 执行 |
Java 编译器把 .java 源码编译成 字节码(.class 文件)。字节码是一种中间形式(intermediate form),比源码更接近机器表示,但还不是某台具体机器的机器码。
任何目标平台上的解释器(虚拟机 / interpreter)负责把字节码翻译成该平台的机器指令来执行。
② 平台无关、可移植
字节码对所有平台相同;要把 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: 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。public和static关键字后续详解。
System.out.println("Hello World!");
- 作用类似 C 的
printf,但println在 Java 里叫方法(method),而 C 的printf叫函数(function)。 System.out:out是System类里的一个对象;System类定义在java.lang包中。
1.5编译运行 + 命令行参数
编译与运行
- 用文本编辑器写程序,存为
HelloWorld.java。 - 确认机器装好了 Java 构建与运行环境:
javac -version、java -version。 - 编译:
javac HelloWorld.java—— 成功后生成HelloWorld.class。 - 运行:
java HelloWorld—— 输出Hello World!。
javac 文件名.java(编译,带 c,吃 .java)→ 生成 .class;java 类名(运行,不带扩展名)。
命令行参数(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 没有
goto、sizeof、typedef。 - Java 没有结构体
struct和联合体union。 - Java 没有显式的指针类型。
- Java 没有预处理器:没有
#define、#include、#ifdef。 - Java 安全且定义良好:内存由虚拟机管理,而不是由程序员管理。
1.7标识符 Identifiers
标识符(Identifier):唯一标识某个程序元素(类、对象、变量、方法)的名字。
规则(必须遵守)
- 不能以数字开头。
- 所有字符必须是字母、数字或下划线
_。 - 理论上可以任意长。
- 大小写敏感:
Rate、rate、RATE是三个不同的变量。
约定(惯例,不是强制)
- 变量 / 方法 / 对象:小写字母开头,用大写字母标记"单词"边界(小驼峰),如
topSpeed、bankRate、timeOfArrival。 - 类:大写字母开头(大驼峰),如
PrintDemo、HelloWorld。
关键字 / 保留字(如 public class void static)绝对不能用作标识符。
预定义标识符(如 System String println)可以被重新定义,但这样会改变它们的标准含义,既危险又容易造成混淆。
1.8数据类型 Data Types
byte 1、short 2、int 4、long 8 字节;浮点:float 4、double 8 字节。float是单精度,double是双精度;双精度在表示小数时精度更高。- 浮点数默认按 double 处理。
boolean只能取true或false,如boolean x = true;
想强制单精度,必须给数字加后缀 f 或 F:float 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;✅ 合法,但y是 2 不是 3——是截断,不是四舍五入!int intVariable = 2.99;❌;double doubleVariable = 2;✅(int→double 自动拓宽)。int与boolean互相不能赋值。
变量的三种分类
- 实例变量(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) |
当一个操作数是实数、另一个是整数时,叫混合模式算术表达式:只要有一个操作数是实数类型,另一个也会被转成实数,然后做实数运算。
关系运算符
< <= > >= ==(等于) !=(不等于)。
关系运算符的结果类型是 boolean。
逻辑运算符 + 真值表
&& 逻辑与、|| 逻辑或、! 逻辑非。
| op1 | op2 | op1 && op2 | op1 || op2 | !op1 |
|---|---|---|---|---|
| true | true | true | true | false |
| true | false | false | true | false |
| false | true | false | true | true |
| false | false | false | false | true |
位运算符 Bitwise
& 按位与、| 按位或、^ 按位异或、~ 取反(one's complement)、<< 左移、>> 右移、>>> 无符号右移(补零)。
自增自减 + 条件(三元)运算符
- 自增自减:
++和--。 - 条件运算符:
exp1 ? exp2 : exp3。例如x = (a>b) ? a : b;等价于"若 a>b 则 x=a 否则 x=b"。
数学函数 Math 类
Java 用 Math 类(定义在 java.lang 包)提供 cos、sin、log、sqrt 等函数,用法 Math.方法名():
double y = Math.sqrt(x);
double z = Math.cos(y);
1.12流程控制 Flow of Control
流程控制(Flow of Control):Java 中的分支(branching)与循环(looping)机制。大多数分支和循环由 boolean 表达式控制——它只会求值为 true 或 false。
分支 Branching
四种:if-else、多路 if-else(multi-way)、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);循环可嵌套。
- 三种:
while、do-while、for。
while 先判断条件再执行;do-while 把条件放在末尾(} while(...)),所以循环体至少执行一次。
for (Initialize_Expressions; Terminate_Expression; Update_Expressions) {
...
}
- Initialize:第一次迭代前,初始化/声明控制变量。
- Terminate:每次迭代前求值的 boolean 表达式,决定何时结束。
- Update:每次循环体执行后更新控制变量。
break 与 continue
break:退出整个循环(while / do / for)。可配合标签(label)使用,如break loop1;直接跳出指定的外层循环。continue:跳过循环体内剩下的语句,直接进入下一次迭代。
数组与字符串
这一讲两个主题:数组(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数组是引用(别名陷阱)
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
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>];
}
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] |
数组用 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 是一个 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"; // 即使内容是数字,它仍是字符串
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字符串常用方法
| 功能 | Python | Java |
|---|---|---|
| 长度 | len("Hello") | "Hello".length() |
| 大写 | s.upper() | s.toUpperCase() |
| 分割 | s.split() | s.split(" ") |
| 是否包含子串 | "Hell" in s | s.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"; | true | 同样来自常量池 |
String s="Hello"; | true | 两个字面量指向常量池里同一个 "Hello" |
String s="Hello"; | false | new String(...) 在堆内存(heap)里造了一个新对象,引用不同 |
- 字符串常量池:字符串字面量(如
"Hello")放这里,相同内容共享同一个。 - 堆内存 heap:
new String("Hello")每次都新建一个对象。
== 比较的是两个引用是不是指向同一个对象,不是比较内容。所以它对字符串字面量"碰巧"为 true,遇到 new String 就翻车。
要判断两个对象内容是否相等,必须用 .equals 方法(这是每个类都应该有的标准方法):
String s = "Hello";
String s2 = new String("Hello");
System.out.println(s == s2); // false(引用不同)
System.out.println(s.equals(s2)); // true (内容相同)
.equals 定义:用于检查两个对象是否相等的方法。比较内容用 .equals,不要用 ==。
类与对象
全课最核心的一章(原为三讲)。围绕一个贯穿示例——画图板里的 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 六大特性总览
面向对象设计范式的关键特性:数据抽象、封装、信息隐藏、委托、继承、多态。本章覆盖前四个,继承与多态在后续章节。
通过定义新类来创建适合应用的新数据类型的技术。类是一种特殊的、程序员自定义的数据类型,类似 C 的 struct,但多了属性和方法。类的定义决定了对象能含哪些数据(属性)以及能执行哪些动作(方法)。
通过定义类,把数据(属性)和操作这些数据的方法组合到单一实体的能力。这种封装是 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; // 半径
}
上面这些属性叫实例变量:每个类的实例(对象)各自独有的属性。给 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),区别于 int、float 这种原始数据类型(Primitive)。
Circle aCircle; // 没有创建对象!
Circle bCircle;
这两行只是声明了两个引用(reference),它们目前指向 nothing,即 null 引用。
- null:Java 关键字,意思是"这里没有对象"。null 对象不能被访问来取变量或方法,也不能以任何方式使用。
- new:指示 JVM 为对象分配内存,即实例化(instantiate)它。对象在被实例化之前都是 null。
Circle aCircle, bCircle;
aCircle = new Circle(); // aCircle 现在指向一个对象
bCircle = new Circle(); // bCircle 现在指向另一个对象
bCircle = aCircle; // 赋类引用:两个名字指向同一对象
上面 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);
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(); 右边其实是在调用类的构造函数。
定义:用于创建并初始化对象的方法。它的特点:
- 是一种方法,用来初始化对象;
- 名字必须和类同名;
- 不能返回值;
- 一个类可以有一个或多个构造函数,每个参数集不同(这就是重载,见 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
方法同名,靠签名(signature)区分:参数的个数 / 类型 / 位置。任何方法都能重载(构造函数或普通方法)。
重载是多态(polymorphism)的一种形式——同一个方法名、不同行为。
多态定义:根据对象的数据类型/类,不同地处理对象的能力。
重载 Overloading(同名不同签名,本节)≠ 重写 Overriding(继承里子类覆盖父类方法,后续章节)。考试常考区别。
构造函数陷阱 → this 关键字
public Circle(double centreX, double centreY, double radius) {
centreX = centreX; // ❌ 把参数赋给自己, 实例变量没被改
centreY = centreY;
radius = radius;
}
当参数名和实例变量同名,centreX = centreX; 操作的都是那个参数,实例变量根本没被赋值。
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.numCircles、Circle.printNumCircles();。
- 静态方法只能调用其他静态方法。
- 静态方法只能访问静态数据。
- 静态方法不能用
this或super——因为它们关联的是对象(类的实例),而静态方法不依赖具体对象。 - 不要把所有方法和属性都设成 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>;
}
- 用
==做相等判断只会检查引用是否相等,不会检查对象内容是否相等。 - "两个对象怎样才算相等"由你决定——用对象的一个或多个属性来判断。
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>
}
- 是一个参数为同类型的单参数构造函数。
- 为传入的对象创建一个独立的副本:是一个独立对象,但实例变量被设成和参数对象完全一致。
如果某些实例变量是对其他对象的引用,必须用那个对象自己的拷贝构造函数新建一个状态相同的对象——这叫深拷贝(而不是只复制引用)。
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 环境变量中(类似 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
- 一个类可以把它的职责委托给其他类。
- 一个对象可以通过包含关系(containership)调用另一个对象里的方法。
- 这是类之间的关联(Association)关系(后续详讲)。
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
int、double 这类原始类型(Primitive):只含数据,没有属性和方法,不能"执行动作"(比如 parse 解析)。
Primitive 定义:只含数据、无属性无方法的信息单元。
- Java 为原始类型提供"包装"类。
- 让原始类型能被"打包"/"装箱(boxed)"成对象。
- 让原始类型"假装"是类(这一点在后面很重要)。
- 为原始类型提供额外功能。
Wrapper 定义:给 int 等原始类型额外功能、让它们能像对象一样行动的类。
| Primitive | Wrapper Class | Primitive | Wrapper Class |
|---|---|---|---|
boolean | Boolean | int | Integer |
byte | Byte | float | Float |
char | Character | double | Double |
short | Short | long | Long |
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) 等
把原始类型与它对应的包装类互相转换的过程。Java 可以自动完成,例如 Integer x = Integer.parseInt("20"); int y = x;。
继承与多态
用国际象棋的棋子做贯穿示例:所有棋子(Pawn/Rook/Knight…)有大量共同属性和方法。继承让你把共性提到父类,子类只写差异;多态让你用一个父类引用统一操作各种子类对象。这章是考试重头戏,重点在重写、动态绑定、抽象类。
4.1为什么需要继承(动机)
象棋里 Pawn、Rook、Knight、Bishop、King、Queen 每个棋子都有 isAlive、isWhite、currentRow、currentColumn 这些属性和 move、isValidMove 方法。如果每个棋子写一个独立类:
- 大量重复代码/功能,难调试;
- 无法体现棋子之间的"相似性"/关系;
- 实现工作量大;
- 难以扩展。
例如 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"(是一个)关系:
- 所有 Rook 对象都是 Piece;所有 Dog 都是 Animal;所有 Husky 都是 Dog。
- 只在 "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 的参数类型必须匹配父类的某个构造函数。
4.4方法重写 Overriding(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;
}
}
- 重写 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:对一个对象父类的引用。就像 this 指向自身,super 指向父类的属性和方法。
进一步把 isValidMove 的调用收进 move 方法内部,使用者不必自己显式检查合法性:move 内部先 if (!isValidMove(...)) return false; 再移动。
4.6重写的规则与陷阱
重写方法不能改变返回类型——除非改成原返回类型的子类型(协变返回)。
父类 private 方法无法被重写。子类里写一个同名 private 方法是合法的,但它不是重写——不会被父类引用调用,也不能用 super 调到父类那个。给它加 @Override 会编译报错。
重写时子类不能进一步限制被重写方法的可见性:
- 父类
public方法 → 子类必须保持public; - 父类
protected方法 → 子类可保持protected或放宽为public; - 父类
private方法 → 无法重写。
final:表示属性/方法/类只能被赋值/声明/定义一次。final 方法不能被子类重写,例如把 move 声明为 public final boolean move(...) 就锁死了它。
4.7继承中的访问控制 + 隐私泄露
- 子类不能调用父类的
private方法,不能访问父类private属性。 - 子类可以调用父类的
protected方法,可以访问父类protected属性。
把属性设成 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。
遮蔽定义:在重叠的作用域(如子类和父类)里用相同名字声明了两个或多个变量。访问哪个变量取决于引用类型而非对象。课件原话:"Don't. Do. It." ——公共变量只在父类定义一次就够了,不要在子类重复声明。
4.9Object 类(toString / equals / hashCode)
- Java 中每个类都隐式继承自 Object 类。
- 所有类都是
Object类型;所有类都有toString方法、equals方法(以及其他次要的东西)。
- 继承来的 toString 没用:默认打印类似
Rook@1540e19d(类名@哈希),所以要重写它,返回有意义的字符串。 - 继承来的 equals 也没用:默认返回 false(只比引用),所以要重写。
写 public boolean equals(Piece otherPiece) 能编译,但它没有真正重写 Object 的 equals——因为 Object 里的签名是 public boolean equals(Object otherObject)。参数类型不同 = 重载而非重写。
@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); }
}
- 只有抽象类可以有抽象方法;具体类不能有抽象方法。
- 只要类里有抽象方法,这个类就必须声明为 abstract。
- 抽象类不能被实例化:
Piece p = new Piece(3,2);❌ 非法。 - 抽象方法不能有任何实现,全部逻辑必须由子类实现。
4.13继承的类型 + Java 不支持多继承
继承可以有多级,例如 Shape → Circle → GraphicCircle。更一般地,继承有几种形式:
- 单继承 Single:只有一个父类
- 多继承 Multiple:多个父类
- 层次继承 Hierarchical:一个父类,多个子类
- 多级继承 Multi-Level:从一个派生类再派生
- 混合继承 Hybrid:多于两种类型的组合
- 多路继承 Multi-path:从两个来源继承某些属性
- Java 不支持多继承(以及涉及多继承的形式,如多路继承)。
- Java 通过接口(Interfaces,下一章)"算是"支持多继承,但并不完全等同于多继承。
工具与 Bagel
工具课:Git 版本控制、Maven 构建、IntelliJ 调试器、Bagel 游戏框架、开源项目。考试主要考概念与术语(不会让你背完整操作流程),所以这里聚焦定义、命令清单和 Bagel 的核心 API。
5.1Git 版本控制(模型 + 命令)
- 软件版本控制:系统化地管理软件制品(文档、源码、数据等)的并发版本。个人能回退到旧版本;团队能在本地写测后再合入代码库。
- Git:最常用的现代版本控制系统(约 70%),由 Linus Torvalds 创建(也是 Linux 作者)。
- 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(Basic Academic Graphical Engine Library):为 SWEN20003 定制的 Java 图形库(基于 LWJGL),Project 必须使用。
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。你把更新状态的逻辑写在这里。 - Image:
new 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。
输入输出
三种输入(命令行参数、键盘、文件)和两种输出(终端、文件)。考试会要求你会用 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"} ← 多词字符串用引号, 算一个元素
命令行参数全是字符串。要当数字用必须转换:
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,否则会出问题(bad things happen)。
System.in:代表标准输入流(命令行/终端)的对象。Scanner 的读取方法:
| 方法 | 读取内容 |
|---|---|
nextLine() | 读一整行文本,直到回车/换行符 |
next() | 读一个 token(单词) |
nextInt() / nextDouble() / nextBoolean() / nextFloat() | 读一个与方法名匹配类型的值 |
hasNext() / hasNextXXX() | 是否还有输入 / 下一个 token 是否匹配 XXX 类型 |
6.3Scanner 两大陷阱 ★
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 不是整数
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读文件
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,会自动关闭文件;异常处理见"异常"章节。
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 文件
- 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(); }
new FileWriter("file.txt", true) 的第二个参数 true 表示追加模式:每次运行会往文件追加内容,而不是覆盖。
两个典型应用:① 用文件在计算过程中存储/恢复中间数据;② 用 Java 解析/操作/转换文件(读入→处理→写出)。
6.8考试要求
- 会用 Scanner。
- 会用 Reader 类(FileReader、BufferedReader)。
- 会用 Writer 类(FileWriter、PrintWriter)。
- 会写从文件读和向文件写的代码,熟悉讲座/workshop/Grok 里介绍的方法。
- 会用命令行参数。
接口 Interfaces
接口(interface)是比抽象类更"纯"的一种抽象:它只规定"一个对象会做哪些事",完全不管"怎么做"。它解决了 Java 一个大限制——一个类只能继承一个父类,但可以实现多个接口。这一章讲清楚接口是什么、那些容易踩的隐含规则、和抽象类怎么区分,以及你早就用过的 Comparable 接口。
(课程时间线上,这一讲紧跟在第 4 章「继承与多态」之后。)
7.1接口是什么
接口 Interface:声明一组常量和/或方法,用来定义一个对象的行为(behaviour)。
- 定义了一个"抽象"的实体——不能被实例化(不能
new)。 - (传统上)只能包含常量和抽象方法。
- 定义了一组跨多个类共有的行为/动作。
- 一个类可以选择"实现(implement)"某个接口。
接口像一份"岗位职责清单":上面写着"能打印""能比较大小""能上色",但不写具体怎么做。任何类只要愿意"签下这份清单"(implement),就承诺自己会提供这些能力的具体做法。清单本身招不来人干活(不能实例化),它只是个约定。
7.2定义接口的隐含规则 ★
public interface Printable {
int MAXIMUM_PIXEL_DENSITY = 1000; // 一个常量
void print(); // 一个抽象方法
}
- 方法永远没有方法体(没有
{ ... }代码)。 - 所有方法都隐含为
abstract(不用、也不必写abstract关键字)。 - 所有属性都隐含为
static final——也就是常量。所以MAXIMUM_PIXEL_DENSITY自动是常量。 - 所有方法和属性都隐含为
public。
因为属性隐含 static final,你不能在接口里放一个"普通的、可变的成员变量"——写进去的字段一律是常量,且必须当场赋值。这点常被拿来出判断题。
7.3实现接口 implements
public class Image implements Printable {
public void print() {
// 具体怎么打印图片
}
}
public class Spreadsheet implements Printable {
public void print() {
// 具体怎么打印表格
}
}
- 具体类(concrete class)实现一个接口时,必须实现接口里定义的所有方法。
- 如果一个类没有实现全部方法,那它必须是抽象类(abstract)——把没实现的留给它的子类去完成。
Image 和 Spreadsheet 都是 Printable,所以你可以用统一类型 Printable 来装它们、调用 print(),各自表现各自的行为——这就是第 4 章和第 12 章反复出现的多态与开闭原则的基础。
7.4default 方法
default:给接口里的方法指定一个标准实现;如果某个实现类觉得这个默认行为不符合自己的预期,可以重写(override)它。
public interface Printable {
default void print() {
System.out.println(this.toString());
}
}
有了 default,实现 Printable 的类就算不写 print() 也能用(用默认那套);想要不一样的行为时再重写。它打破了"接口方法一定没有方法体"的老规矩,是现代 Java 的补充。
还记得函数式接口吗?它"只能有一个抽象方法,但可以有多个 default 或 static 方法"——正是因为 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 抽象类 ★
这是接口这一讲最高频的辨析考点。两者都"不能实例化、都用来做抽象",但定位不同。先回顾抽象类:
代表其子类共有的属性和方法,但缺少某些子类特有的信息,不能被实例化。它表示一个"不完整的概念",而不是问题里一个具体的"东西"。Piece p = new Piece(3,2); 这种写法是非法的。
| 抽象类 Abstract Class | 接口 Interface | |
|---|---|---|
| 能否实例化 | 不能 | 不能 |
| 能有的成员 | 普通字段、构造器、有实现的方法、抽象方法都行 | (传统上)只有常量 + 抽象方法;后来加了 default/static 方法 |
| 字段 | 可有普通可变成员变量 | 字段一律 public static final(常量) |
| 一个类能要几个 | 只能 extends 一个 | 可 implements 多个 |
| 表达的语义 | is-a("是一种…",共享实现) | can-do("能做…",行为契约) |
| 关键字 | extends | implements |
想共享一部分代码实现、且是"同一族东西"→ 用抽象类;只想规定"必须具备某种能力"、还希望一个类能同时具备好几种能力 → 用接口。课件把接口形容为抽象类"vague, distant 的远房亲戚"——更抽象、更松、更灵活。
7.7Comparable 接口
第 9 章排序时用到的 Comparable 就是一个接口。实现 Comparable<ClassName> 的类:能和同类对象比较大小、必须实现 public int compareTo(ClassName object)、因此能被自动排序(如 Collections.sort)。它定义了一个类的默认 / 自然顺序。
比较"当前对象 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 单继承);以及 Comparable 的 compareTo 负/零/正约定。
泛型 Generics
泛型让你写一次逻辑、适用于任意类型,同时保留编译期类型检查。先学怎么用泛型类(Comparable、ArrayList),再学怎么自己定义泛型类和泛型方法。
8.1泛型是什么 + 类型参数 T
Java 允许类、接口或方法的定义里带参数类型,这种定义叫泛型(Generics)。好处:能写出适用于任何类型的通用逻辑、实现代码复用。
public interface Comparable<T> {
public int compareTo(T other);
}
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
- 能像数组一样 for-each 遍历;自动处理扩容;任意位置增/删/取/改;自带
toString()。 - 不能用
[]索引(用get(i))。 - 本质: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[];❌
除此之外,大多数操作都可以。
集合与映射
Java 两大框架:Collections(存一组对象——list/set/queue)和 Maps(存键值对)。重点在选对数据结构、排序(Comparable vs Comparator)、泛型不变性,以及 HashMap/TreeMap 的使用。
9.1Collections vs Maps 框架概览
- Collections:用于存储、访问、操作一组对象(如 list、set、queue)的框架。
- Maps:用于存储、访问、操作键值对(key-value pairs)的框架。
PriorityQueue、LinkedList、ArrayList、TreeSet、HashSet、TreeMap、HashMap。注意 Map 不属于 Collection 体系,自成一支。9.2选哪种集合
- 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() 判断是否匹配。对自定义类,通常必须重写 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 是一个能让我们一次一个元素遍历集合的对象,是 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 ★
可以往 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> 为类定义默认(自然)排序。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> 为 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(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:存键值对,按键快速查找,不保持键有序。大多数 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),还能让编译器提前发现错误,避免运行时错误。
做电话簿 HashMap 时,若把国家码 61 和号码 136352 都存成 Integer 再相加,会被当成整数加法得到 136413,而不是想要的拼接 61136352。号码含 + ( ) 等符号也只能用 String。所以:HashMap<String, String> phonebook。
设计 Map 时,键选什么取决于你主要想"用谁查谁"。如宠物追踪:键为 petID 适合"按宠物找主人"(动物救助中心);键为 owner 适合"按主人找宠物"(宠物登记的地方议会)。
类关系与 UML 建模
从问题描述识别类、理清关系、用 UML 画类图。Project 2 的设计基础,也是考点(关联/聚合/组合区别明确"可考")。
10.1设计流程:识别类与关系
写代码前先设计系统,再按设计写代码。四步:
- 识别类:名词提取(Noun Extraction),从描述里找名词当候选类。
- 识别类关系:找 has-a、is-a、can-do。
- 精炼类与关系:合并、调整。
- 画类图:用 UML(业界广泛接受的记号)。
10.2UML 是什么
统一建模语言(UML):图形化建模语言,表示面向对象的分析、设计和实现。你学的是类建模,UML 只是其中一种记号。
10.3UML 表示一个类
一个类用三栏框表示:类名 / 属性 / 方法。
numObjects/getNumObjects() 带下划线=static;属性 可见性 名称: 类型 = 初值;方法 可见性 名称(参数: 类型): 返回类型。| 符号 | 含义 |
|---|---|
+ | public |
# | protected |
~ | package-private(default) |
- | private |
- 属性:名称、类型(
: int)、初值(= 0)、可见性、多重性、static。 - 多重性写法:
[10]固定、[1..10]已知范围、[1..*]未知上限、[*]零或多。 - 方法:名称、可见性、返回类型、参数。static 成员用下划线。
10.4四种类关系总览 ★
| 关系 | 含义 | UML 画法 |
|---|---|---|
| 关联 Association | has-a,含另一个类作属性 | 实线 |
| ↳ 聚合 Aggregation | "有",但两者独立存在 | 实线 + 空心菱形◇ |
| ↳ 组合 Composition | "有",部分离开整体无意义 | 实线 + 实心菱形◆ |
| 泛化 Generalization | is-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 组合 ★
聚合和组合都是关联的子集,区别在"部分"能否脱离"整体"独立存在。
| 聚合 ◇ | 组合 ◆ | |
|---|---|---|
| 菱形 | 空心 | 实心 |
| 部分能否独立存在 | 能 | 不能(依赖整体) |
| 整体被销毁时 | 部分仍存在 | 部分也失去意义 |
| 例 | Pond◇Duck;Library◇Book | University◆Department;Person◆Arm |
一条关系线不能两端各放一个菱形(一空心一实心)。聚合和组合不能在同一关系里混用。
10.8泛化 / 实现 / 依赖
表示 is-a,用实线 + 空心三角箭头指向父类。UML 里斜体类名/方法名表示 abstract 抽象。
一个类实现某接口,用虚线 + 空心三角箭头指向接口(如 Attacker 接口含 target()、attack())。
类之间的弱关系,一个类改动可能影响另一个,用虚线箭头。典型:方法把另一个类作输入参数,如 Player.move(p: Position) 依赖 Position。
10.9UML 工具
课件推荐:draw.io(SWEN30006 首选)、LucidChart(学生邮箱注册解锁完整功能)、StarUML、带插件的 IDE(可把 UML 转代码)等。
异常 Exceptions
运行时出错时如何优雅地捕获、恢复或终止,而不是崩溃。重点:try/catch/finally、throw vs throws、自定义异常、checked vs unchecked。
11.1三类错误
- 语法错误 Syntax:写的不是合法代码,编辑器/编译器发现。
- 语义错误 Semantic:能跑完但结果不对,靠软件测试发现。
- 运行时错误 Runtime:导致程序提前结束(崩溃),靠执行发现。
除以零、数组越界、存不兼容数据、用负数当数组大小、把 "abc" 转 int、文件错误(读不存在的文件、写只读文件)等。
11.2处理运行时错误的三种方案
- 方案一:什么都不做,听天由命——显然不理想,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 里抛了异常、或执行了 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 声明 |
| 继承自 | RuntimeException | Exception 但不是 RuntimeException |
| 例子 | NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException、NumberFormatException、ArithmeticException | IOException、FileNotFoundException、ClassNotFoundException、ParseException |
- Throwable(顶层)分两支:Exception 和 Error。
- Error:unchecked(如系统级严重错误)。
- Exception:本身是 checked,但其子类 RuntimeException 是 unchecked;Exception 的其他子类是 checked。
11.9Catch-or-Declare + try-with-resources
- 所有 checked 异常必须二选一处理:① 用 try-catch 包住可能抛异常的代码;或 ② 用 throws 声明方法可能抛出。
- 同一方法里两种方式可对不同异常分别使用。
- 异常应留给"无法用其他方式轻松处理的、不寻常或意外的情况"。
用于"用完需要关闭"的对象(如文件)。资源在 try(...) 的括号里创建,Java 会在 try 块结束时自动关闭它——无论正常结束还是抛了异常。常用于文件 IO(如 BufferedReader)。
try (BufferedReader reader = ...) {
...
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
软件设计 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 是 JDK(Java 开发工具包)自带的一个命令行工具,能从一种特殊注释里自动生成文档。
- JavaDoc 注释以
/**开头、*/结尾(普通注释是/* */或//,多一个星号就变成文档注释)。 - 可以被编译成 HTML 网页。
- 用来给包、类、方法、属性等写文档。
- 有各种
@标签,如@param(参数)、@return(返回值)、@author、@version。
- 注释(comments):写给"和你一起写代码的人",解释内部是怎么实现的。
- 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是一个模块。- 如果它只管玩家的状态和行为 → 高内聚。
- 如果它让
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)。
public class Game {
private KeyboardController controller = new KeyboardController();
}
这里 Game 把自己钉死在 KeyboardController 上——想换成 AI 控制器或测试用的假控制器,就得改 Game 内部代码。
public class Game {
private Controller controller;
public Game(Controller controller) { // 从外面传进来
this.controller = controller;
}
}
Game 只认 Controller 这个"通用类型",不在乎具体是谁。这样无论是键盘控制器、AI 控制器还是测试控制器,都能直接塞进去用——这正是第 10 章"依赖 Dependency"和构造注入的设计动机。
内聚要高(一个类只干一类事),耦合要低(类之间别缠太死)。两者方向相反,常一起考。
12.10抽象 / 封装 / 多态 / 委托
抽象:通过给真实概念建立简化模型来解决问题。比如 Player 类不会去模拟一个真人的所有细节,只建模"游戏需要的那部分"。
public class Player {
private int health;
private int x, y;
public void move(int dx, int dy) { ... }
public void takeDamage(int amount) { ... }
}
多态:通过一个共享类型来使用不同的对象类型(呼应第 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)。例:
Player让GameRenderer来画自己。
12.11开闭原则 Open-Closed ★
开闭原则 Open-Closed Principle:模块应当对扩展开放,对修改关闭。
意思是:尽量通过写新代码来添加新行为,而不是去改已经能正常工作的旧代码。
- 用一个共享类型(接口 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"); }
}
课程补充课件提醒:软件设计往往没有唯一正确答案,同一个需求可以有好几种合理设计。关键不是背"标准答案",而是能用本章这些原则(高内聚、低耦合、开闭……)为自己的设计讲出合理的理由。这一点也是可考的。
设计模式 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 | 跟对象的创建有关的解法 | Singleton、Factory Method |
| 结构型 Structural | 跟类的结构和关系有关 | Adapter、Bridge |
| 行为型 Behavioural | 跟类之间的交互有关 | Strategy、Template Method、Observer |
单例、工厂方法 → 创建型;策略、模板方法、观察者 → 行为型。(Adapter、Bridge 这两个结构型只是举例,本章不展开。)
13.4单例 Singleton ★
保证一个类只有一个实例,并提供一个全局访问点。当全程只想要"某样东西的唯一一份"时用它。
"维多利亚州州长(Premier)同时有几个?"——永远只有一个。再比如打印机的任务队列管理器、缓存管理器,全系统只该有一份。
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()是全局访问点:没有就造,有就直接返回旧的。
x = Singleton.getInstance();
y = Singleton.getInstance();
System.out.println(x == y); // true —— x 和 y 指向同一个对象
无论调用几次 getInstance(),拿到的都是同一个对象,所以 x == y 为 true(呼应第 2/3 章:== 比的是"是不是同一个对象")。
Shadow Aliens 里的 GameConfig 就是单例:私有构造 + 静态 instance + getInstance(),任何地方都能拿到同一份游戏配置。它还加了一个 init() 做一次性初始化,没初始化就调用会抛 IllegalStateException 提醒开发者——这是工程上的常见加固。
课件提醒:单例要小心用,滥用会导致糟糕的设计(它本质是个"全局变量",用多了会让代码到处隐式依赖它,反而提高耦合)。
13.5工厂方法 Factory Method
不要在通用类里直接用 new 创建对象;而是调用一个专门负责"造出正确对象"的方法。
// 不这样:
players.add(new RPGPlayer());
// 而是这样:
players.add(createPlayer()); // 由子类决定 createPlayer() 造哪种 Player
- Factory 工厂:制造(创建)对象的通用技术。
- Product 产品:一个抽象类,泛化"工厂要造出来的那些对象"。
- Creator 创造者:一个抽象类,泛化"会消费/生产产品的那些对象";通常有个操作(如构造函数)会去调用工厂方法。
抽象 Game 声明 createPlayer(): Player;RPGGame 和 ShooterGame 各自重写它,分别造出 RPGPlayer 和 ShooterPlayer。Game 的通用代码只认抽象的 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
在一个操作里定义算法的骨架,把其中某些步骤推迟到子类去实现。它是"用继承做复用"的典型例子。
- 把标准/不变的算法放在基类里。
- 把会变化的具体实现留给子类(声明为抽象方法)。
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()。
用继承的代价是子类对基类有强依赖(呼应第 12 章:继承耦合较紧)。如果想避免这种强依赖,策略模式(Strategy)是它的替代方案——下一节登场。
13.7策略 Strategy
把"会变化的行为"抽成一个独立对象,通过委托(delegation)来用它:把一个"代表某种行为的对象"传进来,主类把这部分行为委托给它去做。这是"用组合做复用"。
- Strategy:一个接口,声明可替换的算法,如
algorithm()。 - ConcreteStrategyA / B:实现该接口的不同算法。
- Context:持有一个
Strategy引用,在contextMethod()里把活儿交给它。
public interface DiscountStrategy {
double applyDiscount(double price);
}
// 不同折扣 = 不同实现类, 都实现这个接口
// Context 持有一个 DiscountStrategy, 需要打折时就调它
Shadow Aliens 里敌人的移动就是策略模式:MovementStrategy 是接口,StraightDownMovement、StrafingMovement 是不同算法,敌人持有一个 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 的对象,以便对它的状态及任何变化做出响应。
UP 主(Subject)发了新视频,所有订阅者(Observer)就自动收到通知,各自做各自的反应(点开看 / 点赞 / 忽略)。UP 主不需要知道每个订阅者是谁、要干嘛,只管"广播"一声。观察者模式就是这个"发布—订阅"机制。
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(它已被官方弃用)。
// 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 的栏目分析它。把每个模式的"意图 + 一句话机制 + 一个例子"记牢就够应付。
软件测试 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 是 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、写准确是开发者责任"这个考点,就覆盖了本章主要内容。
事件驱动编程 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.
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 轮询
你做 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();
}
}
- JavaFX = 纯回调(callback):你只写 handler 并注册,框架在事件发生时回调你。
- Bagel = 轮询(polling):框架每帧把
Input传给你的update(),你主动去问"这一帧某个键按了吗"。
共同点都是事件驱动的核心:你的 update() 不是你自己调的,是框架每帧自动回调的——同样是控制反转。你项目里 handleBattleState 里那些 input.wasPressed(Keys.ESCAPE) 转去暂停、Keys.R 重开,就是在 update 这个被回调的方法里,根据按键事件改变游戏状态(state)。
15.7它和异常处理 / 观察者的联系
课件特意提了一句:事件驱动这种"信号—响应"行为,你其实之前见过类似的——
| 已学过的东西 | 相似在哪 |
|---|---|
| 异常处理 Exception handling(第 11 章) | throw 像"触发事件",catch 块像"处理器"——不在正常顺序里,出事了才跳过去执行。 |
| 观察者模式 Observer(第 13 章) | Subject 状态变化 → 通知所有 Observer,正是"事件 → 监听器响应";观察者也要事先注册。 |
| 图形界面 GUI | 点击、按键本身就是最典型的事件来源。 |
记牢:定义(用事件+回调按状态变化控制流程)、三个关键词 State/Event/Callback、监听器要事先注册、最核心的控制反转(你写的 handler/update 由框架自动回调,你自己不显式调用)、四条优点,以及它与异常处理、观察者模式的相通之处。JavaFX 的具体 API 不要求会写,理解范式即可。
进阶 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(下面会用到的 Predicate、Function、Consumer)。常见的内置函数式接口有 Runnable、Callable、Comparator(第 9 章排序用过!)。
16.4Lambda 表达式 ★
Lambda 表达式:一种把"代码当成数据"、能像"对象"一样使用的技术;比如让我们不用写实现类就能"实例化"一个接口。
(参数1, 参数2, ...) -> (对这些参数做的操作)
它接收零个或多个参数,对它们施加一个操作。例子:
Predicate<Integer> p = (i) -> (i > 0);
这里 Predicate<Integer> 是个函数式接口(它的抽象方法是"测试一个值返回 true/false")。这一行就地造出一个对象,实现了"测试整数是否大于 0"的功能——没有单独写一个 implements Predicate 的类。
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 | 把整条流聚合成一个值 |
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() >= 18 是 lambda,它是函数式接口 Predicate 的实例;Person::getName 是方法引用。所以"函数式接口 → lambda → 方法引用 → stream"是层层叠上来的一套组合拳:函数式接口提供"单方法契约",lambda/方法引用提供"简洁地写出这个方法",stream 提供"把这些操作串成流水线"。
记牢:enum 是"有限常量的类"(自然顺序=声明顺序、自带 ordinal());可变参数 type... 收成数组;函数式接口=只有一个抽象方法(SAM);lambda 是函数式接口的实例、写法 (args) -> (op);方法引用 类名::方法 替代"只调一个方法"的 lambda;stream 的 map/filter/collect/reduce 及如何逐行读懂链式调用。