资讯动态

Java Scanner入门:从输入解析到异常防御的实战指南

发布时间:2026/10/14 5:42:44 来源:尧图企业网站定制
给初学 Java 的同学讲讲 scanner 吧。这东西说复杂真不复杂说简单用错了晚上又得睡不着。1. 重新认识 Scanner输入读取器的职责边界很多教程一上来就告诉你new Scanner(System.in)然后nextInt()、nextLine()好像Scanner生来就是为了读键盘输入的。这个理解不能说错但太窄了。Scanner的本质是一个通用文本解析器它负责把一个数据源里的字符流按你指定的规则切成一段段可以用的内容。它的数据源可以是键盘System.in可以是文件File对象可以是字符串String甚至可以是网络流。日常写练习、写小工具时我们大部分时间在跟System.in打交道但搞清楚它能读什么、不能读什么后面很多问题会少走弯路。先看一段最常见的代码import java.util.Scanner; public class Demo { public static void main(String[] args) { Scanner sc new Scanner(System.in); System.out.print(请输入一个整数); int n sc.nextInt(); System.out.println(你输入的是 n); sc.close(); } }这段代码跑起来没问题但它隐含着一个前提用户真的会输入一个整数。如果用户手抖输了个abc程序直接抛InputMismatchException一句话不说就崩了。这正是Scanner的第一个认知要点Scanner不负责校验输入是否合法它只负责把能匹配的内容解析出来匹配不上就抛异常。所以基础用法并不意味着不需要防御恰恰相反基础用法里就必须考虑输入非法时怎么办。2. 输入源、分隔符与读取方法的对应关系Scanner内部维护了一个缓冲区还有一个分隔符模式。默认情况下分隔符是空白字符——空格、制表符、换行符都会被当作分隔符处理。当你调用next()、nextInt()这类方法时Scanner会先跳过所有分隔符再读取下一个有效内容并且这个有效内容到下一个分隔符之前为止。这就带出了一个关键结论nextInt()并不会吃掉你输入行尾的回车。它只把整数取走换行符还留在缓冲区里。很多人第一次用nextLine()接在nextInt()后面时发现读到了一个空字符串根源就在这里。如果你打算严格按行来读取内容比较稳的做法是一行一行读进来再解析Scanner sc new Scanner(System.in); while (sc.hasNextLine()) { String line sc.nextLine().trim(); if (line.isEmpty()) { break; } // 按业务需求解析这一行 }这个写法在竞赛、OJ题里很常见因为它对输入格式的适应性更强。你不需要预先知道一行里有几个整数拿到字符串之后再用手头的工具拆就行。关于读取方法的选区我整理了个简单的对照表新同学可以直接照着用方法适合的场景注意点next()读取单个单词默认按空白切分不含空格nextLine()读取一整行会消耗掉行尾换行nextInt()读取单个整数不能攒着不用否则可能阻塞nextDouble()读取浮点数小数格式与操作系统区域设置有关hasNext()判断还有没有内容常配合while循环使用useDelimiter()自定义分隔符正则表达式形式进阶再试初次接触时不用急着把每个方法都记住能把nextLine()和nextInt()区别清楚已经能避开一大半的坑了。3. 一个容易被忽略的事实Scanner 工作的是流而不是命令在学习Scanner的过程中有一个思维转变特别重要System.in是流不是一条条等待回答的问答式命令。这意味着用户输入的内容是提前准备好的或者至少要有一个持续供给的机制程序只是按自己的节奏去消费这些数据。有个经典现象能说明这个区别你在控制台交互式地跑程序第一次nextInt()会停下来等待输入这让人误以为程序在问用户问题。其实Scanner只是发现缓冲区里没有数据阻塞等待流中有数据到来而已。用户在终端里敲了一堆数字和空格后按回车这些内容才一次性进入缓冲区Scanner再逐个解析。换个场景就更好理解了。如果数据源不是System.in而是一个文件Scanner sc new Scanner(new File(data.txt)); while (sc.hasNextInt()) { int value sc.nextInt(); System.out.println(value); } sc.close();文件的内容是静态的从头到尾Scanner按顺序消费不会等一下用户输入。同一个Scanner两种数据源它的工作方式一样但感知完全不同。理解了流的这个特性你在读代码时就不会困惑循环里调用nextInt()数据到底从哪来。反而要注意的是hasNextInt()并不保证下一个一定是整数。它只是Scanner尝试用当前分隔符模式下次去匹配如果匹配失败就返回false。如果你的文件里混着数字和文字用hasNextInt()做循环条件并不总是安全。这时更合适的做法是先hasNextLine()读整行再对行内内容做拆分解析。4. 阻塞、死循环与异常那些让新手崩溃的边界情况基础用法的文章往往会给人一个错觉Scanner的代码看起来很简单照着写总不会出问题。实际上真正让初学者卡壳的往往是几个不太基础的边界情况。第一个是阻塞。如果你在循环里写了while (sc.hasNextLine())且数据源是键盘那么程序永远不会自行结束——因为它一直在等待下一行输入。想人为终止得在循环里判断特定字符串比如break或exit。用文件做数据源时没有这个问题文件读完了流也就结束了。第二个是nextInt()配合nextLine()时那个经典尴尬场景Scanner sc new Scanner(System.in); int n sc.nextInt(); String name sc.nextLine(); // 这里往往读到的是空字符串就像前面说的nextInt()留下了换行符nextLine()一上来就把这个残留换行消费掉了自然读不到用户后面输入的内容。解决办法有两个习惯一个是在两个读取之间加一行sc.nextLine()把残留换行吃掉另一个是干脆统一用nextLine()读行再用Integer.parseInt()转换。后者在需要连续读多行不同数据类型时更加省心。第三个是异常处理。Scanner读文件时构造方法会抛FileNotFoundException解析类型不匹配时会抛InputMismatchException这些异常如果不在代码层面兜住程序就只能崩溃。很多新同学在本地IDE里跑通了代码一到在线判题环境或者打包给别人用时突然崩掉多半就是忽略了异常分支。一个相对稳妥的交互式输入模板长这样Scanner sc new Scanner(System.in); int n 0; while (true) { System.out.print(请输入一个整数); if (sc.hasNextInt()) { n sc.nextInt(); break; } else { System.out.println(输入无效请重新输入。); sc.next(); // 消耗掉错误的 token防止死循环 } } sc.close();这里有个细节值得拎出来说else分支里的sc.next()是干什么用的。如果用户输入了一个非整数这个非整数的token会一直留在缓冲区里如果不清掉下一次循环hasNextInt()又看到它又校验失败就形成了死循环。先用next()把这个坏token消耗掉才能继续等待用户重新输入。这个小小的防御动作能把一次糟糕的交互体验变成勉强可用的交互体验。再看 NoSuchElementException。当你调用sc.nextInt()时如果流里已经没有任何内容了hasNextInt()这个判断你都跳过了直接读取就会抛这个异常。典型场景是在循环中读取文件时循环条件错误地多跑了一次。应对方案就是在读取前用hasNext系列方法判断一下这是成本最低的防御手段。5. 从读得动到读得对Scanner 的进阶使用技巧当你不满足于能把内容读出来而是希望读出来的内容刚好是我想要的Scanner 的一些技巧就派上用场了。5.1 自定义分隔符不用再手动切割默认情况下Scanner用空白字符作为分隔符这覆盖了大多数日常场景。但有些格式的数据比如用逗号分隔的CSV行或者用管道符分隔的日志片段你就得自掏腰包了。useDelimiter方法可以改变分隔规则Scanner sc new Scanner(apple,banana,orange); sc.useDelimiter(,); while (sc.hasNext()) { System.out.println(sc.next()); } sc.close();分隔符参数是正则表达式所以useDelimiter(\\s*,\\s*)这类的写法允许逗号前后有空格解析出来的内容会自动trim掉两端空白。实际项目中这种处理方式比读一整行再split来得更直接——尤其当数据流很大比如几十万行的日志逐行split会产生不少临时对象而Scanner配合自定义分隔符可以在流式读取过程中消费数据内存占用表现好一些。但也要注意如果分隔符设置得过于复杂正则匹配的开销会抵消掉Scanner的轻便优势。大部分情况下split完全够用只有当数据源确实是流形态比如从网络连接读入时Scanner 的自定义分隔符才显得不可替代。5.2 配合正则表达式做模式匹配Scanner的next(Pattern)和hasNext(Pattern)支持传入正则这能在读取时就完成筛选。比如一段文本里混杂了身份证号、手机号、普通单词你想把所有手机号段给提取出来Scanner sc new Scanner(source); Pattern phonePattern Pattern.compile(1[3-9]\\d{9}); while (sc.hasNext()) { if (sc.hasNext(phonePattern)) { System.out.println(sc.next(phonePattern)); } else { sc.next(); } } sc.close();这段代码的逻辑是挨个token检查能匹配手机号模式就打出来不能匹配就跳过。这种做法在数据清洗场景中很实用它把Scanner的流式处理和正则表达式的匹配能力结合在了一起代码量比传统读全部然后正则匹配更省内存也更符合数据流的思维。不过正则表达式这个切入点对新手不算友好第一次接触时如果看不懂可以先跳过不影响掌握基础用法。等你用Scanner处理过几个真实项目之后再回来品味会发现这个结合点确实是设计得很巧妙的。5.3 用 Scanner 读配置文件时注意编码问题有人会偷懒直接用Scanner读含中文的配置文件然后发现读出来的内容是乱码。这不是Scanner的机制有问题而是构造方法默认使用了平台默认字符集在不同操作系统上表现不一致。解决方法是显式指定字符集Scanner sc new Scanner(new File(config.txt), StandardCharsets.UTF_8.name());现在Java 11以上已经推荐用Charset参数不推荐传字符串。代码写起来长一点但跨平台表现稳定。凡是涉及文件读取我不建议省略字符集参数省一行代码的成本在某个用户机器的Windows上会变成一晚上的排查。6. Scanner 与 BufferedReader 的选择什么时候不该用它聊到读取输入总是绕不开Scanner和BufferedReader的对比。这个话题很多新手会感到困惑因为网上说法很杂有人说Scanner好有人说BufferedReader快。它们其实定位不同Scanner 功能完整自带类型转换、分隔符、正则匹配适合做解析。BufferedReader 只做字符流的缓冲读取readLine()读出来的是原始字符串后续转换得自己写。从性能角度衡量如果只是读大量文本行而不需要复杂解析BufferedReader 通常更快因为它少了很多类型判断和模式匹配逻辑。反过来如果你读到的每行都要拆成整数、小数、单词Scanner 写起来省力太多。我的个人习惯是刷算法题、写在线判题代码用BufferedReader读整行再splitparse省去Scanner的自定义配置性能也放心。写业务工具、解析非标准格式输入用Scanner尤其需要用自定义分隔符或正则筛选时。交互式控制台程序用Scanner因为它对类型读取的支持好代码可读性高。有人担心Scanner不关闭会资源泄漏。在System.in场景下程序结束进程就回收了不关也不会有实际危害但读文件时如果不关闭文件句柄会一直被占用在Windows上甚至会导致文件被锁住无法删除。所以规范写法里涉及文件或网络流的Scanner一定要在try-with-resources或finally里关闭。另外有一个细节值得注意sc.close()会连带关闭它读的底层流。如果你把同一个文件流既用Scanner读又用其他方式读先执行sc.close()后续的流操作就可能异常。一般不建议在代码里多处共享同一个流对象这也是一个实操排坑的经验。7. 从数据格式出发反过来设计你的 Scanner 代码最后说一个思路层面的东西。我看过不少同学写的Scanner代码它们都有一个共同特点先从new Scanner(System.in)开始然后摸黑往下写遇到读不出来再往上补。这种从工具出发的写法在数据格式稍微复杂一点时就会翻车。更顺的流程其实是从数据格式出发来设计代码。先问自己三个问题数据源是谁键盘、文件、字符串、网络流数据是按行组织还是按空白/token组织读出来的原始内容需要转换成哪些类型还是保持字符串即可想清楚这三件事之后Scanner的用法自然就清晰了按行读就围绕nextLine()和hasNextLine()展开按token读就围绕next()、nextInt()、nextDouble()展开要自定义分隔符就加上useDelimiter()。举个例子如果输入数据是这种格式张三,95 李四,88 王五,73那么按行读最稳妥读到后拆,左右两侧再对右侧转int统计。如果非要用useDelimiter([,\\s])一个token一个token地扫虽然也能做出来但遇到数据里姓名本身带空格时你就会被自己设计的规则坑到。Scanner是个很朴素的好工具没有太多花哨的魔法把握住数据源、分隔符、读取方法、异常防御这四个点大部分应用场景都已经覆盖了。实际写代码的过程中多顺手处理些hasNext和无效token的边界情况能省下后面很多调试时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑