资讯动态

基于Java Swing的股票交易模拟系统:数据结构课程设计完整实践

发布时间:2026/10/9 10:38:51 来源:尧图企业网站定制
简介这是一份基于Java Swing的股票交易模拟系统课程设计资源面向计算机专业学生及Java开发者适合课程作业、毕业设计或学习图形界面开发与数据库综合应用。系统实现了行情展示、模拟交易、系统管理、账户管理、技术指标与策略分析等功能采用随机数模拟行情数据并结合JFreeChart绘制走势图。资源包共187个文件约10.79MB包含Java源文件、class编译文件、jar依赖库、png/jpg界面图片以及PPT和说明文档目录结构清晰便于分模块查阅。目前已有758人学习/下载。下载后可获得完整源码与数据库覆盖数据库连接、K线图绘制、盘口面板、自选股、买卖面板等核心模块同时体现链表、队列应用文件读写与测试用例设计有助于掌握从界面交互到业务逻辑的完整开发流程。1. 数据结构课程设计选它股票交易模拟系统为什么比图书管理更值得做如果你是计算机专业的学生大概率正在为“数据结构课程设计”选题目发愁。图书管理、学生信息管理这类老掉牙的题目答辩时老师连听都不想听而纯算法演示又不像一个“系统”。这次给的标题很明确基于Java swing的股票交易模拟系统附带源码、数据库和PPT。它把数据结构里的线性表、队列、栈、排序、甚至图论的思想全部塞进了一个看得见摸得着的桌面应用里既能展示编程能力又能让答辩评委觉得“这东西有实际价值”。股票交易的撮合、行情排序、持仓汇总、历史回放每一个功能背后都对应着一个明确的数据结构考点。更关键的是它用Java swing做界面不需要搭建Web服务器本地就能跑对课程设计来说成本低、演示效果直观。接下来我按照自己做过类似项目的思路把这个系统的拆解、建模、编码和踩坑过程完整讲一遍你照着做就能交付一份站得住脚的课程设计。2. 先定数据结构再写界面订单簿、成交队列与持仓表怎么落到Java代码2.1 股票交易的核心场景与数据结构映射股票交易模拟系统要模拟的是这样一件事多个用户提交买入或卖出委托系统按照价格优先、时间优先的规则撮合成交。这里最经典的数据结构是队列和顺序表。买入委托和卖出委托分别存成两个队列按照到达时间排好当新委托进入时拿队首的委托来匹配。如果你追求更真实的价格优先撮合那就得用优先队列PriorityQueue按照价格高低排序买价高的优先卖价低的优先。PriorityQueue底层是堆这正好是数据结构课程里“堆排序”的活教材。持仓数据适合用顺序表ArrayList存每行代表一个用户持有的一只股票用户ID、股票代码、持仓数量、可用数量、成本价。每次成交后更新这张表增删查改都围绕它进行。历史成交记录适合用栈来存因为回放“最近一笔成交”时栈顶拿数据就是O(1)的。如果你想让系统更直观地展示数据结构可以专门写一个交易日志类内部维护一个栈每次push一条成交记录提供peek和pop方法。答辩时可以现场演示连续成交几笔后弹出最后一笔评委一看就明白栈的用途。2.2 别为了用图而用图但股票关联关系可以这样讲很多同学为了展示“图”这个知识点硬把股票之间的涨跌关联设计成有向图。我的建议是不要这么做因为课程设计不是算法炫技而是让数据结构为业务服务。不过如果你真想用图有一个场景是合理的把“自选股”之间的关系做成无向图顶点是股票边代表“同时被用户关注”这样可以用图的邻接表结构来存储自选股列表。但更务实的做法是把图的知识用在K线走势的最短路径上——其实用不上。所以我不建议在核心功能里强行加图除非你的题目要求必须体现每一种数据结构。我一般会在系统的“股票池管理”模块用一个**哈希表HashMap**来维护股票代码和股票对象的映射。这样用户输入“600519”能立刻得到对应的股票对象时间复杂度O(1)比遍历ArrayList快得多。这个点很好写进报告哈希表作为索引结构在股票数量大到几千只时性能优势明显。2.3 订单匹配的核心代码用队列实现“时间优先价格辅助”下面是一段可以直接放进你项目里的订单匹配核心逻辑。这段代码我用Queue接口实现买卖委托队列撮合时先检查价格是否满足条件再决定是否成交。为了简化这里只做“按价格区间撮合”实际完整系统还需要处理部分成交。import java.util.LinkedList; import java.util.Queue; public class OrderMatcher { // 买盘队列按时间顺序存放买入委托 private QueueOrder buyQueue new LinkedList(); // 卖盘队列按时间顺序存放卖出委托 private QueueOrder sellQueue new LinkedList(); public void addOrder(Order order) { if (order.getType() Order.BUY) { buyQueue.offer(order); // 入队 } else { sellQueue.offer(order); } match(); // 新订单到达后立即尝试撮合 } private void match() { while (!buyQueue.isEmpty() !sellQueue.isEmpty()) { Order buy buyQueue.peek(); // 只看队首不弹出 Order sell sellQueue.peek(); if (buy.getPrice() sell.getPrice()) { // 撮合成交数量取两者较小值 int tradeQty Math.min(buy.getRemainQty(), sell.getRemainQty()); System.out.println(成交: buy.getStockCode() 价格: sell.getPrice() 数量: tradeQty); // 更新剩余数量 buy.decreaseQty(tradeQty); sell.decreaseQty(tradeQty); if (buy.getRemainQty() 0) buyQueue.poll(); // 出队 if (sell.getRemainQty() 0) sellQueue.poll(); } else { // 价格不匹配等待后续订单 break; } } } }这段代码的核心是peek()和poll()的配合。peek()只查看队首元素而不删除适合先判断是否满足成交条件一旦确认成交再用poll()弹出已经完成的订单。这样既遵循了队列FIFO的语义又避免了错误地提前出队。offer()方法在队列满时返回falseadd()会抛异常在无界队列场景下两者都可以但用offer()更安全。你可以把Order类设计为包含价格、类型、股票代码、剩余数量等字段。如果要改成价格优先只需要把LinkedList换成PriorityQueue并在Order里实现compareTo方法买价高的排前面卖价低的排前面。这个改动不用动撮合逻辑正好体现了“面向接口编程”的好处。3. Swing界面与线程模型让JTable实时刷新行情而不卡死3.1 用观察者模式解耦行情数据与界面组件很多课程设计项目把数据查询、业务计算全写在ActionListener里界面稍有动作就卡顿。这个系统我建议分三层实体层Stock, Order, Account, TradeRecord、服务层StockService, OrderService, AccountService、界面层MainFrame, LoginDialog, MarketPanel。服务层负责操作数据结构界面层只负责显示和接收事件。为了让行情变化自动通知界面可以在服务层维护一个listenerList手动实现观察者模式当股票价格发生变动时调用所有注册的监听器。import javax.swing.*; import javax.swing.table.*; import java.util.ArrayList; import java.util.List; public class StockDataModel extends AbstractTableModel { private final String[] columns {代码, 名称, 现价, 涨跌幅}; private ListStock stockList new ArrayList(); Override public int getRowCount() { return stockList.size(); } Override public int getColumnCount() { return columns.length; } Override public Object getValueAt(int rowIndex, int columnIndex) { Stock s stockList.get(rowIndex); switch (columnIndex) { case 0: return s.getCode(); case 1: return s.getName(); case 2: return s.getPrice(); case 3: return s.getChangePercent(); default: return null; } } Override public String getColumnName(int column) { return columns[column]; } public void updateStock(Stock stock) { for (int i 0; i stockList.size(); i) { if (stockList.get(i).getCode().equals(stock.getCode())) { stockList.set(i, stock); fireTableRowsUpdated(i, i); // 通知JTable刷新 return; } } stockList.add(stock); fireTableRowsInserted(stockList.size() - 1, stockList.size() - 1); } }这个自定义的AbstractTableModel是整个界面刷新的关键。很多人直接用DefaultTableModel然后setValueAt但那样每次刷新都要重新构建Vector数据量大时非常慢。自定义模型的好处是你可以在getValueAt里只返回需要显示的字段刷新时调用fireTableRowsUpdated只是通知表重绘那一行。注意fireTableRowsUpdated必须在**事件分发线程EDT**中调用否则界面不会立即更新。后续我会讲怎么用SwingWorker避免这个问题。3.2 行情刷新的正确姿势Swing Timer vs 多线程模拟行情需要每隔几秒生成一个新的价格然后刷新表格。最简单的做法是用javax.swing.Timer它在EDT里执行回调不需要手动同步。import javax.swing.*; import java.util.Random; public class MarketTicker { private Timer timer; private final StockDataModel model; private final StockService stockService; public MarketTicker(StockDataModel model, StockService stockService) { this.model model; this.stockService stockService; } public void start() { timer new Timer(2000, e - { // 每2秒刷新一次 Random rand new Random(); for (Stock s : stockService.getAllStocks()) { double delta (rand.nextDouble() - 0.5) * 2; // 模拟涨跌 s.setPrice(Math.round((s.getPrice() delta) * 100.0) / 100.0); model.updateStock(s); } }); timer.start(); } public void stop() { if (timer ! null) { timer.stop(); } } }这里有个坑java.util.Timer和Swing Timer是不同的东西。如果你用java.util.Timer在后台线程里更新Swing组件会触发线程安全问题轻则闪烁重则卡死。所以Swing界面里的定时任务一律用Swing Timer。另外如果数据库查询耗时较长不要在Timer回调里直接查库因为EDT会被阻塞界面就会“假死”。正确做法是用SwingWorkerVoid, Stock把查询放到后台线程然后在done()或process()里更新界面。3.3 用SwingWorker做后台加载避免启动卡顿程序启动时要初始化股票列表如果直接用JFrame的构造函数里同步查库窗口要等好几秒才能显示。我会这样处理import javax.swing.*; import java.util.List; public class InitWorker extends SwingWorkerListStock, Stock { private final StockDataModel model; private final StockService service; public InitWorker(StockDataModel model, StockService service) { this.model model; this.service service; } Override protected ListStock doInBackground() throws Exception { // 模拟耗时加载 Thread.sleep(1500); return service.getAllStocks(); } Override protected void process(ListStock chunks) { // 可以在这里分批次更新model } Override protected void done() { try { ListStock stocks get(); for (Stock s : stocks) { model.updateStock(s); } } catch (Exception e) { JOptionPane.showMessageDialog(null, 初始化失败: e.getMessage()); } } }doInBackground运行在后台线程process和done在EDT线程执行。这里没有直接操作Swing组件所以不会违反Swing的单线程规则。注意done()里要用get()获取结果不要直接在doInBackground里返回之前就去更新界面。这样设计后窗口能立刻显示数据加载完成后表格自动填充。这个模式在你的课程设计报告里写“使用SwingWorker优化用户体验”很加分。4. 持久化层设计数据库表结构、JDBC连接与课程设计报告的组织4.1 五张核心表用户、股票、持仓、委托、成交记录课程设计附带的数据库文件通常是一个SQL脚本。我会设计五张表user用户、stock股票基本信息、position持仓、order_list委托单、trade_record成交记录。字段设计如下表所示表名关键字段用途userid, username, password, account_balance模拟账户余额stockcode, name, current_price, history_data股票池和行情positionuser_id, stock_code, quantity, available_qty, cost_price每个用户对每只股票的持仓order_listorder_id, user_id, stock_code, type, price, quantity, status, create_time买卖委托队列的持久化trade_recordtrade_id, buy_order_id, sell_order_id, price, quantity, trade_time成交历史order_list表很重要它对应我们内存里的QueueOrder。如果程序重启需要把未成交的委托从数据库恢复出来按create_time排序插入队列。这就是为什么Order对象里必须保存order_id和create_time。position表可以用联合主键(user_id, stock_code) 来保证一个用户对同一只股票只有一条持仓记录更新时用INSERT ... ON DUPLICATE KEY UPDATEMySQL或INSERT OR REPLACESQLite。4.2 JDBC连接与DAO层封装PreparedStatement必须用数据库操作如果直接散落在界面层后续改库、加字段会非常痛苦。我习惯封装一个BaseDao提供通用的增删改查方法。连接参数放到db.properties里避免硬编码。示例代码如下import java.sql.*; import java.util.Properties; import java.io.InputStream; public class DBUtil { private static String url; private static String user; private static String password; static { try (InputStream in DBUtil.class.getResourceAsStream(/db.properties)) { Properties props new Properties(); props.load(in); url props.getProperty(db.url); user props.getProperty(db.user); password props.getProperty(db.password); Class.forName(props.getProperty(db.driver)); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (Exception ignored) {} try { if (ps ! null) ps.close(); } catch (Exception ignored) {} try { if (conn ! null) conn.close(); } catch (Exception ignored) {} } }注意Class.forName在JDBC 4.0以后可以省略但写上更兼容。数据库驱动Jar包必须放到lib目录并Build Path。在DAO层所有SQL语句一律使用PreparedStatement不仅能防止SQL注入而且处理字符串里的引号时不会出错。比如用户输入的股票名称平安银行用字符串拼接就容易出问题。import java.sql.*; public class UserDao { public User findUserByUsername(String username) { String sql SELECT * FROM user WHERE username ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setPassword(rs.getString(password)); u.setBalance(rs.getDouble(account_balance)); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }这里的try-with-resources非常关键它可以自动关闭Connection、PreparedStatement、ResultSet你不需要手动写finally。很多同学的代码里连接泄漏就是因为在finally里没关干净。还有一点数据库里的user表名在MySQL中不算保留字但在某些版本中可能与系统表冲突建议用t_user作为表名省得换环境时报错。4.3 课程设计PPT怎么组织从问题陈述到数据结构分析标题里的PPT是要交给老师的课程设计答辩材料。我见过太多PPT从“Java简介”开始讲浪费前五页。正确的结构应该是第一页题目和成员第二页需求概述放系统功能模块图可以用文字代替图避免插入失败第三页数据结构分析这是重点分别讲顺序表存股票池、队列存委托、栈存成交记录、哈希表做快速查找、排序算法用于行情排序第四页关键算法——撮合算法给出伪代码或流程图第五页数据库设计放ER图第六页系统运行截图和测试用例第七页总结和展望。每一页都要回答“数据结构在哪里”。答辩时老师问“为什么用队列不用数组”你要能说出队列先进先出的语义与时间优先规则天然一致而数组的删除操作是O(n)队列出队是O(1)。这个回答是整门课程的核心考点。5. 避坑指南数据结构课程设计里容易翻车的6个细节5.1 现象JTable数据不刷新或者界面直接卡死原因在ActionListener里同步访问数据库或者model.updateStock()方法没有在EDT中执行。 解决所有耗时操作放到SwingWorker的doInBackground中模型更新操作统一在done或process中调用。我自己的项目中曾遇到点击“买入”按钮后窗口白屏就是因为executeQuery查成交记录耗时2秒阻塞了EDT。换成SwingWorker后问题消失。5.2 现象订单撮合结果总是差一分钱成交数量和预期不符原因浮点数直接比较buy.getPrice() sell.getPrice()而价格是double类型精度误差导致边界判断错误。 解决价格统一用BigDecimal存储和比较或者把价格乘以100转成整数“分”再比较。以下是我常用的改法public static boolean canMatch(double buyPrice, double sellPrice) { int buyFen (int) Math.round(buyPrice * 100); int sellFen (int) Math.round(sellPrice * 100); return buyFen sellFen; }5.3 现象程序启动时内存占用持续上涨GC无法回收原因股票历史K线数据用递归加载或者HashMap的Key是可变对象。如果你把Stock对象直接当HashMap的Key而Stock的price字段每次行情刷新都变那么hashCode会变导致内存泄漏。 解决使用不可变字段——股票代码作为HashMap的Key。同时历史数据遍历使用迭代器不要在for-each循环里删除元素。课程设计里的历史K线数据量不大直接一次性加载到ArrayList即可。5.4 现象Swing窗口中文显示成乱码按钮上的汉字变成方块原因Java源文件的编码与JVM运行时编码不一致。比如源文件是UTF-8但编译时用了GBK编码或者数据库连接URL没有指定characterEncodingutf8。 解决编译器统一用javac -encoding UTF-8IDE里设置项目文件编码为UTF-8。MySQL连接串加上?useUnicodetruecharacterEncodingutf8。另外在db.properties里加载的字符串也要注意Properties类默认使用ISO-8859-1编码中文内容需要转成Unicode转义符或者直接用资源文件。5.5 现象连不上数据库报ClassNotFoundException或Access denied原因驱动Jar包没有添加到Build Path或者数据库密码错误又或者你连接的是MySQL 8但driver配置还写着com.mysql.jdbc.Driver。 解决MySQL 8要把驱动类改成com.mysql.cj.jdbc.DriverURL中也建议加上serverTimezoneAsia/Shanghai否则时区报错。这是初次运行的项目最容易碰到的坑。5.6 现象答辩时老师说“你这个和数据结构有什么关系就是CRUD”原因项目里所有数据都用ArrayList存取没有呈现任何数据结构设计思想。 解决在项目里显式实现至少两个数据结构类。比如自定义一个SortedArrayList插入时按价格二分查找位置这就是综合了顺序表和折半查找再用ArrayDeque实现OrderQueue封装入队出队方法。答辩时拿出这两个类明确说“我把数据结构封装成了服务类的底层存储”评分立刻不一样。6. 答辩前这样验收构造测试用例把数据结构讲给评委听课程设计交付前我建议你把系统当成真实软件来做一轮功能测试。准备三组数据一组正常数据买入价高于卖出价产生成交一组边界数据恰好价格相等、数量刚好全部成交、用户余额刚好足够一组异常数据买入价格低于当前最优卖价委托进入等待队列卖出数量超过持仓数量重复登录。每组数据都记录输入、预期输出、实际输出写进报告。这样评委问“你怎么验证系统正确”你就能直接亮出测试表。针对数据结构展示你可以设计一个“场景演示”脚本第一步打开委托队列可视化面板依次提交五笔买单观察队列的入队顺序第二步提交一笔卖单价格低于队伍中最高买价观察队首的买单被弹出并成交第三步连续弹出历史成交记录演示栈的后进先出。如果能在界面上用两个JList组件实时显示队列内容效果会非常直观。我自己的习惯是在代码里保留一个DebugRunner入口运行它可以在控制台打印队列操作日志比如“buy queue after add: [60131810.2, 6005191500.0]”。答辩时现场运行这个入口比空口解释白得多。这不算炫技而是让老师快速看到数据结构的真实运转。最后提醒一句不要试图把真实股票数据接口接入系统课程设计模拟环境用固定种子随机生成就够了。做好上面这些哪怕界面朴素一点分数也不会低。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑