尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java输入全解析:从System.in到Scanner与BufferedReader的底层原理和实战

Java输入全解析:从System.in到Scanner与BufferedReader的底层原理和实战 如果你接触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.encodingGBK同一段代码解析出来的字符串就可能是乱码。我实测过不少情况中文乱码大概率不是数据坏了而是输入解码用的字符集和你预期的不一致。再往下走字符流上面往往会套一个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是否能被解析成整数”。如果用户迟迟不输入它会一直阻塞等待。只有遇到明确的结束标记如命令行的CtrlD / CtrlZ或者读到了非数字内容循环才会退出。这在交互式命令行程序里是个问题程序会“卡”在读取输入的地方用户不敲东西它就永远不动。解决方案是明确设计终止条件比如让用户输入特殊字符结束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 不要随手关闭Scannertry (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封装其实非常舒服ListString 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就必须非常谨慎。我建议的实践是不要在业务代码里关闭System.in除非是程序退出前的最后一刻如果一定要封装读入工具类不要让它实现Closeable或者让close()方法是空操作避免被误调用用测试框架时特别注意有些单元测试会继承进程级的标准输入一旦某个测试把System.in关了后续所有依赖标准输入的测试都会连锁崩溃。很多年我都在这个坑上栽过一个后台任务进程初始化时读了用户输入后来进程改成长驻服务某次重构时有人在finally里sc.close()了结果整个进程再也不能热加载配置查了很久才定位到是标准输入被关闭。所以这块真的值得多写两行防御代码。4.4 大输入的流式处理思维面对超大文本一次性读进内存是最容易爆内存的做法。正确的思维是“流式”无论数据多大内存里始终只保留当前处理的那一行或那一个块。try (StreamString 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); ListInterval 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 ListInteger readAllIntsFromLine(String line) { ListInteger 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; } } ListInteger 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设为truetry (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时先从“数据从哪来、字节怎么变成字符、字符怎么变成对象、资源由谁关闭、性能是否过关”这五个问题入手逐一排查。按这个顺序来大部分输入问题都会清晰很多。这个清单我用了很久每次都很管用希望你也能用得上。
返回列表