
直接从一行代码开始说起吧Scanner scanner new Scanner(System.in);这大概是 Java 世界里被引用次数最多、也最容易被误解的一行代码。“Scanner”这个词本身就有三重身份办公一体机里的硬件扫描模块、局域网管理工具 Advanced IP Scanner、以及我们代码里天天用的 java.util.Scanner。当它们同时出现在搜索结果里新手很容易搞混。但我想提醒你的是真正决定一个程序“好不好用”的从来不是这行代码本身而是你拿它去构建的那套用户交互闭环。这篇文章我会围绕“用户交互 Scanner”这个主题讲讲我在实际项目中怎么用它、为什么有人坚持不用它以及那种“随机生成两个一位数加法练习题”的场景下带 Scanner 和不带 Scanner 的写法到底差在哪。希望你看完以后能把 Scanner 用出“交互设计”的味道而不是停留在背 API 的阶段。1. 先把“用户交互Scanner”这个词掰开揉碎1.1 同名同姓的三种 Scanner先说清楚免得后面聊跑偏。第一种是物理硬件比如设备管理器里常见的 “Generic 18BW-7EN Scanner”。这是打印机一体机驱动暴露出来的扫描仪接口负责把纸质内容变成图像。它解决的是“光学采集”问题。第二种是网络工具典型代表就是 Advanced IP Scanner。运维人员拿它扫一遍局域网看看哪些设备在线、开放了哪些端口本质上是“网络探测”工具。第三种是 Java 标准库里的java.util.Scanner它拿来做的事情是扫描数据源中的文本。数据源可以是键盘输入流System.in、一个文件、一段字符串或者一个管道流。它把一个连续的字符流切成一段一段有意义的 token并自动转成 int、double、String 等类型。今天这篇只聊第三种因为“用户交互”这个前缀天然指向它。但你也看到了搜索引擎不会帮你分流所以很多人在搜 Scanner 教程时会被硬件驱动和网络工具的页面干扰。我在开头把这三种身份列出来就是想让后面的内容干净一点。1.2 完整交互闭环里Scanner 只负责入口很多教程只教你“ Scanner 能读什么类型”却忽略了一个更重要的问题真实的用户交互是什么样拿一个最简单的命令行答题程序举例。用户看到题目后输入答案程序判断对错、给出反馈、继续下一题。这整个流程里Scanner 只负责“读取用户输入”这一小步。但这一小步的质量会直接决定后面几步的实现难度。我把完整闭环拆成五步向用户展示提示告诉对方要输入什么格式的数据通过 Scanner 读取输入验证输入是否合法、是否属于预期类型执行业务逻辑算分、判断、记录给出反馈循环回到第 1 步Scanner 提供的方法里hasNextInt()、hasNextLine()就是为第 3 步服务的nextInt()、nextLine()则负责第 2 步。为什么有的人写出来的交互代码又脆又容易崩大多数情况是只用了next系列方法完全没搭理hasNext系列导致非法输入直接抛异常。所以我一直觉得判断一个人会不会写交互程序看他用 Scanner 的方式就够了。2. new Scanner(System.in) 到底拆成了什么2.1 System.in 是一个“只会输出字节”的流System.in是一个InputStream代表标准输入也就是默认的键盘输入。但你要是直接在它上面操作会非常痛苦它一次只能读一个字节你得自己处理换行符、编码、缓冲区还得把读进来的字节拼成字符串再转成整数。举个例子你想读一个整数用System.in的原始方式大概是int input System.in.read() - 0;这还只能处理单个数字多位数、负数、带空格的输入全部得自己写逻辑。这就是为什么我们需要 Scanner它是包在InputStream外面的一层“翻译器”把笨重的字节流变成友好的类型解析接口。2.2 Scanner 的三个内部动作缓冲、切分、转换我习惯用三个词来理解 Scanner 的内部工作缓冲、切分、转换。先说缓冲。Scanner 内部维护了一个缓冲区默认会成批读取输入流里的数据而不是用户敲一个键读一个字节。所以它不至于慢到让人没法用。再说切分。Scanner 通过正则表达式来定位 token 的边界。默认分隔符是\p{javaWhitespace}也就是所有空白字符包括空格、换行、制表符。这意味着你输入12 34然后连续调用两次nextInt()第一次读到 12第二次读到 34中间不需要任何额外处理。这个设计极大简化了“多个数据在同一行输入”的场景。最后是转换。当nextInt()找到一个 token 后会在内部调用类似Integer.parseInt(token)的操作把它从字符串变成整数。如果转换失败就会抛出InputMismatchException。nextDouble()、nextBoolean()也是同一个套路。理解这三点以后很多经典问题其实不用背答案也能推理出来。比如为什么nextInt()之后再调nextLine()会读到一个空串因为nextInt()只把数字 token 消费掉了数字后面的换行符还留在缓冲区里nextLine()一读读到的是那个残留的换行符于是返回空字符串。2.3 初始化时的三个工程细节第一是否要close()。独立小工具里main方法跑完 JVM 就退出了你不关 Scanner 问题不大。但如果你在写一个会被多次调用的模块Scanner 包装的是System.in最好不要随便关。Scanner.close()会把底层的输入流也关掉System.in一旦关闭在当前 JVM 进程里基本等于废了。第二不要重复创建。有人喜欢在循环里new Scanner(System.in)这不会立即报错但容易造成资源浪费而且一旦关闭其中一个后面全乱套。全局维护一个实例就够了。第三从文件读取时务必指定字符集。new Scanner(Paths.get(data.txt), StandardCharsets.UTF_8)这种写法才能避免中文乱码。如果你图省事只写文件路径会走平台默认编码在 Windows 上经常读出一堆问号。3. 实战写一个随机加法练习交互程序Scanner 版3.1 需求拆解想要的交互效果是什么有个热词是“java 随机生成两个一位数加法练习题 不用 scanner”。这个需求很有意思它有两种理解一种是只要生成练习题不需要用户输入纯输出文件或打印另一种是想要一个交互式答题器但不想用 Scanner 读输入。我们先做交互版本因为这才是标题“用户交互 Scanner”最对口的场景。需求定成这样随机出 5 道题每道题是两个一位数相加用户在控制台输入答案程序判断对错并反馈结束后统计正确率和用时这个需求很小但覆盖了交互闭环里最关键的三步提示、读取验证、反馈。3.2 完整代码5题计分版加法练习器import java.util.Random; import java.util.Scanner; public class AdditionQuiz { public static void main(String[] args) { Scanner sc new Scanner(System.in); Random random new Random(); int total 5; int correct 0; long start System.currentTimeMillis(); System.out.println(随机两位数加法练习一共 total 题每题请输入一个整数。); for (int i 1; i total; i) { int a random.nextInt(9) 1; // 生成 1~9 int b random.nextInt(9) 1; System.out.printf(第%d题%d %d , i, a, b); if (sc.hasNextInt()) { int answer sc.nextInt(); if (answer a b) { correct; System.out.println( 回答正确); } else { System.out.println( 回答错误正确答案是 (a b)); } } else { System.out.println( 输入不是整数本题记为错误。); sc.next(); } } sc.close(); long cost System.currentTimeMillis() - start; System.out.printf(答题结束共答对 %d/%d 题正确率 %.0f%%用时 %.1f 秒。%n, correct, total, correct * 100.0 / total, cost / 1000.0); } }这里有几个关键点。random.nextInt(9) 1生成的是 1 到 9 的整数正好对应“一位数”的约束。hasNextInt()是校验闸门用户输入abc时不会抛异常而是走 else 分支提示输入无效。最后用sc.next()把非法 token 消费掉避免死循环。3.3 运行效果真实控制台对话什么样上面这段代码跑起来控制台会是这样随机两位数加法练习一共5题每题请输入一个整数。 第1题5 3 8 回答正确 第2题9 1 11 回答错误正确答案是 10 第3题abc 输入不是整数本题记为错误。 第4题2 4 6 回答正确 第5题7 7 14 回答正确 答题结束共答对 3/5 题正确率 60%用时 12.6 秒。看到没非法输入也被当成一次有效的流程分支处理这就是hasNextInt()的功劳。如果你去掉这个判断直接sc.nextInt()程序会在第 3 题直接炸掉。这个区别就是“能用”和“好用”的分水岭。3.4 扩展思路菜单循环、非法输入重试真实项目里的交互不会这么简单通常是一个主循环里嵌套多个分支。我经常用这种菜单模式while (true) { System.out.println(请选择功能1新一组练习2查看成绩0退出); if (sc.hasNextInt()) { int cmd sc.nextInt(); if (cmd 0) break; switch (cmd) { case 1 - runQuiz(sc); case 2 - showScore(); default - System.out.println(未知指令); } } else { System.out.println(请输入数字。); sc.next(); // 清掉非法输入 } }这个模式里hasNextInt()配合sc.next()清理非法输入是核心配合。很多初学者喜欢用try-catch包住nextInt()来处理异常其实没必要hasNext系列方法就是为了让你避免异常而设计的用异常当流程控制既不优雅也影响性能。4. 不用 Scanner 怎么写三个替代方案一次说清4.1 什么场景适合放弃 Scanner前面聊了这么多 Scanner 的好处但确实有一些场景用 Scanner 反而是负担。第一种是纯批处理。比如你要生成一张包含 50 道加法题的练习卷输出到一个文本文件。整个过程没有任何人参与输入Scanner 根本派不上用场真正的核心是Random生成题目和BufferedWriter写文件。第二种是性能敏感的程序。Scanner 做类型转换很方便但它内部的正则切分和自动装箱有开销。如果是从超大文件里逐行读取几十万条数据直接上BufferedReader会更快内存也更好控。第三种是你希望自己掌控每一步解析逻辑。Scanner 的默认分隔符是空白字符但有些格式要求按逗号、按固定长度切分这时候你可能会更愿意用split()加手动转换而不是调整 Scanner 的正则表达式。4.2 方案一BufferedReader 手动转换BufferedReader是 Scanner 最常见的替代者典型写法BufferedReader reader new BufferedReader( new InputStreamReader(System.in, StandardCharsets.UTF_8) ); String line reader.readLine(); int answer Integer.parseInt(line.trim());这种方式的优点是把“读一行”和“解析内容”拆开逻辑更直白。但你要自己处理各种NumberFormatException不像hasNextInt()那么体贴。如果只读一个整数Scanner 明显更省事如果是读几千行处理日志BufferedReader 更合适。维度ScannerBufferedReader易用性高自动类型转换低需要手动解析性能中等较高内存自带缓冲、内部正则状态更轻量适合场景交互对话、小文件解析大文本逐行读取非法输入处理有 hasNext 系列方法只能 try-catch4.3 方案二System.console() 的取舍System.console()也是一个交互读取入口用法Console console System.console(); if (console ! null) { String name console.readLine(请输入姓名); char[] password console.readPassword(请输入密码); }readPassword()能在终端里隐藏输入回显这是 Scanner 做不到的。但问题是System.console()在绝大多数 IDE 的 Run 控制台中返回null因为那些环境里没有被分配真正的控制台。也就是说你隔离了 IDE 跑就崩溃。所以它更适合成熟的命令行应用不适合学习和快速原型。4.4 方案三纯批处理模式不读用户输入回到热词“随机生成两个一位数加法练习题 不用 Scanner”。如果需求只是生成题单真正的无交互版本可以这样import java.io.BufferedWriter; import java.io.FileWriter; import java.nio.charset.StandardCharsets; import java.util.Random; public class AdditionWorksheetGenerator { public static void main(String[] args) throws Exception { Random random new Random(); try (BufferedWriter writer new BufferedWriter( new FileWriter(addition_quiz.txt, StandardCharsets.UTF_8))) { for (int i 0; i 50; i) { int a random.nextInt(9) 1; int b random.nextInt(9) 1; writer.write(String.format(第%02d题%d %d ____, i 1, a, b)); writer.newLine(); } } } }没有 Scanner因为这里压根没有读取用户输入的需要。数据流是单向的程序产生内容直接输出到文件。这个例子刚好说明一个常被忽略的事实——Scanner 是输入工具不是输出工具。搞混输入和输出方向是初学者最容易犯的逻辑错误。5. Scanner 高频坑与排查实录5.1 nextInt() 后面跟 nextLine() 为什么会读到空串这是我在各种技术群里见过最多的问题。现象是int num sc.nextInt(); String line sc.nextLine();结果line永远是空字符串。原理前面说过nextInt()只消费数字 token后面的换行符留在缓冲区nextLine()读到这个换行符直接结束。解决办法有三种在nextInt()后面补一个sc.nextLine()把换行符吃掉全部用nextLine()读再自己Integer.parseInt()转换如果需要混合读取就统一用第 2 种方案避免心智负担我个人的经验是交互程序里尽量全部用nextLine()然后手动转换。虽然代码多写两行但至少不会出现“读着读着突然吃掉一行”这种诡异问题。5.2 close() 之后 System.in 也“凉了”很多人习惯在函数末尾写scanner.close()在简单 demo 里没问题但在真实系统里可能埋雷。Scanner.close()如果传入的是System.in会把标准输入流也关闭。同一个 JVM 里一旦关闭System.in后续任何想从控制台读取的代码都会拿到“流已关闭”的异常而且没法恢复。所以我的建议是在 main 方法这种“一次性脚本”里想关就关在一个可被复用的组件里尽量别关如果实在担心资源可以看实际场景用try-with-resources但你要清楚它同样会关闭底层流5.3 hasNext() 不退出卡死循环有同学写了个循环想“读完全部输入”写成这样while (sc.hasNext()) { String word sc.next(); System.out.println(word); }从文件读取没问题因为文件有结尾但从System.in读hasNext()会一直等待新输入永远不会返回 false。想在交互模式下结束你得输入 CtrlDUnix/Linux/macOS或 CtrlZWindows 老的控制台来发送 EOF。正确做法是在交互循环里给个退出条件while (sc.hasNextLine()) { String line sc.nextLine(); if (quit.equalsIgnoreCase(line)) { break; } System.out.println(收到 line); }永远不要在交互程序里依赖 EOF 来结束循环用户根本不知道要按 CtrlD。5.4 中文乱码别让 Scanner 背锅Scanner 在读取控制台输入时编码跟随系统默认字符集。你在 Windows 上跑中文程序如果系统区域设置是 GBK而你的代码文件是 UTF-8很容易出现中文乱码。一个比较稳妥的写法是Scanner sc new Scanner(System.in, StandardCharsets.UTF_8);但要注意控制台本身的编码也必须匹配。从 JDK 18 开始UTF-8 已经成为默认字符集乱码问题在较新版本里会少很多。如果你还在维护老版本 JDK记得检查file.encoding相关配置。5.5 避免用 try-catch 当流程控制最后一个坑也是我特别想强调的不要把InputMismatchException当作正常的流程分支来处理。那种写法长这样try { int num sc.nextInt(); } catch (InputMismatchException e) { System.out.println(输入非法); }看起来也没问题但异常处理的代价远高于普通的条件判断而且一旦出现异常Scanner 内部的状态也需要小心处理你可能还得手动清掉非法 token。正确思路是用hasNextInt()、hasNextLine()做前置校验把非法输入挡在门外异常处理只留给那些真正预料之外的错误。我写交互程序的经验就一句话Scanner 不是洪水猛兽只要你把它当成“带缓冲、带正则、带类型转换的输入翻译器”来用而不是当成一个黑盒 API 来背绝大多数问题都能用底层原理推出来。从最简单的加法练习器到复杂的命令行菜单系统核心永远是用hasNext做校验、用next做读取、用循环做交互这三件事想清楚你的用户交互代码就能比大多数人都稳。