简介面向Java初学者与准备毕业设计的大学生这份资源以Java Swing技术为核心完整呈现黄金矿工桌面游戏从窗口搭建、角色绘制到交互逻辑的实现过程。适配Idea 2021与Java8需要使用者已能完成环境变量配置并对面向对象编程有初步认识适合作为从基础语法迈向小型项目开发的过渡练习。资源包约257.84MB压缩包内共664个文件包含120个java源码与164个class编译产物26个mp4教学视频按步骤讲解png、gif、jpg素材覆盖游戏角色和场景贴图xml、iml等工程配置文件便于导入Idea后直接运行。已有111人学习下载。通过完整项目可清晰看到Swing界面布局、事件监听、碰撞检测与小游戏循环的实现方式代码按开发阶段组织视频支持逐行跟写既能整体运行调试也能逐步复现对完成课程设计或提升Java GUI开发能力均有直接帮助。1. 为什么 Java 课程设计首选《黄金矿工》一个 800 行的项目讲透半本 Java 基础如果你正在找 Java 课程设计案例源码又不想交一个只会查数据库的“管理系统”《黄金矿工》是性价比很高的题目。需求一句话能说清矿工在屏幕底部钩爪伸出去抓金块、躲石头限时内赚够目标分数。拆开看全是 Java 基础考点——类和继承建模金块和石头线程控制钩爪伸缩集合和排序管排行榜对象深拷贝能做存档撤销。这个项目适合三类人刚学完 Java 基础想串知识的新手想在简历上放一个能讲清楚的小项目的人正在做 Java 课程设计、想从“能跑”做到“能答辩”的学生。下面按我平时带项目的方式从零到把钩爪、碰撞、计分、存档拆完整每一步给能跑的代码也把参数和坑交待清楚。2. 用 Swing 搭游戏骨架窗口、矿工和第一根钩爪2.1 为什么不上游戏引擎JFrame 自定义面板课程设计最稳的选型很多初学者一上来就搜 LibGDX、Unity想把画面做得华丽。但课程设计评估的是 Java 语言能力不是游戏引擎熟练度。用 Swing 的好处很实在零第三方依赖一台装了 JDK 的机器就能编译运行代码量控制在几百行老师看得懂你也看得懂答辩时不会被追问到哑火。Swing 里做游戏的标准方式是 JFrame 做窗口JPanel 做画布重写面板的 paintComponent 方法画每一帧。别用 JLabel 堆界面也别直接在 JFrame 里 new 一堆按钮——游戏的渲染循环和事件循环要分开这是后面所有逻辑能跑稳的前提。如果你连 Java 环境都还没搞定先把 JDK 装好、配好 Java 环境变量命令行里java -version和javac -version都能正常输出再开始。很多人卡在环境变量配置上双击 jar 没反应第一反应是代码问题最后发现是 PATH 没配对。2.2 最小可运行代码窗口、面板和 60 帧定时器先建一个 GoldMiner 类作为主窗口。import javax.swing.*; import java.awt.*; public class GoldMiner extends JFrame { public GoldMiner() { setTitle(黄金矿工 - Java课程设计); setSize(800, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); // 固定窗口避免坐标拉伸 setLocationRelativeTo(null); // 窗口居中 add(new GamePanel()); } public static void main(String[] args) { // Swing 组件必须放到事件分发线程里创建 SwingUtilities.invokeLater(() - new GoldMiner().setVisible(true)); } }这段负责两件事创建 800x600 的窗口把游戏面板塞进去。setSize定的是整体窗口尺寸不是画布尺寸所以面板绘制区域实际会比 800x600 小一圈标题栏会占掉几十像素。我做项目时一般会把面板自身尺寸再固定一次避免后续算坐标时出现误差。SwingUtilities.invokeLater不是可有可无的仪式。Swing 的组件不是线程安全的如果在 main 线程里直接 new JFrame某些平台会出现偶发渲染竞态这种问题最难查。写成 lambda 是 Java 8 起的标准写法也顺便复习了函数式接口。游戏面板是核心先给一个能画出矿工的最小版本。import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class GamePanel extends JPanel implements ActionListener { private final Timer timer; public GamePanel() { setBackground(new Color(135, 206, 235)); // 天空蓝 timer new Timer(16, this); // 16ms 一帧约 60 FPS timer.start(); } Override public void actionPerformed(ActionEvent e) { updateLogic(); // 先更新逻辑 repaint(); // 再请求重绘 } private void updateLogic() { // 钩爪逻辑稍后填 } Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 矿工固定在底部中间 g.setColor(new Color(139, 69, 19)); g.fillOval(370, 520, 60, 60); g.setColor(Color.BLACK); g.drawString(矿工, 385, 556); } }Swing 的 Timer 不是 java.util.Timer它的事件回调会回到事件分发线程里执行所以在这里直接更新坐标、调用 repaint 是安全的。每 16ms 一帧1 秒约 60 次触发刚好和显示器刷新率对齐看起来就是连续动画如果改成 100ms画面会明显“一帧一帧跳”。updateLogic 和 repaint 的顺序不要反过来。先改数据再让面板把所有组件重画一遍反过来会先画出旧状态视觉上慢半拍。那种“画面反应迟钝”的毛病多半是这个顺序反了。2.3 让钩爪动起来键盘输入和三个手感参数钩爪的运动可以拆成两个状态变量angle 表示当前方向弧度length 表示已伸出的长度。每次按键只改角度钩爪端点坐标用三角函数算出来。import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.HashSet; import java.util.Set; public class GamePanel extends JPanel implements ActionListener { private final SetInteger pressedKeys new HashSet(); private double angle Math.PI / 2; // 垂直向上 private double length 0; // 钩爪已伸出的长度 private boolean expanding true; // 当前是否在伸出 private boolean retracting false; // 当前是否在回收 private static final int HOOK_START_X 400; private static final int HOOK_START_Y 560; private static final double ANGLE_STEP 0.03; private static final int EXTEND_SPEED 4; private static final int RETRACT_SPEED 3; private static final int MAX_LENGTH 600; public GamePanel() { setFocusable(true); addKeyListener(new KeyAdapter() { Override public void keyPressed(KeyEvent e) { pressedKeys.add(e.getKeyCode()); } Override public void keyReleased(KeyEvent e) { pressedKeys.remove(e.getKeyCode()); } }); } private void updateLogic() { if (pressedKeys.contains(KeyEvent.VK_LEFT)) { angle - ANGLE_STEP; } if (pressedKeys.contains(KeyEvent.VK_RIGHT)) { angle ANGLE_STEP; } // 限制在 10 度到 170 度防止钩爪往屏幕外或地面里钻 angle Math.max(Math.toRadians(10), Math.min(Math.toRadians(170), angle)); if (expanding) { length EXTEND_SPEED; if (length MAX_LENGTH) { expanding false; retracting true; } } else if (retracting) { length - RETRACT_SPEED; if (length 0) { retracting false; expanding true; } } } Override protected void paintComponent(Graphics g) { super.paintComponent(g); int hookX HOOK_START_X (int) (length * Math.sin(angle)); int hookY HOOK_START_Y - (int) (length * Math.cos(angle)); g.setColor(new Color(139, 69, 19)); g.fillOval(370, 520, 60, 60); g.setColor(Color.GRAY); g.drawLine(HOOK_START_X, HOOK_START_Y, hookX, hookY); g.setColor(Color.RED); g.fillOval(hookX - 8, hookY - 8, 16, 16); } }这里有几个要点用 pressedKeys 集合而不是在 keyPressed 里直接改角度。直接改的话按住左键会触发系统键盘重复事件左右摆动会一顿一顿用集合记录按住的键每帧统一判断手感是连续的。坐标系里 y 轴向下所以水平方向用 sin垂直向上用 -cos。数学里的角度习惯和屏幕坐标很容易混画出来的钩爪乱飞时先检查这里。角度限制用Math.toRadians(10)和Math.toRadians(170)不要写 magic number 0.17 之类的后面读代码的人一眼看不懂含义。伸出速度比回收速度快 1 像素/帧这是故意的。收得太快物品来不及看清收得太慢又会拖节奏这个差值可以当手感参数慢慢调。三个手感参数按我的习惯调参数值说明ANGLE_STEP0.03 弧度/帧约每秒 1.8 弧度左右摆动跟手但不飘EXTEND_SPEED4 像素/帧约每秒 240 像素半屏多一点需 2 秒多RETRACT_SPEED3 像素/帧略慢于伸出让玩家看清抓到了什么Timer 延迟16 ms约 60 FPS逻辑和渲染都走这里这几个值属于“手感秘方”实际开发时可以暴露成常量甚至放到配置文件里方便即时调。把private static final改成private int再用 JSpinner 或配置文件加载是很容易演示的扩展点。3. 用面向对象把金块、石头和钩爪建模类的拆分与抓取判定3.1 抽象类还是枚举怎么建模矿场里的东西游戏里要抓的东西类型很明确金块有大有小价值不同石头没有价值但特别重钩子抓回来慢。数据结构上有两种常见做法一种是用一个类加物品类型枚举另一种是抽象类加子类。// 方式一单类 枚举类型一多 switch 爆炸 public class MineObject { public enum Type { GOLD, STONE } private Type type; private int x, y, width, height, value; // 每次判断物品类型都要 switch新增钻石就要改多个地方 } // 方式二抽象类 子类 public abstract class MineObject { protected int x, y, width, height, value; protected boolean visible true; public MineObject(int x, int y, int width, int height, int value) { this.x x; this.y y; this.width width; this.height height; this.value value; } public abstract void draw(Graphics g); public abstract MineObject copy(); }课程设计建议用方式二。原因不只是“多态听起来高级”而是后续每个分支逻辑会自然落到各子类里代码不会越写越厚。金块画金色圆、石头画灰色圆角方块、以后加炸弹加钻石每个类只改自己那份。面试问到“面向对象三大特性”你手里正好有真实例子继承解决共性重写解决差异。注意这里所有坐标、尺寸、价值都用 int。这就是 Java 基本数据类型最直观的用法也顺带说明为什么局部变量和成员变量要显式初始化——width、visible 这些字段没赋值时会用默认值不小心就会在碰撞检测里产生“隐形物体”。金块和石头分别实现public class Gold extends MineObject { public Gold(int x, int y, int size, int value) { super(x, y, size, size, value); } Override public void draw(Graphics g) { if (!visible) return; g.setColor(new Color(255, 215, 0)); g.fillOval(x, y, width, height); g.setColor(new Color(184, 134, 11)); g.drawOval(x, y, width, height); // 描边让金块有立体感 } Override public MineObject copy() { Gold g new Gold(x, y, width, value); g.setVisible(visible); return g; } }public class Stone extends MineObject { private final int weight; // 重量只属于石头 public Stone(int x, int y, int w, int h, int value, int weight) { super(x, y, w, h, value); this.weight weight; } public int getWeight() { return weight; } Override public void draw(Graphics g) { if (!visible) return; g.setColor(new Color(128, 128, 128)); g.fillRoundRect(x, y, width, height, 12, 12); } Override public MineObject copy() { Stone s new Stone(x, y, width, height, value, weight); s.setVisible(visible); return s; } }到这里能看出一个区别weight 只存在于 Stone不会跑到父类里成为“金块不用填的字段”。父类里每个字段都该是所有子类共有的东西把 weight 塞父类会让 Gold 构造函数尴尬。这也是“字段上移/下移”的一个小例子答辩时可以顺口提一句。3.2 随机生成关卡物品并把抓取判定写进逻辑物品生成时最忌随机位置完全无规则。钩爪从底部中央出发物品如果刷在底部边缘或者和钩爪起点重叠开局就会直接卡住。private void generateLevel(int count) { Random rand new Random(); for (int i 0; i count; i) { int x 50 rand.nextInt(700); // 水平方向留边 int y 50 rand.nextInt(300); // 只生成在上半屏 // 避开矿工附近区域约 100x100 的范围 if (Math.abs(x - HOOK_START_X) 100 Math.abs(y - HOOK_START_Y) 100) { continue; } if (rand.nextBoolean()) { Gold gold new Gold(x, y, 24 rand.nextInt(30), 20 rand.nextInt(80)); objects.add(gold); } else { Stone stone new Stone(x, y, 30 rand.nextInt(30), 20 rand.nextInt(20), 0, 5 rand.nextInt(10)); objects.add(stone); } } }注意new Random()每次运行 seed 都不同所以每局物品位置是“真随机”。如果调 bug 时想复现某一局建议先用固定种子比如new Random(2024)等逻辑稳定后再去掉。这是一个非常实用的调试习惯能省掉大量“这局我能复现但下局就变了”的折磨。抓取逻辑放进 updateLogicprivate MineObject caught; // 当前钩住的物品 private boolean retracting false; private void updateLogic() { // 角度控制同 2.3略 if (expanding) { length EXTEND_SPEED; int hookX currentHookX(); int hookY currentHookY(); for (MineObject obj : objects) { if (obj.isVisible() obj.hit(hookX, hookY)) { caught obj; obj.setVisible(false); // 从场景里“摘下来” expanding false; retracting true; break; } } } if (retracting) { int speed RETRACT_SPEED; if (caught instanceof Stone) { speed - ((Stone) caught).getWeight(); // 钩到石头会变慢 } // 最慢也要 1 像素/帧否则石头可能完全拖不动 length - Math.max(1, speed); if (length 0) { if (caught ! null) { score caught.getValue(); caught null; } retracting false; expanding true; } } } private int currentHookX() { return HOOK_START_X (int) (length * Math.sin(angle)); } private int currentHookY() { return HOOK_START_Y - (int) (length * Math.cos(angle)); }抓取策略是最朴素的做法每帧检查钩爪端点是否碰到某个可见物体碰到就“勾住”。instanceof Stone在课程设计里能跑但它是设计上的坏味道如果想拿高分把减速系数做成 MineObject 的方法让子类返回不同的数值。例如父类getSpeedPenalty()返回 0Stone 重写返回 weightGold 不重写抓取逻辑统一调用caught.getSpeedPenalty()就不需要 instanceof 了。3.3 碰撞检测两种写法矩形相交还是圆心距离判断钩爪碰到物体常见两种几何判据。第一种是点是否在矩形内public boolean hit(int hookX, int hookY) { return hookX x hookX x width hookY y hookY y height; }第二种是把物体近似成圆public boolean hitByCircle(int hookX, int hookY) { int cx x width / 2; int cy y height / 2; int radius Math.max(width, height) / 2; int dx hookX - cx; int dy hookY - cy; return dx * dx dy * dy radius * radius; }注意这里没有用 Math.sqrt 开平方而是直接比较距离的平方r*r和dx*dxdy*dy都是整数避免了浮点运算和开方开销也顺带让这个函数在被高频调用时保持轻量。面试里问到“怎么判断一个点和一个圆的位置关系”这套代码就是标准回答。实战中矩形判断简单直接适合金块这类方方正正的对象圆心距离判断更接近直觉适合形状不规则的钻石或石头。要留意的是钩爪尖端是红点实际上也有半径更精确的是“圆对矩形”碰撞但课程设计不用做到那一步。黄金矿工原作的抓取区域其实适度放大了一点点给玩家一种“差不多就行”的宽容感避免差一像素勾不到的挫败。4. 分数、关卡和榜单把集合、排序与深拷贝用出价值4.1 计分模型和数据结构选择分数的类型用什么直觉是 int。早期关卡没问题单局几十万分也不会爆。但一旦做了无限关卡或者加入了“奶牛、多个大金块”之类的高价值物品int 上限 21 亿并不遥远。课程设计里被追问“为什么用 long 而不用 int”能答出“考虑溢出边界”就是加分项。private long score 0; // 用 long防御后期加了很多关卡后溢出 private long targetScore 1000; // 过关目标关卡里的物品列表用什么最常见的是 ArrayList 。需要随机读取、按顺序遍历ArrayList 最合适。面试常问的“ArrayList 和 LinkedList 区别”在游戏里观察很直观抓取时每帧都要遍历整个列表做碰撞检测ArrayList 的连续内存访问明显更快如果走到“删除被抓物品”这步直接用迭代器。数据量很小性能不是硬需求但能理解这层差异就不只是背八股文。Java 集合框架有个口语叫法叫“容器”面试官说“Java 容器有哪些”指的就是 Collection/List/Map 这一整套。这个项目里用到的 ArrayList 装物品、HashSet 记录按键、HashMap 可以用物品 id 映射对象都是容器的具体落地。对一个几百行的小游戏来说容器不是硬凑进去的概念而是每天都在调用的工具。4.2 分数排行榜手写冒泡排序和 Comparator 两条路都备好排行榜记录前 10 名。常见做法ScoreItem 实现 Comparable 提供降序比较ScoreBoard 维护列表。public class ScoreItem implements ComparableScoreItem { public final String name; public final int score; public ScoreItem(String name, int score) { this.name name; this.score score; } Override public int compareTo(ScoreItem o) { return Integer.compare(o.score, this.score); // 降序 } }import java.util.*; public class ScoreBoard { private final ListScoreItem list new ArrayList(); public void add(String name, int score) { list.add(new ScoreItem(name, score)); list.sort(Comparator.naturalOrder()); // 按 Comparable 降序排 if (list.size() 10) { list.remove(list.size() - 1); // 只保留前 10 } } public String display() { StringBuilder sb new StringBuilder(); for (int i 0; i list.size(); i) { sb.append(i 1).append(. ) .append(list.get(i).name) .append( ) .append(list.get(i).score) .append( 分\n); } return sb.toString(); } }这里有两个值得展开的点。第一个是比较器的写法不要写成return o.score - this.score;score 跨度大时减法会溢出Comparator 排序直接乱套。Integer.compare(a, b)内部是分支比较不会溢出。第二个是 StringBuilder 的用法循环里拼字符串如果用每次都会新建 String 对象排行榜一长就能感受到 GC 压力用 StringBuilder 的 append 是面试里最常被问到的字符串拼接题项目里正好用了。如果老师指定“手写冒泡排序”也要能写ScoreItem[] arr ...; for (int i 0; i arr.length - 1; i) { for (int j 0; j arr.length - 1 - i; j) { if (arr[j].score arr[j 1].score) { ScoreItem tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; } } }这段逻辑就是 O(n^2) 的双层循环内层每轮把最“轻”的往下沉所以叫冒泡。课程设计里真不必手写但不该手写不出来。用 Lombok 的人会写Data自动生成 equals/hashCode但课程设计里尽量少依赖这类注解手写 getter/setter 是基本功夫。4.3 深拷贝存档给玩家一个“后悔药”黄金矿工很容易误抓石头钩子出去一瞬间石头卡在路径上硬生生拖回来。现实中玩家最想要的是“撤销上一抓”。实现一个 Snapshot 类做存档恢复时把整个游戏对象列表还原回来就是玩家的后悔药。public class Snapshot { private ListMineObject objects; private long score; public Snapshot(GamePanel panel) { this.score panel.getScore(); this.objects new ArrayList(panel.getObjects().size()); for (MineObject obj : panel.getObjects()) { // 必须深拷贝不能直接把引用丢进来 objects.add(obj.copy()); } } public void restore(GamePanel panel) { panel.setScore(score); panel.setObjects(objects); } }这里唯一的坑是“深拷贝”。new ArrayList(oldList)只是把引用复制进新列表列表里的金块和石头对象还是同一批。如果旧关卡里一个金块 visiblefalse恢复后它仍然是 false因为改的是同一个对象。正确做法是让每个 MineObject 提供 copy()逐个 new 出新对象Stone 多一个 weightcopy 时也要带过去。这也是理解“对象拷贝”概念的完整过程。面试题“Java 对象深度拷贝怎么做”问的就是这几行浅拷贝 vs 深拷贝、Cloneable vs 手动 copy。课程设计里用手动 copy 最稳代码短、可控性强。用clone()反而容易踩坑因为 Object.clone() 默认是浅拷贝子类不重写就是错的。5. 避坑Java 小游戏五个典型翻车现场这一章集中写血泪经验以下每一条我都见过不止一个学生栽进去。5.1 画面不刷新或者一卡一卡的现象程序启动后窗口一片空白或者要拖一下窗口才刷新内容。原因忘在定时器回调里调用 repaint()。很多人以为改了坐标就会自动画上去实际上 Swing 的绘制是“请求制”更新完数据不请求重绘画面永远保持旧帧。另一个常见问题是把 Thread.sleep 写进了 paintComponent直接在绘制线程里睡界面直接卡死。解决游戏循环固定为 updateLogic repaint 两步耗时操作全部丢到后台线程绘制方法里只做绘制。Swing 的 JPanel 默认开了双缓冲不用自己再折腾 BufferStrategy我在项目里见过有人手动关掉双缓冲去“提升性能”结果闪屏到没法玩。5.2 钩爪速度越来越快抓一次像抽风现象游戏玩几秒后钩爪开始抽搐式加速完全不受控制。原因最常见的是在 keyPressed 里直接驱动钩子伸缩同时 Timer 的 actionPerformed 也驱动一次两套循环同时推进。或者在一个循环里既改了 length 又叠加了 EXTEND_SPEED忘了把上次的长度存下来。// 错误示范按键里直接改逻辑同时 Timer 也在改 public void keyPressed(KeyEvent e) { if (e.getKeyCode() KeyEvent.VK_SPACE) { length 10; // 这里动了Timer 里又动一次 } }解决只保留一个游戏循环。钩爪的方向、长度、速度全部由 Timer 回调统一更新按键事件只记录“哪个键被按下”逻辑每帧只算一次。单一数据源原则在这里能救命。5.3 抓东西的时候抛 ConcurrentModificationException现象钩爪碰到金块正要把金块从列表里移除控制台直接抛 ConcurrentModificationException游戏崩溃。原因在 for-each 遍历 objects 时调用了 objects.remove()。ArrayList 的迭代器会检查结构被修改的次数遍历过程中删除元素迭代器直接报错。解决不用 for-each改用迭代器删除IteratorMineObject it objects.iterator(); while (it.hasNext()) { MineObject obj it.next(); if (obj.isVisible() obj.hit(hookX, hookY)) { it.remove(); // 迭代器自己删安全 caught obj; break; } }或者更保守的做法遍历时只标记要删的对象遍历结束后统一 removeAll。后一种方案在处理“一次抓多个”的复杂规则时更可控。5.4 存档读档后分数对不上物品对不上现象点了“重新开始”上一局被抓过的金块还挂在场景里读完档石头和金块的位置错乱。原因十有八九是浅拷贝。把 panel 的 objects 直接赋值给新列表新旧列表共享同一个对象数组操作互相污染。前面 Snapshot 用深拷贝就是为了避开这个。解决所有需要独立保存的对象都实现 copy()Snapshot 里逐个复制。重新开始时不要试图清空现有 objects 里的元素而是直接panel.setObjects(new ArrayList())让所有旧引用失效避免其他地方还持有旧列表引用。5.5 钩子回收突然变慢或音效一放就卡顿现象游戏逻辑全对但只要播放一次 wav 音效动画就卡一下。原因在事件分发线程里做了文件 IO。Swing 的 Timer 回调运行在 EDT 上文件读取和音频解码都是阻塞操作画面自然被卡住。这属于典型的“黑匣子”看不到报错只看到卡顿很多人会去调音效本身实际要调的是线程。解决音效在新线程里播放或者用 AudioClip 预加载。常见课程设计方案是在初始化时把所以音效先 open 好运行时只调 start/stop如果音效文件多就建一个很小的线程池去加载。记住一条原则EDT 上只做轻量绘制和状态更新任何可能阻塞的操作都往外扔。6. 从能玩到能讲给这个 Java 项目加分的三个升级方向6.1 用状态机管住游戏流程如果你按 Java 学习路线走学完集合和多线程就该做个像样的东西收尾状态机就是把这个东西做完整的转折点。把游戏分成 READY、PLAYING、PAUSED、GAMEOVER 四个状态最简单的实现是枚举加判断public enum GameState { READY, PLAYING, PAUSED, GAMEOVER } private GameState state GameState.READY; Override public void actionPerformed(ActionEvent e) { if (state GameState.PLAYING) { updateLogic(); } repaint(); }按钮“开始”“暂停”“重开”都只是改 state游戏逻辑不会在暂停时偷偷跑。这是把“能用”变成“像个产品”的最廉价改法。6.2 存档用序列化还是 JSON课程设计图省事用 Java 自带的 ObjectOutputStream 足够try (ObjectOutputStream out new ObjectOutputStream( new FileOutputStream(save.dat))) { out.writeObject(snapshot); } catch (IOException e) { e.printStackTrace(); }别忘了让 MineObject、Gold、Stone 都实现 Serializable否则直接抛 NotSerializableException。以后再加字段会破坏旧存档所以学期中段就把存档格式定下来不要在答辩前一天加字段。6.3 面试时怎么讲这个项目我给学生排练的讲法是这样几句话带过关键词“这是一个 Swing 课程设计。每一帧用一个 Timer 驱动钩爪的运动拆成角度和长度两个状态抓取判定用点矩形碰撞。物品用抽象类建模金块和石头是子类新增物品不用改旧逻辑。排行榜用 Comparable 排序存档用深拷贝做快照。”然后准备好被追问Timer 为什么选 16ms为什么用深拷贝不用 cloneArrayList 和 LinkedList 在这个项目里谁更合适每一个问题都能从前面的代码里找到答案这样的项目讲出来才立得住。这个项目我自己带过多轮学生翻车最多的地方往往不是算法而是最基础的工程习惯JDK 环境变量没配好、类名和文件名不一致、编译完不会打 jar。把这些基本功补上这个几百行的项目足够支撑你走完课程设计和第一轮 Java 面试。希望帮到你。本文还有配套的精品资源点击获取