☰
Java ACM模式输入输出全攻略:从Scanner到BufferedReader避坑指南
2026/10/20 9:09:13 网站建设 项目流程

不少人刷题时在本地IDE里跑得好好的,一上OJ就疯狂报错,尤其是用Java的同学,经常在输入输出上栽跟头。ACM模式又叫“标准输入输出模式”,和平时写业务代码时那种“拿参数调方法”完全不一样,它要求你的程序自己去从控制台读数据、往控制台写结果。很多人一开始不适应,其实核心就一句话:你得搞清楚OJ给你喂了什么格式的数据,以及你该怎么把它们稳稳当当地接住、计算、再吐出去。

这篇东西我准备了很久,把我这些年用Java刷各种OJ平台(牛客、洛谷、力扣周赛、各类高校OJ都算)踩过的输入输出坑,系统性地捋一遍。覆盖EOF多组输入、固定行数读取、一行不定长数据、字符串与数字混读、大数处理、以及输出时的性能优化。不光给代码模板,还会解释每个关键写法背后的道理,让你不只是“背模板”,而是真正理解为什么这么写,遇到变体也能灵活应对。

如果你正在准备面试手撕代码、参加算法竞赛,或者刚开始在OJ上刷题,这篇强烈建议收藏,按需查阅。

1. 理解OJ输入的本质:一个文件流从头读到尾

1.1 为什么本地跑得好好的,一上OJ就出事

你在IDE里写Scanner scanner = new Scanner(System.in),然后nextInt()一个接一个读,本地测试时手动敲几个数字,程序跑完收工,看起来没问题。但OJ不是这么玩的,OJ会把你程序的标准输入重定向到一个测试文件,里面可能装着几百行、几千KB的数据,你的程序要自己判断“数据什么时候结束”,而不是等人敲回车。

举个最常见的例子,很多题目要求“多组测试数据,每组两个整数,读到文件末尾为止”。你以为读两个数就完事了,结果下一组数据还堆在输入流里,程序已经提前退出,输出自然就少了一截。或者在循环里用while(true)去读,没有正确的终止条件,结果超时或者数组越界。

这就是为什么必须搞清楚OJ评测的输入格式约定:是单组数据,还是多组数据,多组数据以什么标志结束(EOF?读到0 0?先给一个T?)。每个OJ页面题目的Input描述都会写清楚,但很多人根本不看,直接抄模板,然后就是无尽的WA(错误答案)和RE(运行时错误)。

1.2 输入数据的三种基本形态

我总结下来,OJ的输入形态就三种,90%的题都能归进去:

第一种:单组输入。就一组数据,读完了直接算,不需要循环。比如“读入一个整数n,输出它的阶乘”。

第二种:固定组数的多组输入。第一行告诉你一共有T组测试数据,然后后面跟着T块数据,每块数据的格式可能相同也可能不同。比如“第一行是T,接下来T行,每行两个整数a和b,输出a+b”。

第三种:未知组数的多组输入(EOF结束)。题目说“输入包含多组测试数据,处理到文件末尾”,这时候你必须在循环里一直读,直到hasNext()返回false或者readLine()返回null。

还有一种比较特殊但很常见的变种:多组输入,但以某个特殊值作为结束标志。比如“每行两个数,以0 0作为输入的结束标志,不处理0 0这一行”。这种本质上也是在循环里读,只不过终止条件变成了“读到特定的值”,和EOF处理的思路很接近,只是多个判断而已。

理解了这三种形态,你就知道为什么那么多模板代码里第一行都是while (scanner.hasNext())了。

2. 读入层的选型:Scanner、BufferedReader与StreamTokenizer

2.1 各有各的活法:三种读入工具的性格差异

Java里常用的读入工具就这么几个:Scanner、BufferedReader、StreamTokenizer,可能还有DataInputStream和一些第三方快读库。

Scanner用起来最爽,nextInt()、nextLine()、hasNext()这些方法名字一看就懂,而且内置了正则解析,能直接帮你把字符串转数字,处理空格、Tab、换行都得很干净。但它慢,特别慢。原因是它内部做了一层正则匹配和字符解码,每次读一个数字都要消耗额外的解析开销。数据量小的时候感觉不出来,一旦数据量上到几十万行、几百万个数字,Scanner就能把你的程序从1秒拖到5秒,直接TLE(超时)。

BufferedReader是字符流,按行读取,速度快,它本身就是用来做缓冲的。但你得自己处理字符串转数字的活,Integer.parseInt()自己调,读一行字符串再split,虽然代码啰嗦一点,但性能比Scanner高一个档次,是绝大多数OJ选手的主力工具。

StreamTokenizer是更底层的流标记器,它按字节/字符来切分token,不搞正则,速度比BufferedReader还要快,写起来也最别扭。适合数据量特别大的极端场景,比如单场10万级别的数据量。我一般只在被TLE逼急了的时候才用它。

2.2hasNext()和nextLine()的经典陷阱

hasNext()这个方法,判断的是“输入流中是否还有下一个token”,不是“是否还有下一行”。什么是token?就是被分隔符隔开的一段字符,默认分隔符包括空格、Tab、换行。所以如果你用hasNextLine()判断,只要后面还有换行符就算true,即使那一行其实只有空字符串。这两个方法混用是刷题新手翻车的高发区。

另一个坑是nextLine()和nextInt()混用。nextInt()只读走数字,留在输入缓冲区里的换行符它不管,紧接着调用nextLine(),你读到的往往是一个空字符串。我见过无数人在这上面卡一个多小时,反复调试都不知道错在哪。

如果看到题目数据里下一行是个字符串,而上一行你用nextInt()读了数字,那就要小心了。稳妥的做法是:要么全部用nextLine()读整行再手动解析,要么在nextInt()之后先补一个nextLine()把残留在缓冲区的换行符消费掉。

2.3 私藏的日常读入模板

我个人最常用的读入组合是BufferedReader + StringTokenizer,写一个简单的封装:

import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer tokenizer; String line; while ((line = reader.readLine()) != null) { if (line.trim().isEmpty()) { continue; // 跳过空行,有些OJ数据末尾会有空行 } tokenizer = new StringTokenizer(line); while (tokenizer.hasMoreTokens()) { int a = Integer.parseInt(tokenizer.nextToken()); // 处理数据... } } reader.close(); } }

这个模板的好处是:readLine()读到文件末尾返回null,EOF判断非常明确;StringTokenizer按空白分割,不关心多个空格还是Tab。而且BufferedReader读行本身就带缓冲,性能足够对付绝大多数题。

3. 七种最常见的输入场景解析与代码模板

3.1 场景一:多组整数对,EOF结束

这是最基础的场景,题目长这样:“输入包含多组测试数据,每组占一行,包含两个整数a和b,处理到文件末尾。”输出的也是每行一个结果。

核心就是一个while循环,直接上代码:

import java.io.*; public class Main { public static void main(String[] args) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String line; while ((line = reader.readLine()) != null) { // 跳过可能出现的空行 if (line.isEmpty()) { continue; } String[] parts = line.trim().split("\\s+"); int a = Integer.parseInt(parts[0]); int b = Integer.parseInt(parts[1]); System.out.println(a + b); } reader.close(); } }

为什么要line.trim()再split("\\s+")?因为split(" ")遇到多个连续空格或者Tab就会切出空字符串,Integer.parseInt("")直接炸。trim()去掉行首行尾空白,\\s+表示一个或多个空白字符。这两步组合出来的代码,遇到各种奇奇怪怪的空白格式都能扛住。

如果题目要读的是多行的一组数据,比如每组先给一个n,再给一行n个数,那就在循环体内用同一个reader继续读下一行,别忘了每次重新new一个StringTokenizer或者重新split。

3.2 场景二:固定组数T,每组数据行数不同

这类题先给一个T,然后循环T次,但每组里可能又包含更多子结构。典型例子:“第一行一个整数T,表示测试组数。每组第一行一个整数n,第二行n个整数。”

这种场景要注意的是:T读完之后,每组内的数据怎么读取。很多人直接在while(T--)里读n,再读第二行,但第二行的n个整数可能在换行上不固定——可能全在一行,也可能按每个数字换一行,甚至折成两行。如果你默认“第二行正好放n个整数”,一旦折行就出问题。

稳妥的做法是不管换行,直接用token流式读取:

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer tokenizer; String line; // 先用readLine读T while ((line = reader.readLine()) != null) { if (line.trim().isEmpty()) continue; int T = Integer.parseInt(line.trim()); for (int i = 0; i < T; i++) { int n = Integer.parseInt(reader.readLine().trim()); // 读n int[] arr = new int[n]; int index = 0; while (index < n) { tokenizer = new StringTokenizer(reader.readLine()); while (tokenizer.hasMoreTokens() && index < n) { arr[index++] = Integer.parseInt(tokenizer.nextToken()); } } // 处理arr... } }

这个内层while处理的是“n个整数可能分布在多行”的情况,每一行都取token,直到取够n个。很多标答直接用for (int i = 0; i < n; i++) arr[i] = nextInt(),那是建立在数据严格每行一个整数的基础上。你按我的写法,不管它怎么折行都不会翻车。

类似的还有“每组开头一个M,后面跟着M个字符串”“每组两个数组长度等”,思路都是一样的:先读结构头,再按数量取数据。

3.3 场景三:一行内不定长数据,以分隔符分割

有一些题目比较阴,它不告诉你一行有几个数,只告诉你“输入一行若干个整数,以空格(或逗号)分隔”。比如:“输入一行包含若干个整数,计算它们的和。”

这种就老老实实读整行再split,但要注意分隔符到底是空格还是逗号:

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String line = reader.readLine(); String[] items; if (line.contains(",")) { // 兼容逗号+空格混用 items = line.split("[,\\s]+"); } else { items = line.trim().split("\\s+"); } long sum = 0; for (String item : items) { sum += Long.parseLong(item.trim()); }

[,\\s]+这种写法表示“逗号或空白字符出现一次或多次”,这样逗号后面带不带空格都能正确切分。有些题目的数据分隔符可能更变态,比如分号、竖线,那你把正则里的字符换掉就行。

如果整数可能特别大,超过int范围,那就用Long.parseLong;如果超过long,就得走下面的BigInteger路线。

3.4 场景四:矩阵和网格数据的读取

矩阵类题目(比如搜索、动态规划、图遍历)的输入格式一般是“第一行两个整数n和m,表示有n行m列,接下来n行,每行m个整数(或字符)”。

整数矩阵没什么特别的,逐行读逐行切就行。但字符矩阵有个坑:字符串可能中间没有空格,直接用toCharArray()就行;如果字符之间用空格隔开,那就split之后取charAt(0)。

这里我要特别提醒:字符矩阵的读入不要用nextInt()和nextLine()混搭!比如你读完n和m,用nextInt()吃了两个整数,然后立刻nextLine(),你拿到的可能是空串。要么统一用BufferedReader一行一行读,要么在Scanner方案里先补一个nextLine()消费换行。

形如:

3 4 #S.. ..E. ####

这种字符迷宫我的读法是这样:

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String[] first = reader.readLine().split(" "); int n = Integer.parseInt(first[0]); int m = Integer.parseInt(first[1]); char[][] grid = new char[n][m]; for (int i = 0; i < n; i++) { String row = reader.readLine(); // 注意有些数据中间有空格,那就去掉空格再转char数组 row = row.replace(" ", ""); grid[i] = row.toCharArray(); }

replace(" ", "")这步很多人不知道,它能一次性搞定“字符间带不带空格”两种格式,实测下来很稳。

3.5 场景五:EOF后还需要继续读多行数据

有一类题特别像场景一和场景二的结合体:“输入包含多个测试用例,每个测试用例第一行是一个整数n,如果n为0表示结束;接下来n行,每行若干数据。”

这种你就不能只读一行就判断EOF了,因为EOF是“整个文件结束”,而不是“这一组结束”。你要在循环里先读n,判断是不是0,是0就break,不是0就把这组内的多行全部读完,然后再进外层循环读下一个n。

注意:这里外层循环的终止条件不是(line = reader.readLine()) != null,而是读一个n之后判断值。如果文件末尾恰好有一行什么都没有,那你readLine()返回null,也要判一下,防止起手就是NPE。

String line; while ((line = reader.readLine()) != null) { if (line.trim().isEmpty()) continue; int n = Integer.parseInt(line.trim()); if (n == 0) break; for (int i = 0; i < n; i++) { String dataLine = reader.readLine(); // 处理 dataLine } }

这里“n为0结束”属于“特殊值终止”,本质就是多组输入加一个break判断,千万别忘了跳出外层循环。

3.6 场景六:Java输入的兜底方案(数据量大到爆炸)

如果你的程序其他部分都能跑到最优了,但读入还是卡时间,或者说遇到那种输入量几十万、上百万行的极端题目,Scanner和BufferedReader都嫌慢,那就得上StreamTokenizer了。

我自己写过一套封装,比较好用:

import java.io.*; public class FastScanner { private BufferedReader reader; private StreamTokenizer tokenizer; public FastScanner(InputStream in) { reader = new BufferedReader(new InputStreamReader(in)); tokenizer = new StreamTokenizer(reader); tokenizer.resetSyntax(); // 关掉默认的语法规则 tokenizer.wordChars(33, 126); // 把可见ASCII字符都当word tokenizer.whitespaceChars(32, 32); // 空格是分隔符 } public int nextInt() throws IOException { tokenizer.nextToken(); return (int) tokenizer.nval; } }

这个配置意味着:除了空格之外,什么换行符、Tab都是分隔符(因为它们不在33到126之间),碰到数字字符串能被解析成double,转成int就是我们要的整数。这个类读几百万数字几乎不耗时,是我打高难度竞赛时的压箱底工具。

如果你们讨论的OJ是那种远古在线评测系统,java.io包不全,那用DataInputStream配合readInt()也是一种方案,不过现在很少见了。

3.7 场景七:大整数与高精度数据

有些题要处理超过long范围的整数,或者要求小数点后好几位精确,这时候就要BigInteger和BigDecimal。

关于BigInteger在OJ里的正确打开方式,我见过太多人踩坑:

第一个坑:不要用new BigInteger("...")从字符串构造大量数据时还用Scanner,Scanner的nextToken解析字符串本身就很慢,加上BigInteger的构造开销,双重debuff。正确姿势是用BufferedReader读行,split之后传给BigInteger构造函数。

第二个坑:BigInteger的运算返回新对象,别在循环里频繁创建引用。比如计算一个很大的累加和:

BigInteger sum = BigInteger.ZERO; for (...) { BigInteger cur = new BigInteger(token); sum = sum.add(cur); }

BigInteger.ZERO是常量,可以直接用;add返回新对象没问题,但别写sum.add(cur)却不赋值,那是无效操作。

高精度除法要小心:divide是整除,不保留小数;要保留小数得用setScale或者divide(divisor, scale, RoundingMode.HALF_UP)。比如:

BigDecimal a = new BigDecimal("10"); BigDecimal b = new BigDecimal("3"); BigDecimal result = a.divide(b, 10, RoundingMode.HALF_UP);

这个保留10位小数、四舍五入的写法,在OJ高精度题里是默认标准。

输出大数时直接用System.out.println(bigValue)就行,BigInteger和BigDecimal都重写了toString(),不会出现科学计数法的问题(除非你用BigDecimal.toPlainString()处理特殊格式)。

4. 输出层的优化:printf、println、PrintWriter、StringBuilder

4.1 别小看输出,输出的坑一样能让你TLE

很多人把心思全花在读入上,输出就无脑System.out.println。数据量小确实无所谓,但输出几万行时,System.out.println的性能会让你怀疑人生。每个println都是一次系统级写操作,没有缓冲,一万行就是一万次IO,再叠加构造字符串的开销,严重时会直接TLE。

两种常用的优化姿势:

第一种,用StringBuilder把所有结果拼接起来,最后一次性打印:

StringBuilder sb = new StringBuilder(); while (...) { sb.append(result).append("\n"); } System.out.print(sb);

这种做法把多次小IO合并成一次大IO,速度快得飞起。但要注意:如果结果有几十万行,StringBuilder的默认容量可能不够,追加时会频繁扩容。可以预估行数和每行长度,初始化时给足容量:new StringBuilder(rows * 10)之类。

第二种,用PrintWriter:

PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); out.println(result); // 最后必须flush out.flush(); out.close();

PrintWriter内部自带缓冲,println只是写到缓冲区,攒到一定量才真正写出去,速度比直接System.out高很多。千万别忘了最后的flush(),很多人本地测试没flush也看得见输出是因为程序退出时缓冲自动清空,但OJ的判题程序不会等你优雅退出,漏了flush输出就全丢了,WA得不甘心。

4.2 格式化输出的正确姿势

有些题要求输出保留几位小数,或者按某种格式输出。System.out.printf是好东西,但要注意它和PrintWriter的printf是一回事。

保留两位小数:

System.out.printf("%.2f%n", value);

%n是换行,兼容不同操作系统的换行符;用\n也不是不行,但不规范。输出固定宽度时用%5d这种语法。

有个坑:printf遇到BigDecimal或者其他对象时,如果格式符不匹配会抛异常。比如你用%d去输出一个BigInteger,直接IllegalFormatConversionException。解决方法是先把大数转成字符串再用%s输出,或者直接用println。

4.3 何时用System.out,何时必须上BufferedWriter

我个人的经验是这样的:

  • 输出总行数少于1000行:System.out.println随便用,不必折腾。
  • 输出1万到10万行:StringBuilder拼接后一次性输出,或者PrintWriter。
  • 输出超过10万行,还要求每个结果单独一行:BufferedWriter直接写:
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out)); for (...) { writer.write(result + "\n"); } writer.flush();

BufferedWriter比PrintWriter更底层一些,没有自动编码转换的额外开销,极端情况下快一丁点。但绝大数题目PrintWriter够用了。

5. 输出格式的多场景演示:每组结果之间的空行,最后一组到底要不要换行

5.1 输出“每组结果之间空一行”的经典坑

有一种恶心的输出要求:“每组测试数据的输出之间用一个空行隔开。”也就是第一组结果输出完,第二组结果前面要先空一行。

很多人直接在每组输出完加两个println,结果最后一组后面也带了空行,OJ对这种格式判错是毫不留情的:Presentation Error(格式错误),或者直接WA。

正确做法是控制“非第一组”输出之前先输出空行:

boolean first = true; while (hasNextData()) { // 计算结果... if (!first) { sb.append("\n"); } sb.append(result); first = false; }

这样第一组结果前没有空行,最后一组后也没有多余空行,刚好卡住题目要求。

另一个类似的变体:“每组输出之后空一行”。有的题明确说每个case的输出后都要空一行,包括最后一个。那反而简单,每组结果后加一个空行就行,不用管最后一组。但你需要确认题目到底怎么写的。

5.2 输出结束时,到底要不要多打一个换行

我见过不少判题系统对“最后一个字符是换行符”这件事要求很宽松,多一个换行不会判错。但有些严格的系统里,多打一个换行就可能PE。最保险的做法就是:所有下一组输出前的空行都通过“前置空行”实现,而不是“后置空行”,这样无论哪一组是最后一组,末尾都干净利落。

如果你使用StringBuilder方案,最后System.out.print(sb)时天然没有多余的换行符。如果你用PrintWriter逐行println,最后一组后会有个换行,但绝大部分OJ能接受。真要对这个有洁癖,可以用一个boolean判断是不是最后一行再决定println还是print。

6. 判题系统返回值识别:PE、WA、TLE在输入输出上的含义

6.1 常见OJ返回状态与输入输出问题的对应关系

很多人WA之后一脸懵,其实判题状态已经给了线索。

  • WA (Wrong Answer):程序跑完了,输出内容和标准答案不完全一致。如果确定算法逻辑没问题,多半是输入读取边界没处理好,某些测试数据的格式和你假设的不一样。
  • PE (Presentation Error):答案数值全对,但空格、换行、空行的位置不对。这个最直接指向输出格式问题。很多OJ把PE当成WA的一种,统一返回WA,那你就要格外注意空行和尾部换行。
  • TLE (Time Limit Exceeded):超时了,输入数据量太大,读入或输出性能不够,考虑换BufferedReader和PrintWriter。
  • RE (Runtime Error):运行过程中抛异常。比如NumberFormatException——你把空字符串或非法字符串转int了;或者ArrayIndexOutOfBoundsException——数据行数比你假设的少;再或者NullPointerException——readLine()返回null之后你还在调方法。
  • MLE (Memory Limit Exceeded):内存超限,通常不是输入造成的,但如果你用StringBuilder存了过大的内容或开了超大数组,也会MLE。

关于状态码还有一个常识性问题:不同OJ的状态码命名可能不一样,有的叫Presentation Error,有的直接归入WA,所以遇到WA先怀疑格式,再怀疑算法,这个排查顺序很重要。

6.2 本地测试“看起来对”但OJ报错怎么办

很多经典输入输出坑,本地测试根本复现不出来。因为你自己手动敲数据时,格式和OJ的测试文件不一样。比如OJ的测试文件末尾可能多一个回车,而你的本地输入没有;OJ的某行可能有多个连续空格,而你自己敲的是一个空格。

所以调试输入输出问题时,我的建议是:

第一,构造“脏数据”测试。在本地故意给程序喂带空行、带多余空格、带Tab、末尾带换行的数据,看程序会不会崩。

第二,用重定向跑数据。把测试数据保存成文件,像OJ一样重定向输入:java Main < data.txt,这样可以验证EOF和文件末尾的边界效果。

第三,观察输出文件的细节。把程序输出重定向到另一个文件,用十六进制查看或od -c看末尾是不是多了换行,对比题目要求的输出格式。

这些经验很难从书上学到,都是被OJ毒打过才总结出来的。

7. 一份相对通用的“稳吃输入输出”Java刷题模板

整合前面所有内容,我整理了一份适合我自己的通用模板,每次刷题都从这份改起,省去大量重复思考。代码不复杂,但每个环节都针对上面提过的坑做了防御。

import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); StringBuilder sb = new StringBuilder(); String line; // EOF循环,通用多组输入 while ((line = reader.readLine()) != null) { line = line.trim(); if (line.isEmpty()) { continue; } // 这里按需解析 line,比如: String[] parts = line.split("\\s+"); long a = Long.parseLong(parts[0]); long b = Long.parseLong(parts[1]); sb.append(a + b).append("\n"); } System.out.print(sb); reader.close(); } }

这是最简单的形态。遇到复杂结构时,我会把while循环体换成用StringTokenizer或者其他解析逻辑。如果你不确定“这行到底有几个数”,就统一走split;如果确定“每行固定两个整数”,直接split也行,性能没区别。

7.1 模板里每个“看起来多余”的步骤都有原因

line.trim():去掉行首行尾的空白。有些测试数据的每行前面或后面可能有空格,不trim的话,split结果里可能出现空串(比如行尾的一个空格会切出一个空字符串)。isEmpty()判断:有些原始数据里可能夹着空行,直接忽略比让它报错好。StringBuilder拼接:减少输出IO次数。reader.close():虽然是OJ程序,关不关都无所谓,但好习惯不亏。

每行代码不是凭空写的,都是在各种WA、RE的尸骨上堆出来的。

7.2 从模板出发,如何快速适配不同的输入格式

如果题目先给一个T,那就在while循环之外先读一个T,然后for循环T次。如果每组数据里还有子结构,就在for循环里嵌套。如果题目要求读字符矩阵,就把split出来的第一行当n和m,再循环读n行。如果题目要求读大量数据,就换成StreamTokenizer。如果要求高精度,就把Long.parseLong换成new BigDecimal。

这个模板看起来简单,但它所有的防御性写法都考虑了OJ数据的各种边缘情况。我建议你以它为底色,自己动手往里面填你的题目的解析逻辑,多填空自然就熟练了。

8. 常见错误排查实录:从WA到AC的输入输出自救指南

8.1nextLine()返回空字符串:Scanner混读陷阱

一次我用Scanner写某个模拟赛题,第一行读入int T,第二行读入一个字符串。本地测试输入:

2 abc def

结果nextLine()读到的全是空串。后来才明白,nextInt()读完2以后,换行符还在输入流里,第一次nextLine()读走的是那个换行符。排查方法很直接:在nextInt()之后加一行scanner.nextLine()把残留换行消费掉。如果你不想用这么别扭的写法,就贯彻“要么全用nextLine要么全用nextInt,不混用”的原则。

8.2 多组数据最后一组被吃掉:EOF判断错误

有一个题要求“直到读入两个0结束”,我的原始写法是:

while (true) { int a = scanner.nextInt(); int b = scanner.nextInt(); if (a == 0 && b == 0) break; // 处理 }

本地测试好好的,一提交就WA。后来怀疑是不是有数据的某一行前面有空白,或者某一行只有0 0但前面还有个空行。改成了先读行再判断,用BufferedReader逐行处理,问题才解决。原因是Scanner有时会把0 0后面的换行吃进去,导致最后一次循环读取异常。改用按行读取后,数据解析和结束判断都更加直观可控。

8.3 输出超时:StringBuilder 存入内存导致 TLE?不是,是输出没合并

有一次写“输出n行整数”,我直接循环System.out.println,数据量20万行,直接TLE。加了一个StringBuilder把所有输出拼起来再统一打印,时间立刻降下来了。同一套算法逻辑,仅仅改输出方式就AC了,这个教训我一直记得。

8.4 最后一条输出后面多了一个换行:PE反复提示

有时候题目说每个结果占一行,你多输出一个空行并不会WA,但某些判题就比较死板。后来我按照“前置空行”的方法来处理分组输出,不仅控制了组间空行,还顺便解决了末尾多一个换行的问题。如果题目的标准输出文件本身最后没有换行,你程序里多一个换行,有的OJ对比二进制就判PE。

8.5 排查CDN:兜底调试技巧

真排查不出错的时候,可以把中间的变量打印到System.err,因为System.err不经过输出缓冲区,能直接看到。在OJ平台提交时,System.err的输出一般会被忽略,或者显示在错误日志里。用System.err打调试信息不影响System.out的正常输出,本地和OJ都不怕。不过要小心别在printf里面混入调试输出,不然输出文件直接多了脏数据。

如果连报错信息都没有,那就是输出格式不匹配。拿自己的输出重定向到文件,再手动比对样例,逐字符找差异是最笨但最有效的方法。

9. 一套我一直在用的性能实测与选择建议

9.1 不同读入方式在100万整数场景下的实测对比

我拿本机数据做过一次不那么严谨的对比测试,随机生成100万个整数,分别用Scanner、BufferedReader+split、BufferedReader+StringTokenizer、StreamTokenizer去读,运行时间大概是:

| 读入方式 | 耗时 | 稳定性 | |--- | --- | | Scanner | 很慢(可能超时) | 稳但慢 | | BufferedReader + split | 中等 | 稳定 | | BufferedReader + StringTokenizer | 较快 | 稳定 | | StreamTokenizer | 最快 | 需要小心配置 |

稳定性的意思是,在不同机器和不同数据分布下性能波动不大。Scanner慢是因为它的词法解析内部用了正则,每读一个数字都走一遍流程,这种开销在百万级数据下非常可观。

只要不是极端高数据量,BufferedReader + StringTokenizer基本上就是最优解。如果题目保证一行内数据个数是固定的,直接用split其实也不差太多,而且代码看起来更简洁。

9.2 什么时候才需要掏出 StreamTokenizer

我自己的使用经验是:单次测试数据量在50万行以上,或者单行元素很多的时候,StreamTokenizer的优势能体现出来。但它的API反直觉,很多人用不惯。如果你不是天天打需要极限性能的竞赛,平时刷题、写周赛,BufferedReader足够。

很多周赛的输入规模大概在2万到10万行,BufferedReader+StringTokenizer能在0.5秒内读完,根本不会成为瓶颈。与其花时间背StreamTokenizer,不如把精力放在算法优化上。

9.3 关于读入性能的一个核心认知

很多人误以为OJ卡的只是算法复杂度,其实IO也是会算进总耗时里的。同样是O(n)的代码,别人用BufferedReader你Scanner慢了几倍,那timelimit可能就卡在你身上。这个“输入输出性能也是战斗力”的认知,我想了很久才彻底明白,之前总觉得Scanner用着顺手就没换,结果被好几道题教做人了。

数据量小时Scanner的超时风险低,但既然写模板可以统一成BufferedReader,为什么不一开始就写在舒适区里呢?适应一下,用几天就不觉得别扭了。

10. 最终梳理:一个总结框架之外的进阶使用心得

说这么多,核心观点其实一句话:ACM模式下Java的输入输出,核心不是“能不能读进来”,而是“能不能高效地、健壮地读进来,并且在各种数据格式下都不炸”。

我个人建议的学习路线是这样的:

第一步,先搞清楚题目输入到底有几种形态,不动手实践的情况下能在脑内模拟出完整的数据流。第二步,熟悉BufferedReader+StringTokenizer这套标准组合,做到条件反射。第三步,针对特殊场景(大数、矩阵、多组输出空行)建立自己的模板库。第四步,真遇到TLE再考虑StreamTokenizer,不要一开始就上。第五步,多刷题形成肌肉记忆,看到题目描述就知道该用哪种模板。

不少同学会纠结“Java刷题是不是吃亏”,其实在输入输出上下足功夫后,Java和C++的差距远没有想象中大。至少对大多数题目来说,瓶颈是算法思维,而不是语言够不够快。

我刷了各种OJ这么多年,非常深的体会是:输入输出是OJ系统里最不性感但最容易翻车的一环。算法再漂亮,读入读崩了、输出末尾多一个空行,一样是WA。把这块打磨成肌肉记忆之后,刷题过程中的烦躁感能少一大半。

下次上OJ前,建议把本文提到的几个关注点再过一遍:空行、换行符、EOF、Token数量和性能缓冲。折腾一两次之后,你也能像我一样,把输入输出稳稳拿捏住,把更多精力放在真正的算法设计上。

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

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

立即咨询