简介:一份面向Java课程设计与数据库大作业的智慧公交管理系统项目,基于Java GUI与MySQL 8.0实现,覆盖车辆、员工、线路、站点、排班等核心管理模块,并提供登录和修改密码功能。系统内置管理员、调度员、员工三种角色,不同角色对应独立的用户登录表,随附的sql脚本包含员工表、登录表、排班表、站点表、线路表、车辆表及线路-站点表共七张数据表,可在MySQL Workbench 8.0 CE中直接导入。压缩包共2个文件,zip为完整Idea工程源码,sql为busmanage数据库脚本,整体约10.72MB,已在Win10专业版、JDK 1.8环境下调通运行,配置好依赖后运行Main包中的Login.java即可启动。该项目已有1136人学习,运行入口和环境要求交代得比较清楚,适合希望参考完整Swing项目结构的开发者。借助这份项目可以重点学习Swing界面布局、JDBC数据库操作、多表关联查询等实际开发技巧,作为课程设计或毕业设计参考都很合适。
1. Java GUI 的智慧公交管理系统能解决什么
在公交调度室或校园实训场景里,值班员需要的不是炫酷的大屏,而是一个装好 JDK 就能跑的桌面程序。用 Java GUI 开发的智慧公交管理系统,不依赖 Tomcat、Spring 或前端工程,在 Swing 组件之上把线路表、站点序号、车辆实时位置和发车间隔组织起来,就能完成到站预测、车辆追踪、排班调整这类日常调度工作。这类选题之所以高频出现在课程设计和初级开发者的项目清单里,是因为它能把集合、线程、JDBC、事件监听这四类 Java 高频知识点串成一个完整闭环。下文按技术选型、数据建模、调度逻辑、打包发布四步展开,跟着做就能复现。
2. Java GUI 智慧公交的技术选型与分层骨架
2.1 Swing 还是 JavaFX:课程设计与轻量桌面应用的现实答案
动手写代码之前,先回答一个绕不开的问题:图形界面用哪套类库。同样是 Java GUI,Swing 和 JavaFX 的维护成本完全不一样,选错会导致后面两周的时间都花在配置环境上。
| 对比项 | Swing | JavaFX |
|---|---|---|
| JDK 内置 | 是(JDK 1.2 起) | JDK 8 内置,JDK 11 起独立成模块 |
| 运行时 | 无需额外下载 | javafx-controls 等模块要和 JDK 版本精确匹配 |
| 学习曲线 | 平缓,事件模型老但资料多 | 需另学 FXML、CSS、属性绑定 |
| 打包复杂度 | jar 可直接双击 | 需要 jlink 或插件处理非 JDK 模块 |
| 适合场景 | 内部管理、教学演示、工具软件 | 动画交互复杂、期望做成商业桌面产品 |
从实际维护角度看,Swing 的组件外观虽然朴素,但智慧公交管理系统的主要界面就是表格、下拉框、按钮和状态栏,这些恰好是 Swing 最成熟的领域。JavaFX 学习成本不低,而课程设计或内部工具通常没有动画和酷炫视觉效果的需求,选择 Swing 能把精力留到业务逻辑上。
顺带说一下 AWT。AWT 更底层,提供的组件少,按钮、文本框这些基础控件之外几乎都要自己拼,直接用它做项目会把工作量拖高。Swing 是 AWT 的轻量级替代,既保留事件监听机制,又有 JTable、JTree 这类现成组件,是当前“Java GUI 入门项目”里最不容易翻车的选择。
2.2 四层包结构:把 SQL、调度和界面分开
如果图省事,把线路数据查询、发车模拟、表格刷新全塞进一个 JFrame 类里,项目初期运行没问题,但后面加“智慧”算法时会发现代码越改越乱。比较稳妥的做法是建四个包:model 放实体类,dao 只做数据访问,service 承载调度算法,ui 管界面渲染和事件。
com.example.bus/ ├── model/ # Route、Station、Bus 实体 ├── dao/ # JDBC 数据访问 ├── service/ # 发车调度、到站预测、客流统计 ├── ui/ # 主界面、表格模型、刷新线程 └── Main.java # 程序入口// model/Bus.java public class Bus { private int busId; private int routeId; private int stationIndex; // 当前所在区间:从第 stationIndex 站出发 private double progress; // 本区间完成比例,取值 0.0 到 1.0 private int passengerCount; private final int capacity = 80; public double getRemainingTime(double segmentTime) { return (1 - progress) * segmentTime; } }这个实体把一辆车“在哪个区间、走完了多少、载了多少人”放在内存中。实际排班需要预测到站时间时,调用getRemainingTime()就能算出距离下一站的分钟数,不需要反复查询数据库。这里也体现了 Java 面向对象的基础设计:将行为挂到实体上,而不是在 UI 回调里写一长串逻辑。
分层之后,dao 层里的 JDBC 代码不会出现在actionPerformed按钮回调中,service 层可以独立测试,ui 层只看展示结果。这个习惯在被问到你做过的 Java 项目时,能直接作为封装的例子讲出来。
2.3 主界面最小骨架与事件线程前置设计
主界面用 JFrame 承载,内部放一个 JTabbedPane,分别挂“线路管理”“车辆监视”“排班调整”三个页签。首版只做一个能显示“欢迎”状态栏的窗口,验证运行环境正常,再逐步往里加表格:
// ui/DashboardFrame.java public class DashboardFrame extends JFrame { private final BusService busService; public DashboardFrame(BusService busService) { this.busService = busService; setTitle("智慧公交管理系统"); setSize(1280, 800); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setJMenuBar(createMenuBar()); add(createVehicleTablePanel(), BorderLayout.CENTER); add(createStatusBar(), BorderLayout.SOUTH); } }代码中通过构造器注入BusService,界面不直接 new DAO,这是依赖倒置的简单形式,方便后期把模拟数据源替换成真实数据。窗口尺寸设为 1280×800,表格和状态栏分别放在中间和底部,这种布局在桌面管理系统里最常见。
有一点要在骨架阶段就定好:所有 Swing 组件只能在事件分发线程(EDT)里创建和更新。如果直接new Thread(() -> tableModel.setValueAt(...))去改表格,轻则界面卡死,重则抛ConcurrentModificationException。所以在 ui 包里预留一个refreshView()方法,后面无论是用 Swing Timer 还是独立线程驱动,都统一走这个方法刷新界面。
3. Java GUI 智慧公交的数据模型与线路查询实现
3.1 用站序号和进度描述车辆位置,不碰地图坐标
“智慧公交”听起来很复杂,但如果目标是做调度系统,核心并不需要 GIS 地图坐标。车辆位置用两个字段就能刻画:当前所在站点区间的起点序号,以及在该区间内完成的进度。这样设计不仅省去坐标纠偏,还让“到站预测”变成简单的乘法。
CREATE TABLE route ( route_id INTEGER PRIMARY KEY, route_name TEXT NOT NULL, base_interval INTEGER DEFAULT 10, -- 平峰发车间隔,单位分钟 peak_interval INTEGER DEFAULT 5 -- 高峰发车间隔,单位分钟 ); CREATE TABLE station ( station_id INTEGER PRIMARY KEY, route_id INTEGER NOT NULL, station_order INTEGER NOT NULL, -- 第几站,从 0 开始 station_name TEXT NOT NULL ); CREATE TABLE bus ( bus_id INTEGER PRIMARY KEY, bus_no TEXT NOT NULL, route_id INTEGER NOT NULL, station_index INTEGER DEFAULT 0, -- 刚离开第几站 progress REAL DEFAULT 0.0, -- 到下一站的进度 passenger_count INTEGER DEFAULT 0 );station_order是从 0 递增的序号,配合bus.station_index就能确定车辆在线路中的相对位置。progress是 0 到 1 的小数,模拟线程每走一步就增加一个固定步长,到了 1 就把station_index加一、progress归零。这种数据模型是模拟类系统的常用做法,面试时被问到“车辆位置怎么表示”时,可以顺带解释为什么不用经纬度:调度关心的是到站时间,不是地图渲染。
3.2 JDBC 查询某条线路的全部站点
查询语句用 Java 15 起的文本块写法,多行 SQL 不需要再用+拼接字符串,这本身就是 Java 多行字符串的常见优化经验:
// dao/StationDAO.java public List<String> findStationsByRoute(int routeId) throws SQLException { String sql = """ SELECT station_name FROM station WHERE route_id = ? ORDER BY station_order """; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, routeId); try (ResultSet rs = ps.executeQuery()) { List<String> list = new ArrayList<>(); while (rs.next()) { list.add(rs.getString("station_name")); } return list; } } }routeId作为查询参数通过占位符传入,避免 SQL 注入;try-with-resources保证连接、语句和结果集自动关闭,这在 JDBC 开发里是必须养成的习惯。值得注意的一点是,DBUtil.getConnection()里的连接 URL、用户名、密码不要硬编码在类中,从jdbc.properties读取更通用,换库时只改配置不改代码。
如果使用 SQLite,连接串可以简化为jdbc:sqlite:bus.db,不需要用户名和密码,非常适合单机版管理系统。若换成 MySQL,连接串变成jdbc:mysql://localhost:3306/bus,还要带上useSSL=false&serverTimezone=Asia/Shanghai这类参数,否则新版驱动的时区和 SSL 校验容易报错。
3.3 数据库选型:SQLite 为什么比 MySQL 和纯文件合适
这个系统虽然不是高并发应用,但数据持久化不能省,否则程序重启后线路和车辆全部丢失。三种存储方式的取舍如下:
| 存储方式 | 部署成本 | 查询能力 | 推荐场景 |
|---|---|---|---|
| SQLite | 零部署,单文件 | 支持标准 SQL | 单机 GUI 管理 |
| MySQL | 需要安装服务 | 强,适合多端并发 | Web 或 C/S 多客户端 |
| 纯文件(JSON/CSV) | 最低 | 弱,排序关联要手写 | 原型演示 |
文本文件方案在项目初期写起来最快,但“智慧调度”一上来就要做“查某线路所有站点并按序号排序”这种关联操作,手写解析很快就会失控。SQLite 单文件部署,备份就是把 .db 文件复制走,JDBC 接口又和 MySQL 完全一致,是老项目里最顺手的过渡方案。
4. 智慧调度与界面实时刷新:Java GUI 多线程的实战点
4.1 Swing Timer 模拟车辆移动,避免卡死界面
做动态刷新时,最容易犯的错误是在点击事件里写while (true) { Thread.sleep(1000); ... },这会让界面直接无响应。Swing Timer 是入门首选,它保证回调在事件分发线程执行,适合驱动简单的刷新场景:
// ui/DashboardFrame.java 中的启动方法 Timer refreshTimer = new Timer(1000, e -> { busService.simulateOneStep(); // 更新车辆位置与客流 vehicleTable.repaint(); // 触发表格重绘 statusBar.setText(busService.getSummary()); }); refreshTimer.start();但 Swing Timer 回调里不能做耗时操作,否则仍然会阻塞 UI 线程。对真正的模拟环境,常见做法是用ScheduledExecutorService跑后台 tick,计算完成后再切回 EDT 更新界面:
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); pool.scheduleAtFixedRate(() -> { busService.simulateOneStep(); SwingUtilities.invokeLater(() -> { vehicleTableModel.fireTableDataChanged(); }); }, 0, 1, TimeUnit.SECONDS);这里的核心参数是 1 秒 tick 间隔。后台线程负责推进所有车辆的状态,UI 刷新交给invokeLater排队执行,界面永远不会因为模拟计算变慢而冻结。
| 参数 | 含义 | 建议值 |
|---|---|---|
| tick 周期 | 每次模拟推进的时间片 | 1000 ms |
| progress 步长 | 每 tick 车辆前进比例 | 0.02,对应 50 秒过一站 |
| 线程池大小 | 调度与刷新任务线程数 | 2 |
| 刷新方式 | 直接 repaint 或 tableModel 通知 | fireTableDataChanged |
4.2 智慧调度规则:平均客流超过阈值就加密发车
“智慧”不能只停留在展示实时位置,需要让发车计划随客流动态调整。一个可复现的规则是:当线路平均满载率超过 0.8,把发车间隔缩到基准的 70%;超过 0.6 则缩到 85%;否则保持基准间隔。
public int getAdjustedInterval(Route route) { double loadFactor = route.getAverageLoad(); int base = route.getBaseInterval(); if (loadFactor > 0.8) { return (int) (base * 0.70); } if (loadFactor > 0.6) { return (int) (base * 0.85); } return base; }getAverageLoad()由 service 层计算,遍历某线路上的所有车辆,把passengerCount求和后除以总容量。这里有个实际踩过的坑:如果只按实时客流调整,高峰刚结束时会立刻把发车间隔拉回平峰,导致站台积压乘客迟迟走不完。改进方案是取最近 5 个 tick 的平均客流再决策,并在计算里加一个最低间隔下限,防止出现空车密集发车的浪费现象。
4.3 用 HashMap 做站点名缓存,同时处理并发修改异常
线路名和站点名在运行期基本不变,每次都查数据库没必要。一般会在 service 层启动时加载一次站点列表,用HashMap以“站名 -> 站点对象”缓存起来,查询复杂度降到 O(1)。但这里马上会遇到并发问题:后台模拟线程每秒都会更新bus集合,UI 线程同时遍历集合生成表格数据,会出现ConcurrentModificationException。
解决方式普通但不花哨:遍历前复制快照。
List<Bus> snapshot = new ArrayList<>(busService.getAllBuses()); for (Bus b : snapshot) { // 更新表格行数据 }快照复制的开销是 O(n),公交车辆规模一般在几百辆这个量级,完全可接受。需要注意,不能只用Collections.synchronizedList包住集合并在外面手动遍历,那依然要自己处理同步;快照方式虽然牺牲了一点实时性,却换来代码简单和没有死锁风险。这个点值得在项目总结里拿出来聊聊,因为集合的并发处理本来就是 Java 面试统考的领域。
4.4 用 CountDownLatch 等待初始化完成再显示主窗口
系统启动时,往往需要同时加载线路、站点缓存、车辆初始位置。如果让主窗口先出现再慢慢填充数据,用户可能会在数据未就绪时点按钮触发空指针。用CountDownLatch等三批数据全部加载完成后,再切换到主窗口,逻辑更稳妥。
CountDownLatch latch = new CountDownLatch(3); ExecutorService executor = Executors.newFixedThreadPool(3); executor.submit(() -> { routeService.load(); latch.countDown(); }); executor.submit(() -> { stationCache.init(); latch.countDown(); }); executor.submit(() -> { busService.init(); latch.countDown(); }); boolean done = latch.await(3, TimeUnit.SECONDS); SwingUtilities.invokeLater(() -> showMainWindow(done));await的第二个参数是超时时间。设置 3 秒而不是无限等待的原因是:如果某个加载任务抛异常卡住,程序至少能降级启动并在状态栏提示加载失败,而不是整个黑屏无响应。这比“启动后白屏等数据”的体验好很多,也是用 Java 做桌面管理类项目时经常被忽略的守护逻辑。
5. Java GUI 智慧公交系统的打包发布与高频坑
5.1 jpackage 一键打包本机安装程序
开发完成后,交付给没有 IDE 的同事或老师,最省心的方式是用 JDK 自带的 jpackage 工具打成安装包。JDK 14 起支持该命令,当前使用 JDK 17 或 21 时已经是稳定功能:
jpackage \ --name BusSystem \ --input libs \ --main-jar bus-system.jar \ --main-class com.example.bus.Main \ --type exe--input指向包含第三方依赖 jar 的目录,--main-class指定启动类,--type在 Windows 下填 exe、macOS 可以填 dmg。这个命令会生成一个单独的可执行安装程序,目标机器不装 JDK 也能跑,因为 jpackage 默认会带上运行时镜像。如果只是临时发给别人看一下,也可以直接发可执行 jar,但对方机器上要配好 java 环境变量,这依然是跨机器交付最常见的门槛。
5.2 三个高频坑及排查顺序
第一个坑是ClassNotFoundException。程序在 IDE 里正常,打包后双击就崩溃,几乎都是依赖 jar 没有进入包内。先检查 jar 的MANIFEST.MF是否声明了Class-Path,或者打包命令的--input目录是否覆盖了 mysql/sqlite 驱动。
第二个坑是中文乱码。源码是 UTF-8,但 Windows 命令行编译时默认用了 GBK 编码,运行后界面全是问号。让代码里所有文件统一 UTF-8,并给 Maven 或 javac 加上-encoding UTF-8参数,乱码基本就能解决。
第三个坑是uncaught exception java.lang.NoClassDefFoundError: java/applet/Applet。这是把 JDK 8 老工程往新版本迁移时最典型的报错,Applet API 在 Java 9 已被移除,老代码里的import java.applet.*必须删干净;若项目确实用到小应用程序相关能力,要改成javax.swing.JApplet或使用其他实现。
5.3 用无头模式压测模拟性能
关闭图形界面,写一个简单的 main 方法模拟 600 个 tick,统计耗时,能快速发现调度逻辑性能瓶颈:
long start = System.nanoTime(); for (int i = 0; i < 600; i++) { busService.simulateOneStep(); } long costMs = (System.nanoTime() - start) / 1_000_000; System.out.printf("600 tick 总耗时: %d ms%n", costMs);这个测试在我的机器上,250 辆车、10 条线路的规模跑出约 900ms,平摊到每 tick 只有 1.5ms,符合实时刷新要求。如果耗时异常高,最可能的问题是模拟线程中混入了数据库写操作,每条 SQL 都带来 IO 阻塞。解决技巧是把写库操作改为批量延迟执行:模拟 10 个 tick 后只落库一次,或直接改用 SQLite 事务包裹,性能通常能提升数倍。
本文还有配套的精品资源,点击获取