一、IO框架概述
Java IO 学习是一项非常艰巨的任务,这挑战来自于其覆盖了所有的技术可能性。它不仅存在各种 I/O 源端还有想要和它通信的接收端(文件/console/网络),而且还需要以不同的方式与它们进行通信(顺序/随机存取/缓冲/二进制/字符/行/字 等等)。
二、IO简介
数据流是一组有序、有起点和终点的字节的数据序列。包括输入流和输出流。流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此 Java 中的流分为两种:
- 字节流:数据流中最小的数据单元是字节
- 字符流:数据流中最小的数据单元是字符
Java.io 包中最重要的就是 5 个类和一个接口。5 个类指的是 File、OutputStream、InputStream、Writer、Reader;一个接口指的是 Serializable。
2.1 Java I/O 三个层次介绍
- 流式部分 – 最主要的部分。如:OutputStream、InputStream、Writer、Reader 等
- 非流式部分 – 如 File 类、RandomAccessFile 类和 FileDescriptor 等类
- 其他 – 文件读取部分的安全相关的类,如:SerializablePermission 类,以及与本地操作系统相关的文件系统的类,如:FileSystem 类和 Win32FileSystem 类和 WinNTFileSystem 类。
三、装饰器模式在 IO 中的应用
3.1 设计模式 - 装饰器模式
从流的整个发展史来看,各种类之间的关系都沿用了装饰器模式,一个类的功能可以用来修饰其他类,然后组合成一个比较复杂的流:
DataOutputStream out = new DataOutputStream( |
从上面的代码块中可以看出这些类的关系:为了向文件中写入数据,首先需要创建一个 FileOutputStream,然后为了提升访问的效率,将它发送给具备缓存功能的 BufferedOutputStream,而为了实现与机器类型无关的 Java 基本类型数据的输出,将缓存的流传给 DataOutputStream。 其基本目的都是为 OutputStream 添加额外的功能。这种额外的功能的添加就是采用了装饰模式来构建的代码。
3.2 装饰器 vs 继承
使用装饰器模式而非继承的原因:如果每种功能组合都用一个子类实现,会导致类的爆炸式增长。例如,假设有 N 种基础流和 M 种修饰功能,使用继承需要 N * 2^M 个子类(每个组合一个类),而使用装饰器只需要 N + M 个类。
四、字节流深度解析
4.1 字节流体系
InputStream (abstract) |
4.2 字节流功能分类
OutputStream -> FileOutputStream/FilterOutputStream -> DataOutputStream -> BufferedOutputStream
相应的学习 InputStream 方法即可。这里必须强调:所有的写入写出,其参考的对象都是内存,从内存写入文件,从文件读取到内存,这一点搞清楚,才能理解输入输出的奥义。
FilterOutputStream
从学习的角度来,我们首先应该掌握 FilterOutputStream 以及 FileOutputStream,这两个类是基本的类,从继承关系来看,不难发现它们都是对 abstract 类 OutputStream 的拓展,都是它的子类。然而,伴随着对 Stream 流的功能的拓展,后面出现了 DataOutputStream(将 Java 中的基础数据类型写入数据字节输出流中,并保存在存储介质中,然后可以用 DataInputStream 从存储介质中读取到程序中还原成 Java 基础类型)。
BufferedOutputStream
为了提升 Stream 的执行效率,出现了 BufferedOutputStream。BufferedOutputStream 实质上是本地添加了缓存的数组。在使用 BufferedOutputStream 之前每次从磁盘读入数据的时候都是需要访问多少 byte 数据就向磁盘中读多少个 byte 的数据,而出现 BufferedOutputStream 之后,策略就改了,会先读取整个缓存空间相应大小的数据,这样就是从磁盘读取了一块比较大的数据,然后缓存起来,从而减少了对磁盘的访问次数以达到提升性能的目的。
默认缓冲区大小是 8192 字节(8KB)。对于顺序读写,适当增大缓冲区(如 64KB)可以显著提升性能。
4.3 BufferedInputStream 的内部实现
// BufferedInputStream 核心源码简化 |
五、字符流深度解析
5.1 字符流体系
Reader (abstract) |
5.2 字符流功能分类
Writer -> FilterWriter -> BufferedWriter -> OutputStreamWriter -> FileWriter -> 其他
BufferedWriter/BufferedReader
BufferedWriter 是 Writer 类的一个子类。它的功能是为传入的底层字符输出提供缓存功能。当使用底层字符输出流向目的地写入字符或字符数组时,每写入一次就要打开一次与目的地的连接,这样频繁的访问会降低写入写出效率,也有可能导致对存储介质造成一定的破坏。当我们使用 BufferedWriter 将底层字符输出流(比如 FileWriter)包装一下后,便可以在程序中将要写入到文件中的字符先写入到 BufferedWriter 的内置缓存中,当达到一定数量时,一次性写入 FileWriter 流中,此时 FileWriter 就可以打开一个通道,将这个数据块写入到文件中。
OutputStreamWriter/InputStreamReader
输入字符转换流,是字节流转向字符流的桥梁,用于将字节流转换成字符流,通过指定的或者默认的编码将从底层读取的字节转换成字符返回到程序中。其本质是使用内部类来完成所有工作:
- StreamEncoder:使用指定的或者默认的编码集将字符转码为字节
- StreamDecoder:使用指定的或者默认的编码集将字节转码为字符
OutputStreamWriter/InputStreamReader 只是对 StreamEncoder/StreamDecoder 进行了一层封装,其内部所有方法核心都是调用 StreamEncoder/StreamDecoder 来完成的。
在使用这两个流的时候需要注意:由于这两个流要频繁对读取或者写入的字节或者字符进行转码、解码、以及与底层流的源和目的地进行交互,所以使用的时候用 BufferedWriter、BufferedReader 进行包装,以达到最高效率和保护存储介质。
FileReader/FileWriter
FileReader 和 FileWriter 继承于 InputStreamReader/OutputStreamWriter。FileWriter 文件字符输出流,主要用于将字符写入到指定的打开的文件中,其本质是通过传入的文件名、文件、或者文件描述符来创建 FileOutputStream,然后使用 OutputStreamWriter 使用默认编码将 FileOutputStream 转换成 Writer。
FileReader 文件字符输入流,主要用于将文件内容以字符形式读取出来,一般用于读取字符形式的文件内容,也可以读取字节形式,但因为 FileReader 内部也是通过传入的参数构造 InputStreamReader,并且只能使用默认编码,所以由于其无法控制编码问题,容易导致出现乱码。
5.3 字符编码深入
// 字符编码的关键问题 |
六、字节流与字符流的关系
6.1 两者区别
字节流在操作的时候本身是不会用到缓冲区(内存)的,是与文件本身直接操作有关的,而字符流在操作的时候是使用到缓冲区的;字节流在操作文件时,即使不关闭资源(close 方法),文件也能输出,但是如果字符流不使用 close 方法的话,则不会输出任何内容。说明字符流用的是缓冲区,并且可以使用 flush 方法强制刷新缓冲区,这样操作之后才能在不 close 的情况下输出内容。
在所有的硬盘上保存文件或者进行传输的时候都是以字节的方法进行的,包括图片也是按字节完成的。而字符是只有在内存中才会形成,所以使用字节的操作是最多的。
如果 Java 程序实现一个拷贝功能,则应该选用字节流进行操作(可能拷贝图片),并且采用边读边写的方式(节省内存)。
6.2 两者转换
虽然 Java 支持字节流和字符流,但有时需要在字节流和字符流两者之间做转换。InputStreamReader 和 OutputStreamWriter,这两个类就是作为字节流和字符流之间相互转换的类。
InputStreamReader 用于将一个字节流中的字节解码成字符:
InputStreamReader(InputStream in); // 默认字符集 |
OutputStreamWriter 用于将写入的字符编码成字节后写入一个字节流:
OutputStreamWriter(OutputStream out); // 默认字符集 |
为了避免频繁的转换字节流和字符流,对以上两个类进行了封装:
- BufferedWriter 类封装了 OutputStreamWriter 类
- BufferedReader 类封装了 InputStreamReader 类
七、Java NIO 架构
7.1 NIO 的三大核心组件
Java NIO(New IO / Non-blocking IO)引入了三大核心组件:
- Buffer(缓冲区):一个连续的内存块,提供对数据的结构化访问。所有数据通过 Buffer 读写。
- Channel(通道):模拟传统 I/O 中的流,但它是全双工的(可同时读写)。主要实现:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。
- Selector(选择器):单个线程可以监控多个 Channel 的 I/O 事件(OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT)。底层使用操作系统的 I/O 多路复用机制(Linux 的 epoll、macOS 的 kqueue)。
// NIO Selector 的基本使用模式 |
7.2 Buffer 的精髓
// Buffer 的三个关键属性: |
7.3 DirectByteBuffer vs HeapByteBuffer
// HeapByteBuffer:数据在 JVM 堆上 |
7.4 零拷贝技术
// 1. FileChannel.transferTo / transferFrom |
八、Android 特有的 I/O 关注点
8.1 Android Parcel vs Serializable
Android 的 Parcelable 是专门为 Android 设计的序列化接口,性能远超 Java 的 Serializable:
| 特性 | Serializable | Parcelable |
|---|---|---|
| 实现方式 | 反射 + JVM 序列化机制 | 手动编码 writeToParcel/createFromParcel |
| 内存开销 | 产生大量临时对象 | 几乎没有额外对象分配 |
| 性能 | 慢(反射开销大) | 快(直接写入 Parcel 的 native buffer) |
| 用途 | 文件存储、网络传输 | IPC(Intent/Bundle 传递)、进程间传递 |
| 代码量 | 只需实现接口 | 需要手动编写序列化逻辑 |
// Parcelable 实现模板 |
8.2 Parcel 的底层原理
Parcel 的底层实现在 native 层(frameworks/native/libs/binder/Parcel.cpp)。它是一个内存缓冲区,序列化数据按顺序写入,反序列化时按相同顺序读取。Parcel 使用共享内存(ashmem)在进程间传递大数据,避免了拷贝。
// Parcel 的 native 实现(简化) |
8.3 Okio:Square 的现代 I/O 库
Okio 是对 Java IO/NIO 的补充,已被 OkHttp、Moshi 等库采用:
// Okio 的核心概念 |
九、大文件处理与 mmap
// 使用 mmap 处理大型文件的随机访问 |
十、面试常问题目
Q1: 字节流和字符流的核心区别是什么?什么场景下使用哪个?
字节流以 byte(8-bit)为最小单位操作,不关心数据编码,适用于所有文件类型(图片、视频、二进制文件等)。字符流以 char(16-bit Unicode)为最小单位操作,自动处理字符编码转换,只适用于文本文件。选择原则:如果不是纯文本格式,一律使用字节流。对于文本文件,如果需要在流中直接操作字符(如 readLine)且关心编码,使用字符流;如果只是简单的字节拷贝,使用字节流(避免编解码开销)。
Q2: Java NIO 相比传统 IO 的核心优势是什么?
(1) 非阻塞 I/O:Selector 允许单线程管理多个 Channel,避免了传统 IO 中一个连接一个线程的资源浪费;(2) 零拷贝:FileChannel.transferTo/transferFrom 使用 sendfile 系统调用,数据在内核中传输不经过用户空间,性能远超传统的 while(read→write);(3) DirectByteBuffer 减少 I/O 操作中的内存拷贝;(4) Channel 是全双工的。
Q3: DirectByteBuffer 和 HeapByteBuffer 有什么区别?什么情况下应该使用 DirectByteBuffer?
HeapByteBuffer 数据在 JVM 堆上,分配回收快,受 GC 管理,但 I/O 操作时需要额外拷贝到堆外内存。DirectByteBuffer 数据在堆外内存(通过 Unsafe.allocateMemory),分配回收成本高,但 I/O 操作可零拷贝执行。使用建议:对于需要长期持有的大缓冲区(如网络数据缓冲区、文件映射)使用 DirectByteBuffer;对于临时小缓冲区使用 HeapByteBuffer。注意:DirectByteBuffer 受 MaxDirectMemorySize 限制,超出会 OOM。
Q4: Parcelable 和 Serializable 的区别?为什么 Android 推荐使用 Parcelable?
Serializable 使用 Java 反射机制实现序列化,会产生大量临时对象,触发 GC,性能差。Parcelable 由开发者手动实现序列化逻辑,性能好,几乎无额外对象分配。Serializable 适合持久化存储和网络传输(标准 Java 格式),Parcelable 专为 Android IPC 设计(性能优先)。在 Intent 传递数据、进程间传递对象时,必须使用 Parcelable(或实现 Serializable,但不推荐)。
Q5: mmap 在文件 I/O 中的优势和局限是什么?
优势:(1) 操作系统按需加载页面(demand paging),只读需要的数据部分;(2) 无需用户空间与内核空间的拷贝(页面直接映射到进程地址空间);(3) 多个进程共享同一文件的映射(MAP_SHARED);(4) 修改自动同步回文件(MAP_SHARED 模式下)。局限:(1) 无法映射超过进程地址空间大小的文件(32位系统约 3GB);(2) 没有内置的写原子性保证;(3) 文件大小不能动态增长(需提前确定大小);(4) 映射开销大,不适合小文件。
参考源码路径:
- Java IO 源码:
$JAVA_HOME/src/java.base/share/classes/java/io/ - Android Parcel:
frameworks/base/core/java/android/os/Parcel.java - Android Parcel native:
frameworks/native/libs/binder/Parcel.cpp - Okio:
https://github.com/square/okio - MMKV:
https://github.com/Tencent/MMKV

