
简介面向 Java 课程设计与股票模拟项目开发者这份压缩包提供股票行情与交易模拟系统的完整实现方案涵盖源码、数据库连接与演示 PPT。系统基于 Java Swing 开发引入 JFreeChart 绘制价格走势覆盖账户管理、行情展示、模拟交易、技术指标和策略分析等模块同时对对象建模、随机数模拟行情、链表与队列使用、图形界面开发、文件读写等课程设计常考点都有较好示范。包内共 187 个文件以 54 个 java 源文件、87 个编译后 class、20 个 jar 依赖库为主配合 18 张界面图片、PPT 和说明文档压缩包大小约 10.79MB工程结构清晰便于导入对照。已有 758 人学习下载适合需要完整实训方案、数据库设计思路和答辩演示素材的高校学生参考。 如果你正在为数据结构课程设计的选题发愁这个标题会非常对味一个基于 Java Swing 的股票交易模拟系统压缩包里配好了源码、数据库脚本和答辩 PPT。它要解决的核心问题不是真的炒股而是用委托排队、行情排序、持仓映射这些交易场景把课程里讲的队列、排序、散列表真正用起来再顺带把 Java GUI、JDBC 和数据库设计一起交掉。适合谁正在赶课程设计又不想只交一个算法练习题的学生以及想快速补完一套完整 Java 课设代码的初级开发者。我当年拿到类似项目时第一步就是搞清楚这些文件如何跑起来、怎么给老师讲明白这篇就按“它是什么、怎么做、坑在哪”的顺序把这个过程拆开。2. 数据结构怎么落进股票系统委托队列、排序与持仓管理买卖委托是股票交易系统的入口。用户下单后委托不会立刻成交而是进入一个等待队列由撮合器按规则匹配。数据结构课里最常见的考察就是“这个队列该用 Queue 还是 Stack”。按照交易所的时间优先规则相同价格的委托先到先成交这是典型的先进先出场景所以用Queue接口LinkedList做实现。queue.offer(order)把委托放入队尾queue.poll()取出队头最早的委托撮合顺序和业务语义完全一致。// 时间优先先进入队列的委托先被撮合这就是 FIFO QueueOrder buyQueue new LinkedList(); // 提交买单 buyQueue.offer(new Order(600519, 100, 1580.00, BUY)); // 撮合时取出队头委托poll 返回 null 表示队列已空 Order first buyQueue.poll();逻辑说明offer在队列尾部添加元素并返回 boolean适合判断是否加成功poll从队头取出并移除队列空时返回 null 而不是抛异常。如果用add和pop空队列会直接抛NoSuchElementException课设里处理起来很别扭。要演示“价格优先”也很简单换成优先队列即可。买价更高的委托应该排到前面代码里用 Comparator 指定降序QueueOrder buyPriority new PriorityQueue( (o1, o2) - Integer.compare(o2.getPriceCents(), o1.getPriceCents()) );这里的getPriceCents()返回以“分”为单位的 int是为了避开 double 比较精度问题。Comparator 里 o2 在前、o1 在后表示降序也就是买价高者优先一旦写反就成了低价优先买卖撮合直接翻车。答辩时老师问“为什么不用栈”你还可以补一句栈是后进先出如果模拟撤销委托或回退操作栈反倒合适但撮合排队必须用队列。这样的选型解释比单纯背定义有说服力得多。2.1 买卖委托用队列还是栈FIFO 与价格优先的取舍在交易所的时间优先规则里同一价格的委托先到先成交这正好对应队列的先进先出。如果业务改成“最新委托先成交”那才轮到栈出场。课程设计报告里写数据流时通常会把买单队列、卖单队列拆成两个集合撮合器从两个队头各取一单比较价格能撮合就成交不能撮合就把价格不合适的那一单放回去。这里的“放回去”如果只是放回队头操作是peek()而非poll()很多初学者会在这里搞混。为了让队列更直观我一般会在 UI 右侧放一个JList每次撮合后把队列元素刷进去。这样数据结构是“看得见”的而不是只在控制台里打一行日志。至于 Order 对象至少要包含委托编号、股票代码、买卖方向、委托价格、委托数量、委托时间六个字段其中委托时间用System.currentTimeMillis()生成 long 类型方便后续按时间排序。2.2 行情列表与成交记录的排序把冒泡和快排用在该用的地方股票列表按涨跌幅排序、成交记录按时间倒序、自选股按加入顺序显示这三个场景在数据结构课程设计里属于“排序算法”的必考范围。Java 里最省事的做法是让实体实现Comparable再调用Collections.sort。写数据结构实验报告的时候可以额外准备一段冒泡排序的代码用于演示排序过程但正式功能里不要自己造轮子直接用标准库的 TimSort它对于部分有序数据表现接近 O(n)。public class Stock implements ComparableStock { private String code; // 股票代码 private String name; // 股票名称 private double currentPrice; // 现价 private double changeRate; // 涨跌幅百分比 Override public int compareTo(Stock o) { // 涨跌幅从高到低排序 return Double.compare(o.changeRate, this.changeRate); } }逻辑说明Double.compare(o.changeRate, this.changeRate)是降序写法当前对象的涨跌幅比参数小时返回正数Collections.sort 就会把它排在后面。千万别写成return (int) (o.changeRate - this.changeRate);double 相减后转 int差值小于 1 时会直接变成 0排序直接失效。// 课程设计演示用的冒泡排序每轮把最大涨跌幅的股票冒到末尾 for (int i 0; i list.size() - 1; i) { for (int j 0; j list.size() - 1 - i; j) { if (list.get(j).getChangeRate() list.get(j 1).getChangeRate()) { Collections.swap(list, j, j 1); } } System.out.println(第 (i 1) 轮: list); }这份冒泡代码只用来展示“排序过程”真实行情列表刷新时我会用Collections.sort(stocks)。如果你要按成交时间倒序就让Trade类实现Comparable内部用Long.compare(o.timestamp, this.timestamp)同样可以一行调用。排序完记得通知表格模型刷新否则 JTable 上数据不变但这部分放到避坑章节再展开。2.3 持仓与自选股列表数组、链表还是散列表用户持有哪些股票、各持有多少股这是系统里最需要设计的数据结构。最简单的写法是维护一个ArrayListPosition买入时先 for 循环查一遍有没有已经持有该股票。数据量少时确实能跑但课程设计答辩时老师问一句“查找复杂度是多少”你就只能答 O(n)。常见做法是叠加一个HashMapString, Integer股票代码映射到持仓列表的下标把查找降到 O(1)。public class PositionService { private ListPosition positions new ArrayList(); private MapString, Integer positionIndex new HashMap(); // 买入已有持仓则更新数量否则新增 public void addPosition(Stock stock, int quantity) { Integer idx positionIndex.get(stock.getCode()); if (idx null) { positions.add(new Position(stock, quantity)); positionIndex.put(stock.getCode(), positions.size() - 1); } else { Position p positions.get(idx); p.setQuantity(p.getQuantity() quantity); } } }逻辑说明positionIndex里保存的是 ArrayList 的下标新增持仓时记录下标再次买入同一只股票时直接定位到原有持仓。这里的风险是如果从positions中删除某条记录后面所有下标都会移动index 全部失效。做卖出逻辑时如果删除了持仓必须重建 index或者删除时也同步把 map 里受影响的下标全部改一遍两者取其一。自选股列表要保持用户添加顺序就不建议用普通HashMap换成LinkedHashMap更合适。它内部是哈希表加双向链表既保留了 O(1) 查找又记住了插入顺序。这类“选择哪种集合”的细节恰恰是数据结构课设评分时老师最爱听的加分点。3. 用 Java Swing 把交易流程跑通界面、数据库与核心代码数据结构的模型立住之后下一步是把界面和数据库串起来。常见的课设源码解压后大致是这种结构src下按ui、dao、model、service、util分包db目录放初始化脚本ppt目录里是答辩文稿。拿到压缩包后建议先用 IDE 打开项目检查依赖 jar 是否导入然后执行数据库脚本最后再运行入口类。这个顺序能避免“代码打开就一堆报错”的挫败感。3.1 项目结构与入口MainFrame、登录窗口和数据库初始化入口类是理解整个系统的钥匙。大多数 Java 课程设计项目都会写一个Main.java里面初始化数据库连接后再启动登录窗口。有一个细节容易被忽略创建 Swing 窗口要放到SwingUtilities.invokeLater()里这是 Swing 官方约定的线程模型虽然不是强制但老师问“界面为什么要在事件分发线程创建”时你能接得住。public class Main { public static void main(String[] args) { // 读取配置文件并初始化数据库连接 DBHelper.init(); // 在事件分发线程中启动登录窗口 SwingUtilities.invokeLater(() - { LoginFrame frame new LoginFrame(); frame.setVisible(true); }); } }逻辑说明DBHelper.init()放在 UI 启动之前作用是加载 JDBC 驱动、读取数据库连接参数。真正的连接可以延迟到登录时创建避免程序一开就报数据库连不上。invokeLater会把窗口创建任务排进事件队列确保所有组件都在 EDT 中创建。数据库初始化脚本通常长这样CREATE DATABASE IF NOT EXISTS stock_db DEFAULT CHARSET utf8mb4; USE stock_db; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, balance DECIMAL(12,2) NOT NULL DEFAULT 100000.00 );参数说明balance字段用DECIMAL(12,2)而不是 double这是金额精度的第一道防线。默认余额 10 万符合模拟盘习惯。表结构做到用户表、股票表、持仓表、成交记录表四张就够不用过度设计外键和索引。MySQL 5.7 和 8.0 执行这段脚本都没问题连接 URL 记得带上characterEncodingutf8和serverTimezoneAsia/Shanghai。3.2 股票行情表JTable 与 TableModel 的联动刷新主界面最常见布局是左侧JTable显示股票列表右侧一个JPanel显示分时或 K 线。JTable 本身不存数据它依赖TableModel所以第一步先写一个股票表格模型。public class StockTableModel extends AbstractTableModel { private final String[] columns {代码, 名称, 现价, 涨跌幅}; private ListStock stocks new ArrayList(); public void setStocks(ListStock data) { this.stocks data; fireTableDataChanged(); // 关键通知表格重新绘制 } Override public int getRowCount() { return stocks.size(); } Override public int getColumnCount() { return columns.length; } Override public Object getValueAt(int rowIndex, int columnIndex) { Stock s stocks.get(rowIndex); return switch (columnIndex) { case 0 - s.getCode(); case 1 - s.getName(); case 2 - String.format(%.2f, s.getCurrentPrice()); case 3 - s.getChangeRate() %; default - ; }; } }逻辑说明fireTableDataChanged()是刷新核心数据变化后调用它JTable 才会重新执行getValueAt。不要把刷新逻辑写进getValueAt里这个方法在绘制时会被频繁调用放耗时逻辑会把 UI 卡住。行情模拟可以启动一个javax.swing.Timer每 2 秒随机波动一次价格然后调用模型刷新。刷新间隔太短会让表格抖动明显2000ms 是演示观感和性能的平衡值。如果要做分时图不想引入第三方库可以在JPanel里重写paintComponent画折线想省事就用 JFreeChart两三行代码出一张图但答辩时要有思想准备老师会问库原理。3.3 买卖核心逻辑余额扣减、持仓变更与事务边界交易逻辑是整个系统最容易出数据一致性问题的地方。买入应该走四步校验余额、扣减余额、更新持仓、写入成交记录。这四步必须包在同一个数据库事务里否则就会出现“钱扣了股票没到账”的翻车现场。public boolean buy(String userId, Stock stock, int quantity) throws Exception { Connection conn null; try { conn DBHelper.getConnection(); conn.setAutoCommit(false); BigDecimal total stock.getCurrentPrice() .multiply(BigDecimal.valueOf(quantity)) .setScale(2, RoundingMode.HALF_UP); BigDecimal balance accountDao.getBalance(conn, userId); if (balance.compareTo(total) 0) { return false; // 资金不足 } accountDao.decreaseBalance(conn, userId, total); positionDao.addPosition(conn, userId, stock, quantity); tradeDao.insert(conn, userId, stock, BUY, quantity, stock.getCurrentPrice()); conn.commit(); return true; } catch (Exception e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }逻辑说明setAutoCommit(false)之后同一连接上的多条 SQL 处于一个事务中commit 成功后一次性生效中途任何一步失败rollback 会把余额、持仓、流水全部撤回。注意在 finally 里把 autoCommit 恢复为 true并关闭连接否则连接池中的连接会一直处于事务状态影响后续请求。参数说明买入数量建议限制为 100 股的整数倍模拟 A 股规则价格用当前市价即可如果要整委托队列可以在 service 层先用buyQueue去撮合撮合成功再走这一段。手续费按成交金额万分之 2.5 计算加到 total 中用BigDecimal计算并保留两位小数。卖出逻辑与买入对称不同点是先查positionIndex确认持仓数量够不够卖出成功后持仓数量减少到 0 时从positions中移除该条记录并重建 index。很多同学在“删除后忘记重建”这里踩坑下一章重点排查。4. 避坑清单Swing 卡死、金额精度和资源路径的排查记录4.1 点击登录后界面白屏不动数据库查询占用了 UI 线程现象点击“登录”按钮后整个窗口无响应Windows 标题栏提示未响应十几秒后突然跳回主界面。原因按钮事件默认在事件分发线程执行。如果直接在ActionListener里写 JDBC 查询数据库连接慢或查询慢EDT 就被同步调用阻塞界面无法处理重绘事件看起来像死机。解决耗时操作丢进新线程执行完切回 EDT 更新界面。最轻量的写法是new Thread(() - { ListStock list stockDao.findAll(); // 耗时查询 SwingUtilities.invokeLater(() - { tableModel.setStocks(list); }); }).start();逻辑说明invokeLater把 UI 更新操作重新排回 EDT查询期间主窗口能正常响应拖动和点击。如果项目用了SwingWorker更规范的做法是在doInBackground里查数据、在done里更新界面。答辩提到 EDT 和 SwingWorker比只说“我开了线程”要有说服力。4.2 卖了 100 股后总资产变成 99.999999999double 精度翻车现象多次买入卖出后余额出现一长串小数尾巴比如 99999.999999999更隐蔽的是比较余额是否充足时误判明明钱够却提示资金不足。原因二进制浮点数无法精确表示 0.1 这类十进制小数double 的误差在连续运算里不断累积。只要有一个字段用了 double 存金额最终总要对不上。解决金额字段统一用BigDecimal比较用compareTo不用equals数据库字段用DECIMAL。计算示例BigDecimal price new BigDecimal(9.99); BigDecimal quantity new BigDecimal(100); BigDecimal total price.multiply(quantity).setScale(2, RoundingMode.HALF_UP);注意new BigDecimal(9.99)也会拿到二进制近似值必须用字符串构造。setScale(2, HALF_UP)则是把结果四舍五入到分交易系统里这是底线操作。4.3 数据库连接报错ClassNotFoundException 或时区问题现象运行后报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或者The server time zone value ?й?ʱ?? is unrecognized。前者是驱动没加载后者是 MySQL 8 驱动不认系统时区。原因驱动 jar 没有打进 classpath或者类名写错。时区问题则是因为 MySQL 8 的驱动要求连接串里明确指定serverTimezone默认读取系统时区时格式无法解析。解决Maven 项目在 pom 里加依赖普通项目把 jar 放进 lib 并在 IDE 中添加依赖。连接 URL 写完整jdbc:mysql://localhost:3306/stock_db? useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai注意 MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver老版本才是com.mysql.jdbc.Driver。如果项目里驱动版本较老时区参数可能不被识别就把serverTimezone删掉再试。4.4 打成 jar 包后运行崩溃数据库脚本和图片资源读不到现象IDE 里运行正常用java -jar启动后报FileNotFoundException窗口背景图消失数据库初始化失败。原因IDE 运行时资源文件放在磁盘上可以用相对路径找到jar 包内的资源不是普通文件new File(schema.sql)自然读不到。解决改成类路径加载InputStream in Main.class.getResourceAsStream(/schema.sql);逻辑说明getResourceAsStream从 classpath 根目录读取文件打包成 jar 后依然有效。图片资源同理用ImageIO.read(Main.class.getResourceAsStream(/images/bg.png))。如果资源放在 jar 外部某个目录那才适合用new File并做好路径拼接。4.5 表格数据改了但界面不刷新忘了通知 TableModel现象后台行情模拟线程一直在更新股票价格列表但 JTable 上的数字纹丝不动点击一下表格又恢复正常。原因JTable 只认 TableModel你改了自定义的 List没有通知模型发生变化界面不知道要重新取数。解决在数据更新后调用fireTableDataChanged()注意如果更新发生在非 UI 线程还需要配合SwingUtilities.invokeLater。ListStock stocks stockService.refreshPrices(); stockTableModel.setStocks(stocks); // 内部会 fireTableDataChanged这段逻辑前面在 StockTableModel 里已经体现排查时先看setStocks有没有被调用再看方法里有没有fireTableDataChanged。很多代码报“刷新没用”其实都是少了这一行。5. 让课设从「能跑」到「能讲」验证技巧与答辩演示改进5.1 用一条买卖用例把整个系统验一遍代码跑通不等同于系统正确。我会在演示前走一套固定用例确保核心链路没有隐藏问题。注册一个新用户初始余额 100000.00买入 100 股价格 10.00此时余额应为 99000.00把模拟行情涨到 11.00 后卖出余额应变为 100100.00若算手续费则为扣除手续费后的值再次卖出系统应提示持仓不足而不是抛异常。步骤操作预期结果1注册并登录初始余额 100000.002买入 100 股 10.00余额 99000.00持仓 100 股3卖出 100 股 11.00余额 100100.00持仓为 04再次卖出提示持仓不足无异常这个用例同时覆盖了余额扣减、持仓更新、成交记录写入和失败分支答辩前跑一遍基本能证明系统不是表面“能跑”而是流程正确。5.2 把数据结构过程打印到控制台答辩当场加分课设答辩时最怕老师问“你的队列到底在哪”。我会在撮合器里加一个演示开关把队列变化打印到控制台让数据结构过程可视化。System.out.println([撮合前] 买单队列: buyQueue); Order buy buyQueue.poll(); System.out.println([撮合] 取出买单: buy);逻辑说明给Order重写toString()让输出显示代码、价格、数量、方向。每次撮合前打印队列状态取出一单再打印一次老师一眼就能看到先进先出或价格优先的规则在执行。打印语句用 boolean 开关控制平时关闭、答辩时打开避免日志刷屏。我当年做课设最吃亏的就是代码写完了但老师问“队列在哪里”时只能指着一行new LinkedList()说“就这里”。后来养成习惯把关键数据结构的每一步操作都打印出来多跑几遍再整理进 PPT整个答辩过程会顺很多。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取