💡 摘要:你是否曾对Java中众多的IO流类感到困惑?是否分不清什么时候用字节流什么时候用字符流?是否想知道为什么要有这么多相似的流类?
别担心,IO流是Java中处理输入输出的核心机制,理解其分类和用途是掌握Java编程的关键。
本文将带你从IO流的基本概念讲起,理解字节流和字符流的根本区别。然后深入字节流体系,学习FileInputStream、FileOutputStream等类的使用。
接着探索字符流体系,掌握FileReader、FileWriter和字符编码的重要性。最后通过实战案例展示如何正确选择和使用不同的流。从文件复制到编码转换,从性能考虑到异常处理,让你全面掌握Java IO流的基础知识。文末附常见问题解析,助你避免IO编程中的常见陷阱。
一、IO流概述:输入与输出的桥梁
1. 什么是IO流?
流的概念:流是数据传输的抽象,可以看作数据的管道或序列。Java IO流提供了统一的方式来处理各种输入输出操作。
流的分类:
text
按数据单位:字节流(8位字节) vs 字符流(16位字符)
按流向:输入流(读取数据) vs 输出流(写入数据)
按功能:节点流(直接操作数据源) vs 处理流(包装其他流)
2. IO流体系结构
Java IO主要类体系:
text
InputStream (字节输入流)
├── FileInputStream
├── ByteArrayInputStream
├── ObjectInputStream
└── FilterInputStream
├── BufferedInputStream
├── DataInputStream
└── ...
OutputStream (字节输出流)
├── FileOutputStream
├── ByteArrayOutputStream
├── ObjectOutputStream
└── FilterOutputStream
├── BufferedOutputStream
├── DataOutputStream
└── ...
Reader (字符输入流)
├── FileReader
├── InputStreamReader
├── CharArrayReader
└── BufferedReader
Writer (字符输出流)
├── FileWriter
├── OutputStreamWriter
├── CharArrayWriter
└── BufferedWriter
二、字节流:处理二进制数据
1. FileInputStream:文件字节输入流
基本用法:
java
// 1. 创建文件输入流
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
// 2. 读取数据
int data;
while ((data = fis.read()) != -1) { // 每次读取一个字节
System.out.print((char) data); // 转换为字符输出
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3. 关闭流
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
批量读取优化:
java
try (FileInputStream fis = new FileInputStream("largefile.bin")) {
byte[] buffer = new byte[1024]; // 1KB缓冲区
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// 处理读取的数据
processData(buffer, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
2. FileOutputStream:文件字节输出流
基本用法:
java
// 使用try-with-resources自动关闭流
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
String text = "Hello, Java IO!";
byte[] bytes = text.getBytes(); // 字符串转换为字节数组
fos.write(bytes); // 写入整个字节数组
fos.write('\n'); // 写入单个字节
fos.write("Another line".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
追加模式:
java
// 第二个参数true表示追加模式
try (FileOutputStream fos = new FileOutputStream("log.txt", true)) {
String logEntry = LocalDateTime.now() + " - User logged in\n";
fos.write(logEntry.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
3. 文件复制实战
字节流文件复制:
java
public class FileCopy {
public static void copyFile(String sourcePath, String targetPath) {
// 使用try-with-resources确保资源关闭
try (FileInputStream fis = new FileInputStream(sourcePath);
FileOutputStream fos = new FileOutputStream(targetPath)) {
byte[] buffer = new byte[8192]; // 8KB缓冲区
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead); // 写入实际读取的字节数
}
System.out.println("文件复制完成");
} catch (FileNotFoundException e) {
System.err.println("文件未找到: " + e.getMessage());
} catch (IOException e) {
System.err.println("IO错误: " + e.getMessage());
}
}
public static void main(String[] args) {
copyFile("source.jpg", "copy.jpg");
}
}
三、字符流:处理文本数据
1. 字符编码的重要性
常见字符编码:
- UTF-8:变长编码,兼容ASCII,支持所有Unicode字符
- GBK:中文编码标准,兼容GB2312
- ISO-8859-1:Latin-1编码,西欧语言
- UTF-16:定长或变长编码,Java内部使用
编码问题示例:
java
String text = "中文测试";
try {
// 不同编码的字节表示
byte[] utf8Bytes = text.getBytes("UTF-8");
byte[] gbkBytes = text.getBytes("GBK");
System.out.println("UTF-8字节数: " + utf8Bytes.length); // 12
System.out.println("GBK字节数: " + gbkBytes.length); // 8
// 错误编码导致的乱码
String wrongText = new String(utf8Bytes, "ISO-8859-1");
System.out.println("乱码: " + wrongText); // 输出乱码
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
2. FileReader:文件字符输入流
基本用法:
java
// 读取文本文件
try (FileReader reader = new FileReader("text.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[1024];
int charsRead;
while ((charsRead = reader.read(buffer)) != -1) {
String content = new String(buffer, 0, charsRead);
System.out.print(content);
}
} catch (IOException e) {
e.printStackTrace();
}
逐行读取的局限:
java
// FileReader没有readLine方法,需要包装为BufferedReader
try (FileReader fr = new FileReader("text.txt");
BufferedReader br = new BufferedReader(fr)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
3. FileWriter:文件字符输出流
基本用法:
java
// 写入文本文件
try (FileWriter writer = new FileWriter("output.txt", StandardCharsets.UTF_8)) {
writer.write("Hello, 世界!\n");
writer.write("这是第二行\n");
writer.write("数字: " + 42 + "\n");
// 写入字符数组
char[] chars = {'A', 'B', 'C'};
writer.write(chars);
writer.write('\n');
} catch (IOException e) {
e.printStackTrace();
}
追加模式:
java
try (FileWriter writer = new FileWriter("log.txt", StandardCharsets.UTF_8, true)) {
writer.write(LocalDateTime.now() + " - 日志条目\n");
} catch (IOException e) {
e.printStackTrace();
}
四、字节流 vs 字符流:正确选择
1. 使用场景对比
字节流适用场景:
- ✅ 二进制文件:图片、视频、音频、PDF等
- ✅ 网络数据传输
- ✅ 对象序列化
- ✅ 加密/压缩数据
字符流适用场景:
- ✅ 文本文件:TXT、XML、JSON、HTML等
- ✅ 配置文件读写
- ✅ 日志文件处理
- ✅ 用户输入输出
2. 编码转换桥梁
InputStreamReader和OutputStreamWriter:
java
// 字节流到字符流的转换(指定编码)
try (FileInputStream fis = new FileInputStream("source.txt");
InputStreamReader isr = new InputStreamReader(fis, "GBK"); // 指定源文件编码
FileOutputStream fos = new FileOutputStream("target.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8")) { // 指定目标编码
char[] buffer = new char[1024];
int charsRead;
while ((charsRead = isr.read(buffer)) != -1) {
osw.write(buffer, 0, charsRead); // 编码转换
}
System.out.println("文件编码转换完成");
} catch (IOException e) {
e.printStackTrace();
}
五、实战案例:综合应用
1. 文件加密解密
简单的XOR加密:
java
public class FileEncryptor {
public static void encryptFile(String sourcePath, String targetPath, byte key) {
try (FileInputStream fis = new FileInputStream(sourcePath);
FileOutputStream fos = new FileOutputStream(targetPath)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// 对每个字节进行XOR加密
for (int i = 0; i < bytesRead; i++) {
buffer[i] = (byte) (buffer[i] ^ key); // XOR运算
}
fos.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 解密使用相同的key(XOR的特性)
public static void decryptFile(String sourcePath, String targetPath, byte key) {
encryptFile(sourcePath, targetPath, key); // XOR两次等于原数据
}
public static void main(String[] args) {
byte key = 0x55; // 加密密钥
encryptFile("secret.txt", "encrypted.dat", key);
decryptFile("encrypted.dat", "decrypted.txt", key);
}
}
2. 配置文件读取
Properties文件处理:
java
public class ConfigReader {
public static Properties loadConfig(String configPath) {
Properties props = new Properties();
try (FileInputStream fis = new FileInputStream(configPath);
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
props.load(isr); // 加载properties文件
} catch (IOException e) {
System.err.println("无法读取配置文件: " + e.getMessage());
}
return props;
}
public static void saveConfig(String configPath, Properties props) {
try (FileOutputStream fos = new FileOutputStream(configPath);
OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) {
props.store(osw, "Application Configuration");
} catch (IOException e) {
System.err.println("无法保存配置文件: " + e.getMessage());
}
}
public static void main(String[] args) {
Properties config = loadConfig("config.properties");
String username = config.getProperty("username", "defaultUser");
int timeout = Integer.parseInt(config.getProperty("timeout", "30"));
System.out.println("用户名: " + username);
System.out.println("超时: " + timeout + "秒");
}
}
六、异常处理与资源管理
1. 传统的try-catch-finally
手动资源管理:
java
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("source.txt");
fos = new FileOutputStream("target.txt");
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
System.err.println("IO错误: " + e.getMessage());
} finally {
// 手动关闭资源
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
System.err.println("关闭输入流失败: " + e.getMessage());
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
System.err.println("关闭输出流失败: " + e.getMessage());
}
}
}
2. try-with-resources(推荐)
自动资源管理:
java
// Java 7+ 自动关闭资源
try (FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("target.txt")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
System.err.println("IO错误: " + e.getMessage());
}
// 不需要finally块,资源自动关闭
自定义可关闭资源:
java
// 实现AutoCloseable接口
class DatabaseConnection implements AutoCloseable {
public DatabaseConnection(String url) {
System.out.println("连接数据库: " + url);
}
public void query(String sql) {
System.out.println("执行查询: " + sql);
}
@Override
public void close() {
System.out.println("关闭数据库连接");
}
}
// 使用自定义资源
try (DatabaseConnection conn = new DatabaseConnection("jdbc:mysql://localhost:3306/test")) {
conn.query("SELECT * FROM users");
} // 自动调用close()方法
七、总结:IO流最佳实践
1. 选择正确的流类型
选择指南:
- 文本文件:使用字符流(FileReader/FileWriter)
- 二进制文件:使用字节流(FileInputStream/FileOutputStream)
- 需要指定编码:使用InputStreamReader/OutputStreamWriter
- 需要缓冲:包装为BufferedInputStream/BufferedReader
2. 性能优化建议
缓冲区大小:
java
// 合适的缓冲区大小(通常8KB-64KB)
byte[] buffer = new byte[8192]; // 8KB
char[] charBuffer = new char[8192];
// 对于大文件,可以使用更大的缓冲区
byte[] largeBuffer = new byte[65536]; // 64KB
资源管理:
- ✅ 使用try-with-resources确保资源关闭
- ✅ 及时关闭不再使用的流
- ✅ 避免同时打开过多文件
3. 编码处理建议
统一编码规范:
java
// 明确指定编码
String text = new String(bytes, StandardCharsets.UTF_8);
byte[] output = text.getBytes(StandardCharsets.UTF_8);
// 读写文件时指定编码
try (InputStreamReader reader = new InputStreamReader(
new FileInputStream("file.txt"), StandardCharsets.UTF_8)) {
// 读取内容
}
八、面试高频问题
❓1. 字节流和字符流有什么区别?
答:字节流以字节为单位操作数据,适合处理二进制文件。字符流以字符为单位操作数据,适合处理文本文件,会自动处理字符编码。
❓2. 为什么需要字符编码?常见的编码有哪些?
答:字符编码定义了字符和字节之间的映射关系。常见编码:UTF-8(推荐)、GBK、ISO-8859-1、UTF-16等。
❓3. try-with-resources有什么优势?
答:自动关闭资源,代码更简洁,避免资源泄漏,异常处理更清晰。
❓4. 如何提高IO操作的性能?
答:使用缓冲区、选择合适的缓冲区大小、使用NIO、批量读写操作。
❓5. FileInputStream和BufferedInputStream有什么区别?
答:FileInputStream是基础的文件字节流,每次读取都要进行系统调用。BufferedInputStream提供了缓冲区,减少系统调用次数,提高读取性能。