Java使用字节流、缓冲流、转换流与对象流读写文件的示例代码
一、Ja va 中有哪些流 在Ja va的世界里,文件操作是绕不开的基础。无论是读取配置文件,还是保存用户数据,都离不开“流”这个概念。简单来说,流就是数据流动的通道。上面这张图,就为我们清晰地展示了Ja va中几种核心流类协作完成文件读写的典型流程。 二、Ja va 中流的分类 要理清Ja va的
一、Ja va 中有哪些流

在Ja va的世界里,文件操作是绕不开的基础。无论是读取配置文件,还是保存用户数据,都离不开“流”这个概念。简单来说,流就是数据流动的通道。上面这张图,就为我们清晰地展示了Ja va中几种核心流类协作完成文件读写的典型流程。
二、Ja va 中流的分类
要理清Ja va的流体系,首先得从分类入手。根据功能角色,流主要分为两大类:
- 节点流:这是数据流的源头或终点,直接与数据源(如文件、内存、网络连接)打交道。你可以把它想象成连接水龙头和水池的那根水管。
- 字节流:以字节为单位进行读写,比如
FileInputStream和FileOutputStream。 - 字符流:以字符为单位进行读写,专门处理文本,比如
FileReader和FileWriter。
- 字节流:以字节为单位进行读写,比如
- 处理流:也叫包装流,它不直接连接数据源,而是“套”在节点流(或其他处理流)之上,提供额外的功能,比如缓冲、转换数据类型等。这就像在水管上加装了一个过滤器或增压泵。
- 缓冲流:如
BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter。 - 转换流:如
InputStreamReader,OutputStreamWriter,用于在字节和字符之间搭桥。 - 对象流:如
ObjectInputStream,ObjectOutputStream,用于序列化与反序列化对象。
- 缓冲流:如
三、Ja va 中流如何使用
了解了分类,接下来看看不同流各自的应用场景和特点,这是选择合适工具的关键。
- 字节流
- 这是最通用的流,可以读写任何类型的文件,因为所有文件在底层都是字节序列。无论是文本、图片、音频还是视频,比如
.docx、.png、.mp4等,字节流都能处理。
- 这是最通用的流,可以读写任何类型的文件,因为所有文件在底层都是字节序列。无论是文本、图片、音频还是视频,比如
- 字符流
- 顾名思义,它专为文本文件设计,比如
.txt、.ja va、.py、.c等源代码或配置文件。 - 这里有个常见的误区:像
.docx、.xlsx、.pptx这类文件,虽然里面包含文字,但它们的格式是复杂的二进制结构,并非纯文本文件,因此不能用字符流直接读取其内容。
- 顾名思义,它专为文本文件设计,比如
- 缓冲流
- 它的核心作用是提升效率。原理是把数据先攒到内存的一块缓冲区里,等缓冲区满了,或者主动刷新时,再一次性写入磁盘或从磁盘读取。
- 如果不使用缓冲流,每次读写几个字节就要和磁盘交互一次,这种频繁的I/O操作非常耗时。缓冲流通过减少物理磁盘的访问次数,显著提高了读写性能。
- 转换流
- 它是字节流和字符流之间的桥梁,核心解决的是字符编码问题。文件里存储的始终是二进制数据,我们需要通过“解码”将其转换成我们能看懂的字符;反之,将字符“编码”成二进制才能存入文件。
- 解码:按照指定的字符集(如UTF-8、GBK)将字节数据转换为字符。
- 编码:按照指定的字符集将字符转换为字节数据。
- 对象流
- 用于实现对象的序列化与反序列化。序列化就是把内存中的Ja va对象转换成可存储或可传输的二进制字节序列;反序列化则是将这个字节序列恢复成内存中的对象。
- 这在需要持久化对象状态(如保存游戏存档)或通过网络传输对象(如RPC调用)时,是不可或缺的技术。
四、使用字节流读写文件


字节流是文件操作的基石。下面是一个典型的文件复制示例,展示了如何使用 FileInputStream 和 FileOutputStream:
public void copyFileWithFile(String srcPath, String destPath) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//1. 创建File对象,指定源文件和目标文件
File srcFile = new File(srcPath);
File destFile = new File(destPath);
//2. 创建节点流
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
//3. 核心读写过程:使用缓冲区循环读取写入
int len;
byte[] buffer = new byte[100];
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4. 关闭资源,确保释放系统资源
try {
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fis != null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
代码逻辑清晰:创建流、循环读写、关闭资源。注意,使用字节数组作为缓冲区(这里是100字节)是标准做法,比单字节读写高效得多。最后在finally块中关闭流是必须的好习惯。
五、使用缓冲流读写文件

在节点流的基础上套上缓冲流,性能往往能得到立竿见影的提升。下面是使用缓冲流进行文件复制的代码:
public void copyFileWithBuffered(String srcPath, String destPath) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
//1. 创建File对象
File srcFile = new File(srcPath);
File destFile = new File(destPath);
//2. 先创建节点流,再在其基础上创建缓冲流
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile);
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
//3. 读写过程(与字节流类似,但操作的是缓冲流)
int len;
byte[] buffer = new byte[100];
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4. 关闭资源:只需关闭外层缓冲流,它会自动关闭内层的节点流
try {
if(bos != null)
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if(bis != null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
// 内层的fis和fos关闭可以省略,因为关闭bos/bis时会自动处理
// fos.close();
// fis.close();
}
}


关键点在于流的嵌套结构:BufferedInputStream 包装了 FileInputStream。关闭时,只需要关闭最外层的缓冲流即可,这是一种标准的装饰器模式应用。
六、字符流只能读写文本文件
字符流,如 FileReader 和 FileWriter,是处理文本文件的利器。但更常见的做法是使用转换流 InputStreamReader 和 OutputStreamWriter,因为它们可以显式指定字符集,避免乱码。下面是一个指定编码进行文本文件复制的例子:
public void test() {
InputStreamReader isr = null;
OutputStreamWriter osw = null;
try {
// 用GBK编码读取源文件
isr = new InputStreamReader(new FileInputStream("康师傅的话.txt"),"gbk");
// 用UTF-8编码写入目标文件
osw = new OutputStreamWriter(new FileOutputStream("C:\Users\shkstart\Desktop\寄语.txt"),"utf-8");
char[] cbuf = new char[1024];
int len;
while ((len = isr.read(cbuf)) != -1) {
osw.write(cbuf, 0, len);
osw.flush(); // 及时刷新缓冲区,确保数据写入
}
System.out.println("文件复制完成");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (isr != null)
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (osw != null)
osw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这段代码实现了编码转换:将一个GBK编码的文本文件,读取并转换为UTF-8编码保存。注意,字符流操作的单位是字符数组 char[]。
七、使用转换流进行解码编码
使用 UTF8 解码文件得到文件内容后使用 GBK 编码文件输出到新的文件中
转换流的核心价值在于处理字符集转换。下面的例子更清晰地展示了如何将一个文件从一种编码转换为另一种编码:
public void test4() {
InputStreamReader isr = null;
OutputStreamWriter osw = null;
try {
//1. 创建文件对象
File file1 = new File("dbcp_gbk.txt");
File file2 = new File("dbcp_gbk_to_utf8.txt");
//2. 创建流,并指定编码
FileInputStream fis = new FileInputStream(file1);
// 解码:用GBK字符集将字节转换为字符(必须与源文件编码一致)
isr = new InputStreamReader(fis,"GBK");
FileOutputStream fos = new FileOutputStream(file2);
// 编码:用UTF-8字符集将字符转换为字节,存入新文件
osw = new OutputStreamWriter(fos,"utf8");
//3. 读写过程
char[] cBuffer = new char[1024];
int len;
while((len = isr.read(cBuffer)) != -1){
osw.write(cBuffer,0,len);
}
System.out.println("操作完成");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//4. 关闭资源
try {
if(osw != null)
osw.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
if(isr != null)
isr.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
这个过程可以概括为:字节流(文件) -> 解码(InputStreamReader) -> 字符数据 -> 编码(OutputStreamWriter) -> 字节流(新文件)。正确指定编码是解决乱码问题的关键。
八、使用对象流序列化反序列化


对象流让存储和恢复Ja va对象变得简单。首先,需要序列化的类必须实现 ja va.io.Serializable 接口。
person 对象
package com.atguigu05.objectstream;
import ja va.io.Serializable;
public class Person implements Serializable { //Serializable:属于一个标识接口
String name;
int age;
int id;
Account acct;
// 显式声明序列化版本UID,强烈推荐,用于控制版本兼容性
static final long serialVersionUID = 422334254234L;
// 构造器、getter/setter、toString方法省略...
// ... (为保持内容完整,此处保留原代码结构)
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
", id=" + id +
", acct=" + acct +
'}';
}
}
class Account implements Serializable{
double balance;
static final long serialVersionUID = 422234L;
// 构造器、toString方法省略...
}
注意 serialVersionUID 的作用。它相当于对象的“版本号”。如果序列化和反序列化时这个值不一致,就会导致 InvalidClassException。显式声明可以避免因类细节(如增加一个方法)改变而导致的意外不兼容。
序列化 person 对象
将对象写入文件:
public void test3(){
ObjectOutputStream oos = null;
try {
File file = new File("object1.dat");
oos = new ObjectOutputStream(new FileOutputStream(file));
//2. 写出对象,即序列化
Person p1 = new Person("Tom",12);
oos.writeObject(p1);
oos.flush(); // 刷新缓冲区
Person p2 = new Person("Jerry",23,1001,new Account(2000));
oos.writeObject(p2);
oos.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
if(oos != null)
oos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
反序列化 person 对象
从文件中读取并恢复对象:
public void test4() {
ObjectInputStream ois = null;
try {
File file = new File("object1.dat");
ois = new ObjectInputStream(new FileInputStream(file));
//2. 读取对象,即反序列化(需强制类型转换)
Person person = (Person) ois.readObject();
System.out.println(person);
Person person1 = (Person) ois.readObject();
System.out.println(person1);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} finally {
try {
if(ois != null)
ois.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
反序列化时,JVM会根据字节流中的类描述信息找到对应的类,并创建对象。如果找不到类,则会抛出 ClassNotFoundException。
至此,我们梳理了Ja va中从基础的字节流、字符流,到提升性能的缓冲流,再到处理编码的转换流,以及用于对象持久化的对象流。理解它们各自的定位和组合方式,是掌握Ja va I/O编程的关键一步。


































