首页 / 资讯中心 / 文章详情

Java输入流读完存哪里?字节数组、字符串、文件与对象封装的选型指南

Java输入流读完存哪里?字节数组、字符串、文件与对象封装的选型指南 ★ FEATURED ARTICLE
1. 为什么每次读完流都栽在存哪儿这个问题上做Java开发的人迟早都会跟输入流打交道不管是读文件、接HTTP请求体还是从Socket里拿数据。我记得刚工作那两年写接口上传文件时最头疼的还不是流怎么读而是读完往哪儿放。你想啊InputStream本身是个一次性消耗品它不像List或Map那样能直接遍历好几遍流里的数据是顺着管道往前冲的你不在它到达的时候把它接住它就没了。更麻烦的是流读取还牵扯到字节、字符、编码、内存、临时文件这一堆破事怎么存放、用什么容器接直接决定了你后续的代码好不好写、性能稳不稳、甚至会不会OOM。市面上搜Java输入流十个有八个在讲read()、readLine()怎么调用但很少有人把一个最关键的问题讲透你读出来的这些内容到底用什么“容器”兜住它是丢进byte[]是直接拼成String还是落盘成File这几种做法看起来都能放实际用起来的差别大了去了。这篇博文就把这层窗户纸捅破。我从最常用的字节数组存放开始逐一到字符串存放、文件存放、对象封装存放把每种方案的原理、代码、适用场景和坑全都过一遍。最后再附上我自己踩过的几个典型问题以及排查的思路。如果你正在处理文件上传、HTTP请求体读取、数据流转换这类需求这篇文章应该能帮你在选型时少走弯路直接踩在正确的答案上。2. 输入流读取的前置认知先搞懂流的本质再谈存放2.1 InputStream到底是个什么管道InputStream是所有字节输入流的抽象父类。它描述的核心能力就一个从某个数据源一个字节一个字节地往外拿数据。这个数据源可以是文件、网络连接、内存字节数组甚至可以是另一个输出流。关键点在于InputStream不负责保存数据它只负责搬运。字节从源头出来经过流管道到达你代码里的某个变量。你要是没接住这些字节就永远消失了。我习惯把一个字节比作“一滴水”。流就是一根从水源地通向你家水桶的管子你开着水龙头水就一直流。你的代码就像是在水桶边拿杯子接水的人杯子就是存放容器。你不接水就洒在地上。接慢了水溢出来。接多了桶装不下。这样想就通了存放方式的核心问题就是怎么高效地把流里的字节“接住”放进一个后续能反复使用的容器里。2.2 流的一次性特性决定了存放必须趁早因为InputStream是单向、一次性的消费完就没了所以存放动作往往发生在读取的同时。请注意如果你把一个InputStream对象传给了两个方法第一个方法把数据读完了第二个方法再读就是-1空手而归。这不是Bug这是流的天然属性。因此在实际开发中流的消费场景必须提前想清楚。比如你要写一个接口接收上传的图片既要保存原图又要生成缩略图。此时如果你只拿到了一个InputStream就得先把它完整读出来存好再基于存好的数据做两次处理。你要是天真地以为可以读两次流那第二次读到的就是空气。理解了上面这个核心前提再往下看各种存放方案你就知道每一种选择背后的动机了。3. 字节数组存放最通用的水桶但细节里全是坑3.1 基础写法与原理字节数组byte[]是Java里存放二进制数据最原始的容器。把一个输入流读完并放进字节数组最标准的“教科书写法”是这样public static byte[] toByteArray(InputStream input) throws IOException { ByteArrayOutputStream output new ByteArrayOutputStream(); byte[] buffer new byte[8192]; int length; while ((length input.read(buffer)) ! -1) { output.write(buffer, 0, length); } return output.toByteArray(); }这段代码的工作逻辑相当直观起手准备一个ByteArrayOutputStream相当于一个可动态扩容的“蓄水池”然后准备一个8KB大小的byte[]作为“水瓢”不断从输入流里舀水读取数据一旦舀到水就往蓄水池里倒写入ByteArrayOutputStream直到水流干返回-1。最后调用toByteArray()一次性把蓄水池里的水全部倒进一个新的字节数组容器里。3.2 为什么不用input.readAllBytes()Java 9开始提供了InputStream.readAllBytes()方法可以直接把整个流读到字节数组一行搞定听着很爽。但实际用的时候我一般比较谨慎。readAllBytes()的底层实现是先把数据读到一个自动扩容的ByteArrayOutputStream里再转为字节数组。对于小流来说这个API的体验确实简洁优雅。但问题出在无法控制这个内部缓冲区的增长行为如果输入流是网络数据长度未知且体量较大readAllBytes()可能会在不知不觉中把内存吃得很厉害。相比之下手动用固定大小的byte[]循环读取虽然代码啰嗦但在面对大流量时你的每一步都在自己的掌控范围内。所以我的个人建议是小数据量比如几KB的请求体解析可以放心用readAllBytes()一旦流的体量可能超过MB级还是老老实实写循环或者直接用下面要讲的文件方案。3.3 字节数组存放的适用边界字节数组适合存放什么一切二进制数据比如图片、PDF、ZIP包、序列化对象。它不适合直接用于文本内容因为字节数组本身不关心字符集你拿着它去做字符串操作还得自己转麻烦不说还容易出错。另外字节数组在内存里的形态是连续的复制成本高。ByteArrayOutputStream.toByteArray()内部会做一次拷贝如果你要多次操作这份字节数据建议只转一次后续始终持有同一个byte[]引用避免反复转换。4. 字符串存放文本场景的捷径编码陷阱必须绕开4.1 为什么你会想把流读成String我做接口开发时最常见的一个场景就是接收JSON请求体。请求体本质上是字节流但业务代码里我要的是String然后丢给Jackson或者Gson去反序列化。这个时候把输入流转换成String就成了最自然的选择。代码也很简单public static String toString(InputStream input, Charset charset) throws IOException { ByteArrayOutputStream result new ByteArrayOutputStream(); byte[] buffer new byte[1024]; int length; while ((length input.read(buffer)) ! -1) { result.write(buffer, 0, length); } return result.toString(charset.name()); }4.2 编码问题乱码从哪来这段代码的核心就是Charset。很多人在这里栽跟头觉得用平台的默认编码就行了。反正自己的开发机和服务器都是Linux默认UTF-8没啥毛病。但一旦你把代码部署到Windows机器上或者对接方的数据源是GBK编码乱码就出现了。我处理过一个真实案例老系统导出的CSV文件是GBK编码新系统读取后直接用UTF-8解码结果所有中文全部变成锟斤拷一类的符号。排查过程不难但修复成本高因为上游已经写到了数据库里脏数据还得清洗。所以从流到String一定要显式指定编码永远不要依赖平台默认值。推荐做法String content new String(bytes, StandardCharsets.UTF_8);如果你的JDK版本还不支持StandardCharsets虽然不太可能就用Charset.forName(UTF-8)。4.3 那些容易忽略的低级误区很多人读文本流时喜欢用BufferedReader的readLine()来逐行读取然后拼接字符串。这个方案在行数少、内容短的场景下还凑合但有两个隐患使用String 拼接字符串在循环里会产生大量中间字符串对象触发频繁GC性能极差。必须用StringBuilder或StringBuffer。readLine()会丢弃换行符。如果你需要保留原始换行比如对文本做哈希摘要那就不能简单用它拼接需要自己做换行处理。我自己一般用Apache Commons IO里的IOUtils.toString(input, charset)但说实话它底层做的事和我前面写的那段循环一模一样。了解了原理你就不会被工具库的黑盒行为坑到。4.4 字符串存放的边界思考字符串存放只适合纯文本内容。如果你把图片、压缩包这类二进制数据直接转成String虽然Java的String底层是byte[]理论上有损的概率不高但是一旦遇到某些字节序列正好无法用指定字符集解码就可能产生非预期的字符替换。再从String转回byte[]时原始数据就已经变了。对于二进制内容请坚持使用字节数组或直接落盘别走字符串这条捷径。5. 文件存放大文件与临时处理的务实之选5.1 什么时候该把流读到文件里字节数组适合几MB以内的数据字符串适合文本数据。那如果输入流动辄几百MB比如视频上传、日志数据导入、数据库备份恢复你还要把它硬塞进内存吗不行内存会直接被拉爆。这时就该让文件登场了。把输入流写入磁盘相当于给无穷无尽的水流接了一根引水渠让水流进一个大水库而不是用你手里的杯子去接。文件存放是一个几乎不受内存限制的方案代价是磁盘I/O和后续清理工作。5.2 最简单的落盘方法Java 7开始引入的Files.copy提供了异常简洁的落盘APIFiles.copy(inputStream, Paths.get(/tmp/upload/abc.zip), StandardCopyOption.REPLACE_EXISTING);实测下来这个API很方便。但请注意REPLACE_EXISTING这个参数要慎重。如果目标路径已有文件不写这个参数会抛出FileAlreadyExistsException写了又会静默覆盖旧文件。我建议在业务代码里先判断文件是否存在确认逻辑后再决定是否覆盖避免误删历史数据。另一个需要注意的点是Files.copy(InputStream, Path, CopyOption...)只会拷贝输入流当前可读的数据如果你在同一个流上设置了skip()跳过了一部分内容落盘的就是跳过的部分。这和用FileOutputStream循环写入是一样的道理。5.3 手动循环写入的IO细节如果你对落盘过程有特殊控制需求比方说要分批写入、边读边算哈希、或需要记录进度就要手动操作了public static long saveToFile(InputStream input, File dest) throws IOException { Objects.requireNonNull(input, input must not be null); try (FileOutputStream fos new FileOutputStream(dest); BufferedOutputStream bos new BufferedOutputStream(fos)) { byte[] buffer new byte[8192]; int len; long total 0; while ((len input.read(buffer)) ! -1) { bos.write(buffer, 0, len); total len; } return total; } }这里做了一个额外的BufferedOutputStream包装目的很明确减少底层系统调用次数。FileOutputStream每次write都可能触发一次系统调用而加一层8KB的缓冲相当于攒够一批数据再统一写盘性能提升非常明显。实测下来同样是写100MB数据带缓冲的时间可能只有不带缓冲的三分之一甚至更少。5.4 临时文件的清理策略把流写进文件以后你得负责善后。尤其当文件是存到系统临时目录的进程结束后不会被自动清理迟早把磁盘堆满。我的常规做法是写完之后立刻记录文件路径在finally块或使用完的代码路径里显式删除。如果用的是Java NIO的Files.createTempFile还可以挂一个File.deleteOnExit()钩子JVM正常退出时会尝试删除。但注意这只在JVM正常退出时生效如果你的程序是被kill -9强杀的这个钩子也不会执行。6. 对象封装存放从管数据升级到管语义6.1 只存数据不够还得存状态和来源字节数组、字符串、文件都是“裸数据”容器。但在真实的业务开发中你常常还需要连同流一起记录一下来源信息、处理状态、先后顺序等元数据。这时候就该上对象封装了。我举个例子。你在写一个批处理程序从消息队列里拉取一批文件流每个文件还带着对应的业务单号、上传时间、所属用户。流里的内容只是数据这些业务信息不在流里如果你只拿个MapString, Object到处拼凑代码会很快乱成麻。更合理的做法是定义一个封装对象public class UploadedFile { private String bizId; private String fileName; private long size; private byte[] content; // 构造方法、getter/setter 略 }6.2 如何把流数据合理封装到对象里封装对象里的content字段类型要视数据体量而定。小文件用byte[]很方便大文件则在对象里存File path而不是把整个文件载入内存。public class UploadedFile { private String bizId; private String fileName; private long size; private Path filePath; // 存储落盘位置 }这样设计的好处是数据所在的物理位置和业务元数据在同一个对象里传参、序列化、日志打印都方便。对象里还可以带上处理状态比如status字段表示已读取处理中已完成这样后续流水线处理时就可以通过字段判断当前进度。6.3 不要什么都往对象里装对象封装也不是越全越好。我有一次接过一个同事的代码他给文件对象加了二十多个字段除了基本的文件名、大小之外还有什么编码格式、压缩算法、长宽像素、颜色模式、CRC校验值。理想上这样记录很完备但实际运行时很多字段根本没被用到每读一个文件就要做一堆额外的计算来填充这些字段白白拖慢了处理速度。经验是先只封装你确定要用的字段等数据处理链条上确实出现缺失信息的需求时再考虑在解析过程中顺手补上。过度设计在流处理这种性能敏感的场景里是很伤的。7. 存放方式横向对比与选型决策7.1 四个维度的对比表我把上面讲的几种存放方式放在一张表里方便你按场景直接查存放方式内存占用适用数据规模是否适合文本是否适合二进制典型场景字节数组与数据量成正比小中型建议几MB内需自行转码适合图片验证码、小文件上传字符串与数据量成正比且翻倍字符底层字节小中型适合需指定编码不适合JSON请求体、CSV/文本解析文件落盘几乎不占内存任意规模适合适合大文件上传、临时文件处理对象封装取决于内部容器视内部容器而定取决于容器取决于容器业务处理流水线、多字段场景7.2 场景驱动的选型建议选型不用背表问自己三个问题就够了。第一个问题数据会不会超过内存可承受范围如果答案是会直接选文件落盘如果不会进入第二个问题。第二个问题后续要处理的是文本还是二进制文本优先转String二进制用byte[]。第三个问题除了内容本身之外还有没有别的业务属性要一起传递有就封装对象把前面的内容容器作为对象的字段。拿我最近做的一个Excel导入功能举例。用户上传Excel文件文件能有多大一般几MB内存完全扛得住。所以方案定为先把上传的InputStream全部读成byte[]再做一次合法性校验校验通过后转交Apache POI解析。为什么不直接让POI去消费原始流因为原始流只能消费一次一旦POI解析失败我想再从源头读就没机会了。而字节数组可以反复使用解析失败还能从数组里再读出来排查问题。7.3 别忽视JVM堆外的世界这里额外提一句容易忽略的点当处理超大流时与其把数据放到JVM堆内的数组或字符串里不如考虑堆外方案例如使用ByteBuffer.allocateDirect()把数据放到直接内存中。直接内存不受JVM堆大小限制也不参与普通的GC回收可以容纳更大的数据。缺点是读写管理和内存释放都更复杂需要小心手动释放否则容易出现Native内存泄漏。对于绝大多数业务系统我不建议直接上手直接内存完全可以用文件落盘平替。只有当你确实在做一个高性能、低延迟的网络网关项目而对文件的磁盘I/O敏感时才值得考虑堆外方案。8. 读取与存放实操手把手完成一个上传-存储-再处理流程8.1 典型场景接口接收文件流先落盘再做后续处理假设你现在要写一个文件上传接口前端把文件作为multipart请求体发过来后端拿到CommonsMultipartFile或MultipartFile后需要做三件事把原图保存到本地磁盘、从原图生成一张缩略图、把文件的元数据写入数据库。第一步读取原始流并落盘public Path storeOriginalFile(MultipartFile file) throws IOException { Path dir Paths.get(/data/uploads, LocalDate.now().toString()); if (!Files.exists(dir)) { Files.createDirectories(dir); } Path dest dir.resolve(UUID.randomUUID().toString() _ file.getOriginalFilename()); try (InputStream in file.getInputStream()) { Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); } return dest; }这里特别解释一下为什么用UUID重命名真实业务场景中用户上传的文件名极有可能重名而且文件名里可能带上路径符号、特殊字符甚至脚本注入的内容。直接用原始文件名落盘既可能互相覆盖也有安全隐患。让系统生成一个新名字把原始文件名存入数据库元数据再在前端下载时恢复展示名称这个套路是行业里最稳的。第二步基于落盘后的路径生成缩略图public void createThumbnail(Path sourceFile, Path thumbFile) throws IOException { try (InputStream in Files.newInputStream(sourceFile)) { // 交给图片处理库或第三方工具生成缩略图 // 这里用ImageIO做简化示例 BufferedImage img ImageIO.read(in); // 缩放并写出... } }因为流只能读一次没关系我们在第一步已经落盘了现在可以从文件路径再次构建新的输入流想读几次读几次。8.2 内存受限场景实时计算SHA-256而不占大内存有时你既不想把数据全放内存又不想在磁盘上留一个临时文件那就需要边读边算、流式处理。比如校验下载文件完整性思路是边读边更新摘要对象。public static String sha256OfStream(InputStream in) throws Exception { MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { digest.update(buffer, 0, len); } byte[] hash digest.digest(); StringBuilder sb new StringBuilder(hash.length * 2); for (byte b : hash) { sb.append(Character.forDigit((b 4) 0xf, 16)); sb.append(Character.forDigit(b 0xf, 16)); } return sb.toString(); }这个写法的精妙之处在于byte[] buffer只分配了一次后续循环不断复用整个读取过程的内存占用控制在固定的8KB上下。不管输入流是10MB还是10GB占用的临时内存都是这么多。这就是流式处理的好处——“用时间换空间”。8.3 多次读取同一个流的替代方案如果你真的在代码里遇到一个“需要读两次流”的场景最省事的办法不是在读取方式上纠结而是把流先存到一个临时存储中。这个临时存储可以是字节数组、临时文件甚至是一个byte[]包装的ByteArrayInputStream。byte[] bytes toByteArray(originalStream); InputStream copy1 new ByteArrayInputStream(bytes); InputStream copy2 new ByteArrayInputStream(bytes);ByteArrayInputStream实质上是把字节数组包装成流的外观每次new都是独立的流对象所以想读多少次都行。这是一个成本极低又有效的方案。需要注意的是ByteArrayInputStream内部持有的是同一个byte[]的引用并不会复制数组。假如你之后修改了这个byte[]的内容所有基于它的ByteArrayInputStream都会受到影响。所以如果后续会有写操作建议先clone()一份。9. 常见问题速查读取后存放的典型故障与排查实录9.1 问题现象一读取出来的字符串是乱码/锟斤拷原因通常是字符集不一致。上游写入时用A编码你解码时用了B编码。排查思路先确认数据源声明的编码格式。HTTP请求看Content-Type里的charset文件看文件头或环境约定数据库看字段字符集。在代码里强制指定解码字符集不要依赖默认编码。如果数据已经变成乱码尝试用正确的编码重新解码原始字节数组。当你手里只有String而非byte[]时乱码数据往往已经不可逆所以一开始就要用字节数组兜底。我处理线上问题时往往先打印出原始字节的十六进制看看是不是特定编码下的固定字节序列这样可以快速判断到底是GBK还是UTF-8的问题不至于瞎猜。9.2 问题现象二读取到的数据少了一截如果是HTTP接口接收请求体用readLine()读时出现少数据常见原因是BufferedReader的缓冲机制与Content-Length计算方式不匹配。更常见的情况是你自己在流前面做了一些pushBack或skip操作导致后来读到的内容起始位置不对。解决办法不要在上层业务里对同一个InputStream做多次条件性skip。需要精确控制位置就用ByteArrayInputStream或RandomAccessFile它们支持随机访问。9.3 问题现象三内存溢出OOM读取大文件时用readAllBytes()或者用StringBuilder循环拼接超大文本都会触发内存问题。排查思路查看-Xmx堆内存配置和GC日志确认是堆内溢出还是直接内存溢出。考虑改用文件落盘方案或流式处理。检查是否在循环里创建了大量临时对象导致GC来不及回收。我曾经接手过一个报表导出功能导出几万行数据时接口直接卡死。一查代码发现团队为了便于拼接将每一行数据先转成JSONObject存到List里再统一序列化。几万行对象全部挤在堆里GC压力剧增。后来改成一行一行写出到OutputStream内存占用从数百MB直接降到几十MB。这个改动给了我一个很深的影响流式处理的灵魂在于“不需要同时持有全部数据”。9.4 问题现象四流未关闭导致文件句柄泄漏文件句柄泄漏属于慢刀子割肉的问题平时看不出异常运行几天后突然报“Too many open files”。排查时用lsof查看进程打开的文件句柄数发现大量指向临时文件的句柄没有被释放。解决这类问题的核心法则就一条所有创建了InputStream/OutputStream的地方都要用try-with-resources来确保关闭。Java 7之后这已经是标准操作自定义封装对象时只要实现Closeable接口同样可以用try-with-resources管理。9.5 问题现象五ByteArrayOutputStream数据量超过预期使用ByteArrayOutputStream时如果写入的数据量超出了JVM堆限制会抛出OutOfMemoryError。它内部是动态扩容的byte[]每扩容一次就开辟新数组、复制旧数据这既浪费CPU又浪费内存。如果是大批量数据写入优先改用FileOutputStream或分批写盘不要迷信ByteArrayOutputStream的“无限增大”。10. 工具库与框架的正确蹭法10.1 Apache Commons IO省心但别滥用Apache Commons IO里的IOUtils几乎成了读转工具类的标准答案。byte[] bytes IOUtils.toByteArray(inputStream); String text IOUtils.toString(inputStream, StandardCharsets.UTF_8); ListString lines IOUtils.readLines(inputStream, StandardCharsets.UTF_8);这些API确实好用我自己的项目也一直在用。但有一条红线别让它们出现在大文件场景里。IOUtils.toString内部也会把所有数据读进内存大文件该炸照样炸。用工具库之前想清楚它对内存的立场不是“用了框架就自动高效”。10.2 Guava的ByteStreams与CharStreamsGuava提供了ByteStreams.toByteArray(InputStream)行为与Commons IO的IOUtils.toByteArray类似。它的另一个亮点是ByteStreams.copy方法可以高效地将输入流复制到输出流底层针对不同JDK版本做了优化。ByteArrayOutputStream out new ByteArrayOutputStream(); ByteStreams.copy(inputStream, out); byte[] result out.toByteArray();10.3 Spring框架里的StreamUtils如果你的项目已引入Springorg.springframework.util.StreamUtils同样提供了copyToString和copyToByteArray方法。我通常在Controller层直接用StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8)读取请求体省得自己手写循环。但对于大文件下载Spring的Resource抽象往往比直接操作流更方便不需要自己手动管存放。10.4 自定义封装当工具类都不能表达你的意图时工具库解决的是“读转“的通用问题解决不了“业务语义”的问题。当你的存放目标是为了给下游提供明确的业务数据而不是单纯复制字节时就该写自己的转换方法了。记得有一次做对接上游返回的是一种变长格式的二进制流前4个字节是数据长度后续字节是数据本体最后4个字节是校验和。这种场景下通用工具类完全无能为力必须自己写解析逻辑并且把解析结果封装到自定义对象里。这类“格式感知”的存放方式在实战中才是真正创造价值的代码。11. 聊聊我自己踩过的那些坑做流处理这几年我犯过几次特别低级的错写出来供大家引以为戒。第一次是用read()方法而不是read(byte[], int, int)方法循环读取。read()每次只读一个字节而且返回的是int一个文件读下来方法调用次数和数据量成正比性能能差出一两个数量级。后来凡是写循环读取我永远先备一个8192的byte[]缓冲。第二次是忘了在解析完文件后关闭流。当时写的是某个批处理任务每天跑一次每次处理大概上千个文件运行两周后Linux系统报“打开文件过多”。排查了半天最终定位到是某个分支里漏写了close()。从那以后我给自己定了一条死规矩凡是创建流对象的代码写完立刻写try-with-resources不要等回头再补。拖延的结果就是漏。第三次是盲目用available()判断流长度。available()只是表示当前缓冲区中可不阻塞读取的字节数而不是整个流的长度。对于一个网络输入流它返回的值往往很小甚至可能是0根本不能用于决定分配多大数组。正确做法是通过循环读取动态扩容而不是依赖这个方法来预估。第四次没处理好的是文件落盘后临时目录被写满。当时做的是批量导入功能入口接收一批上传的Excel文件解析后生成CSV临时文件处理完删除临时文件。但有一次上游系统异常连续重试而每次重试生成的新临时文件都还没执行到删除步骤服务就崩了。等磁盘满了以后系统所有写操作都开始报错连带影响了其他正常业务。后来我在任务入口处就加了定时清理临时目录的逻辑并对单个任务的临时文件数量做了上限控制。12. 一个值得养成的惯用法先画数据流向图再写代码现在每当我接到一个涉及输入流读取的功能不管是接口上传、文件导入还是消息消费我都会先用几分钟在脑子里把整条数据链路画一遍数据从哪里来经过哪些转换最终落在哪个容器会不会被二次消费这几个节点上有没有内存、编码、清理方面的风险。想清楚了再动手写代码基本就不会在“存放方式”上翻车。落地到实操我会先写一个核心的读取工具类把字节数组、字符串、文件这三个基础操作封装好然后各业务的处理逻辑都建立在统一的工具层上。这样不需要每个业务类里都重复写一遍while ((len input.read(buffer)) ! -1)代码整体也干净得多。最后再分享一个实战习惯在写读取逻辑时把读取容器选择和流关闭这两件事当成整个代码块的两条主线主线想通了边缘问题就不会冒出来。读进来的数据到底放字节数组、字符串、文件还是对象本质上取决于你的下游要什么以及你的内存、磁盘承受力。把这些约束条件想清楚你自然会得出正确的答案。
阅读完成 · 觉得有帮助?
咨询建站