☰
基于Java Swing的股票交易模拟系统:数据结构课程设计完整实践
2026/10/9 10:38:38 网站建设 项目流程

简介:这是一份基于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 Queue<Order> buyQueue = new LinkedList<>(); // 卖盘队列:按时间顺序存放卖出委托 private Queue<Order> 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()方法在队列满时返回false,add()会抛异常,在无界队列场景下两者都可以,但用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 List<Stock> 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会被阻塞,界面就会“假死”。正确做法是用SwingWorker<Void, Stock>把查询放到后台线程,然后在done()或process()里更新界面。

3.3 用SwingWorker做后台加载,避免启动卡顿

程序启动时要初始化股票列表,如果直接用JFrame的构造函数里同步查库,窗口要等好几秒才能显示。我会这样处理:

import javax.swing.*; import java.util.List; public class InitWorker extends SwingWorker<List<Stock>, Stock> { private final StockDataModel model; private final StockService service; public InitWorker(StockDataModel model, StockService service) { this.model = model; this.service = service; } @Override protected List<Stock> doInBackground() throws Exception { // 模拟耗时加载 Thread.sleep(1500); return service.getAllStocks(); } @Override protected void process(List<Stock> chunks) { // 可以在这里分批次更新model } @Override protected void done() { try { List<Stock> 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表很重要,它对应我们内存里的Queue<Order>。如果程序重启,需要把未成交的委托从数据库恢复出来,按create_time排序插入队列。这就是为什么Order对象里必须保存order_id和create_time。position表可以用联合主键(user_id, stock_code) 来保证一个用户对同一只股票只有一条持仓记录,更新时用INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或INSERT OR REPLACE(SQLite)。

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没有指定characterEncoding=utf8。 解决:编译器统一用javac -encoding UTF-8,IDE里设置项目文件编码为UTF-8。MySQL连接串加上?useUnicode=true&characterEncoding=utf8。另外,在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.Driver,URL中也建议加上serverTimezone=Asia/Shanghai,否则时区报错。这是初次运行的项目最容易碰到的坑。

5.6 现象:答辩时老师说“你这个和数据结构有什么关系,就是CRUD”

原因:项目里所有数据都用ArrayList存取,没有呈现任何数据结构设计思想。 解决:在项目里显式实现至少两个数据结构类。比如自定义一个SortedArrayList,插入时按价格二分查找位置,这就是综合了顺序表和折半查找;再用ArrayDeque实现OrderQueue,封装入队出队方法。答辩时拿出这两个类,明确说“我把数据结构封装成了服务类的底层存储”,评分立刻不一样。

6. 答辩前这样验收:构造测试用例,把数据结构讲给评委听

课程设计交付前,我建议你把系统当成真实软件来做一轮功能测试。准备三组数据:一组正常数据(买入价高于卖出价,产生成交);一组边界数据(恰好价格相等、数量刚好全部成交、用户余额刚好足够);一组异常数据(买入价格低于当前最优卖价,委托进入等待队列;卖出数量超过持仓数量;重复登录)。每组数据都记录输入、预期输出、实际输出,写进报告。这样评委问“你怎么验证系统正确”,你就能直接亮出测试表。

针对数据结构展示,你可以设计一个“场景演示”脚本:第一步,打开委托队列可视化面板,依次提交五笔买单,观察队列的入队顺序;第二步,提交一笔卖单,价格低于队伍中最高买价,观察队首的买单被弹出并成交;第三步,连续弹出历史成交记录,演示栈的后进先出。如果能在界面上用两个JList组件实时显示队列内容,效果会非常直观。

我自己的习惯是,在代码里保留一个DebugRunner入口,运行它可以在控制台打印队列操作日志,比如“buy queue after add: [601318@10.2, 600519@1500.0]”。答辩时现场运行这个入口,比空口解释白得多。这不算炫技,而是让老师快速看到数据结构的真实运转。

最后提醒一句:不要试图把真实股票数据接口接入系统,课程设计模拟环境用固定种子随机生成就够了。做好上面这些,哪怕界面朴素一点,分数也不会低。希望帮到你。

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

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

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

立即咨询