
简介这是一份基于Java Swing开发的股票行情与交易模拟系统课程设计资源适合高校数据结构或Java课程设计学生参考。系统实现了实时行情揭示、交易模拟、账户管理、技术指标与策略分析等核心功能采用面向对象设计结合数据库存储、JFreechart第三方图表库和股票数据接口并以随机数模拟服务器行情数据完整覆盖从数据层到图形界面的开发流程。压缩包共187个文件包含54个Java源文件、87个class编译文件、20个jar依赖包、9个png及9个jpg图片资源另有数据库脚本、工程配置和PPT演示文稿整体约10.79MB目录结构清晰。实现中覆盖了链表与队列应用、文件读写、图形界面设计、程序测试用例等课程设计常见考察模块。已有758人学习适合需要完整项目源码、数据库设计及答辩PPT参考的同学可直接导入开发环境运行学习。 朋友们我把这套「数据结构课程设计-基于Java swing的股票交易模拟系统源码数据库ppt」从打开压缩包到答辩演示的完整路径说一遍。很多人以为这是纯GUI作业其实它的核心是拿队列、排序、查找去实现一个能跑通买入卖出的撮合流程。这篇文章会给到数据模型怎么建、数据库连到哪个参数、Swing里哪些更新必须放线程、以及答辩时最容易翻车的几个问题。已经装好JDK和MySQL、想在几天内把它变成自己东西的人照着做就行。1. 数据结构课程设计选它为什么用一个Swing股票交易模拟器去交作业数据结构课程设计里基于Java Swing的股票交易模拟系统是出现频率很高的题目。它不要求你懂量化交易而是把课堂上的队列、栈、排序、查找塞进一个看得见摸得着的界面。老师想看到的不是华丽的炒股界面而是你能不能解释清楚买单和卖单怎么排队、价格优先和时间优先怎么落地成了Comparator、行情列表排序的时间复杂度是多少。对拿到源码的同学来说这个题目最大的价值在于「能演示、能改、能答辩」。这篇笔记就沿着源码落地、数据库连调、界面刷新和答辩演示的顺序把每一步拆开包括参数和那些让人挠头的坑。2. 从数据模型到撮合逻辑订单队列、价格排序和Swing界面怎么搭2.1 订单模型用BigDecimal和UUID把买卖单变成可排序的数据结构做股票模拟第一件事不是画窗口而是把订单模型定下来。金额为什么不建议用double股票价格精确到分double在累加时会出现0.15加多少次都不是整数的现象。更麻烦的是账户余额和成交记录对不上答辩时老师用Excel一算就能发现你的账是平的但内部其实是浮点误差在互相抵消。常见做法是订单里的价格、金额、手续费全部用BigDecimal数量用int或者long时间戳用long。这样和数据库里的DECIMAL字段也天然对齐。import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; import java.util.UUID; public class Order { private final String orderId; private final boolean isBuy; // true买单false卖单 private final String code; // 股票代码 private final int quantity; // 数量 private final BigDecimal price; // 单价 private final long timestamp; // 下单时间单位毫秒 public Order(boolean isBuy, String code, int quantity, BigDecimal price) { this.orderId UUID.randomUUID().toString().replace(-, ).substring(0, 16); this.isBuy isBuy; this.code code; this.quantity quantity; this.price price; this.timestamp System.currentTimeMillis(); } public BigDecimal getTotalAmount() { return price.multiply(BigDecimal.valueOf(quantity)); } }逻辑说明orderId取UUID前16位是为了在日志和成交记录里能关联到原始订单。getTotalAmount用multiply是因为BigDecimal不支持乘号必须用方法调用。这里还有一个新手容易错的地方外部传入价格时不要用new BigDecimal(3.15)这个写法会得到一长串二进制浮点展开值要用BigDecimal.valueOf(3.15)它内部会对double做规范化。参数说明timestamp的类型我用了long而不是LocalDateTime。原因是找两个Order谁先到时long比较最直接Long.compare(a.getTimestamp(), b.getTimestamp())这种代码在Comparator里简单明了。LocalDateTime要转Instant再比多一层没必要。如果你在数据库里存trade_time再用DATETIME那属于持久化层的事内存里用long更合适。2.2 撮合引擎价格优先让位给排序时间优先靠队列保证股票交易撮合规则是价格优先、时间优先。买单价高的先成交卖单价低的先成交同一价格下先来的先成交。用数据结构描述就是两个有序队列买单队列按价格从高到低卖单队列按价格从低到高。最直接的是用PriorityQueue让入队、出队都是O(log n)。如果课程设计要求突出队列也可以自己实现一个基于链表的优先队列每个新订单用插入排序方式找到位置。PriorityQueueOrder buyQueue new PriorityQueue( (a, b) - { int cmp b.getPrice().compareTo(a.getPrice()); // 价格高在前 return cmp ! 0 ? cmp : Long.compare(a.getTimestamp(), b.getTimestamp()); } ); PriorityQueueOrder sellQueue new PriorityQueue( (a, b) - { int cmp a.getPrice().compareTo(b.getPrice()); // 价格低在前 return cmp ! 0 ? cmp : Long.compare(a.getTimestamp(), b.getTimestamp()); } );逻辑说明买入队列的Comparator里b.getPrice().compareTo(a.getPrice())会让价格高的对象排到队首。当价格相同再用timestamp判断先来后到。这就是把业务规则翻译成了Java语法也是答辩时最值得讲的一段。卖单队列正好相反价格低在前。撮合时需要注意peek和poll的区别。新买单进来先用sellQueue.peek()看当前最低卖价如果能成交才poll()取走然后处理数量。很多实现翻车在peek都没用直接poll把没成交的卖单也拿走了价格不匹配的订单被错误成交。处理完数量和剩余量之后剩余部分要重新offer回队列因为PriorityQueue会在入队时重新定位位置。在这个模块里数据结构的落点不止是PriorityQueue。股票列表可以放在ArrayList里按code做二分查找持仓明细可以用LinkedList因为买入卖出经常在中间插入和删除负责撤销上一次操作这个功能的时候ArrayDeque作为栈来用。这些都会成为答辩时回答你这个题目用了哪些数据结构的素材建议在源码里提前给每个集合写一行注释。2.3 JTable与股票列表DefaultTableModel刷新的最小可用代码Swing界面里选JTable是因为它天然支持表头、列宽、行选中。股票列表、持仓列表、成交记录都用JTable承载。刷新数据最稳的做法是持有自己的DefaultTableModel实例而不是每次从table.getModel()强转。DefaultTableModel model new DefaultTableModel( new Object[]{代码, 名称, 现价, 涨跌幅}, 0 ); JTable table new JTable(model); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // 每次行情刷新时 model.setRowCount(0); for (Stock stock : stockList) { model.addRow(new Object[]{ stock.getCode(), stock.getName(), stock.getPrice().toPlainString(), stock.getChangePercent() }); }逻辑说明setRowCount(0)先清空旧数据addRow再逐行追加。如果不先清空第二次刷新时表格里会累积旧行。toPlainString用于避免BigDecimal输出成科学计数法。参数说明SINGLE_SELECTION限制表格只能选中一行。点击买入卖出按钮时你只需要拿table.getSelectedRow()然后从股票列表里取那一行的对象。如果允许多选就得遍历selectedRows课程设计里没什么必要。需要提醒的是这段代码必须在Swing的事件派发线程EDT里执行否则会出现界面刷新异常或者数据状态不一致后面第4章会单独讲。3. 数据库设计与JDBC连接三张表、连接参数和结算SQL3.1 三张表股票、用户持仓、成交记录的字段和类型选择源码包里通常有一个database/init.sql我自己做这个题目时习惯把表分成三张股票表stock持仓表position成交记录表trade_record。这是能支撑演示的最简结构也覆盖了增删改查。如果需要登录功能再加user表但核心业务三张就够。CREATE TABLE stock ( code VARCHAR(10) PRIMARY KEY, name VARCHAR(30) NOT NULL, price DECIMAL(10,2) NOT NULL, change_percent DECIMAL(5,2) DEFAULT 0.00, update_time DATETIME NOT NULL ); CREATE TABLE position ( user_id INT NOT NULL, code VARCHAR(10) NOT NULL, quantity INT NOT NULL, available INT NOT NULL, PRIMARY KEY (user_id, code) ); CREATE TABLE trade_record ( trade_id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, code VARCHAR(10) NOT NULL, direction CHAR(4) NOT NULL, order_id VARCHAR(16) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, amount DECIMAL(12,2) NOT NULL, trade_time DATETIME NOT NULL, INDEX idx_code_time (code, trade_time) );逻辑说明price用DECIMAL(10,2)总长度10位、小数2位最大可存99999999.99对股票单价足够。amount用DECIMAL(12,2)是为了聚合时不会溢出。trade_record里的direction字段很重要它标识这笔成交是buy还是sell否则后面计算持仓成本时只能靠订单号猜测方向很痛苦。order_id就是2.1里订单模型生成的16位字符串这样内存里那个Order和数据库的成交记录能对上。参数说明position表使用联合主键(user_id, code)同一个用户同一只股票只会有一行。买入时更新用INSERT ... ON DUPLICATE KEY UPDATE quantity quantity VALUES(quantity)卖出前先检查available是否大于等于卖出数量。这两步必须放在同一个事务里否则并发操作时会出现负数库存。课程设计虽然没有并发压力但老师说你这个case自减一下的时候你要能接住。3.2 JDBC连接参数useSSL、serverTimezone和字符集一个都不能少很多同学反映JDBC连MySQL是门玄学其实大部分情况是本地MySQL 8的驱动要求显式指定时区SSL握手又让连接变慢。我一般用下面这串参数能覆盖大多数环境String url jdbc:mysql://localhost:3306/stock?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8rewriteBatchedStatementstrue; Connection conn DriverManager.getConnection(url, root, your_password);逻辑说明useSSLfalse是让本地开发不要走证书握手否则每次连库都要多几十毫秒serverTimezoneAsia/Shanghai是因为MySQL 8.0.23之后驱动默认取系统时区系统时区不明确就抛异常characterEncodingutf8解决中文乱码rewriteBatchedStatementstrue是为了批量插入成交记录时驱动能把多条INSERT合并成一条明显减少网络往返。参数说明如果本地MySQL是5.xserverTimezone不写也能连但加上无妨。另一个老项目里常见的useUnicodetrue在这个连接串里可以不写因为characterEncodingutf8存在时驱动会自动启用。连接串里不要有空格尤其不要把多个参数之间的写成中文分号。还有一点不要直接把这个url里的密码硬编码到所有代码里建议单独抽一个DBConfig类后面连数据库工具替换时只改一处。3.3 用一条SQL把成交记录聚合成持仓盈亏课程设计演示时一个常见动作是点刷新持仓然后看到盈亏数字变化。不要用Java把全部成交记录读到内存里再逐条求和那样代码冗长答辩时也不好讲。用SQL聚合一行就能说清楚。SELECT code, SUM(amount) / SUM(quantity) AS avg_cost FROM trade_record WHERE direction buy GROUP BY code;逻辑说明这段SQL算的是某只股票的加权平均买入成本。SUM(amount)是买入总金额SUM(quantity)是买入总数量两者相除得到每股成本。要注意SQL里的整数除法问题因为amount和quantity分别是DECIMAL和INTMySQL自动把结果转成DECIMAL不用像Java那样担心精度但如果你把price设计成FLOAT这里就会出现很多小数位污染。实际上在源码里我更推荐把成本和现价分开查先查持仓现价从stock表拿再用上面的SQL查成本最后在Java里用BigDecimal计算盈亏。别在SQL里直接join太多表课程设计演示数据量很小join多了反而显得刻意。这里的trade_record表还做了一个INDEX idx_code_time (code, trade_time)当演示查某只股票最近10笔成交时这个索引就能作为答辩理由。4. 常见避坑指南Swing线程、浮点金额和窗口退出这三个翻车点怎么排查4.1 界面卡死把耗时查询扔回EDT线程是错的第一步现象点击刷新行情按钮后窗口白屏鼠标箭头转圈拖动窗口要几秒才恢复。严重的时候控制台直接抛Exception in thread AWT-EventQueue-0。原因Swing所有UI操作必须在EDT线程里执行。新手在ActionListener里直接调用数据库查询和排序算法把EDT给占住了每个UI事件都要排队等它跑完界面自然卡死。我之前见过一个同学在EDT里跑了冒泡排序排序几万条模拟数据按钮点下去直接卡了半分钟。解决把耗时操作放进SwingWorkerdoInBackground做查询和排序done里更新表格。这是一个最小可用的版本JButton refreshBtn new JButton(刷新行情); refreshBtn.addActionListener(e - { refreshBtn.setEnabled(false); // 防止重复点击 new SwingWorkerVoid, Void() { Override protected Void doInBackground() { ListStock stocks dao.fetchStocks(); Collections.sort(stocks); // 这里可以替换成你自己的排序算法 return null; } Override protected void done() { model.setRowCount(0); for (Stock s : stockList) { model.addRow(new Object[]{s.getCode(), s.getName(), s.getPrice()}); } refreshBtn.setEnabled(true); } }.execute(); });逻辑说明doInBackground跑在后台线程done回到EDT执行所以UI更新放在done是正确的。代码里我用了成员变量stockList保存后台查询结果这是一种取巧写法。更规范的做法是在done里调用get()拿到返回值但课程设计里用成员变量更直白。参数说明SwingWorkerVoid, Void两个泛型第一个是中间数据第二个是结果数据。如果你需要刷新过程里显示正在加载第几页的进度可以用第一个泛型配合publish和process。这里不需要中间进度所以都用Void。4.2 金额对不上double计算下的数量误差怎么用BigDecimal收口现象买入100股、成交价3.15账户余额显示少了314.99999999999994用Excel重算盈亏和程序里差一分钱。原因double是二进制浮点十进制小数0.15无法精确表示。只要计算里出现除法误差就会被放大。还有一种隐蔽情况是数据库里的DECIMAL字段用getDouble()读取转换时精度已经丢了。解决统一用BigDecimalJBDC读取用getBigDecimal()输出用toPlainString()除法必须指定小数位和舍入模式。BigDecimal avgCost totalAmount.divide( BigDecimal.valueOf(totalQty), 4, RoundingMode.HALF_UP );逻辑说明divide三个参数分别是除数、小数位和舍入模式。HALF_UP是四舍五入和股票交易里按分结算的直觉一致。不指定小数位时除不尽会抛ArithmeticException这是源码里最容易炸的一行。参数说明4位小数是计算中的精度展示给用户前再保留2位。如果你想更严格地处理可以全部保留4位最后只在显示层做四舍五入。不要在模型层用setScale(2)那样中间计算会丢失精度。4.3 关窗口进程不退JFrame默认关闭策略和线程池的互相拉扯现象点击窗口右上角X窗口消失了但Java进程还在任务管理器里IDE显示运行中数据库连接也没释放。原因JFrame默认的关闭操作是HIDE_ON_CLOSE只是把窗口隐藏不退出进程。如果代码里还启动了定时行情线程或者连接池这些非守护线程会把进程一直拖住。main方法里如果只写了setVisible(true)点X后整个程序并不会结束。解决把关闭操作改成EXIT_ON_CLOSE同时在窗口关闭时释放数据库连接和线程池。frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.addWindowListener(new WindowAdapter() { Override public void windowClosing(WindowEvent e) { scheduler.shutdownNow(); // 关闭定时行情线程 if (conn ! null) { try { conn.close(); } catch (Exception ex) { ex.printStackTrace(); } } } });逻辑说明EXIT_ON_CLOSE最终调用System.exit(0)会强制结束所有非守护线程。这个方式最省心但缺点是数据库连接没有优雅关闭。所以在windowClosing里手动关一次保证程序结束后数据库连接数归零。参数说明shutdownNow会尝试中断正在执行的任务适合行情刷新这种每次都很快的任务。如果用shutdown它会等当前任务跑完再退万一定时任务阻塞窗口关闭就会卡在那里。4.4 中文乱码与日期时区连接参数里两块最容易忽略的配置现象JTable里股票名显示成问号或者控制台输出中文乱码trade_time存进数据库后比本地时间晚了8小时。原因数据库表或连接不是utf8驱动会话时区不是Asia/Shanghai。Windows上更容易踩坑因为IDE保存源码默认utf8而MySQL命令行是GBK两边一旦上下文不一致就乱码。解决建库建表时统一utf8mb4连接串加characterEncodingutf8和serverTimezoneAsia/Shanghai。CREATE DATABASE stock DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;说明utf8mb4是utf8的超集可以存emoji和生僻字。虽然股票名称用不上表情但这代表一种习惯老师看到你建库语句写了utf8mb4会认为你注意到编码问题。collation选general_ci是大小写不敏感的比较规则查询时不区分大小写。5. 把课程设计讲成答辩加分项PPT的顺序、复杂度表格和追问应对5.1 PPT四段式从演示一个买入动作到展示排序算法的选择PPT不要上来就摆代码老师想先看到系统能跑。我建议按四段走第一功能演示截图放一个买入操作的前后对比第二数据流图概括一个买单从界面、队列到数据库的路径第三数据结构分析列出队列、排序、查找分别落在哪个类或哪个方法第四测试数据展示不同行情数量下的排序耗时。演示截图时一定要截两次下单前和下单后。老师只看到买入成功提示不知道内部发生了什么。两次截图之间最好在成交记录表格上高亮新插入的那行配合一句这笔成交由买单和卖单队列撮合产生。这个桥段会让答辩分量完全不一样。5.2 复杂度对比表把冒泡、插入、快速排序放进演示文档PPT里放一张复杂度对比表老师通常都会往这个方向提问因为你主动给了对比表他就顺着问那你为什么用这个排序。下面这张表可以直接用算法最好时间平均时间最坏时间空间什么时候换它冒泡排序O(n)O(n^2)O(n^2)O(1)数据量小于100时演示用插入排序O(n)O(n^2)O(n^2)O(1)行情列表近乎有序时更好快速排序O(n log n)O(n log n)O(n^2)O(log n)数据量大时优先PriorityQueue入队O(log n)O(log n)O(log n)O(n)撮合队列默认选择注意表中快速排序这几个字要和你源码里实际用的对得上。如果源码直接用了Collections.sort那是TimSort不是快速排序。这种情况下可以改成TimSort/归并插入或者干脆自己写一个快速排序让PPT和代码严丝合缝。课上还有一个高频问题明明冒泡也能跑完为什么要用快速排序 回答方向是1000只股票冒泡平均比较50万次快速排序平均约1万次数据量再翻10倍差距更明显。5.3 答辩追问清单队列为什么不用栈、索引为什么建在这几个字段这串问题其实很像java面试题里的数据结构部分问来问去就那么几个点。提前把答案写在PPT附录老师问到直接翻页。为什么撮合队列不用栈因为栈是后进先出会导致同一价格的订单后到先成交违背时间优先。为什么PriorityQueue而不是普通数组排队普通数组入队O(1)但取最高/最低价要O(n)扫描堆结构让入队和最高价出队都是O(log n)。数据量小看不出差别但你可以把复杂度写在PPT里。为什么索引建在trade_time上演示里最常做的查询是按股票代码加时间范围查成交记录复合索引(code, trade_time)能命中这个查询。登录密码为什么不能明文存至少用SHA-256或者MD5加盐。老师可能不要求但你写了他会觉得安全意识到位。还有一个准备技巧把压缩包里的README重写一遍标好数据结构所在位置。我用过这样一个模板# 股票交易模拟系统 ## 运行环境 JDK 8, MySQL 5.7, IDE不限 ## 初始化 执行database/init.sql修改DBConfig.java中的用户名密码 ## 数据结构落点 - ArrayDeque: UserController中的操作撤销栈 - PriorityQueue: TradeEngine中的买/卖订单队列 - TimSort: StockListPanel中的行情排序逻辑说明README是给老师快速定位代码用的也方便自己答辩前顺着目录把每个类重新过一遍。如果源码包原本的README写得不清不楚花半小时改成这样很值。6. 往真实系统再走一步用定时任务模拟行情推送并验证不卡界面6.1 用ScheduledExecutorService定时刷新行情并在后台线程验证耗时把静态的股票列表改成每2秒自动变价一次演示效果会好很多。实现上不要直接在定时任务里更新JTable因为定时线程不在EDT上触碰UI组件会触发并发修改异常。我一般会用一个单线程定时调度器拉取行情再用SwingUtilities.invokeLater把UI更新丢回EDT。ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() - { ListStock newQuotes dao.fetchQuotes(); SwingUtilities.invokeLater(() - { model.setRowCount(0); for (Stock s : newQuotes) { model.addRow(new Object[]{s.getCode(), s.getName(), s.getPrice()}); } }); }, 0, 2, TimeUnit.SECONDS);逻辑说明第一个参数0表示首次执行不延迟第二个参数2表示每2秒执行一次TimeUnit.SECONDS是时间单位。invokeLater会把更新UI这段逻辑排到EDT队列的末尾所以调度线程永远不会阻塞界面。把2秒改成200毫秒就能在演示时看到CPU占用明显上去这就变成了一个压力验证工具。验证方式可以这样在窗口上放一个计时按钮点击后按钮文字变成计时中2秒后变回来。定时刷新运行期间连续点这个按钮如果按钮没有明显卡顿说明后台行情刷新没有抢占EDT。我之前做的时候没加scheduler的关闭逻辑窗口插掉后进程一直不退出查了很久才发现是没有在windowClosing里shutdownNow后来养成了启动线程池就顺手写关闭回调的习惯。这套定时推送的改法希望帮到你。本文还有配套的精品资源点击获取