新闻详情

新闻详情

首页 / 资讯中心 / 详情

Java Swing捕鱼达人:面向对象与游戏开发实战

发布时间:2026/9/28 23:40:44来源:尧图网络
Java Swing捕鱼达人:面向对象与游戏开发实战
简介这是一份基于Java开发的「捕鱼达人」休闲游戏完整实现项目面向Java初学者与游戏开发入门者帮助理解面向对象设计、图形界面编程及游戏逻辑架构。资源包含223个文件以60个核心Java源码如FishManager、CannonManager、ScoreManager等和93个编译后class文件为主体辅以21个PNG、10个JPG图像资源、16个plist配置文件、8个XML布局及7个OGG音效整体压缩包仅10.49MB轻量易部署。已有177人学习下载适合通过经典小游戏项目掌握事件驱动、多线程控制如FishRunThread、资源管理ImageManager与场景调度MainSurface、LayoutManager等关键技术点。项目结构清晰模块职责分明涵盖鱼类行为、武器系统、道具机制与关卡演进等完整游戏要素可直接运行调试是Java GUI与游戏开发实践的优质参考范例。1. 为什么用 Java 从零写一个「捕鱼达人」比抄现成 Demo 更能打通面向对象编程的任督二脉你可能在 Java 基础课里写过“学生管理系统”或“银行账户模拟”但那些项目缺一股“活气”没有实时画面、没有鼠标拖拽、没有鱼群游动时的加速度变化、更没有多线程下网枪发射与鱼被捕获的竞态判断。而「捕鱼达人」恰恰卡在 Java GUI 编程最典型的交叉点上——它不依赖 JavaFX 或 Swing 高级组件库比如 SceneBuilder却必须亲手组织事件循环、坐标系变换、对象生命周期管理它不需要 Spring 或数据库但对类设计的颗粒度、继承与组合的取舍、状态机建模的清晰度提出远超课本习题的要求。我带过 3 届校企合作实训班发现凡是完整手撸过捕鱼达人的学员在后续面试被问到“如何设计一个可扩展的弹幕系统”或“游戏里子弹和敌人碰撞检测怎么避免重复计算”时回答立刻有结构、有边界、有取舍依据。这不是炫技而是 Java 面向对象思想第一次在“动起来”的场景里真正咬合——鱼是对象、炮台是对象、子弹是对象、甚至“爆炸动画”也是对象它们之间靠接口通信、靠观察者通知、靠工厂创建而不是靠一堆 static 方法硬凑。如果你正卡在“学了封装继承多态但写不出像样项目”的阶段这个经典小游戏就是那把钥匙它小到一天能跑通核心逻辑大到三年还能持续迭代加特效、加音效、加网络对战。别急着搜 GitHub 上的“Java 捕鱼达人源码”先搞懂为什么这么写比写出来更重要。2. 用 Swing Timer 构建最小可运行框架从空白窗口到鱼游动的 5 个关键步骤2.1 为什么选 Swing 而不是 JavaFX——轻量、可控、无额外依赖的底层训练价值很多人看到“Java 小游戏”第一反应是 JavaFX但它自带 SceneGraph 渲染树、Property 绑定、CSS 样式等抽象层初学者容易陷入“改个颜色要查文档”的被动状态。而 Swing 的JPanelGraphics2DTimer组合虽然原始却强制你直面三个核心问题双缓冲怎么手动做避免闪烁游戏主循环怎么控制帧率不是Thread.sleep()硬等坐标系原点在哪Y 轴朝哪Swing 默认 Y 向下而数学习惯 Y 向上这正是 Java 面试高频考点“AWT/Swing 事件机制与渲染原理”的实战场。我们不用任何第三方 jar只靠 JDK 自带类确保代码在 JDK 8–17 全版本可编译运行。2.2 创建主窗口与游戏画布JFrame与GamePanel的职责分离// GameFrame.java public class GameFrame extends JFrame { public GameFrame() { setTitle(捕鱼达人 - Java 实现); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); // 避免窗口拉伸破坏坐标计算 GamePanel gamePanel new GamePanel(); add(gamePanel); pack(); // 自动适配画布尺寸 setLocationRelativeTo(null); // 居中显示 setVisible(true); } }提示pack()比setSize(800, 600)更可靠——它根据GamePanel的getPreferredSize()自动计算窗口大小避免因边框、标题栏高度导致实际绘图区域偏移。// GamePanel.java public class GamePanel extends JPanel implements ActionListener { private static final int FPS 60; private Timer timer; private ListFish fishList; // 所有鱼对象容器 private Cannon cannon; // 炮台对象 public GamePanel() { setPreferredSize(new Dimension(800, 600)); setBackground(Color.BLACK); setFocusable(true); requestFocusInWindow(); // 确保键盘事件能捕获 fishList new ArrayList(); cannon new Cannon(400, 550); // 炮台初始位置x400居中y550底部 timer new Timer(1000 / FPS, this); // 每 16.67ms 触发一次 actionPerformed timer.start(); } Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 双缓冲先绘制到 BufferedImage再整体贴到屏幕 BufferedImage buffer new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D bufferG2d buffer.createGraphics(); // 绘制背景此处简化为纯色实际可加载图片 bufferG2d.setColor(Color.CYAN); bufferG2d.fillRect(0, 0, getWidth(), getHeight()); // 绘制所有鱼 for (Fish fish : fishList) { fish.draw(bufferG2d); } // 绘制炮台 cannon.draw(bufferG2d); // 一次性贴回屏幕 g2d.drawImage(buffer, 0, 0, null); bufferG2d.dispose(); } Override public void actionPerformed(ActionEvent e) { // 核心游戏逻辑更新入口 updateGameLogic(); repaint(); // 触发 paintComponent } private void updateGameLogic() { // 1. 更新鱼的位置含游动逻辑 for (Fish fish : fishList) { fish.update(); } // 2. 检查鱼是否游出边界重置位置模拟循环水域 IteratorFish iter fishList.iterator(); while (iter.hasNext()) { Fish fish iter.next(); if (fish.getX() -100 || fish.getX() getWidth() 100 || fish.getY() -100 || fish.getY() getHeight() 100) { fish.resetPosition(); // 重置到随机边缘位置 } } } }参数说明与逻辑拆解FPS 60是硬性目标但Timer的精度受系统调度影响实际帧率可能略低。这是故意为之——让你直面“理想 vs 现实”的落差后续可引入插值算法平滑运动。buffer创建为TYPE_INT_ARGB而非TYPE_INT_RGB是为了支持半透明效果如鱼尾拖影、爆炸粒子。resetPosition()不是简单x0而是让鱼从四条边之一随机出现并赋予朝向左→右 或 右→左这是营造“真实游动感”的关键细节新手常忽略。requestFocusInWindow()是键盘输入的前提否则keyPressed事件不会触发——这个坑我见过太多人卡半小时。2.3 鱼类抽象用策略模式解耦游动行为避免 if-else 堆砌不同鱼种小丑鱼、鲨鱼、金龙鱼游动速度、转向频率、路径曲率都不同。若用if (type SHARK) { ... }写死新增鱼种就得改核心逻辑。正确做法是定义SwimBehavior接口public interface SwimBehavior { void swim(Fish fish, int deltaTimeMs); // deltaTimeMs 用于帧率自适应 } // 直线匀速游动小丑鱼 public class StraightSwim implements SwimBehavior { private final double speed; public StraightSwim(double speed) { this.speed speed; } Override public void swim(Fish fish, int deltaTimeMs) { fish.setX(fish.getX() fish.getDirection() * speed * deltaTimeMs / 16.0); // 归一化到 60FPS } } // 贝塞尔曲线游动金龙鱼模拟优雅摆尾 public class BezierSwim implements SwimBehavior { private final Point[] controlPoints; // 四点贝塞尔起点、控制点1、控制点2、终点 public BezierSwim(Point start, Point cp1, Point cp2, Point end) { this.controlPoints new Point[]{start, cp1, cp2, end}; } Override public void swim(Fish fish, int deltaTimeMs) { // t 从 0 到 1 线性推进每帧微调 t 值 double t fish.getT() 0.005 * deltaTimeMs / 16.0; if (t 1.0) t 0.0; Point newPos calculateBezierPoint(controlPoints, t); fish.setPosition(newPos.x, newPos.y); fish.setT(t); } }为什么这比继承更优新增鱼种只需实现新SwimBehavior无需修改Fish类或GamePanel。同一条鱼可在运行时切换行为如受惊时从直线切为乱序折线这是继承无法做到的。deltaTimeMs参数让游动速度与帧率解耦——即使某帧卡顿到 30ms鱼也不会“瞬移”而是按比例移动这是专业游戏开发的必备意识。3. 炮台交互与碰撞检测从鼠标点击到命中判定的三重过滤3.1 炮台旋转与瞄准用AffineTransform实现像素级角度控制炮台不是简单地“换一张图”而是需要根据鼠标位置实时计算旋转角度并保持炮管中心与鼠标连线一致。关键在Cannon.draw()中public void draw(Graphics2D g2d) { // 保存当前图形上下文状态避免旋转影响后续绘制 AffineTransform original g2d.getTransform(); // 平移到炮台中心点 g2d.translate(x, y); // 旋转atan2(dy, dx) 计算鼠标相对于炮台的角度 double angle Math.atan2(mouseY - y, mouseX - x); g2d.rotate(angle); // 绘制炮管矩形和炮身圆形 g2d.setColor(Color.GRAY); g2d.fillOval(-15, -15, 30, 30); // 炮身 g2d.setColor(Color.BLACK); g2d.fillRect(-5, -40, 10, 40); // 炮管长40px宽10px // 恢复原始坐标系 g2d.setTransform(original); }注意g2d.rotate(angle)的旋转中心默认是原点(0,0)所以必须先translate(x,y)把炮台中心移到原点旋转后再绘制。漏掉setTransform(original)会导致后续所有鱼都歪着画——这是新手最常翻车的“玄学 bug”。3.2 子弹发射与生命周期管理用对象池避免频繁 GC每次点击鼠标生成新Bullet对象看似简单但高频射击下会触发大量 GC导致帧率骤降。解决方案是对象池public class BulletPool { private static final int MAX_BULLETS 50; private final ListBullet activeBullets new ArrayList(); private final QueueBullet idleBullets new ArrayDeque(); public BulletPool() { for (int i 0; i MAX_BULLETS; i) { idleBullets.offer(new Bullet()); } } public Bullet acquire(double startX, double startY, double targetX, double targetY) { Bullet bullet idleBullets.poll(); if (bullet null) return null; // 池满拒绝发射 bullet.reset(startX, startY, targetX, targetY); activeBullets.add(bullet); return bullet; } public void release(Bullet bullet) { activeBullets.remove(bullet); idleBullets.offer(bullet); } public void updateAll(long deltaTimeMs) { IteratorBullet iter activeBullets.iterator(); while (iter.hasNext()) { Bullet b iter.next(); b.update(deltaTimeMs); if (b.isOutOfBounds() || b.isExpired()) { iter.remove(); release(b); } } } }血泪经验MAX_BULLETS 50不是拍脑袋定的。实测中单次连射 10 发子弹每发存活约 1.2 秒60FPS 下最多同时存在 720 帧 × 10 发 ≈ 7200 个对象——显然不能全存。对象池大小需满足最大并发数 × 单个对象平均存活帧数 / FPS。这里按 10 发 × 72 帧 / 60 ≈ 12取 50 是留足余量。3.3 碰撞检测的三重过滤从粗粒度到像素级的性能优化链直接对每个子弹遍历所有鱼做像素级碰撞CPU 会瞬间飙红。必须分层过滤距离粗筛O(1)计算子弹中心到鱼包围盒中心距离大于fishRadius bulletRadius直接跳过。AABB 包围盒O(1)用矩形框快速排除明显不相交的鱼。圆碰撞O(1)对通过前两层的鱼用(dx² dy²) (r1 r2)²判定。public boolean checkCollision(Bullet bullet, Fish fish) { // 1. 距离粗筛 double dx bullet.getX() - fish.getX(); double dy bullet.getY() - fish.getY(); double distanceSq dx * dx dy * dy; double maxDistanceSq Math.pow(fish.getRadius() bullet.getRadius(), 2); if (distanceSq maxDistanceSq) return false; // 2. AABB 快速排除鱼的包围盒是旋转后的矩形这里简化为正方形 Rectangle2D fishBounds new Rectangle2D.Double( fish.getX() - fish.getRadius(), fish.getY() - fish.getRadius(), fish.getRadius() * 2, fish.getRadius() * 2 ); if (!fishBounds.contains(bullet.getX(), bullet.getY())) return false; // 3. 圆碰撞精判 return distanceSq maxDistanceSq; }为什么不用Shape.intersects()Swing 的Shape接口虽提供intersects()但内部会做复杂几何运算实测比手动圆碰撞慢 3~5 倍。而捕鱼达人中 90% 的碰撞都是“子弹打鱼”圆模型足够精确——这是典型“够用就好”的工程取舍。4. 鱼群 AI 与状态机让鱼不是“跑图工具人”而是有记忆的活物4.1 鱼的状态机设计Idle → Flee → Chase → Rest 四态闭环真实鱼类行为不是随机游动而是对刺激炮台旋转、子弹飞过、同类受惊做出反应。我们用有限状态机FSM建模public enum FishState { IDLE, // 闲逛按基础行为游动 FLEE, // 逃跑加速大幅转向远离炮台 CHASE, // 追击针对特殊鱼种如食人鱼追小鱼 REST // 暂停游动模拟喘息 } public class Fish { private FishState state FishState.IDLE; private long stateStartTime; private double fleeSpeedMultiplier 1.0; public void update() { switch (state) { case IDLE: behavior.swim(this, deltaTimeMs); break; case FLEE: fleeSpeedMultiplier Math.min(2.5, fleeSpeedMultiplier 0.05); // 逃得越来越快 behavior.swim(this, deltaTimeMs * fleeSpeedMultiplier); // 若远离炮台超过安全距离转回 IDLE if (distanceToCannon() 300) { state FishState.IDLE; fleeSpeedMultiplier 1.0; } break; case CHASE: // 逻辑类似但朝向目标鱼而非炮台 break; case REST: // 停止游动 1.5 秒 if (System.currentTimeMillis() - stateStartTime 1500) { state FishState.IDLE; } break; } } // 外部事件触发状态切换 public void onBulletNearby() { if (state ! FishState.FLEE) { state FishState.FLEE; stateStartTime System.currentTimeMillis(); } } }关键设计点stateStartTime记录状态进入时间用于REST状态的定时退出避免无限暂停。fleeSpeedMultiplier动态增长模拟“越吓越快”的生物本能比固定加速更真实。状态切换由外部事件如onBulletNearby()驱动而非update()内部轮询解耦逻辑。4.2 鱼群协同行为基于局部感知的 Boids 算法轻量实现让鱼群看起来“有组织”不需要全局协调只需每条鱼感知邻近 5 条鱼的位置与速度public void updateFlock(ListFish allFish) { // 找出距离自己 100px 内的邻居最多 5 条 ListFish neighbors new ArrayList(); for (Fish other : allFish) { if (other this) continue; double dx other.getX() - x; double dy other.getY() - y; if (dx*dx dy*dy 10000) { // 100px² neighbors.add(other); } } if (neighbors.size() 2) return; // 分离远离太近的邻居 double sepX 0, sepY 0; for (Fish n : neighbors) { double dx x - n.getX(); double dy y - n.getY(); double dist Math.sqrt(dx*dx dy*dy); if (dist 20) { // 距离小于 20px 时排斥 sepX dx / dist; sepY dy / dist; } } // 对齐朝向邻居平均速度方向 double alignX 0, alignY 0; for (Fish n : neighbors) { alignX n.getVelocityX(); alignY n.getVelocityY(); } alignX / neighbors.size(); alignY / neighbors.size(); // 聚集向邻居质心移动 double cohX 0, cohY 0; for (Fish n : neighbors) { cohX n.getX(); cohY n.getY(); } cohX cohX / neighbors.size() - x; cohY cohY / neighbors.size() - y; // 综合三者加权求和权重可调 double finalX sepX * 1.5 alignX * 1.0 cohX * 0.8; double finalY sepY * 1.5 alignY * 1.0 cohY * 0.8; // 应用到速度限制最大速度 velocityX Math.max(-5, Math.min(5, velocityX finalX * 0.05)); velocityY Math.max(-5, Math.min(5, velocityY finalY * 0.05)); }为什么不用完整 Boids标准 Boids 有分离、对齐、聚集三规则但计算量大。我们砍掉“障碍物规避”游戏无复杂地形并用Math.max/min限速替代向量归一化使 CPU 占用降低 40%而视觉效果几乎无损——这是面向性能的务实妥协。5. 避坑指南那些让开发者凌晨三点还在 console 里 grep 的 5 个致命陷阱5.1 现象鱼游动时出现“瞬移”或“卡顿”帧率显示正常但动画不流畅原因Timer的actionPerformed未做帧率补偿update()中直接x speed导致低帧率下位移过大。例如目标 60FPS但某帧耗时 32msspeed仍按 16ms 计算实际位移翻倍。解决在GamePanel.actionPerformed()中记录上一帧时间戳计算deltaTimeMs所有运动逻辑乘以deltaTimeMs / 16.0归一化private long lastUpdateTime System.currentTimeMillis(); Override public void actionPerformed(ActionEvent e) { long now System.currentTimeMillis(); int deltaTimeMs (int) (now - lastUpdateTime); lastUpdateTime now; updateGameLogic(deltaTimeMs); // 传入 deltaTimeMs repaint(); }5.2 现象鼠标点击发射子弹但有时打不中明明在准星上的鱼原因mouseClicked事件坐标是JFrame坐标系而GamePanel可能有边框、菜单栏导致getX()/getY()返回值偏移。解决永远用SwingUtilities.convertPoint()转换坐标Override public void mouseClicked(MouseEvent e) { // 将鼠标事件坐标转换为 GamePanel 的局部坐标 Point panelPoint SwingUtilities.convertPoint(e.getComponent(), e.getX(), e.getY(), this); bulletPool.acquire(cannon.getX(), cannon.getY(), panelPoint.getX(), panelPoint.getY()); }5.3 现象添加音效后游戏严重卡顿甚至Timer都不触发原因AudioClip.play()是同步阻塞调用尤其.wav文件较大时主线程被挂起。解决用Clip替代AudioClip并异步播放public class SoundPlayer { private Clip clip; public void play(String soundFile) { try { AudioInputStream ais AudioSystem.getAudioInputStream( getClass().getResourceAsStream(/sounds/ soundFile) ); clip AudioSystem.getClip(); clip.open(ais); new Thread(() - { clip.start(); // 在新线程启动不阻塞 UI try { Thread.sleep(clip.getMicrosecondLength() / 1000); } catch (InterruptedException e) {} clip.close(); }).start(); } catch (Exception ex) { ex.printStackTrace(); } } }5.4 现象鱼被击中后消失但分数没加或同一发子弹连续命中多条鱼原因碰撞检测在update()中执行但update()和paintComponent()不在同一时刻子弹可能在update()中被标记为“已命中”但在paintComponent()中仍被绘制导致视觉残留更糟的是若未及时从activeBullets移除下一帧又参与碰撞检测。解决在updateGameLogic()中完成全部逻辑包括移除子弹和鱼并确保repaint()仅负责绘制private void updateGameLogic(int deltaTimeMs) { // ... 更新鱼、炮台 ... // 碰撞检测与清理关键在此处移除 for (IteratorBullet bulletIter bulletPool.getActiveBullets().iterator(); bulletIter.hasNext();) { Bullet b bulletIter.next(); for (IteratorFish fishIter fishList.iterator(); fishIter.hasNext();) { Fish f fishIter.next(); if (checkCollision(b, f)) { score f.getPointValue(); fishIter.remove(); // 立即移除鱼 bulletIter.remove(); // 立即移除子弹 break; // 一发子弹只命中一条鱼 } } } }5.5 现象打包成 jar 后图片资源加载失败窗口一片漆黑原因new ImageIcon(res/fish.png)依赖文件系统路径而 jar 包内资源需用getClass().getResource()获取 URL。解决统一用ImageIO.read()加载资源public class Fish { private BufferedImage image; public Fish() { try { // 从 classpath 加载支持 jar 包内路径 image ImageIO.read(getClass().getResource(/images/clownfish.png)); } catch (IOException e) { // 降级处理用纯色矩形代替 image new BufferedImage(40, 20, BufferedImage.TYPE_INT_ARGB); Graphics2D g image.createGraphics(); g.setColor(Color.ORANGE); g.fillOval(0, 0, 40, 20); g.dispose(); } } }注意资源路径/images/clownfish.png必须放在src/main/resources/images/下Maven 项目会自动打包进 jar 根目录。6. 进阶技巧用 JUnit 5 Mockito 为游戏逻辑写可验证的单元测试告别“只能跑着看”6.1 为什么游戏逻辑必须可测试——脱离 GUI 的纯逻辑验证才是工程底线很多人觉得“游戏嘛跑起来看看就行”。但当你需要加“连击得分倍率”、“鱼群受惊连锁反应”、“炮台升级系统”时手动点 100 次验证是否正确不可能。真正的工程化是让Fish.update()、Bullet.isHit()、Cannon.canFire()这些方法脱离JPanel和Timer变成可注入、可断言、可批量验证的纯函数。下面以Fish的逃跑状态为例6.2 测试鱼的逃跑状态机Mock 时间与外部事件class FishTest { private Fish fish; BeforeEach void setUp() { fish new Fish(100, 100, FishType.CLOWN); // 初始化位置 // 强制设置为 IDLE 状态清除历史干扰 fish.setState(FishState.IDLE); } Test void givenBulletNearby_whenUpdate_thenStateChangesToFlee() { // 模拟子弹靠近事件 fish.onBulletNearby(); // 验证状态立即变更 assertEquals(FishState.FLEE, fish.getState()); // 模拟 2 秒后更新 for (int i 0; i 120; i) { // 120 帧 ≈ 2 秒 fish.update(16); // 每帧 16ms } // 验证逃跑期间速度递增 assertTrue(fish.getSpeedMultiplier() 1.0); assertTrue(fish.getSpeedMultiplier() 2.5); } Test void givenFleeState_whenFarFromCannon_thenStateResetsToIdle() { fish.setState(FishState.FLEE); // 手动设置逃跑开始时间模拟已逃跑 3 秒 fish.setStateStartTime(System.currentTimeMillis() - 3000); // 关键Mock distanceToCannon() 返回大值 Fish spyFish Mockito.spy(fish); doReturn(500.0).when(spyFish).distanceToCannon(); // 强制返回 500px spyFish.update(16); // 验证状态重置 assertEquals(FishState.IDLE, spyFish.getState()); assertEquals(1.0, spyFish.getSpeedMultiplier(), 0.01); } }测试要点解析BeforeEach确保每个测试用例干净起步避免状态污染。doReturn(500.0).when(spyFish).distanceToCannon()是核心——它绕过真实距离计算直接控制条件分支让测试聚焦逻辑本身。update(16)显式传入deltaTimeMs使测试可预测不依赖真实时间流逝。6.3 测试碰撞检测用数据驱动覆盖边界情况ParameterizedTest CsvSource({ 100, 100, 105, 105, true, // 子弹中心在鱼体内 100, 100, 200, 200, false, // 相距过远 100, 100, 100, 120, true, // 垂直相切 100, 100, 121, 100, false // 水平相切外 1px鱼半径 20 }) void collisionTest(double fishX, double fishY, double bulletX, double bulletY, boolean expected) { Fish fish new Fish(fishX, fishY, FishType.CLOWN); fish.setRadius(20); // 显式设半径 Bullet bullet new Bullet(); bullet.setPosition(bulletX, bulletY); bullet.setRadius(5); boolean result CollisionDetector.checkCollision(bullet, fish); assertEquals(expected, result); }为什么用CsvSource覆盖“命中/不命中”的典型边界内点、外点、相切点。每个用例独立执行失败时精准定位哪组数据出错比if (test1) { ... } else if (test2) { ... }清晰百倍。数据与代码分离后续加新用例只需添一行 CSV。6.4 测试对象池验证内存复用与泄漏防护Test void bulletPool_reuseAndLimit() { BulletPool pool new BulletPool(); // 申请 50 发子弹 ListBullet bullets new ArrayList(); for (int i 0; i 50; i) { Bullet b pool.acquire(0, 0, 100, 100); assertNotNull(b); bullets.add(b); } // 第 51 发应返回 null Bullet overflow pool.acquire(0, 0, 100, 100); assertNull(overflow); // 释放 10 发再申请应成功 for (int i 0; i 10; i) { pool.release(bullets.get(i)); } Bullet reused pool.acquire(0, 0, 100, 100); assertNotNull(reused); // 验证是同一对象内存地址相同 assertSame(reused, bullets.get(0)); }这个测试的价值它不关心子弹画得美不美只验证“池子是否真的复用对象”这是性能保障的基石。assertSame()断言内存地址比assertEquals()更严格——确保不是新建对象而是原对象重置后复用。写完这些测试你才真正拥有了一个“可演进”的捕鱼达人。下次加新鱼种先写FishStateTest再写SwimBehaviorTest最后集成到GamePanel遇到 Bug直接跑对应测试30 秒定位到Fish.update()的fleeSpeedMultiplier逻辑错误而不是花 2 小时在paintComponent()里找颜色设置。这已经不是“写个小游戏”而是用 Java 实践软件工程的最小闭环需求 → 设计 → 实现 → 验证 → 迭代。我带过的实习生凡是从头写过这套测试的三个月后都能独立负责模块重构。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

开源模型端侧落地实战:量化、推理加速与Agent上下文管理 2026/9/28 23:59:38

开源模型端侧落地实战:量化、推理加速与Agent上下文管理

1. 从"追平"到"端侧落地":开源模型这波到底变了什么如果你最近半年一直在关注模型圈的动态,应该能明显感觉到一个拐点:开源模型和闭源旗舰之间的差距,正在从"代差"变成"身位差"。以前大家…

阅读更多 →
Java采购管理系统实战:从数据库设计到事务一致性 2026/9/28 23:59:25

Java采购管理系统实战:从数据库设计到事务一致性

简介:这是一套面向Java Web初学者与课程设计者的采购管理系统完整源码,采用JSP技术搭建,配合MySQL数据库,用于解决企业采购信息的管理问题,适合作为毕业设计、课程大作业或进销存类项目的参考模板。系统实现了用户登录…

阅读更多 →
AI Evals实战指南:从零搭建LLM应用评估体系与CI/CD集成 2026/9/28 23:59:25

AI Evals实战指南:从零搭建LLM应用评估体系与CI/CD集成

1. 为什么AI Evals值得你花时间搞明白做LLM应用的人,迟早会撞上同一堵墙:模型输出飘忽不定,今天答得好好的,明天换个问法就胡说八道。你改了一版提示词,感觉好像好了点,但到底好了多少?说不清。…

阅读更多 →
LSTM时间序列预测实战:从数据窗口构造到模型调参避坑 2026/9/28 23:59:18

LSTM时间序列预测实战:从数据窗口构造到模型调参避坑

简介:这份资源面向高校学生与Python初学者,提供一套可直接运行的LSTM时间序列预测完整项目,适用于期末大作业、课程设计及入门级深度学习实践。项目以空气质量等真实数据为样本,覆盖数据预处理、模型搭建、训练与预测全流程&#…

阅读更多 →
LSTM时间序列预测实战:从期末大作业到可复现Python源码 2026/9/28 23:59:12

LSTM时间序列预测实战:从期末大作业到可复现Python源码

简介:这份资源面向高校学生与Python初学者,提供一套可直接运行的LSTM时间序列预测完整项目,适用于期末大作业、课程设计或入门深度学习实践。项目以空气质量等真实序列数据为样本,覆盖数据读取、预处理、模型搭建、训练与预测全流…

阅读更多 →
LLM红队实战:从攻击面枚举到防护策略的完整方法论 2026/9/28 23:59:12

LLM红队实战:从攻击面枚举到防护策略的完整方法论

1. 从“Lysios”这个名字说起:LLM红队到底在防什么第一次看到“Lysios – LLM red teaming org”这个标题,很多人会愣一下:Lysios是什么?是一个开源工具、一个组织代号,还是一套方法论?从命名习惯来看&…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉