☰
Java输入全解析:从System.in到Scanner与BufferedReader的底层原理和实战
2026/10/8 9:52:40 网站建设 项目流程

如果你接触Java有一阵子,大概会同意一件事:这语言里“输入”这块地方,看起来简单,实际是最容易翻车、也最容易被人敷衍的部分。我见过不少同学能熟练写出各种业务代码,结果面试时让他写个从控制台读取多行整数的程序,当场卡住;也见过项目里的同事因为一个Scanner用顺手了,没处理底层编码,线上日志全是乱码。这篇就把Java输入这件事从底层到实战彻底讲明白。

先说清楚这篇文章讲什么、适合谁。标题是“输入(java)”,但我不想只讲一个API怎么调用。这里会涵盖Java输入的核心层次结构、Scanner的各种隐蔽坑、大数据量场景下的快速输入方案、以及从刷题环境到真实业务系统里输入模块该怎么设计。适合的人群很宽:刚学Java基础的人可以把它当成输入部分的补习材料,参加工作一两年的开发能从中找到不少排查思路,准备面试的人也可以用它来应对常见的输入处理问题。

1. 输入这件事的底子:从System.in到Reader之间到底有几层

很多人写Java输入,代码是这样开始的:

Scanner sc = new Scanner(System.in);

然后就没有然后了。System.in是什么,为什么能扔给Scanner,中间经历了什么,很多人答不上来。但这些东西不搞清楚,后面遇到的很多怪问题都没有解释方向。

1.1 InputStream才是所有输入流的根

先说结论:System.in的类型是java.io.InputStream,它代表的是一个字节输入流。字节输入流意味着它每次读到的只是字节,也就是0到255之间的整数。如果用户在键盘上敲了一个a,这个流里出现的不是字符串"a",而是它的ASCII码97。

直接操作InputStream是极痛苦的。要读一个整数,你得自己处理字节拼接、判断结束、处理换行符。这也正是Scanner、BufferedReader这些工具存在的意义。但请记住一条铁律:所有高级输入类,底层最终都是靠InputStream这个字节流在拉数据。只要看到输入乱码、输入阻塞这类问题,往这个底层方向排查一定没错。

System.in还有一个特殊之处:它是JVM启动时就由系统绑定好的标准输入流,对应着运行程序的控制台。如果程序是在IDE里跑的,它对应IDE的控制台输入框;如果是在服务器上跑的,可能对应着管道输入。这个细节很重要,后面讲资源关闭时会再次提到。

1.2 从字节到字符:桥接的关键一步

字节流本身是没有“字符”概念的。同一个字节序列,用UTF-8解析和用GBK解析,结果是完全不同的字符串。Java对此提供的桥梁是InputStreamReader,它把字节流按照指定字符集解码成字符流:

InputStreamReader reader = new InputStreamReader(System.in, StandardCharsets.UTF_8);

这里就藏着一个非常经典的坑:如果你不指定字符集,InputStreamReader会使用JVM的默认字符集。这个默认值取决于操作系统和JVM启动参数。开发机上可能是UTF-8,但生产服务器如果设置了file.encoding=GBK,同一段代码解析出来的字符串就可能是乱码。我实测过不少情况,中文乱码大概率不是数据坏了,而是输入解码用的字符集和你预期的不一致。

再往下走,字符流上面往往会套一个BufferedReader。它做的事是在内存里开一块缓冲区,一次性从底层流里读一大批字节/字符,然后程序要数据时直接从缓冲区拿,不用每次都触发系统调用。这一层缓冲对性能的影响极其巨大,尤其是做在线评测系统题目那种大规模输入时,差了不是一点半点。

1.3 一张图搞清楚关系:System.in / InputStreamReader / BufferedReader / Scanner

我用文字给这些类的关系画个像:

键盘/文件/网络 --> System.in (InputStream字节流) | v InputStreamReader (字节解码成字符) | v BufferedReader (字符缓冲,readLine按行读) | v Scanner (按token解析,nextInt/nextFloat)

Scanner不是BufferedReader的子类,也不是它的替代品。Scanner自己内部也带有缓冲区,只是它更偏"解析":它把输入拆成一个个token,支持nextInt()、nextDouble()这种携带类型转换的读法。BufferedReader则更偏"原始":它只有readLine()这类基础方法,读取之后怎么办全看你自己。

这里建议先记住一句话:Scanner适合做交互式输入、解析混合类型;BufferedReader适合高效读大量文本;如果你要性能还要解析能力,就自己封装一层。后面每一个结论都会回到这句话上。

2. Scanner用起来顺手,但这些坑你必须心里有数

Scanner确实是很多人的首选:用起来简单,方法名直观,还能自动处理各种类型。但我这些年见过的线上问题和面试失分,很多恰恰出在Scanner身上。

2.1 nextLine与nextInt混用:回车符去哪了

这段代码你应该不陌生:

Scanner sc = new Scanner(System.in); int n = sc.nextInt(); String line = sc.nextLine(); System.out.println("n=" + n + ", line=[" + line + "]");

如果你输入5回车,然后输入hello回车,你会发现输出的line是空的,hello像消失了一样。原因在于:nextInt()读取整数时,只取走了数字部分,输入里的回车符\n仍然留在缓冲区。紧接着调用nextLine()时,它看到缓冲区开头就是一个换行符,于是直接返回空字符串,把真正的下一行留在了缓冲区里。

这种问题在需要“先读数字再读整行字符串”的场景里极其常见,比如读一个整数N后,再读N行文本数据。

解决办法有两个方向:

第一,在nextInt()、nextDouble()这类方法之后,主动调用一次nextLine()把残留的回车消耗掉:

int n = sc.nextInt(); sc.nextLine(); // 吃掉残留换行 String line = sc.nextLine();

第二,干脆不用nextLine(),全都用next()或者nextInt()配合next()。但这样做的代价是无法读取包含空格的字符串,因为next()默认以空白符切分。实际使用中我更推荐第一种,它符合多数业务数据的读取习惯。

2.2 hasNext与阻塞:你以为的“不输入就等”其实在等什么

Scanner有一个常用模式:

while (sc.hasNextInt()) { int x = sc.nextInt(); ... }

很多人在刷题时依赖这个模式,认为循环会在用户输入结束时自动停止。但实际它很狡猾:hasNextInt()不是在判断“后面还有没有整数”,而是在判断“下一个token是否能被解析成整数”。如果用户迟迟不输入,它会一直阻塞等待。只有遇到明确的结束标记(如命令行的Ctrl+D / Ctrl+Z)或者读到了非数字内容,循环才会退出。

这在交互式命令行程序里是个问题:程序会“卡”在读取输入的地方,用户不敲东西它就永远不动。解决方案是明确设计终止条件,比如让用户输入特殊字符结束:

while (sc.hasNextLine()) { String line = sc.nextLine(); if ("exit".equalsIgnoreCase(line.trim())) { break; } // 处理业务 }

相比之下,读文件时hasNext()的语义更明确,因为文件有确定的末尾,到达末尾后不会再阻塞。搞清楚这个区别,能省去很多瞎调试的时间。

2.3 Scanner为什么在大量数据面前变慢

这一点是性能向的硬伤。Scanner解析每个token时,内部都要做正则表达式匹配和类型转换校验。比如nextInt()会先用正则判断当前token是否符合整数格式,然后才做转换。对于只有几个输入的场景,这点开销无所谓。但如果是几十万行、几百万个数字的输入,正则匹配的累计开销会把你的程序从“秒过”拖成“超时”。

我自己做过一个简单实验:用Scanner读100万个整数,再用BufferedReader配合split处理相同数据,前者的耗时差不多是后者的2到3倍。如果改成自己封装的快速输入类,差距能拉到5倍以上。所以参加算法比赛、处理日志文件、解析大数据文本时,请直接放弃Scanner,不要有任何犹豫。

2.4 不要随手关闭Scanner

try (Scanner sc = new Scanner(System.in)) { // 业务代码 } catch (Exception e) { ... }

Java的try-with-resources写法很优雅,但用在Scanner(System.in)上是不合适的。关闭Scanner时,如果它包裹的InputStream实现了Closeable,那么内部的System.in也会被一并关闭。System.in一旦被关闭,程序整个生命周期内都无法再从标准输入读取任何数据。

如果你的程序没退出还在继续跑,后续想再次读取输入,只会得到NoSuchElementException或者读取到null。这在单一任务的刷题程序里问题不大,但在服务器进程、长生命周期应用或者要写单元测试的代码里就是严重bug。原则是:谁创建System.in谁负责关闭,JVM负责的事情别越权。如果是包装了文件流的Scanner,关闭则是必须的。

3. 性能不够用怎么办:高吞吐输入的几种实用方案

当Scanner顶不住的时候,绝大多数需求都可以用BufferedReader加自封装来解决。这一章按推荐程度给出几种方案,从最简单到最彻底。

3.1 方案一:BufferedReader加split,性价比最高

大多数刷题和日志处理场景,这个组合已经够用了:

BufferedReader br = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); String line; while ((line = br.readLine()) != null) { if (line.trim().isEmpty()) { continue; } String[] parts = line.split(" "); int id = Integer.parseInt(parts[0]); double score = Double.parseDouble(parts[1]); // 处理业务 }

它比Scanner快的核心原因有两个:readLine()只负责一次缓冲批量读取,字符串分割用正则只执行一次;parseInt、parseDouble这些封装转换方法非常高效,不会额外做两次检查。代码写起来也直观,任何人都能维护。

这个方案唯一的弱点是split本质上仍然是正则表达式,如果一行有几百万个字符、用几十种不同分隔符切分,它依然会成为瓶颈。但绝大多数业务输入到不了这个量级,所以它是我的默认选择。

3.2 方案二:自己实现FastScanner,榨干剩余性能

如果你需要极致性能,比如算法竞赛里遇到几十MB的输入,可以自己写一个FastScanner。核心思路是:内部维护一个较大的byte[]缓冲区,不断从InputStream批量读取;解析时直接用指针扫描,遇到空白符就完成一个token的提取,数字转换也不走正则,而是手工累加。

一个可用的实现大概长这样(基于常见实践补充的封装):

public final class FastScanner { private static final int BUFFER_SIZE = 1 << 16; private final byte[] buf = new byte[BUFFER_SIZE]; private int pointer, byteCount; private final InputStream in; public FastScanner(InputStream in) { this.in = in; } private boolean hasNextByte() throws IOException { if (pointer < byteCount) return true; pointer = 0; byteCount = in.read(buf); return byteCount > 0; } private int nextByte() throws IOException { return hasNextByte() ? buf[pointer++] : -1; } public int nextInt() throws IOException { int b; do { b = nextByte(); } while (b <= ' ' && b != -1); int sign = 1; if (b == '-') { sign = -1; b = nextByte(); } int result = 0; while (b >= '0' && b <= '9') { result = result * 10 + (b - '0'); b = nextByte(); } return result * sign; } }

这种类在算法竞赛圈子里很常见,我根据常见实践做了一个精简版。它没有任何正则、没有任何锁、没有多余的临时对象,所以吞吐量远超Scanner。实际使用中它的性能大概是Scanner的5到10倍。代价是要自己处理各种边界情况,比如EOF、负数、超长整数、非数字字符混入等,调试成本略高。

3.3 方案三:NIO的Files.readAllLines适合处理文件输入

注意,以上方案主要针对控制台标准输入。真实项目中更多场景是读取文件输入。如果文件不是超大(几十MB以内),用JDK的NIO封装其实非常舒服:

List<String> lines = Files.readAllLines(Paths.get("input.txt"), StandardCharsets.UTF_8);

它本质上还是读取全部内容到内存,好处是代码极简,适合中小文件、离线脚本。劣势也明显:大文件会把内存打爆,这时需要改用Files.lines()返回的Stream做流式读取,或者回到BufferedReader逐行处理的方案。

选择哪种方案,核心取决于数据量和场景:交互式程序用Scanner就够,中等规模的跑批用BufferedReader加split,追求极限或处理超大数据量再用自封装。

4. 输入模块的健壮性:编码、异常与资源管理的正确姿势

性能只是一方面。真实系统里“输入挂了”往往不是慢,而是各种边界情况和环境因素。这一章把所有跟健壮性相关的点集中讲一遍。

4.1 字符集不指定,迟早出乱子

前面提到过InputStreamReader不指定字符集的问题。在这里给出更具体的建议:

  • 凡是读取文本输入,显式传入StandardCharsets.UTF_8,不要依赖JVM默认值;
  • 如果是对接遗留系统,需要根据对方实际编码来选,比如GBK,这时别硬用UTF-8;
  • 如果需要检测编码,可以使用第三方库如juniversalchardet做启发式判断,但永远不要在生产环境里默认“系统说是啥就是啥”。

在Linux服务器上,环境变量LANG、LC_ALL和JVM的默认编码未必一致,所以最稳妥的方式就是写代码时把字符集焊死。

4.2 空输入与格式错误:不能把“读不到”直接当错误

处理输入时常见两个极端:一个是无视空输入,程序一读到null就崩;另一个是把空输入当重要业务逻辑,层层排查半天。正确的是分情况处理。以文件输入为例:

try (BufferedReader br = Files.newBufferedReader(Paths.get(path), StandardCharsets.UTF_8)) { String line; while ((line = br.readLine()) != null) { if (line.isBlank()) { continue; // 跳过空行,而不是报错 } // 正常解析 } }

空行是否该“跳过”还是“报错”,取决于业务规则。比如某些配置文件允许空行出现;但如果是用户填写固定格式的表格导入,空行可能意味着导出方的bug。我的习惯是:先判断输入是否允许为空,再决定如何响应。解析时遇到的异常要带行号、token内容一起抛出,方便排查:

try { int id = Integer.parseInt(parts[0]); } catch (NumberFormatException e) { throw new IllegalArgumentException("第" + lineNumber + "行数据格式错误:[" + line + "]", e); }

这样写出来的代码,出了问题找起来效率会高很多。

4.3 资源关闭与控制台输入的冲突处理

这一块是很多线上事故的根源。用文件流时,try-with-resources自动关闭完全没有问题;但面对System.in就必须非常谨慎。我建议的实践是:

  1. 不要在业务代码里关闭System.in,除非是程序退出前的最后一刻;
  2. 如果一定要封装读入工具类,不要让它实现Closeable,或者让close()方法是空操作,避免被误调用;
  3. 用测试框架时特别注意:有些单元测试会继承进程级的标准输入,一旦某个测试把System.in关了,后续所有依赖标准输入的测试都会连锁崩溃。

很多年我都在这个坑上栽过:一个后台任务进程初始化时读了用户输入,后来进程改成长驻服务,某次重构时有人在finally里sc.close()了,结果整个进程再也不能热加载配置,查了很久才定位到是标准输入被关闭。所以这块真的值得多写两行防御代码。

4.4 大输入的流式处理思维

面对超大文本,一次性读进内存是最容易爆内存的做法。正确的思维是“流式”:无论数据多大,内存里始终只保留当前处理的那一行或那一个块。

try (Stream<String> lines = Files.lines(Paths.get("big.log"), StandardCharsets.UTF_8)) { lines.filter(line -> line.contains("ERROR")) .limit(100) .forEach(System.out::println); }

Files.lines()返回的是懒加载流,它不会把所有行都加载进内存。配合filter、limit这种中间操作,能有效控制内存占用。这里有个细节:流必须写在try-with-resources里,因为Stream底层持有文件的读取句柄,不关闭会导致句柄泄漏。文件句柄泄漏到一定数量,进程就算内存够用也读不了新文件。

5. 从刷题到业务系统:输入解析模块该如何设计

处理完底层细节,我们把视角拉高一点:一个项目中输入处理不该是随手写的一坨代码,它值得作为独立模块来对待。

5.1 刷题场景:多行、多区间输入的解析套路

很多算法题是“先给一个N,接着N行数据”或“先给个区间数,然后每行两个整数”。典型的热搜词里有“禾木省电”这类问题:第一行是灯泡数量,后两行是区间,每行两个整数。解析时如果不知道套路,代码会变得很乱。

一个比较稳定的解析套路:

FastScanner fs = new FastScanner(System.in); int n = fs.nextInt(); int l1 = fs.nextInt(), r1 = fs.nextInt(); int l2 = fs.nextInt(), r2 = fs.nextInt(); // 直接进入业务处理

关键是把“解析输入”和“业务处理”分开。输入格式一旦确定,就用一个专门的方法负责把所有数据读成本地变量或简单数据结构;业务逻辑只认这些变量。不要边读边算,否则一旦格式变了,整个逻辑都要推倒重来。

5.2 业务系统:把输入从“读控制台”抽象成“输入源”

真实业务系统很少直接从控制台读输入,更多是从HTTP请求、配置文件、消息队列、Excel导入这些渠道进来。这时候你需要的不是Scanner,而是一个统一的“输入解析层”。举个例子,一个导入用户数据的接口可能长这样:

public class UserImportProcessor { public void process(InputStream rawInput) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(rawInput, StandardCharsets.UTF_8))) { String line; int lineNum = 0; while ((line = reader.readLine()) != null) { lineNum++; UserRecord record = UserRecord.parse(line, lineNum); // 校验、落库 } } catch (IOException e) { throw new ImportException("读取导入文件失败", e); } } }

注意这里方法接收的是InputStream而不是具体文件路径。这样好处是调用方可以是文件上传、FTP下载、HTTP请求体,甚至可以直接传System.in,最大程度复用代码。你在设计输入模块时,多往“抽象”方向靠,后期会非常省心。

5.3 单元测试里的输入模拟:如何可靠地测试输入逻辑

很多开发者头疼“怎么测试那些读取控制台的代码”。一个有效的办法是:不要在你的核心方法里直接引用System.in,而是把输入流作为方法参数传入。这样测试时可以直接构造字符串流:

String data = "3\n1 5\n2 6\n"; InputStream in = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); YourParser parser = new YourParser(in); List<Interval> intervals = parser.readIntervals(); assertEquals(2, intervals.size());

ByteArrayInputStream可以把任意字符串变成InputStream,测试时无需模拟用户敲键盘,也不依赖环境。这个方法可以用到所有涉及输入的方法上,是一个非常划算的重构。

我再补充一个我自己踩过的坑:不要用Scanner去读ByteArrayInputStream以外的数据源时随意混用hasNextLine和nextLine,它们的缓存和游标机制在循环中很容易出现多读一行或少读一行的问题。遇到复杂输入格式,用BufferedReader.readLine()按行拿数据,再用split或正则解析,逻辑上更好控制。

5.4 输入解析的扩展点:自定义分隔符与自定义对象解析

当输入不是简单的空格分隔时,你需要考虑扩展。比如CSV文件用逗号分隔、Excel导出数据可能带引号、日志可能用|分隔,这些东西Scanner都支持自定义分隔符:

sc.useDelimiter("\\|");

但我个人更推荐用String.split或者成熟的解析库(如Apache Commons CSV、Jackson CSV)处理复杂格式,因为正则和状态机处理带引号的转义非常痛苦。输入解析模块的正确分层是:

  • 低级IO层:负责从流中读字节/字符,解决编码、缓冲、关闭问题;
  • 语法解析层:负责按分隔符拆token、按格式转换类型,出错时报出准确位置;
  • 业务模型层:负责把token映射成业务对象,做业务校验。

照这个思路写代码,即使将来输入格式大改,你也能把改动控制在一个层面内,不会全线崩溃。

6. 一套可以直接用的输入工具类与配套使用示范

这一章我给出一个完整的、可以直接复制进项目的输入工具类,以及配套的使用示范。它结合了前几章的要点:性能好、支持UTF-8指定、按行处理、不易误关流。

6.1 工具类:增强版LineInputReader

这个工具类的设计目标是:兼顾BufferedReader的按行读取和类型转换能力,同时把源头InputStream的所有权交给调用方。

public final class LineInputReader implements AutoCloseable { private final BufferedReader reader; private final boolean closeSource; public LineInputReader(InputStream source, Charset charset) { this(source, charset, false); } public LineInputReader(InputStream source, Charset charset, boolean closeSource) { this.reader = new BufferedReader(new InputStreamReader(source, charset)); this.closeSource = closeSource; } public String nextLine() throws IOException { return reader.readLine(); } public int nextIntLine() throws IOException, NumberFormatException { String line = nextLine(); if (line == null) { throw new NoSuchElementException("输入流已到达末尾"); } return Integer.parseInt(line.trim()); } public String[] splitLine(String line, String separator) { if (line == null) return new String[0]; return line.split(separator); } public List<Integer> readAllIntsFromLine(String line) { List<Integer> result = new ArrayList<>(); for (String part : line.trim().split("\\s+")) { if (part.isEmpty()) continue; result.add(Integer.parseInt(part)); } return result; } @Override public void close() throws IOException { if (closeSource) { reader.close(); } } }

设计上有一个细节我觉得特别值得提:因为closeSource默认为false,你创建一个读取System.in的工具类实例后,即使它被try-with-resources包裹也只会关闭BufferedReader本身,System.in不会被关闭。只有你明确知道某个InputStream来源需要管理时,才把closeSource设为true。这个开关极大减少了误关标准输入的概率。

6.2 使用示范:解析多行多区间输入

回到热搜词里的场景“输入两个区间,把区间内灯泡关上,求亮着的灯泡”,它的输入解析用这个工具类可以这样写:

public static void main(String[] args) throws IOException { LineInputReader input = new LineInputReader(System.in, StandardCharsets.UTF_8); int n = Integer.parseInt(input.nextLine().trim()); // 第一行:灯泡数量 int[] l = new int[2]; int[] r = new int[2]; for (int i = 0; i < 2; i++) { String[] parts = input.nextLine().trim().split("\\s+"); l[i] = Integer.parseInt(parts[0]); r[i] = Integer.parseInt(parts[1]); } boolean[] off = new boolean[n + 1]; for (int i = 0; i < 2; i++) { for (int pos = l[i]; pos <= r[i]; pos++) { off[pos] = true; } } List<Integer> onBulbs = new ArrayList<>(); for (int i = 1; i <= n; i++) { if (!off[i]) onBulbs.add(i); } System.out.println(onBulbs.size()); }

这是一个很朴素的处理方式,实际竞赛题还可能要求输出编号、或者这类区间操作有更优解法。但单从输入解析来看,这种写法保证每一行只读取一次,解析和业务隔离得很清楚。即使区间规律复杂,修改成本也很低。

6.3 应用在真实业务中的示例:读取配置文件

业务系统里,一台机器上经常只有一个System.in,但会有多个配置文件。用这个工具类读取配置文件时,建议还是用文件InputStream,并且把closeSource设为true:

try (LineInputReader configReader = new LineInputReader( new FileInputStream("app.properties"), StandardCharsets.UTF_8, true)) { String line; while ((line = configReader.nextLine()) != null) { if (line.isBlank() || line.startsWith("#")) { continue; } String[] kv = line.split("=", 2); settings.put(kv[0].trim(), kv[1].trim()); } }

注意这里try-with-resources会在结束时自动关闭,而FileInputStream也会被正确关闭,不会泄漏句柄。同一个工具类,在控制台输入和文件输入两种场景下各自表现得当,靠的就是那个closeSource开关。

7. 我处理Java输入时踩过的三个真实坑,希望你绕开

这一章不写理论,写我自己真实踩过、并且花了不少时间排查的故障。如果能帮你少走几步弯路,这篇东西就算没白写。

7.1 生产环境中文乱码的排查:别只查业务代码

有一年我做定时任务,需要从外部FTP拉取一批包含中文的文本文件,入库后统一转成大写。结果上线后,某一天突然有客户反馈名称里的“张”变成了乱码。

我当时第一反应是数据库连接串没指定characterEncoding,查了一遍没有问题。后来又怀疑FTP传输模式,把二进制模式改来改去也没有根治。最后才发现,问题出在读取文件的代码压根没指定编码,而FTP服务器的地区设置让JVM默认字符集变成了GBK,文件本身其实是UTF-8。两边一错位,中文全部变样。

那次之后,我给团队定了一条硬规矩:凡是读写文本,都必须显式指定Charset,任何地方都不允许使用依赖环境默认编码的构造器。后续再也没出过同类问题。如果你也在写类似代码,强烈建议现在就自查一遍,尤其是那些用new InputStreamReader(inputStream)没带第二参数的地方。

7.2 一次误关System.in导致的半夜故障

还有一次线上问题更隐蔽。服务里有个功能允许用户在控制台手动输入一个业务标识,本来只在开发调试时用。后来有人图省事,在主流程里加了个Scanner读取一段配置,代码长这样:

public static String loadToken() { try (Scanner sc = new Scanner(System.in)) { return sc.nextLine().trim(); } }

当这个服务被放上服务器,以守护进程方式运行时,System.in其实没有真实输入,进程的标准输入被重定向到了/dev/null。loadToken每次都会读到null或者直接抛异常。按理说这个函数会失败然后走备用逻辑,但诡异的是,它在失败前先执行了sc.close(),把标准输入流给关了。后续其他组件想要再读标准输入,全都在拿一个已经关掉的流,抛出一堆IOException。

排查过程很痛苦,因为报错分散在好几个模块里,谁也不觉得是自己同事的一行Scanner.close()引起的。最后是在进程启动参数上加了-verbose:gc之类的一通折腾,才靠代码走查发现问题。这个教训让我在写“资源关闭”相关逻辑时变得非常偏执:永远问一句,这个流的所有者是谁?我有没有权力关它?

7.3 刷题时的超时,不一定是算法慢

第三个坑是关于性能误判的。我有段时间练习算法题,总觉得自己写的解法复杂度已经很低了,但提交上去就是超时。反复优化算法没效果后,我用随机大数据在本地测了一次,才发现光是Scanner读数据就占了将近一半的时间。把Scanner换成BufferedReader + StringTokenizer之后,同样的算法直接从超时变成接近满分的耗时。

这件事给我的启发是:性能问题要先定位瓶颈再优化,而输入读取恰恰是很多人性能分析的第一步盲区。如果你也是那种“算法没问题但总是超时”的选手,先别怀疑人生,把输入方案换成快速IO试试,大概率有惊喜。

这几个坑串起来,其实都指向同一个核心:Java的输入不只是一行Scanner sc = new Scanner(System.in)那么简单。底层字节流、字符集、资源所有权、性能特性,每个维度都值得认真对待。

最后再分享一个小技巧:拿到任何输入相关的需求和bug时,先从“数据从哪来、字节怎么变成字符、字符怎么变成对象、资源由谁关闭、性能是否过关”这五个问题入手逐一排查。按这个顺序来,大部分输入问题都会清晰很多。这个清单我用了很久,每次都很管用,希望你也能用得上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询