Java停车场管理系统:数据结构选型与核心实现

📅 发布时间:2026/10/8 15:50:28
Java停车场管理系统:数据结构选型与核心实现
简介这份资源是面向高校计算机专业学生的Java课程设计参考方案主题为基于数据结构实现的停车场管理系统适合正在完成数据结构或Java大作业、需要完整项目范例的学习者。压缩包共15个文件约17KB以6个java源码文件和9个class编译文件为主源码涵盖自定义链表队列MyListQueue、栈MyStack、链表节点ListNode、车辆信息类CarInfo以及负责整体数据管理的Data类class文件则对应各模块的编译结果便于直接运行验证。系统采用Java图形界面实现存车与取车操作通过队列模拟车位先进先出管理并借助栈处理车位满载时车辆进入候车区的逻辑同时包含异常处理与事件监听等细节。目前已有2640人学习下载读者可从中获得一套结构清晰、可直接参考的课程设计实现思路理解队列、栈与链表在实际场景中的综合运用并借鉴其类划分与界面事件组织方式为独立完成类似系统提供可复用的代码基础。1. 停车场管理系统从数据结构选型到 Java 落地的完整拆解很多同学做 Java 课程设计时第一反应是打开 IDE 直接写ParkingLot类结果写到一半发现车位查找要遍历整个列表、出场计费时找不到入场记录、VIP 车位被普通车占了还没法追溯。停车场管理系统看起来是个 CRUD 小项目但它真正考的是你对数据结构与算法的选择能力——栈、队列、哈希表、优先队列各自该用在哪直接决定了代码是「能跑」还是「能拿优」。这篇笔记面向正在做 Java 课程设计、或者想用这个题目把数据结构串一遍的读者从需求拆解、结构选型、核心代码到调试踩坑给出一条能直接复现的路径。读完你应该能独立写出一个带计时计费、车位分配、历史查询的停车场管理系统并且说得清每个结构为什么这么选。2. 需求拆解与数据结构选型别急着写类2.1 停车场管理系统到底要管哪几件事先把功能列清楚不然后面结构选型全是拍脑袋。一个课程设计级别的停车场管理系统核心需求通常收敛为五件事车辆入场时分配车位、车辆出场时释放车位并计费、查询当前场内车辆、查询某个车位的占用状态、查看历史进出记录。如果再往上加一层就是 VIP 车位优先分配、月卡车辆免费、按时间段差异化计费。这五件事里真正吃数据结构的是前三件。入场要快速找到空车位出场要快速定位这辆车的入场时间和车位号查询场内车辆要能按车牌号或车位号两个维度检索。如果你用ArrayList存所有车位每次找空位就是 O(n) 遍历用ArrayList存入场记录出场时按车牌号找记录又是 O(n)。车一多虽然课程设计数据量不大但代码会写得又臭又长答辩时老师一问「为什么不用哈希表」就露馅了。所以正确的顺序是先明确每个操作的频率和查询维度再决定用哪个结构。入场和出场是高频写操作查询场内车辆是高频读操作历史记录是低频追加、偶尔查询。这个读写特征直接指向哈希表 队列 栈的组合。2.2 栈、队列、哈希表、优先队列各自负责什么车位分配用优先队列PriorityQueue最合适。普通车位和 VIP 车位可以统一编号VIP 车位编号小、优先级高用最小堆每次弹出编号最小的空车位天然实现「VIP 优先、普通车位按序分配」。如果你用普通队列VIP 车来了没法插队用栈的话车位分配顺序会变成后进先出不符合停车场实际。入场记录用哈希表HashMap存key 是车牌号value 是入场信息对象车位号、入场时间、是否 VIP。出场时map.get(车牌号)直接 O(1) 拿到记录不用遍历。同时再维护一个HashMapInteger, Car以车位号为 key用于按车位查车。两个 map 共享同一个 Car 对象引用不会有一致性问题。历史记录用队列或LinkedList追加。出场后的记录按时间顺序追加到LinkedList尾部查询历史时从头遍历。如果要求「最近出场的先看到」就用Deque从头部插入。这里用队列是因为历史记录天然是先进先出的时间序列。空闲车位池用优先队列已占用车位用哈希表入场记录用哈希表历史记录用链表。四个结构各司其职没有一个是多余的。这就是数据结构课程设计真正想考的东西不是你会不会写类而是你能不能说清楚每个结构在系统里承担什么角色。2.3 用一张表把操作和结构对应起来操作数据结构时间复杂度选型理由分配空车位PriorityQueueO(log n)VIP 优先自动排序车辆入场登记HashMapO(1)按车牌号快速索引车辆出场计费HashMapO(1)按车牌号取入场记录按车位查车HashMapO(1)车位号到车辆的映射查询场内车辆HashMap.values()O(n)遍历当前在场车辆历史记录追加LinkedListO(1)尾部追加顺序保留历史记录查询LinkedListO(n)按时间顺序遍历这张表建议直接写进课程设计报告的系统设计章节。老师看的就是你能不能把「为什么选这个结构」讲清楚而不是你写了多少行代码。表格里每个 O(1) 和 O(log n) 都是答辩时的得分点。3. 核心模块的 Java 实现从车位分配到计费3.1 车位分配用 PriorityQueue 的最小堆写法先定义车位和车辆的基础类。车位只需要编号和类型车辆需要车牌号、入场时间、车位号。这里用record还是普通类看你的 Java 版本课程设计一般用 Java 8 以上普通类更稳妥。import java.time.LocalDateTime; import java.util.PriorityQueue; // 车位类实现 Comparable 以便优先队列排序 class ParkingSpot implements ComparableParkingSpot { int spotId; // 车位编号VIP 车位编号从 1 开始 boolean isVip; // 是否 VIP 车位 public ParkingSpot(int spotId, boolean isVip) { this.spotId spotId; this.isVip isVip; } // 编号小的优先VIP 车位编号小自然优先分配 Override public int compareTo(ParkingSpot other) { return Integer.compare(this.spotId, other.spotId); } } // 车辆入场记录 class CarRecord { String plate; // 车牌号 int spotId; // 分配的车位号 LocalDateTime entryTime; // 入场时间 boolean isVip; // 是否 VIP public CarRecord(String plate, int spotId, LocalDateTime entryTime, boolean isVip) { this.plate plate; this.spotId spotId; this.entryTime entryTime; this.isVip isVip; } }PriorityQueue的排序逻辑靠compareTo方法。这里用Integer.compare而不是直接相减是为了避免整数溢出。VIP 车位编号设为 1 到 10普通车位从 11 开始这样最小堆每次弹出的就是编号最小的空车位VIP 车来了自然拿到 VIP 车位。如果你想让 VIP 车位只给 VIP 车那就在分配时判断VIP 车从 VIP 队列取普通车从普通队列取两个队列分开维护。课程设计里两种做法都可以但分开维护更贴近真实场景也更容易在报告里画图说明。初始化车位池的代码PriorityQueueParkingSpot vipSpots new PriorityQueue(); PriorityQueueParkingSpot normalSpots new PriorityQueue(); // 初始化 10 个 VIP 车位和 50 个普通车位 for (int i 1; i 10; i) { vipSpots.offer(new ParkingSpot(i, true)); } for (int i 11; i 60; i) { normalSpots.offer(new ParkingSpot(i, false)); }offer和add在PriorityQueue里等价但offer在队列满时返回 false 而不抛异常更安全。入场分配时先判断车辆类型VIP 车优先从vipSpots取取不到再从normalSpots取普通车只能从normalSpots取。这样 VIP 车位的利用率最高也不会出现普通车占 VIP 位的情况。3.2 入场登记用 HashMap 做 O(1) 索引入场操作要做三件事分配车位、记录入场时间、把记录放进两个 map。代码不复杂但顺序和判空要小心。import java.util.HashMap; import java.util.Map; MapString, CarRecord plateToRecord new HashMap(); // 车牌 - 记录 MapInteger, CarRecord spotToRecord new HashMap(); // 车位 - 记录 LinkedListCarRecord history new LinkedList(); // 历史记录 // 车辆入场 public boolean enter(String plate, boolean isVip) { // 先检查是否已经在场 if (plateToRecord.containsKey(plate)) { System.out.println(车牌 plate 已在场内); return false; } // 分配车位VIP 优先从 VIP 队列取 ParkingSpot spot null; if (isVip !vipSpots.isEmpty()) { spot vipSpots.poll(); } else if (!normalSpots.isEmpty()) { spot normalSpots.poll(); } if (spot null) { System.out.println(车位已满无法入场); return false; } // 创建记录并放入两个 map CarRecord record new CarRecord(plate, spot.spotId, LocalDateTime.now(), isVip); plateToRecord.put(plate, record); spotToRecord.put(spot.spotId, record); System.out.println(车牌 plate 入场成功车位号 spot.spotId); return true; }containsKey判重是必须的否则同一辆车入场两次会覆盖旧记录出场时计费就乱了。poll方法在队列为空时返回 null所以后面要判空。两个 map 存的是同一个CarRecord对象引用修改其中一个 map 里的记录另一个 map 看到的也是同一份数据不需要同步。这里有个细节如果 VIP 车来了但 VIP 车位满了代码会落到normalSpots分支这是合理的降级策略。如果你要求 VIP 车必须等 VIP 车位那就把else if改成独立的if并返回失败。3.3 出场计费与车位回收的完整流程出场是最容易出 bug 的地方。要按车牌号找到记录、计算停车时长、根据费率算钱、把车位还回优先队列、把记录从两个 map 移除、追加到历史记录。顺序错了就会出现「车位已还但记录还在」或者「记录删了但车位没还」的脏状态。// 车辆出场 public double exit(String plate) { CarRecord record plateToRecord.get(plate); if (record null) { System.out.println(车牌 plate 不在场内); return -1; } // 计算停车时长分钟 LocalDateTime exitTime LocalDateTime.now(); long minutes java.time.Duration.between(record.entryTime, exitTime).toMinutes(); // 不足一分钟按一分钟算 if (minutes 0) minutes 1; // 计费VIP 每小时 5 元普通每小时 10 元不足一小时按一小时算 double rate record.isVip ? 5.0 : 10.0; long hours (minutes 59) / 60; // 向上取整 double fee hours * rate; // 回收车位到对应的优先队列 ParkingSpot spot new ParkingSpot(record.spotId, record.isVip); if (record.isVip) { vipSpots.offer(spot); } else { normalSpots.offer(spot); } // 从两个 map 移除 plateToRecord.remove(plate); spotToRecord.remove(record.spotId); // 追加到历史记录 history.add(record); System.out.printf(车牌 %s 出场车位 %d停车 %d 分钟费用 %.2f 元%n, plate, record.spotId, minutes, fee); return fee; }计费逻辑里(minutes 59) / 60是向上取整的常用写法比Math.ceil更直观。车位回收时新建了一个ParkingSpot对象而不是复用原来的因为原来的对象已经不在队列里了新建一个编号和类型相同的对象放回去即可。这里有个坑如果你把record里的车位对象直接存下来复用要注意PriorityQueue里的对象不能被外部修改否则堆序会乱。新建对象是最安全的做法。历史记录用LinkedList的add方法追加到尾部查询时从头遍历就是按时间顺序。如果你要按出场时间倒序展示用addFirst或者查询时反向遍历。4. 避坑与排查课程设计里最容易翻车的 5 个点4.1 车位回收后编号重复或丢失现象车辆出场后再次入场分配到的车位号跟之前一样但系统里显示两个相同编号的车位或者车位池越来越少最后提示车位已满但实际场内没几辆车。原因回收车位时没有把车位对象放回优先队列或者放回时用了错误的队列VIP 车位放进了普通队列。还有一种情况是出场时只从plateToRecord移除了记录忘了从spotToRecord移除导致车位号一直被占用。解决出场方法里严格按「回收车位 → 移除两个 map → 追加历史」的顺序执行。回收时根据record.isVip判断放回哪个队列。建议在每次入场和出场后打印两个队列的 size 和两个 map 的 size观察是否守恒vipSpots.size() normalSpots.size() plateToRecord.size()应该等于总车位数。这个守恒等式是排查车位泄漏的最快方法。4.2 同一车牌重复入场导致记录覆盖现象同一辆车连续入场两次第二次入场成功但出场时只计了一次费或者计费时间从第一次入场开始算。原因入场时没有做containsKey判重直接put覆盖了旧记录。旧记录里的车位号还在spotToRecord里但plateToRecord已经指向新记录两个 map 数据不一致。解决入场第一步就检查plateToRecord.containsKey(plate)如果存在直接拒绝并提示。这个检查要放在分配车位之前避免分配了车位又回滚。如果你允许同一车牌多辆车比如临时车牌那就要用「车牌 入场时间」做联合 key但课程设计里一般不需要这么复杂。4.3 计费时长算出负数或零现象出场时显示停车 0 分钟或负数分钟费用为 0 或负数。原因LocalDateTime.now()在测试时被手动修改过或者入场时间用了系统默认时区而出场用了另一个时区。更常见的是测试时直接构造了一个未来时间的CarRecord导致Duration.between算出负数。解决在计费前加一层保护如果minutes 0就按 1 分钟算。同时检查record.entryTime是否为空。测试时不要手动改系统时间而是通过构造CarRecord时传入一个过去的时间来模拟长停车。比如LocalDateTime.now().minusHours(3)表示 3 小时前入场。4.4 PriorityQueue 遍历顺序不等于出队顺序现象用for (ParkingSpot s : vipSpots)打印车位池发现顺序是乱的跟预期的小到大不一致。原因PriorityQueue底层是二叉堆只保证队首元素最小不保证遍历顺序有序。iterator()返回的是堆的数组顺序不是排序顺序。解决如果要按顺序展示车位池不要直接遍历PriorityQueue。可以先把队列复制一份然后循环poll出来打印打印完再放回去或者用Arrays.sort对toArray的结果排序。但注意poll会破坏原队列必须用副本。课程设计里展示车位池时建议用new PriorityQueue(vipSpots)复制一份再遍历。4.5 历史记录查询时并发修改异常现象遍历历史记录的同时有车辆出场追加记录抛出ConcurrentModificationException。原因用for-each遍历LinkedList时如果另一个线程或同一个线程的另一个方法修改了链表结构迭代器会抛异常。课程设计一般是单线程但如果你在遍历历史记录的方法里调用了exit就会触发。解决查询历史记录时用new LinkedList(history)复制一份再遍历或者用ListIterator并手动处理。更简单的做法是查询方法只读不调用任何写方法。如果确实需要边遍历边修改用CopyOnWriteArrayList或者加同步块但课程设计里没必要上并发容器复制一份最省事。5. 进阶技巧用命令行交互把系统跑起来并验证5.1 写一个最小可用的 Main 方法前面都是类和方法现在把它们串成一个能交互的命令行程序。不需要图形界面Scanner读输入就够了。这个 Main 方法可以直接作为课程设计的演示入口。import java.util.Scanner; public class ParkingLotSystem { public static void main(String[] args) { ParkingManager manager new ParkingManager(); Scanner scanner new Scanner(System.in); System.out.println(停车场管理系统已启动); System.out.println(命令in 车牌 VIP(true/false) | out 车牌 | list | history | quit); while (true) { System.out.print( ); String line scanner.nextLine().trim(); if (line.isEmpty()) continue; String[] parts line.split(\\s); String cmd parts[0].toLowerCase(); switch (cmd) { case in: if (parts.length 3) { System.out.println(用法in 车牌 VIP(true/false)); break; } boolean isVip Boolean.parseBoolean(parts[2]); manager.enter(parts[1], isVip); break; case out: if (parts.length 2) { System.out.println(用法out 车牌); break; } manager.exit(parts[1]); break; case list: manager.listCurrentCars(); break; case history: manager.listHistory(); break; case quit: System.out.println(系统退出); scanner.close(); return; default: System.out.println(未知命令); } } } }ParkingManager类把前面所有的 map、队列、方法封装进去Main 只负责解析命令和调用。split(\\s)用正则匹配一个或多个空格避免用户多打空格导致解析失败。Boolean.parseBoolean对非 true 的输入都返回 false所以用户输入 yes 会被当成普通车这个行为要在文档里写清楚。5.2 用一组测试用例验证核心逻辑跑起来之后按下面的顺序输入观察输出是否符合预期。这组用例覆盖了入场、VIP 优先、出场计费、重复入场、车位回收五个场景。# 启动后依次输入 in 京A12345 true in 京B67890 false in 京C11111 true list out 京A12345 list in 京D22222 false history quit预期结果前两条入场分别拿到 VIP 车位 1 和普通车位 11第三条 VIP 车拿到 VIP 车位 2list显示三辆车out 京A12345后费用按 VIP 费率算再次list只剩两辆in 京D22222应该拿到刚释放的 VIP 车位 1 吗不会因为京D22222 是普通车只能从普通队列取拿到的是车位 12。这个细节能验证 VIP 车位不会被普通车占用。history显示京A12345 的出场记录。如果你发现in 京D22222拿到了车位 1说明 VIP 车位回收时放错了队列回到 4.1 检查。5.3 把历史记录导出成 CSV 的小技巧课程设计报告里通常要求附运行截图或数据导出。加一个导出方法把history里的记录写成 CSV用 Excel 打开就能当附件。import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; public void exportHistoryToCsv(String filePath) { try (BufferedWriter writer new BufferedWriter(new FileWriter(filePath))) { writer.write(车牌,车位号,入场时间,出场时间,是否VIP); writer.newLine(); for (CarRecord r : history) { // 出场时间这里用当前时间近似实际应在 exit 时记录 writer.write(String.format(%s,%d,%s,%s,%b, r.plate, r.spotId, r.entryTime, LocalDateTime.now(), r.isVip)); writer.newLine(); } System.out.println(已导出 history.size() 条记录到 filePath); } catch (IOException e) { System.out.println(导出失败 e.getMessage()); } }这里有个不严谨的地方CarRecord里没有存出场时间导出时用LocalDateTime.now()代替所有记录的出场时间都一样。正确的做法是在exit方法里给CarRecord加一个exitTime字段出场时赋值。这个改动很小但能让导出数据真实可用。我一般会在CarRecord里加exitTime初始为 null出场时设置导出时判空输出空字符串。5.4 我踩过的那些坑做这个课程设计的时候我一开始用ArrayList存车位觉得遍历找空位也没什么。结果测试时连续入场 60 辆车每次都要从 0 开始扫代码里嵌套了三层循环自己都看不下去了。后来换成PriorityQueue入场逻辑从 20 行缩到 8 行而且再也不用担心 VIP 插队的问题。另一个坑是计费。我最初用System.currentTimeMillis()算时间差测试时手动改了系统时间结果算出负数费用答辩时被老师问「你这停车场还倒贴钱」后来换成LocalDateTime和Duration并且在计费前加了minutes 0的保护才稳下来。最后一个教训是不要等到全部写完再测试。每写完一个模块就跑一次入场测完测出场出场测完测历史查询。数据结构课程设计的 bug 往往不是逻辑错而是状态不一致——map 和队列里的数据对不上。早测早发现别等到答辩前一天晚上才跑那时候改一个结构可能牵动五个方法。希望帮到你。本文还有配套的精品资源点击获取