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

资讯详情

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

Java实战:双色球模拟系统开发全解析,从随机数生成到面向对象设计

Java实战:双色球模拟系统开发全解析,从随机数生成到面向对象设计 1. 项目概述一个Java程序员的“小确幸”双色球这个几乎家喻户晓的彩票游戏对于程序员来说它不仅仅是一个概率游戏更是一个绝佳的练手项目。为什么这么说因为它麻雀虽小五脏俱全。一个完整的双色球模拟系统几乎涵盖了Java初学者到中级开发者需要掌握的大部分核心知识点从基础的变量、数组、循环、条件判断到面向对象的类设计、集合框架的应用再到随机数生成、输入输出处理、简单的算法逻辑如排序、去重、比较甚至还能延伸到文件操作来记录历史数据。对于正在学习Java或者想巩固基础的朋友来说亲手实现一个这样的系统远比单纯地看书、刷题来得生动和有效。它能让你真切地感受到一行行代码是如何组合起来模拟一个真实世界的小小规则。今天我就来拆解一下这个“Java之双色球系统”分享我的实现思路、核心代码细节以及那些只有动手做了才会遇到的“坑”。2. 核心需求与功能设计拆解在动手敲代码之前我们必须先把业务规则和功能需求理清楚。双色球的官方规则是从1-33个红球中随机选取6个不重复的号码再从1-16个蓝球中随机选取1个号码共同组成一注彩票。我们的系统至少要能模拟这个核心的购彩和开奖过程。2.1 功能模块划分基于核心规则我们可以将系统划分为以下几个模块用户投注模块允许用户通过键盘输入选择自己的红球和蓝球号码或者选择机选由系统随机生成一注。这里需要处理用户输入的合法性校验比如数字范围、是否重复、格式是否正确。彩票生成模块负责生成单注彩票。无论是用户自选还是机选最终都会封装成一个“彩票”对象。这个对象应该包含6个红球号码和1个蓝球号码。开奖模块模拟官方开奖随机生成一组中奖号码6红1蓝。这是系统的另一个核心随机算法必须保证公平性无规律可循和唯一性红球不重复。中奖判断与兑奖模块将用户彩票与中奖号码进行比对根据红球和蓝球匹配的数量确定中奖等级一等奖到六等奖并计算奖金。历史记录与查询模块进阶将每期的投注记录、开奖结果保存到文件或数据库并可以查询历史数据。这引入了数据持久化的概念。2.2 面向对象设计思路采用面向对象的思想能让代码结构更清晰更易于维护。我们至少可以设计两个核心类LotteryTicket(彩票类)代表一注彩票。属性包括一个红球号码数组或集合int[] redBalls和一个蓝球号码int blueBall。方法可以包括生成彩票自选/机选、显示彩票信息等。DoubleColorBallSystem(系统主类)包含程序的主逻辑。它拥有一个LotteryTicket对象作为本期开奖结果一个ListLotteryTicket用来存储用户购买的多注彩票。方法包括运行菜单、处理用户交互、执行开奖、判断中奖等。注意在实际开发中开奖结果和用户彩票在逻辑上是平等的都是LotteryTicket的实例。将它们区分开的是其所处的“角色”和拥有的“状态”例如是否已开奖。3. 核心实现细节与关键技术点3.1 随机数的生成与去重这是开奖和机选功能的核心。Java中生成随机数常用java.util.Random类或Math.random()。为了保证红球不重复我们需要一个高效的策略。方案一使用Set集合Set天生具有去重特性。我们可以循环生成随机数并添加到HashSet中直到集合大小达到6。这是最直观、代码最简洁的方式。import java.util.HashSet; import java.util.Random; import java.util.Set; public SetInteger generateRedBalls() { SetInteger redBalls new HashSet(); Random random new Random(); while (redBalls.size() 6) { // nextInt(33) 生成 [0, 32] 1 后得到 [1, 33] redBalls.add(random.nextInt(33) 1); } // 注意HashSet是无序的如果需要排序输出可以转为List再排序 return redBalls; }方案二使用“抽奖池”算法模拟现实中的抽奖预先准备好所有红球1-33然后随机打乱顺序取前6个。这种方法在理论上更符合“公平抽选”的物理过程。import java.util.ArrayList; import java.util.Collections; public ListInteger generateRedBallsByShuffle() { ArrayListInteger pool new ArrayList(); for (int i 1; i 33; i) { pool.add(i); } Collections.shuffle(pool); // 随机打乱列表 ListInteger result pool.subList(0, 6); // 取前6个 Collections.sort(result); // 排序便于观看和比较 return result; }实操心得Collections.shuffle()内部使用了高质量的随机源且算法效率很高。对于这种小规模数据两种方案性能差异可忽略不计。我个人更推荐方案二因为它逻辑清晰且能自然得到已排序的结果方便后续比较和显示。方案一使用Set虽然简单但得到的集合是无序的如果需要向用户展示通常需要额外排序。3.2 用户输入校验处理用户自选号码时健壮的输入校验至关重要。我们需要考虑各种非法输入非数字、超出范围、重复输入等。import java.util.Scanner; import java.util.HashSet; import java.util.Set; public int[] getUserRedBalls(Scanner scanner) { SetInteger redSet new HashSet(); System.out.println(请依次输入6个红球号码1-33用空格隔开); while (redSet.size() 6) { String input scanner.nextLine(); String[] numbers input.split(\\s); // 按空格分割 if (numbers.length ! 6) { System.out.println(请输入恰好6个数字请重新输入); continue; } try { for (String numStr : numbers) { int num Integer.parseInt(numStr); if (num 1 || num 33) { System.out.println(号码 num 超出范围1-33请重新输入全部6个号码); redSet.clear(); // 清空集合重新开始 break; } if (!redSet.add(num)) { // add方法在重复时会返回false System.out.println(号码 num 重复输入请重新输入全部6个号码); redSet.clear(); break; } } } catch (NumberFormatException e) { System.out.println(输入包含非数字字符请重新输入); redSet.clear(); } } // 将Set转为有序数组并返回 return redSet.stream().sorted().mapToInt(i - i).toArray(); }避坑指南这里使用Set来辅助去重校验非常方便。Scanner.nextLine()会读取整行比多次调用nextInt()更能灵活处理空格分割。异常捕获NumberFormatException是处理非法输入的标准做法。关键点一旦发现某一条校验失败如超范围或重复必须清空当前收集的集合redSet.clear()并让用户重新输入全部6个号码否则会导致部分正确号码被保留逻辑混乱。3.3 中奖判断算法中奖规则是根据红球匹配数和蓝球是否匹配来确定等级。这是一个典型的计数和条件判断问题。我们可以先计算红球匹配数再判断蓝球。public class LotteryUtils { public static int checkPrizeLevel(int[] userReds, int userBlue, int[] winningReds, int winningBlue) { int redMatchCount 0; int blueMatch (userBlue winningBlue) ? 1 : 0; // 计算红球匹配数 // 因为两组号码都可能已排序可以使用双指针法提高效率但这里数据量小简单遍历即可 for (int userRed : userReds) { for (int winningRed : winningReds) { if (userRed winningRed) { redMatchCount; break; // 找到匹配跳出内层循环 } } } // 根据规则判断中奖等级 if (redMatchCount 6 blueMatch 1) { return 1; // 一等奖61 } else if (redMatchCount 6) { return 2; // 二等奖60 } else if (redMatchCount 5 blueMatch 1) { return 3; // 三等奖51 } else if (redMatchCount 5 || (redMatchCount 4 blueMatch 1)) { return 4; // 四等奖50 或 41 } else if (redMatchCount 4 || (redMatchCount 3 blueMatch 1)) { return 5; // 五等奖40 或 31 } else if (blueMatch 1) { return 6; // 六等奖01 或 11 或 21 } else { return 0; // 未中奖 } } }性能与可读性权衡对于仅6个元素的小数组双重循环完全没问题。如果考虑更通用或数据量大的情况可以先将中奖红球放入一个HashSet这样判断用户每个红球是否匹配的时间复杂度可以从O(n)降到O(1)。但在这个具体场景中收益微乎其微代码的清晰度更重要。4. 完整系统搭建与代码组织下面我将展示一个相对完整的、控制台版本的双色球系统核心代码结构。为了突出重点我略去了部分重复的校验代码和菜单细节。4.1 实体类LotteryTicket.javaimport java.util.Arrays; import java.util.List; import java.util.Collections; import java.util.ArrayList; /** * 彩票实体类 */ public class LotteryTicket { private int[] redBalls; // 红球号码长度6 private int blueBall; // 蓝球号码 // 构造方法 - 用于创建一注已知号码的彩票如用户自选、历史记录读取 public LotteryTicket(int[] redBalls, int blueBall) { if (redBalls.length ! 6) { throw new IllegalArgumentException(红球必须为6个号码); } // 这里可以添加对红球范围、重复的校验为了简洁先省略 this.redBalls Arrays.copyOf(redBalls, redBalls.length); // 防御性拷贝 Arrays.sort(this.redBalls); // 内部存储排序后的数组便于比较 this.blueBall blueBall; } // 构造方法 - 用于机选生成一注彩票 public LotteryTicket() { this.redBalls generateRandomRedBalls(); this.blueBall generateRandomBlueBall(); } // 生成随机红球使用抽奖池算法 private int[] generateRandomRedBalls() { ListInteger pool new ArrayList(); for (int i 1; i 33; i) { pool.add(i); } Collections.shuffle(pool); int[] result new int[6]; for (int i 0; i 6; i) { result[i] pool.get(i); } Arrays.sort(result); return result; } // 生成随机蓝球 private int generateRandomBlueBall() { return new java.util.Random().nextInt(16) 1; // [1, 16] } // 显示彩票信息 public void display() { System.out.print(红球: ); for (int ball : redBalls) { System.out.printf(%02d , ball); // 格式化输出两位数不足补零 } System.out.printf(| 蓝球: %02d%n, blueBall); } // Getter 方法 public int[] getRedBalls() { return Arrays.copyOf(redBalls, redBalls.length); // 返回拷贝保护封装性 } public int getBlueBall() { return blueBall; } }4.2 主系统类DoubleColorBallSystem.javaimport java.util.*; /** * 双色球系统主类 */ public class DoubleColorBallSystem { private LotteryTicket winningTicket; // 本期开奖号码 private ListLotteryTicket userTickets; // 用户购买的彩票 private Scanner scanner; public DoubleColorBallSystem() { userTickets new ArrayList(); scanner new Scanner(System.in); } public void start() { System.out.println( 欢迎使用双色球模拟系统 ); boolean running true; while (running) { printMenu(); int choice getMenuChoice(); switch (choice) { case 1: buyTicket(); break; case 2: drawLottery(); break; case 3: showMyTickets(); break; case 4: checkPrize(); break; case 5: System.out.println(感谢使用再见); running false; break; default: System.out.println(无效选择请重新输入。); } } scanner.close(); } private void printMenu() { System.out.println(\n-------------------); System.out.println(1. 购买彩票 (自选/机选)); System.out.println(2. 开始开奖); System.out.println(3. 查看我的彩票); System.out.println(4. 兑奖); System.out.println(5. 退出系统); System.out.println(-------------------); System.out.print(请选择操作: ); } private int getMenuChoice() { try { return scanner.nextInt(); } catch (InputMismatchException e) { scanner.next(); // 清空错误的输入 return -1; } } private void buyTicket() { System.out.println(\n--- 购买彩票 ---); System.out.println(1. 自选号码); System.out.println(2. 机选一注); System.out.print(请选择: ); int choice scanner.nextInt(); scanner.nextLine(); // 消耗换行符 LotteryTicket ticket; if (choice 1) { ticket createManualTicket(); } else if (choice 2) { ticket new LotteryTicket(); // 使用无参构造器机选 System.out.print(机选号码为: ); ticket.display(); } else { System.out.println(选择无效。); return; } userTickets.add(ticket); System.out.println(购买成功); } private LotteryTicket createManualTicket() { System.out.println(请输入6个红球号码 (1-33)用空格分隔:); int[] reds getValidatedRedBallsFromInput(); System.out.println(请输入1个蓝球号码 (1-16):); int blue getValidatedBlueBallFromInput(); return new LotteryTicket(reds, blue); } private int[] getValidatedRedBallsFromInput() { // 此处实现输入校验逻辑参考前面3.2节的代码返回合法的6个红球数组 // 为保持示例简洁这里省略详细校验代码假设输入合法 int[] reds new int[6]; String input scanner.nextLine().trim(); String[] parts input.split(\\s); for (int i 0; i 6; i) { reds[i] Integer.parseInt(parts[i]); } Arrays.sort(reds); return reds; } private int getValidatedBlueBallFromInput() { // 蓝球校验逻辑省略 return scanner.nextInt(); } private void drawLottery() { if (winningTicket ! null) { System.out.println(本期已开奖中奖号码为:); winningTicket.display(); return; } System.out.println(\n--- 正在开奖摇号中... ---); try { // 模拟开奖延迟增加一点仪式感 for (int i 0; i 3; i) { Thread.sleep(500); System.out.print(.); } Thread.sleep(500); System.out.println(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } winningTicket new LotteryTicket(); // 机选一注作为开奖号码 System.out.println(本期双色球开奖号码为:); winningTicket.display(); System.out.println(开奖完成); } private void showMyTickets() { if (userTickets.isEmpty()) { System.out.println(您尚未购买任何彩票。); return; } System.out.println(\n--- 我的彩票列表 ---); for (int i 0; i userTickets.size(); i) { System.out.printf(第 %d 注: , i 1); userTickets.get(i).display(); } } private void checkPrize() { if (winningTicket null) { System.out.println(请先进行开奖操作); return; } if (userTickets.isEmpty()) { System.out.println(您没有可兑奖的彩票。); return; } System.out.println(\n--- 兑奖结果 ---); System.out.println(本期开奖号码:); winningTicket.display(); System.out.println(----------------); int totalPrize 0; int[] prizeLevelCount new int[7]; // 索引0未使用1-6对应一等奖到六等奖 for (int i 0; i userTickets.size(); i) { LotteryTicket userTicket userTickets.get(i); int level LotteryUtils.checkPrizeLevel( userTicket.getRedBalls(), userTicket.getBlueBall(), winningTicket.getRedBalls(), winningTicket.getBlueBall() ); if (level 0) { prizeLevelCount[level]; int prize calculatePrizeMoney(level); totalPrize prize; System.out.printf(第 %d 注彩票: , i 1); userTicket.display(); System.out.printf( 中奖等级: %d 等奖 奖金: %d 元%n, level, prize); } } // 输出中奖统计 System.out.println(\n 中奖统计 ); boolean hasPrize false; for (int level 1; level 6; level) { if (prizeLevelCount[level] 0) { System.out.printf(%d 等奖: %d 注%n, level, prizeLevelCount[level]); hasPrize true; } } if (!hasPrize) { System.out.println(很遗憾您本次购买的彩票均未中奖。); } else { System.out.printf(总计奖金: %d 元%n, totalPrize); } } private int calculatePrizeMoney(int level) { // 这里使用一个简化的奖金表实际金额可能浮动 switch (level) { case 1: return 5_000_000; // 500万 case 2: return 100_000; // 10万 case 3: return 3000; // 3千 case 4: return 200; // 2百 case 5: return 10; // 10元 case 6: return 5; // 5元 default: return 0; } } public static void main(String[] args) { DoubleColorBallSystem system new DoubleColorBallSystem(); system.start(); } }4.3 工具类LotteryUtils.java这个类包含了通用的工具方法例如前面提到的checkPrizeLevel方法。这样做的好处是逻辑分离主系统类更专注于流程控制。/** * 双色球工具类 */ public class LotteryUtils { /** * 检查单注彩票的中奖等级 * return 0-未中奖1-一等奖...6-六等奖 */ public static int checkPrizeLevel(int[] userReds, int userBlue, int[] winningReds, int winningBlue) { // 实现代码见3.3节 // ... } /** * 辅助方法判断一个已排序数组中是否包含某个值二分查找效率更高 */ private static boolean contains(int[] sortedArray, int key) { return Arrays.binarySearch(sortedArray, key) 0; } // 其他可能用到的工具方法例如格式化输出、文件操作等 }5. 常见问题与调试技巧实录在实现和运行这个系统的过程中你几乎一定会遇到下面这些问题。我把它们和解决方法记录下来希望能帮你节省时间。5.1 输入处理中的“坑”问题1Scanner的nextInt()和nextLine()混用导致跳过输入。这是Java新手最常踩的坑。当你使用scanner.nextInt()读取一个整数后紧接着调用scanner.nextLine()读取字符串会发现nextLine()直接返回了一个空字符串好像被跳过了。原因与解决nextInt()只读取了数字而输入流中数字后面的换行符Enter键还留在缓冲区。接下来的nextLine()会立刻读取到这个换行符并认为输入结束。解决方法是在nextInt()后面多加一句scanner.nextLine()来“消耗”掉这个换行符。int choice scanner.nextInt(); scanner.nextLine(); // 关键消耗掉数字后面的换行符 // 现在可以安全地使用 nextLine() 读取后续的字符串输入了 String name scanner.nextLine();问题2用户输入非法字符导致程序崩溃。如果期望输入数字用户却输入了字母程序会抛出InputMismatchException异常并终止。解决使用try-catch块捕获异常并提示用户重新输入。更好的做法是统一使用nextLine()读取整行字符串然后使用Integer.parseInt()进行转换并捕获NumberFormatException。正如我在3.2节用户输入校验中演示的那样这样能获得更灵活的控制权。5.2 数组与集合的抉择问题存储号码用数组还是ArrayList在LotteryTicket类中我选择了int[]来存储红球。因为红球数量固定6个数组在内存和访问速度上略有优势且更直观。如果选择ArrayListInteger则需要额外的装箱/拆箱开销但它在动态操作如插入、删除上更灵活。对于双色球这个固定数量的场景数组是更合适的选择。如果未来规则变化红球数量可变那么ArrayList或HashSet会是更好的后备选项。5.3 随机数的“伪随机”与种子问题每次运行程序开奖号码都差不多如果你使用new Random()而不指定种子它默认以系统当前时间毫秒级为种子。如果程序启动得非常快两次运行的种子可能非常接近导致生成的随机数序列开头部分相似。但在我们这个程序中每次开奖只生成7个随机数且中间有用户交互的时间间隔所以基本不会遇到这个问题。如果是在单元测试中需要可重复的随机结果可以指定一个固定的种子new Random(12345L)。5.4 对象封装与防御性编程问题在LotteryTicket类中为什么getRedBalls()要返回数组的拷贝这是一个非常重要的编程习惯。直接返回内部数组的引用this.redBalls会导致外部代码可以修改这个数组从而破坏彩票对象的不可变性想象一下开奖后有人偷偷改了号码。返回Arrays.copyOf(...)创建的一个新数组副本就保护了内部数据。这被称为防御性拷贝。5.5 扩展思考如何引入持久化一个基础的控制台程序数据在内存中程序退出就没了。一个更完整的系统应该能保存购买记录和开奖历史。简单方案使用文本文件每次购买或开奖后将数据追加写入到一个文本文件如history.txt。数据格式可以用JSON、CSV或者自定义格式。// 示例将开奖结果保存到文件 import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public void saveWinningResultToFile(LotteryTicket ticket) { String record String.format(%s, 红球: %s, 蓝球: %02d%n, LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), Arrays.toString(ticket.getRedBalls()), ticket.getBlueBall()); try { Files.write(Paths.get(lottery_history.txt), record.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND); } catch (Exception e) { System.err.println(保存历史记录失败: e.getMessage()); } }进阶方案使用数据库对于更复杂的数据查询如“查询我所有中过三等奖的彩票”使用SQLite、MySQL等数据库是更专业的选择。这就会涉及到JDBC、数据库连接池等知识是Java后端开发的重要技能点。从这个小小的双色球项目出发你完全可以尝试加入数据库模块让它变成一个“毕业设计”级别的小项目。实现这个双色球系统的过程就像搭积木把Java零散的知识点串联了起来。从最初的变量、循环到后来的类设计、集合使用、异常处理、文件操作每一步都对应着解决一个实际的小问题。当你看到程序成功运行模拟出开奖和中奖判断时那种成就感是单纯学习语法无法比拟的。更重要的是你开始用程序员的思维去设计、去解决需求这才是核心能力的提升。
返回列表