ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java实现电梯调度算法:状态机与LOOK策略详解

Java实现电梯调度算法:状态机与LOOK策略详解 电梯调度算法几乎是每一个学编程的人都绕不过去的经典项目。很多人在学校做课程设计、找实习写项目、或者自学练手时都会选择“模拟电梯”这个题目。它看起来不复杂无非就是电梯上下跑、有人按楼层、开关门而已。但真正动手写的时候很多人会发现一个共同的问题——电梯的运行逻辑远比你想象的难写。如果你现在正在被电梯调度问题困扰或者刚拿到一个“模拟电梯”的课程设计题目毫无头绪这篇文章就是为你准备的。我会从设计角度出发把电梯模拟系统的核心拆解成状态机设计、调度算法、数据结构选型、多线程处理这几个部分并给出完整的 Java 代码示例。这不是一篇只堆概念的文章而是能让你照着写出一个可运行、可扩展、调度逻辑清晰的电梯模拟系统的完整教程。1. 这篇文章真正要解决的问题先说我见过的现状。很多初学者第一次实现电梯系统时最容易陷入以下几种情况第一种把电梯“写死了”。比如用一堆 if-else 判断电梯当前在哪层、目标在哪层结果一旦请求变多逻辑就开始混乱甚至出现电梯永远在某一层来回跑、不响应其他楼层请求的问题。第二种选择了一个错误的调度算法。很多人上来就用先来先服务FCFS虽然逻辑简单但是效率很低——一个去 10 层的请求和一个去 2 层的请求如果按顺序执行电梯可能先从 1 楼跑到 10 楼再回到 2 楼来回折腾乘客体验极差。第三种完全忽略“方向”这一核心概念。电梯调度和普通队列调度本质上不一样因为电梯是“带方向”的运输工具。如果不考虑方向把请求简单地塞进队列就会出现电梯明明在上行途中却响应了一个下行请求导致乘客方向冲突。这篇文章要解决的问题就是带你把电梯模拟从“能跑”提升到“合理地跑”。具体来说读完你会掌握如何用状态机精确描述电梯的运行过程电梯调度时为什么必须区分“上行请求”和“下行请求”LOOK 算法为什么比 FCFS 和 SCAN 更适合实际场景如何用 Java 面向对象设计一个可以扩展、测试的电梯系统多线程模拟时常见的坑和排查思路。如果你正在做的项目还处在“电梯乱跑”阶段这篇文章应该能帮你找到症结。2. 电梯系统的核心概念与调度算法在写代码之前必须先清晰定义电梯系统有哪些核心概念。很多代码写不下去不是因为不懂语法而是因为概念没有在脑子里形成一个完整的模型。2.1 电梯状态机电梯的现实行为可以抽象成一组有限状态这就是状态机思想。状态机听起来很高端其实本质就是一个对象在不同时刻只能处于几种有限状态中的一种并且只有在特定条件下才能从一种状态切换到另一种状态。一个最简单的电梯状态机包含以下几个状态状态含义触发条件IDLE静止待机无任何请求电梯停在某一层MOVING_UP上行中存在高于当前层的目标MOVING_DOWN下行中存在低于当前层的目标DOOR_OPEN开门状态电梯到达目标楼层或响应顺路请求DOOR_CLOSED关门状态乘客进出完毕准备继续运行状态机的好处是它强迫你思考“电梯在当前状态下遇到某个事件应该做什么”。如果你在写代码时发现某个方法里出现过长的 if-else多半是状态没有理清。2.2 内招请求与外呼请求电梯系统的请求分为两类内招请求Car Call乘客在电梯内按下目标楼层按钮。这个请求绑定的是目标楼层乘客已经确定了方向。外呼请求Hall Call乘客在楼道里按下上/下按钮。这个请求只包含“楼层”和“方向”并没有指定具体去哪一层。这个区分非常重要。外呼请求本质上只是一个“想坐电梯”的信号只有等到乘客进入电梯后按下楼层按钮系统才知道最终目的地。所以调度算法处理外呼请求时必须记录方向否则电梯不知道应该以什么方式去响应。2.3 常见的电梯调度算法不同调度算法的区别本质上就是“如何从一堆请求中决定电梯下一步去哪”的策略差异。2.3.1 先来先服务FCFS把所有请求按照到达时间排序电梯按顺序一个接一个响应。优点简单、公平。 缺点效率极低。电梯可能为了响应一个远距离请求而放弃当前方向的顺路请求导致频繁折返。判断适合教学入门不太适合模拟真实电梯。2.3.2 SCAN电梯算法电梯沿一个方向运行直到该方向没有请求再调转方向。类似电梯的实际运行逻辑。优点避免了频繁折返在楼层密集场景下效率不错。 缺点如果一个方向始终有请求另一个方向最远端的请求可能长时间得不到响应出现“饥饿”问题。2.3.3 LOOK智能电梯算法LOOK 是对 SCAN 的优化。电梯不需要一直运行到最顶层/最底层再折返而是运行到当前方向的最远请求楼层后如果没有新请求就立刻折返。优点兼顾效率与实现复杂度代码清晰是绝大多数模拟项目和真实电梯控制系统的默认选择。 缺点仍然存在一定程度的“饥饿”可能性但在模拟场景下足够优秀。判断从实现和效果之间平衡来看LOOK 是首选。这也是下面代码中采用的核心算法。2.4 核心难点方向感知我见过最多的错误是只用了一个“目标楼层列表”然后在每一层检查“当前层在不在列表里”。这样写看起来简单但无法解决一个核心问题电梯上行途中楼层里有人按下行按钮这个请求要不要响应如果只用目标列表电梯到 5 层时发现 3 层有个下行请求可能会直接调头去 3 层结果 7 层的乘客被晾在一边。这就是为什么调度器必须维护“上行请求集合”和“下行请求集合”两个数据结构并且只在当前运行方向上查找请求。3. 系统设计与模块拆分在写代码前先来确定整体架构。电梯模拟系统可以拆成以下几个核心模块3.1 核心类设计类名职责关键属性/方法Elevator电梯实体当前楼层、当前方向、状态、内招请求集合Request乘客请求类型外呼/内招、楼层、方向Scheduler调度器接收请求、计算下一目标楼层Building建筑/环境楼层总数、电梯引用SimulationMain启动类初始化系统、模拟乘客请求、驱动电梯运行这种设计的好处是职责分明Elevator 负责“执行”Scheduler 负责“决策”Building 负责“环境”。后续如果要把控制台模拟改成 GUI 或 Web 服务只需要替换展示层核心逻辑无需大改。3.2 流程时序整个模拟运行的核心流程是初始化建筑和电梯模拟乘客产生请求外呼或内招调度器接收请求更新电梯的待办集合电梯根据当前状态和方向按照调度算法计算下一步行动电梯执行行动上行/下行/开门/关门每次行动后检查是否到达目标层、是否应该开门、是否应该改变方向重复步骤 4 到 6直到所有请求处理完毕。这个流程看起来简单但每一步都需要非常严谨的状态判断尤其在“到达目标层”和“开门”这两个动作上很容易出现逻辑漏洞。4. 环境准备与项目骨架4.1 运行环境本文示例代码使用 Java 语言编写。如果你的机器上还没有 Java 环境先安装 JDK。版本请以实际项目为准本文示例基于 Java 8 的语法通用性较强在更高版本 JDK 上也可以正常运行。以下列出建议的开发环境项目建议操作系统Windows / macOS / Linux 均可JDKJDK 8 及以上构建工具Maven 或 Gradle简单示例可不用IDEIntelliJ IDEA / Eclipse / VS Code本文提供的示例不依赖第三方库直接使用 JDK 自带工具即可编译运行可以避免构建工具带来的额外干扰。4.2 项目结构示例项目采用标准的 Maven 目录结构如果你不使用 Maven也可以直接按这个目录建普通 Java 包。src/main/java/com/elevator/ ├── model/ │ ├── Direction.java │ ├── ElevatorState.java │ └── Request.java ├── core/ │ ├── Elevator.java │ ├── Scheduler.java │ └── Building.java └── app/ └── SimulationMain.java下面逐个实现这些文件。5. 完整示例代码实现5.1 定义基础枚举方向与状态首先是方向枚举。方向是电梯调度的核心必须单独定义避免在代码中使用魔法数字。// 文件路径src/main/java/com/elevator/model/Direction.java package com.elevator.model; public enum Direction { UP, DOWN, IDLE }然后是电梯状态枚举。这里把“开门”和“关门”作为独立状态处理而不是简单地用“静止”来表示更符合真实电梯模型也方便后续扩展开门超时、超重报警等功能。// 文件路径src/main/java/com/elevator/model/ElevatorState.java package com.elevator.model; public enum ElevatorState { IDLE, MOVING, DOOR_OPEN, DOOR_CLOSED }说明这里把 MOVING_UP 和 MOVING_DOWN 合并成了 MOVING因为具体方向由单独的 Direction 枚举管理不需要在状态里重复表达避免状态爆炸。5.2 定义请求对象Request 类表示一个乘客请求。它需要区分是“外呼请求”还是“内招请求”并且携带楼层和方向信息。// 文件路径src/main/java/com/elevator/model/Request.java package com.elevator.model; public class Request { public enum Type { HALL, // 外呼请求乘客在楼层呼梯 CAR // 内招请求乘客在电梯内按目标楼层 } private final Type type; private final int floor; private final Direction direction; public Request(Type type, int floor, Direction direction) { this.type type; this.floor floor; this.direction direction; } public Type getType() { return type; } public int getFloor() { return floor; } public Direction getDirection() { return direction; } Override public String toString() { return Request{ type type , floor floor , direction direction }; } }关于外呼请求的方向定义需要注意如果乘客在 5 楼想上楼那么 Request 的 direction 是 UP这个方向也是电梯未来应该保持的方向。如果乘客在 5 楼想下楼direction 是 DOWN。5.3 实现电梯实体类Elevator 类是系统的执行者。它的职责是维护当前楼层、当前方向、当前状态维护内招请求集合乘客在电梯内按下的目标楼层提供移动、开门、关门等基础动作在执行动作前后打印日志方便观察模拟过程。这里有一个非常关键的设计决策电梯类只负责执行动作不负责决策去哪一层。决策权交给 Scheduler。这样设计可以避免电梯类过度膨胀也使得调度算法可以独立演进。// 文件路径src/main/java/com/elevator/core/Elevator.java package com.elevator.core; import com.elevator.model.Direction; import com.elevator.model.ElevatorState; import java.util.Set; import java.util.TreeSet; public class Elevator { private int currentFloor; private Direction direction; private ElevatorState state; private final int minFloor; private final int maxFloor; private final SetInteger carCallFloors; public Elevator(int startFloor, int minFloor, int maxFloor) { this.currentFloor startFloor; this.minFloor minFloor; this.maxFloor maxFloor; this.direction Direction.IDLE; this.state ElevatorState.IDLE; this.carCallFloors new TreeSet(); } public int getCurrentFloor() { return currentFloor; } public Direction getDirection() { return direction; } public ElevatorState getState() { return state; } public SetInteger getCarCallFloors() { return carCallFloors; } public void setDirection(Direction direction) { this.direction direction; } public void setState(ElevatorState state) { this.state state; } /** * 电梯向上移动一层 */ public void moveUp() { if (currentFloor maxFloor) { throw new IllegalStateException(电梯已经在最高层无法继续向上); } currentFloor; this.direction Direction.UP; this.state ElevatorState.MOVING; System.out.println(电梯向上运行 currentFloor 层); } /** * 电梯向下移动一层 */ public void moveDown() { if (currentFloor minFloor) { throw new IllegalStateException(电梯已经在最低层无法继续向下); } currentFloor--; this.direction Direction.DOWN; this.state ElevatorState.MOVING; System.out.println(电梯向下运行 currentFloor 层); } /** * 开门 */ public void openDoor() { this.state ElevatorState.DOOR_OPEN; System.out.println(电梯到达 currentFloor 层开门); } /** * 关门 */ public void closeDoor() { this.state ElevatorState.DOOR_CLOSED; System.out.println(电梯在 currentFloor 层关门); } /** * 添加一个内招请求 */ public void addCarCall(int floor) { if (floor minFloor || floor maxFloor) { throw new IllegalArgumentException(非法楼层号: floor); } carCallFloors.add(floor); } /** * 移除一个已经响应完成的内招请求 */ public void removeCarCall(int floor) { carCallFloors.remove(floor); } /** * 当前是否有内招请求 */ public boolean hasCarCall() { return !carCallFloors.isEmpty(); } }这里使用 TreeSet 存储内招请求楼层因为它天然有序方便调度器获取当前方向上的最远或最近目标。注意 setDirection 和 setState 是给调度器使用的电梯类本身不主动决策方向避免双向耦合。5.4 实现调度器调度器是整个系统的“大脑”。本文采用 LOOK 算法的变体核心逻辑如下如果电梯处于 IDLE 状态并且没有任何请求则保持静止。如果电梯当前方向为 UP则优先处理所有高于当前楼层的内招请求和外呼上行请求如果这些请求都处理完了再考虑转向处理下行请求。如果电梯当前方向为 DOWN则优先处理所有低于当前楼层的内招请求和外呼下行请求。当电梯在当前方向上没有任何待响应的请求时改变方向。为了让电梯在到达某层时判断“是否应该开门”我们需要一个辅助方法判断当前楼层是否存在于任何一个待响应的请求集合中。这里把外呼请求按方向拆分成两个集合即上行请求集合和下行请求集合。// 文件路径src/main/java/com/elevator/core/Scheduler.java package com.elevator.core; import com.elevator.model.Direction; import com.elevator.model.ElevatorState; import com.elevator.model.Request; import java.util.Set; import java.util.TreeSet; public class Scheduler { private final Elevator elevator; private final SetInteger upHallRequests; private final SetInteger downHallRequests; public Scheduler(Elevator elevator) { this.elevator elevator; this.upHallRequests new TreeSet(); this.downHallRequests new TreeSet(); } /** * 接收新请求 */ public void addRequest(Request request) { if (request.getType() Request.Type.HALL) { if (request.getDirection() Direction.UP) { upHallRequests.add(request.getFloor()); System.out.println(收到外呼请求 request.getFloor() 层方向向上); } else if (request.getDirection() Direction.DOWN) { downHallRequests.add(request.getFloor()); System.out.println(收到外呼请求 request.getFloor() 层方向向下); } } else { elevator.addCarCall(request.getFloor()); System.out.println(收到内招请求去往 request.getFloor() 层); } } /** * 调度电梯一步 * 返回值表示是否还有请求需要处理 */ public boolean step() { if (elevator.getState() ElevatorState.DOOR_OPEN) { // 处理开门状态关门清理已完成的请求 elevator.closeDoor(); elevator.setState(ElevatorState.IDLE); return hasRemainingRequests(); } if (noRemainingRequests()) { elevator.setDirection(Direction.IDLE); elevator.setState(ElevatorState.IDLE); System.out.println(电梯当前无请求停在第 elevator.getCurrentFloor() 层); return false; } // 首次启动时如果电梯是 IDLE 状态需要决定初始方向 if (elevator.getDirection() Direction.IDLE) { decideInitialDirection(); } // 判断当前楼层是否应该开门 if (shouldStopAtCurrentFloor()) { elevator.openDoor(); clearRequestsAtCurrentFloor(); return true; } // 根据方向移动 if (elevator.getDirection() Direction.UP) { elevator.moveUp(); } else if (elevator.getDirection() Direction.DOWN) { elevator.moveDown(); } // 每次移动后检查是否已经处理完当前方向的所有请求若是则换向 if (!hasRequestsInCurrentDirection()) { reverseDirection(); } return true; } /** * 判断当前楼层是否应该开门 */ private boolean shouldStopAtCurrentFloor() { int floor elevator.getCurrentFloor(); if (elevator.getCarCallFloors().contains(floor)) { return true; } if (elevator.getDirection() Direction.UP upHallRequests.contains(floor)) { return true; } if (elevator.getDirection() Direction.DOWN downHallRequests.contains(floor)) { return true; } return false; } /** * 清理当前楼层已经响应的请求 */ private void clearRequestsAtCurrentFloor() { int floor elevator.getCurrentFloor(); elevator.removeCarCall(floor); upHallRequests.remove(floor); downHallRequests.remove(floor); } /** * 当前方向上是否还有请求 */ private boolean hasRequestsInCurrentDirection() { int cur elevator.getCurrentFloor(); Direction dir elevator.getDirection(); if (dir Direction.UP) { for (Integer f : elevator.getCarCallFloors()) { if (f cur) { return true; } } for (Integer f : upHallRequests) { if (f cur) { return true; } } } else if (dir Direction.DOWN) { for (Integer f : elevator.getCarCallFloors()) { if (f cur) { return true; } } for (Integer f : downHallRequests) { if (f cur) { return true; } } } return false; } /** * 判断是否完全没有任何请求 */ private boolean noRemainingRequests() { return elevator.getCarCallFloors().isEmpty() upHallRequests.isEmpty() downHallRequests.isEmpty(); } private boolean hasRemainingRequests() { return !noRemainingRequests(); } /** * 初始方向决策优先选择离当前楼层最近的请求方向 */ private void decideInitialDirection() { int cur elevator.getCurrentFloor(); int nearestUp Integer.MAX_VALUE; int nearestDown Integer.MIN_VALUE; for (Integer f : elevator.getCarCallFloors()) { if (f cur f nearestUp) { nearestUp f; } if (f cur f nearestDown) { nearestDown f; } } for (Integer f : upHallRequests) { if (f cur f nearestUp) { nearestUp f; } if (f cur f nearestDown) { nearestDown f; } } for (Integer f : downHallRequests) { if (f cur f nearestUp) { nearestUp f; } if (f cur f nearestDown) { nearestDown f; } } if (nearestUp ! Integer.MAX_VALUE nearestDown ! Integer.MIN_VALUE) { int upDist nearestUp - cur; int downDist cur - nearestDown; elevator.setDirection(upDist downDist ? Direction.UP : Direction.DOWN); } else if (nearestUp ! Integer.MAX_VALUE) { elevator.setDirection(Direction.UP); } else if (nearestDown ! Integer.MIN_VALUE) { elevator.setDirection(Direction.DOWN); } else { elevator.setDirection(Direction.IDLE); } } /** * 反向当当前方向没有请求时切换方向 */ private void reverseDirection() { if (elevator.getDirection() Direction.UP) { elevator.setDirection(Direction.DOWN); System.out.println(当前方向请求已处理完毕电梯换向为下行); } else if (elevator.getDirection() Direction.DOWN) { elevator.setDirection(Direction.UP); System.out.println(当前方向请求已处理完毕电梯换向为上行); } } }这个调度器的核心设计值得仔细理解一下。每次 step() 只让电梯执行一个动作要么移动一层要么停靠开门。这种“一次一步”的设计对调试非常友好你可以一行一行地观察电梯状态变化而不需要一次性推进整个流程。关于 reverseDirection 的触发时机是在电梯移动之后。如果移动后当前方向没有剩余请求说明这一方向已经执行完毕需要换向。这个判断必须放在移动之后因为电梯可能刚刚移动到最后一个请求楼层需要再判断一次。关于 decideInitialDirection它处理的是电梯从 IDLE 状态启动的场景。这里采用了“就近优先”策略即分别计算离当前楼层最近的上下行请求比较距离后决定先向哪个方向走。这个策略在真实系统中叫“顺路调度”的简化版能有效减少空跑路程。5.5 实现建筑类Building 类用于承载电梯和调度器并提供向系统中添加请求的入口。它不需要太复杂主要起到解耦作用。// 文件路径src/main/java/com/elevator/core/Building.java package com.elevator.core; import com.elevator.model.Request; public class Building { private final int totalFloors; private final Elevator elevator; private final Scheduler scheduler; public Building(int totalFloors) { this.totalFloors totalFloors; this.elevator new Elevator(1, 1, totalFloors); this.scheduler new Scheduler(elevator); } public Elevator getElevator() { return elevator; } public Scheduler getScheduler() { return scheduler; } public void callElevator(Request request) { scheduler.addRequest(request); } public int getTotalFloors() { return totalFloors; } }5.6 实现启动与模拟主程序最后是主程序。它负责初始化系统、模拟乘客请求并在循环中驱动调度器运行。// 文件路径src/main/java/com/elevator/app/SimulationMain.java package com.elevator.app; import com.elevator.core.Building; import com.elevator.model.Direction; import com.elevator.model.Request; public class SimulationMain { public static void main(String[] args) throws InterruptedException { // 初始化一栋 10 层楼的建筑电梯初始停在第 1 层 Building building new Building(10); System.out.println( 电梯模拟系统启动 ); System.out.println(初始化完成总楼层 10 层电梯停在第 1 层); // 模拟第一批请求 // 1. 5 楼有人要上楼 // 2. 7 楼有人要下楼 // 3. 电梯内乘客要去 9 楼 building.callElevator(new Request(Request.Type.HALL, 5, Direction.UP)); building.callElevator(new Request(Request.Type.HALL, 7, Direction.DOWN)); building.callElevator(new Request(Request.Type.CAR, 9, Direction.IDLE)); // 启动调度循环 int stepCount 0; int maxSteps 100; boolean running true; while (running stepCount maxSteps) { stepCount; System.out.println(---------- 第 stepCount 步 ----------); running building.getScheduler().step(); Thread.sleep(500); } if (stepCount maxSteps) { System.out.println(警告模拟步数超过 maxSteps 可能存在死循环请检查调度逻辑); } System.out.println( 电梯模拟结束 ); System.out.println(电梯最终停在第 building.getElevator().getCurrentFloor() 层); } }这里引入 maxSteps 作为保护机制避免调度逻辑出现死循环时程序无限运行。这是一个非常实用的工程习惯在模拟系统中任何循环都应该有安全上限。6. 运行结果与效果验证6.1 编译与运行如果你直接使用 JDK 编译运行可以先进入项目根目录执行以下命令javac -encoding UTF-8 -d out src/main/java/com/elevator/model/*.java src/main/java/com/elevator/core/*.java src/main/java/com/elevator/app/*.java然后运行java -cp out com.elevator.app.SimulationMain如果使用 IDEA 或 Eclipse直接运行 SimulationMain 类即可。6.2 预期输出运行后控制台应该输出类似下面的内容 电梯模拟系统启动 初始化完成总楼层 10 层电梯停在第 1 层 收到外呼请求5 层方向向上 收到外呼请求7 层方向向下 收到内招请求去往 9 层 ---------- 第 1 步 ---------- 当前方向请求已处理完毕电梯换向为上行 ---------- 第 2 步 ---------- 电梯向上运行2 层 ---------- 第 3 步 ---------- 电梯向上运行3 层 ---------- 第 4 步 ---------- 电梯向上运行4 层 ---------- 第 5 步 ---------- 电梯向上运行5 层 ---------- 第 6 步 ---------- 电梯到达 5 层开门 ---------- 第 7 步 ---------- 电梯在 5 层关门 ---------- 第 8 步 ---------- 电梯向上运行6 层 ---------- 第 9 步 ---------- 电梯向上运行7 层 ---------- 第 10 步 ---------- 电梯向上运行8 层 ---------- 第 11 步 ---------- 电梯向上运行9 层 ---------- 第 12 步 ---------- 电梯到达 9 层开门 ---------- 第 13 步 ---------- 电梯在 9 层关门 ---------- 第 14 步 ---------- 当前方向请求已处理完毕电梯换向为下行 ---------- 第 15 步 ---------- 电梯向下运行8 层 ---------- 第 16 步 ---------- 电梯向下运行7 层 ---------- 第 17 步 ---------- 电梯到达 7 层开门 ---------- 第 18 步 ---------- 电梯在 7 层关门 电梯模拟结束 电梯最终停在第 7 层6.3 如何判断运行正确判断模拟逻辑是否正确的标准有三个第一请求没有丢失。发出的 3 个请求5 楼上行、7 楼下行、9 楼内招都在日志中出现了并且都被成功响应。第二方向合理。电梯没有因为 7 楼有下行请求就立刻调头而是先完成上行方向的所有请求5 楼、9 楼再在返回时响应 7 楼的请求。这说明方向感知逻辑生效了。第三电梯没有来回空跑。整个过程电梯最多跑到 9 层就折返没有跑到 10 层再回来说明 LOOK 算法中的“最远请求”判断是正确的。如果你运行后发现电梯的路径和预期不一致优先检查hasRequestsInCurrentDirection()这个方法这是换向判断的核心。7. 常见问题与排查思路下面列几个在实现电梯模拟过程中最常遇到的问题以及对应的排查方式。问题现象可能原因排查方式解决方案电梯无响应一直停在同一层IDLE 状态时没有正确初始化方向查看日志是否有“电梯当前无请求”提示检查是否有请求被加入集合检查 addRequest 和 noRemainingRequests 逻辑电梯在到达请求楼层后没有开门状态机判断顺序错误在 step() 中打印当前楼层、当前方向、请求集合内容确保 shouldStopAtCurrentFloor 在移动之前判断电梯在一个方向上来回跑不换向hasRequestsInCurrentDirection 逻辑不完整打印每个方向的请求集合检查集合中是否残留旧请求清理已响应请求检查换向触发时机电梯跳过某层请求请求集合中使用了 List去重失败检查是否使用了 Set 存储楼层使用 TreeSet 存储楼层和请求集合程序运行一段时间后死循环调度逻辑没有终止条件加入 maxSteps 保护检查 noRemainingRequests 判断检查请求清理是否彻底移动方向与请求方向矛盾初始方向决策错误打印 decideInitialDirection 中的距离计算结果检查“就近优先”逻辑的边界条件并发场景下电梯状态冲突多线程没有加锁使用多线程时检查共享变量的可见性本文示例为单线程循环模拟若改造多线程需使用锁或原子变量这里特别强调一下“电梯跳过某层请求”的问题。如果请求集合使用 List 存储可能会出现同一个楼层被重复添加多次的问题导致电梯需要重复停靠同一层看起来就像“跳过了其他楼层”。使用 Set 去重是最简单的解法。另一个比较容易踩的坑是“清理请求的时机”。在step()方法中电梯开门后本来应该立刻清除该楼层的请求但如果把清除操作放在开门之前会导致电梯到达目标层时调度器认为该层没有请求而直接继续移动。这也是一个经典的顺序问题。8. 最佳实践与工程建议代码跑通只是第一步。如果你希望这个电梯模拟项目成为简历上的亮点或者想把它扩展到更复杂的场景下面这些建议值得认真考虑。8.1 用状态机规范整理逻辑我强烈建议在动手写代码前先画出电梯的状态机图。不需要画得多么复杂只需要列出五种状态以及每种状态下可能触发的事件。比如在 IDLE 状态下如果收到新请求应该进入 MOVING 状态在 MOVING 状态下如果到达目标楼层应该进入 DOOR_OPEN 状态在 DOOR_OPEN 状态下如果关门完成应该进入 DOOR_CLOSED 状态。状态机图的最大价值是让你在编码时能明确知道“当前状态可以执行哪些操作”。很多 bug 都是因为代码中允许了“非法状态迁移”比如电梯还在运行中却直接开门。8.2 调度策略与电梯运行逻辑解耦本文的示例中Scheduler 持有 Elevator但 Elevator 并不持有 Scheduler。这种单向依赖的设计让调度策略可以轻松替换。如果你想尝试实现 SCAN 算法只需要修改 Scheduler 的换向逻辑不需要动 Elevator 的代码。如果你想实现乘客平均等待时间最短的调度算法也只需要扩展 Scheduler 接收更多乘客信息。这种扩展性是面试官非常看重的设计能力。8.3 日志是调试电梯系统的最强工具电梯模拟系统的调试难度主要来自“状态变化非常快”。如果只靠断点很难追踪一次运行中所有状态的变化轨迹。正确的做法是在关键位置加日志收到请求时打印请求内容电梯移动时打印当前楼层和方向换向时打印原因开门关门时打印状态变化。建议统一日志格式比如[时间][楼层][方向][状态] 事件描述。这样可以快速定位问题发生的时序。在真实项目中这段日志逻辑可以直接替换为 SLF4J 等日志框架。8.4 为调度算法编写单元测试当你开始修改调度算法时你一定会后悔没有写测试。这里给一个简单思路编写一个测试方法输入一组请求然后通过多次调用step()推进电梯运行断言电梯最终停靠的楼层序列是否符合预期。核心断言有每个请求楼层都被访问过电梯的运行方向满足请求方向约束所有请求处理后电梯进入 IDLE 状态。8.5 楼层编号与边界条件在真实系统中楼层编号可能不是从 1 开始甚至存在地下车库B1、B2。在设计模型时建议允许配置 minFloor 和 maxFloor而不是硬编码为 1 到 N。另外在移动边界判断时要特别注意“最高层”和“最低层”这两个边界。如果moveUp()在最高层被调用moveDown()在最低层被调用都会抛出异常。这在模拟中是一种保护在真实系统中则对应“限位开关”逻辑。9. 从单电梯到多电梯后续学习方向写完单电梯系统后如果你还想继续深入多电梯调度是一个很好的进阶方向。多电梯相比单电梯难点在于多部电梯共享同一个建筑内的请求集合系统需要决定“哪个电梯去响应这个请求”电梯之间需要避免去同一个楼层造成空跑需要考虑高峰期的人流分布。一种常见的设计是引入中央调度器它维护一个全局请求池并且根据每部电梯的当前位置、当前方向、当前载客状态计算每个电梯的“响应成本”然后选择成本最低的电梯去响应请求。另一种常见设计是“区域划分”比如把一栋 30 层的大楼分成高低区低区电梯只服务 1-15 层高区电梯只服务 16-30 层大堂换乘层单独处理。这种设计在真实写字楼里非常普遍但涉及“换乘”逻辑复杂度会明显上升。如果你对算法感兴趣还可以研究一下遗传算法在电梯群控系统中的应用。这算是电梯调度领域的研究型方向虽然工程落地较少但作为学术训练非常有价值。本文的重点是把单电梯的调度逻辑讲透包括状态机、方向感知、LOOK 算法、请求集合管理这些核心组件。你可以先基于这份代码跑通一个简单模拟再逐步加入更复杂的调度策略和多电梯逻辑。核心的技巧是保持调度器与电梯解耦用日志观察运行过程用状态机约束逻辑边界。做到这三点电梯调度这个经典项目就不会再是你的拦路虎。
返回列表