尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Java IO流知识汇总:字节流、字符流、缓冲流与序列化实战
在 Java 开发里待久了你会发现无论你写的是 CRUD 还是高并发中间件IO 流都是绕不开的那道坎。面试八股文里有它日常排查线上问题有它就连读个配置文件、导个 Excel 都离不开它。“Java IO 流知识汇总”这六个字看着朴素但它背后承载的其实是 Java 程序员从入门到进阶的核心分水岭。这篇我把字节流、字符流、缓冲流、转换流、序列化还有那些你踩过或即将要踩的坑全部掰开揉碎讲清楚顺便附上我这些年实践下来最实用的经验和看法希望能给你省点弯路。花了几天把 Java IO 体系重新梳理了一遍顺手整理成这篇笔记。目标读者不只是刚毕业的面试选手也包括写了两三年代码但碰到 IO 仍心里没底的朋友。我会尽量少讲那些百度一搜一堆的概念多讲讲我是怎么理解它、怎么用它、又在哪些地方栽过跟头。1. 内容整体设计与思路拆解IO 流这玩意儿我第一次学的时候觉得很简单不就是读写文件嘛。后来发现远没那么简单等接触了网络通信、大文件处理、编码转换、序列化协议之后才意识到当初如果能把 IO 体系吃透后面能省太多事。所以这篇文章我想按照“是什么、怎么分、为什么这么设计、实际怎么用”的逻辑来展开而不是像很多教材那样直接列一堆类名让你背。1.1 先理解 IO 到底在解决什么问题任何程序跑起来数据都存在内存里。内存的特点是快但一断电就没了。IOInput/Output输入输出要解决的就是把数据从外部源源不断地搬到内存或者把处理完的结果从内存搬到外部去。外部可以是磁盘文件、网络端口、键盘、显示器甚至另一个程序。理解“流”这个词很关键。你就把数据想象成水管里的水程序是用户水龙头拧开水就顺着管子流进来你拧上开关水就停了。管子里始终流动的是数据方向决定了是读还是写。这个“流”是方向性的、连续性的、有序的。所以InputStream管输入OutputStream管输出名字起得直白到不需要思考。但是光有这两个概念还不够因为数据在水管里是以什么形态流动的决定了你怎么接住它。于是 Java 又从两个维度对 IO 做了划分一个维度是按数据单位分为字节流和字符流另一个维度是按职责分为节点流和处理流。这两个维度交叉组合就构成了 Java IO 庞大的类家族。1.2 两个维度的分类字节流与字符流节点流与处理流先说字节流和字符流。字节流操作的最小单位是 8 位的字节byte不管你读的是图片、视频、压缩包还是文本底层存的就是字节。字符流操作的最小单位是 16 位的字符char专门为文本而生。这里有个最基本的问题为什么要为文本单独搞一套字符流因为文本有编码。同一串字节用 UTF-8 解码和用 GBK 解码得到的是完全不同的字符串。字符流内部封装了字节到字符的转换逻辑让程序员可以专注于“读字符”而不用每次自己手动做编码转换。再说节点流和处理流。节点流是直接对接数据源的流比如FileInputStream直接接文件FileReader也直接接文件。处理流则是包在别的流上面为别的流增加功能。最典型的例子就是BufferedInputStream它本身不直接对接文件但它包在 FileInputStream 外面给它套上一个大缓冲池让每次底层读取不再一个字节一个字节地“挤牙膏”而是攒够一桶水再倒进来性能提升非常明显。1.3 Java IO 生态的类是怎么组织起来的从继承体系看一切字节流的祖先只有两个抽象类InputStream和OutputStream字符流祖先则是Reader和Writer。所有的字节流类名字里大多含 Stream所有字符流类名字里大多含 Reader 或 Writer。这个命名规律虽然简单但非常实用看一眼类名你就能大概猜出它是干什么的。Java 设计 IO 库时用了装饰器模式这是理解整个 IO 体系的核心钥匙。所谓装饰器模式就是像给沙发套套子一样你可以给一个流一层一层地包上增强功能的流。比如我想高效地按行读一个文本文件我可以先做一个 FileInputStream 对接文件再套一个 InputStreamReader 做字节到字符的转换还可以指定字符集最后再套一个 BufferedReader提供缓冲和按行读取。三层各有专职互不干扰。这个设计的代价是类数量爆炸感觉看不过来好处是组合灵活你想要什么样的功能自己动手拼装即可。2. 核心细节解析与实操要点很多同学一开始学 IO光靠死记硬背类名效果很差。我建议大家换个思路先抓住四个核心抽象类再把它们的状态、方法、构造函数的套路搞清楚剩下的就是排列组合。2.1 一切字节流的基石InputStream 与 OutputStreamInputStream 最核心的方法是read()分成几个重载版本。不带参数的read()返回一个 0~255 的 int意思是读一个字节如果读到了流的尽头就返回 -1。返回 int 而不是 byte是因为 byte 的取值范围是 -128~127无法表达“没数据了”这个状态所以用 int 并规定 -1 为结束标记。带字节数组的read(byte[] b)则是一次读入整个数组长度的字节返回实际读到的字节数这是提高效率最直接的手段。OutputStream 对应的是write()同样有写单字节和写字节数组的重载。值得留意的是write 方法把数据写到了内存里的内部缓冲并不代表数据已经落盘。你调用flush()是告诉流把缓冲区的数据强制冲到目的地调用close()则是关闭流并且释放底层资源。提示close()不止是关连接它在关闭之前会隐式 flush 一次。但如果你长时间不关流却想及时把数据写出去手动调flush()是有必要的比如写日志、写报表时。2.2 字符流两兄弟Reader 与 Writer 怎么避免乱码Reader 和 Writer 的结构跟字节流很像只是操作单位从 byte 换成了 char。如果你直接实例化 FileReader 读文件它会用平台默认字符集来解析。这句话在 Windows 上可能是 GBK在 Linux 上通常是 UTF-8一旦文件编码和默认字符集不一致就会出现经典的乱码问题。所以我的习惯是除非你 100% 确定文件编码和平台默认编码一致否则尽量不要直接用 FileReader/FileWriter而是通过InputStreamReader显式指定字符集。InputStreamReader 是个桥接类它接收一个 InputStream再指定一个 Charset底层按你指定的编码把字节解码成字符。Writer 那边对应的是 OutputStreamWriter。这里分享一个真实案例。有次我用程序读一个从 Windows 服务器上下来的 CSV 文件文件是 GBK 编码而服务器环境是 UTF-8。直接 FileReader 读出来全是乱码排查了很久最后用new InputStreamReader(new FileInputStream(file), GBK)瞬间就解决了。所以记住字符流的底层一定是字节流而编码转换永远要在字节流和字符流之间的桥梁上做文章。2.3 缓冲流的价值为什么一个个读那么慢如果每次都只调用一次底层的 read() 读一个字节每次都要从操作系统发起一次系统调用性能极低。缓冲流 BufferedInputStream 的做法是内部维护一个默认 8192 字节8KB的缓冲区底层一次性读入 8KB 到缓冲区然后外部程序从缓冲区里慢慢取。这样系统调用次数大幅减少性能差距可能在几十倍甚至上百倍。有人可能会问既然带数组的 read(byte[]) 也有缓冲的效果那为什么还需要 BufferedInputStream区别在于数组的读写要求你在代码层面自己管理“读到哪了、剩余多少、下一次读多少”这类状态而 BufferedInputStream 把这些状态封装在内部对外仍然是一个优雅的流接口。你不需要关心缓冲区的边界只需要配合 read() 的返回值判断是否结束即可。注意缓冲流也不是万能的。如果你处理的是超大二进制文件比如几十 GB再把整个文件读进缓冲或者内存依然会撑爆。此时要么用带数组的循环读取要么考虑 NIO 的 FileChannel 做内存映射而不是硬堆缓冲。2.4 包装流有什么讲究数据流、对象流与打印流数据流指的是DataInputStream / DataOutputStream它们能直接读写 Java 基本数据类型比如 writeInt、writeLong、readUTF。这在传输结构化数据时很有用但要注意读写顺序必须完全一致否则解析的字节错位数据直接全废。对象流ObjectInputStream / ObjectOutputStream用于序列化也就是把对象变成字节流。后面我会专门讲序列化的坑这里先记住一点要让对象“流起来”这个类必须实现Serializable接口否则运行时会直接抛NotSerializableException。打印流PrintStream / PrintWriter是日常输出用的System.out 本身就是一个 PrintStream。PrintStream 的 write 不会抛 IOException并且自动 flush所以看起来更省事。但自己写落盘代码时不建议用 PrintStream 写二进制它擅长的是人的可读文本输出。3. 实操过程与核心环节实现理论说了一大堆下面来点能直接上手的。我挑选几个日常开发最高频的实操场景每一个都附上了代码和我在实践中用到的细节。在一个项目里如果你认真把这些代码跑一遍你对 IO 的理解会比背二十个类名要深刻得多。3.1 文件拷贝的 N 种写法与性能差异文件拷贝是 IO 的 Hello World。老手和新手写出来的东西完全不一样。我按从傻到优的顺序列几种写法并解释为什么后面的更好。最常见的新手写法是用一个字节数组做缓冲try (InputStream in new FileInputStream(source.bin); OutputStream out new FileOutputStream(target.bin)) { byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } }这一段用了 try-with-resources会自动关流不用手动 close。有些人会忘了在 write 的时候用 0 和 len 限制直接out.write(buffer)如果最后一次读不满 buffer 就会把多余的空字节也写进去属于经典 bug。这种写法性能已经不错因为每次读写 8KB系统调用次数有限。再进一步加入缓冲流包装一次读入变成流内部自动管理块读取try (BufferedInputStream in new BufferedInputStream(new FileInputStream(source.bin)); BufferedOutputStream out new BufferedOutputStream(new FileOutputStream(target.bin))) { byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } }这种方式底层的系统调用次数进一步减少因为 BufferedInputStream 内部有 8KB 缓冲而我又用了 8KB 的数组相当于双重缓冲。在实测小文件上差距不明显但上了几百 MB 的文件两者的耗时差距可以肉眼可见。但是对于 JDK 7 及以上的现代开发我更推荐直接使用Files.copy工具方法Files.copy(Paths.get(source.bin), Paths.get(target.bin), StandardCopyOption.REPLACE_EXISTING);这个方法底层由 JDK 帮你实现了最高效的复制路径代码简洁到令人发指。所以我个人建议除非复制的同时你要做额外加工比如加密、压缩、解析否则没必要手写拷贝循环直接用官方工具类即可。3.2 按行读文本文件与编码处理按行读文件是处理日志、CSV、SQL 脚本最常用的方式。没有 BufferedReader 的话你得自己判断换行符号非常痛苦。有了它readLine()一行搞定但不建议在 readLine 后手动拼接字符串然后还想用流一行行解析时及时处理才是正确姿势。try (BufferedReader reader new BufferedReader( new InputStreamReader(new FileInputStream(data.txt), StandardCharsets.UTF_8))) { String line; while ((line reader.readLine()) ! null) { // 逐行处理逻辑 processLine(line); } }这里有三点值得注意。第一readLine()返回的字符串不包含行尾的换行符如果你需要保留换行符得自己补System.lineSeparator()。第二按行读适合文本如果你处理的是二进制文件千万别用 readLine因为二进制里很容易出现换行符的字节码它会把你的数据拦腰截断。第三显式指定 UTF-8 可以避免跨平台默认编码不一致问题这是我看过很多人掉坑的地方。如果你手里是 JDK 11还有个更舒服的 APIFiles.readAllLines直接返回 List但注意它是一次性把整个文件读进内存所以只适用于小文件。大文件按行处理请用 BufferedReader。3.3 配置文件路径问题和相对路径的坑文件操作里最让人头疼的不是读写代码本身而是“文件在哪”。用相对路径时程序的实际工作目录可能跟你以为的目录完全不一样。特别是你用 IDE 跑和用java -jar跑当前路径不同相对路径就全乱了。我强烈建议优先用类路径资源读取方式try (InputStream in MyClass.class.getClassLoader().getResourceAsStream(config.properties)) { Properties props new Properties(); props.load(in); // ... }getResourceAsStream 是从 classpath 里找资源打包成 jar 也能正常读取不会因为当前工作目录不同而找不到文件。但其缺陷也很明显——只读无法写回。如果配置想动态修改就把它放在外部目录然后用Paths.get(config.properties)读取并做好路径拼接或环境变量指定。另外还有一点常被忽略资源路径里的斜杠。getResourceAsStream(config.properties)是相对 classpath 根目录如果资源在包com/example/conf下路径就要写com/example/conf/config.properties用正斜杠分隔不要用反斜杠。Windows 平台的反斜杠是转义符写路径字符串时要么用\\\\要么干脆用反斜杠改成正斜杠Java 的 Path 和 URL 都支持正斜杠。4. 常见问题与排查技巧实录这部分是我最想写的。理论书到处都有排查经验却往往是血泪换来的。我把平时工作中遇到频率最高的 IO 问题和排查套路集中整理了一下。4.1 经典五连问看似基础实则有深意为什么 read() 返回 int 而不是 byte前面简单提过。byte 范围 -128~127而流结束标记需要额外表示。用 int 返回 0~255这样 0xFF 是 255而 -1 是流结束完美的区分。为什么字符流用 char一次读一个字符性能更差吗不一定是“性能差”而是语义不同。读取一个字符可能底层涉及多个字节的转换比如 UTF-8 中文一个字符占 3 个字节。如果你一个字符一个字符读每个字符都要解码、都要判断是否需要继续读字节效率自然低。所以字符流同样建议缓冲用 BufferedReader 按行读或按 char 数组读。处理二进制和文本到底怎么选判断标准很简单内容是人类能直接阅读的文本用字符流否则一律用字节流。别天真地以为 JSON 是文本就用 FileReader如果你还要处理它的字节长度或做加密签名底层存的还是字节你在字节层面计算才是最稳的。为什么有时候写完文件立刻去读会读不到大概率是没 flush。写操作先到缓冲区还没真正落盘。你立刻在同一个进程里打开文件读读的是磁盘上的旧内容。记得在写完后调用 flush 或 close。文件删不掉或被占用又是怎么回事Windows 系统下如果一个 InputStream 或 OutputStream 没有关闭文件句柄一直被进程占着别的程序删文件会提示“文件正在被另一进程使用”。旧代码里最容易出现这个问题因为开发时没走 try-with-resources某个分支提前 return 导致流没关。排查时可以用lsofLinux或资源监视器Windows查看是谁占用了文件。4.2 流关闭的顺序先外后内还是先内后外传统手工关闭流的经验是先关闭外层流再关闭内层流因为外层流关闭时内部通常会调用内层流的关闭。不过用 try-with-resources 之后这个顺序其实由 JVM 保证它会按照你声明资源的逆序关闭你根本不用操心。所以我的建议是能不用传统写法就别用try-with-resources 是块级代码块作用范围清晰还自带自动关闭。但要留意一个特殊的坑如果你在外层流 close 之后又尝试去读内层流的数据此时内层流虽然句柄还在但多数情况下已经被外层流转为关闭你再读会抛IOException: Stream closed。这种问题在手动管理流的旧代码中最常见。遇到这种报错优先检查是否在某处提前 close 了同一个流对象。4.3 对象序列化的坑serialVersionUID 到底管什么很多人只知道实现 Serializable 就能序列化却不知道它的运行机制其实是在字节流里写入了类的元信息。反序列化时JVM 会拿着流里的类信息跟当前类对比。serialVersionUID就是用来做版本校验的。如果你写了一个类没显式定义 serialVersionUID编译器会自动生成一个这个值跟类的包名、类名、字段名、方法签名等都有关联。只要类结构有任何变化比如加了一个字段自动生成的 serialVersionUID 就变了然后反序列化时 JVM 一对比发现不一致直接抛InvalidClassException报错内容通常会提示“serialVersionUID 不一致”。解决办法很简单显式声明一个固定的 serialVersionUID比如 1L。这样即使类结构有轻微改动只要你能保证字段兼容反序列化依然能继续。但这不代表加字段完全无风险——老版本流里没有这个字段反序列化出来新对象该字段就是默认值你在代码里如果直接使用它就可能是 null 或 0要注意判空。提示涉及长期存储、跨版本升级的业务对象不要依赖自动生成的 UID一定要显式声明。这算是易学难精的序列化里最基本的保命技巧。4.4 三个常见的“伪 IO 坑”第一个坑读文件把文件全加载进内存。老代码里经常看到byte[] data InputStream.readAllBytes()这种写法。它是 JDK 9 提供的方法小文件没问题但大文件会把内存瞬间打满。如果你要处理的是几十 MB 的 Excel、几百 MB 的日志老老实实按块处理。第二个坑字符串拼 SQL 或 JSON 时代码里写死编码。我记得有一种写法是构建字符串时用new String(bytes, utf-8)但这里的 utf-8 是一个字符串字面量一旦手滑拼错或文件编码跟它不一致就会静默乱码不报错但数据已经坏了。更稳的写法是用StandardCharsets.UTF_8常量既省去异常处理也避免字符串指定的编码拼写错误。第三个坑把 IO 操作写进同步代码块导致性能瓶颈。IO 底层是阻塞的如果你在 synchronized 块里做耗时的文件读写其他线程也得排队等同一把锁。优先把锁粒度缩小只保护共享数据的那部分IO 操作尽量放到锁外面。这在日志系统和插件系统里特别常见也是很多并发性能问题的根源。4.5 顺带澄清Java IO 流跟网络 NIO 不是一回事很多人在搜索“Java IO 流知识汇总”时会顺手搜出NIO、Netty、IO 多路复用之类的内容然后把它们混为一谈。这里我做一次简单的切割本文讨论的是 Java SE 标准的阻塞 IOBIO即基于 InputStream、OutputStream、Reader、Writer 这套体系。NIONon-blocking IO是 JDK 1.4 引入的另一套体系核心是 Channel、Buffer、Selector主要用于高并发网络编程。NIO 虽然也常被称为“IO”但它解决的问题完全不同——它是为了摆脱“一个连接一个线程”的阻塞模型而生的。理解 BIO 是理解 NIO 的地基。你如果连阻塞 IO 的一次读写到底卡在哪个系统调用上都说不清直接从 NIO 入手会非常痛苦。所以我建议学习顺序还是先啃完 BIO再上 NIO否则你连“阻塞”和“非阻塞”这两个词在 Java 里到底指什么阶段都分不清。而在实际业务中最容易遇到的还是要区分文件 IO 和网络 IO文件 IO 不需要处理半包粘包问题网络 IO 需要文件 IO 关注的是磁盘缓冲区与用户缓冲区的关系网络 IO 关注的是 Socket 缓冲区与网卡中断。搞清楚这一点遇到“IO 性能明显下降了”这类问题时你的排查方向才不会从一开始就跑偏。最后说点个人体会。我见过太多人把 Java IO 当成一个“背了就能过”的面试知识点其实它是最需要动手体会的一个主题。你可以花半小时写一段文件复制、再花半小时处理一个中文乱码文件这比你翻一百页 API 文档都管用。后来又接触了 NIO 和 Netty回过头来看 BIO 反而更亲切那些“新特性”不过是在底层换了一套更高效的搬运工而已。这套知识后续想深入的话建议你接着试试压缩流GZIP、序列化协议对比Java 原生序列化、Protobuf、JSON以及 NIO 的文件通道每一步都会让你对整个输入输出体系的认识更通透。
RELATED

相关推荐

UnrealBuildTool完全指南:UBT编译流程、模块依赖与跨平台构建

UnrealBuildTool完全指南:UBT编译流程、模块依赖与跨平台构建

1. 一次"灵异编译"让我决定把它彻底搞明白 做UE C项目的人,十有八九都遇到过这种场面:自己明明只在Actor类里加了一行 UPROPERTY ,或者改了一个头文件里某个函数签名,然后编译整个工程,报错信息却指向引擎…

📅 2026/9/30 16:04:32
VMware 安装 Windows Server 2003 虚拟机教程与避坑指南

VMware 安装 Windows Server 2003 虚拟机教程与避坑指南

1. 先搞清楚:为什么今天还要在 VMware 里跑 Windows Server 2003如果你在搜索引擎里敲下"VMware 虚拟机 Windows Server 2003 安装教程",大概率不是出于怀旧。我这些年被问到这个问题,基本集中在三种场景里,而且每一种都…

📅 2026/9/30 15:54:07
芯片烧录自制还是外包?从量产成本到固件安全的决策指南

芯片烧录自制还是外包?从量产成本到固件安全的决策指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/30 15:54:07
MORE NEWS

更多资讯

📰

基于Python的图书馆借阅数据分析:从清洗到可视化

简介:这份资源是一篇围绕Python编程语言与Django框架开展图书馆借阅数据分析系统设计与实现的原创毕业论文,面向专科与本科毕业设计写作及Python数据科学入门者,覆盖数据抓取、清洗、建模、可视化和Web交互等完整流程。内容包含数据分析概念、…

📰

WorkBuddy AI工作台从安装到避坑:API配置与Agent实战指南

1. 为什么我要认真聊聊 WorkBuddy 这个 AI 工作台第一次接触 WorkBuddy 是在一个做企业数字化的朋友那里,他当时正被一堆重复性的文档整理、数据核对和跨系统操作折磨得够呛。他给我演示了一下:在 WorkBuddy 里输入一句“把这份合同里的关键条款提取出来…

📰

Node.js本地AI文档预处理:分片引擎与L0自然语言硬规则调度实战

先说个背景。我做这个模块的目标很简单:让本地AI在处理办公文档时,能先经过一层我们自己可控的预处理,把乱七八糟的Word、PDF、TXT切成模型适合吃的“碎片”,同时用一套自然语言定义的硬规则去约束调度顺序和过滤逻辑。这么做的好…

📰

Python图书馆借阅数据分析:从爬虫到推荐系统的完整实战

简介:一份面向专科与本科毕业生的原创毕业论文,围绕Python在图书馆借阅数据分析中的实际应用展开,结合网络爬虫、数据挖掘与Django框架,完整覆盖数据采集、清洗、可视化、统计分析与系统设计实现等环节。全文经降重处理且超过万字…

📰

Model-Optimizer实战指南:从PyTorch到vLLM+TensorRT-LLM的端到端推理优化

1. 项目概述:Model-Optimizer 不是工具名,而是一类工程实践的统称“Model-Optimizer”这个名称乍看像某个开源库或商业软件,但实际在工业级AI推理部署一线,它根本不是一款现成可下载的“一键优化器”,而是指代一套围绕…

📰

小样本工业缺陷检测全流程指南:从数据策略到漏检闭环

工业缺陷检测这行干久了,你会发现一个特别拧巴的现象:产线上真正致命的缺陷,往往是那些最初没预料到的,而且数量少得可怜。我们接触过一个汽车零部件项目,客户给的第一批培训数据里,某个关键表面缺陷只有27…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬