十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Java Swing的股票交易模拟系统:从订单队列到JDBC事务的完整实践

基于Java Swing的股票交易模拟系统:从订单队列到JDBC事务的完整实践 简介面向数据结构课程设计而整理的Java实战项目实现了一款基于Swing的股票交易模拟系统覆盖行情实时展示、系统管理、账户管理、模拟交易、技术指标与策略分析等完整功能模块适合计算机专业学生用于课程设计、毕业设计或Java Web/桌面应用进阶练习。项目在实现上综合运用数据库、模型-视图-组件设计、JFreeChart图表库、新浪股票实时接口及随机数模拟行情等技术并体现面向对象分析、链表和队列结构、随机数生成、文件读写与程序测试用例设计能够帮助读者理解一个桌面应用从需求分析、系统设计到编码测试的完整流程。压缩包含187个文件以54个Java源码、87个class编译文件为核心另含20个运行依赖jar包、数据库相关配置及文本说明、PPT答辩演示文档和若干界面截图整体大小10.79MB解压后导入开发工具即可查看运行效果与二次开发。目前已有758人学习下载尤其适合正在准备课程设计答辩、需要完整可运行样例的读者。1. 数据结构课程设计里基于 Java Swing 的股票交易模拟系统是最常见题目之一但大多数提交版只把 JTable 当作容器订单排单用顺序扫描答辩时问一句“时间/空间复杂度是多少”就卡壳这个标题的 zip 里通常装着源码、数据库脚本和讲稿 PPT。它的真正价值不是“会用 Swing 画窗口”而是把数据结构里队列、排序、哈希表、事务这些知识点串成一个可运行的场景股票订单要排队撮合成交记录要稳定排序持仓要用键值查询用户资金与订单在数据库里必须一致。适合两类人一类是正在准备“数据库课程设计”或“数据结构课程设计”答辩的在校生另一类是准备 Java 后端面试想用一个真实项目把 Java 基础、集合框架、多线程和 JDBC 绑在一起讲的候选人。下面从订单数据结构、Swing 高频刷新、数据库事务、答辩交付四条线逐层落地每一步都给能在本地跑起来的最小实现。2. 股票交易模拟系统的数据结构选型订单队列、排序与哈希查找2.1 待撮合订单为什么要用 PriorityQueue 而不是数组一个具体的业务场景是多个买家对同一只股票下买单系统要先按“价格优先、时间优先”撮合。最直接的做法是用ArrayList保存所有买单每次插入后Collections.sort排序取最优价时取第一个。插入 O(n)排序 O(n log n)取顶 O(1)。在每日几千条模拟数据里看不出问题但课程设计答辩时市面上经常问到的“数据结构高频核心知识点”就藏在这里堆的主要用途是维护动态集合的最值而买卖订单正是这样一类数据。Java 里的PriorityQueue底层是二叉堆插入和删除堆顶都是 O(log n)取堆顶 O(1)。关键是要自定义比较器因为价格和时间是两个维度买价高者排前价格相同时先到者排前。下面给一个最小实现。import java.math.BigDecimal; import java.time.Instant; import java.util.PriorityQueue; public class OrderBook { public static class Order { String stockCode; BigDecimal price; long time; int quantity; boolean isBuy; Order(String stockCode, BigDecimal price, int quantity, boolean isBuy) { this.stockCode stockCode; this.price price; this.quantity quantity; this.isBuy isBuy; this.time Instant.now().toEpochMilli(); } } PriorityQueueOrder buyQueue new PriorityQueue( (a, b) - { int priceCmp b.price.compareTo(a.price); // 买价高者优先 return priceCmp ! 0 ? priceCmp : Long.compare(a.time, b.time); } ); public void addOrder(Order o) { if (o.isBuy) { buyQueue.offer(o); } // 卖单队列同理价格更低者优先 } }代码逻辑说明PriorityQueue默认是小顶堆买价高者要在比较器里反向比较也就是b.price.compareTo(a.price)这样堆顶始终是当前最高买价。time使用Instant.now().toEpochMilli()这样时间维度是 long 基础类型。测试中如果同一毫秒内插入多笔订单比较器需要再加一个自增序号作为第三关键字否则时间相同时顺序不稳定。价格使用BigDecimal不要用double。股票价格在模拟系统里经常到小数点后三位double 的二进制误差会让排序结果偶尔错位。撮合时先peek本方最优单再peek对方最优单如果价格满足交叉条件就poll剩余数量重新offer回去。整个过程的时间复杂度是 O(log n)。数组与优先队列在订单簿场景下的复杂度对比如下操作数组实现PriorityQueue插入订单O(1) 追加O(n log n) 排序O(log n)取最优价O(n) 扫描O(1)删除最优价O(n) 移动元素O(log n)这个表格可以直接写进课程设计报告的“数据结构与算法”章节。2.1.1 价格精度和数据结构的关联如果用BigDecimal作为HashMap的 key需要注意它的equals和compareTo语义不一致1.0和1.00的equals为 falsecompareTo为 0。哈希表判断 key 是否存在时用的是equals因此直接把 BigDecimal 当 key 可能查不到数据。正确做法是给订单分配一个 long 类型的订单号哈希表 key 使用订单号价格只作为比较字段。2.2 成交记录用稳定排序Collections.sort 与 JDK 的 TimSort成交记录在 JTable 中展示时常见需求是先按时间排序再按股票代码排序。Java 的Collections.sort对对象列表使用 TimSort这是一种稳定排序。稳定性的价值在于如果先把记录按时间排好再按用户分组稳定算法会保留组内的时间顺序。这在多级排序中非常重要。ListTradeRecord trades dao.findTodayTrades(userId); trades.sort(Comparator .comparing(TradeRecord::getTradeTime) .thenComparing(TradeRecord::getStockCode));参数说明getTradeTime最好返回LocalDateTime。如果数据库里存的是VARCHAR格式的2025-01-01 09:30:00需要先解析成LocalDateTime否则字符串字典序会导致10:00排在09:00前面。thenComparing表示第二排序关键字会生成一个组合比较器TimSort 在扫描过程中能识别已有有序片段整体最坏时间复杂度为 O(n log n)。不要在手写快排里浪费时间。快速排序是不稳定排序Java 对对象数组使用 TimSort 正是为了保持相等元素的相对顺序。课程设计里使用 JDK 现成方法就能回答问题不需要自己写一个有边界 bug 风险的递归快排。2.3 持仓查询的哈希表HashMap 扩容与加载因子持仓列表天然适合用HashMapString, Positionkey 是股票代码。股票代码是不可变 StringhashCode 会被缓存适合做 key。HashMap的加载因子默认 0.75扩容时键值对会重新散列。如果后台行情线程和 UI 线程同时读写同一个普通 HashMap可能在扩容时出现链表循环课程设计里最简单的规避方式是把所有对持仓集合的修改收敛到 EDT 线程。如果代码里出现“线程安全的 Map”常见做法是使用ConcurrentHashMap。但要注意它的弱一致性迭代器在遍历时可能看不到最新值不能直接用于界面全量快照。更稳定的一种写法是后台线程把快照写入一个volatile引用UI 线程每次读取引用。private volatile MapString, StockQuote quoteSnapshot Map.of(); public void update(StockQuote q) { MapString, StockQuote copy new HashMap(quoteSnapshot); copy.put(q.code, q); quoteSnapshot Map.copyOf(copy); // 替换引用不修改旧 Map }代码逻辑说明Map.copyOf会创建一个不可变 Map之后任何线程都无法再修改旧快照。volatile只保证引用可见性不保证内部修改可见所以必须每次替换整个 Map。这个思路在面试里比直接回答“加锁”更进一层因为它把同步问题变成了不可变对象替换。能讲明白这一层和死记硬背 java 面试八股里的“HashMap 容量为什么是 2 的幂”是两种效果。3. 用 Java Swing 把高频刷新做稳EDT、JTable 与自绘 K 线Swing 是单线程模型所有组件创建和修改都必须在 EDTEvent Dispatch Thread上执行。后台线程直接调用jLabel.setText()不会立刻崩溃但会出现间歇性刷新错乱、光标卡死甚至抛ConcurrentModificationException。下面按三个具体场景把它做稳。3.1 股票列表用 JTable 而不是动态添加标签股票行情列表是一个滚动表格包含代码、名称、现价、涨跌幅。如果用JPanel手动add几十个 JLabel滚动、选列、排序都要自己处理。正确做法是用JTable配合DefaultTableModel把数据和视图分开。String[] columns {代码, 名称, 现价, 涨幅}; DefaultTableModel model new DefaultTableModel(columns, 0); JTable table new JTable(model); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);不要在main线程调用model.addRow必须通过SwingUtilities.invokeLater回到 EDT。如果行情每 500 毫秒刷新一次刷新间隔不要低于 50 毫秒否则界面会持续重绘CPU 占用上升。涨跌幅列的前景色可以用自定义TableCellRenderer实现正数红色、负数绿色。中国 A 股习惯红涨绿跌和美股相反课程设计按中国习惯写。3.2 价格刷新Swing Timer 或 SwingWorker不要 while(true)sleep很多初版代码会写while(true) { Thread.sleep(1000); updateUI(); }这在生产里十有八九会出问题因为后台线程直接修改界面不是线程安全。常见做法是javax.swing.Timer定时回调或者SwingWorker负责拉数据、done()方法回 EDT 更新界面。Timer timer new Timer(1000, e - { MapString, StockQuote snapshot quoteService.getSnapshot(); model.setRowCount(0); for (StockQuote q : snapshot.values()) { model.addRow(new Object[]{q.code, q.name, q.price, q.changePercent()}); } }); timer.start();代码逻辑说明Timer的回调在 EDT 执行所以model.setRowCount和addRow是安全的。quoteService.getSnapshot()不要在这里连接数据库否则 EDT 阻塞超过几百毫秒窗口拖拽会卡顿。行情量大时addRow频繁触发表格模型事件。可以改为每次刷新构建一个Object[][]一次性替换表格模型减少监听器调用次数。三种刷新方案的对比方案UI 更新位置可中断性适用场景javax.swing.TimerEDT 回调stop()可停固定频率轮询轻量SwingWorkerdone()回 EDTcancel()可中断耗时较长的行情拉取while(true)sleep需invokeLater包装难控制不推荐作为项目主逻辑3.3 K 线图自绘JPanel 的 paintComponent 与有界队列Swing 没有内置 K 线图与其引入不熟悉的第三方库不如用JPanel自绘。历史 K 线数据可以使用ArrayDeque作为双端队列只保留最近 100 根。class Candle { double open, high, low, close; } class ChartPanel extends JPanel { DequeCandle candles new ArrayDeque(); void addCandle(Candle c) { if (candles.size() 100) candles.pollFirst(); // 淘汰最旧 candles.addLast(c); repaint(); } Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (candles.isEmpty()) return; int n candles.size(); int slotW getWidth() / n; double maxHigh candles.stream().mapToDouble(c - c.high).max().orElse(1); double minLow candles.stream().mapToDouble(c - c.low).min().orElse(0); // 按 maxHigh/minLow 换算 Y 坐标画实体矩形和上下影线 } }代码逻辑说明ArrayDeque头尾操作都是 O(1)很适合“永远只保留最近 N 条数据”的场景。paintComponent里不能查询数据库或做复杂 IO否则缩放窗口时每帧都可能卡顿。换算坐标时先求最高价和最低价再映射到 panel 高度避免超出边界。实体矩形由开盘价和收盘价决定影线由最高价和最低价决定。答辩时可以说这里的双端队列在图形系统里等价于一个环形缓冲体现的是数据结构对“只保留近期数据”的控制能力。4. 数据库设计与 JDBC 事务源码里的用户资金和订单必须一致数据库在这个项目里的职责是持久化或者说让系统重启后仍然能恢复用户、持仓、订单和成交记录。很多课程设计只建一张t_user表评委一问“订单存在哪里”就答不上来。一个最小可用的系统至少要四张表用户、股票、订单、成交流水。4.1 核心表结构与建表语句订单表需要保存买卖方向、价格、数量和状态成交流水保存一笔订单撮合后的结果。下面给出核心建表 SQL。CREATE TABLE t_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password_hash VARCHAR(64) NOT NULL, available_cash DECIMAL(12,2) NOT NULL DEFAULT 100000.00 ); CREATE TABLE t_stock ( stock_code VARCHAR(10) PRIMARY KEY, stock_name VARCHAR(30) NOT NULL, last_price DECIMAL(10,3) NOT NULL ); CREATE TABLE t_order ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, stock_code VARCHAR(10) NOT NULL, side TINYINT NOT NULL COMMENT 1买 2卖, order_type TINYINT NOT NULL COMMENT 1限价 2市价, price DECIMAL(10,3), quantity INT NOT NULL, status TINYINT NOT NULL COMMENT 0待撮合 1成交 2撤单, create_time DATETIME NOT NULL, KEY idx_user_time (user_id, create_time), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES t_user(user_id) );t_order关键字段说明字段类型说明order_idBIGINT订单主键user_idINT外键关联 t_userstock_codeVARCHAR(10)股票代码sideTINYINT1 买入2 卖出order_typeTINYINT1 限价2 市价priceDECIMAL(10,3)限价单价格quantityINT股数statusTINYINT0 待成交1 成交2 撤单create_timeDATETIME下单时间建表参数解释金额和价格都用DECIMAL不用FLOAT/DOUBLE这是数据库课程设计里最常被指出的问题。status用数字配合常量类或枚举使用避免字符串导致索引膨胀。(user_id, create_time)联合索引覆盖“某用户按时间倒序查订单”的页面场景。schema.sql要放在项目 resources 目录下演示时重建数据库只需要执行一个脚本而不是翻 PPT 复制字段。4.2 JDBC 连接参数与连接池单用户系统用DriverManager.getConnection也能跑但价格刷新、下单、持仓查询如果各自开连接连续操作几十次后 MySQL 可能报too many connections。用一个连接池是更常规的做法。HikariCP 本身可以脱离 Spring 单独使用。HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/stock_sim?useSSLfalseserverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(3000); DataSource ds new HikariDataSource(config);连接参数说明maximumPoolSize设置为 10足够课程设计。不要用默认的几十否则本机 MySQL 的max_connections可能不够。connectionTimeout3000表示拿不到连接时最多等 3 秒点击下单后不会无限制阻塞。URL 里的serverTimezoneAsia/Shanghai是 MySQL 8 驱动的必填项否则报时区错误。中文乱码的话在 URL 后加characterEncodingutf8。如果不想引入外部 MySQL可以用 H2/SQLite 替代。但 SQLite 写锁严格高频模拟撮合时容易报database is locked建议优先 MySQL。4.3 下单事务订单、资金、持仓需要在同一事务里提交下单的核心是两步扣减用户可用资金、插入订单记录。如果先插入订单再扣钱扣款失败时订单仍然存在就会出现“订单成交但现金没扣”的脏数据。正确做法是关闭自动提交统一 commit 或 rollback。try (Connection conn ds.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement( UPDATE t_user SET available_cash available_cash - ? WHERE user_id ? AND available_cash ?)) { ps.setBigDecimal(1, orderPrice); ps.setInt(2, userId); ps.setBigDecimal(3, orderPrice); if (ps.executeUpdate() 0) { throw new SQLException(可用资金不足); } } try (PreparedStatement ps conn.prepareStatement( INSERT INTO t_order (user_id, stock_code, side, order_type, price, quantity, status, create_time) VALUES (?,?,?,?,?,?,?,?))) { ps.setInt(1, userId); // 剩余字段按界面参数填充 ps.executeUpdate(); } conn.commit(); } catch (SQLException e) { e.printStackTrace(); // 这里应该记录日志并在 catch 中调用 conn.rollback() }事务代码说明setAutoCommit(false)必须在拿到连接后第一行执行。UPDATE ... WHERE available_cash ?把“余额判断扣款”合并成一条原子 SQL比“先 select 再 update”安全。如果资金不足executeUpdate返回 0此时抛异常事务不会提交。真正的源码里应在 catch 块内调用conn.rollback()再抛出自定义业务异常让 UI 弹出提示。这一节其实就是“数据库增删改查”之上最重要的能力事务边界与数据完整性。答辩时讲清楚这一点整个项目深度会明显提升。5. 答辩与交付把源码、数据库、PPT 变成可演示的闭环5.1 启动顺序的最小可复现清单拿到一个包含源码数据库PPT 的 zip先按固定顺序启动建库、执行 schema.sql、启动主类。不要把环境搭建写到答辩现场才查。推荐用 IntelliJ IDEA 导入 Maven 工程运行Main类。注意三个容易翻车的位置数据库脚本字符集用CREATE DATABASE stock_sim DEFAULT CHARACTER SET utf8mb4;避免中文乱码。端口被占用如果 3306 上同时跑着旧版 MySQL 或本地服务先确认连接参数。JDBC 驱动必须通过pom.xml或项目 lib 目录带到 classpath不要只把 jar 放在桌面。5.2 五步演示路线从行情刷新到数据库回放演示顺序建议固定为打开行情列表看到价格每秒跳动借此讲清楚后台行情快照和 EDT 更新。对某只股票下单买入确认后立即看到可用现金减少。查看持仓列表和成交记录说明成交记录来自撮合结果。切到数据库客户端执行SELECT * FROM t_order WHERE user_id1;让评委看到界面数据和库里数据一致。重启程序再登录确认上次的订单和资金还在这一步直接回答“为什么需要数据库”。5.3 PPT 与技术答辩的加分细节PPT 不需要贴大段代码但建议保留三页系统模块图、下单到成交的时序图、E-R 图。答辩时把PriorityQueue的堆排序比较器、Swing Timer 的 EDT 模型、JDBC 事务的扣款与回滚讲清楚比罗列十个菜单更有说服力。演示前把连接池参数调小或者准备一个数据量很小的测试账户避免现场数据库连接超时。如果运行时出现命令行中文乱码在 JVM 参数中加-Dfile.encodingUTF-8。这些细节不会出现在 zip 解压后的第一页但决定了源码、数据库和 PPT 能不能真正合成一个可演示的完整项目。本文还有配套的精品资源点击获取
返回列表