0 · 使用说明与考点地图
0.1 这门课怎么考
WADV_SecA_A0123456X,每个 Section 单独交
特点没有一道背诵题,全部是“给骨架/场景,补代码”
0.2 70 分怎么分布
| Section | 真题 | 小题分值 | 本手册 |
|---|---|---|---|
| A · Java 编程 | BPE 分词:补 4 个静态方法 | 5×4 | §1 |
| B · SQL | 大学库 5 问(索引/连表/聚合/自连接/视图) | 2/2/3/4/4 | §2 |
| C · OOP | Employee 抽象类 + IPayable 接口 | 5/3/3/4 | §3 |
| D · Spring | WeLoveBooks:entity/repo/controller/Thymeleaf/REST | 6/2/2/5/5 | §4 |
样卷结构相同:A 密接追踪(C#,6/2/6/6)、B Tech Ever(5×3)、C Person 家族(5/5/5)、D Minishop(4/2/3/3/3 + Q5 3/2)。
0.3 四节的“题型指纹”
| Section | 看到什么就开始套 | 拿分核心 |
|---|---|---|
| A | “complete the following four methods”、给 main、给方法规格表 | 不改 main;方法签名一字不差;边界(空、长度 1) |
| B | 给 3–5 张表的结构 + “Assume MS SQL Server” | JOIN 写全、GROUP BY 完整、HAVING 而非 WHERE |
| C | 给 abstract class / interface + 期望输出 | 构造器签名、super、toString 格式逐字对齐 |
| D | 给业务场景 + 架构图 + 要你写五层代码 | 注解齐全(@Entity/@Id/@ManyToOne…)、方法签名对 |
0.4 时间分配(150 分钟作答)
| Section | 建议 | 理由 |
|---|---|---|
| C OOP | 25 min | 最套路化,先拿稳 |
| B SQL | 25 min | 5 小题,会就会 |
| A Java | 40 min | 要写完整方法体 |
| D Spring | 45 min | 5 小问,代码量大 |
| 检查 | 15 min | 命名、导 PDF、上传 |
1 · Section A:Java 编程补全
真题 20 分(4 个方法各 5 分)|样卷 20 分(6/2/6/6)。预算 40 分钟。
1.1 这类题的固定套路
- 先读 main:main 已经写好,它告诉你方法的调用顺序与参数,也告诉你方法必须返回什么类型。
- 再读方法规格表:每个方法的输入、输出、提示(如 “substring(i, i+2) can be used”)。
- 只写方法体,签名一个字都不能改(考试判分靠签名匹配)。
- 三处边界必查:空串/空数组、长度 1、找不到(返回 null 还是 -1)。
- 写完在草稿上跑一遍题目给的例子(
aaabdaaabac → zabdzabac),对不上就是逻辑错。
1.2 例题|真题 Q1:BPE 分词(20 分)
main() and four user-defined methods. Complete the four methods only; assume all inputs are valid.For input
"aaabdaaabac": adjacent pairs are aa, aa, ab, bd, da, aa, aa, ab, ba, ac (N−1 pairs); the most frequent pair is “aa” (4 times); replacing it with “z” gives "zabdzabac".Method specification
•
extractPairs(String text) → String[]: extracts all adjacent pairs; hint: substring(i, i+2). (5)•
countNumber(String[] pairs, String[] uniquePairs, int[] counts) → int: counts each unique pair into the two arrays and returns the number of unique pairs. (5)•
getMostFrequentPair(String[] uniquePairs, int[] counts, int uniqueCount) → String: returns the most frequent pair. (5)•
replacePair(String text, String pair, String symbol) → String: replaces the pair; hint: replace(pair, symbol). (5)static main() 的类,要求只补全四个自定义方法,假设输入全部合法。对输入
"aaabdaaabac":相邻对共 N−1=10 个(aa,aa,ab,bd,da,aa,aa,ab,ba,ac);最高频是 “aa”(4 次);把 “aa” 替换成 “z” 得到 "zabdzabac"。四个方法及分值见上(各 5 分)。
- 四个方法是一条流水线:抽对 → 计数 → 找最大 → 替换。谁都不依赖谁的结果,所以每个方法都可以独立拿 5 分——不要因为前面卡住就空着后面。
countNumber的返回值是“唯一对的个数”,并且要同时填uniquePairs与counts两个数组,这是本题最容易写错的地方(很多人只填一个)。extractPairs必须处理text.length() < 2,否则new String[length-1]会抛 NegativeArraySizeException。replacePair一行就够——题目已经提示用replace,不要自己写循环。
// Step 1: 抽出全部相邻对
public static String[] extractPairs(String text) {
int n = text.length();
if (n < 2) return new String[0]; // 边界:空串或单字符
String[] pairs = new String[n - 1];
for (int i = 0; i < n - 1; i++) {
pairs[i] = text.substring(i, i + 2);
}
return pairs;
}
// Step 2: 统计每个唯一对的出现次数,返回唯一对个数
public static int countNumber(String[] pairs, String[] uniquePairs, int[] counts) {
int uniqueCount = 0;
for (String p : pairs) {
int idx = -1;
for (int i = 0; i < uniqueCount; i++) { // 在已记录的唯一对里找
if (uniquePairs[i].equals(p)) { idx = i; break; }
}
if (idx == -1) { // 新出现的对
uniquePairs[uniqueCount] = p;
counts[uniqueCount] = 1;
uniqueCount++;
} else {
counts[idx]++;
}
}
return uniqueCount;
}
// Step 3: 找出现次数最多的对
public static String getMostFrequentPair(String[] uniquePairs, int[] counts, int uniqueCount) {
if (uniqueCount == 0) return null;
int best = 0;
for (int i = 1; i < uniqueCount; i++) {
if (counts[i] > counts[best]) best = i; // 严格大于:并列时取先出现的
}
return uniquePairs[best];
}
// Step 4: 替换
public static String replacePair(String text, String pair, String symbol) {
return text.replace(pair, symbol);
}
运行结果:Original text: aaabdaaabac / Most frequent pair: aa / New text: zabdzabac。
extractPairs 没处理 n<2(抛异常);② countNumber 只返回个数、没填 uniquePairs/counts(考官会检查 main 后续能否用);③ getMostFrequentPair 直接把 counts[0] 当最大而没有循环比较;④ 改了方法签名或 main。1.3 例题|样卷 Q1:密接追踪(C#,20 分)
AllRecords (ID, Name, Entry timestamp, Exit timestamp, Location). Two people are “linked” if they were at the same location and their activity periods overlap. Complete four methods:•
GetRecord(string inId) → string[]: return the row of the given ID, or null if not found. (6)•
WasAtTheSameLocation(string candLoc, string targetLoc) → bool. (2)•
DidActivityOverlap(DateTime cEntry, DateTime cExit, DateTime tEntry, DateTime tExit) → bool; you must evaluate all possible scenarios. (6)•
PrintLinkedIds(string targetId, DateTime tEntry, DateTime tExit, string tLoc): print every linked attendee’s ID. (6)AllRecords(ID、姓名、进场时间、离场时间、地点)。两人被视为“密接”的条件是同一地点且活动时间重叠。补全四个方法(分值见上)。- 时间重叠不要分情况写 if-else 五种,用一句判据:候选进场 < 目标离场 且 候选离场 > 目标进场,所有重叠情形都成立,边界(正好相等)不重叠。
GetRecord返回的是一维数组(二维数组的一行),找不到返回null——main 里就是用 null 判断“无效 ID”。- 三维数组的第二维用
GetLength(1),别写死 5。
private static string[] GetRecord(string inId) {
for (int r = 0; r < AllRecords.GetLength(0); r++) {
if (AllRecords[r, 0] == inId) {
string[] row = new string[AllRecords.GetLength(1)];
for (int c = 0; c < AllRecords.GetLength(1); c++) row[c] = AllRecords[r, c];
return row;
}
}
return null; // 找不到
}
private static bool WasAtTheSameLocation(string candLoc, string targetLoc) {
return string.Equals(candLoc, targetLoc, StringComparison.OrdinalIgnoreCase);
}
private static bool DidActivityOverlap(DateTime cEntry, DateTime cExit,
DateTime tEntry, DateTime tExit) {
return cEntry < tExit && cExit > tEntry; // 交集非空
}
private static void PrintLinkedIds(string targetId, DateTime tEntry,
DateTime tExit, string tLocation) {
for (int r = 0; r < AllRecords.GetLength(0); r++) {
string id = AllRecords[r, 0];
if (id == targetId) continue; // 跳过自己
string loc = AllRecords[r, 4];
DateTime cEntry = GetTimeFromTimeStampString(AllRecords[r, 2]);
DateTime cExit = GetTimeFromTimeStampString(AllRecords[r, 3]);
if (WasAtTheSameLocation(loc, tLocation) && DidActivityOverlap(cEntry, cExit, tEntry, tExit)) {
Console.WriteLine(id + " is linked with " + targetId);
}
}
}
输出格式要与 Figure 2 一致:11236375 is linked with 11235929。
1.4 模拟题 1|统计与众数(5 分)
int mostFrequent(int[] values): return the value that appears most often; if there is a tie, return the smaller value. Assume the array is not empty.int mostFrequent(int[] values):返回出现次数最多的值;并列时返回较小的值。假设数组非空。public static int mostFrequent(int[] values) {
int bestVal = values[0], bestCount = 0;
for (int i = 0; i < values.length; i++) {
int c = 0;
for (int j = 0; j < values.length; j++) if (values[j] == values[i]) c++;
if (c > bestCount || (c == bestCount && values[i] < bestVal)) {
bestCount = c; bestVal = values[i];
}
}
return bestVal;
}
O(n²) 在这类考试里完全可以接受;用 HashMap 也可以,但要多写 5 行且容易在并列规则上出错。
1.5 模拟题 2|字符串压缩(5 分)
String compress(String s): replace each run of the same character with the character followed by its count, e.g. "aaabbc" → "a3b2c1". If the compressed string is not shorter than the original, return the original.String compress(String s):把连续相同字符替换为“字符+次数”,如 "aaabbc" → "a3b2c1";若压缩后不更短则返回原串。public static String compress(String s) {
if (s == null || s.isEmpty()) return s;
StringBuilder sb = new StringBuilder();
int count = 1;
for (int i = 1; i <= s.length(); i++) {
if (i < s.length() && s.charAt(i) == s.charAt(i - 1)) {
count++;
} else {
sb.append(s.charAt(i - 1)).append(count);
count = 1;
}
}
return sb.length() < s.length() ? sb.toString() : s;
}
关键在 i <= s.length():多跑一格才能把最后一组收尾,否则会漏掉末尾。
1.6 模拟题 3|时间段重叠小时数(5 分)
long overlapHours(LocalDateTime aStart, LocalDateTime aEnd, LocalDateTime bStart, LocalDateTime bEnd): return the number of whole hours that the two intervals overlap; return 0 if they do not overlap.long overlapHours(...):返回两个时间段重叠的整小时数;不重叠返回 0。public static long overlapHours(LocalDateTime aStart, LocalDateTime aEnd,
LocalDateTime bStart, LocalDateTime bEnd) {
LocalDateTime start = aStart.isAfter(bStart) ? aStart : bStart; // 较晚的开始
LocalDateTime end = aEnd.isBefore(bEnd) ? aEnd : bEnd; // 较早的结束
if (!start.isBefore(end)) return 0; // 无交集
return java.time.Duration.between(start, end).toHours();
}
与样卷密接题的判据同源:交集 = max(开始) < min(结束)。差别只是这里要算长度。
1.7 常见错误
| 错误 | 后果 |
|---|---|
| 改了 main 或方法签名 | 可能整段 0 分(评分按签名定位) |
| 边界没处理(空、长度 1) | 编译过但运行时异常,扣 1–2 |
| 返回类型写错(void vs String) | 编译失败,整题连锁失分 |
| 用 == 比较字符串 | 内容相同也不相等,逻辑错 |
| 输出格式与样例不一致(多空格/少换行) | “输出一致”那 1–2 分 |
2 · Section B:SQL(MS SQL Server)
真题 15 分(2/2/3/4/4)|样卷 15 分(5×3)。预算 25 分钟。
2.1 五类必考小问
| 问法 | 考什么 | 模板 |
|---|---|---|
| Which attribute can be used as an index? Explain. | 索引选择性 | 选 WHERE 里最常用、取值最离散、更新最少的列 |
| List … taught by “Dr. Smith”,需排序 | 多表 JOIN + ORDER BY | FROM A JOIN B ON … JOIN C ON … |
| Courses with at least two students who scored “A” | 聚合 + HAVING | GROUP BY … HAVING COUNT(*) >= 2 |
| Course with its prerequisite and the number of students enrolled in both | 自连接 + 双次 JOIN 同一张表 | 见 2.3(d) |
| Create a view … then query it | 视图 + 基于视图查询 | CREATE VIEW … AS SELECT … |
| Write a stored procedure … and a statement to test it | 过程 + EXEC | 见 2.4(d) |
2.2 例题|真题 Q2:大学库(15 分)
a) Which attribute in the Course table can be used as an index? Explain. (2)
b) Write a SQL statement to list all students who are enrolled in courses taught by “Dr. Smith”. Output StudentId, StudentName and CourseName, in descending order of StudentName. (2)
c) Write a SQL statement to find the courses with the number of students who scored grade “A”. Only include courses where at least two students scored “A”. Output CourseId, CourseName, InstructorId and A_Students. (3)
d) Write a SQL statement to list each course with its prerequisite and the number of students who are enrolled in both the course and its prerequisite. Output CourseName, PrerequisiteCourseName and StudentsEnrolledInBothCourses. (4)
e) Write SQL statements to create a user view StudentCourseDetails (StudentId, StudentName, CourseId, CourseName, EnrolDate), then query it to list only the students enrolled in ‘Database Systems’. (4)
a) Course 表哪个属性适合做索引?说明理由。(2)
b) 列出所有选修 “Dr. Smith” 所授课程的学生,输出学号、姓名、课程名,按姓名降序。(2)
c) 统计每门课得 “A” 的学生人数,只保留至少 2 人的课程。(3)
d) 列出每门课及其先修课,以及同时修了两者的学生人数。(4)
e) 创建视图 StudentCourseDetails 并查询选修 ‘Database Systems’ 的学生。(4)
- (a) 不是问“哪个是主键”,而是问哪个列值得建索引:看三点——选择性高(取值离散)、WHERE 里常用、更新少。CourseName 三条都满足;Credits 只有 2/3/4 三种取值,选择性低。
- (b) 需要三跳:Student → Enrolment → Course → Instructor。少一跳就查不到“Dr. Smith”。
- (c) “只保留”=HAVING,不是 WHERE。WHERE 在分组前过滤行,HAVING 在分组后过滤组。
- (d) 是自连接(Course ↔ Course 通过 PrerequisiteId),再各自 JOIN 一次 Enrolment,最后按学生 ID 相等来数“同时修”。
- (e) 视图列名要与题目一致(StudentId/StudentName/CourseId/CourseName/EnrolDate),第二个语句直接查视图。
(a) CourseName。理由三条(写两条即可):选择性高(课程名几乎唯一,索引能把候选行压到 1 行);查询频繁(第 (b)(e) 问都按名称过滤);更新频率低。反例:Credits 只有 2/3/4 三种取值,选择性低,建索引帮助有限。
-- (b)
SELECT s.StudentId, s.StudentName, c.CourseName
FROM Student s
JOIN Enrolment e ON e.StudentId = s.StudentId
JOIN Course c ON c.CourseId = e.CourseId
JOIN Instructor i ON i.InstructorId = c.InstructorId
WHERE i.InstructorName = 'Dr. Smith'
ORDER BY s.StudentName DESC;
-- (c)
SELECT c.CourseId, c.CourseName, c.InstructorId, COUNT(*) AS A_Students
FROM Course c
JOIN Enrolment e ON e.CourseId = c.CourseId
WHERE e.Grade = 'A'
GROUP BY c.CourseId, c.CourseName, c.InstructorId
HAVING COUNT(*) >= 2;
-- (d) 自连接:c 是课程,p 是它的先修课;e1/e2 分别修两门课
SELECT c.CourseName,
p.CourseName AS PrerequisiteCourseName,
COUNT(DISTINCT e1.StudentId) AS StudentsEnrolledInBothCourses
FROM Course c
JOIN Course p ON p.CourseId = c.PrerequisiteId
JOIN Enrolment e1 ON e1.CourseId = c.CourseId
JOIN Enrolment e2 ON e2.CourseId = p.CourseId AND e2.StudentId = e1.StudentId
GROUP BY c.CourseName, p.CourseName;
-- (e)
CREATE VIEW StudentCourseDetails AS
SELECT s.StudentId, s.StudentName, c.CourseId, c.CourseName, e.EnrolDate
FROM Student s
JOIN Enrolment e ON e.StudentId = s.StudentId
JOIN Course c ON c.CourseId = e.CourseId;
SELECT * FROM StudentCourseDetails WHERE CourseName = 'Database Systems';
WHERE COUNT(*) >= 2(聚合函数不能出现在 WHERE);② (d) 忘记第二套 Enrolment,只数了修主课的人数;③ (e) 视图里漏了 EnrolDate,或第二个查询重新 join 一遍而不是查视图。2.3 例题|样卷 Q2:Tech Ever 并购(15 分)
a) Write a SQL statement to retrieve the department name of the employee with the name ‘Jenny Lawrence’. (3)
b) The IT manager suspects there are orphaned records. Write a SQL statement to identify employees with no associated department record (list employee id and name). (3)
c) Retrieve the name of departments where the allocated budget for all projects is more than $1,000,000 and which have more than 5 staffs. (3)
d) Write a stored procedure to update the daily consultation rate of an employee given an employee id and the new rate; also write the statement(s) to test it. (3)
e) Create a user view DepartmentEmployeeView (department name, employee name, daily consultation rate), then retrieve all employee names with rate > $500. (3)
a) 查出 Jenny Lawrence 所属部门名;b) 找出没有部门记录的员工;c) 找出“所有项目预算都超过 100 万”且员工数 > 5 的部门;d) 写一个按员工号更新日咨询费的存储过程并测试;e) 建视图并查费率 > 500 的员工。
- (b) 找“孤儿记录”不能用 INNER JOIN(会丢),要用 LEFT JOIN + IS NULL,或
NOT EXISTS。 - (c) 的 “all projects … more than 1,000,000” 是 ALL 语义:等价于
MIN(budget) > 1000000。写成SUM > 1,000,000是常见曲解。 - (d) 存储过程里用参数化 UPDATE;测试用 EXEC + 一条 SELECT 验证。
- (e) 视图把三列命名清楚,第二个查询只查视图。
-- (a)
SELECT d.Name
FROM Employee e
JOIN Department d ON d.DepartmentID = e.DepartmentID
WHERE e.Name = 'Jenny Lawrence';
-- (b) 孤儿记录:左连接后右表为空
SELECT e.EmployeeID, e.Name
FROM Employee e
LEFT JOIN Department d ON d.DepartmentID = e.DepartmentID
WHERE d.DepartmentID IS NULL;
-- (c) ALL 语义 + 人数统计
SELECT d.Name
FROM Department d
JOIN DepartmentProject dp ON dp.DepartmentId = d.DepartmentID
JOIN Employee e ON e.DepartmentID = d.DepartmentID
GROUP BY d.DepartmentID, d.Name
HAVING MIN(dp.DepProjBudget) > 1000000
AND COUNT(DISTINCT e.EmployeeID) > 5;
-- (d)
CREATE PROCEDURE usp_UpdateConsultRate
@EmployeeID varchar(6),
@NewRate int
AS
BEGIN
UPDATE Employee
SET DailyConsultRate = @NewRate
WHERE EmployeeID = @EmployeeID;
END;
GO
-- 测试
EXEC usp_UpdateConsultRate 'E001', 750;
SELECT EmployeeID, Name, DailyConsultRate FROM Employee WHERE EmployeeID = 'E001';
-- (e)
CREATE VIEW DepartmentEmployeeView AS
SELECT d.Name AS DepartmentName,
e.Name AS EmployeeName,
e.DailyConsultRate
FROM Employee e
JOIN Department d ON d.DepartmentID = e.DepartmentID;
SELECT EmployeeName FROM DepartmentEmployeeView WHERE DailyConsultRate > 500;
2.4 模拟题|三题连做(各 5 分)
1) For each department, list the instructor with the highest number of courses, using a window function. (5)
2) List students who have never enrolled in any course. (5)
3) Using a CTE, compute each course’s average grade point (A=4, B=3, C=2, D=1, F=0) and list only courses with an average below 2.5. (5)
-- 1) 窗口函数排名(MS SQL)
WITH CourseCount AS (
SELECT i.InstructorId, i.InstructorName, i.Department,
COUNT(c.CourseId) AS CourseCount,
ROW_NUMBER() OVER (PARTITION BY i.Department
ORDER BY COUNT(c.CourseId) DESC) AS rn
FROM Instructor i
LEFT JOIN Course c ON c.InstructorId = i.InstructorId
GROUP BY i.InstructorId, i.InstructorName, i.Department
)
SELECT Department, InstructorName, CourseCount
FROM CourseCount
WHERE rn = 1;
-- 2) 从未选课的学生
SELECT s.StudentId, s.StudentName
FROM Student s
WHERE NOT EXISTS (SELECT 1 FROM Enrolment e WHERE e.StudentId = s.StudentId);
-- 3) CTE 计算平均绩点
WITH GradePoint AS (
SELECT CourseId,
AVG(CASE Grade WHEN 'A' THEN 4 WHEN 'B' THEN 3
WHEN 'C' THEN 2 WHEN 'D' THEN 1
WHEN 'F' THEN 0 END) AS AvgPoint
FROM Enrolment
WHERE Grade IS NOT NULL
GROUP BY CourseId
)
SELECT c.CourseId, c.CourseName, gp.AvgPoint
FROM GradePoint gp
JOIN Course c ON c.CourseId = gp.CourseId
WHERE gp.AvgPoint < 2.5
ORDER BY gp.AvgPoint;
注意 AVG 会忽略 NULL,但 CASE 遇到 NULL 也返回 NULL,所以要显式写 WHERE Grade IS NOT NULL,否则分母对但语义含糊。
2.5 常见错误
| 错误 | 后果 |
|---|---|
| 聚合条件写在 WHERE | 语法错误(“An aggregate may not appear in the WHERE clause”) |
| GROUP BY 漏列(SELECT 里有未聚合列) | MS SQL 直接报错 |
| 用 INNER JOIN 找孤儿 | 结果恒为空 |
| 多对多连接后 COUNT(*) 重复计数 | 用 COUNT(DISTINCT …) |
| 视图忘了列别名 | 列名与题目要求不符,扣分 |
| 存储过程忘了 GO 或 EXEC | 测试语句不执行 |
3 · Section C:OOP(抽象类 + 接口)
真题 15 分(5/3/3/4)|样卷 15 分(5/5/5)。预算 25 分钟。
3.1 这类题的固定套路
- 从 main 反推签名:main 里
new SalariedEmployee("Alice","E001",3000)告诉你构造器参数顺序与类型。 - 从期望输出反推方法:输出里有
Employee (Salaried): Alice, ID: E001,就说明某处要String.format("Employee (%s): %s, ID: %s", …)。 - super 先行:子类构造器第一行永远
super(...);父类字段 private 就用 getter。 - 接口方法全部实现:少一个就编译错误,整题连锁失分。
- 格式逐字对齐:逗号、空格、$、小数点(
3000.0而不是3000)都算分。
3.2 例题|真题 Q3:Employee / IPayable(15 分)
abstract class Employee { private String name; private String employeeId; public Employee(String name, String employeeId){…} public abstract String getEmployeeType(); }SalariedEmployee and HourlyEmployee inherit from Employee. SalariedEmployee has a private monthlySalary (double), constructor (name, employeeId, monthlySalary), and implements getEmployeeType() returning "Salaried". HourlyEmployee has private hourlyRate (double) and hoursWorked (int), constructor (name, employeeId, hourlyRate, hoursWorked), and returns "Hourly".(a) Show your implementation for the two classes. (5)
The interface
IPayable { double calPaymentAmt(); String getPaymentDetails(); } is used for payable entities. PaymentSystem calls:System.out.println(p.getPaymentDetails()); System.out.println("Payment Amount: $" + p.calPaymentAmt());and produces:
Employee (Salaried): Alice, ID: E001 / Payment Amount: $3000.0 / Employee (Hourly): Bob, ID: E002 / Payment Amount: $2400.0(b) Modify the Employee class to implement IPayable. It must remain abstract. (3)
(c)
getPaymentDetails() returns "Employee ({EMPLOYEE_TYPE}): {NAME}, ID: {EMPLOYEE_ID}". Show its implementation in the appropriate class. (3)(d)
calPaymentAmt(): for salaried employees it is monthlySalary; for hourly employees it is hourlyRate × hoursWorked. Show its implementation in the appropriate classes. (4)(a) 写出两个子类;(3 分起) (b) 让 Employee 实现 IPayable 且保持抽象;(c) getPaymentDetails() 返回格式如上,写在合适的类里;(d) calPaymentAmt() 在两个子类里分别实现。
- 三处设计判断:getPaymentDetails 放父类(只用 name/id/type,三个子类通用),calPaymentAmt 放子类(算法不同)。这正是“能上提就上提”的模板方法思路。
- 父类字段是 private,所以
getPaymentDetails()要用name就必须写在同一类里(可以,因为它就在 Employee 内),不要去改父类字段可见性。 Payment Amount: $3000.0说明calPaymentAmt()返回 double,且没有任何格式化——不要用%.2f。- Hourly 的
2400.0= 20 × 120,注意hoursWorked是 int,乘法会自动提升为 double。
// (a) 两个子类
public class SalariedEmployee extends Employee {
private double monthlySalary;
public SalariedEmployee(String name, String employeeId, double monthlySalary) {
super(name, employeeId);
this.monthlySalary = monthlySalary;
}
@Override public String getEmployeeType() { return "Salaried"; }
// (d)
@Override public double calPaymentAmt() { return monthlySalary; }
}
public class HourlyEmployee extends Employee {
private double hourlyRate;
private int hoursWorked;
public HourlyEmployee(String name, String employeeId, double hourlyRate, int hoursWorked) {
super(name, employeeId);
this.hourlyRate = hourlyRate;
this.hoursWorked = hoursWorked;
}
@Override public String getEmployeeType() { return "Hourly"; }
// (d)
@Override public double calPaymentAmt() { return hourlyRate * hoursWorked; }
}
// (b) 父类实现接口,仍保持 abstract
public abstract class Employee implements IPayable {
private String name;
private String employeeId;
public Employee(String name, String employeeId) {
this.name = name;
this.employeeId = employeeId;
}
public abstract String getEmployeeType();
// (c) 三段信息都在父类,写在这里,子类不用重复
@Override
public String getPaymentDetails() {
return String.format("Employee (%s): %s, ID: %s",
getEmployeeType(), name, employeeId);
}
}
为什么能编过:Employee 声明实现 IPayable,只实现了 getPaymentDetails(),calPaymentAmt() 留给子类——抽象类允许有未实现的接口方法。
3.3 例题|样卷 Q3:Person 家族(15 分)
a) Class Person has attributes name, year of birth and email; all can be read and updated. Implement Person with appropriate constructors so that:
new Person("[email protected]","Albert Einstein",1879) and new Person("[email protected]","Socrates") both work, and Display() prints Email: …, Name: …, Year of Birth: 1879 (or omits the year when unknown). (5)b) Employee derives from Person (adds salary), Faculty derives from Employee (adds tenured flag), Student derives from Person (adds major). All attributes readable and updatable.
System.out.println(obj) must print:Email: [email protected], Name: Cormac, Year of Birth: 2002, Salary: 3000…, Salary: 4000, Is Tenure: True / …, Year of Birth: 1999, Major: Computing. (5)c) Class PersonelRecords allows adding instances of Person, Employee, Faculty or Student and displaying all of them in insertion order. (5)
a) Person(姓名、出生年、邮箱,均可读写),构造器支持“有年份”和“无年份”两种;Display() 按格式输出。(5)
b) Employee 继承 Person(加工资),Faculty 继承 Employee(加是否终身教职),Student 继承 Person(加专业);println 输出格式见上。(5)
c) PersonelRecords 可添加任意 Person 子类并按添加顺序显示。(5)
- “避免重复代码”是明写的评分点:父类 toString() 拼公共部分,子类 super.toString() + 自己的部分。
- Person 有两个构造器:三参和两参;两参构造器应委托给三参(
this(email, name, 0)之类),但“无年份”时不能打印 0,所以内部用一个boolean hasYear或把 year 设为 0 后判断。 - Display() 与 toString() 的分工:Display 负责打印(可复用于 PersonelRecords),toString 负责拼格式。最省事的写法是 Display 调
System.out.println(this)。 - PersonelRecords 用
List<Person>存,遍历调用各自的 toString() → 多态分派。
public class Person {
private String email;
private String name;
private int yearOfBirth; // 0 表示未提供
public Person(String email, String name, int yearOfBirth) {
this.email = email; this.name = name; this.yearOfBirth = yearOfBirth;
}
public Person(String email, String name) { this(email, name, 0); } // 委托,避免重复
public String getEmail() { return email; }
public String getName() { return name; }
public int getYearOfBirth() { return yearOfBirth; }
public void setEmail(String e) { this.email = e; }
public void setName(String n) { this.name = n; }
public void setYearOfBirth(int y) { this.yearOfBirth = y; }
@Override
public String toString() {
String base = "Email: " + email + ", Name: " + name;
return yearOfBirth > 0 ? base + ", Year of Birth: " + yearOfBirth : base;
}
public void Display() { System.out.println(this); } // 依赖 toString,子类自动适配
}
public class Employee extends Person {
private double salary;
public Employee(String email, String name, int yob, double salary) {
super(email, name, yob);
this.salary = salary;
}
public double getSalary() { return salary; }
public void setSalary(double s) { this.salary = s; }
@Override
public String toString() { return super.toString() + ", Salary: " + salary; }
}
public class Faculty extends Employee {
private boolean tenured;
public Faculty(String email, String name, int yob, double salary, boolean tenured) {
super(email, name, yob, salary);
this.tenured = tenured;
}
@Override
public String toString() { return super.toString() + ", Is Tenure: " + tenured; }
}
public class Student extends Person {
private String major;
public Student(String email, String name, int yob, String major) {
super(email, name, yob);
this.major = major;
}
@Override
public String toString() { return super.toString() + ", Major: " + major; }
}
public class PersonelRecords {
private final List<Person> people = new ArrayList<>();
public void Add(Person p) { people.add(p); }
public void Display() {
for (Person p : people) {
System.out.println(p); // 多态:调用各自的 toString()
}
}
}
输出大小写要对:Is Tenure: true 还是 True?题面样例是 True(首字母大写),如果你的输出是 true,判分时会扣“格式”分——这是这道题最阴的坑。
3.4 模拟题|Shape + IResizable(15 分)
Shape with a protected name, an abstract method double area(), and a concrete method String describe() returning "Circle with area 78.54" (two decimals). (5)(b) Write interface
IResizable with void resize(double factor); make Circle (radius) and Rectangle (width, height) extend Shape and implement IResizable. (6)(c) Write a
ShapeRegistry that stores shapes of any subtype, scales them all by 1.1, and prints each description. (4)public abstract class Shape {
protected String name;
protected Shape(String name) { this.name = name; }
public abstract double area();
public String describe() {
return String.format("%s with area %.2f", name, area());
}
}
public interface IResizable {
void resize(double factor);
}
public class Circle extends Shape implements IResizable {
private double radius;
public Circle(double radius) { super("Circle"); this.radius = radius; }
@Override public double area() { return Math.PI * radius * radius; }
@Override public void resize(double factor) { radius *= factor; }
}
public class Rectangle extends Shape implements IResizable {
private double width, height;
public Rectangle(double w, double h) { super("Rectangle"); this.width = w; this.height = h; }
@Override public double area() { return width * height; }
@Override public void resize(double factor) { width *= factor; height *= factor; }
}
public class ShapeRegistry {
private final List<Shape> shapes = new ArrayList<>();
public void add(Shape s) { shapes.add(s); }
public void scaleAllAndPrint(double factor) {
for (Shape s : shapes) {
if (s instanceof IResizable r) r.resize(factor); // 只有可缩放的才缩
System.out.println(s.describe());
}
}
}
注意 instanceof IResizable 这一手:ShapeRegistry 里放的是 Shape,不是所有 Shape 都可缩放——这正是接口存在的意义。Java 16+ 支持模式匹配写法,写成传统 if (s instanceof IResizable) { ((IResizable) s).resize(factor); } 也完全给分。
3.5 常见错误
| 错误 | 后果 |
|---|---|
| 子类构造器第一行没有 super(...) | 编译失败(无默认构造器) |
| 父类字段改成 public 以便子类访问 | 封装被破坏,可能扣“设计”分 |
| 接口方法漏实现 | 编译失败 |
| toString 少个逗号/空格 | 输出不一致,扣格式分 |
用 == 比较字符串 | 逻辑错 |
PersonelRecords 用 List<Employee> 存 | 无法放 Student,类型错误 |
4 · Section D:Spring / JPA / Thymeleaf / REST
真题 20 分(6/2/2/5/5)|样卷 20 分(Q4 4/2/3/3/3 + Q5 3/2)。预算 45 分钟。
4.1 五问对应五层
| 小问 | 要写什么 | 关键注解 |
|---|---|---|
| (a) entity | 实体类 + 关系映射 | @Entity @Id @GeneratedValue @ManyToOne @OneToMany @ManyToMany @JoinTable |
| (b) repository | 接口 + 派生查询或 @Query | extends JpaRepository<T, Long> |
| (c) controller | @GetMapping + Model + 返回视图名 | @Controller @GetMapping |
| (d) Thymeleaf | 循环 + 取值 + 计算展示 | th:each th:text th:href #lists.size() |
| (e) REST | @RestController + @PathVariable + 状态码 | @RestController @PostMapping ResponseEntity |
4.2 例题|真题 Q4:WeLoveBooks(20 分)
Attributes: Author — id, name, year of birth, nationality; Book — id, title, published year, ISBN; Review — id, the respective book, an integer rating from 1 to 5, and review content. An author can write many books, and a book can be written by many authors. A review is for one book, and a book can have many reviews.
a) Design and write the Java code for the entity models — Author, Book and Review. You may add attributes/methods where necessary; you do not need getters and setters. (6)
b) Write the JPA Repository code with the necessary JPA Queries (if any) to support the use case “Display Authors and their Books”. (2)
c) Write the Controller code for that use case. (2)
d) Write the HTML with Thymeleaf for the screen: all authors with name, nationality and year of birth; for each book its title, published year, ISBN, number of reviews and average rating; and for each author the average rating computed as the average of the author’s book averages. (5)
e) A REST API is developed for “Create Book Reviews” at
<base_url>/api/review/{bookId}. The client sends the book id, the rating and the review content. If the book id does not exist, the API must return HttpStatus.UNPROCESSABLE_ENTITY (422). Design and write all necessary Controller and JPA Repository code. (5)实体:Author(id、姓名、出生年、国籍);Book(id、书名、出版年、ISBN);Review(id、所属书、1–5 评分、内容)。作者与书多对多,书与评论一对多。
(a) 写三个实体类(6 分);(b) 写 Repository(2 分);(c) 写 Controller(2 分);(d) 写 Thymeleaf 页面,含每本书的评论数与平均分、每位作者的平均分(作者各书均分的平均)(5 分);(e) 写 REST 接口 `POST /api/review/{bookId}`,书不存在时返回 422(5 分)。
- 关系判断:Author ↔ Book 是 @ManyToMany(必须有一端 mappedBy + 中间表);Book → Review 是 @OneToMany(mappedBy="book"),Review 侧 @ManyToOne。
- 平均分两种算法要在实体里写成方法(getAverageRating()),Thymeleaf 里只负责显示——把计算写进模板(如
${#lists.size(...)}之类硬算)既难读又容易错。 - 作者平均分 = 各书平均分的平均,不是所有评论的平均。差异只在“某本书评论多”时显现,恰恰是命题人的考点。
- REST 用 ResponseEntity 才能返回 422;不要用异常页代替。
@Entity
public class Author {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int yearOfBirth;
private String nationality;
@ManyToMany(mappedBy = "authors") // 由 Book 维护中间表
private List<Book> books = new ArrayList<>();
public double getAverageRating() { // 各书均分的平均
if (books.isEmpty()) return 0.0;
double sum = 0;
for (Book b : books) sum += b.getAverageRating();
return sum / books.size();
}
// getters / setters 略(题目不要求写)
}
@Entity
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private int publishedYear;
private String isbn;
@ManyToMany
@JoinTable(name = "book_author",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name = "author_id"))
private List<Author> authors = new ArrayList<>();
@OneToMany(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Review> reviews = new ArrayList<>();
public double getAverageRating() {
if (reviews.isEmpty()) return 0.0;
int sum = 0;
for (Review r : reviews) sum += r.getRating();
return (double) sum / reviews.size();
}
public int getReviewCount() { return reviews.size(); }
}
@Entity
public class Review {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "book_id", nullable = false)
private Book book;
private int rating; // 1..5
@Column(length = 1000)
private String content;
}
答案 (b):Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
// 一次抓取作者 + 书 + 评论,避免 N+1 与 LazyInitializationException
@Query("select distinct a from Author a " +
"left join fetch a.books b " +
"left join fetch b.reviews")
List<Author> findAllWithBooksAndReviews();
}
public interface ReviewRepository extends JpaRepository<Review, Long> { }
答案 (c):Controller
@Controller
public class AuthorController {
private final AuthorRepository authorRepository;
public AuthorController(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
@GetMapping("/authors")
public String list(Model model) {
model.addAttribute("authors", authorRepository.findAllWithBooksAndReviews());
return "authors"; // → templates/authors.html
}
}
答案 (d):Thymeleaf
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>Authors</title></head>
<body>
<h1>Authors and Books</h1>
<ul>
<li th:each="a : ${authors}">
<b th:text="${a.name}">Name</b>
(<span th:text="${a.nationality}">Nationality</span> author,
born <span th:text="${a.yearOfBirth}">1900</span>)
<ul>
<li th:each="b : ${a.books}">
Book Title: <b th:text="${b.title}">Title</b><br>
Published Year: <span th:text="${b.publishedYear}">1949</span><br>
ISBN: <span th:text="${b.isbn}">978</span><br>
Number of Reviews: <span th:text="${b.reviewCount}">0</span><br>
Average Rating: <span th:text="${#numbers.formatDecimal(b.averageRating,1,1)}">0.0</span>
</li>
</ul>
Author's Average Rating:
<b th:text="${#numbers.formatDecimal(a.averageRating,1,1)}">0.0</b>
</li>
</ul>
</body>
</html>
reviewCount 与 averageRating 都是实体里的 getter,模板里不写运算逻辑——这是这道题最省时也最不容易错的写法。
答案 (e):REST
@RestController
@RequestMapping("/api/review")
public class ReviewRestController {
private final BookRepository bookRepository;
private final ReviewRepository reviewRepository;
public ReviewRestController(BookRepository b, ReviewRepository r) {
this.bookRepository = b; this.reviewRepository = r;
}
@PostMapping("/{bookId}")
public ResponseEntity<Void> create(@PathVariable Long bookId,
@RequestBody ReviewRequest body) {
Optional<Book> book = bookRepository.findById(bookId);
if (book.isEmpty()) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build(); // 422
}
Review review = new Review();
review.setBook(book.get());
review.setRating(body.rating());
review.setContent(body.content());
reviewRepository.save(review);
return ResponseEntity.status(HttpStatus.CREATED).build(); // 201
}
public record ReviewRequest(int rating, String content) { }
}
@ManyToMany 只能有一端写 @JoinTable,另一端必须 mappedBy;② 422 用 ResponseEntity.status(...),返回类型写 ResponseEntity<Void>;③ Thymeleaf 里 ${b.reviewCount} 会调用 getReviewCount(),命名别写成 getReviewNumber()。4.3 例题|样卷 Q4/Q5:Minishop(20 分)
Member{Long memberId; String name; ArrayList<Address> address;} · Order{Long orderId; Double amount; Member member; Address address; Date orderdate; Long orderNum;} · OrderDetail{Long orderDetailId; Product product; Integer quantity; Double amount;} · Delivery{Long deliveryId; Long orderDetailId; Address deliveryAddress; Date deliveryDate; Double amount;}Q4: (a) Map Member, Address and Order with JPA so that persistence enables a flexible design. (4) (b) Write JPA Queries to find the top ten members who spent more than $100,000 worth of orders so far. (2) (c) Complete
OrderService.findAll(OrderSearch orderSearch) where OrderSearch has memberName, orderStatus, member, orderDate. (3) (d) Complete the controller method that reads all existing items and sends them to the items listing page. (3) (e) Complete the essential HTML tags for the item form. (3)Q5: (a) Is validation necessary for the item form? Which Spring components are most suitable? Justify. (3) (b) Code a test case verifying the creation of a new Member entity using the Spring testing framework. (2)
Q4:(a) 用 JPA 映射 Member、Address、Order(4 分);(b) 查消费超过 10 万的 前 10 名会员(2 分);(c) 补全按条件查询(3 分);(d) 补全列表页 Controller(3 分);(e) 补全新增表单的 HTML(3 分)。
Q5:(a) 表单校验是否必要?哪些 Spring 组件最合适?说明理由(3 分);(b) 写一个验证 Member 创建的测试(2 分)。
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
private String name;
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Address> address = new ArrayList<>();
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Address {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long addressId;
private String line1, city, postalCode;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
}
@Entity
@Table(name = "orders") // ORDER 是 SQL 保留字
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long orderId;
private Double amount;
@Temporal(TemporalType.DATE) private Date orderdate;
private Long orderNum;
@ManyToOne @JoinColumn(name = "member_id") private Member member;
@ManyToOne @JoinColumn(name = "address_id") private Address address;
}
答案 (Q4b):Top 10 高消费会员
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("select o.member from Order o " +
"group by o.member " +
"having sum(o.amount) > 100000 " +
"order by sum(o.amount) desc")
List<Member> findTopSpenders(Pageable pageable);
// 调用:findTopSpenders(PageRequest.of(0, 10))
}
答案 (Q4c):动态条件查询
@Override
public List<Order> findAll(OrderSearch s) {
Specification<Order> spec = (root, query, cb) -> {
List<Predicate> ps = new ArrayList<>();
if (s.getOrderStatus() != null)
ps.add(cb.equal(root.get("status"), s.getOrderStatus()));
if (s.getOrderDate() != null)
ps.add(cb.greaterThanOrEqualTo(root.get("orderdate"), s.getOrderDate()));
if (s.getMemberName() != null && !s.getMemberName().isBlank())
ps.add(cb.like(cb.lower(root.get("member").get("name")),
"%" + s.getMemberName().toLowerCase() + "%"));
return cb.and(ps.toArray(new Predicate[0]));
};
return orderRepository.findAll(spec);
}
// 也可以直接用派生查询:
// List<Order> findByMemberNameContainingIgnoreCase(String name);
// List<Order> findByStatus(OrderStatus status);
答案 (Q4d):列表 Controller
@GetMapping("/items")
public String list(Model model) {
model.addAttribute("items", itemRepository.findAll());
return "items"; // templates/items.html(对应 Figure 4 Items Listing)
}
答案 (Q4e):表单 HTML
<form th:action="@{/items}" method="post" th:object="${item}">
<label>Name</label>
<input type="text" th:field="*{name}" required>
<label>Price</label>
<input type="number" step="0.01" th:field="*{price}" required>
<label>Category</label>
<select th:field="*{category}">
<option th:each="c : ${categories}"
th:value="${c.id}" th:text="${c.name}"></option>
</select>
<button type="submit">Add Item</button>
</form>
答案 (Q5a):校验分层
Validation is necessary: price and quantity must be positive and the name is required, otherwise invalid data reaches the database. The most suitable components are Bean Validation annotations on the entity or a form-backing DTO (@NotBlank, @Positive, @Size), enforced in the controller with @Valid plus BindingResult so the form is re-rendered with error messages; business-level rules (e.g. “item must belong to an existing category”) belong to the service layer. Client-side required/min attributes are only a convenience and cannot replace server-side validation.
答案 (Q5b):单元测试
@DataJpaTest
class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Test
void createsMember() {
Member m = new Member();
m.setName("Alice");
Member saved = memberRepository.save(m);
assertNotNull(saved.getMemberId());
assertEquals("Alice", memberRepository.findById(saved.getMemberId()).orElseThrow().getName());
}
}
4.4 模拟题|三个高频变体
PUT /api/books/{id} must return 404 when the id does not exist and 200 with the updated book when it does. Write the method. (4)2) In a Thymeleaf page, show for each book its rating distribution: number of 5-star, 4-star … reviews. Write the entity helper and the template snippet. (4)
3) Write a
@WebMvcTest that verifies GET /books/999 returns 404. (3)PUT /api/books/{id}:不存在返回 404,存在返回 200 与更新后的书;2) Thymeleaf 展示每本书的星级分布;3) 用 @WebMvcTest 验证 GET /books/999 返回 404。// 1)
@PutMapping("/{id}")
public ResponseEntity<Book> update(@PathVariable Long id, @RequestBody Book body) {
return bookRepository.findById(id)
.map(existing -> {
existing.setTitle(body.getTitle());
existing.setAuthor(body.getAuthor());
return ResponseEntity.ok(bookRepository.save(existing)); // 200
})
.orElseGet(() -> ResponseEntity.notFound().build()); // 404
}
// 2) 实体里加一个方法
public Map<Integer, Long> getRatingDistribution() {
return reviews.stream().collect(
Collectors.groupingBy(Review::getRating, Collectors.counting()));
}
// 模板
<ul>
<li th:each="e : ${book.ratingDistribution}">
<span th:text="${e.key}">5</span>-star:
<span th:text="${e.value}">2</span>
</li>
</ul>
// 3)
@WebMvcTest(BookController.class)
class BookControllerTest {
@Autowired MockMvc mvc;
@MockBean BookService bookService;
@Test
void missingBookReturns404() throws Exception {
when(bookService.findById(999L)).thenReturn(null);
mvc.perform(get("/books/999"))
.andExpect(status().isNotFound());
}
}
4.5 常见错误
| 错误 | 后果 |
|---|---|
| @ManyToMany 两端都写 @JoinTable | 映射冲突,启动报错 |
| 忘记 mappedBy,生成两张中间表 | 数据重复 |
实体名用 Order 却没有 @Table(name="orders") | ORDER 是保留字,建表失败 |
| Thymeleaf 里调用不存在的 getter | 500 错误 |
REST 用 @Controller 而不是 @RestController | 返回视图名而不是 JSON |
| 422 写成 400 或 409 | 题目明确要求 422,直接扣分 |
测试里用 @SpringBootTest 起全容器 | 慢,且题目要的是数据层切片 @DataJpaTest |
5 · 附录:速查表
5.1 Spring / JPA 注解速查
| 注解 | 放哪 | 作用 |
|---|---|---|
@Entity | 类 | 声明为 JPA 实体,表名默认类名 |
@Table(name="orders") | 类 | 改表名(避免 SQL 保留字) |
@Id / @GeneratedValue(IDENTITY) | 字段 | 主键 / 自增 |
@Column(length=1000, nullable=false) | 字段 | 列定义 |
@ManyToOne + @JoinColumn | 字段 | 多对一,拥有外键 |
@OneToMany(mappedBy="...") | 字段 | 一对多,通常是被维护端 |
@ManyToMany + @JoinTable | 字段 | 多对多(只有一端写 JoinTable) |
@Temporal(TemporalType.DATE) | 字段 | 日期精度(旧 API) |
@Repository / @Service / @Component | 类 | 交给 Spring 管理 |
@Transactional | 类或方法 | 事务边界;查询可加 readOnly=true |
@Controller / @RestController | 类 | 返回视图名 / 返回 JSON |
@GetMapping @PostMapping @PutMapping @DeleteMapping | 方法 | 路由 |
@PathVariable / @RequestParam | 参数 | 取路径变量 / 查询参数 |
@ModelAttribute / @Valid + BindingResult | 参数 | 表单绑定 / 校验 |
@DataJpaTest / @WebMvcTest / @SpringBootTest | 测试类 | 数据层切片 / MVC 切片 / 全容器 |
5.2 Thymeleaf 指令速查
| 指令 | 用途 | 例子 |
|---|---|---|
th:text | 输出文本(转义) | th:text="${b.title}" |
th:utext | 输出 HTML(不转义,慎用) | — |
th:each | 循环 | th:each="b : ${books}" |
th:if / th:unless | 条件 | th:if="${#lists.isEmpty(books)}" |
th:href / th:action | 链接 / 表单动作 | th:href="@{/books/{id}(id=${b.id})}" |
th:object / th:field | 表单绑定 | th:object="${book}"、th:field="*{title}" |
th:errors | 字段错误 | th:errors="*{title}" |
#lists.size(x) | 集合大小 | th:text="${#lists.size(b.reviews)}" |
#numbers.formatDecimal(x,1,1) | 小数格式 | th:text="${#numbers.formatDecimal(b.averageRating,1,1)}" |
#strings.capitalize / #dates.format | 字符串/日期工具 | — |
5.3 JPA 派生查询关键词
| 关键词 | 生成条件 | 例子 |
|---|---|---|
findBy | = | findByTitle(String t) |
Containing | LIKE %…% | findByTitleContaining("Dune") |
IgnoreCase | 忽略大小写 | findByNameIgnoreCase |
OrderBy…Desc | 排序 | findByAuthorOrderByTitleAsc |
LessThan / GreaterThanEqual | 比较 | findByYearLessThan(2000) |
Between | 区间 | findByYearBetween(a, b) |
IsNull / IsNotNull | 空值 | findByGradeIsNull() |
Top10 | 限制条数 | findTop10ByOrderByAmountDesc() |
findBy…Id(属性路径) | 关联对象的主键 | findByAuthorId(Long id) |
5.4 常用 HTTP 状态码
| 码 | 含义 | 本题里的用法 |
|---|---|---|
| 200 | OK | GET/PUT 成功返回数据 |
| 201 | Created | POST 创建评论成功 |
| 302 | Found | 表单提交后 redirect(PRG) |
| 400 | Bad Request | 参数格式错 |
| 404 | Not Found | 书籍 id 不存在(页面) |
| 422 | Unprocessable Entity | 真题要求:bookId 不存在 |
| 500 | Internal Server Error | 未捕获异常 |
5.5 高频失分 Top 8
| # | 失分点 | 补救 |
|---|---|---|
| 1 | OOP 题改了方法签名/构造器顺序 | 先抄 main,逐个对照 new 的参数 |
| 2 | toString 格式差一个逗号或大小写 | 把期望输出抄在草稿上逐字比 |
| 3 | SQL 聚合条件写进 WHERE | 一律 HAVING |
| 4 | “all projects” 用 SUM 而不是 MIN | all = MIN,any = MAX |
| 5 | @ManyToMany 两端都建中间表 | 一端 mappedBy |
| 6 | Thymeleaf 里塞计算逻辑 | 计算放实体 getter |
| 7 | REST 返回 404 而不是 422 | 题目写什么就返回什么 |
| 8 | Java 方法漏边界(空/长度 1) | 每个方法先写两行守卫 |
5.6 开卷携带清单
- 本手册打印件(约 20 页 A4 双面)。
- 两页速查:注解表 + Thymeleaf 指令表(§5.1、§5.2 单独打)。
- 一页 SQL 模板(JOIN / HAVING / 自连接 / 视图 / 存储过程)。
- 铅笔与草稿纸:Java 题先在草稿上跑样例。
6 · 一页纸速测(15 题)
| # | 问题 | 答案 |
|---|---|---|
| 1 | BPE 的四个方法分别做什么? | 抽相邻对 → 统计唯一对与次数 → 找最高频 → 替换 |
| 2 | countNumber 的返回值是什么? | 唯一对的个数(同时要填 uniquePairs 与 counts) |
| 3 | 两个时间段重叠的判据? | max(start) < min(end),即 cEntry < tExit && cExit > tEntry |
| 4 | 找孤儿记录用什么 JOIN? | LEFT JOIN + WHERE 右表键 IS NULL(或 NOT EXISTS) |
| 5 | “所有项目预算都 > 100 万”怎么表达? | HAVING MIN(budget) > 1000000 |
| 6 | 自连接数“同时修两门课”的学生要注意什么? | 两次 JOIN Enrolment,用 StudentId 相等连接,并 COUNT(DISTINCT) |
| 7 | 视图建好后怎么查? | 直接 SELECT … FROM 视图名 WHERE …,不要重复 join |
| 8 | 抽象类实现接口时可以只实现部分方法吗? | 可以,剩下的由子类实现;类必须保持 abstract |
| 9 | 子类构造器第一行必须写什么? | super(...)(父类没有无参构造器时) |
| 10 | @ManyToMany 的中间表写在哪个类? | 拥有方(写 @JoinTable),另一端 mappedBy |
| 11 | Thymeleaf 里怎么显示两位小数? | ${#numbers.formatDecimal(x,1,1)} |
| 12 | 集合大小怎么取? | ${#lists.size(list)} 或实体 getter |
| 13 | bookId 不存在要返回什么状态码? | 422 UNPROCESSABLE_ENTITY |
| 14 | @DataJpaTest 与 @SpringBootTest 的区别? | 前者只起数据层并回滚,后者起全容器 |
| 15 | 表单校验应该放在哪几层? | 实体/DTO 注解 + Controller 的 @Valid;业务规则放 Service |