《深入探索 Java IO 流进阶:缓冲流、转换流、序列化与工具类引言》
目录
一、缓冲流:提升 IO 效率的利器
1.1 缓冲流概述
1.2 字节缓冲流
构造方法
效率测试对比
1.3 字符缓冲流
构造方法
特有功能方法
1.4 实战练习:文本排序
二、转换流:解决字符编码问题
2.1 字符编码与字符集基础
2.2 编码问题的产生
2.3 InputStreamReader:字节到字符的桥梁
构造方法
示例:指定编码读取
2.4 OutputStreamWriter:字符到字节的桥梁
构造方法
示例:指定编码写出
2.5 实战练习:文件编码转换
三、序列化流:对象的持久化存储
3.1 序列化概述
3.2 ObjectOutputStream:对象序列化
构造方法
序列化条件
示例:序列化对象
3.3 ObjectInputStream:对象反序列化
构造方法
示例:反序列化对象
序列化版本号的重要性
3.4 实战练习:序列化集合
在 Java 编程中,IO(输入 / 输出)操作是与外部设备进行数据交互的核心环节。前文中我们学习了基础的 File 流操作,但在实际开发中,面对高效读写、编码转换、对象持久化等复杂需求,基础流就显得力不从心了。本文将系统讲解 Java IO 流的进阶知识,包括缓冲流、转换流、序列化流、打印流、压缩 / 解压缩流以及实用的 IO 工具包,帮助开发者掌握更强大的 IO 操作技能。
一、缓冲流:提升 IO 效率的利器
1.1 缓冲流概述
缓冲流(也称为高效流)是对 4 个基本 File 流(FileInputStream
、FileOutputStream
、FileReader
、FileWriter
)的增强。它通过内置缓冲区减少系统 IO 次数,从而显著提高读写效率。缓冲流按照数据类型可分为两类:
- 字节缓冲流:
BufferedInputStream
、BufferedOutputStream
- 字符缓冲流:
BufferedReader
、BufferedWriter
缓冲流的核心原理是在创建流对象时,内置一个默认大小的缓冲区数组(字节缓冲流默认 8KB,字符缓冲流默认 8192 个字符)。当进行读写操作时,数据先在缓冲区中暂存,当缓冲区满或手动刷新时才与磁盘交互,大大减少了直接操作磁盘的次数。
1.2 字节缓冲流
构造方法
字节缓冲流通过包装基本字节流创建,构造方法如下:
public BufferedInputStream(InputStream in)
:创建缓冲输入流,包装指定的输入流public BufferedOutputStream(OutputStream out)
:创建缓冲输出流,包装指定的输出流
构造示例代码:
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));
效率测试对比
为了直观展示缓冲流的高效性,我们通过复制 375MB 的大文件进行对比测试:
基本流复制(单字节读写):
long start = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream("jdk9.exe");FileOutputStream fos = new FileOutputStream("copy.exe")) {int b;while ((b = fis.read()) != -1) {fos.write(b);}
} catch (IOException e) {e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("普通流复制时间:" + (end - start) + " 毫秒"); // 耗时极长(通常十几分钟)
缓冲流复制(单字节读写):
long start = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("jdk9.exe"));BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.exe"))) {int b;while ((b = bis.read()) != -1) {bos.write(b);}
} catch (IOException e) {e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("缓冲流复制时间:" + (end - start) + " 毫秒"); // 约8016毫秒
缓冲流 + 数组复制(最优方案):
long start = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("jdk9.exe"));BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.exe"))) {int len;byte[] bytes = new byte[8 * 1024]; // 8KB缓冲区while ((len = bis.read(bytes)) != -1) {bos.write(bytes, 0, len);}
} catch (IOException e) {e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("缓冲流使用数组复制时间:" + (end - start) + " 毫秒"); // 约666毫秒
效率分析:单字节读写时,缓冲流因减少了 IO 次数而远快于基本流;而缓冲流结合数组读写时,通过批量处理数据进一步提升效率,是大文件复制的推荐方案。
1.3 字符缓冲流
构造方法
字符缓冲流通过包装基本字符流创建,构造方法如下:
public BufferedReader(Reader in)
:创建缓冲字符输入流,包装指定的字符输入流public BufferedWriter(Writer out)
:创建缓冲字符输出流,包装指定的字符输出流
构造示例代码:
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));
特有功能方法
字符缓冲流在继承基本字符流方法的基础上,提供了更便捷的特有方法:
-
BufferedReader
的readLine()
方法:- 功能:读取一行文字(不包含换行符)
- 返回值:读取到的行内容,若到达流末尾则返回
null
示例代码:
BufferedReader br = new BufferedReader(new FileReader("in.txt")); String line = null; while ((line = br.readLine()) != null) { // 循环读取每行内容System.out.println(line); // 处理每行数据 } br.close();
-
BufferedWriter
的newLine()
方法:- 功能:写入一个平台无关的换行符(Windows 为
\r\n
,Linux 为\n
) - 优势:无需手动拼接换行符,保证跨平台兼容性
示例代码:
BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt")); bw.write("Hello"); bw.newLine(); // 写入换行 bw.write("World"); bw.close(); // 关闭时自动刷新缓冲区
- 功能:写入一个平台无关的换行符(Windows 为
1.4 实战练习:文本排序
需求:将乱序的文本按行首数字恢复顺序(如 "3.xxx"、"8.xxx" 需排序为 "1.xxx"、"2.xxx"...)。
实现思路:
- 使用
BufferedReader
逐行读取文本 - 将读取的内容存储到
ArrayList
集合 - 使用
Collections.sort()
结合自定义比较器排序 - 通过
BufferedWriter
按顺序写出排序后的内容
代码实现:
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;public class TextSortDemo {public static void main(String[] args) throws IOException {// 1.创建集合存储文本行ArrayList<String> list = new ArrayList<>();// 2.创建缓冲流对象BufferedReader br = new BufferedReader(new FileReader("in.txt"));BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));// 3.读取文本行并存储String line;while ((line = br.readLine()) != null) {list.add(line);}// 4.自定义排序规则(按行首数字升序)Collections.sort(list, new Comparator<String>() {@Overridepublic int compare(String o1, String o2) {// 提取行首数字(如"3.xxx"提取"3")int num1 = Integer.parseInt(o1.split("\\.")[0]);int num2 = Integer.parseInt(o2.split("\\.")[0]);return num1 - num2; // 升序排序}});// 5.写出排序后的内容for (String s : list) {bw.write(s);bw.newLine(); // 换行}// 6.释放资源bw.close();br.close();}
}
二、转换流:解决字符编码问题
2.1 字符编码与字符集基础
计算机中所有数据都以二进制存储,字符编码是字符与二进制的对应规则,而字符集(Charset)是系统支持的所有字符的集合。常见字符集及编码:
字符集 | 编码方式 | 特点 |
---|---|---|
ASCII | 单字节编码 | 仅支持英文字符和控制字符,共 128 个字符 |
ISO-8859-1 | 单字节编码 | 支持欧洲语言,兼容 ASCII,共 256 个字符 |
GB2312 | 双字节编码 | 支持简体中文,收录约 7000 汉字 |
GBK | 双字节编码 | 扩展 GB2312,支持繁体、日韩汉字,共 21003 个汉字 |
Unicode | UTF-8/UTF-16 | 支持全球所有字符,UTF-8 为变长编码(1-4 字节),互联网首选编码 |
编码与解码:
- 编码:字符 → 字节(如 "中"→
0xE4B8AD
(UTF-8)或0xD6D0
(GBK)) - 解码:字节 → 字符(需使用与编码相同的规则,否则会乱码)
2.2 编码问题的产生
当使用FileReader
读取 Windows 系统默认 GBK 编码的文件时,若 IDEA 默认编码为 UTF-8,会因解码规则不匹配导致乱码:
FileReader fr = new FileReader("gbk.txt"); // gbk.txt为GBK编码的"大家好"
int c;
while ((c = fr.read()) != null) {System.out.print((char) c); // 输出乱码:���
}
2.3 InputStreamReader:字节到字符的桥梁
InputStreamReader
是字节流到字符流的转换流,可指定字符集解码字节数据。
构造方法
InputStreamReader(InputStream in)
:使用默认字符集(通常为 UTF-8)InputStreamReader(InputStream in, String charsetName)
:使用指定字符集(如 "GBK")
示例:指定编码读取
public class EncodingDemo {public static void main(String[] args) throws IOException {String fileName = "gbk.txt"; // GBK编码的文件// 使用默认编码(UTF-8)读取GBK文件,乱码InputStreamReader isrUtf8 = new InputStreamReader(new FileInputStream(fileName));// 使用GBK编码读取,正常解码InputStreamReader isrGbk = new InputStreamReader(new FileInputStream(fileName), "GBK");// 测试默认编码读取int c;System.out.println("默认编码读取:");while ((c = isrUtf8.read()) != -1) {System.out.print((char) c); // 乱码}// 测试指定编码读取System.out.println("\nGBK编码读取:");while ((c = isrGbk.read()) != -1) {System.out.print((char) c); // 正常输出:大家好}isrUtf8.close();isrGbk.close();}
}
2.4 OutputStreamWriter:字符到字节的桥梁
OutputStreamWriter
是字符流到字节流的转换流,可指定字符集编码字符数据。
构造方法
OutputStreamWriter(OutputStream out)
:使用默认字符集OutputStreamWriter(OutputStream out, String charsetName)
:使用指定字符集
示例:指定编码写出
public class EncodingWriteDemo {public static void main(String[] args) throws IOException {// 使用UTF-8编码写出(默认)OutputStreamWriter oswUtf8 = new OutputStreamWriter(new FileOutputStream("utf8.txt"));oswUtf8.write("你好"); // "你好"在UTF-8中占6字节oswUtf8.close();// 使用GBK编码写出OutputStreamWriter oswGbk = new OutputStreamWriter(new FileOutputStream("gbk.txt"), "GBK");oswGbk.write("你好"); // "你好"在GBK中占4字节oswGbk.close();}
}
2.5 实战练习:文件编码转换
需求:将 GBK 编码的文本文件转换为 UTF-8 编码。
实现思路:
- 使用
InputStreamReader
指定 GBK 编码读取源文件 - 使用
OutputStreamWriter
默认 UTF-8 编码写出到目标文件
代码实现:
import java.io.*;public class CodeConvertDemo {public static void main(String[] args) throws IOException {String srcFile = "source_gbk.txt"; // 源GBK文件String destFile = "target_utf8.txt"; // 目标UTF-8文件// 创建转换流InputStreamReader isr = new InputStreamReader(new FileInputStream(srcFile), "GBK");OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(destFile));// 读写转换char[] cbuf = new char[1024];int len;while ((len = isr.read(cbuf)) != -1) {osw.write(cbuf, 0, len);}// 释放资源osw.close();isr.close();}
}
三、序列化流:对象的持久化存储
3.1 序列化概述
序列化是将对象转换为字节序列的过程,用于对象的持久化存储或网络传输;反序列化则是将字节序列恢复为对象的过程。Java 通过ObjectOutputStream
和ObjectInputStream
实现序列化功能。
3.2 ObjectOutputStream:对象序列化
ObjectOutputStream
用于将对象写入输出流,实现对象的持久化。
构造方法
public ObjectOutputStream(OutputStream out)
:创建序列化流,包装输出流
序列化条件
- 类必须实现
java.io.Serializable
接口(标记接口,无抽象方法) - 类的所有属性必须可序列化(基本类型默认可序列化,引用类型需同样实现
Serializable
) - 无需序列化的属性可使用
transient
关键字修饰
示例:序列化对象
import java.io.*;// 可序列化的Employee类
class Employee implements Serializable {// 序列版本号(保证序列化与反序列化版本一致)private static final long serialVersionUID = 1L;public String name;public String address;public transient int age; // transient修饰的属性不参与序列化public Employee(String name, String address, int age) {this.name = name;this.address = address;this.age = age;}
}public class SerializeDemo {public static void main(String[] args) throws IOException {Employee emp = new Employee("张三", "北京", 30);// 创建序列化流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("emp.ser"));oos.writeObject(emp); // 序列化对象oos.close();System.out.println("对象已序列化");}
}
3.3 ObjectInputStream:对象反序列化
ObjectInputStream
用于从输入流读取字节序列,恢复为对象。
构造方法
public ObjectInputStream(InputStream in)
:创建反序列化流,包装输入流
示例:反序列化对象
import java.io.*;public class DeserializeDemo {public static void main(String[] args) throws IOException, ClassNotFoundException {Employee emp = null;// 创建反序列化流ObjectInputStream ois = new ObjectInputStream(new FileInputStream("emp.ser"));emp = (Employee) ois.readObject(); // 反序列化对象ois.close();// 输出反序列化结果System.out.println("姓名:" + emp.name); // 张三System.out.println("地址:" + emp.address); // 北京System.out.println("年龄:" + emp.age); // 0(transient属性未序列化)}
}
序列化版本号的重要性
serialVersionUID
用于验证序列化对象与类的版本一致性:
- 若类未显式定义,JVM 会根据类结构自动生成
- 类结构修改后,自动生成的版本号会变化,导致反序列化失败
- 显式定义后,即使类结构修改(如新增属性),仍可反序列化(新增属性取默认值)
3.4 实战练习:序列化集合
需求:序列化存储多个自定义对象的集合,再反序列化并遍历。
实现思路:
- 创建多个自定义对象(需实现
Serializable
) - 将对象存入
ArrayList
集合 - 序列化集合到文件
- 反序列化集合并遍历输出
代码实现:
import java.io.*;
import java.util.ArrayList;// 学生类(可序列化)
class Student implements Serializable {private static final long serialVersionUID = 1L;private String name;private String pwd;public Student(String name, String pwd) {this.name = name;this.pwd = pwd;}public String getName() { return name; }public String getPwd() { return pwd; }
}public class Serial