题目集 5-7 总结性 Blog
前言
题目集 5、6、7 以单部电梯调度程序为主,构建了从基础功能实现到复杂类设计的递进式学习场景。作为 Java 初学者,这三次作业对于我来说也较为困难:题目集 5 因对面向对象设计理解浅薄,陷入单类难以设计的困境;题目集 6 尝试类拆分却因职责划分模糊导致逻辑混乱;题目集 7 引入乘客类后,复杂的请求转换逻辑再次成为拦路虎。尽管三次作业均未完全通关在提交截止时间到来时,看着测试用例一个个亮起的红叉,满心都是遗憾与不甘,但也在不断试错中积累了宝贵的设计经验,深刻体会到面向对象编程的核心魅力与难度。本文将从设计缺陷、采坑经历、改进思路等角度展开,复盘这段充满挑战的学习历程。
一、题目集 5:单类设计的杂乱与运行超时陷阱
1. 初始设计思路与代码结构
面对题目集 5 的单类设计要求,我刚开始认为将所有功能封装在 Elevator 类中即可满足需求。类中包含电梯状态(当前楼层、方向、状态)、请求队列(内部请求 LinkedList<Integer>、外部请求 LinkedList<ExternalRequest>)、调度方法(处理请求、移动、判断方向等)。我的代码结构如下(原则上不能复制源码 但我认为源码比图片更加方便记录与以后的查看):
class Elevator {
private int currentFloor, minFloor, maxFloor;
private Direction direction;
private LinkedList<Integer> internalQueue;
private LinkedList<ExternalRequest> externalQueue;
public void processRequests() {
while (!internalQueue.isEmpty() || !externalQueue.isEmpty()) {
determineDirection();
move();
}
}
}
2. 核心问题剖析:运行超时的根源
(1)较为低效的循环与遍历逻辑
在 move 方法中使用无限循环 while (true),每次移动后通过 checkStop 和 hasNextRequest 遍历队列判断是否停靠或继续。例如:
private boolean hasNextRequest() {
if (direction == Direction.UP) {
for (int floor : internalQueue) if (floor > currentFloor) return true;
for (ExternalRequest req : externalQueue) if (req.floor > currentFloor && req.dir == Direction.UP) return true;
}
return false;
}
当请求量较大时,如输入样例 2 中的重复请求,程序陷入高频次无效遍历,我认为这便是最终导致运行超时的元凶。
也有可能是在对下一楼层查找时出现了问题,以及内部楼层和外部楼层的冲突导致了代码的冲突 或者什么,暂且还没有解决这一问题。
通过 SourceMonitor 对当时代码的分析(如下方图表所示),也能从侧面印证上述问题。从文本信息部分的 “Lines” 可知代码总行数为 276 行,“Statements” 为 80 条,结合 “Percent Branch Statements”(分支语句占比 38.7% )和 “Percent Lines with Comments”(含注释代码行占比 34.1% )等信息,可了解代码整体结构情况。再看图表部分,雷达图能直观展现代码在注释比例、方法数量等多个维度的指标,柱状图则反映出语句数量随代码块深度的分布情况。这些分析数据为我们进一步理解代码的复杂性、查找运行超时根源提供了有力依据。
(2)方向判断的片面性
determineDirection 方法仅比较队首请求,未综合所有请求的方向与楼层关系。例如,当内部队首请求为 3 楼,外部队首请求为 5 楼 UP,当前楼层为 1 楼时,电梯正确向上;但若内部队列后续还有 7 楼请求,外部队列有 6 楼 DOWN 请求,电梯到达 5 楼后可能错误转向,导致同方向请求未完全处理。
3. 试错过程:从盲目编码到初步调试
初期认为问题源于输入处理,反复检查正则表达式解析代码,却忽略了算法效率的核心问题。通过 IDE 调试发现,hasNextRequest 在每次楼层变化时被高频调用,占用 70% 以上运行时间。尝试引入 PriorityQueue 对请求按楼层排序,却因方向判断逻辑复杂未能成功,最终意识到单类设计的天然缺陷 —— 调度逻辑与状态管理混杂,难以进行高效优化。
正是因为这些问题,导致最终未能成功通关题目集 5 的作业,但这次经历也让我意识到算法效率和类设计合理性的重要性,为后续的学习改进指明了方向
二、题目集 6:类设计迭代的探索与职责划分的深渊
1. 类设计目标与初期方案
题目集 6 要求遵循单一职责原则,拆分为电梯类、控制类、请求队列类。初期设计如下:
- 电梯类(
Elevator):管理当前楼层、方向、状态,提供isValidFloor等状态校验方法。 - 控制类(
Controller):负责调度逻辑,包括方向判断、移动控制、停靠处理。 - 请求队列类(
RequestQueue):管理内外请求,支持去重与有效性校验。
2. 类职责划分的错误
(1)控制类与电梯类的职责混淆
初期在 Controller 中直接操作 Elevator 的 currentFloor 属性:
// 错误写法:控制类直接修改电梯状态
elevator.currentFloor = nextFloor;
违背封装原则,电梯状态变更应通过电梯类自身方法完成,如:
// 正确写法:通过电梯类提供的接口修改
elevator.setCurrentFloor(nextFloor);
此错误导致电梯状态校验逻辑(如楼层范围检查)无法统一管理,若后续增加 “电梯不能在移动中反向急停” 等规则,需多处修改控制类代码。
(2)请求队列类的去重逻辑漏洞
在 addInternalRequest 方法中,仅通过 contains 方法去重,但未正确重写 Integer 包装类的比较逻辑(实际应为值比较而非引用比较),导致重复请求如 <3>、<3> 被错误保留。正确做法是使用 equals 方法判断值相等:
if (!internalQueue.contains(targetFloor)) {
internalQueue.add(targetFloor);
}
3. 算法设计的不严谨性:同方向请求处理缺失
在 checkDirection 方法中,初期仅检查队首请求是否同方向,未遍历队列中所有请求:
// 错误逻辑:仅检查第一个请求
if (firstInternalRequest > currentFloor) direction = Direction.UP;
导致电梯在处理完队首请求后,可能忽略队列中后续同方向请求,例如内部队列有 [5,7],电梯到达 5 楼后,未发现 7 楼请求而错误转向。修正后需遍历所有请求,判断是否存在同方向未处理项:
// 正确逻辑:遍历所有内部请求
boolean hasUpRequest = internalQueue.stream().anyMatch(floor -> floor > currentFloor);
三、题目集 7:乘客类引入后的复杂协作与逻辑难以结合
1. 需求变化带来的设计挑战
题目集 7 将外部请求改为 <源楼层,目的楼层>,需引入 Passenger 类存储源与目的楼层,并在处理外部请求时将目的楼层加入内部队列。初期设计的 Passenger 类未正确区分内部请求(无源远请求)与外部请求(有源请求),导致逻辑混乱:
// 错误设计:内部请求与外部请求共用同一类,属性混淆
class Passenger {
int sourceFloor, destinationFloor;
// 内部请求初始化时sourceFloor为0,导致后续判断错误
}
2. 请求转换逻辑的断层
在 Controller 的 stopAtFloor 方法中,处理外部请求时未正确关联源楼层与目的楼层:
// 错误逻辑:直接移除外部请求,未将目的楼层加入内部队列
externalQueue.removeIf(req -> req.sourceFloor == currentFloor);
// 正确逻辑:先提取目的楼层,再加入内部队列
externalQueue.stream()
.filter(req -> req.sourceFloor == currentFloor)
.forEach(req -> internalQueue.add(req.destinationFloor));
此错误导致乘客进入电梯后,目的楼层未被记录,电梯无法继续前往目标楼层,出现 “开门后无后续动作” 的异常现象。
3. 类协作的复杂度提升:依赖关系混乱
乘客类、请求队列类、控制类之间的依赖关系初期设计为双向关联,如 Passenger 类直接引用 RequestQueue 进行入队操作,违背 “高内聚、低耦合” 原则。正确做法是通过控制类统一协调,乘客类仅作为数据载体,不涉及业务逻辑:
// 错误:乘客类依赖请求队列
class Passenger {
public void addToQueue(RequestQueue queue) {
queue.addRequest(this);
}
}
// 正确:控制类处理请求入队
controller.processExternalRequest(passenger);
四、采坑历程:从语法错误到设计思维的问题
1. 输入处理的 “隐性陷阱”
(1)楼层范围校验缺失
题目集 5 初期未校验输入楼层是否在 [minFloor, maxFloor] 范围内,导致用户输入 22 楼(max=20)时,电梯仍尝试前往,触发数组越界异常。修正后在 addRequest 方法中增加校验:
if (floor < minFloor || floor > maxFloor) {
System.out.println("Invalid floor, ignored.");
return;
}
(2)方向字符串解析错误
外部请求方向 UP/DOWN 大小写不敏感处理时,初期使用 Direction.valueOf(parts[1]),未转换为大写,导致输入 Up 时抛出 IllegalArgumentException。修正后统一转换为大写:
Direction requestDirection = Direction.valueOf(parts[1].toUpperCase());
2. 状态管理的处理
电梯状态(State 枚举:MOVING/STOPPED)初期未被正确使用,Controller 在电梯移动时仍允许修改方向,导致 “运行中反向” 的逻辑错误。例如,电梯向上移动时,若下方出现紧急请求,错误代码会立即转向,违背 “优先处理同方向请求” 规则。正确做法是在 State 为 MOVING 时,禁止方向变更,仅允许在 STOPPED 状态重新判断方向。
3. 测试用例驱动开发的重要性
通过输入样例 1 发现,电梯在处理完所有同方向请求后,未正确切换方向处理反方向请求。例如,到达 7 楼(最高请求)后,未检测到 6 楼 DOWN 请求,导致电梯静止。通过逐步调试,发现 checkDirection 方法在方向切换时,未清空已处理请求标记,最终通过增加 “方向切换时重置请求遍历指针” 解决。
五、改进思路:从代码补丁到系统性优化
回顾三次未完成的作业,暴露出诸多问题,针对这些不足,我梳理出以下系统性的优化思路。
1. 类设计的重构策略
(1)严格遵循单一职责原则!!!!!!!!!必须必须 必须
- 电梯类:仅负责状态存储与基本校验(当前楼层、方向、状态、楼层有效性),不涉及任何调度逻辑。
- 控制类:专注调度算法(LOOK 算法实现),通过电梯类接口获取状态,通过请求队列类接口操作请求。
- 请求队列类:封装请求的去重、校验、入队 / 出队操作,提供 “获取同方向请求” 等业务方法。
(2)引入枚举与常量类
定义 Direction、State 枚举明确状态,避免魔法值;创建 Constants 类存储请求格式正则表达式、输出模板等,提升代码可读性:
enum Direction { UP, DOWN, IDLE }
class Constants {
public static final String INTERNAL_REGEX = "<(\\d+)>";
public static final String OUTPUT_OPEN = "Open Door # Floor %d";
}
2. 算法优化的核心方向
(1)请求队列的优先级管理
使用 TreeSet 存储内部请求,按楼层排序;外部请求按 “源楼层 + 方向” 分组,使用 Map<Direction, TreeSet<Integer>> 存储,便于快速获取同方向请求:
// 内部请求按楼层升序排列
TreeSet<Integer> internalRequests = new TreeSet<>();
// 外部请求按方向分组
Map<Direction, TreeSet<Integer>> externalRequests = new EnumMap<>(Direction.class);
(2)LOOK 算法的完整实现
在控制类中,维护当前运行方向的 “最远楼层”:
- 向上运行时,记录最高目标楼层;
- 向下运行时,记录最低目标楼层;
到达最远楼层后,切换方向处理反方向请求,避免无效往返。
3. 代码健壮性的提升路径
(1)异常处理与日志记录
对输入解析、请求入队等关键步骤添加异常捕获,记录错误日志:
try {
// 解析输入楼层
int floor = Integer.parseInt(parts[0]);
} catch (NumberFormatException e) {
System.err.println("Invalid floor format: " + parts[0]);
return;
}
(2)单元测试覆盖关键逻辑
针对 determineDirection、shouldStop 等核心方法编写单元测试,使用 JUnit 验证不同场景下的行为:
@Test
public void testDetermineDirection_UpRequest() {
Elevator elevator = new Elevator(1, 20);
elevator.setCurrentFloor(3);
RequestQueue queue = new RequestQueue();
queue.addInternalRequest(5);
Controller controller = new Controller(elevator, queue);
controller.determineDirection();
assertEquals(Direction.UP, elevator.getDirection());
}
六、总结:在试错中理解面向对象的本质
1. 学习收获:从技术到思维的三重突破
(1)类设计思维
认识到 “类是职责的载体”,而非数据与方法的简单堆砌。题目集 5 的单类臃肿源于职责混杂,题目集 6、7 的类拆分迫使将 “状态管理”“逻辑处理”“数据存储” 分离,体会到单一职责原则如何降低复杂度、提升可维护性。
(2)算法与数据结构的联动意识
理解 “高效的算法需要合适的数据结构支撑”,如使用有序集合存储请求,可将遍历查找的 O(n) 复杂度降为 O(log n),从根本上解决运行超时问题。
(3)Java 语言特性的深度应用
掌握枚举、泛型、集合框架的高级用法,例如通过 EnumMap 优化外部请求的方向分组,利用 Stream API 简化请求过滤与转换逻辑,代码从冗长的循环嵌套进化为更简洁的函数式表达。
2. 未来之路
(1)设计模式的引入
- 策略模式:将调度算法(LOOK、SCAN)封装为策略接口,允许运行时切换,提升扩展性。
- 观察者模式:当电梯状态变化(如到达楼层、处理请求)时,通知日志模块、监控模块,实现模块解耦。
(2)复杂场景的支持
- 多人请求并发处理:当前假设请求串行处理,未来可引入多线程模拟并发请求,使用
BlockingQueue实现线程安全的请求队列。 - 负载均衡优化:若扩展为多部电梯,需实现请求分配算法,如根据电梯当前负载、运行方向动态分配请求。
结语
题目集 5-7 的电梯调度之旅,是从 “面向过程思维” 向 “面向对象思维” 蜕变的痛苦却充实的过程。尽管三次作业均未完美收官,但每一次代码调试、每一次类图重画、每一次逻辑重构,都在加深对 “封装、继承、多态” 的理解。面向对象设计如同雕琢玉器,需要不断打磨类的职责边界,优化协作逻辑,而这正是软件开发的核心魅力所在。未来将带着这些宝贵的试错经验,在更复杂的系统设计中实践所学,让每一行代码都成为理解编程本质的基石。
浙公网安备 33010602011771号