#
文章目录
-
- [引言:Java I/O体系概述](#引言:Java I/O体系概述)
- 第一部分:文件操作流
-
- [1.1 FileInputStream和FileOutputStream](#1.1 FileInputStream和FileOutputStream)
-
- [1.1.1 基本概念](#1.1.1 基本概念)
- [1.1.2 构造方法](#1.1.2 构造方法)
- [1.1.3 核心方法](#1.1.3 核心方法)
- [1.1.4 使用示例](#1.1.4 使用示例)
- [1.1.5 性能考虑](#1.1.5 性能考虑)
- [1.2 FileInputStream的内部工作原理](#1.2 FileInputStream的内部工作原理)
- 第二部分:缓冲流
-
- [2.1 BufferedInputStream和BufferedOutputStream](#2.1 BufferedInputStream和BufferedOutputStream)
-
- [2.1.1 基本概念](#2.1.1 基本概念)
- [2.1.2 工作原理](#2.1.2 工作原理)
- [2.1.3 构造方法](#2.1.3 构造方法)
- [2.1.4 性能对比示例](#2.1.4 性能对比示例)
- [2.1.5 重要注意事项](#2.1.5 重要注意事项)
- [2.2 缓冲流的缓冲区大小选择](#2.2 缓冲流的缓冲区大小选择)
- 第三部分:内存操作流
-
- [3.1 ByteArrayInputStream和ByteArrayOutputStream](#3.1 ByteArrayInputStream和ByteArrayOutputStream)
-
- [3.1.1 基本概念](#3.1.1 基本概念)
- [3.1.2 特点](#3.1.2 特点)
- [3.1.3 ByteArrayInputStream详解](#3.1.3 ByteArrayInputStream详解)
- [3.1.4 ByteArrayOutputStream详解](#3.1.4 ByteArrayOutputStream详解)
- [3.3 其他内存操作流](#3.3 其他内存操作流)
- 第四部分:对象序列化流
-
- [4.1 ObjectInputStream和ObjectOutputStream](#4.1 ObjectInputStream和ObjectOutputStream)
-
- [4.1.1 基本概念](#4.1.1 基本概念)
- [4.1.2 序列化要求](#4.1.2 序列化要求)
- [4.1.3 构造方法](#4.1.3 构造方法)
- [4.1.4 核心方法](#4.1.4 核心方法)
- [4.1.5 使用示例](#4.1.5 使用示例)
- [4.1.6 自定义序列化](#4.1.6 自定义序列化)
- [4.1.7 序列化注意事项](#4.1.7 序列化注意事项)
- 第五部分:数据流
-
- [5.1 DataInputStream和DataOutputStream](#5.1 DataInputStream和DataOutputStream)
-
- [5.1.1 基本概念](#5.1.1 基本概念)
- [5.1.2 特点](#5.1.2 特点)
- [5.1.3 构造方法](#5.1.3 构造方法)
- [5.1.4 核心方法](#5.1.4 核心方法)
- [5.1.5 使用示例](#5.1.5 使用示例)
- [5.1.6 UTF-8字符串格式说明](#5.1.6 UTF-8字符串格式说明)
- 第六部分:管道流
-
- [6.1 PipedInputStream和PipedOutputStream](#6.1 PipedInputStream和PipedOutputStream)
-
- [6.1.1 基本概念](#6.1.1 基本概念)
- [6.1.2 特点](#6.1.2 特点)
- [6.1.3 构造方法](#6.1.3 构造方法)
- [6.1.4 连接方法](#6.1.4 连接方法)
- [6.1.5 使用示例](#6.1.5 使用示例)
- [6.1.6 注意事项](#6.1.6 注意事项)
- 第七部分:回退输入流
-
- [7.1 PushbackInputStream](#7.1 PushbackInputStream)
-
- [7.1.1 基本概念](#7.1.1 基本概念)
- [7.1.2 工作原理](#7.1.2 工作原理)
- [7.1.3 构造方法](#7.1.3 构造方法)
- [7.1.4 核心方法](#7.1.4 核心方法)
- [7.1.5 使用示例](#7.1.5 使用示例)
- [7.1.6 性能考虑](#7.1.6 性能考虑)
- 第八部分:打印流
-
- [8.1 PrintStream](#8.1 PrintStream)
-
- [8.1.1 基本概念](#8.1.1 基本概念)
- [8.1.2 特点](#8.1.2 特点)
- [8.1.3 构造方法](#8.1.3 构造方法)
- [8.1.4 核心方法](#8.1.4 核心方法)
- [8.1.5 使用示例](#8.1.5 使用示例)
- [8.1.6 重要注意事项](#8.1.6 重要注意事项)
- 第九部分:过滤流
-
- [9.1 FilterInputStream和FilterOutputStream](#9.1 FilterInputStream和FilterOutputStream)
-
- [9.1.1 基本概念](#9.1.1 基本概念)
- [9.1.2 自定义过滤流示例](#9.1.2 自定义过滤流示例)
- 第十部分:序列输入流
-
- [10.1 SequenceInputStream](#10.1 SequenceInputStream)
-
- [10.1.1 基本概念](#10.1.1 基本概念)
- [10.1.2 构造方法](#10.1.2 构造方法)
- [10.1.3 使用示例](#10.1.3 使用示例)
- [10.1.4 注意事项](#10.1.4 注意事项)
- 第十一部分:校验和流
-
- [11.1 CheckedInputStream和CheckedOutputStream](#11.1 CheckedInputStream和CheckedOutputStream)
-
- [11.1.1 基本概念](#11.1.1 基本概念)
- [11.1.2 构造方法](#11.1.2 构造方法)
- [11.1.3 Checksum实现类](#11.1.3 Checksum实现类)
- [11.1.4 使用示例](#11.1.4 使用示例)
- 第十二部分:压缩流
-
- [12.1 GZIPInputStream和GZIPOutputStream](#12.1 GZIPInputStream和GZIPOutputStream)
-
- [12.1.1 基本概念](#12.1.1 基本概念)
- [12.1.2 构造方法](#12.1.2 构造方法)
- [12.1.3 使用示例](#12.1.3 使用示例)
- [12.2 ZipInputStream和ZipOutputStream](#12.2 ZipInputStream和ZipOutputStream)
-
- [12.2.1 基本概念](#12.2.1 基本概念)
- [12.2.2 使用示例](#12.2.2 使用示例)
- 第十三部分:组合流与最佳实践
-
- [13.1 流的装饰器模式](#13.1 流的装饰器模式)
- [13.2 常用组合模式](#13.2 常用组合模式)
- [13.3 性能优化最佳实践](#13.3 性能优化最佳实践)
-
- [13.3.1 缓冲区大小选择](#13.3.1 缓冲区大小选择)
- [13.3.2 避免常见陷阱](#13.3.2 避免常见陷阱)
- [13.4 异常处理最佳实践](#13.4 异常处理最佳实践)
- 第十四部分:总结与选择指南
-
- [14.1 实现类对比表](#14.1 实现类对比表)
- [14.2 选择指南](#14.2 选择指南)
- [14.3 性能考虑要点](#14.3 性能考虑要点)
- [14.4 最终建议](#14.4 最终建议)

引言:Java I/O体系概述
Java的I/O(输入/输出)是通过流(Stream)实现的,流是一组有序的数据序列。根据数据的流向,流可以分为输入流(InputStream)和输出流(OutputStream)。字节流以字节(8位二进制)为单位处理数据,可以操作任何类型的文件,包括文本、图片、音频、视频等二进制文件。
InputStream和OutputStream是Java字节流的两个抽象基类,位于java.io包中。它们定义了字节流读写的基本方法,拥有众多的实现类,每个实现类都针对特定的数据源或处理需求进行了优化。本文将详细介绍这些实现类的特点、使用方法和最佳实践。
第一部分:文件操作流
1.1 FileInputStream和FileOutputStream
1.1.1 基本概念
FileInputStream和FileOutputStream是最基础的字节流实现类,用于从文件中读取字节数据或将字节数据写入文件。它们是Java程序与文件系统交互的最直接方式。
1.1.2 构造方法
FileInputStream构造方法:
java
// 通过文件路径创建
FileInputStream fis = new FileInputStream(String name);
// 通过File对象创建
FileInputStream fis = new FileInputStream(File file);
// 通过FileDescriptor创建
FileInputStream fis = new FileInputStream(FileDescriptor fdObj);
FileOutputStream构造方法:
java
// 通过文件路径创建(覆盖模式)
FileOutputStream fos = new FileOutputStream(String name);
// 通过文件路径创建(可指定追加模式)
FileOutputStream fos = new FileOutputStream(String name, boolean append);
// 通过File对象创建(覆盖模式)
FileOutputStream fos = new FileOutputStream(File file);
// 通过File对象创建(可指定追加模式)
FileOutputStream fos = new FileOutputStream(File file, boolean append);
1.1.3 核心方法
FileInputStream主要方法:
int read(): 读取一个字节,返回0-255的字节值,到达文件末尾返回-1int read(byte[] b): 读取最多b.length字节到数组中,返回实际读取的字节数int read(byte[] b, int off, int len): 读取最多len字节到数组的指定位置long skip(long n): 跳过并丢弃n个字节int available(): 返回可读取的字节数估计值void close(): 关闭流并释放相关资源
FileOutputStream主要方法:
void write(int b): 写入一个字节void write(byte[] b): 写入整个字节数组void write(byte[] b, int off, int len): 写入字节数组的一部分void flush(): 刷新缓冲区(对于FileOutputStream,write方法直接写入文件,flush通常不做任何操作)void close(): 关闭流并释放资源
1.1.4 使用示例
基本文件复制示例:
java
public class FileCopyExample {
public static void main(String[] args) {
// 使用try-with-resources自动关闭资源(JDK 7+)
try (FileInputStream fis = new FileInputStream("source.jpg");
FileOutputStream fos = new FileOutputStream("dest.jpg")) {
byte[] buffer = new byte[8192]; // 8KB缓冲区
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
System.out.println("文件复制完成");
} catch (IOException e) {
e.printStackTrace();
}
}
}
追加模式写入示例:
java
public class AppendToFileExample {
public static void main(String[] args) {
String logEntry = "[" + new Date() + "] 用户登录系统\n";
try (FileOutputStream fos = new FileOutputStream("app.log", true)) {
fos.write(logEntry.getBytes(StandardCharsets.UTF_8));
System.out.println("日志写入成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.1.5 性能考虑
- 单字节读写(
read()和write(int))效率极低,每次调用都会触发底层系统调用 - 建议使用缓冲区(字节数组)批量读写,缓冲区大小通常设为4KB-8KB
- 对于更高性能需求,可以配合缓冲流使用
1.2 FileInputStream的内部工作原理
FileInputStream实际通过JNI(Java Native Interface)调用操作系统底层的文件读取API。在Windows上是ReadFile函数,在Linux/Unix上是read系统调用。因此,频繁的单字节读写会频繁陷入内核态,造成性能瓶颈。
第二部分:缓冲流
2.1 BufferedInputStream和BufferedOutputStream
2.1.1 基本概念
BufferedInputStream和BufferedOutputStream是装饰器模式的典型应用,它们为现有的输入输出流添加缓冲功能,通过减少实际的I/O操作次数来显著提升性能。
2.1.2 工作原理
BufferedInputStream 内部维护一个字节数组作为缓冲区。当调用read()方法时,它会尽可能多地读取数据填充缓冲区,后续的读取操作直接从缓冲区返回数据,只有当缓冲区数据耗尽时才再次从底层输入流读取。
BufferedOutputStream 同样维护一个字节数组作为缓冲区。写入的数据首先存入缓冲区,当缓冲区满或调用flush()方法时,才将缓冲区数据一次性写入底层输出流。
2.1.3 构造方法
java
// 使用默认缓冲区大小(8192字节)
BufferedInputStream bis = new BufferedInputStream(InputStream in);
BufferedOutputStream bos = new BufferedOutputStream(OutputStream out);
// 指定缓冲区大小
BufferedInputStream bis = new BufferedInputStream(InputStream in, int size);
BufferedOutputStream bos = new BufferedOutputStream(OutputStream out, int size);
2.1.4 性能对比示例
java
public class BufferedStreamPerformance {
public static void main(String[] args) {
String fileName = "test.dat";
// 测试无缓冲写入
long start = System.nanoTime();
try (FileOutputStream fos = new FileOutputStream(fileName)) {
for (int i = 0; i < 1000000; i++) {
fos.write(i); // 单字节写入,每次都会触发系统调用
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("无缓冲写入时间: " + (end - start) / 1_000_000 + " ms");
// 测试缓冲写入
start = System.nanoTime();
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(fileName))) {
for (int i = 0; i < 1000000; i++) {
bos.write(i); // 写入到缓冲区,满8K时才真正写入
}
} catch (IOException e) {
e.printStackTrace();
}
end = System.nanoTime();
System.out.println("缓冲写入时间: " + (end - start) / 1_000_000 + " ms");
// 清理
new File(fileName).delete();
}
}
运行结果通常显示缓冲流比无缓冲流快几十甚至上百倍。
2.1.5 重要注意事项
关于flush()方法 :
BufferedOutputStream的flush()方法强制将缓冲区中的数据写入底层输出流。在关闭流之前,close()方法会自动调用flush()。但在以下情况需要手动调用:
- 需要确保数据立即写入(如日志记录)
- 写入完成后还要继续使用底层流
- 长时间写入且希望避免数据滞留缓冲区
关于mark/reset功能 :
BufferedInputStream支持mark()和reset()方法,允许读取位置回退。默认情况下mark有效,但如果在标记后读取的字节数超过缓冲区大小,标记可能会失效。可以通过构造函数指定更大的缓冲区来避免这个问题。
java
public class MarkResetExample {
public static void main(String[] args) throws IOException {
byte[] data = "Hello World".getBytes();
try (BufferedInputStream bis = new BufferedInputStream(
new ByteArrayInputStream(data))) {
// 标记当前位置(起始位置)
bis.mark(10); // 参数表示标记有效期内最多可以读取的字节数
// 读取前5个字节
byte[] buffer = new byte[5];
bis.read(buffer);
System.out.println("第一次读取: " + new String(buffer));
// 重置到标记位置
bis.reset();
// 重新读取
bis.read(buffer);
System.out.println("重置后读取: " + new String(buffer));
}
}
}
2.2 缓冲流的缓冲区大小选择
默认的8192字节(8KB)是经过实践检验的较优值,与文件系统的块大小和CPU缓存行大小相匹配。对于大文件顺序读写,可以适当增大缓冲区(如64KB或256KB)来进一步提升性能。但过大的缓冲区可能导致内存浪费和缓存利用率下降。
第三部分:内存操作流
3.1 ByteArrayInputStream和ByteArrayOutputStream
3.1.1 基本概念
ByteArrayInputStream和ByteArrayOutputStream是操作内存中字节数组的流实现类。它们不涉及磁盘或网络I/O,所有操作都在内存中进行,因此速度极快,适合临时数据处理。
3.1.2 特点
- 无需关闭 :这两个流的
close()方法是空实现,调用没有实际效果 - 内存操作:数据存储在JVM堆内存中
- 线程不安全:多个线程同时访问需要外部同步
- 自动扩容 :
ByteArrayOutputStream在写入数据时会自动扩容
3.1.3 ByteArrayInputStream详解
构造方法:
java
// 使用整个字节数组作为数据源
ByteArrayInputStream bais = new ByteArrayInputStream(byte[] buf);
// 使用字节数组的一部分作为数据源
ByteArrayInputStream bais = new ByteArrayInputStream(byte[] buf, int offset, int length);
使用示例:
java
public class ByteArrayInputStreamExample {
public static void main(String[] args) {
byte[] data = "Java I/O 内存操作流示例".getBytes(StandardCharsets.UTF_8);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
byte[] buffer = new byte[1024];
int bytesRead = bais.read(buffer);
String result = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8);
System.out.println("读取的内容: " + result);
// mark/reset支持(基于数组,完全可用)
bais.reset(); // 重置到流的开头
System.out.println("重置后可用字节数: " + bais.available());
// 跳过前5个字节
bais.skip(5);
byte[] remaining = bais.readAllBytes();
System.out.println("跳过5字节后: " + new String(remaining, StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.1.4 ByteArrayOutputStream详解
构造方法:
java
// 默认缓冲区大小32字节
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 指定初始缓冲区大小
ByteArrayOutputStream baos = new ByteArrayOutputStream(int size);
核心方法:
java
// 写入数据
void write(int b)
void write(byte[] b, int off, int len)
// 获取数据
byte[] toByteArray() // 返回当前写入数据的副本
String toString() // 使用平台默认编码转换为字符串
String toString(String charsetName) // 使用指定编码转换为字符串
// 写入到另一个输出流
void writeTo(OutputStream out)
// 重置(清空缓冲区)
void reset()
// 当前缓冲区大小
int size()
综合使用示例:
java
public class ByteArrayOutputStreamExample {
public static void main(String[] args) {
// 场景1:合并多个数据源
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
// 写入不同类型的数据
baos.write("用户名: ".getBytes(StandardCharsets.UTF_8));
baos.write("张三\n".getBytes(StandardCharsets.UTF_8));
baos.write("年龄: ".getBytes(StandardCharsets.UTF_8));
baos.write(String.valueOf(25).getBytes(StandardCharsets.UTF_8));
// 获取合并后的数据
byte[] combinedData = baos.toByteArray();
System.out.println("合并结果:\n" + new String(combinedData, StandardCharsets.UTF_8));
// 场景2:写入到文件
try (FileOutputStream fos = new FileOutputStream("user.txt")) {
baos.writeTo(fos);
}
// 场景3:重用同一个流
baos.reset();
baos.write("新的数据".getBytes(StandardCharsets.UTF_8));
System.out.println("重置后: " + baos.toString(StandardCharsets.UTF_8.name()));
} catch (IOException e) {
e.printStackTrace();
}
// 场景4:作为临时缓冲区处理网络数据
byte[] networkData = simulateNetworkReceive();
ByteArrayInputStream bais = new ByteArrayInputStream(networkData);
processData(bais);
}
private static void processData(InputStream input) {
// 处理输入流数据
// 这里可以是解析协议、解压缩等操作
}
private static byte[] simulateNetworkReceive() {
return "模拟的网络数据包".getBytes(StandardCharsets.UTF_8);
}
}
3.3 其他内存操作流
除了字节数组流,Java还提供了字符数组流和字符串流:
- CharArrayReader / CharArrayWriter:操作字符数组的字符流
- StringReader / StringWriter:操作字符串的字符流
这些流在处理文本数据时更加方便,但本质上是为字符流设计的。
第四部分:对象序列化流
4.1 ObjectInputStream和ObjectOutputStream
4.1.1 基本概念
ObjectInputStream和ObjectOutputStream是用于对象序列化的高级流,可以将Java对象转换为字节序列(序列化),或者将字节序列恢复为Java对象(反序列化)。
4.1.2 序列化要求
- 对象类必须实现
java.io.Serializable接口(标记接口,无方法需要实现) - 所有非静态、非瞬态(transient)字段都会被序列化
- 如果父类没有实现Serializable,父类必须有默认构造函数
- 序列化版本ID:
private static final long serialVersionUID,用于版本控制
4.1.3 构造方法
java
// 需要包装另一个输出流
ObjectOutputStream oos = new ObjectOutputStream(OutputStream out);
// 需要包装另一个输入流
ObjectInputStream ois = new ObjectInputStream(InputStream in);
4.1.4 核心方法
ObjectOutputStream:
java
// 写入对象
void writeObject(Object obj)
// 写入基本类型(实现了DataOutput接口)
void writeInt(int v)
void writeBoolean(boolean v)
void writeUTF(String str) // 写入字符串(Modified UTF-8格式)
// 刷新(确保所有数据写入底层流)
void flush()
// 重置(清除已缓存的对象引用)
void reset()
ObjectInputStream:
java
// 读取对象
Object readObject()
// 读取基本类型
int readInt()
boolean readBoolean()
String readUTF()
4.1.5 使用示例
基本序列化示例:
java
import java.io.*;
import java.time.LocalDateTime;
// 可序列化的用户类
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password; // 敏感信息
private transient String token; // 瞬态字段不序列化
private LocalDateTime loginTime;
public User(String username, String password) {
this.username = username;
this.password = password;
this.loginTime = LocalDateTime.now();
}
@Override
public String toString() {
return "User{username='" + username + "', password='" + password +
"', token='" + token + "', loginTime=" + loginTime + '}';
}
// getters and setters
public void setToken(String token) { this.token = token; }
}
public class SerializationExample {
public static void main(String[] args) {
String fileName = "user.ser";
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(fileName))) {
User user = new User("张三", "password123");
user.setToken("auth-token-xyz");
oos.writeObject(user);
System.out.println("对象序列化完成: " + user);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(fileName))) {
User deserializedUser = (User) ois.readObject();
System.out.println("对象反序列化完成: " + deserializedUser);
// token字段为null,因为它被标记为transient
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
4.1.6 自定义序列化
类可以定义以下方法来自定义序列化行为:
java
private void writeObject(ObjectOutputStream oos) throws IOException {
// 自定义写入逻辑
oos.defaultWriteObject(); // 写入默认字段
// 写入额外的数据
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// 自定义读取逻辑
ois.defaultReadObject(); // 读取默认字段
// 读取额外的数据并初始化
}
private void readObjectNoData() throws ObjectStreamException {
// 当序列化流缺少该类的数据时调用
}
4.1.7 序列化注意事项
- 性能考虑:序列化会遍历整个对象图,对大对象图进行序列化可能较慢
- 安全考虑:反序列化可能执行恶意代码,始终验证输入来源
- 版本兼容:修改类结构可能导致反序列化失败,使用serialVersionUID控制兼容性
- 循环引用:ObjectOutputStream可以处理对象间的循环引用
- 关闭流:必须确保正确关闭,否则可能数据不完整
第五部分:数据流
5.1 DataInputStream和DataOutputStream
5.1.1 基本概念
DataInputStream和DataOutputStream提供了读写Java基本数据类型(int、long、float、double等)和字符串的方法,使得处理二进制数据更加方便。
5.1.2 特点
- 平台无关:使用与机器无关的方式读写数据,保证数据在不同平台间可移植
- 类型安全:可以直接读写基本类型,无需手动转换字节
- 网络传输:常用于网络协议实现和二进制文件格式处理
5.1.3 构造方法
java
// 包装另一个输入流
DataInputStream dis = new DataInputStream(InputStream in);
// 包装另一个输出流
DataOutputStream dos = new DataOutputStream(OutputStream out);
5.1.4 核心方法
DataOutputStream(实现了DataOutput接口):
java
// 写入基本类型
void writeBoolean(boolean v)
void writeByte(int v)
void writeShort(int v)
void writeChar(int v)
void writeInt(int v)
void writeLong(long v)
void writeFloat(float v)
void writeDouble(double v)
// 写入字符串
void writeBytes(String s) // 只写入每个字符的低字节
void writeChars(String s) // 写入完整的字符(每个字符2字节)
void writeUTF(String str) // 写入UTF-8格式字符串(先写入长度,再写入字节)
// 获取写入的字节数
int size()
DataInputStream(实现了DataInput接口):
java
// 读取基本类型
boolean readBoolean()
byte readByte()
short readShort()
char readChar()
int readInt()
long readLong()
float readFloat()
double readDouble()
// 读取字符串
String readUTF()
5.1.5 使用示例
java
public class DataStreamExample {
public static void main(String[] args) {
String fileName = "data.bin";
// 写入不同类型的数据
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream(fileName))) {
dos.writeInt(100);
dos.writeDouble(3.14159);
dos.writeBoolean(true);
dos.writeUTF("Hello World");
dos.writeChar('A');
System.out.println("总共写入 " + dos.size() + " 字节");
} catch (IOException e) {
e.printStackTrace();
}
// 按相同顺序读取
try (DataInputStream dis = new DataInputStream(
new FileInputStream(fileName))) {
int intValue = dis.readInt();
double doubleValue = dis.readDouble();
boolean boolValue = dis.readBoolean();
String strValue = dis.readUTF();
char charValue = dis.readChar();
System.out.println("读取结果:");
System.out.println("int: " + intValue);
System.out.println("double: " + doubleValue);
System.out.println("boolean: " + boolValue);
System.out.println("String: " + strValue);
System.out.println("char: " + charValue);
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.1.6 UTF-8字符串格式说明
writeUTF()方法写入的字符串格式有特定结构:
- 前两个字节:字符串长度(以字节为单位)
- 后续字节:字符串的Modified UTF-8编码
Modified UTF-8与标准UTF-8的区别:
- 空字符('\u0000')编码为两个字节(0xC0, 0x80)而不是一个字节
- 只支持1字节、2字节和3字节的字符(基本多语言平面)
- 增补字符(surrogate pairs)编码为两个3字节序列
因此,用readUTF()读取时必须使用writeUTF()写入的格式。
第六部分:管道流
6.1 PipedInputStream和PipedOutputStream
6.1.1 基本概念
PipedInputStream和PipedOutputStream用于在同一JVM中的不同线程之间建立通信管道。一个线程通过PipedOutputStream写入数据,另一个线程通过PipedInputStream读取数据。
6.1.2 特点
- 线程间通信:专为多线程环境设计
- 阻塞操作:读空或写满时会阻塞线程
- 有限缓冲区:默认缓冲区大小为1024字节
- 一对一连接:一个输入流只能连接一个输出流
6.1.3 构造方法
java
// 创建未连接的管道
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream();
// 创建时连接
PipedInputStream pis = new PipedInputStream(PipedOutputStream src);
PipedOutputStream pos = new PipedOutputStream(PipedInputStream snk);
// 指定缓冲区大小
PipedInputStream pis = new PipedInputStream(int pipeSize);
PipedInputStream pis = new PipedOutputStream(PipedOutputStream src, int pipeSize);
6.1.4 连接方法
java
// 连接输入流和输出流
pis.connect(PipedOutputStream src);
pos.connect(PipedInputStream snk);
6.1.5 使用示例
基本管道通信:
java
public class PipeExample {
public static void main(String[] args) {
try {
// 创建管道流
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
// 写入线程
Thread writerThread = new Thread(() -> {
try {
String[] messages = {"消息1", "消息2", "消息3", "结束"};
for (String msg : messages) {
pos.write(msg.getBytes(StandardCharsets.UTF_8));
pos.write('\n'); // 添加换行符作为分隔
System.out.println("写入: " + msg);
Thread.sleep(1000); // 模拟延迟
}
pos.close(); // 关闭输出流,告诉读取线程数据结束
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
});
// 读取线程
Thread readerThread = new Thread(() -> {
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(pis, StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
System.out.println("读取: " + line);
}
pis.close();
} catch (IOException e) {
e.printStackTrace();
}
});
writerThread.start();
readerThread.start();
// 等待线程完成
writerThread.join();
readerThread.join();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
处理管道断开:
java
public class PipeDisconnectHandling {
public static void main(String[] args) {
try {
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
// 写入少量数据后关闭
Thread writer = new Thread(() -> {
try {
pos.write("部分数据".getBytes());
pos.close(); // 提前关闭
} catch (IOException e) {
e.printStackTrace();
}
});
// 延迟读取
Thread reader = new Thread(() -> {
try {
Thread.sleep(2000); // 等待写入线程结束
byte[] buffer = new byte[1024];
int bytesRead = pis.read(buffer);
if (bytesRead > 0) {
System.out.println("读取到: " + new String(buffer, 0, bytesRead));
}
// 尝试再次读取
bytesRead = pis.read(buffer);
System.out.println("第二次读取结果: " + bytesRead); // 应为-1
} catch (IOException | InterruptedException e) {
System.err.println("管道错误: " + e.getMessage());
} finally {
try {
pis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
writer.start();
reader.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
6.1.6 注意事项
- 必须在多线程环境使用:单线程中使用可能导致死锁
- 避免死锁:读写线程互相等待可能导致死锁
- 缓冲区满:默认缓冲区较小,大数据传输时需调整
- 流关闭:正确关闭流,避免资源泄漏
第七部分:回退输入流
7.1 PushbackInputStream
7.1.1 基本概念
PushbackInputStream是一个特殊的输入流装饰器,允许将读取的字节推回到流中,以便重新读取。这在解析器实现中特别有用,比如需要预读数据来决定下一步操作。
7.1.2 工作原理
PushbackInputStream内部维护一个回退缓冲区(字节数组)。当调用unread()方法时,数据被写入这个缓冲区。读取操作优先从回退缓冲区读取数据,只有当缓冲区为空时才从底层输入流读取。
7.1.3 构造方法
java
// 使用默认回退缓冲区大小(1字节)
PushbackInputStream pbis = new PushbackInputStream(InputStream in);
// 指定回退缓冲区大小
PushbackInputStream pbis = new PushbackInputStream(InputStream in, int size);
7.1.4 核心方法
java
// 推回一个字节(可以多次调用)
void unread(int b) throws IOException
// 推回整个字节数组
void unread(byte[] b) throws IOException
// 推回字节数组的一部分
void unread(byte[] b, int off, int len) throws IOException
// 可用字节数(回退缓冲区中的字节数 + 底层流可用字节数)
int available() throws IOException
7.1.5 使用示例
简单的解析器示例:
java
public class PushbackParserExample {
public static void main(String[] args) {
String data = "123+456";
try (PushbackInputStream pbis = new PushbackInputStream(
new ByteArrayInputStream(data.getBytes()), 2)) {
// 读取第一个数字
int num1 = readNumber(pbis);
System.out.println("第一个数字: " + num1);
// 预读运算符
int next = pbis.read();
if (next == '+') {
System.out.println("遇到加号运算符");
// 读取第二个数字
int num2 = readNumber(pbis);
System.out.println("第二个数字: " + num2);
System.out.println("结果: " + (num1 + num2));
} else {
// 如果不是运算符,推回流中
pbis.unread(next);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 读取连续的数字字符,返回整数
private static int readNumber(PushbackInputStream pbis) throws IOException {
StringBuilder sb = new StringBuilder();
int b;
while ((b = pbis.read()) != -1) {
if (b >= '0' && b <= '9') {
sb.append((char) b);
} else {
// 遇到非数字字符,推回流中
pbis.unread(b);
break;
}
}
return sb.length() > 0 ? Integer.parseInt(sb.toString()) : 0;
}
}
复杂的分词器示例:
java
public class SimpleTokenizer {
private PushbackInputStream input;
private static final int BUFFER_SIZE = 1024;
public SimpleTokenizer(InputStream input) {
this.input = new PushbackInputStream(input, BUFFER_SIZE);
}
public String nextToken() throws IOException {
skipWhitespace();
int b = input.read();
if (b == -1) {
return null; // EOF
}
// 处理不同类型的token
if (isDigit(b)) {
return readNumber(b);
} else if (isLetter(b)) {
return readWord(b);
} else if (isOperator(b)) {
return readOperator(b);
} else {
// 单个字符token
return String.valueOf((char) b);
}
}
private String readNumber(int firstDigit) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append((char) firstDigit);
while (true) {
int b = input.read();
if (b == -1 || !isDigit(b)) {
if (b != -1) {
input.unread(b); // 推回非数字字符
}
break;
}
sb.append((char) b);
}
return sb.toString();
}
private String readWord(int firstLetter) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append((char) firstLetter);
while (true) {
int b = input.read();
if (b == -1 || !isLetterOrDigit(b)) {
if (b != -1) {
input.unread(b);
}
break;
}
sb.append((char) b);
}
return sb.toString();
}
private String readOperator(int firstChar) throws IOException {
// 处理多字符运算符,如 ==, !=, <=, >= 等
StringBuilder sb = new StringBuilder();
sb.append((char) firstChar);
int next = input.read();
if (next != -1) {
String possibleOp = sb.toString() + (char) next;
if (isMultiCharOperator(possibleOp)) {
sb.append((char) next);
} else {
input.unread(next);
}
}
return sb.toString();
}
private void skipWhitespace() throws IOException {
int b;
while ((b = input.read()) != -1 && Character.isWhitespace(b)) {
// 跳过空白字符
}
if (b != -1) {
input.unread(b);
}
}
private boolean isDigit(int b) {
return b >= '0' && b <= '9';
}
private boolean isLetter(int b) {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z');
}
private boolean isLetterOrDigit(int b) {
return isLetter(b) || isDigit(b) || b == '_';
}
private boolean isOperator(int b) {
return b == '+' || b == '-' || b == '*' || b == '/' ||
b == '=' || b == '<' || b == '>' || b == '!';
}
private boolean isMultiCharOperator(String op) {
return op.equals("==") || op.equals("!=") ||
op.equals("<=") || op.equals(">=");
}
public static void main(String[] args) {
String code = "if (x <= 100) { result = 42; }";
try (InputStream is = new ByteArrayInputStream(code.getBytes())) {
SimpleTokenizer tokenizer = new SimpleTokenizer(is);
String token;
while ((token = tokenizer.nextToken()) != null) {
System.out.println("Token: " + token);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
7.1.6 性能考虑
- 回退缓冲区使用从后往前填充的方式,这样读取时可以从前往后访问
- 缓冲区满时会抛出
IOException - 频繁的unread操作会增加内存复制开销
第八部分:打印流
8.1 PrintStream
8.1.1 基本概念
PrintStream是一个功能丰富的输出流装饰器,提供了打印各种数据类型的便利方法。最著名的PrintStream实例就是System.out和System.err。
8.1.2 特点
- 便利的输出方法 :提供重载的
print()和println()方法 - 格式化输出 :支持
printf()和format()方法 - 不抛出IOException :异常被内部捕获,可通过
checkError()检查状态 - 自动刷新:可配置为遇到换行符时自动刷新
- 字符编码:支持指定字符编码
8.1.3 构造方法
java
// 包装OutputStream
PrintStream ps = new PrintStream(OutputStream out);
PrintStream ps = new PrintStream(OutputStream out, boolean autoFlush);
PrintStream ps = new PrintStream(OutputStream out, boolean autoFlush, String encoding);
// 直接创建文件流
PrintStream ps = new PrintStream(String fileName);
PrintStream ps = new PrintStream(String fileName, String encoding);
PrintStream ps = new PrintStream(File file);
8.1.4 核心方法
打印方法:
java
// 打印不换行
void print(boolean b)
void print(char c)
void print(int i)
void print(long l)
void print(float f)
void print(double d)
void print(char[] s)
void print(String s)
void print(Object obj)
// 打印并换行
void println()
void println(boolean x)
// ... 各种类型的println重载
// 格式化输出
PrintStream printf(String format, Object... args)
PrintStream format(String format, Object... args)
错误检查:
java
// 检查是否发生错误
boolean checkError()
// 清除错误状态(protected方法)
protected void clearError()
8.1.5 使用示例
基本输出:
java
public class PrintStreamBasicExample {
public static void main(String[] args) {
try (PrintStream ps = new PrintStream("output.txt", "UTF-8")) {
// 各种打印方法
ps.print(true);
ps.print(' ');
ps.print(123);
ps.print(' ');
ps.print(3.14159);
ps.println(); // 换行
ps.println("这是一行文本");
ps.println(new Object());
// 格式化输出
ps.printf("姓名:%s,年龄:%d,身高:%.2f米%n",
"张三", 25, 1.75);
// 格式化并返回PrintStream,支持链式调用
ps.format("圆周率:%.10f%n", Math.PI)
.format("当前时间:%tF %<tT%n", new Date());
// 检查错误状态
if (ps.checkError()) {
System.err.println("写入过程中发生错误");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
重定向System.out:
java
public class RedirectSystemOutExample {
public static void main(String[] args) {
// 保存原始System.out
PrintStream originalOut = System.out;
try {
// 创建文件输出流
PrintStream fileOut = new PrintStream(
new FileOutputStream("console.log", true),
true, // 自动刷新
"UTF-8"
);
// 重定向System.out
System.setOut(fileOut);
// 这些输出会写入文件
System.out.println("这是写入文件的第一行");
System.out.printf("当前时间:%tF %<tT%n", new Date());
// 创建日志记录
logMessage("系统启动");
logMessage("用户登录");
System.out.println("写入完成");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 恢复原始System.out
System.setOut(originalOut);
System.out.println("已恢复控制台输出");
}
}
private static void logMessage(String message) {
// 此时System.out已被重定向
System.out.printf("[%tF %<tT] %s%n", new Date(), message);
}
}
自定义日志类:
java
public class Logger {
private PrintStream out;
private PrintStream err;
private boolean debugEnabled;
public Logger(String logFile) throws IOException {
// 普通日志输出到文件
this.out = new PrintStream(
new FileOutputStream(logFile, true),
true,
"UTF-8"
);
// 错误日志同时输出到文件和标准错误
this.err = new PrintStream(
new TeeOutputStream(System.err,
new FileOutputStream(logFile + ".err", true)),
true,
"UTF-8"
);
}
public void info(String format, Object... args) {
out.printf("[INFO] " + format + "%n", args);
}
public void error(String format, Object... args) {
err.printf("[ERROR] " + format + "%n", args);
}
public void debug(String format, Object... args) {
if (debugEnabled) {
out.printf("[DEBUG] " + format + "%n", args);
}
}
public void setDebugEnabled(boolean enabled) {
this.debugEnabled = enabled;
}
public void close() {
out.close();
err.close();
}
// 辅助类:将输出同时写入两个流
private static class TeeOutputStream extends OutputStream {
private OutputStream out1;
private OutputStream out2;
public TeeOutputStream(OutputStream out1, OutputStream out2) {
this.out1 = out1;
this.out2 = out2;
}
@Override
public void write(int b) throws IOException {
out1.write(b);
out2.write(b);
}
@Override
public void flush() throws IOException {
out1.flush();
out2.flush();
}
@Override
public void close() throws IOException {
try {
out1.close();
} finally {
out2.close();
}
}
}
}
8.1.6 重要注意事项
- 异常处理 :PrintStream不会抛出IOException,必须通过
checkError()检查错误 - 性能考虑:频繁的print()调用仍然会加锁,多线程环境下可以考虑缓冲
- 自动刷新:启用autoFlush时,写入字节数组、调用println()或遇到'\n'时会自动刷新
- 字符编码:处理非ASCII字符时必须指定正确的编码
第九部分:过滤流
9.1 FilterInputStream和FilterOutputStream
9.1.1 基本概念
FilterInputStream和FilterOutputStream是装饰器模式中的装饰者基类。它们本身不添加额外功能,只是简单地将所有方法调用转发给底层流。所有装饰器类(如BufferedInputStream、DataInputStream)都继承自这些过滤流。
9.1.2 自定义过滤流示例
java
// 自定义过滤流:将读取的字节转换为大写
public class UpperCaseInputStream extends FilterInputStream {
protected UpperCaseInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int b = super.read();
return (b == -1) ? -1 : Character.toUpperCase(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int bytesRead = super.read(b, off, len);
if (bytesRead > 0) {
for (int i = off; i < off + bytesRead; i++) {
b[i] = (byte) Character.toUpperCase(b[i] & 0xFF);
}
}
return bytesRead;
}
}
// 自定义过滤流:计算写入数据的CRC32校验和
public class CRCOutputStream extends FilterOutputStream {
private CRC32 crc = new CRC32();
private long bytesWritten = 0;
public CRCOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(int b) throws IOException {
out.write(b);
crc.update(b);
bytesWritten++;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
crc.update(b, off, len);
bytesWritten += len;
}
public long getCRCValue() {
return crc.getValue();
}
public long getBytesWritten() {
return bytesWritten;
}
}
// 使用示例
public class CustomFilterExample {
public static void main(String[] args) {
String data = "hello world";
// 使用UpperCaseInputStream
try (InputStream is = new UpperCaseInputStream(
new ByteArrayInputStream(data.getBytes()))) {
byte[] buffer = new byte[1024];
int bytesRead = is.read(buffer);
System.out.println("大写转换结果: " +
new String(buffer, 0, bytesRead));
} catch (IOException e) {
e.printStackTrace();
}
// 使用CRCOutputStream
try (CRCOutputStream cos = new CRCOutputStream(
new FileOutputStream("test.dat"))) {
cos.write(data.getBytes());
cos.write("\n第二行".getBytes());
System.out.println("写入字节数: " + cos.getBytesWritten());
System.out.println("CRC32校验和: " + Long.toHexString(cos.getCRCValue()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
第十部分:序列输入流
10.1 SequenceInputStream
10.1.1 基本概念
SequenceInputStream可以将多个输入流连接成一个输入流,按顺序读取每个流,直到所有流都读完。这在处理分段文件、合并多个数据源时非常有用。
10.1.2 构造方法
java
// 使用枚举
SequenceInputStream sis = new SequenceInputStream(Enumeration<? extends InputStream> e);
// 使用两个流
SequenceInputStream sis = new SequenceInputStream(InputStream s1, InputStream s2);
10.1.3 使用示例
java
public class SequenceInputStreamExample {
public static void main(String[] args) {
// 准备多个数据源
String[] data = {"第一部分数据\n", "第二部分数据\n", "第三部分数据\n"};
List<InputStream> streams = new ArrayList<>();
for (String d : data) {
streams.add(new ByteArrayInputStream(d.getBytes()));
}
// 创建枚举
Enumeration<InputStream> enumeration = Collections.enumeration(streams);
// 合并流
try (SequenceInputStream sis = new SequenceInputStream(enumeration);
FileOutputStream fos = new FileOutputStream("combined.txt")) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = sis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
System.out.println("所有数据已合并到 combined.txt");
} catch (IOException e) {
e.printStackTrace();
}
// 读取合并后的文件验证
try (FileInputStream fis = new FileInputStream("combined.txt")) {
byte[] content = fis.readAllBytes();
System.out.println("合并文件内容:\n" + new String(content));
} catch (IOException e) {
e.printStackTrace();
}
}
}
10.1.4 注意事项
- 关闭
SequenceInputStream会自动关闭所有包含的输入流 - 适合处理流式数据,不适合随机访问
- 可以用于分割文件的重新组装
第十一部分:校验和流
11.1 CheckedInputStream和CheckedOutputStream
11.1.1 基本概念
CheckedInputStream和CheckedOutputStream是Java标准库中用于计算数据校验和的过滤流。它们在读写数据的同时更新校验和,常用于数据完整性验证。
11.1.2 构造方法
java
// 需要指定Checksum实现
CheckedInputStream cis = new CheckedInputStream(InputStream in, Checksum cksum);
CheckedOutputStream cos = new CheckedOutputStream(OutputStream out, Checksum cksum);
// 获取校验和
Checksum getChecksum()
11.1.3 Checksum实现类
- CRC32:32位循环冗余校验
- Adler32:比CRC32更快但可靠性略低的校验算法
- 可自定义实现
Checksum接口
11.1.4 使用示例
java
import java.util.zip.CRC32;
import java.util.zip.Adler32;
import java.util.zip.CheckedInputStream;
import java.util.zip.CheckedOutputStream;
public class ChecksumExample {
// 计算文件的CRC32校验和
public static long calculateCRC32(String filePath) throws IOException {
try (CheckedInputStream cis = new CheckedInputStream(
new FileInputStream(filePath), new CRC32())) {
byte[] buffer = new byte[8192];
while (cis.read(buffer) != -1) {
// 持续读取,校验和在内部自动更新
}
return cis.getChecksum().getValue();
}
}
// 复制文件并同时计算校验和
public static long copyWithChecksum(String source, String dest) throws IOException {
try (CheckedInputStream cis = new CheckedInputStream(
new FileInputStream(source), new CRC32());
FileOutputStream fos = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = cis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
return cis.getChecksum().getValue();
}
}
// 写入数据并计算校验和
public static void writeWithChecksum(String filePath, byte[] data) throws IOException {
try (CheckedOutputStream cos = new CheckedOutputStream(
new FileOutputStream(filePath), new Adler32())) {
cos.write(data);
cos.flush();
long checksum = cos.getChecksum().getValue();
System.out.printf("写入完成,Adler32校验和: 0x%x%n", checksum);
// 将校验和单独保存
try (FileOutputStream ckFile = new FileOutputStream(filePath + ".cksum")) {
DataOutputStream dos = new DataOutputStream(ckFile);
dos.writeLong(checksum);
}
}
}
public static void main(String[] args) {
try {
String testFile = "testdata.bin";
// 生成测试数据
byte[] data = new byte[1024 * 1024]; // 1MB
new Random().nextBytes(data);
// 写入数据并计算校验和
writeWithChecksum(testFile, data);
// 读取文件并验证校验和
long calculatedCRC = calculateCRC32(testFile);
System.out.printf("读取文件的CRC32校验和: 0x%x%n", calculatedCRC);
// 复制文件并验证校验和是否一致
String copyFile = "copy_" + testFile;
long copyCRC = copyWithChecksum(testFile, copyFile);
System.out.printf("复制文件的CRC32校验和: 0x%x%n", copyCRC);
if (calculatedCRC == copyCRC) {
System.out.println("校验和一致,文件复制正确");
} else {
System.out.println("校验和不一致,文件可能损坏");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
第十二部分:压缩流
12.1 GZIPInputStream和GZIPOutputStream
12.1.1 基本概念
Java的java.util.zip包提供了多种压缩和解压缩流,其中最常用的是GZIPInputStream和GZIPOutputStream,它们实现了GZIP文件格式的压缩和解压缩。
12.1.2 构造方法
java
// 压缩输出流
GZIPOutputStream gzos = new GZIPOutputStream(OutputStream out);
GZIPOutputStream gzos = new GZIPOutputStream(OutputStream out, int size); // 指定缓冲区大小
// 解压缩输入流
GZIPInputStream gzis = new GZIPInputStream(InputStream in);
GZIPInputStream gzis = new GZIPInputStream(InputStream in, int size);
12.1.3 使用示例
java
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class GZIPExample {
// 压缩文件
public static void compressFile(String sourceFile, String gzipFile) throws IOException {
try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(gzipFile);
GZIPOutputStream gzos = new GZIPOutputStream(fos)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
gzos.write(buffer, 0, bytesRead);
}
// GZIPOutputStream需要finish()来写入压缩数据尾部
gzos.finish();
System.out.println("文件已压缩: " + gzipFile);
}
}
// 解压缩文件
public static void decompressFile(String gzipFile, String outputFile) throws IOException {
try (GZIPInputStream gzis = new GZIPInputStream(new FileInputStream(gzipFile));
FileOutputStream fos = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = gzis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
System.out.println("文件已解压缩: " + outputFile);
}
}
// 压缩数据流
public static byte[] compressData(byte[] data) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) {
gzos.write(data);
gzos.finish();
}
return baos.toByteArray();
}
// 解压缩数据流
public static byte[] decompressData(byte[] compressedData) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(compressedData);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (GZIPInputStream gzis = new GZIPInputStream(bais)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = gzis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
}
return baos.toByteArray();
}
public static void main(String[] args) {
try {
// 创建测试文件
String testFile = "test.txt";
try (FileWriter fw = new FileWriter(testFile)) {
for (int i = 0; i < 10000; i++) {
fw.write("这是一行测试文本,用于演示GZIP压缩功能。\n");
}
}
// 压缩文件
String gzipFile = "test.txt.gz";
compressFile(testFile, gzipFile);
// 检查压缩效果
File original = new File(testFile);
File compressed = new File(gzipFile);
System.out.printf("原始大小: %d bytes%n", original.length());
System.out.printf("压缩后大小: %d bytes%n", compressed.length());
System.out.printf("压缩率: %.2f%%%n",
(1 - (double)compressed.length() / original.length()) * 100);
// 解压缩文件
String decompressedFile = "decompressed.txt";
decompressFile(gzipFile, decompressedFile);
// 验证解压缩后内容相同
String originalContent = new String(Files.readAllBytes(original.toPath()));
String decompressedContent = new String(Files.readAllBytes(
new File(decompressedFile).toPath()));
System.out.println("内容相同: " + originalContent.equals(decompressedContent));
} catch (IOException e) {
e.printStackTrace();
}
}
}
12.2 ZipInputStream和ZipOutputStream
12.2.1 基本概念
ZipInputStream和ZipOutputStream用于处理ZIP格式的压缩文件,支持包含多个条目的ZIP档案。
12.2.2 使用示例
java
import java.util.zip.*;
public class ZipExample {
// 创建ZIP文件
public static void createZipFile(String zipFileName, Map<String, byte[]> files)
throws IOException {
try (ZipOutputStream zos = new ZipOutputStream(
new FileOutputStream(zipFileName))) {
for (Map.Entry<String, byte[]> entry : files.entrySet()) {
// 创建ZIP条目
ZipEntry zipEntry = new ZipEntry(entry.getKey());
zos.putNextEntry(zipEntry);
// 写入文件数据
zos.write(entry.getValue());
// 关闭当前条目
zos.closeEntry();
}
System.out.println("ZIP文件创建完成: " + zipFileName);
}
}
// 读取ZIP文件
public static void readZipFile(String zipFileName) throws IOException {
try (ZipInputStream zis = new ZipInputStream(
new FileInputStream(zipFileName))) {
ZipEntry entry;
byte[] buffer = new byte[8192];
while ((entry = zis.getNextEntry()) != null) {
System.out.println("读取条目: " + entry.getName());
System.out.println(" 大小: " + entry.getSize() + " bytes");
System.out.println(" 压缩后大小: " + entry.getCompressedSize() + " bytes");
System.out.println(" 方法: " + (entry.getMethod() == ZipEntry.DEFLATED ?
"DEFLATED" : "STORED"));
// 读取条目内容
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bytesRead;
while ((bytesRead = zis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
System.out.println(" 内容长度: " + baos.size() + " bytes");
System.out.println();
zis.closeEntry();
}
}
}
// 使用ZipFile随机访问ZIP条目(推荐用于大量文件)
public static void readWithZipFile(String zipFileName) throws IOException {
try (ZipFile zipFile = new ZipFile(zipFileName)) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
System.out.println("ZIP条目: " + entry.getName());
// 直接获取输入流读取内容
try (InputStream is = zipFile.getInputStream(entry)) {
byte[] content = is.readAllBytes();
System.out.println(" 内容大小: " + content.length);
}
}
}
}
public static void main(String[] args) {
try {
// 准备要压缩的文件
Map<String, byte[]> files = new HashMap<>();
files.put("document.txt", "这是文档内容".getBytes(StandardCharsets.UTF_8));
files.put("data.bin", new byte[1000]);
files.put("subdir/notes.txt", "这是子目录中的笔记".getBytes(StandardCharsets.UTF_8));
// 创建ZIP文件
String zipFile = "archive.zip";
createZipFile(zipFile, files);
// 读取ZIP文件
System.out.println("\n使用ZipInputStream读取:");
readZipFile(zipFile);
System.out.println("\n使用ZipFile读取:");
readWithZipFile(zipFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}
第十三部分:组合流与最佳实践
13.1 流的装饰器模式
Java I/O库广泛使用了装饰器模式(Decorator Pattern),允许动态地为流添加功能。这种设计使得组合不同功能的流变得非常简单。
java
// 一个典型的组合流示例
InputStream input = new BufferedInputStream( // 添加缓冲功能
new GZIPInputStream( // 添加解压缩功能
new FileInputStream("data.gz"))); // 基础文件输入
13.2 常用组合模式
java
public class StreamCombinations {
// 高效读取压缩的序列化对象
public static Object readCompressedObject(String file)
throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(
new GZIPInputStream(
new FileInputStream(file))))) {
return ois.readObject();
}
}
// 写入带校验和的压缩数据
public static void writeCompressedWithChecksum(String file, byte[] data)
throws IOException {
try (CheckedOutputStream cos = new CheckedOutputStream(
new GZIPOutputStream(
new BufferedOutputStream(
new FileOutputStream(file))),
new CRC32())) {
cos.write(data);
cos.flush();
System.out.printf("数据CRC32: 0x%x%n", cos.getChecksum().getValue());
}
}
// 网络数据传输组合
public static void sendObjectOverNetwork(Socket socket, Object obj)
throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(
socket.getOutputStream()))) {
oos.writeObject(obj);
oos.flush();
}
}
// 读取配置文件(使用PushbackInputStream处理注释)
public static Properties readConfig(String file) throws IOException {
Properties props = new Properties();
try (PushbackInputStream pbis = new PushbackInputStream(
new BufferedInputStream(
new FileInputStream(file)), 2)) {
StringBuilder currentLine = new StringBuilder();
int b;
while ((b = pbis.read()) != -1) {
// 检查是否是注释行(以#开头)
if (b == '#') {
// 跳过整行
while ((b = pbis.read()) != -1 && b != '\n') {
// 跳过注释内容
}
continue;
}
// 处理非注释行
currentLine.append((char) b);
// 如果遇到换行符,处理这一行
if (b == '\n') {
String line = currentLine.toString().trim();
if (!line.isEmpty()) {
// 解析属性行
String[] parts = line.split("=", 2);
if (parts.length == 2) {
props.setProperty(parts[0].trim(), parts[1].trim());
}
}
currentLine = new StringBuilder();
}
}
}
return props;
}
}
13.3 性能优化最佳实践
13.3.1 缓冲区大小选择
java
public class BufferSizeOptimization {
// 根据设备特性选择缓冲区大小
public static int getOptimalBufferSize() {
// 对于HDD,建议64KB
// 对于SSD,建议16KB-32KB
// 对于网络流,建议8KB-16KB
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("linux")) {
// Linux通常使用4KB页大小,缓冲区设为4KB的倍数
return 8192; // 8KB
} else if (os.contains("win")) {
// Windows通常使用64KB的簇大小
return 65536; // 64KB
} else {
return 16384; // 16KB默认值
}
}
// 智能缓冲流创建
public static InputStream createOptimizedInputStream(File file)
throws IOException {
int bufferSize = getOptimalBufferSize();
return new BufferedInputStream(
new FileInputStream(file), bufferSize);
}
}
13.3.2 避免常见陷阱
java
public class CommonPitfalls {
// 错误:没有使用缓冲
public static void badCopy(File source, File dest) throws IOException {
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(dest)) {
int b;
while ((b = fis.read()) != -1) { // 单字节读取,性能极差
fos.write(b);
}
}
}
// 正确:使用缓冲
public static void goodCopy(File source, File dest) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(source));
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(dest))) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
}
}
// 错误:不正确的编码处理
public static void badEncodingHandling(String file) throws IOException {
try (FileInputStream fis = new FileInputStream(file)) {
byte[] data = fis.readAllBytes();
String text = new String(data); // 使用平台默认编码,可能乱码
System.out.println(text);
}
}
// 正确:指定编码
public static void goodEncodingHandling(String file) throws IOException {
try (FileInputStream fis = new FileInputStream(file)) {
byte[] data = fis.readAllBytes();
String text = new String(data, StandardCharsets.UTF_8);
System.out.println(text);
}
}
// 错误:关闭顺序不当
public static void badCloseOrder(String file) throws IOException {
GZIPOutputStream gzos = null;
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
gzos = new GZIPOutputStream(fos);
gzos.write("data".getBytes());
} finally {
gzos.close(); // 先关闭包装流
fos.close(); // 后关闭底层流 - GZIPOutputStream已经关闭了它
}
}
// 正确:try-with-resources自动处理
public static void goodCloseOrder(String file) throws IOException {
try (FileOutputStream fos = new FileOutputStream(file);
GZIPOutputStream gzos = new GZIPOutputStream(fos)) {
gzos.write("data".getBytes());
} // 自动关闭,顺序正确(后创建的先关闭)
}
}
13.4 异常处理最佳实践
java
public class ExceptionHandlingBestPractices {
// 完整的异常处理模式
public static void safeFileCopy(File source, File dest) {
// 使用try-with-resources自动关闭资源(JDK 7+)
try (InputStream in = new BufferedInputStream(new FileInputStream(source));
OutputStream out = new BufferedOutputStream(new FileOutputStream(dest))) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.flush(); // 确保所有数据写入
} catch (FileNotFoundException e) {
System.err.println("文件未找到: " + e.getMessage());
// 根据业务需求处理,可能是创建文件或抛出业务异常
} catch (IOException e) {
System.err.println("I/O错误: " + e.getMessage());
// 记录日志,可能重试或通知用户
e.printStackTrace();
} catch (SecurityException e) {
System.err.println("没有权限访问文件: " + e.getMessage());
// 权限不足的处理
}
}
// 处理部分写入的情况
public static void robustWrite(File file, byte[] data) throws IOException {
try (FileOutputStream fos = new FileOutputStream(file)) {
int written = 0;
int remaining = data.length;
while (remaining > 0) {
int bytesToWrite = Math.min(remaining, 8192);
fos.write(data, written, bytesToWrite);
written += bytesToWrite;
remaining -= bytesToWrite;
}
System.out.printf("成功写入 %d 字节到 %s%n", written, file);
}
}
// 使用finally确保资源关闭(JDK 6及之前版本)
public static void legacyResourceHandling(String file) {
InputStream in = null;
try {
in = new FileInputStream(file);
// 处理数据
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// 记录关闭异常,但不应掩盖原始异常
e.printStackTrace();
}
}
}
}
}
第十四部分:总结与选择指南
14.1 实现类对比表
| 实现类 | 类型 | 主要用途 | 特点 | 适用场景 |
|---|---|---|---|---|
| FileInputStream/FileOutputStream | 节点流 | 文件读写 | 基础文件操作 | 所有文件I/O的基础 |
| BufferedInputStream/BufferedOutputStream | 处理流 | 缓冲功能 | 提高性能,减少I/O次数 | 所有需要提高性能的场景 |
| ByteArrayInputStream/ByteArrayOutputStream | 节点流 | 内存操作 | 速度快,无需关闭 | 临时数据存储、数据转换 |
| ObjectInputStream/ObjectOutputStream | 处理流 | 对象序列化 | 保存和恢复对象 | 对象持久化、RMI |
| DataInputStream/DataOutputStream | 处理流 | 基本类型读写 | 平台无关的二进制数据 | 网络协议、二进制文件格式 |
| PipedInputStream/PipedOutputStream | 节点流 | 线程间通信 | 阻塞式管道通信 | 生产者-消费者模式 |
| PushbackInputStream | 处理流 | 回退功能 | 允许重新读取 | 语法解析、协议解析 |
| PrintStream | 处理流 | 格式化输出 | 方便的打印方法 | 日志输出、System.out |
| SequenceInputStream | 处理流 | 流合并 | 合并多个输入流 | 文件合并、分段数据处理 |
| GZIPInputStream/GZIPOutputStream | 处理流 | 数据压缩 | GZIP格式压缩 | 文件压缩、网络传输优化 |
| ZipInputStream/ZipOutputStream | 处理流 | ZIP文件处理 | 支持多条目 | ZIP档案操作 |
| CheckedInputStream/CheckedOutputStream | 处理流 | 校验和计算 | 数据完整性验证 | 文件传输验证、存储校验 |
14.2 选择指南
根据不同的使用场景,推荐以下选择策略:
文件读写场景:
- 小文件:直接使用FileInputStream/FileOutputStream
- 大文件:使用BufferedInputStream/BufferedOutputStream包装
- 需要频繁读写:始终使用缓冲流
内存数据处理:
- 临时数据:使用ByteArrayInputStream/ByteArrayOutputStream
- 数据格式转换:内存流作为中间缓冲区
数据持久化:
- 对象存储:使用ObjectInputStream/ObjectOutputStream
- 基本类型数据:使用DataInputStream/DataOutputStream
- 文本数据:使用字符流(Reader/Writer)
网络通信:
- 高效传输:组合BufferedOutputStream和DataOutputStream
- 对象传输:组合ObjectOutputStream和缓冲流
- 压缩传输:组合GZIPOutputStream和缓冲流
多线程编程:
- 线程间数据交换:使用PipedInputStream/PipedOutputStream
- 注意:必须在不同线程中使用,避免死锁
特殊需求:
- 需要预读功能:使用PushbackInputStream
- 需要格式化输出:使用PrintStream
- 需要数据验证:使用CheckedInputStream/CheckedOutputStream
- 需要处理ZIP档案:使用ZipInputStream/ZipOutputStream或ZipFile
14.3 性能考虑要点
- 缓冲区大小:8KB-64KB通常是最佳范围
- 减少系统调用 :每次
read()/write()都可能触发系统调用 - 批量操作 :尽量使用数组参数的
read(byte[])和write(byte[])方法 - 避免频繁创建流:流创建有开销,考虑复用
- 及时关闭:使用try-with-resources确保资源释放
14.4 最终建议
Java的I/O体系虽然庞大,但掌握了核心概念和常用实现类后,可以灵活组合出满足各种需求的解决方案。建议:
- 优先使用缓冲流提高性能
- 始终使用try-with-resources自动管理资源
- 根据数据类型选择合适的流(字节流还是字符流)
- 注意字符编码问题,始终明确指定编码
- 理解装饰器模式,学会组合不同功能的流