☰
Java课程设计停车场管理系统:用栈、队列、链表讲透数据结构
2026/10/2 13:14:37 网站建设 项目流程

简介:面向数据结构课程设计场景的Java停车场管理系统项目,完整提供用户界面、车辆存取、候车区调度等功能模块。项目通过自定义链表队列、链栈及链表节点等数据结构,实现车位占用与候车区调度逻辑,车辆信息类保存车牌号和停车时间,数据总控类统一维护停车场及候车区状态,整体层次清晰。压缩包共15个文件,包含6个可读的Java源文件和9个编译后的class字节码文件,大小仅17KB,非常适合直接阅读源码、学习界面与数据层的关联,或作为课程设计实现参考。目前已有2640人学习,尤其适合正在学习Java界面编程、链表队列栈原理,或需要完成同类课设的高校学生。借助这一项目,读者能理解栈和队列在真实管理系统中的具体应用,以及面向对象封装带来的可维护性。

1. 停车场管理系统:Java课程设计里真正能讲清楚的数据结构题

很多人的Java课程设计是从网上拖一份停车场管理系统源码、改个作者名就交的,答辩前夜还在背代码。结果老师一问“你这个停车场为什么要用栈,队列用在哪”,当场卡壳。停车场管理系统这个题目的价值恰恰在这里:栈、队列、链表、排序、时间计算这些数据结构知识点,被一个真实的业务场景串起来了。它不考验你写多炫的界面,而是考验你能不能把“一辆车进来、一辆车出去、临时让路、排队等待”这些动作,用正确的数据结构一一对应。这篇文章会把需求拆解、数据结构选型、核心代码、典型踩坑和答辩技巧一次讲透,适合正在做Java课程设计的学生,也适合想拿真实业务复习数据结构的人。

2. 需求先于代码:把停车业务翻译成栈、队列和链表

2.1 停车场业务拆解:先来后到、倒车让路、排队等待

课程设计里的停车场,通常不是地上画好一个个独立车位,而是“狭长通道式”停车场——车只能从一端进入,开出去的时候,如果目标车前面停着别的车,那些车得先倒出来让路,等目标车开走后再按原顺序倒回去。这个设定很关键,它就是栈的适用场景。

把业务拆成三个连续的动作。第一,车辆驶入:停车场内有空位就停进去,没空位就在入口处的候车道排队。第二,车辆驶出:找到目标车,把它后面停的车临时挪到旁边,目标车开出去,临时挪走的车按原顺序倒回去,然后从候车道放一辆车进来补位。第三,计费和统计:车辆离场时根据停车时长算费用,日报表要按停车时长从长到短排序。

几乎每本教材里的车辆管理题都是这套规则。学生党最容易犯的错是一上来就写代码,写着写着发现“这辆车怎么跑到那辆车前面去了”。所以动手前,先把这三条业务规则写在最显眼的地方,后面的数据结构选型是跟着它们走的。

2.2 为什么是栈、队列和链表,而不是直接上HashMap

有人会觉得:都是存车嘛,用一个HashMap存车牌号和车辆信息不是更简单?HashMap适合“按键查值”,比如“按车牌号查这辆车停了多久”。但停车场管理的核心动作是“先进后出”和“先进先出”,这是顺序语义,HashMap完全帮不上忙,而且HashMap的遍历顺序也不稳定。

先来后到、没车位就在外面排着,这一条对应队列Queue,FIFO(先进先出),先进候车道的车先获得补位资格。倒车让路,这一条对应栈Stack,LIFO(后进先出),最后停进来的车最先被挪走,正好是栈顶元素。临时挪走的车再倒回去,还是栈,因为挪出时是“从上往下挪”,回去时也得“从下往上放”,顺序不能乱。

空闲位的管理,最朴素的实现是一个计数器(capacity - parkingStack.size()),如果想做得更真实,也可以用链表记录哪些车位是空闲的。这里的数据结构链表不是花架子,它可以按车位编号组织空闲位,每次分配从头部拿一个,释放时挂到尾部,配合计数器使用。报表排序再用到List + Comparator,一个课设下来,栈、队列、链表、排序算法全沾上了,这正是老师想看到的选型思路。

数据结构选型对照表如下:

业务场景数据结构选择理由
临时让路与倒回栈 Stack后进先出,正好匹配“后面的车先挪走先倒回”
候车道等待补位队列 Queue先进先出,先排队的先进入停车场
空闲车位分配计数器/链表计数器最简单,链表可扩展空闲位顺序管理
历史记录排序List + Comparator存储对象后再按时长字段排序,复习排序算法

2.3 收费规则与时间差:把业务变量拆成参数,别写死在代码里

计费模块看着简单,其实是课设答辩时老师最爱追问的点。常见收费规则有三种:固定费用(不论停多久都是X元)、阶梯费用(首小时X元,之后每小时Y元,封顶Z元)、时段费用(白天和夜间单价不同)。第一版不要贪多,做固定费用或阶梯费用就够用,但要把单价、封顶金额这些值从代码里拆出来。

推荐做法是在src目录下放一个parking.properties配置文件:

# 停车场基础配置 parking.capacity=10 fee.base=5 fee.perHour=2 fee.maxPerDay=30

Java侧用一个配置类去读,而不是散落在各处硬编码。这样答辩的时候你可以说:“单价和容量都是从配置文件读的,改配置不用重新编译。”这一句话能抵过很多代码。时间差计算也有讲究,入场时间用LocalDateTime存下来,离场时用Duration.between(入场时间,离场时间)取分钟数,再向上取整到小时。注意别用System.currentTimeMillis()存两个long再减,虽然也能算,但可读性差,而且面试官看到LocalDateTime会给你加印象分。

3. 最小可运行版本:车辆进场、出场与报表的三段核心代码

3.1 车辆进入:用Stack模拟停车场,用Queue模拟候车道

先定义Vehicle实体类,字段越少越好,够用就行。这里用Java 8以上的localDateTime记录入场时间。

import java.time.LocalDateTime; public class Vehicle { private String plateNo; // 车牌号 private LocalDateTime entryTime; // 入场时间 public Vehicle(String plateNo) { this.plateNo = plateNo; this.entryTime = LocalDateTime.now(); } public String getPlateNo() { return plateNo; } public LocalDateTime getEntryTime() { return entryTime; } }

然后写ParkingLot核心类。停车场用Stack ,候车道用Queue (实现类用LinkedList,它实现了Queue接口,用起来最顺手)。

import java.util.Queue; import java.util.LinkedList; import java.util.Stack; public class ParkingLot { private int capacity; // 总车位数,从配置文件读取 private Stack<Vehicle> parkingStack = new Stack<>(); private Queue<Vehicle> waitingQueue = new LinkedList<>(); public ParkingLot(int capacity) { this.capacity = capacity; } // 车辆驶入 public void enter(Vehicle vehicle) { if (parkingStack.size() < capacity) { parkingStack.push(vehicle); System.out.println(vehicle.getPlateNo() + " 已进入停车场,停在" + parkingStack.size() + "号位"); } else { waitingQueue.offer(vehicle); System.out.println(vehicle.getPlateNo() + " 停车场已满,进入候车道等待"); } } }

逻辑说明:enter方法先判断栈的大小是否小于容量,小于说明还有空位,直接入栈;等于容量说明满了,进候车道等待,用offer方法入队。这里用parkingStack.size()而不是维护一个独立计数器,是为了避免两个变量不同步的麻烦。参数说明:capacity是停车场总位数,记得从配置类里读出来传给构造函数;waitingQueue用LinkedList实例化,因为LinkedList同时实现了Queue和List,后续要打印排队车辆时遍历也方便。

3.2 车辆驶出:栈倒车+队列补位,顺序千万别反

驶出是这整个项目里最容易翻车的逻辑,核心是“临时栈”的运用。要开走一辆车,得先把停在它后面的车(栈顶到它上面一层)依次弹出,暂存在一个临时栈里,等目标车弹出后,再把临时栈里的车一辆辆弹回停车场栈。

import java.util.Stack; public void exit(String plateNo) { Stack<Vehicle> tempStack = new Stack<>(); boolean found = false; // 第一步:从停车栈顶往下找目标车,沿途车辆暂存到临时栈 while (!parkingStack.isEmpty()) { Vehicle vehicle = parkingStack.pop(); if (vehicle.getPlateNo().equals(plateNo)) { System.out.println(vehicle.getPlateNo() + " 驶出停车场,停车时长:" + vehicle.getEntryTime() + " 到 " + LocalDateTime.now()); found = true; break; } tempStack.push(vehicle); } if (!found) { System.out.println("未找到车牌号为 " + plateNo + " 的车辆"); // 没找到时,要把临时栈的车倒回去,否则停车场就乱了 while (!tempStack.isEmpty()) { parkingStack.push(tempStack.pop()); } return; } // 第二步:临时栈的车按原顺序倒回停车栈 while (!tempStack.isEmpty()) { parkingStack.push(tempStack.pop()); } // 第三步:候车道补一辆车进来 if (!waitingQueue.isEmpty()) { Vehicle nextVehicle = waitingQueue.poll(); parkingStack.push(nextVehicle); System.out.println(nextVehicle.getPlateNo() + " 从候车道进入停车场"); } }

这段代码有三个关键点。第一,临时栈的倒回顺序:临时栈里最顶上的车是最后被挪走的,倒回时它最后被push进停车栈,正好又停在最外面,和原来的相对顺序一致。第二,目标车没找到时,临时栈里的车必须倒回去再return,漏掉这一步会导致停车场栈里的车凭空消失。第三,补位动作放在最后,先倒回再补位,顺序不能反,否则停车栈的栈顶顺序会错乱。答辩时老师喜欢问“为什么不是先补位再倒回”,回答就是:倒回动作针对的是已经在停车场里的车,补位动作针对的是候车道的车,业务顺序上必须等现有车辆归位后再放新车进来。

3.3 报表与排序:按停车时长排序,顺便复习比较器

课程设计通常要求输出当日停车报表,按停车时长从长到短排序。实际项目中车辆离场时可以先把记录放进一个ArrayList,报表时统一排序。Java里对对象排序有两种常见方式:让Vehicle类实现Comparable,或者单独写Comparator。建议单独写Comparator,把排序规则和实体类解耦。

import java.time.Duration; import java.util.ArrayList; import java.util.Comparator; import java.util.List; public static void printReport(List<Vehicle> history) { List<Vehicle> sortedList = new ArrayList<>(history); // 按停车时长从长到短排序,时长从入场时间与出场时间差计算 sortedList.sort(Comparator.comparingLong((Vehicle v) -> { LocalDateTime exitTime = v.getExitTime(); if (exitTime == null) { return 0L; } return Duration.between(v.getEntryTime(), exitTime).toMinutes(); }).reversed()); for (Vehicle v : sortedList) { System.out.println(v.getPlateNo() + " 停车时长:" + Duration.between(v.getEntryTime(), v.getExitTime()).toMinutes() + " 分钟"); } }

参数说明:Vehicle类里需要额外加一个getExitTime方法,驶出时setExitTime(LocalDateTime.now()),报表才能算时长。这样写的好处是:Comparator的comparingLong指定了排序字段的提取方式,reversed()实现降序,要改成升序就去掉reversed()。排序算法的底层仍然是Java的TimSort(一种稳定的归并+插入的混合排序,时间复杂度O(n log n)),答辩提到这里可以直接说“我调用了Collections的排序,底层是稳定排序,不需要自己重写排序算法,但我知道它的复杂度。”这一句能把排序算法知识点带出来。

4. 避坑:课设里最容易让程序崩掉的5个地方

4.1 用ArrayList当栈用,出场顺序全乱了

现象:打印停车顺序时发现,后进来的车反而被排在列表前面,倒车逻辑也乱了,明明应该最后进来的车最先让路,结果变成最先来的车先被挪走。

原因:图省事用ArrayList管理场内车辆,直接add()把新车加到尾部,然后remove()删中间元素。ArrayList是线性表,它本身不提供“先取末尾元素”的语义,你需要自己维护size并在每次删除时搬移元素,过程中很容易漏更新索引。

解决:直接用java.util.Stack或ArrayDeque。Stack的push/pop就是教科书级栈操作。如果出于性能考虑,用ArrayDeque替代Stack也可以,它实现了Deque接口,push/pop语义一致,而且线程不安全但性能更好。课设层面用Stack最直观,面试官挑不出问题。

4.2 SimpleDateFormat在多线程下报错或时间错乱

现象:把进场时间用FastDateFormat格式化,或者用SimpleDateFormat做格式化时,偶尔出现时间显示成“2023-13-45 99:99”这样的乱码,甚至抛出NumberFormatException。

原因:SimpleDateFormat不是线程安全的,它的内部calendar是共享可变状态。课程设计里如果你在车辆进出场逻辑里用了多线程(比如模拟多辆车同时到),并发调用同一个SimpleDateFormat实例就会出问题。

解决:改用java.time包下的LocalDateTime和DateTimeFormatter,它们是不可变且线程安全的。格式化示例如下:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String timeStr = vehicle.getEntryTime().format(formatter);

如果没有用多线程也出现了时间问题,多半是格式化pattern写错了,检查是不是把HH(24小时制)写成了hh(12小时制),以及分钟是否用了MM(月份)而不是mm。

4.3 车辆没停就驶出,时间差计算空指针

现象:调用exit方法时报NullPointerException,定位到Duration.between(entryTime, exitTime)这行。

原因:要么Vehicle对象在构造时没有给entryTime赋值(比如从数据库读出来的时候没映射上),要么exitTime在set之前就被拿去计算时长。业务上没停就驶出属于非法操作,你的程序没有拦截。

解决:在exit方法的一开始就校验车辆状态,入场时间和出场时间任何一个为null就直接抛业务异常,而不是等到计算时才爆空指针。

if (vehicle.getEntryTime() == null) { throw new IllegalStateException("车辆 " + plateNo + " 缺少入场时间,无法计费"); }

另外注意:LocalDateTime.now()不会返回null,入场时间基本不会为空,空指针大概率出在“退场记录”的出场时间字段上,该set的时候没set,或者set了另一个对象的字段。

4.4 遍历集合时删除元素抛ConcurrentModificationException

现象:在报表模块里想顺手把超时车辆从List里移除,用for-each循环遍历,循环体里调list.remove(vehicle),运行时报ConcurrentModificationException。

原因:for-each循环底层是Iterator,删除用的是list.remove,这会改变集合的modCount,和Iterator内部的expectedModCount不一致,迭代器检测到并发修改直接拒绝继续迭代。

解决:改用Iterator显式删除,这是最规矩的写法:

Iterator<Vehicle> iterator = history.iterator(); while (iterator.hasNext()) { Vehicle v = iterator.next(); if (v.getExitTime() != null && Duration.between(v.getEntryTime(), v.getExitTime()).toMinutes() > 24 * 60) { iterator.remove(); } }

如果只是为了排序后取前几条数据,完全不用删除,直接sortedList.subList(0, Math.min(5, sortedList.size()))就够了,少动集合结构就少一个坑。

4.5 中文车牌号在控制台读进来乱码

现象:控制台输入“京A12345”回车,程序打印出来的车牌变成“???A12345”或“浜?A12345”。

原因:Windows控制台默认GBK编码,而IDE的项目文件是UTF-8编码,System.in读入的字节流按GBK解成字符,到了内部字符串再打印时,编码对不上就成乱码。

解决:分情况处理。如果你用的是IDEA,菜单里设置Help -> Edit Custom VM Options,加上-Dfile.encoding=UTF-8,然后重启IDE,同时确保控制台编码也是UTF-8(Run -> Edit Configurations,VM options里加-Dfile.encoding=UTF-8)。如果你用的是Windows命令行直接跑,可以在代码里指定读入的编码:

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));

不过更省心的做法是:第一版先别在控制台中文输入上较劲,用英文车牌或数字测试,把业务逻辑跑通后再处理编码。课设评审更看重逻辑正确,而不是中文显示——但如果你主动把编码问题处理好,这本身就是个可讲的加分项。

5. 从课设到面试:让停车场代码变成你讲得清的面试题

5.1 用实验报告把复杂度分析写清楚,答辩才站得住

做课设不只是交代码,数据结构实验报告才是评分重头。报告里值得写清楚的三张表:各操作的时间复杂度(入栈、出栈、队列poll都是O(1),但在停车栈里查找指定车牌是O(n));空间复杂度(栈+队列共O(n));以及“如果停车场扩容到1000个车位会怎么样”——引导老师往你准备好的方向问。写报告时注意把“为什么出栈是O(1)但找人要O(n)”说透,这才是数据结构知识的真正考点,代码反而其次。

5.2 两个进阶方向:文件持久化与并发模拟

基础版跑通后,想拿高分通常做两件事。第一件是把进出场记录持久化到本地文件,用serialization或文本追加都行。第二件是用并发模拟多辆车同时进场,比如用线程池创建5个线程同时调用enter和exit,这时候你就能真切理解为什么第4章提到SimpleDateFormat线程安全问题了。两个方向都不难,挑一个深入即可。

做并发模拟时记得给enter和exit加锁(synchronized作用于ParkingLot对象),否则停车栈会线程不安全。也因为这个原因,面试题里常出现“Stack是线程安全的吗”——Stack继承自Vector,方法加了synchronized,但只在单个方法级别加锁,复合操作(比如先判断size再push)仍然需要外部加锁才能保证原子性。这个知识点能答上来,比你多写100行代码都管用。

最后分享一个教训。当年我自己做这个课设时,堆了300行代码,封装了六七个类,结果答辩讲得磕磕绊绊,因为心里只有代码块,没有把业务动作连成故事线。后来养成的习惯是:开写之前在纸上画出车辆从进到出的动作链,标清楚哪一步对应栈、哪一步对应队列,再动手写代码。这个习惯后来也带到了项目问题的排查里。做一个项目课设,值钱的不是交上去的代码,而是你脑子里能不能随时把业务讲成数据结构。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询