目录
4.3.1 BufferedInputStream/BufferedOutputStream
4.3.1 BufferedInputStream/BufferedOutputStream
4.3.2 BufferedReader/BufferedWriter
4.5.3 序列化版本号(serialVersionUID)
7.2 Java IO与Apache Commons IO对比
Q4: Java中如何实现文件复制?请给出不同性能级别的实现。
1. 引言
当你尝试从键盘读取用户输入、将数据保存到文件、或是通过网络传输信息时,你实际上都在处理数据的"流动"。想象一下,数据就像水一样,需要从一个地方"流"到另一个地方 - 从文件流向程序,或从程序流向网络。这就是为什么我们需要学习IO流技术。
Java的IO流技术为我们提供了一套完整的解决方案,使我们能够轻松地处理数据的输入与输出,无论是处理本地文件、网络通信,还是内存中的数据交换。掌握IO流,就像拥有了数据处理的"水管工具箱",让你能够自如地控制数据的流向和处理方式。
2. 基础概念
2.1 什么是IO流?
IO流(Input/Output Stream)是Java中用于处理输入和输出数据传输的抽象概念。它将数据的传输过程形象地比喻为"流水",数据如同水流一样从一个位置流向另一个位置。
IO流体系
|
+-----------------------+
| |
字节流 字符流
| |
+------+------+ +------+------+
| | | |
InputStream OutputStream Reader Writer
3. 主要特性
3.1 方向性
IO流具有明确的方向性,每个流要么是输入流,要么是输出流,不能同时兼具两种功能。这种单向设计使得API更加简洁,使用更加清晰。
// 输入流示例:从文件读取数据
InputStream input = new FileInputStream("data.txt");
// 输出流示例:向文件写入数据
OutputStream output = new FileOutputStream("result.txt");
3.2 装饰者模式
Java IO流采用了装饰者设计模式,允许在基本流的基础上叠加额外的功能。这种模式使得IO流组件可以灵活组合,按需添加所需功能。
// 基本文件流 + 缓冲功能 + 对象序列化功能
ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream("object.dat")));
3.3 缓冲机制
IO流中的缓冲机制大幅提高了数据处理效率。缓冲流通过减少底层系统调用次数,显著提升IO操作性能。
// 不使用缓冲的文件读取
FileReader fr = new FileReader("data.txt");
// 使用缓冲的文件读取,性能更好
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
3.4 自动关闭资源
Java 7引入的try-with-resources语法可以自动关闭IO资源,避免资源泄漏,简化了异常处理代码。
// 自动关闭资源,不需要手动调用close()
try (FileInputStream fis = new FileInputStream("input.txt")) {
// 使用输入流
}
3.5 字符编码处理
字符流可以自动处理字符编码转换,使得处理不同语言和编码的文本文件变得简单。
// 指定UTF-8编码读取文本文件
Reader reader = new InputStreamReader(
new FileInputStream("text.txt"), "UTF-8");
4. 详细用法
Java IO流体系
|
+-------------+-------------+
| |
字节流 字符流
| |
+----+----+ +----+----+
| | | |
输入流 输出流 输入流 输出流
| | | |
InputStream OutputStream Reader Writer
| | | |
+----+----+ +---+----+
| |
+----+----+----------------+ |
| | | | |
文件流 缓冲流 数据流 对象流 打印流
| | | | |
FileXXXStream BufferedXXX DataXXX ObjectXXX PrintXXX
4.1 文件字节流 - 处理二进制数据的基础流
常用API
FileInputStream 常用方法
方法 | 描述 |
---|---|
public FileInputStream(File file) | 通过File对象创建文件输入流 |
public FileInputStream(String name) | 通过文件路径名创建文件输入流 |
public int read() | 读取单个字节,返回0-255的整数,如果到达文件末尾则返回-1 |
public int read(byte[] b) | 读取多个字节到字节数组,返回实际读取的字节数 |
public int read(byte[] b, int off, int len) | 读取len个字节到数组b的off位置,返回实际读取的字节数 |
public long skip(long n) | 跳过n个字节不读取 |
public int available() | 返回可以无阻塞地读取的字节数 |
public void close() | 关闭流并释放相关资源 |
FileOutputStream 常用方法
方法 | 描述 |
---|---|
public FileOutputStream(File file) | 通过File对象创建文件输出流(默认覆盖原文件) |
public FileOutputStream(String name) | 通过文件路径名创建文件输出流(默认覆盖原文件) |
public FileOutputStream(File file, boolean append) | 创建文件输出流,append为true表示追加模式 |
public void write(int b) | 写入单个字节 |
public void write(byte[] b) | 写入字节数组 |
public void write(byte[] b, int off, int len) | 写入字节数组的一部分(从off开始,长度为len) |
public void flush() | 刷新缓冲区,确保所有数据被写出 |
public void close() | 关闭流并释放相关资源 |
文件字节流体系
|
+-------------------+
| |
FileInputStream FileOutputStream
| |
读取二进制文件 写入二进制文件
4.1.1 FileInputStream
文件字节流是最基础的IO流,用于读写文件中的原始字节数据。
4.1.1 FileInputStream
try (FileInputStream fis = new FileInputStream("data.bin")) {
// 读取单个字节
int data = fis.read(); // 返回-1表示已到达文件末尾
// 读取多个字节到缓冲区
byte[] buffer = new byte[1024];
int bytesRead = fis.read(buffer); // 返回实际读取的字节数
// 获取可用字节数
int available = fis.available();
// 跳过字节
long skipped = fis.skip(100); // 跳过100个字节
} catch (IOException e) {
e.printStackTrace();
}
新手常见问题
- 忘记关闭流:使用完毕后必须关闭流释放资源,推荐使用try-with-resources自动关闭
- 误用返回值:
read()
返回的是0-255的字节值,到达文件末尾返回-1,不要误解为字节本身 - 读取效率低:单字节读取效率极低,应使用字节数组批量读取
- 编码问题:处理文本时,字节流不处理字符编码,可能导致乱码,应使用字符流
- 缓冲区大小:缓冲区太小会导致频繁读取,太大会浪费内存,一般1KB-8KB为宜
4.1.2 FileOutputStream
try (FileOutputStream fos = new FileOutputStream("output.bin")) {
// 写入单个字节
fos.write(65); // 写入字符'A'的ASCII码
// 写入字节数组
byte[] data = {66, 67, 68}; // 'B', 'C', 'D'
fos.write(data);
// 写入字节数组的部分内容
fos.write(data, 1, 2); // 只写入'C', 'D'
// 刷新缓冲区,确保数据写入
fos.flush();
} catch (IOException e) {
e.printStackTrace();
}
字节流使用细节
-
文件覆盖问题:
FileOutputStream
默认会覆盖现有文件,如需追加内容,使用追加模式构造函数// 追加模式 FileOutputStream fos = new FileOutputStream("log.txt", true);
-
文件锁定:如果文件正被其他进程独占写入,可能导致
FileNotFoundException
-
大文件处理:处理大文件时,建议使用缓冲流提高性能
-
二进制一致性:字节流可确保二进制数据的精确复制,适合非文本文件
-
目录自动创建:输出流不会自动创建不存在的目录,需要手动处理
File file = new File("data/logs/app.log"); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); // 创建父目录 } FileOutputStream fos = new FileOutputStream(file);
4.2 文件字符流 - 专门处理文本数据的基础流
常用API
FileReader 常用方法
方法 | 描述 |
---|---|
public FileReader(File file) | 通过File对象创建字符输入流 |
public FileReader(String fileName) | 通过文件路径名创建字符输入流 |
public int read() | 读取单个字符,返回0-65535的整数,到达末尾返回-1 |
public int read(char[] cbuf) | 读取多个字符到字符数组,返回实际读取的字符数 |
public int read(char[] cbuf, int off, int len) | 读取字符到数组指定位置,返回实际读取的字符数 |
public void close() | 关闭流并释放相关资源 |
FileWriter 常用方法
方法 | 描述 |
---|---|
public FileWriter(File file) | 通过File对象创建字符输出流(默认覆盖) |
public FileWriter(String fileName) | 通过文件路径名创建字符输出流(默认覆盖) |
public FileWriter(File file, boolean append) | 创建字符输出流,append为true表示追加模式 |
public void write(int c) | 写入单个字符 |
public void write(char[] cbuf) | 写入字符数组 |
public void write(String str) | 写入字符串 |
public void write(String str, int off, int len) | 写入字符串的一部分 |
public void flush() | 刷新缓冲区 |
public void close() | 关闭流并释放资源 |
文件字符流结构
|
+---------+---------+
| |
FileReader FileWriter
| |
读取文本文件 写入文本文件
| |
自动处理字符编码 自动处理字符编码
| |
基于默认字符集 默认使用系统编码
4.2.1 FileReader
文件字符流专门用于处理文本数据,会自动进行字符编码/解码。
4.2.1 FileReader
try (FileReader fr = new FileReader("text.txt")) {
// 读取单个字符
int character = fr.read(); // 返回-1表示已到达文件末尾
// 读取多个字符到缓冲区
char[] buffer = new char[1024];
int charsRead = fr.read(buffer); // 返回实际读取的字符数
// 将读取的字符转换为字符串
String content = new String(buffer, 0, charsRead);
System.out.println(content);
} catch (IOException e) {
e.printStackTrace();
}
字符流编码注意事项
-
默认字符集问题:FileReader/FileWriter使用系统默认字符集,不同系统可能不同
- Windows中文系统通常默认GBK/GB2312
- Linux/Mac通常默认UTF-8
- 跨平台应用应显式指定字符集
-
编码不匹配导致乱码:读写文件时使用的编码必须与文件的实际编码一致
// 使用指定编码需要使用InputStreamReader/OutputStreamWriter try (Reader reader = new InputStreamReader( new FileInputStream("text.txt"), "UTF-8")) { // 读取内容 }
-
BOM标记问题:某些UTF-8文件可能包含BOM标记,需要特殊处理
-
缓冲区刷新:FileWriter有内部缓冲区,必须flush()或close()才能确保数据写入文件
-
非文本文件:字符流不适合处理二进制文件(图片、音频等),会导致数据损坏
4.2.2 FileWriter
try (FileWriter fw = new FileWriter("output.txt")) {
// 写入单个字符
fw.write('A');
// 写入字符串
fw.write("Hello, Java IO!");
// 写入字符数组
char[] chars = {'J', 'a', 'v', 'a'};
fw.write(chars);
// 写入字符数组的部分内容
fw.write(chars, 1, 2); // 只写入'a', 'v'
// 刷新缓冲区
fw.flush();
} catch (IOException e) {
e.printStackTrace();
}
4.3 缓冲流 - 大幅提升IO性能的包装流
常用API
BufferedInputStream 常用方法
方法 | 描述 |
---|---|
public BufferedInputStream(InputStream in) | 创建缓冲输入流,使用默认缓冲区大小 |
public BufferedInputStream(InputStream in, int size) | 创建缓冲输入流,指定缓冲区大小 |
public int read() | 读取单个字节 |
public int read(byte[] b, int off, int len) | 读取多个字节到数组指定位置 |
public void mark(int readlimit) | 标记当前位置,readlimit指定在标记失效前可读取的最大字节数 |
public void reset() | 重置到最后一次mark的位置 |
public boolean markSupported() | 判断是否支持mark/reset操作 |
public void close() | 关闭流并释放资源 |
BufferedOutputStream 常用方法
方法 | 描述 |
---|---|
public BufferedOutputStream(OutputStream out) | 创建缓冲输出流,使用默认缓冲区大小 |
public BufferedOutputStream(OutputStream out, int size) | 创建缓冲输出流,指定缓冲区大小 |
public void write(int b) | 写入单个字节 |
public void write(byte[] b, int off, int len) | 写入字节数组的一部分 |
public void flush() | 刷新缓冲区 |
public void close() | 关闭流并释放资源 |
BufferedReader 常用方法
方法 | 描述 |
---|---|
public BufferedReader(Reader in) | 创建缓冲字符输入流,使用默认缓冲区大小 |
public BufferedReader(Reader in, int sz) | 创建缓冲字符输入流,指定缓冲区大小 |
public String readLine() | 读取一行文本,不包含换行符,到达末尾返回null |
public int read() | 读取单个字符 |
public void close() | 关闭流并释放资源 |
BufferedWriter 常用方法
方法 | 描述 |
---|---|
public BufferedWriter(Writer out) | 创建缓冲字符输出流,使用默认缓冲区大小 |
public BufferedWriter(Writer out, int sz) | 创建缓冲字符输出流,指定缓冲区大小 |
public void write(int c) | 写入单个字符 |
public void write(String s, int off, int len) | 写入字符串的一部分 |
public void newLine() | 写入一个行分隔符(平台相关) |
public void flush() | 刷新缓冲区 |
public void close() | 关闭流并释放资源 |
缓冲流体系
|
+--------------------+
| |
字节缓冲流 字符缓冲流
| |
+----+----+ +---+----+
| | | |
BufferedIn BufferedOut BufferedReader BufferedWriter
Stream Stream | |
可按行读取 提供newLine()方法
4.3.1 BufferedInputStream/BufferedOutputStream
缓冲流通过使用内存缓冲区,大幅提高IO操作效率。
4.3.1 BufferedInputStream/BufferedOutputStream
// 缓冲字节输入流
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("data.bin"), 8192)) { // 指定缓冲区大小为8KB
byte[] buffer = new byte[1024];
int bytesRead;
// 读取数据(性能比直接使用FileInputStream更好)
while ((bytesRead = bis.read(buffer)) != -1) {
// 处理读取的数据
}
} catch (IOException e) {
e.printStackTrace();
}
// 缓冲字节输出流
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("output.bin"))) {
byte[] data = new byte[1024];
// 假设data中已有数据
// 写入数据(性能比直接使用FileOutputStream更好)
bos.write(data);
// 强制刷新缓冲区
bos.flush();
} catch (IOException e) {
e.printStackTrace();
}
4.3.2 BufferedReader/BufferedWriter
// 缓冲字符输入流
try (BufferedReader br = new BufferedReader(new FileReader("text.txt"))) {
// 按行读取文本(这是BufferedReader的独特功能)
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
// 缓冲字符输出流
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
// 写入文本
bw.write("Hello, Java IO!");
// 写入行分隔符(平台无关)
bw.newLine();
// 写入更多文本
bw.write("这是第二行");
// 刷新缓冲区
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
4.4 数据流 - 读写基本数据类型的便捷流
常用API
DataInputStream 常用方法
方法 | 描述 |
---|---|
public DataInputStream(InputStream in) | 创建数据输入流 |
public final boolean readBoolean() | 读取一个布尔值 |
public final byte readByte() | 读取一个字节 |
public final short readShort() | 读取一个短整型值 |
public final int readInt() | 读取一个整型值 |
public final long readLong() | 读取一个长整型值 |
public final float readFloat() | 读取一个浮点型值 |
public final double readDouble() | 读取一个双精度浮点型值 |
public final String readUTF() | 读取UTF-8编码的字符串 |
public void close() | 关闭流并释放资源 |
DataOutputStream 常用方法
方法 | 描述 |
---|---|
public DataOutputStream(OutputStream out) | 创建数据输出流 |
public final void writeBoolean(boolean v) | 写入布尔值 |
public final void writeByte(int v) | 写入字节 |
public final void writeShort(int v) | 写入短整型值 |
public final void writeInt(int v) | 写入整型值 |
public final void writeLong(long v) | 写入长整型值 |
public final void writeFloat(float v) | 写入浮点型值 |
public final void writeDouble(double v) | 写入双精度浮点型值 |
public final void writeUTF(String str) | 写入UTF-8编码的字符串 |
public void flush() | 刷新缓冲区 |
public void close() | 关闭流并释放资源 |
数据流体系
|
+-------------------+
| |
DataInputStream DataOutputStream
| |
读取基本数据类型 写入基本数据类型
| |
处理二进制格式 处理二进制格式
使用特点
- 必须按照写入顺序读取数据,否则会得到错误的结果
- 可以方便地处理各种Java基本数据类型,而不用手动进行类型转换
- 读写的数据是二进制格式,而非文本格式
- 通常用于网络传输或数据持久化需要保存Java数据类型的场景
数据流可以方便地读写Java基本数据类型和字符串。
// 写入数据
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream("data.dat"))) {
// 写入各种数据类型
dos.writeInt(100);
dos.writeDouble(3.14);
dos.writeBoolean(true);
dos.writeUTF("Hello, DataStream!"); // 写入UTF-8编码的字符串
} catch (IOException e) {
e.printStackTrace();
}
// 读取数据 - 必须按写入顺序读取
try (DataInputStream dis = new DataInputStream(
new FileInputStream("data.dat"))) {
// 按写入顺序读取各种数据类型
int i = dis.readInt();
double d = dis.readDouble();
boolean b = dis.readBoolean();
String s = dis.readUTF();
System.out.println(i + ", " + d + ", " + b + ", " + s);
} catch (IOException e) {
e.printStackTrace();
}
4.5 对象流 - 实现Java对象序列化和反序列化
对象流/序列化体系
|
+------------+------------+
| |
ObjectInputStream ObjectOutputStream
| |
(反序列化) (序列化)
| |
读取对象 写入对象
| |
readObject() writeObject()
4.5.1 序列化和反序列化基础
序列化:将Java对象转换成字节序列的过程,便于存储或网络传输 反序列化:将字节序列恢复为Java对象的过程
4.5.2 序列化的使用条件
- 必须实现Serializable接口
- Serializable是一个标记接口,无需实现任何方法
- 不实现此接口会抛出
NotSerializableException
异常
// 可序列化的类定义示例
public class Person implements Serializable {
// 建议显式定义序列化版本号
private static final long serialVersionUID = 1L;
private String name;
private int age;
// transient关键字标记的字段不会被序列化
private transient String password;
// 构造方法、getter和setter方法略
}
4.5.3 序列化版本号(serialVersionUID)
- 作用:确保序列化与反序列化的类版本一致
- 问题:如果不指定,JVM会根据类结构自动生成,修改类后会变化
- 后果:类结构变化但未指定版本号,反序列化时会抛出
InvalidClassException
// 正确指定序列化版本号
private static final long serialVersionUID = 1L;
4.5.4 常用API
ObjectOutputStream 常用方法
方法 | 描述 |
---|---|
public ObjectOutputStream(OutputStream out) | 创建对象输出流 |
public final void writeObject(Object obj) | 将对象序列化并写入流 |
public void writeBoolean(boolean val) | 写入布尔值 |
public void writeInt(int val) | 写入整型值 |
public void writeUTF(String str) | 写入UTF-8编码的字符串 |
public void flush() | 刷新缓冲区 |
public void close() | 关闭流并释放资源 |
ObjectInputStream 常用方法
方法 | 描述 |
---|---|
public ObjectInputStream(InputStream in) | 创建对象输入流 |
public final Object readObject() | 读取并反序列化对象(需要强制类型转换) |
public boolean readBoolean() | 读取布尔值 |
public int readInt() | 读取整型值 |
public String readUTF() | 读取UTF-8编码的字符串 |
public void close() | 关闭流并释放资源 |
4.5.5 对象序列化示例
// 序列化对象到文件
public static void serializeObject() {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("person.dat"))) {
// 创建对象
Person person = new Person("张三", 25);
// 序列化对象
oos.writeObject(person);
System.out.println("对象已序列化到文件");
} catch (IOException e) {
e.printStackTrace();
}
}
// 从文件反序列化对象
public static void deserializeObject() {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("person.dat"))) {
// 反序列化对象 (需要强制类型转换)
Person person = (Person) ois.readObject();
// 使用反序列化后的对象
System.out.println("姓名: " + person.getName());
System.out.println("年龄: " + person.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
4.5.6 序列化的核心细节
- transient关键字阻止字段序列化
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
// 敏感信息不序列化
private transient String password;
// 反序列化后password字段值为null
}
- 静态字段不参与序列化
public class Configuration implements Serializable {
// 静态字段不会被序列化,因为它们属于类而非对象
private static String appName = "MyApp";
// 实例字段会被序列化
private String userPreference;
}
- 父类字段序列化条件
// 父类必须实现Serializable,否则其字段不会被序列化
public class Person implements Serializable {
protected String name;
}
// 子类会自动序列化从父类继承的字段
public class Student extends Person {
private int studentId;
}
- 自定义序列化行为
public class Account implements Serializable {
private String accountNumber;
private transient double balance; // 不想直接序列化余额
// 自定义序列化方法
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 先调用默认的序列化
// 对敏感数据进行加密后序列化
out.writeDouble(balance * 1.5); // 简单示例:存储加密后的余额
}
// 自定义反序列化方法
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 先调用默认的反序列化
// 读取并解密数据
balance = in.readDouble() / 1.5; // 简单示例:解密余额
}
}
4.5.7 序列化常见问题与解决方案
-
NotSerializableException
- 问题: 类未实现Serializable接口
- 解决: 让类实现Serializable接口
-
InvalidClassException
- 问题: 类结构变化导致serialVersionUID不匹配
- 解决: 显式定义serialVersionUID保持一致
-
序列化安全问题
- 问题: 敏感数据被序列化存储
- 解决: 使用transient修饰敏感字段或自定义序列化方法
-
父类不可序列化
- 问题: 父类没有实现Serializable
- 解决: 让父类实现Serializable或在子类自定义序列化方法
-
序列化对象中引用了不可序列化对象
- 问题: 类包含不可序列化的引用对象
- 解决: 使引用对象实现Serializable或将该引用标记为transient
对象流用于序列化和反序列化Java对象。
// 首先,确保需要序列化的类实现Serializable接口
class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// 构造函数、getter和setter略
}
// 序列化对象
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("person.obj"))) {
Person person = new Person("张三", 25);
oos.writeObject(person); // 将对象序列化到文件
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化对象
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("person.obj"))) {
Person person = (Person) ois.readObject(); // 从文件读取并反序列化对象
System.out.println(person.getName() + ", " + person.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
4.6 转换流 - 连接字节流和字符流的桥梁
转换流原理结构
|
+----------+----------+
| |
InputStreamReader OutputStreamWriter
| |
字节流 → 字符流 字符流 → 字节流
| |
数据源 目的地
(FileInputStream) (FileOutputStream)
4.6.1 转换流的作用
-
作用1:指定字符集读写
- 可以指定任意字符集进行读写,解决中文乱码问题
- 常用于处理非UTF-8编码的文本文件
-
作用2:字节流和字符流之间的桥梁
- 让字节流能够使用字符流中的方法
- 解决跨平台字符编码不一致的问题
常用API
InputStreamReader 常用方法
方法 | 描述 |
---|---|
public InputStreamReader(InputStream in) | 使用默认字符编码创建 InputStreamReader 对象,将字节流转换为字符流 |
public InputStreamReader(InputStream in, String charsetName) | 使用指定的字符编码 charsetName 创建 InputStreamReader 对象,把字节流转换为字符流 |
public int read() | 从输入流读取单个字符,返回字符的 Unicode 码值,若到达流末尾返回 -1 |
public int read(char[] cbuf, int offset, int length) | 从输入流中将最多 length 个字符读入到字符数组 cbuf 中,从偏移量 offset 处开始存储,返回读取的字符数,到达流末尾时返回 -1 |
public void close() | 关闭流并释放与之关联的资源 |
OutputStreamWriter 常用方法
方法 | 描述 |
---|---|
public OutputStreamWriter(OutputStream out) | 使用默认字符编码创建 OutputStreamWriter 对象,将字符流连接到字节流 |
public OutputStreamWriter(OutputStream out, String charsetName) | 使用指定的字符编码 charsetName 创建 OutputStreamWriter 对象,把字符流连接到字节流 |
public void write(int c) | 写入单个字符 |
public void write(char[] cbuf, int off, int len) | 将字符数组 cbuf 中从偏移量 off 开始的 len 个字符写入流中 |
public void write(String str, int off, int len) | 将字符串 str 中从偏移量 off 开始的 len 个字符写入流中 |
public void flush() | 刷新该流的缓冲,确保缓冲中的字符都被写入到底层输出流 |
public void close() | 关闭流并释放与之关联的资源,在关闭前会先调用 flush () 方法 |
4.6.2 InputStreamReader使用示例
// 使用指定字符集读取文件(例如GBK编码的文件)
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream("gbkfile.txt"), "GBK")) {
// 读取单个字符
int c;
while ((c = isr.read()) != -1) {
System.out.print((char)c);
}
// 也可以使用字符数组批量读取
/*
char[] buffer = new char[1024];
int len;
while ((len = isr.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
*/
} catch (IOException e) {
e.printStackTrace();
}
4.6.3 OutputStreamWriter使用示例
// 使用指定字符集写入文件
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("output.txt"), "UTF-8")) {
// 写入中文内容
osw.write("这是使用OutputStreamWriter写入的中文");
// 写入多行内容
osw.write("\r\n第二行内容");
osw.write("\r\n第三行内容");
// 刷新缓冲区确保写入
osw.flush();
} catch (IOException e) {
e.printStackTrace();
}
4.6.4 新手常见问题与细节
-
编码与解码必须一致
- 读文件时的编码必须与文件实际编码一致
- 否则会出现乱码,尤其是中文、日文等非ASCII字符
-
平台默认编码差异
- Windows中文系统默认GBK/GB2312
- Linux/Mac系统默认UTF-8
- 不同JDK版本默认编码可能不同
-
字符集命名规范
- JDK支持的字符集命名:UTF-8, GBK, ISO-8859-1等
- Java 7+可使用StandardCharsets常量:StandardCharsets.UTF_8
-
BOM标记问题
- 某些UTF-8文件包含BOM标记(EF BB BF),可能导致文件开头出现乱码
- 解决方法:跳过BOM或使用专门处理BOM的库
-
JDK11+推荐写法
// JDK11+更简洁的文件读写(自动处理字符集) String content = Files.readString(Path.of("test.txt"), StandardCharsets.UTF_8); Files.writeString(Path.of("output.txt"), content, StandardCharsets.UTF_8);
实际案例:跨平台文件编码转换器
/**
* 文件编码转换器
* 将一个编码的文本文件转换为另一个编码
*/
public static void convertFileEncoding(String sourceFile, String targetFile,
String fromEncoding, String toEncoding) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(sourceFile), fromEncoding));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(targetFile), toEncoding))) {
// 逐行读取并写入
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine(); // 写入行分隔符
}
// 确保所有数据写入
writer.flush();
System.out.println("文件编码已从 " + fromEncoding + " 转换为 " + toEncoding);
}
}
转换流用于字节流和字符流之间的转换,可以指定字符编码。
```java
// 将字节输入流转换为字符输入流,并指定字符编码
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream("text.txt"), "UTF-8")) {
char[] buffer = new char[1024];
int charsRead;
while ((charsRead = isr.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, charsRead));
}
} catch (IOException e) {
e.printStackTrace();
}
// 将字符输出流转换为字节输出流,并指定字符编码
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("output.txt"), "UTF-8")) {
osw.write("这是使用UTF-8编码写入的中文文本");
osw.flush();
} catch (IOException e) {
e.printStackTrace();
}
4.7 打印流 - 方便输出各种数据类型的工具流
打印流体系
|
+------------+------------+
| |
PrintStream PrintWriter
| |
基于字节输出流 基于字符输出流
| |
System.out/err 文本处理更便捷
| |
不处理异常,更安全 不处理异常,更安全
4.7.1 打印流的特点
-
有字节打印流和字符打印流两种
- PrintStream(字节打印流):操作目的地,不操作数据源
- PrintWriter(字符打印流):操作目的地,不操作数据源
-
打印流的共同特点
- 不会抛出IOException,内部会捕获异常
- 都有自动刷新功能,调用println方法会自动刷新
- 可以方便地打印各种数据类型
- 是输出流,不能读取数据
4.7.2 常用API - PrintStream
方法 | 描述 |
---|---|
public PrintStream(OutputStream out) | 创建打印输出流 |
public PrintStream(String fileName) | 创建输出到指定文件的打印流 |
public PrintStream(File file) | 创建输出到指定File对象的打印流 |
public PrintStream(OutputStream out, boolean autoFlush) | 创建打印流,可设置自动刷新 |
public void print(boolean b) | 打印布尔值 |
public void print(int i) | 打印整数 |
public void print(Object obj) | 打印对象(调用toString()) |
public void println() | 打印换行符 |
public void println(String x) | 打印字符串后跟换行符 |
public PrintStream printf(String format, Object... args) | 格式化打印 |
public boolean checkError() | 检查是否有错误发生 |
public void close() | 关闭流并释放资源 |
4.7.3 常用API - PrintWriter
方法 | 描述 |
---|---|
public PrintWriter(Writer out) | 创建字符打印输出流 |
public PrintWriter(OutputStream out) | 基于字节流创建字符打印流 |
public PrintWriter(String fileName) | 创建输出到指定文件的打印流 |
public PrintWriter(File file) | 创建输出到指定File对象的打印流 |
public PrintWriter(Writer out, boolean autoFlush) | 创建打印流,可设置自动刷新 |
public void print(String s) | 打印字符串 |
public void print(Object obj) | 打印对象(调用toString()) |
public void println(String x) | 打印字符串后跟换行符 |
public PrintWriter printf(String format, Object... args) | 格式化打印 |
public PrintWriter format(String format, Object... args) | 格式化输出,与printf相似 |
public boolean checkError() | 检查是否有错误发生 |
public void close() | 关闭流并释放资源 |
4.7.4 PrintStream使用示例
// 1. 创建打印流
try (PrintStream ps = new PrintStream("log.txt")) {
// 2. 打印不同类型的数据
ps.println("这是一行文本"); // 打印字符串并换行
ps.println(100); // 打印整数并换行
ps.println(3.14); // 打印浮点数并换行
ps.println(true); // 打印布尔值并换行
// 3. 格式化打印
ps.printf("姓名: %s, 年龄: %d, 成绩: %.1f%n", "张三", 18, 92.5);
// 4. 打印对象
Date now = new Date();
ps.println(now); // 调用对象的toString()方法
// 5. 不换行打印
ps.print("ABC");
ps.print("DEF"); // 输出结果为ABCDEF
System.out.println("数据已写入log.txt文件");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
4.7.5 PrintWriter使用示例
// 1. 创建字符打印流
try (PrintWriter pw = new PrintWriter(new FileWriter("report.txt"), true)) { // true表示自动刷新
// 2. 打印内容
pw.println("==== 系统报告 ====");
pw.println("生成时间: " + new Date());
// 3. 格式化打印表格
pw.println("\n---- 用户信息 ----");
pw.printf("%-10s %-8s %-8s%n", "用户名", "年龄", "级别");
pw.printf("%-10s %-8d %-8s%n", "张三", 18, "普通用户");
pw.printf("%-10s %-8d %-8s%n", "李四", 24, "管理员");
// 4. 使用format方法(与printf功能相同)
pw.format("当前系统内存使用率: %.2f%%%n", 78.55);
System.out.println("报告已生成到report.txt文件");
} catch (IOException e) {
e.printStackTrace();
}
4.7.6 System.out与System.err
System类中预定义了两个打印流对象:
// 标准输出流(控制台输出)
public static final PrintStream out;
// 标准错误流(控制台错误输出)
public static final PrintStream err;
可以重定向这些流:
// 重定向标准输出
PrintStream originalOut = System.out;
try {
// 将标准输出重定向到文件
PrintStream fileOut = new PrintStream(new FileOutputStream("output.log"));
System.setOut(fileOut);
// 现在System.out的输出会写入文件
System.out.println("这条消息将写入文件而不是控制台");
// 完成后恢复原始输出
System.setOut(originalOut);
System.out.println("回到控制台输出");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
4.7.7 打印流与其他流的区别
-
异常处理
- 普通流的写入方法会抛出IOException
- 打印流的写入方法不抛出异常,使用checkError()检查错误
-
格式化能力
- 普通流只能写入字节/字符
- 打印流可以格式化输出各种数据类型
-
自动刷新
- 普通流需要手动调用flush()
- 打印流可以设置自动刷新模式
-
易用性
- 普通流API相对底层
- 打印流提供了更易用的高级API
4.7.8 自动刷新机制
PrintWriter的自动刷新必须同时满足两个条件:
- 构造方法中启用了自动刷新(传入true)
- 调用了println(), printf()或format()方法(注意:print()方法不会自动刷新)
// 启用自动刷新的PrintWriter
PrintWriter pw = new PrintWriter(new FileWriter("auto-flush.txt"), true);
pw.print("不会自动刷新"); // 不会触发刷新
pw.println("会自动刷新"); // 会触发刷新
pw.printf("也会%s", "自动刷新"); // 会触发刷新
``` |
自动刷新支持 自动刷新支持
打印流的特点
- 不会抛出IOException:内部捕获异常,可通过
checkError()
方法查看是否有错误 - 自动刷新:构造时可设置自动刷新模式,每次调用println、printf或format方法后自动刷新
- 格式化输出:支持类似C语言的printf格式化功能
- 永不阻塞:写入方法从不抛出IOException
常用的格式化打印
PrintWriter pw = new PrintWriter(new FileWriter("log.txt"));
// 格式化日期
pw.printf("当前日期:%tF%n", new Date()); // 2023-06-01
// 格式化数字
pw.printf("浮点数:%.2f%n", 3.14159); // 浮点数:3.14
// 表格式输出
pw.printf("%-10s%-8s%-8s%n", "姓名", "年龄", "成绩"); // 左对齐
pw.printf("%-10s%-8d%-8.1f%n", "张三", 18, 92.5);
pw.printf("%-10s%-8d%-8.1f%n", "李四", 17, 87.5);
打印流提供了方便的打印功能,可以轻松输出各种数据类型。
// 文件打印流
try (PrintStream ps = new PrintStream("log.txt")) {
// 打印各种数据
ps.print("String");
ps.println(100); // 打印并换行
ps.printf("浮点数: %.2f%n", 3.14159); // 格式化打印
// 重定向标准输出
System.setOut(ps);
System.out.println("这条信息将输出到log.txt文件");
} catch (IOException e) {
e.printStackTrace();
}
// 字符打印流
try (PrintWriter pw = new PrintWriter("report.txt", "UTF-8")) {
pw.println("PrintWriter支持指定字符编码");
pw.printf("当前日期: %tF%n", new Date()); // 格式化日期
} catch (IOException e) {
e.printStackTrace();
}
5. 实际应用场景
Java IO应用场景
|
+-------------------+
| | |
文件操作 配置文件 数据处理
| | |
复制/移动 读/写 序列化
删除/重命名 格式转换
5.1 文件复制
文件复制是IO流的基础应用,可以使用字节流高效复制任何类型的文件。
public static void copyFile(String sourcePath, String targetPath) throws IOException {
try (InputStream in = new BufferedInputStream(new FileInputStream(sourcePath));
OutputStream out = new BufferedOutputStream(new FileOutputStream(targetPath))) {
byte[] buffer = new byte[8192]; // 8KB缓冲区
int bytesRead;
// 循环读取和写入数据
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
// 确保所有数据写入目标文件
out.flush();
}
}
5.2 配置文件读写 - 处理应用程序配置
配置文件处理
|
+------------+------------+
| | |
Properties文件 XML配置 JSON配置
| | |
键值对格式 结构化数据 轻量级数据
5.2.1 Properties文件处理
Properties类是Java中专门用于处理.properties
配置文件的工具类。
Properties常用API
方法 | 描述 |
---|---|
public void load(InputStream inStream) | 从输入流加载属性 |
public void load(Reader reader) | 从字符输入流加载属性 |
public void store(OutputStream out, String comments) | 将属性保存到输出流 |
public void store(Writer writer, String comments) | 将属性保存到字符输出流 |
public String getProperty(String key) | 获取指定键的属性值 |
public String getProperty(String key, String defaultValue) | 获取属性值,若不存在返回默认值 |
public void setProperty(String key, String value) | 设置属性值 |
public Set<String> stringPropertyNames() | 获取所有属性名 |
典型用法
// 读取配置文件
public static Properties loadConfig(String filePath) throws IOException {
Properties props = new Properties();
try (FileInputStream fis = new FileInputStream(filePath)) {
props.load(fis);
}
return props;
}
// 使用示例
try {
Properties config = loadConfig("app.properties");
// 读取配置
String dbUrl = config.getProperty("database.url");
String username = config.getProperty("database.username");
// 提供默认值
int timeout = Integer.parseInt(config.getProperty("database.timeout", "30"));
System.out.println("数据库连接: " + dbUrl);
// 修改配置
config.setProperty("database.timeout", "60");
// 保存配置
try (FileOutputStream fos = new FileOutputStream("app.properties")) {
config.store(fos, "Updated database configuration");
}
} catch (IOException e) {
System.err.println("配置文件处理错误: " + e.getMessage());
}
5.2.2 Properties文件常见问题
-
文件编码问题:
.properties
文件默认使用ISO-8859-1编码,中文需要转义// 使用UTF-8读取包含中文的配置 try (InputStreamReader reader = new InputStreamReader( new FileInputStream("config.properties"), "UTF-8")) { props.load(reader); }
-
配置路径问题:路径可能因运行环境而异
// 从类路径加载配置 try (InputStream in = Main.class.getClassLoader().getResourceAsStream("config.properties")) { if (in != null) { props.load(in); } }
-
配置热更新:如何实现不重启应用更新配置
// 简单的定时重载配置示例 ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); executor.scheduleAtFixedRate(() -> { try { reloadConfig(); } catch (Exception e) { e.printStackTrace(); } }, 1, 1, TimeUnit.MINUTES);
-
多级配置合并:开发、测试、生产环境配置管理
// 合并多个配置文件 Properties baseProps = loadConfig("base.properties"); Properties envProps = loadConfig(env + ".properties"); // 环境特定配置覆盖基础配置 for (String key : envProps.stringPropertyNames()) { baseProps.setProperty(key, envProps.getProperty(key)); }
5.3 数据序列化应用 - 对象持久化与传输
序列化应用场景
|
+-----------+-----------+
| | |
对象持久化 网络传输 深拷贝
| | |
保存到文件 RMI/网络协议 克隆对象
5.3.1 对象序列化保存示例
/**
* 保存用户对象到文件
*/
public static void saveUsers(List<User> users, String filePath) {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream(filePath)))) {
// 首先写入用户数量
oos.writeInt(users.size());
// 逐个序列化用户对象
for (User user : users) {
oos.writeObject(user);
}
System.out.println("成功保存 " + users.size() + " 个用户到 " + filePath);
} catch (IOException e) {
System.err.println("保存用户数据失败: " + e.getMessage());
e.printStackTrace();
}
}
5.3.2 对象反序列化读取示例
/**
* 从文件读取用户对象列表
*/
public static List<User> loadUsers(String filePath) {
List<User> users = new ArrayList<>();
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream(filePath)))) {
// 读取用户数量
int count = ois.readInt();
// 逐个反序列化用户对象
for (int i = 0; i < count; i++) {
User user = (User) ois.readObject();
users.add(user);
}
System.out.println("成功加载 " + users.size() + " 个用户从 " + filePath);
} catch (IOException | ClassNotFoundException e) {
System.err.println("加载用户数据失败: " + e.getMessage());
e.printStackTrace();
}
return users;
}
5.3.3 序列化版本控制实例
/**
* 带版本控制的用户类
*/
public class User implements Serializable {
// 显式定义序列化版本号
private static final long serialVersionUID = 1L;
private String username;
private String email;
// 新增字段 (v2)
private Date registrationDate;
// 敏感字段不序列化
private transient String password;
// 自定义序列化方法
private void writeObject(ObjectOutputStream out) throws IOException {
// 调用默认序列化
out.defaultWriteObject();
// 写入额外数据(如版本号)
out.writeInt(2); // 当前类版本为2
}
// 自定义反序列化方法
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 调用默认反序列化
in.defaultReadObject();
// 读取版本号
int version = in.readInt();
// 根据版本处理兼容性
if (version == 1) {
// v1版本没有registrationDate字段,设置一个默认值
this.registrationDate = new Date();
}
// 重新计算/初始化transient字段
this.password = ""; // 安全起见,不恢复密码
}
}
5.3.4 序列化在网络传输中的应用
// 客户端发送序列化对象
public void sendObject(Socket socket, Serializable obj) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream())) {
oos.writeObject(obj);
oos.flush();
}
}
// 服务端接收序列化对象
public Object receiveObject(Socket socket) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(socket.getInputStream())) {
return ois.readObject();
}
}
5.3.5 使用序列化实现对象深拷贝
/**
* 通过序列化实现深拷贝
*/
public static <T extends Serializable> T deepCopy(T obj) {
try {
// 将对象写入字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
// 从字节数组读取对象副本
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
@SuppressWarnings("unchecked")
T copy = (T) ois.readObject();
ois.close();
return copy;
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("深拷贝失败", e);
}
}
5.4 实际应用案例 - 日志记录器
下面展示一个使用多种IO流实现的简单日志记录器:
/**
* 一个简单的日志记录器实现
*/
public class SimpleLogger {
private final PrintWriter logWriter;
private final boolean autoFlush;
private final String dateFormat = "yyyy-MM-dd HH:mm:ss.SSS";
/**
* 创建日志记录器
* @param logFile 日志文件路径
* @param append 是否追加模式
* @param autoFlush 是否自动刷新
*/
public SimpleLogger(String logFile, boolean append, boolean autoFlush) throws IOException {
// 创建日志目录
File file = new File(logFile);
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
// 创建带缓冲的PrintWriter
this.logWriter = new PrintWriter(
new BufferedWriter(
new FileWriter(logFile, append)),
autoFlush);
this.autoFlush = autoFlush;
}
/**
* 记录一条日志
*/
public void log(String level, String message) {
// 获取当前时间
SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
String timestamp = sdf.format(new Date());
// 格式化日志消息
logWriter.printf("%s [%s] %s%n", timestamp, level, message);
// 如果不是自动刷新,则手动刷新
if (!autoFlush) {
logWriter.flush();
}
}
/**
* 记录普通信息
*/
public void info(String message) {
log("INFO", message);
}
/**
* 记录警告信息
*/
public void warn(String message) {
log("WARN", message);
}
/**
* 记录错误信息
*/
public void error(String message) {
log("ERROR", message);
}
/**
* 记录异常信息
*/
public void error(String message, Throwable t) {
log("ERROR", message);
// 使用StringWriter捕获堆栈信息
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
t.printStackTrace(pw);
log("TRACE", sw.toString());
}
/**
* 关闭日志记录器
*/
public void close() {
if (logWriter != null) {
logWriter.close();
}
}
// 使用示例
public static void main(String[] args) {
try {
SimpleLogger logger = new SimpleLogger("logs/app.log", true, true);
logger.info("应用启动");
logger.warn("配置文件不存在,使用默认配置");
try {
// 模拟异常
int result = 10 / 0;
} catch (Exception e) {
logger.error("计算过程中发生异常", e);
}
logger.info("应用退出");
logger.close();
} catch (IOException e) {
System.err.println("无法创建日志记录器: " + e.getMessage());
}
}
}
6. 注意事项和最佳实践
IO操作最佳实践
|
+-------------------------+
| | |
资源管理 性能优化 安全防护
| | |
自动关闭 使用缓冲 异常处理
避免泄漏 批量操作 权限检查
合适的缓冲区 编码指定
6.1 资源管理
- 始终关闭IO流:未关闭的流会导致资源泄漏。优先使用try-with-resources语法自动关闭资源。
- 关闭最外层的流:当使用装饰者模式嵌套多个流时,只需关闭最外层的流,它会级联关闭内部所有流。
// 推荐:使用try-with-resources自动关闭资源
try (InputStream in = new FileInputStream("file.txt")) {
// 使用流
}
// 不推荐:手动关闭容易在异常情况下泄露资源
InputStream in = null;
try {
in = new FileInputStream("file.txt");
// 使用流
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// 处理关闭异常
}
}
}
6.2 性能优化
- 使用缓冲流:BufferedInputStream/BufferedOutputStream可以显著提高IO性能。
- 选择合适的缓冲区大小:对于大文件,使用8KB~16KB的缓冲区通常能获得最佳性能。
- 减少IO操作次数:批量读写比单字节读写效率高得多。
- 使用NIO的内存映射文件:处理超大文件时,使用内存映射更高效。
// 高效的文件复制实现
public static void fastCopy(File source, File target) throws IOException {
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(source), 16384);
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(target), 16384)) {
byte[] buffer = new byte[16384]; // 16KB缓冲区
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
}
6.3 异常处理
- 具体捕获IO异常:尽量捕获具体的异常类型,如FileNotFoundException,而不是笼统的IOException。
- 不忽略关闭异常:关闭资源时的异常可能表明数据未完全写入。
- 保留原始异常信息:使用异常链(exception chaining)保留原始异常信息。
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
// 使用流
} catch (FileNotFoundException e) {
System.err.println("找不到指定文件: " + e.getMessage());
// 处理文件未找到的情况
} catch (IOException e) {
System.err.println("IO操作失败: " + e.getMessage());
// 处理其他IO异常
}
6.4 字符编码
- 显式指定字符编码:避免依赖平台默认编码,总是明确指定编码(如UTF-8)。
- 统一应用中的编码:整个应用使用一致的字符编码,避免乱码问题。
- 处理BOM标记:某些UTF-8文件可能包含BOM标记,可能导致问题。
// 推荐:显式指定字符编码
Reader reader = new InputStreamReader(new FileInputStream("text.txt"), StandardCharsets.UTF_8);
Writer writer = new OutputStreamWriter(new FileOutputStream("output.txt"), StandardCharsets.UTF_8);
// 不推荐:依赖平台默认编码,可能导致跨平台问题
Reader badReader = new InputStreamReader(new FileInputStream("text.txt"));
6.5 安全最佳实践
- 验证用户输入的路径:防止目录遍历攻击。
- 限制文件操作权限:使用Java的安全管理器限制IO操作。
- 避免在临时目录中存储敏感信息:临时文件可能被其他用户访问。
- 使用安全的序列化机制:普通序列化存在安全风险,考虑JSON或其他安全替代方案。
// 验证文件路径是否安全
public static boolean isPathSafe(String path) {
File file = new File(path).getCanonicalFile();
File safeRoot = new File("/safe/directory").getCanonicalFile();
// 确保文件在安全目录下
return file.toPath().startsWith(safeRoot.toPath());
}
6.6 资源关闭顺序
- 先打开后关闭:后打开的流应该先关闭
- 从外到内关闭:外层流关闭时会自动关闭内层流
- 先写入后关闭:关闭前确保所有数据都已写入
// 资源关闭顺序示例
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
// 先打开FileInputStream
fis = new FileInputStream("file.txt");
// 后打开BufferedInputStream
bis = new BufferedInputStream(fis);
// 使用流...
} finally {
// 先关闭后打开的流
if (bis != null) {
try {
bis.close(); // 这里会自动关闭fis
} catch (IOException e) {
// 处理异常
}
}
}
6.7 flush与close的区别
- flush:刷新缓冲区,将数据写入底层设备,但流仍然可用
- close:关闭流,释放资源,之后不能再使用该流
- close自动flush:大多数流的close()方法会自动调用flush()
// flush用法
FileWriter writer = new FileWriter("data.txt");
writer.write("一些数据");
writer.flush(); // 确保数据写入文件
writer.write("更多数据"); // 流仍然可用
// close用法
writer.close(); // 自动flush并关闭流
// writer.write("错误!"); // 错误!流已关闭
7. 与相关技术的对比
IO技术对比
|
+----------------+----------------+
| | |
Java IO Java NIO 工具库
| | |
流式API 通道和缓冲 Commons IO
阻塞IO 非阻塞IO Guava
简单直观 高级功能 Hutool
7.1 Java IO流与NIO对比
特性 | 传统IO (java.io) | NIO (java.nio) |
---|---|---|
设计模式 | 流式,阻塞 | 通道和缓冲区,可非阻塞 |
处理方式 | 一次一个字节/字符 | 基于缓冲区块处理 |
适用场景 | 简单顺序访问,内容较小 | 高并发,大文件,需要随机访问 |
API复杂度 | 简单直观 | 较复杂 |
缓冲特性 | 需要显式使用BufferedInputStream等 | 内置缓冲功能 |
异步能力 | 不支持 | 支持异步IO |
文件锁定 | 不支持 | 支持 |
内存映射文件 | 不支持 | 支持 |
// 传统IO读取文件
try (FileInputStream fis = new FileInputStream("data.txt")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// 处理数据
}
}
// NIO读取文件
try (FileChannel channel = FileChannel.open(Paths.get("data.txt"), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (channel.read(buffer) != -1) {
buffer.flip(); // 切换到读模式
// 从buffer读取数据
buffer.clear(); // 准备下一次写入
}
}
7.2 Java IO与Apache Commons IO对比
特性 | Java标准IO | Apache Commons IO |
---|---|---|
API设计 | 较底层 | 高级封装,更加便捷 |
代码量 | 较多 | 显著减少 |
功能特性 | 基础功能 | 增强功能(如文件监控、文件过滤等) |
依赖性 | 无外部依赖 | 需要引入第三方库 |
维护成本 | 只依赖JDK | 需跟踪第三方库更新 |
// 标准Java IO复制文件
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
// Apache Commons IO复制文件
File srcFile = new File("source.txt");
File destFile = new File("destination.txt");
FileUtils.copyFile(srcFile, destFile);
7.3 其他常用工具库对比
工具库 | 主要特点 | 适用场景 |
---|---|---|
Apache Commons IO | 简化常见IO操作,提供实用工具类 | 简单高效的日常IO操作 |
Guava IO | 与Google Guava集合库无缝集成 | 需要与Guava其他功能集成的项目 |
Hutool | 全面的IO工具类,国产开源工具集 | 需要中文文档和全面功能支持的项目 |
FastJSON | 高性能JSON处理 | 需要高性能JSON序列化/反序列化 |
Jackson | 功能全面的JSON处理库 | 企业级应用,需要全面JSON功能 |
Hutool工具包示例
// 读取文件内容
String content = FileUtil.readUtf8String(new File("test.txt"));
// 写入文件
FileUtil.writeUtf8String("这是测试内容", "output.txt");
// 复制文件
FileUtil.copy(new File("source.txt"), new File("dest.txt"), true);
// 创建多级目录
FileUtil.mkdir("path/to/directory");
// 文件类型判断
boolean isImg = FileTypeUtil.isImage(new File("test.jpg"));
Guava工具包示例
// 读取文本文件内容为字符串
String content = Files.asCharSource(new File("test.txt"), Charsets.UTF_8).read();
// 将字符串写入文件
Files.asCharSink(new File("output.txt"), Charsets.UTF_8).write("这是测试内容");
// 复制文件
Files.copy(new File("source.txt"), new File("dest.txt"));
// 递归遍历目录中的所有.java文件
Iterable<File> javaFiles = Files.fileTraverser()
.breadthFirst(new File("src"))
.filter(f -> f.getName().endsWith(".java"));
8. 面试常见问题
Q1: Java中的IO流分为哪几种?主要特点是什么?
A: Java IO流主要分为字节流和字符流两大类:
- 字节流:以字节为单位处理数据,适合处理任何类型的数据,包括二进制文件。核心抽象类是
InputStream
和OutputStream
。 - 字符流:以字符为单位处理数据,内部进行了字符编码/解码,适合处理文本数据。核心抽象类是
Reader
和Writer
。
每种流又可以根据功能和用途细分为文件流、缓冲流、数据流、对象流等多种类型。
Q2: 为什么要使用缓冲流?它是如何提高性能的?
A: 缓冲流通过减少系统底层的IO操作次数来提高性能。具体工作原理:
- 读取数据时,缓冲流一次性从底层流读取大量数据到内存缓冲区,后续读取直接从缓冲区获取,减少磁盘访问
- 写入数据时,先写入内存缓冲区,当缓冲区满或刷新时才真正写入底层流,将多次零散写入合并为一次批量写入
性能提升尤其明显在进行小批量、频繁的读写操作时。在文件复制等场景中,使用缓冲流可以提升5-10倍甚至更高的性能。
Q3: 序列化和反序列化是什么?如何实现?有哪些注意事项?
A: 序列化是将Java对象转换为字节序列的过程,反序列化是将字节序列恢复为Java对象的过程。
实现方式:
- 类需要实现
Serializable
接口(这是一个标记接口,没有方法需要实现) - 使用
ObjectOutputStream
的writeObject()
方法将对象序列化 - 使用
ObjectInputStream
的readObject()
方法将字节反序列化为对象
注意事项:
static
和transient
修饰的成员变量不会被序列化- 为保证版本兼容性,建议手动定义
serialVersionUID
- 所有引用的对象也必须是可序列化的
- 反序列化不调用构造方法,但会调用最近的非序列化父类的无参构造方法
- 注意安全风险,序列化可能导致敏感数据泄露
Q4: Java中如何实现文件复制?请给出不同性能级别的实现。
A: 文件复制有多种实现方式,性能从低到高:
- 基础字节流:逐字节复制,性能最差
try (FileInputStream in = new FileInputStream(source);
FileOutputStream out = new FileOutputStream(target)) {
int c;
while ((c = in.read()) != -1) {
out.write(c);
}
}
- 带缓冲的字节数组:使用字节数组作为缓冲,性能有明显提升
try (FileInputStream in = new FileInputStream(source);
FileOutputStream out = new FileOutputStream(target)) {
byte[] buffer = new byte[8192];
int length;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
}