EIE3320 考前最后练手 · 校园二手市场(Campus Marketplace)
一句话:用一条 7 阶段的流水线,把 HW1–HW5 的重难点各走一遍,全程只用控制台(不碰 GUI),90–150 分钟。 配套:eie3320 homework 精读(每题拆解)、eie3320 课件精读(ch11/ch14 逐页)。 用法:每个阶段都有逐字期望输出。写完就编译运行、跟期望输出对齐——对齐了才进下一阶段。
0. 怎么练(先读这一节)
0.1 三条规则
| 规则 | 说明 |
|---|---|
| 不许看答案 | 本文不含实现代码,只给「必须有哪些成员」+「期望输出」+ 分层提示。卡住时按 §11 从 L1 逐级看到 L3。 |
| 一次只推进一个阶段 | 每个阶段都能独立编译运行。跑不通就别往下走——后面的阶段都建立在前面之上。 |
| 对齐输出,不对齐就改 | 期望输出是从真实运行结果导出的,逐字符可信。用 §10.3 的自动比对命令,别用肉眼看。 |
0.2 三档练法(按剩余时间选)
| 档 | 时间 | 做什么 | 适合 |
|---|---|---|---|
| 全量 | 2–2.5 h | S0 → S7 全走一遍 | 考前一天完整过一遍 |
| 主线 | 60–75 min | 只做 S4 + S5 + S6(继承 / 多态 / 集合),S1–S3 的类直接抄本文的成员表 | 只想补最可能考崩的地方 |
| 速查 | 25 min | 只读 §2 覆盖矩阵 + §12 报错速查 + §11 提示,不动手 | 进考场前最后 20 分钟 |
不想手抄驱动? vault 的
SYS/SYS2000-asset-media/eie3320-final-practice/eie3320-final-practice-starter.zip里是 7 个驱动文件(Step1.java…Step6.java、TestMarket.java)+ 7 份expected/stepN.txt+ 一份README.md。解压到任意目录后直接javac,你只需要新建那 9 个领域类。 想看完整分步代码? vault 的SYS/SYS2000-asset-media/eie3320-final-practice/eie3320-final-practice-solution.zip里是starter/+step1/–step7/+expected/+build.py;每步都可直接编译运行,用来对照。
0.3 为什么用「校园二手市场」这个题目
因为它和 HW4 的 dome 项目同构但不同题:同样有「父类 + 三个子类 + 一个集合管理器 + 三个搜索」,但字段(价格、时间、卖家、运费)全是新的。抄不动,只能真的理解——这正是考前练手要的。
1. 项目全貌
1.1 十个文件
| 文件 | 阶段 | 职责 | 一句话 |
|---|---|---|---|
Owner.java | S1 | 值对象 | 卖家:姓名 + 电话,不可变 |
Money.java | S1 | 工具类 | Money.format(4300) → "$4,300.00" |
Listing.java | S2 → S4 | 核心父类 | S2 写成具体类;S4 改成 abstract |
Basket.java | S3 | 定长容器 | 5 个槽位的数组 + 实际条数 |
Shippable.java | S4 | 接口 | 只有「能寄」的子类才实现 |
TextBook.java | S4 | 子类 1 | 课程 + 版次;不实现 Shippable |
Device.java | S4 | 子类 2 | 品牌 + 保修;实现 Shippable |
Furniture.java | S4 | 子类 3 | 尺寸 + 重量;实现 Shippable |
Market.java | S5 → S6 | 集合管理器 | ArrayList<Listing> + 三搜索 + 遍历删除 + 排序 |
Step1..Step6.java / TestMarket.java | 各阶段 | 验收驱动 | 照抄,不许改 |
1.2 类关系
Listing «abstract»
-id -title -price -owner -postedHour -postedMinute
+postedTime() +slot() +toString() +print() +compareTo(Listing)
+category() «abstract» +detail() «abstract»
▲
extends │
┌─────────────────────┼─────────────────────────┐
│ │ │
TextBook Device Furniture
-course -brand -widthCm
-edition -warrantyMonths -depthCm
│ │ implements -weightKg
│ ▼ │
│ Shippable ◀───────────────────┘
│ +shippingFee()
│
不实现 Shippable → print() 里显示 self-pickup
三个要点,写完 S4 就该能自己讲出来:
print()是父类的具体方法,却调用了子类的category()/detail()—— 模板方法,HW4 Q2的super.print()的升级版。Shippable只有两个子类实现 —— 接口表达「能力」,不是「是什么」(ch14 p.36–38)。Listing数组能装三种子类 —— 抽象类作数组元素类型(ch14 p.17)。
1.3 终点:TestMarket 的完整输出
这就是 S7 要复现的东西(细节见 §10):
== 1. list() ==
ID CATEGORY TITLE PRICE OWNER POSTED
---- ---------- ------------------------ ---------- ------------------ ------------
#1 TextBook EIE3320 Lecture Notes $120.00 Chan Tai Man 2:31 p.m.
#2 Device iPad Air 11 $4,300.00 Lee Siu Ming 9:05 a.m.
#3 Furniture Study Desk $480.00 Chan Tai Man 9:47 p.m.
#4 TextBook Calculus 8e $260.00 Lee Siu Ming 8:00 a.m.
#5 Device Mechanical Keyboard $350.00 Chan Tai Man 11:59 p.m.
(完整版 54 行输出在 §10.2。)
2. 考点覆盖矩阵(先看这张,知道自己在补什么)
| 作业 / 考点 | 本项目哪一步 | 具体在哪 |
|---|---|---|
| HW1 Q1 金字塔对齐 | S5 | list() 的 printf 列宽 |
HW1 Q2 / 与 % | S2 | postedTime() 的 hour % 12 |
| HW1 Q3 写方法、每行 N 个 | S3 | Basket.print() 一条一行 |
HW1 Q4 switch 分组穿透 | S2 | slot() 的 case 5..11 → morning |
| HW2 Q1b 12 小时制 + a.m./p.m. | S2 | postedTime() |
HW2 Q2 构造器 + double 字段 | S4 | 三个子类的构造器 |
| HW2 Q3 对象协作(共享引用) | S2 / S5 | Listing 持有 Owner;Market 持有 List |
| HW3 Q1 数组 + 条数≠容量 | S3 | Basket.add() 的 count >= items.length |
HW3 Q1 toString 委派 | S3 | Basket.print() 里 items[i] 直接拼接 |
| HW4 Q1→Q2 抽父类 | S4 | Listing 从具体类改成 abstract |
HW4 Q2 super(...) / 覆盖 | S4 | 三个子类构造器首行、两个抽象方法 |
| HW4 Q3 三种遍历 | S5 / S6 | 下标(totalValue)、for-each(list)、Iterator(removeByOwner) |
HW5 Q1a equals 精确搜索 | S5 | searchByTitle |
HW5 Q1c Pattern.find 子串搜索 | S5 | searchByTitlePattern |
| HW5 Q1d 任意字段搜索 | S5 | searchByPattern(靠父类 toString) |
| HW5 Q2 遍历 + 过滤 | S6 | removeByOwner / shippableItems |
| Ch14 抽象类 + 接口 | S4 | abstract category/detail、Shippable |
| Ch14 接口作返回类型 | S6 | ArrayList<Shippable> shippableItems() |
Ch11 Comparable | S6 | Listing implements Comparable<Listing> |
| Ch10 不可变对象 | S1 | Owner 全 final 字段、无 setter |
本项目故意不覆盖(练手时间有限,这两块去作业精读里补):
| 没覆盖 | 去哪补 |
|---|---|
HW4 Q4 Iterator vs get() 的性能对比 | 作业精读 §5.4(LinkedList + 下标是 O(n²)) |
HW4 Q5 HashMap | 作业精读 §5.5;本项目用 Comparable + Collections.sort 替代了「换一种集合」的练习 |
| GUI / 事件驱动 | 那是 Lecture 7–8,不在本次范围 |
3. S0 · 环境自检(5 分钟)
目标:确认 javac 能编译、java 能运行、控制台编码不乱码。
S0.1 三行命令
javac -version
java -version期望看到 javac 21.x 一类版本号(19、17 都够用)。两个命令都找不到 → 装 JDK 或检查 JAVA_HOME。
S0.2 建目录并跑一个 Hello
mkdir C:\Temp\market; cd C:\Temp\market
@'
public class Hello {
public static void main(String[] args) {
System.out.println("ok");
}
}
'@ | Out-File -Encoding utf8 Hello.java
javac Hello.java; java Hello期望输出 ok。
S0.3 验收清单
-
javac -version有版本号 -
java Hello打印ok - 目录里出现了
Hello.class(说明javac真的在工作)
BlueJ 用户:可以完全在 BlueJ 里做,但 §10.2 的自动比对需要命令行。建议还是用命令行练——考试环境也是命令行/IDE 混着的。 IDE 用户(Eclipse / IntelliJ / VS Code):新建一个 default package 的项目,所有
.java平铺在同一个目录里。不要建 package,否则javac *.java的路径会变。
4. S1 · Owner + Money(10 分钟)
目标:把「值对象」和「格式化」两件小事做对。看着简单,但 $1,234.50 这一行能刷掉一半人。
4.1 必须实现的成员
| 类 | 成员 | 要求 |
|---|---|---|
Owner | Owner(String name, String phone) | 两个字段都 private final |
getName() / getPhone() | 只读 | |
toString() | 返回 "Chan Tai Man (6123 4567)" | |
Money | Money 构造器 | private,且类声明为 final(工具类不让实例化) |
static String format(double) | 返回 "$" + 带千分位、两位小数 |
4.2 硬约束
Owner不许有setName()/setPhone()(ch10 不可变对象)。Money不许自己写循环拼小数——用String.format。- 千分位必须有:
1234.5→$1,234.50(不是$1234.50)。
4.3 驱动 Step1.java(照抄,不许改)
public class Step1
{
public static void main(String[] args)
{
Owner chan = new Owner("Chan Tai Man", "6123 4567");
Owner lee = new Owner("Lee Siu Ming", "9876 5432");
System.out.println(chan);
System.out.println(lee);
System.out.println(chan.getName() + " / " + chan.getPhone());
System.out.println(Money.format(120));
System.out.println(Money.format(1234.5));
System.out.println(Money.format(0));
System.out.println(Money.format(5510));
System.out.println(Money.format(0.005));
}
}4.4 期望输出(逐字)
Chan Tai Man (6123 4567)
Lee Siu Ming (9876 5432)
Chan Tai Man / 6123 4567
$120.00
$1,234.50
$0.00
$5,510.00
$0.01
4.5 验收清单
- 前 3 行完全一致(含括号、空格、斜杠两侧各一个空格)
-
1234.5→$1,234.50(千分位) -
0→$0.00(补两位) -
0.005→$0.01(不是$0.00,也不是$0.005)
4.6 三个坑
| 坑 | 现象 | 原因 |
|---|---|---|
| 忘了千分位 | $1234.50 | 用了 %.2f 而不是 %,.2f |
1234 传成 int | 编译不过 | format(int) 与 format(double) 是两个方法,int 实参能自动拓宽,没问题——但如果你把参数写成 int 就丢失小数了 |
toString 写成 return name; | 第 1 行只有名字 | println(对象) 调的是 toString(),不是字段 |
$0.01这一条是故意的:String.format("%,.2f", 0.005)走的是HALF_UP,而0.005在二进制里其实略小于 0.005……能得$0.01说明你用的是String.format(它按十进制字符串舍入)。用Math.round手工算反而可能得$0.00。不要在这上面纠结,对齐输出即可。
5. S2 · Listing:12 小时制 + switch 时段(20 分钟)
目标:这一阶段把 HW2 Q1b 的 12 小时制 和 HW1 Q4 的 switch 分组 一次写完。此时 Listing 还是具体类(能 new),S4 才会把它改成 abstract。
5.1 必须实现的成员
| 成员 | 要求 |
|---|---|
private static int count | 静态计数器,每创建一条就 ++ |
private final int id | 构造器里 this.id = ++count(先自增再赋值,所以 id 从 1 开始) |
private final String title / double price / Owner owner | 三个基本字段 |
private final int postedHour / postedMinute | 发布时间(24 小时制,0..23 与 0..59) |
Listing(String title, double price, Owner owner, int hour, int minute) | 唯一构造器 |
getId() / getCount()(static) / getTitle() / getPrice() / getOwner() | getter |
String postedTime() | 12 小时制,见 5.2 |
String slot() | 用 switch 返回时段,见 5.3 |
String toString() | title + " (" + Money.format(price) + ") by " + owner.getName() |
void print() | 四行块,见 5.4 |
5.2 postedTime() 的规则(HW2 Q1b 的核心)
内部 postedHour | postedTime() |
|---|---|
| 0 | 12:xx a.m. |
| 1–11 | 1:xx a.m. … 11:xx a.m. |
| 12 | 12:xx p.m. |
| 13–23 | 1:xx p.m. … 11:xx p.m. |
- 小时不补零(
9:05,不是09:05)。 - 分钟必须两位(
9:05,不是9:5)。 - 后缀是
a.m./p.m.,带两个点,前面一个空格。
5.3 slot() 的分组(HW1 Q4 的 switch 穿透)
| 小时 | 返回 |
|---|---|
| 5, 6, 7, 8, 9, 10, 11 | morning |
| 12, 13, 14, 15, 16 | afternoon |
| 17, 18, 19, 20 | evening |
| 其余(21, 22, 23, 0, 1, 2, 3, 4) | night |
必须用 switch,且用「多个 case 标签共享一段语句 + 最后 return」的写法(不要每个 case 都写 break)。这是 HW1 Q4 那道 daysInMonth 的同款动作。
5.4 print() 的格式
#1 [Listing] EIE3320 Notes
price : $120.00
owner : Chan Tai Man (6123 4567)
posted : 2:31 p.m. (afternoon)
- 第 1 行:
"#" + id + " [Listing] " + title([Listing]是字面量,S4 会换成category())。 - 后 3 行都以三个空格开头,字段名后补空格对齐到 7 列再跟
:。 即price :、owner :、posted :。 owner那行直接拼owner对象(靠它的toString())。
5.5 驱动 Step2.java
public class Step2
{
public static void main(String[] args)
{
Owner chan = new Owner("Chan Tai Man", "6123 4567");
Listing a = new Listing("EIE3320 Notes", 120.0, chan, 14, 31);
Listing b = new Listing("iPad Air 11", 4300.0, chan, 9, 5);
Listing c = new Listing("Midnight Snack", 12.5, chan, 0, 5);
Listing d = new Listing("Late Deal", 8.0, chan, 23, 59);
Listing e = new Listing("Noon Deal", 15.0, chan, 12, 0);
Listing[] all = {a, b, c, d, e};
for (Listing item : all)
{
System.out.println("#" + item.getId() + " " + item.postedTime()
+ " -> " + item.slot());
}
System.out.println("count = " + Listing.getCount());
System.out.println();
a.print();
}
}5.6 期望输出
#1 2:31 p.m. -> afternoon
#2 9:05 a.m. -> morning
#3 12:05 a.m. -> night
#4 11:59 p.m. -> night
#5 12:00 p.m. -> afternoon
count = 5
#1 [Listing] EIE3320 Notes
price : $120.00
owner : Chan Tai Man (6123 4567)
posted : 2:31 p.m. (afternoon)
5.7 验收清单
- 五个拐点全对:
0:05 → 12:05 a.m.、9:05 → 9:05 a.m.、12:00 → 12:00 p.m.、14:31 → 2:31 p.m.、23:59 → 11:59 p.m. - 小时无前导零、分钟有前导零
-
slot()中21..23与0..4都落到night(23:59那条就是验证) -
count = 5、id 是1..5(注意++count的位置) -
print()三行缩进与[Listing]字面量一致
5.8 三个坑
| 坑 | 现象 | 原因 |
|---|---|---|
12:05 a.m. 写成 0:05 a.m. | 第 3 行不对 | 忘了 h12 == 0 → 12 |
2:31 p.m. 写成 02:31 p.m. | 第 1 行不对 | 用了 %02d 格式化小时(只需要给分钟补零) |
| id 从 0 开始 | #0 | 写成了 this.id = count++(后置自增) |
6. S3 · Basket:定长数组 + 条数 ≠ 容量(15 分钟)
目标:把 HW3 Q1 那个 NullPointerException 陷阱再踩一次、再解决一次。
6.1 必须实现的成员
| 成员 | 要求 |
|---|---|
private final Listing[] items | 长度固定 5:new Listing[5] |
private int count | 初始 0,表示实际装了几件 |
boolean add(Listing item) | 装得下 → 放进 items[count]、count++、返回 true;装不下 → 返回 false(不许扩容、不许抛异常) |
int size() | 返回 count(不是 items.length) |
double total() | 求和前 count 个 |
void print() | 一条一行 + 一行汇总,见 6.3 |
6.2 硬约束
- 循环上限只能用
count。写items.length会打出 5 行之后继续访问null→NullPointerException(add满 5 件后第 6 件被拒,数组里就有null)。 - 不许用
ArrayList——这一阶段就是要练「数组 + 条数」这套手工管理。
6.3 输出格式
1: A ($10.00) by Chan Tai Man
...
items = 5, total = $150.00
- 序号从 1 开始(不是 0)——
(i + 1) + ": "。 - 每条后面直接拼
items[i]对象(自动调toString())——这就是 HW3 要求的「必须调用toString」。 - 最后一行:
"items = " + count + ", total = " + Money.format(total())。
6.4 驱动 Step3.java
public class Step3
{
public static void main(String[] args)
{
Owner chan = new Owner("Chan Tai Man", "6123 4567");
Basket basket = new Basket();
String[] names = {"A", "B", "C", "D", "E", "F"};
double[] prices = {10.0, 20.0, 30.0, 40.0, 50.0, 60.0};
for (int i = 0; i < names.length; i++)
{
Listing item = new Listing(names[i], prices[i], chan, 10, 0);
boolean ok = basket.add(item);
System.out.println("add " + names[i] + " -> " + ok);
}
System.out.println("size = " + basket.size());
basket.print();
}
}6.5 期望输出
add A -> true
add B -> true
add C -> true
add D -> true
add E -> true
add F -> false
size = 5
1: A ($10.00) by Chan Tai Man
2: B ($20.00) by Chan Tai Man
3: C ($30.00) by Chan Tai Man
4: D ($40.00) by Chan Tai Man
5: E ($50.00) by Chan Tai Man
items = 5, total = $150.00
6.6 验收清单
- 只有前 5 个
true,第 6 个false - 没有异常(如果抛
NullPointerException,说明print()/total()用了items.length) -
size = 5而不是size = 5之后又冒出空行/空条目 -
total = $150.00(只算 5 件,第 6 件没进去)
6.7 一句话总结
容量是
items.length,条数是count。凡两者并存,遍历与求和一律用条数。
7. S4 · 继承 + 抽象类 + 接口(25 分钟,最重要的一步)
目标:把 S2 的 Listing 从「一个能干活的类」改造成「一个规定契约的抽象父类」,再加三个子类和一个接口。这一步同时吃掉 HW4 Q1→Q2 与 Ch14 的抽象类 / 接口。
7.1 动手前先想 30 秒
S2 的 Listing 里,print() 打印的是 [Listing]。现在要打印 [TextBook] / [Device] / [Furniture]——这个字符串只能由子类决定,但 print() 又只有一份(写在父类里)。
这就是 ch14 p.6 那个洞:父类需要一个「有名字、没实现」的方法。答案是:
public abstract String category();7.2 要改的 Listing
| 改动 | 内容 |
|---|---|
| 类声明 | public abstract class Listing |
| 新增抽象方法 1 | public abstract String category(); —— 返回 "TextBook" / "Device" / "Furniture" |
| 新增抽象方法 2 | public abstract String detail(); —— 一行补充信息 |
改 toString() | 见 7.3 |
改 print() | 见 7.4 |
其他字段 / 构造器 / postedTime() / slot() / 全部 getter | 一行都不许动 |
注意:Listing 一旦 abstract,S2 / S3 的 Step2.java、Step3.java 里的 new Listing(...) 就编译不过了——这是预期的,它们已经完成使命,删掉或改名即可(例:Step2.java.bak)。
7.3 新的 toString()(一句话可搜索形式)
[TextBook] EIE3320 Lecture Notes ($120.00) by Chan Tai Man | 6123 4567 | Course: EIE3320, ed.3
拼接顺序:"[" + category() + "] " + title + " (" + Money.format(price) + ") by " + owner.getName() + " | " + owner.getPhone() + " | " + detail()
为什么要把
detail()塞进toString():S5 的searchByPattern("Apple")要能靠toString()命中「品牌」这种子类才有的字段。父类看不见子类字段,但父类能调toString()——而toString()又是动态绑定的。这一条是 HW5 Q1d 的全部秘密。
7.4 新的 print()(模板方法 + 接口判断)
#1 [TextBook] EIE3320 Lecture Notes
price : $120.00
owner : Chan Tai Man (6123 4567)
posted : 2:31 p.m. (afternoon)
detail : Course: EIE3320, ed.3
ship : self-pickup
- 第 1 行把
[Listing]换成category()。 - 第 5 行
detail :后拼detail()。 - 第 6 行分两种:
- 若
this instanceof Shippable→ 转型后调shippingFee(),输出ship : $63.00 - 否则 →
ship : self-pickup
- 若
关键字都在这一行里:this instanceof 接口 + (接口) this 转型。ch14 p.25–28 的 object instanceof Edible 同款。
7.5 Shippable 接口
public interface Shippable
{
double shippingFee();
}- 接口里不写
public abstract(省略即默认,p.29)。 - 只有
Device和Furniture实现它,TextBook不实现——「能力」不是「身份」。
7.6 三个子类
| 子类 | 新增字段 | category() | detail() | shippingFee() |
|---|---|---|---|---|
TextBook | String course、int edition | "TextBook" | "Course: " + course + ", ed." + edition | — (不实现接口) |
Device | String brand、int warrantyMonths | "Device" | "Brand: " + brand + ", warranty " + warrantyMonths + " mo" | 20 + 0.01 * getPrice() |
Furniture | double widthCm、double depthCm、double weightKg | "Furniture" | "Size: " + widthCm + " x " + depthCm + " cm, " + weightKg + " kg" | weightKg * 5.0 |
构造器签名(顺序必须一致,Step4 就是这么调的):
| 子类 | 构造器 |
|---|---|
TextBook | (String title, double price, Owner owner, int hour, int minute, String course, int edition) |
Device | (String title, double price, Owner owner, int hour, int minute, String brand, int warrantyMonths) |
Furniture | (String title, double price, Owner owner, int hour, int minute, double widthCm, double depthCm, double weightKg) |
7.7 三条硬约束(每条都对应一个考点)
- 每个子类构造器第一行必须是
super(title, price, owner, hour, minute);—— 因为Listing只有带参构造器,隐式super()找不到目标(ch11 p.37 的Apple/Fruit报错就是没写)。 category()/detail()的可见性必须是public—— 接口/抽象方法默认public,写成包可见会报attempting to assign weaker access privileges。print()里不许写if (category().equals("Device"))—— 判断「能不能寄」只能靠instanceof Shippable,不能靠字符串。用字符串判断等于把多态退化成switch。
7.8 驱动 Step4.java
public class Step4
{
public static void main(String[] args)
{
Owner chan = new Owner("Chan Tai Man", "6123 4567");
Owner lee = new Owner("Lee Siu Ming", "9876 5432");
Listing[] items = {
new TextBook("EIE3320 Lecture Notes", 120.0, chan, 14, 31, "EIE3320", 3),
new Device("iPad Air 11", 4300.0, lee, 9, 5, "Apple", 12),
new Furniture("Study Desk", 480.0, chan, 21, 47, 120.0, 60.0, 18.0)
};
for (Listing item : items)
{
item.print();
System.out.println();
}
System.out.println("-- one-line form (toString) --");
for (Listing item : items)
{
System.out.println(item);
}
}
}注意 Listing[] items = { new TextBook(...), new Device(...), new Furniture(...) }; —— 抽象类型的数组装三种子类(ch14 p.17)。
7.9 期望输出
#1 [TextBook] EIE3320 Lecture Notes
price : $120.00
owner : Chan Tai Man (6123 4567)
posted : 2:31 p.m. (afternoon)
detail : Course: EIE3320, ed.3
ship : self-pickup
#2 [Device] iPad Air 11
price : $4,300.00
owner : Lee Siu Ming (9876 5432)
posted : 9:05 a.m. (morning)
detail : Brand: Apple, warranty 12 mo
ship : $63.00
#3 [Furniture] Study Desk
price : $480.00
owner : Chan Tai Man (6123 4567)
posted : 9:47 p.m. (night)
detail : Size: 120.0 x 60.0 cm, 18.0 kg
ship : $90.00
-- one-line form (toString) --
[TextBook] EIE3320 Lecture Notes ($120.00) by Chan Tai Man | 6123 4567 | Course: EIE3320, ed.3
[Device] iPad Air 11 ($4,300.00) by Lee Siu Ming | 9876 5432 | Brand: Apple, warranty 12 mo
[Furniture] Study Desk ($480.00) by Chan Tai Man | 6123 4567 | Size: 120.0 x 60.0 cm, 18.0 kg
7.10 验收清单
-
TextBook显示self-pickup,另外两个显示运费(这是接口有没有接对的分水岭) - 运费数值:
$63.00(20 + 43)、$90.00(18 × 5) -
posted行的时段:21:47 → night(因为slot()只把 17–20 算evening) - 一行形式的
toString()三个|分隔符齐全、detail()在最末 - 把
TextBook的detail()临时删掉 → 应立刻编译失败(TextBook is not abstract and does not override abstract method detail())。看到这个报错说明抽象方法真的生效了
7.11 四个坑
| 坑 | 报错 / 现象 | 原因 |
|---|---|---|
忘了 super(...) | constructor Listing in class Listing cannot be applied to given types | 父类只有带参构造器 |
抽象方法写成 protected | attempting to assign weaker access privileges | 覆盖/实现不能降可见性(ch11 p.75) |
shippingFee() 写成 int | Device is not abstract and does not override abstract method shippingFee()(返回类型不匹配) | 接口声明是 double,实现必须同为 double |
print() 里 this.shippingFee() | cannot find symbol: method shippingFee() | Listing 没有这个方法;必须先 instanceof + 转型 |
8. S5 · Market:集合 + 对齐表 + 三个搜索(25 分钟)
目标:第一次拥有「一个类管一堆对象」。三个搜索是本阶段的主体——它们分别对应 HW5 Q1a / Q1c / Q1d。
8.1 必须实现的成员
| 成员 | 要求 |
|---|---|
private final ArrayList<Listing> listings | 泛型写全:new ArrayList<Listing>() |
void add(Listing item) | 直接 listings.add(item) |
int size() | listings.size() |
double totalValue() | 用下标 for 累加(for (int i = 0; i < listings.size(); i++)) |
void list() | 对齐表格,见 8.2 |
Listing searchByTitle(String title) | 精确匹配 title,用 equals;找不到返回 null |
Listing searchByTitlePattern(String pat) | title 包含 pat;用 Pattern + Matcher.find() |
Listing searchByPattern(String pat) | 任意字段包含 pat;对 toString() 做 find() |
8.2 list() 的表头与列宽(照抄这四行)
System.out.printf("%-4s %-10s %-24s %-10s %-18s %s%n",
"ID", "CATEGORY", "TITLE", "PRICE", "OWNER", "POSTED");
System.out.println("---- ---------- ------------------------ ---------- "
+ "------------------ ------------");
for (Listing item : listings)
{
System.out.printf("%-4s %-10s %-24s %-10s %-18s %s%n",
"#" + item.getId(), item.category(), item.getTitle(),
Money.format(item.getPrice()), item.getOwner().getName(),
item.postedTime());
}六个列的宽度:4 / 10 / 24 / 10 / 18 / 不限。表头与数据行必须用同一个格式串,否则列会错开。
8.3 三个搜索的区别(这一页要能默写)
| 方法 | 匹配对象 | 用什么 | 大小写 | 典型调用 → 结果 |
|---|---|---|---|---|
searchByTitle | title 整串 | String.equals | 敏感 | "iPad Air 11" → 命中;"ipad air 11" → null |
searchByTitlePattern | title 任意位置 | Pattern.compile(pat).matcher(title).find() | 敏感 | "Desk" → Study Desk |
searchByPattern | 整条 toString() | 同上,但对象换成 item.toString() | 敏感 | "Apple" → iPad Air 11(品牌字段) |
三个判断:
find()不是matches()。matches()要求整串匹配,"Desk"就永远匹配不到"Study Desk"。searchByPattern只能靠toString()。父类看不见brand/course/weightKg,但这些字段都被子类写进了detail(),而detail()被拼进了toString()——所以「搜得到」是 S4 那个设计的回报。equals不是==。==比引用,换成动态拼接出来的标题就会失配。
8.4 驱动 Step5.java
public class Step5
{
public static void main(String[] args)
{
Market market = buildMarket();
market.list();
System.out.println("size = " + market.size());
System.out.println("total = " + Money.format(market.totalValue()));
System.out.println();
show("searchByTitle(\"iPad Air 11\")", market.searchByTitle("iPad Air 11"));
show("searchByTitle(\"ipad air 11\")", market.searchByTitle("ipad air 11"));
show("searchByTitlePattern(\"Desk\")", market.searchByTitlePattern("Desk"));
show("searchByTitlePattern(\"e\")", market.searchByTitlePattern("e"));
show("searchByPattern(\"Apple\")", market.searchByPattern("Apple"));
show("searchByPattern(\"EIE3320\")", market.searchByPattern("EIE3320"));
show("searchByPattern(\"Nintendo\")", market.searchByPattern("Nintendo"));
}
private static void show(String call, Listing result)
{
if (result == null)
{
System.out.println(call + " -> null");
}
else
{
System.out.println(call + " -> " + result.getTitle());
}
}
static Market buildMarket()
{
Owner chan = new Owner("Chan Tai Man", "6123 4567");
Owner lee = new Owner("Lee Siu Ming", "9876 5432");
Market market = new Market();
market.add(new TextBook("EIE3320 Lecture Notes", 120.0, chan, 14, 31, "EIE3320", 3));
market.add(new Device("iPad Air 11", 4300.0, lee, 9, 5, "Apple", 12));
market.add(new Furniture("Study Desk", 480.0, chan, 21, 47, 120.0, 60.0, 18.0));
market.add(new TextBook("Calculus 8e", 260.0, lee, 8, 0, "AMA1120", 8));
market.add(new Device("Mechanical Keyboard", 350.0, chan, 23, 59, "Keychron", 6));
return market;
}
}
buildMarket()是static且包可见,S6 会直接复用它(Step5.buildMarket()),所以不要把它改成private,也不要改方法名。
8.5 期望输出
ID CATEGORY TITLE PRICE OWNER POSTED
---- ---------- ------------------------ ---------- ------------------ ------------
#1 TextBook EIE3320 Lecture Notes $120.00 Chan Tai Man 2:31 p.m.
#2 Device iPad Air 11 $4,300.00 Lee Siu Ming 9:05 a.m.
#3 Furniture Study Desk $480.00 Chan Tai Man 9:47 p.m.
#4 TextBook Calculus 8e $260.00 Lee Siu Ming 8:00 a.m.
#5 Device Mechanical Keyboard $350.00 Chan Tai Man 11:59 p.m.
size = 5
total = $5,510.00
searchByTitle("iPad Air 11") -> iPad Air 11
searchByTitle("ipad air 11") -> null
searchByTitlePattern("Desk") -> Study Desk
searchByTitlePattern("e") -> EIE3320 Lecture Notes
searchByPattern("Apple") -> iPad Air 11
searchByPattern("EIE3320") -> EIE3320 Lecture Notes
searchByPattern("Nintendo") -> null
8.6 验收清单
- 表格六列全部对齐(
$4,300.00那行最容易把 PRICE 列挤歪) -
total = $5,510.00(120 + 4300 + 480 + 260 + 350) -
"ipad air 11"→null(大小写敏感,证明用了equals) -
searchByPattern("Apple")→iPad Air 11(证明toString()真的把detail()拼进去了) -
searchByPattern("Nintendo")→null(找不到要返回null,不能抛异常、不能返回空对象)
8.7 三个坑
| 坑 | 现象 | 原因 |
|---|---|---|
searchByPattern 搜不到 Apple | 返回 null | toString() 没拼 detail()(S4 漏了),或搜的是 title 而不是 toString() |
searchByTitlePattern("Desk") 返回 null | 找不到 Study Desk | 用了 matches() 而不是 find() |
| 表格错列 | $4,300.00Lee | PRICE 列宽写成 %-9s($4,300.00 是 9 字符,正好顶满、没有间隔) |
9. S6 · Iterator + Comparable + 排序(20 分钟)
目标:把 HW4 Q3(边遍历边删)、HW4 Q6(Comparable + Collections.sort)、Ch14(接口作返回类型) 各练一遍。
9.1 要给 Listing 补的两处
| 改动 | 内容 |
|---|---|
| 类声明 | public abstract class Listing implements Comparable<Listing> |
| 新增方法 | public int compareTo(Listing other) —— 先比 price,相等再比 id |
compareTo 的返回约定:小于 → 负数、相等 → 0、大于 → 正数。所以:
if (price < other.price) return -1;
if (price > other.price) return 1;
return Integer.compare(id, other.id);9.2 要给 Market 加的三个方法
| 方法 | 要求 |
|---|---|
int removeByOwner(String ownerName) | 用 Iterator 遍历,命中就 iterator.remove(),返回删掉几条 |
ArrayList<Shippable> shippableItems() | 遍历 listings,instanceof Shippable 的转型后加进新列表,返回它 |
void sortByPrice() | Collections.sort(listings);(一行) |
9.3 为什么删除必须用 Iterator
错误写法(很多人会这么写):
for (int i = 0; i < listings.size(); i++)
{
if (listings.get(i).getOwner().getName().equals(ownerName))
{
listings.remove(i); // 删完之后 i 已经指向下一个元素,会漏删
}
}删除后后面的元素整体前移,i++ 又跳过一个,于是连续两条同属主时会漏删一条。本项目的数据正好是「Lee 的两条不相邻」——所以这个 bug 不会在本数据上暴露,但换成相邻两条就会。
正确写法(Iterator 的 remove() 是唯一安全方式):
Iterator<Listing> iterator = listings.iterator();
while (iterator.hasNext())
{
Listing item = iterator.next();
if (item.getOwner().getName().equals(ownerName))
{
iterator.remove();
removed++;
}
}连带一个坑:
for (Listing item : listings)里调listings.remove(item)会抛ConcurrentModificationException。增强 for 只能读,不能删。
9.4 为什么 shippableItems() 的返回类型是 ArrayList<Shippable>
因为元素的声明类型是接口,不是 Listing。这样调用方拿到的每个元素都能直接调 shippingFee(),不需要再 instanceof:
ArrayList<Shippable> shippable = market.shippableItems();
for (Shippable item : shippable)
{
item.shippingFee(); // 直接调,无需转型
}这正是 Ch14 p.32「接口也是类型」 的用处,也是 HW5 Q1 返回 Item 的同构写法(那边返回父类,这边返回接口)。
9.5 驱动 Step6.java
import java.util.ArrayList;
public class Step6
{
public static void main(String[] args)
{
Market market = Step5.buildMarket();
System.out.println("before: size = " + market.size());
market.list();
System.out.println();
int removed = market.removeByOwner("Lee Siu Ming");
System.out.println("removeByOwner(\"Lee Siu Ming\") -> removed = " + removed);
market.list();
System.out.println("size = " + market.size()
+ ", total = " + Money.format(market.totalValue()));
System.out.println();
ArrayList<Shippable> shippable = market.shippableItems();
System.out.println("shippable = " + shippable.size());
for (Shippable item : shippable)
{
System.out.println(" fee = " + Money.format(item.shippingFee()));
}
System.out.println();
market.sortByPrice();
System.out.println("after sortByPrice():");
market.list();
}
}
Step6依赖Step5.java(调Step5.buildMarket()),所以这个阶段Step5.java必须留在同一目录里。
9.6 期望输出
before: size = 5
ID CATEGORY TITLE PRICE OWNER POSTED
---- ---------- ------------------------ ---------- ------------------ ------------
#1 TextBook EIE3320 Lecture Notes $120.00 Chan Tai Man 2:31 p.m.
#2 Device iPad Air 11 $4,300.00 Lee Siu Ming 9:05 a.m.
#3 Furniture Study Desk $480.00 Chan Tai Man 9:47 p.m.
#4 TextBook Calculus 8e $260.00 Lee Siu Ming 8:00 a.m.
#5 Device Mechanical Keyboard $350.00 Chan Tai Man 11:59 p.m.
removeByOwner("Lee Siu Ming") -> removed = 2
ID CATEGORY TITLE PRICE OWNER POSTED
---- ---------- ------------------------ ---------- ------------------ ------------
#1 TextBook EIE3320 Lecture Notes $120.00 Chan Tai Man 2:31 p.m.
#3 Furniture Study Desk $480.00 Chan Tai Man 9:47 p.m.
#5 Device Mechanical Keyboard $350.00 Chan Tai Man 11:59 p.m.
size = 3, total = $950.00
shippable = 2
fee = $90.00
fee = $23.50
after sortByPrice():
ID CATEGORY TITLE PRICE OWNER POSTED
---- ---------- ------------------------ ---------- ------------------ ------------
#1 TextBook EIE3320 Lecture Notes $120.00 Chan Tai Man 2:31 p.m.
#5 Device Mechanical Keyboard $350.00 Chan Tai Man 11:59 p.m.
#3 Furniture Study Desk $480.00 Chan Tai Man 9:47 p.m.
9.7 验收清单
-
removed = 2(Lee 的两条) - 删完
size = 3、total = $950.00(120 + 480 + 350) - id 不变:剩下的是
#1 #3 #5,不是重新编号成#1 #2 #3 -
shippable = 2,两条运费$90.00(桌子)与$23.50(键盘:20 + 3.5)——TextBook不在里面 - 排序后是
$120 → $350 → $480(升序),id 顺序变成#1 #5 #3
9.8 三个坑
| 坑 | 现象 | 原因 |
|---|---|---|
排序后 #1 #3 #5 → 变成 #1 #2 #3 | id 变了 | id 是构造时分配的,排序不改变 id;如果变了,说明你把 id 存成了「列表下标」 |
| 用下标删除 | 偶发漏删 | 见 9.3 |
Collections.sort(listings) 编译不过 | no suitable method found for sort(ArrayList<Listing>) | Listing 没写 implements Comparable<Listing> |
10. S7 · 终局验收(15 分钟)
目标:用完整驱动跑一遍,把五份作业的考点一次性串起来。
10.1 驱动 TestMarket.java(这是最终判分器,照抄不许改)
import java.util.ArrayList;
public class TestMarket
{
public static void main(String[] args)
{
Owner chan = new Owner("Chan Tai Man", "6123 4567");
Owner lee = new Owner("Lee Siu Ming", "9876 5432");
Market market = new Market();
market.add(new TextBook("EIE3320 Lecture Notes", 120.0, chan, 14, 31, "EIE3320", 3));
market.add(new Device("iPad Air 11", 4300.0, lee, 9, 5, "Apple", 12));
market.add(new Furniture("Study Desk", 480.0, chan, 21, 47, 120.0, 60.0, 18.0));
market.add(new TextBook("Calculus 8e", 260.0, lee, 8, 0, "AMA1120", 8));
market.add(new Device("Mechanical Keyboard", 350.0, chan, 23, 59, "Keychron", 6));
section("1. list()");
market.list();
section("2. totals");
System.out.println("size = " + market.size());
System.out.println("totalValue = " + Money.format(market.totalValue()));
System.out.println("count(static) = " + Listing.getCount());
section("3. searches");
report("searchByTitle(\"iPad Air 11\")", market.searchByTitle("iPad Air 11"));
report("searchByTitle(\"ipad air 11\")", market.searchByTitle("ipad air 11"));
report("searchByTitlePattern(\"Desk\")", market.searchByTitlePattern("Desk"));
report("searchByPattern(\"Apple\")", market.searchByPattern("Apple"));
report("searchByPattern(\"Nintendo\")", market.searchByPattern("Nintendo"));
section("4. basket");
Basket small = new Basket();
String[] names = {"pen", "ruler", "eraser", "clip", "glue", "tape"};
for (int i = 0; i < names.length; i++)
{
boolean ok = small.add(new TextBook(names[i], 10.0 + i, chan, 10, 0, "EIE3320", 1));
System.out.println("add " + names[i] + " -> " + ok);
}
small.print();
section("5. ship + remove + sort");
ArrayList<Shippable> shippable = market.shippableItems();
System.out.println("shippable = " + shippable.size());
for (Shippable item : shippable)
{
System.out.println(" fee = " + Money.format(item.shippingFee()));
}
System.out.println("removed = " + market.removeByOwner("Lee Siu Ming"));
market.sortByPrice();
market.list();
section("6. detail block of the cheapest listing");
market.searchByTitlePattern("").print();
}
private static void section(String title)
{
System.out.println();
System.out.println("== " + title + " ==");
}
private static void report(String call, Listing result)
{
System.out.println(call + " -> "
+ (result == null ? "null" : result.getTitle()));
}
}两个彩蛋,跑完再回看:
- 第 2 节的
count(static) = 5:Listing.count是类的静态计数器,与 market 里有几条无关。S4 之后TextBook/Device/Furniture都继承它、共用同一个count(ch8 p.56–58)。 - 第 6 节的
searchByTitlePattern(""):空字符串模式能匹配任何标题(find()对空模式恒为真),于是返回第一条——这就是「先排序再取最便宜」的偷懒写法。
10.2 期望输出(54 行 + 前置空行,逐字)
== 1. list() ==
ID CATEGORY TITLE PRICE OWNER POSTED
---- ---------- ------------------------ ---------- ------------------ ------------
#1 TextBook EIE3320 Lecture Notes $120.00 Chan Tai Man 2:31 p.m.
#2 Device iPad Air 11 $4,300.00 Lee Siu Ming 9:05 a.m.
#3 Furniture Study Desk $480.00 Chan Tai Man 9:47 p.m.
#4 TextBook Calculus 8e $260.00 Lee Siu Ming 8:00 a.m.
#5 Device Mechanical Keyboard $350.00 Chan Tai Man 11:59 p.m.
== 2. totals ==
size = 5
totalValue = $5,510.00
count(static) = 5
== 3. searches ==
searchByTitle("iPad Air 11") -> iPad Air 11
searchByTitle("ipad air 11") -> null
searchByTitlePattern("Desk") -> Study Desk
searchByPattern("Apple") -> iPad Air 11
searchByPattern("Nintendo") -> null
== 4. basket ==
add pen -> true
add ruler -> true
add eraser -> true
add clip -> true
add glue -> true
add tape -> false
1: [TextBook] pen ($10.00) by Chan Tai Man | 6123 4567 | Course: EIE3320, ed.1
2: [TextBook] ruler ($11.00) by Chan Tai Man | 6123 4567 | Course: EIE3320, ed.1
3: [TextBook] eraser ($12.00) by Chan Tai Man | 6123 4567 | Course: EIE3320, ed.1
4: [TextBook] clip ($13.00) by Chan Tai Man | 6123 4567 | Course: EIE3320, ed.1
5: [TextBook] glue ($14.00) by Chan Tai Man | 6123 4567 | Course: EIE3320, ed.1
items = 5, total = $60.00
== 5. ship + remove + sort ==
shippable = 3
fee = $63.00
fee = $90.00
fee = $23.50
removed = 2
ID CATEGORY TITLE PRICE OWNER POSTED
---- ---------- ------------------------ ---------- ------------------ ------------
#1 TextBook EIE3320 Lecture Notes $120.00 Chan Tai Man 2:31 p.m.
#5 Device Mechanical Keyboard $350.00 Chan Tai Man 11:59 p.m.
#3 Furniture Study Desk $480.00 Chan Tai Man 9:47 p.m.
== 6. detail block of the cheapest listing ==
#1 [TextBook] EIE3320 Lecture Notes
price : $120.00
owner : Chan Tai Man (6123 4567)
posted : 2:31 p.m. (afternoon)
detail : Course: EIE3320, ed.3
ship : self-pickup
10.3 自动比对(不要用肉眼)
把期望输出存成 expected\step7.txt(或任意名字),然后:
javac -encoding UTF-8 *.java
java TestMarket > mine.txt
Compare-Object (Get-Content expected\step7.txt) (Get-Content mine.txt) -SyncWindow 0没有任何输出 = 完全一致。 有输出时每条会是 <=(期望里有、你没有)或 =>(你多出来),定位很快。
Compare-Object默认忽略行尾空格,这对本题是好事(编辑器会吃掉行尾空格)。但行首空格、列间空格不会被忽略——所以缩进和表格对齐照样能测出来。
10.4 最终验收清单(六条全过 = 可以进考场)
- 六节标题都在,顺序对(
== 1. list() ==…== 6. detail block … ==) - 第 1 节 5 行数据,六列对齐,
$4,300.00不挤压 OWNER 列 - 第 3 节五个搜索结果与期望完全一致(尤其
"ipad air 11" → null与"Apple" → iPad Air 11) - 第 4 节第 6 个
add tape -> false,且items = 5, total = $60.00(没有异常) - 第 5 节
shippable = 3;删完后 id 是#1 #5 #3;排序后价格升序 - 第 6 节
ship : self-pickup(TextBook不实现接口)
11. 卡住了怎么办(分层提示,从 L1 开始看)
建议:每卡住一次,只看一级。看完还想不通再看下一级。三级都看完基本等于答案。
S1 没跑对
L1 · 方向
你的
format少了千分位,或者toString拼错了。
L2 · 结构
format:只有一个表达式,用String.format并且格式串里要有,(分组)与.2(两位小数)。toString:name + " (" + phone + ")",注意括号前有一个空格。
L3 · 关键行
return "$" + String.format("%,.2f", amount);
S2 的 12 小时制不对
L1 · 方向
0和12两个点是特例,其余就是取余。
L2 · 结构
先把 24 小时转成 1–12(用
%),再把 0 改成 12;后缀由原始小时决定(< 12就是 a.m.),不是由转换后的小时决定。
L3 · 关键行
int h12 = postedHour % 12; if (h12 == 0) h12 = 12;return h12 + ":" + String.format("%02d", postedMinute) + " " + (postedHour < 12 ? "a.m." : "p.m.");
S2 的 slot() 报「missing return statement」
L1 · 方向
不是所有路径都
return了。
L2 · 结构
switch的每个分支都return时,仍然需要一个兜底的default: return "night";——因为编译器不检查「switch是否覆盖了全部int」。
L3 · 关键行
default: return "night";
S3 抛 NullPointerException
L1 · 方向
某个循环跑到了
null元素上。
L2 · 结构
items长度 5,但只装了count个;print()与total()的循环上限必须是count。
L3 · 关键行
for (int i = 0; i < count; i++)
S4 编译报「is not abstract and does not override abstract method」
L1 · 方向
某个子类少实现了抽象方法(通常是
detail())。
L2 · 结构
三个子类各要实现
category()与detail()两个方法,且都是public。
L3 · 关键行
public String detail() { return "Course: " + course + ", ed." + edition; }
S4 运费不显示 / 显示成 self-pickup
L1 · 方向
instanceof判断没生效,或者子类没implements Shippable。
L2 · 结构
print()里判断的是this,转型也是转this;Device/Furniture的类声明末尾要有implements Shippable,并实现shippingFee()。
L3 · 关键行
if (this instanceof Shippable) { Shippable s = (Shippable) this; ... s.shippingFee() ... }
S5 表格错列
L1 · 方向
表头与数据行的格式串不一致,或列宽不够。
L2 · 结构
六个宽度依次是
4 / 10 / 24 / 10 / 18 / 不限;表头与数据行用同一个printf格式串;$4,300.00是 9 字符,PRICE 必须给 10。
L3 · 关键行
"%-4s %-10s %-24s %-10s %-18s %s%n"
S5 searchByPattern("Apple") 找不到
L1 · 方向
你搜的范围里没有「品牌」。
L2 · 结构
searchByTitle*只看title;searchByPattern必须搜item.toString(),而toString()里必须拼了detail()。
L3 · 关键行
pattern.matcher(item.toString()).find()
S6 排序不生效 / 编译报 sort
L1 · 方向
Listing没有实现Comparable。
L2 · 结构
类声明加
implements Comparable<Listing>,并实现compareTo(Listing);Collections.sort要求元素类型实现了它。
L3 · 关键行
public int compareTo(Listing other) { if (price < other.price) return -1; if (price > other.price) return 1; return Integer.compare(id, other.id); }
S6 删除漏了一条
L1 · 方向
你用的是下标删除,删完会跳过下一个元素。
L2 · 结构
用
Iterator的remove();或者从后往前删(for (int i = size-1; i >= 0; i--))。但考试要求用Iterator。
L3 · 关键行
while (iterator.hasNext()) { if (...) { iterator.remove(); } }
12. 常见编译错误速查(按报错原文找)
| 报错(原文片段) | 几乎总是 | 修法 |
|---|---|---|
constructor X in class Y cannot be applied to given types | 子类构造器首行没写 super(...),或参数不对 | 对齐父类构造器的参数表 |
X is not abstract and does not override abstract method m() | 子类漏实现抽象方法 / 接口方法 | 补方法,或把子类也声明 abstract |
cannot find symbol: method getXxx() | 忘了写 getter / 大小写拼错 | 补 getter;注意 getTitle 不是 gettitle |
cannot find symbol: class ArrayList | 忘了 import java.util.ArrayList; | 加 import(String/Math 不用,它们在 java.lang) |
incompatible types: Object cannot be converted to Listing | 裸 ArrayList / 裸 Iterator 没写泛型 | ArrayList<Listing> / Iterator<Listing> |
incompatible types: possible lossy conversion from double to int | 把 double 赋给 int | 要么改类型,要么显式 (int) |
attempting to assign weaker access privileges | 实现接口/覆盖抽象方法时写了 protected 或包可见 | 一律 public |
missing return statement | 有分支没 return | 补 default: return ...; |
non-static variable listings cannot be referenced from a static context | 在 main 里直接用了实例字段/实例方法 | 先 new 对象再调;或把方法改 static |
class X is public, should be declared in a file named X.java | 文件名与 public class 名不一致 | 文件名严格等于类名(大小写也算) |
reached end of file while parsing | 少一个大括号 } | 数 {};用 IDE 的自动缩进(Auto layout)定位 |
variable price might not have been initialized | final 字段没在构造器里赋值 | 构造器覆盖全部分支,或声明时给初值 |
Exception ... ConcurrentModificationException(运行时) | 增强 for 里删元素 | 改用 Iterator.remove() |
Exception ... ClassCastException(运行时) | 转型前没 instanceof | 先判断再转 |
13. 时间预算与二刷建议
13.1 单次通刷(约 2 小时 15 分)
| 阶段 | 预算 | 超时说明 |
|---|---|---|
| S0 环境 | 5 min | 超过 10 分钟说明环境有问题,先解决环境 |
| S1 Owner/Money | 10 min | 千分位想不出来就直接看 L3 |
| S2 Listing | 20 min | 12 小时制是重点,慢一点值得 |
| S3 Basket | 15 min | 这阶段纯复习 HW3,应该最快 |
| S4 继承 + 抽象 + 接口 | 25 min | 最该花时间的 25 分钟 |
| S5 Market + 三搜索 | 25 min | 表格对齐耗时可跳过(抄 §8.2) |
| S6 Iterator / Comparable | 20 min | compareTo 想不清就看 L3 |
| S7 终局 | 15 min | 对齐输出 + 逐条核对清单 |
13.2 二刷(只重做四个「手会生」的点)
考前第二次不要重写全部,只做这四个,每个 10 分钟:
postedTime()—— 12 小时制,写 5 个拐点自测(0:05、11:59、12:00、14:31、23:59)。print()的instanceof Shippable—— 接口 + 转型 + 运费。- 三个搜索的差别 —— 在纸上写三行伪代码:
equals/title.find/toString.find。 Iterator.remove()—— 默写那四行while。
13.3 从「能跑」到「能考」:把代码翻译成答案
这个项目不只练手,它的每个部分都能直接变成考试答案:
| 考试可能问 | 用本项目的哪段回答 |
|---|---|
| 「抽象类和接口的区别」 | Listing 有字段/构造器/具体方法(抽象类);Shippable 只有签名(接口) |
| 「为什么需要 abstract 方法」 | print() 一份实现 + category() 由子类决定 |
| 「多态的实际用处」 | market.list() 一行 item.category() 处理三种类型 |
| 「向下转型为什么必要」 | print() 里不转型就调不到 shippingFee() |
「== 和 equals」 | searchByTitle 必须 equals |
| 「Iterator 的优势」 | removeByOwner 边遍历边删 |
「为什么 ArrayList<Listing> 不用强转」 | S5 里 listings.get(i).getPrice() 直接可用 |
| 「静态成员属于谁」 | Listing.getCount() 统计三种子类共创建了多少条 |
14. 自测:合上代码,回答这 8 个问题
Listing为什么必须是abstract?如果去掉abstract只留两个抽象方法会怎样?print()里为什么不能写if (category().equals("Device"))?TextBook为什么不实现Shippable?如果强行让它实现,shippingFee()该怎么写才合理?searchByPattern("Apple")能命中,靠的是哪三件事串起来?(提示:字段私有 →detail()→toString()→ 动态绑定)removeByOwner用下标删会漏删,为什么?举一个会漏的具体数据。sortByPrice()之后#3排到了最后,为什么 id 没变?Step2与Step3在 S4 之后为什么编译不过?- 如果把
Market的ArrayList<Listing>换成Listing[],add方法要改什么?(提示:回到 S3)
第 8 题的答案就是「为什么标准库要用
ArrayList」——数组要自己扩容、自己维护条数,而ArrayList替你做了。这也是 ch11 p.88 那四条痛点的由来。
链接
配套精读
- eie3320 homework 精读(HW1–5) — 本题每一步的「作业原型」都在这里;S4 对 HW4 Q2,S5 对 HW5 Q1,S6 对 HW4 Q3/Q6
- eie3320 课件精读(ch11 / ch14) — S4/S6 的语法依据:ch11 p.19–46(
super/覆盖)、p.50–71(多态/转型)、p.78–97(泛型/ArrayList)、ch14 p.3–17(抽象类)、p.19–29(接口)、p.30–43(对比与List) - eie3320 ch09 walkthrough — S5 的
Pattern/Matcher在它 p.26–30
课堂笔记
- EIE3320 Week 1 — S0–S3 的基础:
javac、printf、switch、for、方法 - EIE3320 Week 2 — S1–S3:构造器、
static、可见性、传对象 - EIE3320 Week 3 — S1:不可变对象(
Owner全final) - EIE3320 Week 4 — S5:
Pattern、String比较
素材
| 文件 | 说明 |
|---|---|
EIE3320 HW 1..5 | 五份作业原件;本项目的每阶段都对应其中一段 |
dome-v2 / auction 项目 | 与本项目同构的原始 BlueJ 工程,可对照看结构 |