尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Java实战:从零构建双色球系统,掌握集合框架与OOP设计

Java实战:从零构建双色球系统,掌握集合框架与OOP设计 1. 项目缘起为什么用Java写一个双色球系统如果你是一个Java开发者或者正在学习Java你可能已经写过不少“学生管理系统”、“图书管理系统”这类经典的练手项目。这些项目固然能巩固基础但说实话做多了难免有些枯燥缺乏一点“玩”的趣味性。今天我们来点不一样的——用Java实现一个完整的“双色球系统”。这不仅仅是一个简单的随机数生成器。一个完整的双色球系统麻雀虽小五脏俱全。它需要模拟现实中的双色球玩法从红球池1-33中随机选出6个不重复的号码再从蓝球池1-16中随机选出1个号码组合成一注有效的彩票。然后它还需要具备模拟开奖、对比用户投注与开奖结果、根据规则自动计算中奖等级和奖金等一系列逻辑。整个过程涉及到Java核心的多个知识点集合框架特别是List、Set的高效运用、随机数生成与控制、算法的逻辑判断、面向对象的设计思想甚至是简单的输入输出交互。对于初学者这是一个绝佳的综合性练习能将散落的知识点串联起来对于有经验的开发者则可以借此机会思考如何优化代码结构、处理边界条件写出更健壮、更优雅的代码。网络上关于Java实现双色球的代码片段很多但往往只解决了“生成一注号码”这个单一问题缺乏系统性的设计和完整的流程闭环。我们这次就从头到尾构建一个可运行、可交互、逻辑清晰的双色球模拟系统。2. 核心需求分析与系统设计在动手敲代码之前我们必须先把需求理清楚并设计好系统的骨架。盲目开始很容易导致代码结构混乱后期难以维护和扩展。2.1 功能需求拆解一个最小可用的双色球系统应该包含以下核心功能用户投注允许用户手动输入或机选生成一注或多注双色球号码。每注号码必须符合规则6个红球1-33不重复1个蓝球1-16。模拟开奖系统随机生成一注号码作为当期的开奖号码。同样需符合上述规则。兑奖逻辑将用户投注的每一注号码与开奖号码进行比对根据官方中奖规则确定中奖等级。一等奖6红1蓝二等奖6红0蓝三等奖5红1蓝四等奖5红0蓝 或 4红1蓝五等奖4红0蓝 或 3红1蓝六等奖2红1蓝 或 1红1蓝 或 0红1蓝结果显示清晰展示开奖号码、用户投注号码、每注的中奖情况以及总计中奖金额可预设各等级奖金。2.2 面向对象设计OOP思路用面向对象的思想来建模会让代码更清晰。我们可以设计以下几个核心类LotteryTicket(彩票类)代表一注双色球彩票。属性包括一个SetInteger类型的红球集合用Set天然保证不重复和一个Integer类型的蓝球。它应该提供验证号码有效性的方法。DrawResult(开奖结果类)本质上也是一注彩票但为了概念清晰可以单独作为一个类继承或包含一个LotteryTicket对象并附加开奖期号等信息。LotterySystem(核心系统类)这是大脑。它负责生成随机有效的彩票机选。执行开奖动作产生DrawResult。持有DrawResult和用户购买的ListLotteryTicket。实现核心的兑奖算法遍历每一注彩票计算其中奖等级。AwardRules(奖金规则类)可以是一个枚举Enum或配置类定义各个中奖等级对应的奖金数额。这样便于修改而不需要硬编码在逻辑里。2.3 数据结构选型为什么用Set和List这是本项目的一个关键点。红球要求不重复HashSetInteger是最佳选择因为它基于哈希表add操作可以自动去重并且判断某个数字是否存在于集合中的效率contains方法是O(1)级别这对于后续兑奖时比对号码非常高效。用户可能购买多注彩票所以用一个ListLotteryTicket来存储是合适的。开奖号码虽然只有一注但为了与用户彩票比对也需要以类似形式红球Set蓝球int存储。注意HashSet是无序的。如果你希望机选的红球号码按从小到大排序输出更直观可以在生成后将其转存到一个ArrayList中然后调用Collections.sort()进行排序。但核心存储和比对时仍使用Set以保证高效和去重。3. 关键实现步骤与代码详解下面我们分模块实现这个系统。我会先给出代码片段然后解释其背后的意图和注意事项。3.1 定义核心数据模型LotteryTicket类import java.util.HashSet; import java.util.Set; import java.util.List; import java.util.ArrayList; import java.util.Collections; public class LotteryTicket { private SetInteger redBalls; // 使用Set确保红球不重复 private int blueBall; // 通过集合构造一注彩票 public LotteryTicket(SetInteger redBalls, int blueBall) { // 防御性拷贝避免外部修改影响内部数据 this.redBalls new HashSet(redBalls); this.blueBall blueBall; if (!isValid()) { throw new IllegalArgumentException(彩票号码无效红球必须是1-33的6个不重复数字蓝球必须是1-16。); } } // 便捷构造方法传入列表内部转为Set public LotteryTicket(ListInteger redBallList, int blueBall) { this(new HashSet(redBallList), blueBall); } // 验证彩票有效性 private boolean isValid() { if (redBalls null || redBalls.size() ! 6) { return false; } if (blueBall 1 || blueBall 16) { return false; } for (int ball : redBalls) { if (ball 1 || ball 33) { return false; } } return true; } // 获取排序后的红球列表用于展示 public ListInteger getSortedRedBalls() { ListInteger sortedList new ArrayList(redBalls); Collections.sort(sortedList); return sortedList; } public SetInteger getRedBalls() { // 返回不可修改的视图或拷贝保护数据 return new HashSet(redBalls); } public int getBlueBall() { return blueBall; } Override public String toString() { return 红球 getSortedRedBalls() 蓝球[ blueBall ]; } }代码解读与踩坑点数据封装与验证在构造方法中直接进行有效性验证isValid()一旦传入非法数据立即抛出异常。这是一种“快速失败”原则便于在创建对象时就发现问题而不是等到后续兑奖时才出错。防御性编程注意构造方法中的new HashSet(redBalls)。如果直接this.redBalls redBalls那么外部对传入的Set的修改会直接影响彩票对象这是非常危险的。这里进行了一次拷贝隔离了内外数据。getSortedRedBalls()方法这是一个很好的设计。内部存储用HashSet追求效率对外展示时提供一个排序好的List满足用户查看习惯。注意这里每次调用都会生成新的ArrayList并排序如果展示调用非常频繁可以考虑缓存排序结果但鉴于一注彩票只有6个数字这个开销可以忽略。3.2 实现机选与开奖随机数的艺术随机生成符合规则的号码是本项目的核心。我们需要一个工具方法。import java.util.HashSet; import java.util.Random; import java.util.Set; public class LotteryUtils { private static final Random RANDOM new Random(); private static final int RED_BALL_POOL_SIZE 33; private static final int RED_BALLS_TO_DRAW 6; private static final int BLUE_BALL_POOL_SIZE 16; // 生成一注随机彩票 public static LotteryTicket generateRandomTicket() { SetInteger redBalls new HashSet(); // 循环直到抽满6个不重复的红球 while (redBalls.size() RED_BALLS_TO_DRAW) { int ball RANDOM.nextInt(RED_BALL_POOL_SIZE) 1; // 生成 [1, 33] 的随机数 redBalls.add(ball); } int blueBall RANDOM.nextInt(BLUE_BALL_POOL_SIZE) 1; // 生成 [1, 16] 的随机数 return new LotteryTicket(redBalls, blueBall); } // 也可以单独生成红球或蓝球集合 public static SetInteger generateRedBalls() { SetInteger redBalls new HashSet(); while (redBalls.size() RED_BALLS_TO_DRAW) { redBalls.add(RANDOM.nextInt(RED_BALL_POOL_SIZE) 1); } return redBalls; } public static int generateBlueBall() { return RANDOM.nextInt(BLUE_BALL_POOL_SIZE) 1; } }代码解读与踩坑点Random对象复用将Random对象声明为静态常量private static final Random RANDOM new Random();是推荐做法。如果每次生成都new Random()在短时间内连续调用可能会因为种子相同而导致生成的随机数序列高度相似。while循环的必然性由于使用HashSetadd重复数字时会失败返回false但集合大小不变所以循环会继续直到凑满6个不重复的数字。这是实现“无放回抽取”的简洁方式。边界值RANDOM.nextInt(n)生成的是[0, n)的整数所以nextInt(33)生成[0, 32]加1后得到[1, 33]。这是一个常见的“差一错误”Off-by-one error易发点务必小心。3.3 核心大脑LotterySystem兑奖逻辑这是整个系统最复杂的部分需要精确实现中奖规则。import java.util.ArrayList; import java.util.List; import java.util.Set; public class LotterySystem { public enum AwardLevel { FIRST(6, 1, 一等奖, 5_000_000), SECOND(6, 0, 二等奖, 500_000), THIRD(5, 1, 三等奖, 3000), FOURTH(5, 0, 四等奖, 200), FOURTH_ALT(4, 1, 四等奖, 200), // 另一种情况 FIFTH(4, 0, 五等奖, 10), FIFTH_ALT(3, 1, 五等奖, 10), // 另一种情况 SIXTH(2, 1, 六等奖, 5), SIXTH_ALT(1, 1, 六等奖, 5), SIXTH_ALT2(0, 1, 六等奖, 5), NONE(0, 0, 未中奖, 0); // 注意这里仅用于初始化实际匹配不按此规则 public final int redMatch; public final int blueMatch; public final String description; public final int prize; AwardLevel(int redMatch, int blueMatch, String description, int prize) { this.redMatch redMatch; this.blueMatch blueMatch; this.description description; this.prize prize; } } // 兑奖核心算法 public static AwardLevel checkTicket(LotteryTicket userTicket, LotteryTicket drawResult) { SetInteger userReds userTicket.getRedBalls(); SetInteger drawReds drawResult.getRedBalls(); // 1. 计算红球匹配数利用Set的交集思想 int redMatchCount 0; for (int red : userReds) { if (drawReds.contains(red)) { redMatchCount; } } // 更优雅的写法redMatchCount (int) userReds.stream().filter(drawReds::contains).count(); // 2. 判断蓝球是否匹配 boolean blueMatch (userTicket.getBlueBall() drawResult.getBlueBall()); // 3. 根据规则匹配中奖等级 // 注意必须先判断高等级匹配条件严格的再判断低等级 if (redMatchCount 6 blueMatch) { return AwardLevel.FIRST; } else if (redMatchCount 6) { // 隐含 blueMatch false return AwardLevel.SECOND; } else if (redMatchCount 5 blueMatch) { return AwardLevel.THIRD; } else if (redMatchCount 5 || (redMatchCount 4 blueMatch)) { // 四等奖有两种情况奖金相同可以统一返回FOURTH但描述需注意 // 这里简单处理返回一个代表四等奖的枚举即可。更精细的做法可以区分。 return AwardLevel.FOURTH; } else if (redMatchCount 4 || (redMatchCount 3 blueMatch)) { return AwardLevel.FIFTH; } else if ((redMatchCount 2 blueMatch) || (redMatchCount 1 blueMatch) || (redMatchCount 0 blueMatch)) { return AwardLevel.SIXTH; } else { return AwardLevel.NONE; } } // 批量检查并打印结果 public static void checkAndDisplay(ListLotteryTicket userTickets, LotteryTicket drawResult) { System.out.println( 本期开奖号码 ); System.out.println(drawResult); System.out.println(); System.out.println(您的投注及中奖情况); int totalPrize 0; for (int i 0; i userTickets.size(); i) { LotteryTicket ticket userTickets.get(i); AwardLevel level checkTicket(ticket, drawResult); int prize level.prize; totalPrize prize; System.out.printf(第%02d注%s -- 【%s】 奖金%d元%n, i 1, ticket, level.description, prize); } System.out.println(); System.out.printf(总计中奖金额%d元%n, totalPrize); } }代码解读与踩坑点枚举AwardLevel的设计将中奖规则定义为枚举是很好的实践。它把红球匹配数、蓝球是否匹配、描述和奖金绑定在一起结构清晰。注意NONE是一个特殊值它的redMatch和blueMatch字段没有实际匹配意义仅用于表示“未中奖”。兑奖算法checkTicket红球匹配计算这是性能关键点。我们遍历用户彩票的每个红球检查是否存在于开奖红球Set中。由于HashSet的contains是O(1)操作整个匹配过程是O(6)即常数时间复杂度非常高效。如果使用两个List并嵌套循环比对效率就是O(6*6)O(36)虽然数字小差别不大但体现了算法思维的差异。条件判断的顺序中奖规则是有优先级的一等奖条件最严。代码中的if-else if链必须从高等级到低等级书写。如果先判断redMatchCount 4那么一个“5红1蓝”的彩票就会被错误地判定为四等奖而不是三等奖。四等奖和五等奖的合并代码中将同奖金的不同情况合并返回了同一个枚举值FOURTH和FIFTH。如果你需要更精确地记录是哪种子情况可以修改枚举设计或返回值。checkAndDisplay方法这是一个完整的业务流程方法负责展示、计算和汇总。它分离了核心逻辑checkTicket和展示逻辑符合单一职责原则。3.4 组装与交互完成系统闭环最后我们需要一个Main类或启动类来把一切串联起来模拟一次完整的购买、开奖、兑奖流程。import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class DoubleColorBallApp { public static void main(String[] args) { Scanner scanner new Scanner(System.in); ListLotteryTicket myTickets new ArrayList(); System.out.println(欢迎使用双色球模拟系统); System.out.println(1. 机选一注); System.out.println(2. 手动输入一注); System.out.println(3. 机选5注); System.out.println(4. 开始开奖并兑奖); System.out.println(0. 退出); boolean running true; while (running) { System.out.print(\n请选择操作); int choice scanner.nextInt(); switch (choice) { case 1: LotteryTicket randomTicket LotteryUtils.generateRandomTicket(); myTickets.add(randomTicket); System.out.println(已机选 randomTicket); break; case 2: System.out.println(请输入6个红球号码1-33用空格隔开); ListInteger manualReds new ArrayList(); for (int i 0; i 6; i) { manualReds.add(scanner.nextInt()); } System.out.println(请输入1个蓝球号码1-16); int manualBlue scanner.nextInt(); try { LotteryTicket manualTicket new LotteryTicket(manualReds, manualBlue); myTickets.add(manualTicket); System.out.println(已添加手动票 manualTicket); } catch (IllegalArgumentException e) { System.out.println(输入无效 e.getMessage()); } break; case 3: System.out.println(正在为您机选5注...); for (int i 0; i 5; i) { LotteryTicket ticket LotteryUtils.generateRandomTicket(); myTickets.add(ticket); System.out.println(第 (i1) 注: ticket); } break; case 4: if (myTickets.isEmpty()) { System.out.println(您还没有购买任何彩票请先投注); break; } System.out.println(\n 正在摇奖请稍候... ); // 模拟开奖延迟增加趣味性 try { Thread.sleep(1500); } catch (InterruptedException e) { } LotteryTicket drawResult LotteryUtils.generateRandomTicket(); LotterySystem.checkAndDisplay(myTickets, drawResult); // 一次开奖后清空当前投注可重置 System.out.println(本期开奖兑奖结束。); myTickets.clear(); break; case 0: running false; System.out.println(感谢使用再见); break; default: System.out.println(无效选择请重新输入。); } } scanner.close(); } }代码解读与踩坑点用户交互与健壮性手动输入部分case 2使用了try-catch来捕获LotteryTicket构造方法可能抛出的IllegalArgumentException给用户友好的错误提示而不是让程序崩溃。资源管理在程序最后关闭了Scanner(scanner.close())这是一个好习惯尽管对于标准输入流(System.in)不一定必须但体现了资源释放的意识。体验细节在开奖前case 4增加了简单的延时 (Thread.sleep(1500)和提示语模拟真实开奖的等待感提升了控制台程序的趣味性。流程闭环一次开奖兑奖后选择清空了myTickets列表 (myTickets.clear())模拟现实中新一期的开始。你也可以选择不清空实现多期投注的累计这取决于你的设计。4. 项目进阶思考与优化方向一个基础版本的系统已经完成。但如果你想把它当作一个更严肃的练习项目或者面试时的谈资可以考虑以下优化方向这能极大体现你的工程能力。4.1 引入配置文件与管理奖金池目前的奖金是硬编码在枚举里的。更优的做法是将其外置到配置文件如award.properties或config.json中。# award.properties first.prize5000000 second.prize500000 third.prize3000 fourth.prize200 fifth.prize10 sixth.prize5然后在系统启动时通过一个ConfigManager类加载这些配置并动态设置到AwardLevel枚举或一个专门的PrizePool类中。这样奖金调整无需重新编译代码。4.2 实现更真实的“奖池”与中奖概率现实中的双色球一等奖奖金是浮动的来自当期奖池的累积。我们可以模拟一个简单的奖池模型设定每注彩票售价2元。销售额的50%进入奖池。各奖项奖金按固定比例从奖池中分配例如一等奖占奖池的70%二等奖占15%等并设置封顶限额。如果当期无人中一等奖则奖金滚入下期奖池。实现这个模型需要引入SalesRecord销售记录、PrizePool奖池管理等更复杂的类并修改兑奖逻辑从固定奖金变为基于奖池的计算。这涉及到状态管理和更复杂的业务规则是绝佳的面向对象设计练习。4.3 持久化存储连接数据库将用户的投注记录、历史开奖结果保存到数据库如MySQL或文件中。这让你可以实现“历史开奖查询”功能。模拟“多期连续投注”。分析号码频率等简单数据。你需要引入JDBC或简单的文件IO操作。可以设计一个TicketDAOData Access Object类来处理彩票数据的增删改查。4.4 单元测试与代码质量为核心类编写单元测试使用JUnit。例如LotteryTicket的构造方法测试传入无效数据时是否抛出异常。LotteryUtils.generateRandomTicket测试其生成的数据是否100%有效运行成千上万次。LotterySystem.checkTicket这是重点需要为每一个中奖等级包括未中奖编写测试用例确保逻辑百分百正确。import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; public class LotterySystemTest { Test void testCheckTicket_FirstPrize() { SetInteger drawReds Set.of(1,2,3,4,5,6); int drawBlue 7; LotteryTicket draw new LotteryTicket(drawReds, drawBlue); // 用户票与开奖票完全一致 LotteryTicket user new LotteryTicket(Set.of(1,2,3,4,5,6), 7); AwardLevel level LotterySystem.checkTicket(user, draw); assertEquals(AwardLevel.FIRST, level); } Test void testCheckTicket_NoPrize() { SetInteger drawReds Set.of(1,2,3,4,5,6); int drawBlue 7; LotteryTicket draw new LotteryTicket(drawReds, drawBlue); // 用户票完全不同 LotteryTicket user new LotteryTicket(Set.of(10,11,12,13,14,15), 8); AwardLevel level LotterySystem.checkTicket(user, draw); assertEquals(AwardLevel.NONE, level); } // ... 更多测试用例 }编写全面的单元测试不仅能保证代码质量也是现代软件开发中非常重要的技能。4.5 图形化界面GUI用Java Swing或JavaFX为这个系统做一个简单的桌面图形界面。界面可以包含机选、手动输入区域。已选号码的展示区域。一个“摇奖”按钮点击后动画模拟摇奖过程。一个区域清晰展示开奖结果和中奖信息。这对于巩固Java GUI编程知识非常有帮助。虽然Swing已不是主流但其MVC模型-视图-控制器思想是相通的。你可以将我们之前写的核心逻辑LotterySystem,LotteryTicket等作为“模型”ModelGUI部分作为“视图”View和“控制器”Controller。从控制台到GUI项目的复杂度和成就感会提升一个档次。你会发现之前精心设计的、业务逻辑清晰的底层代码在接入GUI时会非常顺畅这正是良好架构设计的价值所在。
返回列表