☰
Java Swing智慧公交管理系统实战:从SQLite到BFS换乘算法
2026/10/14 15:45:18 网站建设 项目流程

简介:一份面向 Java 初学者的智慧公交管理系统完整源码包,基于 Java GUI 与 MySQL 8.0 实现,涵盖车辆、员工、线路、站点、排班等核心业务模块,并提供管理员、调度员、员工三类角色的独立登录与权限管理。系统附带 busmanage.sql 数据库脚本,建好了员工表、登录表、排班表、站点表等共 7 张数据表,配合 mysql-connector-java-8.0.29.jar 驱动,在 IDEA 2021.3.3 与 JDK 1.8 环境下配置后可直接运行。资源共 2 个文件,包含 1 个项目压缩包和 1 个数据库脚本,总大小约 10.72MB,适合用于课程设计、数据库大作业或 Java GUI 开发实战参考。目前已有 1137 人学习,压缩包目录结构清晰,可帮助理解登录鉴权、多表关联查询与界面事件绑定等关键实现,是快速上手完整桌面管理系统的实用素材。

1. 智慧公交管理系统:为什么Java GUI仍是课程设计最稳的选型

每年到了课程设计和毕业设计季,总有人纠结同样的选题。智慧公交管理系统被拿出来讨论的次数最多,理由也最直白:这是典型的窗口应用,需求看得见摸得着,能查线路、管车辆、做换乘查询,业务逻辑不深不浅,Java GUI 正好落在能力圈内。我的观点可能和很多人反过来——这个项目的难点从来不在 Swing 拖几个控件,而在数据结构和线程模型。你把这层想清楚,界面只是时间问题。

选择 Java Swing 不是在开倒车。对于这类管理系统,Swing 的成熟度、资料密度和部署方便程度,真实项目里反而比炫技方案更省心。适合谁做?适合还没接触过框架、想用一门语言完整走一遍“设计表结构—写业务逻辑—做界面”全过程的人。本文不按教程腔从头讲到尾,只讲我按这个标题做下来认为最值得抄的路径,以及那些会让你通宵查问题的坑。

2. 搭骨架:Swing + SQLite 的分层架构与数据持久化

2.1 Swing 还是 JavaFX:选型依据一张表说清楚

每次有人问界面用 Swing 还是 JavaFX,我都先问一句:你的运行环境是不是要带着 JDK 到处跑?如果答案是“是”,Swing 基本没有对手。做个管理系统,最关键的不是控件多炫,而是别人拿过去能跑起来。

对比项SwingJavaFX
JDK 自带程度从 JDK 1.2 起就在,开箱即用JDK 8 内嵌,JDK 11 起需单独引入模块
学习资料密度课程设计、毕业设计案例最多偏企业级,中文案例略少
表格能力JTable 成熟稳定,够用TableView 功能更强但有学习曲线
部署复杂度java -jar 直接运行需要处理模块化参数

我一般直接选 Swing,不为别的,JTable 配合 TableModel 做数据台账,省下来的时间去写换乘算法,性价比高得多。JavaFX 的优势在动画和样式,但公交管理系统的消耗大头是数据表格和表单,Swing 的表现足够稳定。

2.2 包结构与三层架构:从 model 到 dao 到 view

架构上不需要过度设计,但这套分层必须在一开始就立住,否则后期改需求会非常痛苦。我的习惯是把工程分成五个包,分别放实体、数据访问、业务逻辑、窗口界面和工具类。常见做法是:

com.xxx.bus ├── MainApp.java # 入口,只负责启动 ├── model/ # 实体类:BusLine, Station, Vehicle ├── dao/ # JDBC 数据访问,每个类对应一张表 ├── service/ # 业务层:换乘算法、排班判断 ├── view/ # Swing 窗口:主窗体、各种面板 └── util/ # 数据库连接、常量配置

入口类看起来简单,但有一个细节容易忽略:Swing 界面必须在事件分发线程上创建,直接写在 main 里跑会埋下偶发问题。规范写法是这样:

public class MainApp { public static void main(String[] args) { // Swing 的单线程模型要求界面创建放在 EDT 上 SwingUtilities.invokeLater(() -> { MainFrame frame = new MainFrame(); frame.setVisible(true); }); } }

这段代码里 invokeLater 的作用是把界面初始化任务丢给事件分发线程串行执行。这样做的原因是 Swing 的组件绝大多数不是线程安全的,如果在主线程直接 new 窗口,遇到后面加定时器刷新时,界面重绘和状态更新就可能打架。刚开始不写这行也能跑,但学会写它是做所有 Swing 项目的第一课。

2.3 SQLite 还是 MySQL:连接代码与配置要点

数据库选型上,我倾向 SQLite。它把整个库封装在一个文件里,交作业时直接连同 .db 文件一起给,不需要评审现场装数据库服务,省掉大量环境问题。MySQL 适合多人协作或数据量大的场景,但单机版智慧公交管理系统用不到那个量级。

SQLite 的 JDBC 驱动配置特别简单,把 sqlite-jdbc 的 jar 放进 lib 目录后,连接代码只有一行:

Connection conn = DriverManager.getConnection("jdbc:sqlite:db/bus_system.db");

连接串里的 db/bus_system.db 是数据库文件路径。第一次运行如果文件不存在,驱动会自动创建空的库文件,不需要手动建库。这点对新手很友好,但也带来一个坑:如果程序跑起来发现表不存在,不是连接失败,而是没人执行建表脚本。我把建表语句放在初始化类里,程序启动时自动执行:

CREATE TABLE IF NOT EXISTS bus_line ( id INTEGER PRIMARY KEY AUTOINCREMENT, line_no TEXT NOT NULL, start_station TEXT NOT NULL, end_station TEXT NOT NULL, interval_min INTEGER DEFAULT 10 );

CREATE TABLE IF NOT EXISTS 这种写法可以反复执行,不会报错。注意 SQLite 的整数主键用 INTEGER PRIMARY KEY 就能自增,不需要额外写 AUTOINCREMENT,除非你有特殊需求。interval_min 表示发车间隔,这个字段后面做排班和准点率统计都要用,一开始就要建上。

2.4 数据访问层:DAO 的标准写法与参数说明

做数据访问层时,最常见的翻车是每查一次就打开一个连接,用完不关,最后 SQLite 文件锁死。我的做法是:整个程序启动时创建一个连接,通过工具类集中管理,数据库操作都在这个连接上执行。为了演示完整写法,这里给出线路表的数据访问代码:

public class LineDao { private final Connection conn; // 构造时传入连接,避免每次查询都重新建立连接 public LineDao(Connection conn) { this.conn = conn; } public List<BusLine> findAll() throws SQLException { List<BusLine> list = new ArrayList<>(); String sql = "SELECT id, line_no, start_station, end_station, interval_min FROM bus_line"; try (PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { BusLine line = new BusLine(); line.setId(rs.getInt("id")); line.setLineNo(rs.getString("line_no")); line.setStartStation(rs.getString("start_station")); line.setEndStation(rs.getString("end_station")); line.setIntervalMin(rs.getInt("interval_min")); list.add(line); } } return list; } }

这里有两个关键点需要说明。第一,PreparedStatement 必须放在 try 括号里,这是 JDK 7 的 try-with-resources 语法,它保证无论查询是否抛异常,PreparedStatement 和 ResultSet 都会被自动关闭,不会占住数据库句柄。第二,传入的 Connection 是外部共享的,所以 DAO 内部不能关闭它,只关自己的 Statement 和 ResultSet,这个边界要分清楚。

同理,新增线路和修改线路的写法也是同一个套路,只是 SQL 换成 INSERT 和 UPDATE,参数通过 ps.setString 和 ps.setInt 绑定。参数绑定不要用字符串拼接,哪怕现在数据全是你自己录入的,也要养成绑定的习惯,后面接外部数据时能少一类注入问题。

3. 把界面做出来:主窗体布局、表格定制与查询联动

3.1 主窗体布局与功能导航:一屏装下所有子模块

智慧公交管理系统的功能一般有线路管理、站点管理、车辆信息、换乘查询、数据统计这几块。界面结构我用的是左侧导航加右侧内容区的经典布局,右侧用 CardLayout 切换不同功能页,这样切页时不会重建组件,数据状态能保留,体验也稳定。

布局代码核心部分如下:

public class MainFrame extends JFrame { private CardLayout cardLayout = new CardLayout(); private JPanel contentPanel = new JPanel(cardLayout); private LinePanel linePanel; private VehiclePanel vehiclePanel; public MainFrame() { setTitle("智慧公交管理系统"); setSize(1000, 650); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); linePanel = new LinePanel(); vehiclePanel = new VehiclePanel(); contentPanel.add(linePanel, "line"); contentPanel.add(vehiclePanel, "vehicle"); // 左侧按钮触发切页 JButton lineBtn = new JButton("线路管理"); lineBtn.addActionListener(e -> cardLayout.show(contentPanel, "line")); // vehicleBtn 同理 } }

每个功能面板都是一个独立的 JPanel 子类,比如 LinePanel 内部自己完成表格、表单按钮和事件处理,代码隔离清晰。CardLayout 的好处是切换只是显示和隐藏,面板里的表格数据不会因为切页而丢失,回到页面不需要重新查询数据库。

3.2 用 JTable + TableModel 做可增删改的线路台账

表格是这套系统的核心交互区。JTable 的使用有一个重要认知:不要直接把二维数组丢给 JTable 完事,要自定义 AbstractTableModel。这样做的直接收益是,后续刷新数据只需更新 Model 再触发一个事件,表格自动重绘,而不是整表销毁重建。

public class LineTableModel extends AbstractTableModel { private final String[] columns = {"线路编号", "起点站", "终点站", "发车间隔(分钟)"}; private final List<BusLine> lines = new ArrayList<>(); public void setLines(List<BusLine> newLines) { lines.clear(); lines.addAll(newLines); // 通知表格整个数据范围都变了,触发重绘 fireTableDataChanged(); } public void addLine(BusLine line) { lines.add(line); // 只通知插入了新行,避免整个表格闪烁 fireTableRowsInserted(lines.size() - 1, lines.size() - 1); } @Override public int getRowCount() { return lines.size(); } @Override public int getColumnCount() { return columns.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { BusLine line = lines.get(rowIndex); switch (columnIndex) { case 0: return line.getLineNo(); case 1: return line.getStartStation(); case 2: return line.getEndStation(); case 3: return line.getIntervalMin(); default: return null; } } }

setLines 方法用 fireTableDataChanged 做整体刷新,适合初始化或条件搜索后重新灌数据;addLine 用 fireTableRowsInserted 只通知新增一行,适合表单提交后局部刷新。这两者的区别在数据量大时会直接体现为界面卡顿和闪烁,属于必须掌握的细节。

JTable 本身的配置也有讲究。至少要关掉列拖拽,否则用户随手一拖就把表头顺序打乱了:

table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.getTableHeader().setReorderingAllowed(false); table.getColumnModel().getColumn(0).setPreferredWidth(80); table.getColumnModel().getColumn(1).setPreferredWidth(200);

selectionMode 设为单选,避免用户按住 Ctrl 选中多行导致删除操作目标不确定。列宽设置里,线路编号给窄值,站点给宽值,这属于纯视觉调节,但做不做对评委的第一观感影响很大。

3.3 搜索筛选与表格刷新的联动处理

搜索功能如果实现得糙,最常见的毛病是每次按键都去数据库查一遍,导致输入框打字都卡。我一般把搜索做成两种模式:一是查询按钮触发精确搜索,二是按回车或防抖延迟触发实时过滤。课程设计阶段不用做太复杂,做一个“线路编号模糊搜索”加一个“起点站精确匹配”就够覆盖演示需求。

做法是在查询按钮的事件里重新走一遍 DAO 查询,然后把结果灌进 TableModel:

searchBtn.addActionListener(e -> { String keyword = searchField.getText().trim(); // 注意参数传递使用 PreparedStatement 绑定,不做字符串拼接 List<BusLine> result = lineService.searchLines(keyword); tableModel.setLines(result); });

searchLines 在 Service 层内部根据传入关键字拼 SQL 的 WHERE 条件,用 LIKE 做模糊匹配。参数说一下:LIKE 的匹配模式用 keyword + "%",代表前缀匹配,也可以用 "%" + keyword + "%" 做包含匹配,但包含匹配在数据量大时无法走索引,这个表规模无所谓,养成前缀匹配的习惯没有坏处。

刷新时机要注意一点:如果用户在表格里删了一行,然后立即点搜索,界面上的数据要重新同步。常见做法是删除成功后统一调用 refreshTable 方法,内部逻辑就是重新查一次当前列表并 setLines。不要试图去手动删表格行,那会让数据源和界面状态分叉,后续操作各种对不上。

4. “智慧”在哪里:线路建模、换乘算法与车辆实时模拟

4.1 数据模型:线路-站点-车辆的三张核心表关系

管理系统做得像不像样,首先要看数据库表设计。公交领域的核心关系是:一条线路经过多个站点,一个站点能有多条线路经过,一辆车归属于某条线路。这中间必须用关联表把多对多关系拆开,否则换乘查询写不出来。

我设计的核心表有这几张,外加一张线路站点关联表:

表名作用关键字段
bus_line线路基本信息和发车参数line_no, start_station, end_station, interval_min
bus_station站点信息station_name, lng, lat
bus_line_station线路与站点的关联line_id, station_id, seq
bus_vehicle车辆信息vehicle_no, line_id, status

bus_line_station 里的 seq 字段是精髓,它表示站点在线路上的顺序,从 1 开始递增。有了这个字段,两个站点是否在同一线路上、中间隔几站,直接查这张表排序即可,不需要在代码里做复杂推导。车辆表的 status 字段我用整数表示,0 表示停运、1 表示运营中、2 表示维修,用整数而不是字符串,是为了后续做统计时能直接 sum 和 count。

4.2 换乘推荐:无权图求最少换乘的 BFS 实现

换乘查询是“智慧”二字的验收点。这里有一个容易想复杂的点:如果只要求“换乘次数最少”,问题等价于在无权图上求最少边数,用 BFS 就能解决,不需要上 Dijkstra。只有在计算“最短距离”时才需要带权重的算法。

我的建图思路是把每个站点当作节点,把同一条线路上的相邻站点用边连起来。这样一次换乘的路径,在图上就是经过两个节点之间的若干条边。用 BFS 从起点站扩散,一旦到达终点站,路径就是换乘次数最少的方案。

public List<TransferPath> findMinTransfer(String startStation, String endStation) { // BFS 队列,节点为站点ID Queue<Integer> queue = new LinkedList<>(); // prevStation 记录每个站点是从哪个站点过来的 Map<Integer, Integer> prevStation = new HashMap<>(); // prevLine 记录每条转移用的线路,用于后续描述换乘方案 Map<Integer, Integer> prevLine = new HashMap<>(); Set<Integer> visited = new HashSet<>(); int startId = stationDao.findIdByName(startStation); int endId = stationDao.findIdByName(endStation); queue.add(startId); visited.add(startId); while (!queue.isEmpty()) { int current = queue.poll(); if (current == endId) break; List<Link> links = routeDao.findLinksByStation(current); for (Link link : links) { if (!visited.contains(link.getTargetStationId())) { visited.add(link.getTargetStationId()); prevStation.put(link.getTargetStationId(), current); prevLine.put(link.getTargetStationId(), link.getLineId()); queue.add(link.getTargetStationId()); } } } // 回溯 prevStation 得到完整路径 return buildPath(startId, endId, prevStation, prevLine); }

findLinksByStation 返回当前站点能通过哪条线路到达的下一个站点。visited 集合是 BFS 的关键,它保证每个站点只在第一次被发现时被处理,避免回头路和死循环。由于公交站点的图可能成环,不加 visited 程序会直接进入无限循环,这是很多同学第一次写 BFS 时踩的坑。

buildPath 从终点站一路按 prevStation 回溯到起点,再把这段路径按换乘点切开,得到“几号线→在某站换乘→几号线”的文本描述。这个回溯过程是 BFS 的固定套路,本质上是用空间换时间,存下每个节点的前驱,最后倒着找回完整路径。

这里提醒一个和直觉有关的问题:BFS 保证换乘次数最少,但不保证总站数最少。用户可能觉得“明明有更近的路线”,因为近似路线换了三次但站数少。真实系统中一般把换乘次数作为第一优先级,总站数作为第二排序条件,在结果展示里标注“换乘 1 次,共 12 站”,逻辑就站得住。

4.3 车辆位置模拟:定时器驱动的“伪实时”数据流

“实时监控车辆位置”听起来像是要接 GPS 和大屏可视化,但在单机课程设计里,完全可以用定时器驱动模拟数据流来实现。这个方案的原理很简单:车辆表里存当前所在站点序号,定时器每隔几秒把在线车辆的位置字段向后推进一个站点,界面同时刷新表格里的“当前站点”列,视觉上就有了车辆在线路上移动的效果。

Timer timer = new Timer(3000, e -> { // 从 service 层取得所有运营中的车辆 List<Vehicle> vehicles = vehicleService.getRunningVehicles(); for (Vehicle v : vehicles) { int currentSeq = v.getCurrentSeq(); int nextSeq = currentSeq + 1; if (nextSeq > v.getTotalSeq()) { nextSeq = 1; // 到达终点后从起点重新开始,模拟循环线路 } vehicleService.updateCurrentSeq(v.getId(), nextSeq); } // 刷新界面表格,只发射数据变更事件 refreshVehicleTable(); }); timer.start();

javax.swing.Timer 的回调是放在事件分发线程上的,所以在这个回调里直接操作表格组件是安全的,不需要再做 SwingUtilities.invokeLater 包装。触发间隔 3000 毫秒是个可以调的参数,写 2000 会感觉车跑得快,写 5000 更适合演示时逐站讲清楚。注意 updateCurrentSeq 不要每辆车单独提交事务,可以攒一批再统一更新,减少数据库写入压力。

这个模拟方案的真实价值在于,它把表格从“一次性查询结果”变成了“持续变化的数据源”,这才配得上“智慧”两个字。数据量大了之后,可以考虑把 Timer 的刷新目标和表格的 ToolTip 结合,让鼠标悬停到车辆编号时显示“下一站预计到达时间”,实现成本极低但演示效果提升明显。

5. Java GUI 高频踩坑与排查:从假死到乱码的现场记录

5.1 界面假死:为什么 UI 线程不能跑数据库查询

现象:点击“加载全部线路”按钮后,窗口立刻变成无响应状态,标题栏显示“正在响应”,过几秒才恢复,数据量大时直接像死机。

原因:按钮的事件回调运行在事件分发线程上,也就是 EDT。EDT 做的事越少,界面越流畅。如果你在回调里查询数据库,要么是查询慢,要么是 SQLite 在写数据时被占锁,EDT 被卡住就停止绘制和响应鼠标事件,看起来就是假死。

解决:把耗时操作挪出 EDT。SwingWorker 是最标准的做法,doInBackground 里执行查询,done 里操作界面组件更新表格。界面操作的代码永远不要出现在 doInBackground 和任何后台线程里,这是 Swing 并发的第一铁律。

5.2 中文乱码:编译期到运行期一整条链路排查

现象:控制台输出正常,但窗口界面上所有中文都变成“锟斤拷”或问号,数据库里读出来的中文也显示乱码。

原因:编码问题是一条链路,任何一环断了都会乱码。先查源文件编码,再查编译编码,然后查 JDBC 连接串是否声明 UTF-8,最后查数据库表字符集。SQLite 不存在字符集问题,所以这个项目里乱码几乎都出在前两环。

解决:IDE 里把工程文件编码统一为 UTF-8,如果用命令行编译,javac 加上 -encoding UTF-8。Main 方法里可以强制设置 JVM 默认字符集,但这属于兜底手段,根子上还是要统一源文件编码。我建议在代码里尽量避免用 new String(bytes, "GBK") 这种硬编码转换,让数据从入库到展示一直保持 UTF-8。

5.3 表格刷新闪烁:整表重建与增量更新的差别

现象:搜索或删除操作后,表格区域明显发白闪一下,操作频繁时视觉上像屏幕在跳动,观感很差。

原因:代码里用了 table.setModel(new DefaultTableModel(...)) 或者 setRowCount(0) 后重新 addRow,这等于把整个表格销毁重建,重绘工作量变大,闪烁就出现了。默认表格模型在每次 addRow 时都会触发结构事件,多次连续 addRow 会产生大量重绘请求。

解决:改用自定义 AbstractTableModel,并且用带精确位置的更新方法。整体数据变化时用 fireTableDataChanged,单独加一行用 fireTableRowsInserted。只改某一行的某个单元格时用 fireTableCellUpdated,尽量避免无差别的整表通知。真需要整表刷新的场景,先把界面重绘挂起,数据更新完再恢复。

5.4 SQLite 数据库被锁:连接关闭的“后悔药”与预防

现象:程序运行一段时间后,执行某个新增或更新操作时抛异常提示 database is locked,重启程序又恢复正常,过一阵再次出现。

原因:SQLite 是文件型数据库,并发写同一文件时会对文件加锁。应用里如果创建了多个连接,一个连接打开了写事务没提交,另一个连接去写就被阻塞。最常见的情况是 DAO 里每查一次构建新连接,查完只关 ResultSet 没关 Connection,连接对象被垃圾回收时事务状态不明确,锁就释放不掉。

解决:单机应用只保留一个全局连接,所有 DAO 共享,写操作做成短事务,执行完立刻 commit。Swing 的定时刷新如果用后台线程写数据,要在写入方法上加锁,或者统一回到 EDT 上执行写库。这条经验算是血泪教训,排查时最隐蔽,因为报错信息不是每次必现,只在时间凑巧时冒出来。

5.5 换乘结果与直觉反着:BFS 返回了但用户不认可

现象:换乘查询能跑通,但结果是一条很绕的路径,比如明明在站台对面就能换乘,算法给的方案却要多走一站。

原因:换乘次数最少不等于步行距离最少或总站数最少。BFS 以“经过的相邻站点边数”为代价,两个站点若在相同的两条线上,BFS 可能给出上车后多坐几站再换乘的结果,而人眼直觉会选“这站下车原地等另一条线”。算法数学上正确,但不符合直觉。

解决:在图建模阶段把“同站换乘”定义为代价 0 的边,也就是把同一个站点在不同线路上的节点视为连通,这样 BFS 在代价相同情况下就能返回换乘点更合理的路径。如果还有余力,在 BFS 的结果集中做二次排序,把总站点数最少的结果排在最前面,展示时附上换乘点和预计站数,让用户有确认依据。

6. 升级技巧:用 SwingWorker 把静态 CRUD 变成可控的实时监控台

这一章不写新功能,讲一个把现有系统从“能交差”提升到“能演示”的技巧:把界面启动时的数据加载和定时刷新统一放进 SwingWorker,让工程真正具备后台任务的正确处理范式。

先看核心用法。程序启动进入主界面时,往往要加载线路、车辆、站点三类数据,串行查询要一两秒。把这个过程挪到后台线程,界面先显示“数据加载中”,加载完成自动切入内容页面,用户感知会好很多:

SwingWorker<Void, Void> loader = new SwingWorker<>() { @Override protected Void doInBackground() { // 后台线程执行,不能触碰任何界面组件 allLines = lineDao.findAll(); allStations = stationDao.findAll(); allVehicles = vehicleDao.findAll(); return null; } @Override protected void done() { // 回到 EDT,可以安全更新组件 lineTableModel.setLines(allLines); stationTableModel.setStations(allStations); vehicleTableModel.setVehicles(allVehicles); cardLayout.show(contentPanel, "line"); } }; loader.execute();

doInBackground 里的任务是全局唯一的连接,所以不存在并发问题。done 里代码都在 EDT 上执行,不需要再包 invokeLater。注意不要在 doInBackground 里调用任何 setText、setModel、fireTableXxx 之类操作,那会触发跨线程访问组件的异常,表现为控制台刷一堆异常但界面有时正常有时错乱。

接下来是第二个习惯:定时器也要用 SwingWorker 包一层吗?分情况。如果定时任务只是内存里计算再刷新表格,直接在 Timer 回调里做即可,因为 Timer 回调本身就在 EDT。如果任务要查数据库,哪怕 SQLite 很快,也应先由后台线程查出结果再交给 EDT 更新表格。最稳妥的模式是混合使用:SwingWorker 管加载,Timer 管周期触发,两者都遵循“后台取数据,EDT 更新视图”的规则。

最后一个扎实的习惯是统一封装刷新方法。我一般会在每个功能面板内部写一个私有方法 refreshAll,它负责从 service 层拿最新数据、更新 TableModel 并处理异常。界面所有入口都调这个统一方法,包括搜索按钮、删除成功后的回调、Timer 的 tick,这样逻辑只有一份,排查问题时只要看一个方法就够了。

我之前就是因为加载逻辑散落在按钮事件和构造器里,导致启动时表格空白、点搜索才出现数据,调了半小时才意识到是初始化时序问题。统一后这个问题再没复现过。这套做法你不一定原样照搬,但“耗时操作不进 EDT、数据刷新只走一个方法”这两条能守住,项目稳定性和演示效果都不会差。希望帮到你。

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

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

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

立即咨询