简介:这份资源是面向Java初学者与数据结构课程设计学习者的停车场管理系统完整项目,采用Java语言结合Swing或JavaFX构建图形界面,解决车辆存取、车位容量限制与候车区调度等实际问题。压缩包共15个文件,包含6个java源码与9个class编译文件,整体约17KB,涵盖自定义链表队列MyListQueue、栈MyStack、链表节点ListNode、车辆信息类CarInfo以及数据管理类Data等核心模块,UI内部类负责按钮事件监听。系统实现存车取车逻辑:车位空闲时直接存入,满载则转入候车区队列,取车时按信息查找处理,并配有异常提示机制。目前已有2640人学习下载,适合需要参考完整课设方案、理解队列与栈在真实场景中应用、梳理类结构与事件监听写法的读者,可据此快速搭建并运行项目,深化对Java与数据结构的综合运用能力。
1. 停车场管理系统:一个把栈、队列和哈希表串起来的 Java 课程设计
如果你正在做数据结构课程设计,又不想再写一遍学生成绩管理或者图书管理,停车场管理系统是个很值得动手的选题。它表面上是个业务系统,骨子里却是一道综合数据结构题:车辆进场要排队、出场要计费、车位要分配、历史记录要能查。每一个动作背后都对应一个经典结构,选错了结构,代码能跑但逻辑会拧巴。
这份 Java 课程设计资源围绕一个核心场景展开:一个多层停车场,入口排队、车位分配、出场结算、记录查询。它适合两类人——正在赶课程设计、需要一份能跑通且讲得清原理的参考实现;以及想用一个小项目把栈、队列、哈希表、排序算法串起来复习的 Java 学习者。下面我从结构选型讲到代码落地,再到实际跑起来会遇到的坑,一步步拆开。
2. 结构选型:为什么入口用队列、车位用哈希、出场用栈
2.1 入口排队为什么必须是队列而不是 List
车辆到达入口时,如果车位已满,需要等待。这个等待序列有一个硬性约束:先到的车先进入。这就是队列的典型场景,FIFO。用 ArrayList 也能实现,但每次从头部移除元素都要移动后续所有元素,时间复杂度 O(n)。车辆一多,这个开销会累积。
Java 里实现队列有两种常见选择:LinkedList 和 ArrayDeque。LinkedList 实现了 Queue 接口,入队出队都是 O(1),但每个节点有额外的指针开销。ArrayDeque 底层是循环数组,内存更紧凑,大多数场景下性能更好。我一般会选 ArrayDeque,除非需要频繁在中间插入。
import java.util.ArrayDeque; import java.util.Queue; public class EntranceQueue { // 用 ArrayDeque 实现等待队列,FIFO private final Queue<Car> waitingQueue = new ArrayDeque<>(); private final int maxWaitSize; public EntranceQueue(int maxWaitSize) { this.maxWaitSize = maxWaitSize; } // 车辆加入等待队列,返回是否成功 public boolean enqueue(Car car) { if (waitingQueue.size() >= maxWaitSize) { return false; // 等待区已满,拒绝进入 } return waitingQueue.offer(car); } // 车位空出时,从队首取一辆车 public Car dequeue() { return waitingQueue.poll(); // 队列为空返回 null } public boolean isEmpty() { return waitingQueue.isEmpty(); } public int size() { return waitingQueue.size(); } }这段代码里,offer和poll是队列的标准操作,前者在容量允许时入队,后者从队首取出并移除。maxWaitSize控制等待区上限,防止无限排队。实际停车场入口通常只能容纳几辆车等待,这个参数一般设 3 到 5。
2.2 车位分配用哈希表:O(1) 查找空位
停车场有若干层,每层有固定车位。车辆进场时需要快速找到一个空位。如果用数组遍历,每次都要从头扫到尾,车位多了效率很低。哈希表可以把「车位编号」映射到「车辆信息」,查找、插入、删除都是 O(1)。
但这里有个细节:哈希表存的是「已占用」的车位,找空位时需要反向查找。常见做法是维护一个空闲车位编号的队列或栈,或者用一个布尔数组加指针。我倾向于用 HashMap 存占用信息,同时用一个 TreeSet 维护空闲编号,这样找最小可用编号很快。
import java.util.HashMap; import java.util.Map; import java.util.TreeSet; public class ParkingLot { private final int totalSpots; // key: 车位编号, value: 当前停放的车辆 private final Map<Integer, Car> occupied = new HashMap<>(); // 空闲车位编号,自动排序,方便分配最小号 private final TreeSet<Integer> freeSpots = new TreeSet<>(); public ParkingLot(int totalSpots) { this.totalSpots = totalSpots; for (int i = 1; i <= totalSpots; i++) { freeSpots.add(i); } } // 分配一个空车位,返回车位编号,无空位返回 -1 public int allocate(Car car) { if (freeSpots.isEmpty()) { return -1; } int spotId = freeSpots.pollFirst(); // 取最小空闲编号 occupied.put(spotId, car); return spotId; } // 释放车位 public Car release(int spotId) { Car car = occupied.remove(spotId); if (car != null) { freeSpots.add(spotId); } return car; } public boolean hasFreeSpot() { return !freeSpots.isEmpty(); } }TreeSet的pollFirst保证每次分配的是当前最小空闲编号,这样车位使用看起来整齐,也方便人工核对。HashMap的remove返回被移除的车辆对象,直接用于后续计费。这两个结构配合,进场和出场的核心操作都是 O(log n) 和 O(1),足够应对课程设计的规模。
2.3 出场计费用栈:后进先出模拟通道约束
有些停车场设计是单通道,车辆只能从最外侧依次驶出。如果里面停的车要出来,外面的车必须先挪走。这个场景用栈来模拟最自然:最后进入通道的车最先离开。
但实际业务里,出场计费并不需要严格栈序,因为每辆车有独立车位。栈在这里更多用于「临时通道」的模拟,或者用于撤销操作。课程设计里常见的做法是:出场时把通道内的车依次弹出,计费后再压回。这个操作能体现栈的特性,也能让代码有东西可讲。
import java.util.ArrayDeque; import java.util.Deque; public class ExitChannel { // 用 Deque 当栈,push/pop 都在头部 private final Deque<Car> channel = new ArrayDeque<>(); public void push(Car car) { channel.push(car); } public Car pop() { return channel.pop(); } public boolean isEmpty() { return channel.isEmpty(); } // 让指定车牌的车出场:先弹出挡路的车,再压回 public Car releaseCar(String plate) { Deque<Car> temp = new ArrayDeque<>(); Car target = null; while (!channel.isEmpty()) { Car c = channel.pop(); if (c.getPlate().equals(plate)) { target = c; break; } temp.push(c); } // 把临时移出的车压回 while (!temp.isEmpty()) { channel.push(temp.pop()); } return target; } }releaseCar方法演示了栈的典型操作:为了取出中间某辆车,先把上面的车临时移到另一个栈,取完再移回来。这个逻辑在课程设计答辩时很能说明问题,面试里也常被问到。
3. 从零跑起来:环境、建表和核心业务代码
3.1 环境准备与项目结构
这份课程设计是纯 Java 项目,不依赖 Spring 或数据库也能跑。如果你想让数据持久化,可以加 MySQL,但核心逻辑用内存结构就能演示完整。我建议先用控制台版本跑通,再考虑加界面。
JDK 版本用 8 或 11 都行,代码里没有用高版本特性。IDE 用 IntelliJ IDEA 或者 Eclipse 都可以,新建一个 Java 项目,包名按com.parking组织。主要类包括:
| 类名 | 职责 |
|---|---|
| Car | 车辆实体,含车牌、进场时间 |
| ParkingLot | 车位管理,分配与释放 |
| EntranceQueue | 入口等待队列 |
| ExitChannel | 出场通道栈 |
| BillingService | 计费逻辑 |
| Main | 控制台入口,菜单驱动 |
如果你要加数据库,建表语句大概是这样:
CREATE TABLE parking_record ( id INT PRIMARY KEY AUTO_INCREMENT, plate VARCHAR(20) NOT NULL, spot_id INT NOT NULL, enter_time DATETIME NOT NULL, exit_time DATETIME, fee DECIMAL(10,2), status TINYINT DEFAULT 0 -- 0 在场, 1 已出场 );这个表结构把每次停车记录成一行,进场时插入,出场时更新。status字段区分在场和已出场,查询历史记录时按exit_time排序即可。
3.2 计费逻辑:按小时还是按次
计费规则直接影响代码结构。常见的有两种:按次收费,每次固定金额;按小时收费,不足一小时按一小时算。课程设计里按小时更常见,因为能体现时间计算。
import java.time.Duration; import java.time.LocalDateTime; public class BillingService { private static final double RATE_PER_HOUR = 5.0; // 每小时 5 元 private static final double MAX_DAILY = 40.0; // 单日上限 public double calculate(Car car, LocalDateTime exitTime) { LocalDateTime enterTime = car.getEnterTime(); long minutes = Duration.between(enterTime, exitTime).toMinutes(); // 不足一小时按一小时算 long hours = (minutes + 59) / 60; double fee = hours * RATE_PER_HOUR; return Math.min(fee, MAX_DAILY); } }Duration.between计算两个时间点之间的间隔,toMinutes得到分钟数。(minutes + 59) / 60是向上取整的常用写法,避免用 Math.ceil 带来的浮点问题。Math.min限制单日最高收费,防止长时间停车费用过高。
3.3 主流程串联:进场、分配、出场、计费
把前面几个类串起来,主流程就是:车辆到达 → 判断有无空位 → 有空位直接分配,无空位进等待队列 → 出场时释放车位 → 检查等待队列 → 计费。
public class ParkingSystem { private final ParkingLot lot; private final EntranceQueue queue; private final BillingService billing; public ParkingSystem(int spots, int maxWait) { this.lot = new ParkingLot(spots); this.queue = new EntranceQueue(maxWait); this.billing = new BillingService(); } // 车辆进场 public String enter(Car car) { if (lot.hasFreeSpot()) { int spotId = lot.allocate(car); return "分配车位: " + spotId; } else if (queue.enqueue(car)) { return "车位已满,进入等待队列,位置: " + queue.size(); } else { return "等待区已满,请稍后再来"; } } // 车辆出场 public String exit(String plate, LocalDateTime exitTime) { // 遍历已占用车位找到该车牌 Integer targetSpot = null; Car targetCar = null; for (Map.Entry<Integer, Car> entry : lot.getOccupied().entrySet()) { if (entry.getValue().getPlate().equals(plate)) { targetSpot = entry.getKey(); targetCar = entry.getValue(); break; } } if (targetCar == null) { return "未找到该车辆"; } double fee = billing.calculate(targetCar, exitTime); lot.release(targetSpot); // 有空位后,从等待队列取一辆车 if (!queue.isEmpty()) { Car waiting = queue.dequeue(); int newSpot = lot.allocate(waiting); // 这里可以记录日志或通知 } return "车牌: " + plate + ", 费用: " + fee + " 元"; } }enter方法先判断空位,再判断等待队列,逻辑清晰。exit方法遍历occupied找到车牌对应的车位,这里用entrySet遍历是 O(n),如果车辆多可以再加一个车牌到车位的索引HashMap<String, Integer>来优化到 O(1)。课程设计规模下 O(n) 足够,但答辩时能说出优化方向是加分项。
4. 避坑与排查:课程设计里最容易翻车的五个点
4.1 车牌号作为 HashMap 的 key 却忘了重写 hashCode 和 equals
现象:用HashMap<String, Car>存车辆,按车牌查不到,或者两个相同车牌被当成不同 key。
原因:如果自定义了 Car 类并用 Car 对象作为 key,没有重写hashCode和equals,HashMap 会用对象地址比较,永远不相等。
解决:用 String 类型的车牌作为 key,String 已经正确实现了这两个方法。如果非要用 Car 对象做 key,必须重写hashCode和equals,且保证车牌相同的两个 Car 返回相同哈希值和 true。
4.2 时间计算用了 System.currentTimeMillis 导致精度丢失
现象:停车 1 小时 1 分钟,计费按 1 小时算,用户觉得少收了;或者停车 59 分钟,按 2 小时算,用户觉得多收了。
原因:System.currentTimeMillis()返回毫秒,手动除 3600000 取整容易在边界上出错。
解决:用java.time.LocalDateTime和Duration,先算总分钟数,再用(minutes + 59) / 60向上取整。这样 1 分钟也算 1 小时,59 分钟也算 1 小时,逻辑一致。
4.3 等待队列没有容量上限导致内存溢出
现象:程序跑一段时间后卡死或者抛 OutOfMemoryError。
原因:入口等待队列用了LinkedList且没有限制大小,车辆不断加入但没人消费。
解决:给队列设maxWaitSize,入队前检查size() >= maxWaitSize,满了就拒绝并提示。实际停车场入口也不可能无限排队。
4.4 出场时忘记从等待队列补车
现象:一辆车出场后,车位空着,但等待队列里的车一直不动。
原因:exit方法只做了释放车位和计费,没有检查等待队列。
解决:在release之后加判断if (!queue.isEmpty()),取出队首车辆并调用allocate。这个逻辑要放在同一个方法里,保证原子性。
4.5 控制台输入没有做异常处理导致程序崩溃
现象:输入车位数量时输入了字母,程序直接抛NumberFormatException退出。
原因:Scanner.nextInt()遇到非数字输入会抛异常,没有 try-catch。
解决:用Scanner.nextLine()读字符串,再用Integer.parseInt并捕获异常,提示用户重新输入。或者用hasNextInt()先判断。
Scanner scanner = new Scanner(System.in); int spots = 0; while (true) { System.out.print("请输入车位数: "); String line = scanner.nextLine(); try { spots = Integer.parseInt(line); if (spots > 0) break; System.out.println("必须大于 0"); } catch (NumberFormatException e) { System.out.println("请输入数字"); } }这段代码把输入校验和异常处理放在循环里,直到用户输入合法值才继续。课程设计答辩时,这种细节能体现工程思维。
5. 进阶技巧:用排序算法做停车记录统计与验证
课程设计如果只做到增删改查,深度不够。数据结构课通常要求体现排序和查找算法,停车场管理系统里最适合加统计功能:按停车时长排序、按费用排序、查找特定时间段内的记录。
我一般会在BillingService里加一个方法,接收List<ParkingRecord>,用归并排序按费用从高到低排。为什么不用Collections.sort?因为课程设计要展示你理解排序原理。归并排序稳定且时间复杂度 O(n log n),适合记录数量大的场景。
import java.util.ArrayList; import java.util.List; public class RecordSorter { // 归并排序,按费用降序 public static List<ParkingRecord> sortByFeeDesc(List<ParkingRecord> records) { if (records.size() <= 1) { return records; } int mid = records.size() / 2; List<ParkingRecord> left = sortByFeeDesc(new ArrayList<>(records.subList(0, mid))); List<ParkingRecord> right = sortByFeeDesc(new ArrayList<>(records.subList(mid, records.size()))); return merge(left, right); } private static List<ParkingRecord> merge(List<ParkingRecord> left, List<ParkingRecord> right) { List<ParkingRecord> result = new ArrayList<>(); int i = 0, j = 0; while (i < left.size() && j < right.size()) { if (left.get(i).getFee() >= right.get(j).getFee()) { result.add(left.get(i++)); } else { result.add(right.get(j++)); } } while (i < left.size()) result.add(left.get(i++)); while (j < right.size()) result.add(right.get(j++)); return result; } }sortByFeeDesc递归拆分列表,merge按费用降序合并。>=保证稳定性,费用相同的记录保持原有顺序。这个实现可以直接替换Collections.sort,在答辩时说明为什么选归并而不是快排——归并稳定,快排最坏 O(n²)。
验证方法也简单:造一批测试数据,费用分别是 10、30、20、50、40,排序后应该是 50、40、30、20、10。再测边界:空列表、单元素、全部相同费用。跑通这三个用例,基本能说明排序逻辑没问题。
还有一个实用技巧:用HashMap统计每个车位的使用次数,找出最繁忙的车位。这个功能用哈希表做计数,遍历一次记录即可,时间复杂度 O(n)。代码很短,但能体现你对哈希表的灵活运用。
public Map<Integer, Integer> spotUsage(List<ParkingRecord> records) { Map<Integer, Integer> count = new HashMap<>(); for (ParkingRecord r : records) { count.merge(r.getSpotId(), 1, Integer::sum); } return count; }merge方法是 Java 8 引入的,key 存在时用Integer::sum累加,不存在时设为 1。一行代码完成计数,比手动判断简洁得多。
从那以后我每次做课程设计,都会先把核心数据结构的操作复杂度写在一张纸上,再动手写代码。选型错了,后面怎么优化都是白费。这个停车场管理系统看起来简单,但把队列、栈、哈希表、排序都串了一遍,认真做下来对数据结构的理解会上一个台阶。希望帮到你。
本文还有配套的精品资源,点击获取