从底层字节流到高层缓冲流,从文件操作到网络通信
I/O 的本质是数据在不同介质之间的流动。理解 I/O 的关键在于确定以内存为参照点:
记忆口诀:外→内为输入,内→外为输出。永远站在内存的视角看问题。
| 流向 | 操作 | 方法示例 |
|---|---|---|
| 外部 → 内存 | 输入(Input) | read() |
| 内存 → 外部 | 输出(Output) | write() |
关键认知:File file = new File(path) 并不会在磁盘上创建一个真实的文件!它只是在内存中创建了一个表示路径的对象。
当你把它作为参数传给 FileInputStream时,如果文件不存在就会创建出来。
File file = new File("test.txt"); // 只是内存中的一个路径对象,不是物理文件
FileInputStream fis = new FileInputStream(file); // 此时如果文件不存在,则创建物理文件
FileInputStream和 FileOutputStream是 Java 中最基础的字节流,用于按字节读写文件。传路径或传File 对象都可以。
// 方式一:传路径字符串
InputStream inputStream = new FileInputStream("path/to/file.txt");
OutputStream outputStream = new FileOutputStream("path/to/file.txt");
// 方式二:传 File 对象
File file = new File("path/to/file.txt");
InputStream inputStream = new FileInputStream(file);
OutputStream outputStream = new FileOutputStream(file);
字节流体系的核心:InputStream 是抽象类,定义了 read()规范;具体实现类决定从哪里读(文件、字节数组、网络等)。
使用 try块来自动管理资源,无需手动调用 close():
try (InputStream inputStream = new FileInputStream(file)) {
// 使用 inputStream 读取数据
int data = inputStream.read();
// ...
} catch (IOException e) {
e.printStackTrace();
}
// try 块结束时,inputStream.close() 会被自动调用
这是用来读字节的。InputStream 以 byte为单位操作数据,是最底层的读取方式。
InputStream 只能读字节,Reader 才能读字符。在字节流之上再套一个 InputStreamReader来读取字符:
// 通用方式(推荐):InputStreamReader 可以适配任何 InputStream
Reader reader = new InputStreamReader(inputStream);
// 不限于文件 — 网络流、内存流等任何 InputStream 都可以转成 Reader
当然也可以直接读文件:
// 便捷方式:直接从文件读取字符
Reader reader = new FileReader("path/to/file.txt");
这个设计相对更通用— 因为你并不是总是从文件里面读取数据,网络、内存、管道等各种来源都可以通过 InputStream→ InputStreamReader这条路径处理。
| 特性 | 字节流 | 字符流 |
|---|---|---|
| 处理单位 | byte(8位) |
char(16位) |
| 适用场景 | 二进制文件(图片、音视频、压缩包) | 文本文件(.txt、.java、.xml) |
| 编码处理 | 不处理编码 | 自动处理字符编码 |
| 抽象基类 | InputStream/ OutputStream |
Reader/ Writer |
| 文件操作类 | FileInputStream/ FileOutputStream |
FileReader/ FileWriter |
| 桥梁转换 | - | InputStreamReader/ OutputStreamWriter |
选择原则:处理文本用字符流,处理二进制数据用字节流。当不确定时,用字节流总是安全的。
在 Reader之上再包一层 BufferedReader,实现整行读取:
BufferedReader bufferedReader = new BufferedReader(reader);
String line = bufferedReader.readLine(); // 整行整行地读
BufferedReader 的目的其实是一次性缓存更多字节,避免每次都从文件里面操作,减少磁盘 I/O 开销。
核心机制:就好像从超市拿东西,每次家里一需要就去超市拿,一来一回很不方便。但如果建了一个仓库,一次性多存一点,要的时候直接去仓库拿就行了。BufferedReader 里有个私有变量 private static int defaultCharBufferSize = 8192,也就是默认一次性读 8KB 个字节。
写的时候也可以用 Buffer:
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
bufferedOutputStream.write(data); // 并不会直接写入文件!
bufferedOutputStream.flush(); // 主动将缓冲区字节冲入文件
关键细节:调用 bufferedOutputStream.write()并不会直接写入文件,数据先存在缓冲区,直到缓冲区满了才会真正写入文件。你需要主动调用 flush()将缓冲区字节冲入文件。
设计目的:主要是为了把多次的 I/O 操作合并成一次,减少磁盘操作次数,大幅提升性能。
自动 flush:BufferedOutputStream 在关闭(close)的时候也会自动把缓冲区的字节冲入文件,所以即使忘记调用 flush(),只要正常关闭了流,数据也不会丢失。
// 原始流
InputStream in = ...;
// 套缓冲流(标准写法 — 有利无害)
BufferedInputStream bin = new BufferedInputStream(in);
// 再用 bin.read(buffer) 读
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bin.read(buffer)) != -1) {
// 处理读取到的数据
}
这种写法有利无害。即使数据量小,套一层 Buffered 也不会引入额外问题;数据量大时,性能提升非常明显。
| 缓冲区大小 | 适用场景 | 说明 |
|---|---|---|
| 4KB | 小文件、频繁读写 | 内存占用小,适合频繁操作 |
| 8KB(默认) | 通用场景 | 平衡性能与内存,推荐默认使用 |
| 16KB-64KB | 大文件复制、批量处理 | 减少 I/O 次数,提升吞吐量 |
| 128KB+ | 超大文件、顺序读写 | 配合磁盘预读,最大化吞吐量 |
注意:缓冲区不是越大越好。过大的缓冲区会增加内存占用,且可能无法被 CPU 缓存有效利用。通常 8KB-32KB 是最优范围。
同理,OutputStream → BufferedOutputStream、Writer → BufferedWriter 也是一样的层次结构。
文件的"关闭"其实是内存的释放。
当你读写文件时,系统会腾出一块内存来存储文件的相关信息(文件描述符、缓冲区等),关闭文件 = 释放这块内存。
因为 InputStream和 OutputStream都实现了 Closeable接口,且 Reader也实现了 Closeable,BufferedReader又继承自 Reader,所以它们都可以通过 try 来自动管理资源。
// 所有实现了 Closeable 的都可以这样用
try (InputStream in = new FileInputStream("read.txt");
OutputStream out = new FileOutputStream("write.txt");
BufferedReader reader = new BufferedReader(new FileReader("data.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("out.txt"))) {
// 所有流都会在 try 块结束时自动关闭
// 关闭顺序与声明顺序相反
} catch (IOException e) {
e.printStackTrace();
}
Android 中复制文件的方法多种多样:
FileUtils工具类提供现成的复制方法File自带扩展方法 copyTo()Java 7+ 提供的 Files.copy()是最简单高效的文件复制方式:
// 最简单的复制方式
Path source = Paths.get("source.txt");
Path target = Paths.get("target.txt");
Files.copy(source, target);
// 覆盖已存在的目标文件
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
// 保留文件属性(权限、时间戳等)
Files.copy(source, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
// 复制目录(不递归)
Files.copy(sourceDir, targetDir);
// 递归复制目录
Files.walk(sourceDir)
.forEach(sourcePath -> {
Path targetPath = targetDir.resolve(sourceDir.relativize(sourcePath));
try {
Files.copy(sourcePath, targetPath);
} catch (IOException e) {
e.printStackTrace();
}
});
推荐理由:Files.copy()底层调用操作系统原生 API,性能最优,代码简洁。
纯 Java 就是一个字节一个字节地搬。其实那些复制文件的底层 API 也是一个一个字节照搬的,只是它们做了更多封装和优化。
// 纯 Java 文件复制 — 带缓冲的字节搬运
try (FileInputStream in = new FileInputStream("source.txt");
FileOutputStream out = new FileOutputStream("target.txt")) {
byte[] buffer = new byte[8192]; // 一次搬 8KB
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead); // 读到多少写多少
}
} catch (IOException e) {
e.printStackTrace();
}
本质:所有文件复制操作的底层都是字节的搬运,区别只在于缓冲区大小和是否用了更底层的系统调用。
| 方法 | 代码复杂度 | 性能 | 适用场景 |
|---|---|---|---|
Files.copy() |
低 | 最高(系统调用) | 通用场景,推荐首选 |
FileUtils.copy() |
低 | 高 | Android 项目,需要额外依赖 |
Kotlin File.copyTo() |
低 | 高 | Kotlin 项目 |
| 手动字节流 | 中 | 中 | 需要自定义处理逻辑 |
BufferedInputStream/OutputStream |
中 | 较高 | 需要缓冲控制 |
Socket 的本质就是把网络连接也当成一个流来读写。和文件操作一样,你拿到的是 InputStream和 OutputStream。
try {
Socket socket = new Socket("hencoder.com", 80);
// 往网络输出(发送数据)
OutputStream out = socket.getOutputStream();
// 从网络读入(接收数据)
InputStream in = socket.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
其实套一层 Buffered 更好:
try {
Socket socket = new Socket("hencoder.com", 80);
// 用 BufferedWriter 按字符写入
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
// 用 BufferedReader 按字符读取
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
// reader 和 writer 是根据【字符】来写入和读取的,而不是字节
} catch (IOException e) {
e.printStackTrace();
}
关键区别:Reader和 Writer是根据字符来写入和读取的,而不是字节。发送文本数据时,按字符处理远比按字节方便。
// 发送 HTTP GET 请求报文
writer.write("GET / HTTP/1.1\n"
+ "Host: www.example.com\n"
+ "\n"); // 空行表示请求头结束
writer.flush(); // 一定要 flush!否则数据还在缓冲区
// 读取服务器响应
System.out.println(reader.readLine());
ServerSocket serverSocket = new ServerSocket(80); // 监听 80 端口
Socket socket = serverSocket.accept(); // 阻塞等待客户端连接
// 别人连接你之后,你会得到一个 Socket 对象
// 然后可以调用这个 socket 对象来给对方发送消息
一样可以套一层 BufferedReader或 BufferedWriter:
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
private static void io3() {
try (
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
BufferedWriter bufferedWriter = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
) {
String html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>简单页面</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>这是一个简单的HTML页面</p>
</body>
</html>
""";
bufferedWriter.write("HTTP/1.1 200 OK\r\n");
bufferedWriter.write("Content-Type: text/html; charset=UTF-8\r\n");
bufferedWriter.write("Content-Length: " + html.getBytes().length + "\r\n");
bufferedWriter.write("\r\n");
bufferedWriter.write(html);
bufferedWriter.flush();
} catch (IOException _) {}
}
| 步骤 | 关键点 | 说明 |
|---|---|---|
| HTTP 响应格式 | 状态行 → 响应头 → 空行 → 响应体 |
HTTP 协议规定格式,不能错序 |
| Content-Length | html.getBytes().length |
必须用字节长度,因为网络传输按字节 |
| flush() | 关键步骤 | 没有 flush,浏览器会一直在等待数据 |
| try-with-resources | 4层嵌套资源 | 全部自动关闭,顺序:writer → reader → socket → serverSocket |
| 操作场景 | 推荐做法 | 避免做法 |
|---|---|---|
| 文本读取 | BufferedReader + try-with-resources |
逐字符读取,不使用缓冲 |
| 文本写入 | BufferedWriter + 指定编码 |
频繁调用 write()不使用缓冲 |
| 二进制文件 | BufferedInputStream/OutputStream |
使用字符流处理二进制数据 |
| 文件复制 | Files.copy()(Java 7+) |
手动逐字节复制 |
| 资源管理 | try-with-resources |
手动 try-finally 管理资源 |
| 字符编码 | 显式指定 StandardCharsets.UTF_8 |
依赖系统默认编码 |