Java GUI智慧公交系统开发:Swing界面、JDBC数据与多线程调度实战
简介一份面向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 GUISwing 和 JavaFX 的维护成本完全不一样选错会导致后面两周的时间都花在配置环境上。对比项SwingJavaFXJDK 内置是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 ListString 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()) { ListString 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还要带上useSSLfalseserverTimezoneAsia/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 msprogress 步长每 tick 车辆前进比例0.02对应 50 秒过一站线程池大小调度与刷新任务线程数2刷新方式直接 repaint 或 tableModel 通知fireTableDataChanged4.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。解决方式普通但不花哨遍历前复制快照。ListBus 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 事务包裹性能通常能提升数倍。本文还有配套的精品资源点击获取