Java实现电梯调度算法:状态机与LOOK策略详解
2026/8/27 23:08:13 网站建设 项目流程

电梯调度算法,几乎是每一个学编程的人都绕不过去的经典项目。很多人在学校做课程设计、找实习写项目、或者自学练手时,都会选择“模拟电梯”这个题目。它看起来不复杂:无非就是电梯上下跑、有人按楼层、开关门而已。但真正动手写的时候,很多人会发现一个共同的问题——电梯的运行逻辑,远比你想象的难写

如果你现在正在被电梯调度问题困扰,或者刚拿到一个“模拟电梯”的课程设计题目毫无头绪,这篇文章就是为你准备的。我会从设计角度出发,把电梯模拟系统的核心拆解成:状态机设计、调度算法、数据结构选型、多线程处理这几个部分,并给出完整的 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 流程时序

整个模拟运行的核心流程是:

  1. 初始化建筑和电梯;
  2. 模拟乘客产生请求(外呼或内招);
  3. 调度器接收请求,更新电梯的待办集合;
  4. 电梯根据当前状态和方向,按照调度算法计算下一步行动;
  5. 电梯执行行动(上行/下行/开门/关门);
  6. 每次行动后检查是否到达目标层、是否应该开门、是否应该改变方向;
  7. 重复步骤 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 Set<Integer> 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 Set<Integer> 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 算法的变体,核心逻辑如下:

  1. 如果电梯处于 IDLE 状态,并且没有任何请求,则保持静止。
  2. 如果电梯当前方向为 UP,则优先处理所有高于当前楼层的内招请求和外呼上行请求;如果这些请求都处理完了,再考虑转向处理下行请求。
  3. 如果电梯当前方向为 DOWN,则优先处理所有低于当前楼层的内招请求和外呼下行请求。
  4. 当电梯在当前方向上没有任何待响应的请求时,改变方向。

为了让电梯在到达某层时判断“是否应该开门”,我们需要一个辅助方法:判断当前楼层是否存在于任何一个待响应的请求集合中。这里把外呼请求按方向拆分成两个集合,即上行请求集合和下行请求集合。

// 文件路径: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 Set<Integer> upHallRequests; private final Set<Integer> 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 算法、请求集合管理这些核心组件。你可以先基于这份代码跑通一个简单模拟,再逐步加入更复杂的调度策略和多电梯逻辑。核心的技巧是:保持调度器与电梯解耦,用日志观察运行过程,用状态机约束逻辑边界。做到这三点,电梯调度这个经典项目就不会再是你的拦路虎。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询