本文最后更新于11 天前,其中的信息可能已经过时,如有错误请发送邮件到2327470875@qq.com
一、先把核心概念理清(最重要)
- “流(Stream)”:一种抽象,用来顺序读取或写入数据(像水流一样)。流是一次性、单向的(读或写),读到末尾返回
-1
(字节流)或null
(readLine)。 - 字节流(byte stream) vs 字符流(char stream):
- 字节流(
InputStream
/OutputStream
)适合二进制数据(图片、音频、压缩包)。 - 字符流(
Reader
/Writer
)适合文本,内部处理字符编码(但要注意编码选择)。
- 字节流(
- 阻塞 I/O:大多数传统
java.io
操作是阻塞的(读/写会等待数据)。 - 流 与 通道(Channel)/NIO 的区别:
java.nio
用 Buffer + Channel,支持更高性能、内存映射、非阻塞 IO(进阶部分会介绍)。
二、类的总体结构(重要类一览)
字节抽象
- 抽象类:
InputStream
,OutputStream
- 常用实现:
- 文件:
FileInputStream
,FileOutputStream
- 缓冲:
BufferedInputStream
,BufferedOutputStream
- 数据型:
DataInputStream
,DataOutputStream
(读写原始类型) - 对象序列化:
ObjectInputStream
,ObjectOutputStream
- 推回:
PushbackInputStream
- 串联:
SequenceInputStream
- 管道:
PipedInputStream
/PipedOutputStream
- 打印:
PrintStream
(System.out
属此类)
- 文件:
字符抽象
- 抽象类:
Reader
,Writer
- 常用实现:
- 文件:
FileReader
,FileWriter
(注意:使用平台默认编码) - 缓冲:
BufferedReader
,BufferedWriter
- 转换:
InputStreamReader
/OutputStreamWriter
(用它们把InputStream
↔Reader
,可指定Charset
) - 打印:
PrintWriter
- 字符串:
StringReader
,StringWriter
,CharArrayReader/Writer
- 文件:
工具 / 新 API
java.io.RandomAccessFile
(随机访问)java.nio.file.Path
,Files
(NIO.2,简化文件操作:Files.copy
,Files.readAllBytes
等)java.nio.channels.FileChannel
,ByteBuffer
(高性能 I/O)- 异步通道:
AsynchronousFileChannel
(进阶)
三、常用方法与语义(必须记住)
int read()
:字节/字符;返回读取的字节(0–255)或-1
(EOF)。int read(byte[] b)
/int read(byte[] b, int off, int len)
:返回实际读到的字节数,-1
表示 EOF。不要假设一次能读满整个数组。int read(char[] cbuf, int off, int len)
:Reader 的对应方法。long skip(long n)
:跳过 n 字节/字符,返回实际跳过数量。int available()
:返回可立即读取而不阻塞的字节数(估计值,不能完全依赖)。void write(byte[] b)
/void write(byte[] b, int off, int len)
:写操作。void flush()
:把缓冲区的数据写出到底层目标(对于OutputStream
/Writer
,显式刷新很重要)。void close()
:关闭流并释放资源。通常close()
会先flush()
(但不要总依赖,最好手动 flush)。mark(int readlimit)
/reset()
:在支持标记的流上回退到 mark。不是所有流都支持(用markSupported()
判断)。boolean ready()
(Reader
):是否可以不阻塞地读取字符。ObjectInputStream.readObject()
:反序列化会抛出ClassNotFoundException
。
四、编码相关(非常容易出错)
FileReader
/FileWriter
使用 平台默认编码 —— 这会导致跨平台乱码(强烈建议不要用它们处理明确编码的文本)。- 使用
InputStreamReader
/OutputStreamWriter
并指定编码,例如StandardCharsets.UTF_8
:new BufferedReader(new InputStreamReader(new FileInputStream("a.txt"), StandardCharsets.UTF_8));
- 总原则:文本要显式声明编码,尤其是读写网络或跨平台文件时。
五、缓冲(为什么用)与性能
BufferedInputStream
/BufferedOutputStream
、BufferedReader
/BufferedWriter
可以显著减少底层系统调用(I/O 调用昂贵)。- 默认缓冲大小通常是 8KB(8192),对多数场景足够。复制大文件时可以用更大的缓冲(例如 16KB、32KB)做测试。
- 性能建议:
- 对文本使用
BufferedReader
+BufferedWriter
。 - 对二进制用
BufferedInputStream
+BufferedOutputStream
。 - 对超大文件或要求极限性能,考虑
FileChannel.transferTo/transferFrom
或内存映射MappedByteBuffer
。
- 对文本使用
六、常见 API 用法 & 代码示例(实用)
- 按行读取文本(推荐)
Path path = Paths.get("file.txt");
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
- 写文本(推荐)
Path path = Paths.get("out.txt");
try (BufferedWriter bw = Files.newBufferedWriter(path, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
bw.write("第一行");
bw.newLine();
bw.write("第二行");
}
- 复制二进制文件(经典)
try (InputStream in = new BufferedInputStream(new FileInputStream("a.jpg"));
OutputStream out = new BufferedOutputStream(new FileOutputStream("b.jpg"))) {
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
}
更简洁(且可能更快):
Files.copy(srcPath, destPath, StandardCopyOption.REPLACE_EXISTING);
- 读写基本类型(Data streams)
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin"))) {
dos.writeInt(42);
dos.writeDouble(Math.PI);
}
try (DataInputStream dis = new DataInputStream(new FileInputStream("data.bin"))) {
int x = dis.readInt();
double d = dis.readDouble();
}
- 对象序列化(注意兼容性与安全)
// 写对象
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.dat"))) {
oos.writeObject(myObject); // myObject 必须 implements Serializable
}
// 读对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.dat"))) {
MyClass obj = (MyClass) ois.readObject();
}
注意:
- 类必须
implements Serializable
,并最好声明private static final long serialVersionUID
。 transient
字段不会被序列化。- 不要从不可信来源反序列化(安全风险)。
- RandomAccessFile(随机访问)
try (RandomAccessFile raf = new RandomAccessFile("file.dat", "rw")) {
raf.seek(100);
raf.writeInt(123);
raf.seek(0);
int x = raf.readInt();
}
- FileChannel + transferTo(高性能复制)
try (FileChannel in = FileChannel.open(src, READ);
FileChannel out = FileChannel.open(dest, WRITE, CREATE, TRUNCATE_EXISTING)) {
in.transferTo(0, in.size(), out);
}
七、try-with-resources 与关闭(必须养成的好习惯)
- 用 try-with-resources(Java 7+)自动关闭:
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
// ...
}
- 总结:永远确保流被关闭(否则资源泄露)。外层流关闭时会关闭内层流(关闭包装流会自动关闭被包裹的流)。
八、常见坑(面试/实战常考)
- 忽略编码 → 导致乱码(尤其用
FileReader
/FileWriter
)。 - 以为
read(byte[])
会填满数组:它可能返回少于数组长度的数据;必须用循环判断返回值。 - 忘记 flush():缓冲写入需要
flush()
才能确保落盘或发送。 - 使用
available()
判断 EOF:available()
不能作为 EOF 判断。 - 序列化兼容性问题:类结构改变会导致反序列化失败(
serialVersionUID
)。 - 不关闭流导致句柄耗尽:特别在循环创建文件流时容易出现 “Too many open files”。
- 在多线程共享同一个流未同步:会出现竞争问题。最好每个线程独立流或外部同步。
ObjectInputStream
反序列化不可信数据:可能导致远程代码执行或数据篡改风险。
九、进阶:NIO / NIO.2(为什么学习)
- 为什么:更高性能、更灵活(非阻塞、选择器、直接内存、内存映射),以及更现代的
Files
、Path
API。 - 关键概念:
ByteBuffer
:数据容器(读/写模式切换需flip()
/clear()
)Channel
:类似流,但读写用ByteBuffer
,支持transferTo/transferFrom
。Selector
:用于管理多个非阻塞SelectableChannel
(常用于高并发网络服务)。Files
(NIO.2):Files.newBufferedReader/Writer
、Files.copy
、Files.walk
等,写起来更简洁。
- 简单示例(ByteBuffer + FileChannel)
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(4096);
while (ch.read(buf) > 0) {
buf.flip();
while (buf.hasRemaining()) {
// 处理字节
byte b = buf.get();
}
buf.clear();
}
}
十、快速参考——任务到 API(速查表)
- 读小文本文件(逐行):
Files.newBufferedReader
或BufferedReader.readLine()
- 写文本:
Files.newBufferedWriter
或PrintWriter
- 复制文件:
Files.copy
或FileChannel.transferTo
- 读写二进制:
BufferedInputStream
/BufferedOutputStream
- 读写原始类型:
DataInputStream
/DataOutputStream
- 随机访问:
RandomAccessFile
- 对象持久化:
ObjectOutputStream
/ObjectInputStream
(注意安全) - 高性能:
FileChannel
+ByteBuffer
/MappedByteBuffer
- 网络 socket:
Socket#getInputStream()
/getOutputStream()
(通常与BufferedReader/PrintWriter
或DataStreams
包装)
十一、练习建议(边做边学)
- 写一个工具把文本文件从 GBK 转成 UTF-8(练习编码转换)。
- 实现一个二进制文件复制器,测不同 buffer 大小的速度差异(性能感知)。
- 实现小型 RPC:客户端发送命令,服务端用
BufferedReader.readLine()
逐行读取并回应(练习 socket 流封装)。 - 序列化一个对象,改类字段后再反序列化试验兼容性(理解
serialVersionUID
)。 - 用
FileChannel.transferTo
复制大文件并比较Files.copy
和传统流的速度。
十二、总结性建议(实战经验)
- 首选
Files
API(NIO.2) 处理常见文件操作:更简洁、更安全。 - 处理文本时明确编码(
StandardCharsets.UTF_8
)。 - IO 操作用缓冲(
Buffered*
)以提升性能。 - 资源用 try-with-resources 自动关闭。
- 需要高并发/高吞吐时学习 NIO(Channel/Selector/ByteBuffer)。
- 不要从不可信来源反序列化,也不要把序列化当作长期兼容的存储格式(用 JSON/Protobu