Java IO 流:从基础原理到实战应用
1.1 本章学习目标与重点
- 提示 掌握 IO 流的核心概念与分类,理解字节流与字符流的区别和适用场景。
- 提示 熟练使用字节流完成文件的读取与写入操作,解决文件拷贝等实际问题。
- 提示 掌握字符流的使用方法,处理文本文件的编码与解码问题。
- 提示 了解缓冲流、转换流、对象流等高级 IO 流的原理,提升 IO 操作效率。
- 注意 本章重点是 字节流与字符流的核心用法 和 高级 IO 流的实战应用,这是 JAVA 文件操作的必备技能。
1.2 IO 流核心概念与分类
1.2.1 什么是 IO 流
- IO 流(Input/Output Stream)是 JAVA 中用于处理设备之间数据传输的技术,主要负责数据的读取(Input)和写入(Output)。
常见的 IO 操作包括文件读写、网络通信数据传输等。IO 流的核心思想是以流的方式处理数据,数据像水流一样从一个设备流向另一个设备,实现数据的传输与处理。
1.2.2 IO 流的分类标准
JAVA 中的 IO 流体系庞大,可按照不同标准进行分类,核心分类方式有以下三种:
1. 按数据流向分类
- 输入流:数据从外部设备流向程序,用于读取数据,核心抽象类为
InputStream、Reader。
- 输出流:数据从程序流向外部设备,用于写入数据,核心抽象类为
OutputStream、Writer。
2. 按数据类型分类
- 字节流:以字节(1 字节=8 位)为单位处理数据,可处理任意类型的文件,如文本、图片、视频等,核心抽象类为
InputStream、OutputStream。
- 字符流:以字符(2 字节)为单位处理数据,仅适用于处理文本文件,可直接处理字符编码问题,核心抽象类为
Reader、Writer。
3. 按功能分类
- 节点流:直接与数据源相连,读写数据的基础流,如
FileInputStream、FileReader。
- 处理流:包裹在节点流之上,增强节点流的功能,如缓冲流、转换流、对象流等。
1.2.3 IO 流的核心体系图
IO 流体系 ├── 字节流 │ ├── 输入字节流:InputStream │ │ ├── FileInputStream(文件字节输入流) │ │ ├── BufferedInputStream(缓冲字节输入流) │ │ └── ObjectInputStream(对象字节输入流) │ └── 输出字节流:OutputStream │ ├── FileOutputStream(文件字节输出流) │ ├── BufferedOutputStream(缓冲字节输出流) │ └── ObjectOutputStream(对象字节输出流) └── 字符流 ├── 输入字符流:Reader │ ├── FileReader(文件字符输入流) │ ├── BufferedReader(缓冲字符输入流) │ └── InputStreamReader(转换输入流) └── 输出字符流:Writer ├── FileWriter(文件字符输出流) ├── BufferedWriter(缓冲字符输出流) └── OutputStreamWriter(转换输出流)
✅ 核心结论:字节流可处理所有类型文件,是 IO 流的基础;字符流专门处理文本文件,避免编码问题;处理流需依赖节点流使用,提升 IO 操作效率。
1.3 字节流的核心用法
1.3.1 字节输入流(FileInputStream)
- 提示
FileInputStream 是最常用的字节输入流,用于从文件中读取字节数据,适用于所有类型的文件。
基本使用步骤
- 创建
FileInputStream 对象,绑定数据源文件路径。
- 调用
read() 方法读取文件数据。
- 关闭流资源,释放系统占用。
代码实操 1:单字节读取文件
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamDemo1 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
int readByte;
while ((readByte = fis.read()) != -1) {
System.out.print((char) readByte);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
⚠️ 注意事项:单字节读取效率极低,因为每次都要访问磁盘,适合小文件读取,大文件不推荐使用。
代码实操 2:字节数组读取文件
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamDemo2 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
✅ 核心结论:字节数组读取是推荐的高效读取方式,通过缓冲区减少磁盘 IO 次数,大幅提升读取效率。
1.3.2 字节输出流(FileOutputStream)
- 提示
FileOutputStream 是最常用的字节输出流,用于向文件中写入字节数据,支持覆盖写入和追加写入两种模式。
基本使用步骤
- 创建
FileOutputStream 对象,绑定目标文件路径。
- 调用
write() 方法写入数据。
- 关闭流资源。
代码实操 1:覆盖写入文件
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo1 {
public static void main(String[] args) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream("output.txt");
fos.write(97);
byte[] bytes = "Hello FileOutputStream".getBytes();
fos.write(bytes);
byte[] partBytes = "Java IO Stream".getBytes();
fos.write(partBytes, 0, 4);
System.out.println("文件写入成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
代码实操 2:追加写入文件
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo2 {
public static void main(String[] args) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream("output.txt", true);
fos.write("\r\n".getBytes());
fos.write("这是追加的内容".getBytes());
System.out.println("内容追加成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
⚠️ 注意事项:写入文件时,如果目标文件不存在,FileOutputStream 会自动创建文件;如果父目录不存在,则会抛出 FileNotFoundException。
1.3.3 实战:字节流实现文件拷贝
- 提示 文件拷贝是 IO 流的典型应用场景,通过字节输入流读取源文件数据,再通过字节输出流写入目标文件,可实现任意类型文件的拷贝。
代码实现
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopyDemo {
public static void main(String[] args) {
String sourcePath = "source.jpg";
String targetPath = "target_copy.jpg";
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(sourcePath);
fos = new FileOutputStream(targetPath);
byte[] buffer = new byte[1024 * 8];
int len;
long startTime = System.currentTimeMillis();
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
System.currentTimeMillis();
System.out.println( + (endTime - startTime) + );
} (IOException e) {
e.printStackTrace();
} {
{
(fos != ) fos.close();
(fis != ) fis.close();
} (IOException e) {
e.printStackTrace();
}
}
}
}
✅ 核心结论:字节流拷贝文件的核心是边读边写,通过合理设置缓冲区大小(如 8KB、16KB),可以大幅提升拷贝效率,这是文件拷贝的标准实现方式。
1.4 字符流的核心用法
1.4.1 字符流的设计初衷
- 提示 字节流处理文本文件时,需要手动处理字符编码问题,容易出现乱码。字符流以字符为单位处理数据,内部集成了编码解码逻辑,专门用于处理文本文件,避免乱码问题。
字符流与字节流的核心区别:字节流处理字节,字符流处理字符;字符流底层还是基于字节流实现,只是多了编码解码的步骤。
1.4.2 字符输入流(FileReader)
基本使用步骤
- 创建
FileReader 对象,绑定数据源文本文件。
- 调用
read() 方法读取字符数据。
- 关闭流资源。
代码实操:字符数组读取文本文件
import java.io.FileReader;
import java.io.IOException;
public class FileReaderDemo {
public static void main(String[] args) {
FileReader fr = null;
try {
fr = new FileReader("test.txt");
char[] buffer = new char[1024];
int len;
while ((len = fr.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
1.4.3 字符输出流(FileWriter)
基本使用步骤
- 创建
FileWriter 对象,绑定目标文本文件。
- 调用
write() 方法写入字符数据。
- 调用
flush() 方法刷新缓冲区(可选),关闭流资源。
代码实操:字符流写入文本文件
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterDemo {
public static void main(String[] args) {
FileWriter fw = null;
try {
fw = new FileWriter("text_output.txt", true);
fw.write('J');
char[] chars = {'a', 'v', 'a'};
fw.write(chars);
fw.write(" 字符流写入测试\r\n");
fw.flush();
fw.write("刷新后继续写入的内容");
System.out.println("文本写入成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
⚠️ 注意事项:字符流的 flush() 方法用于将缓冲区数据写入文件,close() 方法会自动调用 flush();字节流没有缓冲区,无需调用 flush()。
1.5 高级 IO 流的实战应用
1.5.1 缓冲流:提升 IO 操作效率
- 提示 缓冲流(
BufferedInputStream/BufferedOutputStream、BufferedReader/BufferedWriter)是处理流的一种,通过在内存中创建缓冲区,减少磁盘 IO 次数,大幅提升读写效率。
缓冲流的核心原理:读取数据时,先将数据读取到缓冲区,后续读取直接从缓冲区获取;写入数据时,先写入缓冲区,缓冲区满或关闭流时再写入磁盘。
代码实操 1:缓冲字节流实现高效文件拷贝
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BufferedFileCopyDemo {
public static void main(String[] args) {
String sourcePath = "large_file.zip";
String targetPath = "large_file_copy.zip";
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(new FileInputStream(sourcePath));
bos = new BufferedOutputStream(new FileOutputStream(targetPath));
byte[] buffer = new byte[1024 * 8];
int len;
long startTime = System.currentTimeMillis();
((len = bis.read(buffer)) != -) {
bos.write(buffer, , len);
}
System.currentTimeMillis();
System.out.println( + (endTime - startTime) + );
} (IOException e) {
e.printStackTrace();
} {
{
(bos != ) bos.close();
(bis != ) bis.close();
} (IOException e) {
e.printStackTrace();
}
}
}
}
代码实操 2:缓冲字符流读取文本文件(按行读取)
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReaderDemo {
public static void main(String[] args) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("test.txt"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
✅ 核心结论:缓冲流是提升 IO 效率的首选方式,处理大文件时优势明显;BufferedReader 的 readLine() 方法是读取文本文件的标准方式。
1.5.2 转换流:处理字符编码问题
- 提示 转换流(
InputStreamReader/OutputStreamWriter)是字节流与字符流之间的桥梁,用于指定字符编码读取或写入文本文件,解决乱码问题。
当系统默认编码与文件编码不一致时,必须使用转换流指定编码,否则会出现乱码。
代码实操:指定编码读取文本文件
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class InputStreamReaderDemo {
public static void main(String[] args) {
InputStreamReader isr = null;
try {
FileInputStream fis = new FileInputStream("utf8_file.txt");
isr = new InputStreamReader(fis, "UTF-8");
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();
} finally {
if (isr != null) {
try {
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
⚠️ 注意事项:转换流的编码参数必须与文件实际编码一致,常见的编码格式有 UTF-8、GBK、GB2312 等。
1.5.3 对象流:实现对象的序列化与反序列化
- 提示 对象流(
ObjectInputStream/ObjectOutputStream)用于将 JAVA 对象转换为字节序列(序列化),或将字节序列恢复为 JAVA 对象(反序列化),实现对象的持久化存储或网络传输。
序列化的前提条件
- 目标对象的类必须实现
Serializable 标记接口(该接口没有任何方法,仅用于标记)。
- 类中的所有属性必须是可序列化的(基本数据类型默认可序列化,引用类型需实现
Serializable)。
- 可通过
transient 关键字修饰属性,使其不参与序列化。
代码实操 1:对象序列化
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private int age;
private transient String password;
public User(String username, int age, String password) {
this.username = username;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return "User{" + "username='" + username + '\'' + ", age=" + age + ", password='" + password + '\'' + '}';
}
}
public class ObjectOutputStreamDemo {
public static void main {
;
{
oos = ( ());
(, , );
oos.writeObject(user);
System.out.println();
} (IOException e) {
e.printStackTrace();
} {
(oos != ) {
{
oos.close();
} (IOException e) {
e.printStackTrace();
}
}
}
}
}
代码实操 2:对象反序列化
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class ObjectInputStreamDemo {
public static void main(String[] args) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("user.obj"));
User user = (User) ois.readObject();
System.out.println("反序列化得到的对象:" + user);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
输出结果
反序列化得到的对象:User{username='zhangsan', age=20, password='null'}
✅ 核心结论:对象流是实现对象持久化的核心技术,serialVersionUID 用于保证类版本一致,transient 关键字可排除不需要序列化的属性。
1.6 IO 流的最佳实践与常见问题
1.6.1 IO 流的最佳实践
- 优先使用缓冲流:缓冲流能大幅提升 IO 效率,处理大文件时必须使用。
- 合理设置缓冲区大小:缓冲区大小建议设置为 1024 的倍数,如 8KB、16KB,过大或过小都会影响效率。
- 及时关闭流资源:流资源使用完毕后必须关闭,释放系统资源,推荐使用
try-with-resources 语法自动关闭流。
- 处理文本文件优先使用字符流:字符流自动处理编码问题,避免乱码;处理非文本文件必须使用字节流。
- 指定编码格式:读取文本文件时,尽量指定编码格式,避免依赖系统默认编码导致乱码。
1.6.2 常见问题与解决方案
❌ 问题 1:文件找不到异常(FileNotFoundException)
- 原因:文件路径错误、父目录不存在、文件权限不足。
- 解决方案:检查文件路径是否正确;创建父目录;确保程序有文件访问权限。
❌ 问题 2:文本文件读取乱码
- 原因:编码格式不匹配。
- 解决方案:使用转换流
InputStreamReader 指定正确的编码格式。
❌ 问题 3:对象反序列化失败(InvalidClassException)
- 原因:序列化和反序列化的类版本不一致、类未实现
Serializable 接口。
- 解决方案:添加
serialVersionUID;确保类实现 Serializable 接口。
❌ 问题 4:流关闭顺序错误
- 原因:先关闭节点流,再关闭处理流,导致处理流关闭时出错。
- 解决方案:关闭流时先关闭处理流,再关闭节点流;或直接关闭处理流,底层节点流会自动关闭。
1.6.3 try-with-resources 语法自动关闭流
- 提示 JDK 7 引入的
try-with-resources 语法,可自动关闭实现了 AutoCloseable 接口的资源,无需手动关闭流,代码更简洁,且能避免资源泄漏。
代码实操
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResourcesDemo {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
✅ 核心结论:try-with-resources 是推荐的流资源管理方式,能有效避免因忘记关闭流导致的资源泄漏问题。
1.7 实战案例:文件内容搜索工具
1.7.1 需求分析
- 提示 实现一个文件内容搜索工具,支持在指定目录下的所有文本文件中搜索指定关键词,并输出包含关键词的文件路径和行内容。
需求如下:
- 支持指定搜索目录和关键词。
- 递归遍历目录下的所有文本文件(
.txt、.java、.xml 等)。
- 输出包含关键词的文件路径、行号和行内容。
- 处理 IO 异常,保证程序健壮性。
1.7.2 代码实现
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class FileContentSearcher {
private static final String[] SUPPORT_EXTENSIONS = {".txt", ".java", ".xml", ".properties"};
public static void search(File dir, String keyword) {
if (!dir.exists() || !dir.isDirectory()) {
System.out.println("目录不存在或不是有效目录!");
return;
}
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
search(file, keyword);
} else {
if (isTextFile(file.getName())) {
searchFileContent(file, keyword);
}
}
}
}
{
(String ext : SUPPORT_EXTENSIONS) {
(fileName.endsWith(ext)) {
;
}
}
;
}
{
( ( (file))) {
String line;
;
((line = br.readLine()) != ) {
lineNum++;
(line.contains(keyword)) {
System.out.println();
System.out.println( + file.getAbsolutePath());
System.out.println( + lineNum);
System.out.println( + line);
}
}
} (IOException e) {
System.out.println( + file.getAbsolutePath());
e.printStackTrace();
}
}
{
;
;
(dirPath);
System.out.println( + keyword + );
search(dir, keyword);
System.out.println();
}
}
1.7.3 案例总结
✅ 这个文件内容搜索工具综合运用了 IO 流的核心知识,核心亮点如下:
- 使用
BufferedReader 按行读取文本文件,保证读取效率。
- 递归遍历目录,实现多级目录下的文件搜索。
- 使用
try-with-resources 自动关闭流,避免资源泄漏。
- 处理了各种异常情况,保证程序的健壮性。
- 可灵活扩展支持的文本文件类型,实用性强。
1.8 本章总结
- IO 流是 JAVA 处理数据传输的核心技术,分为字节流和字符流,字节流处理所有类型文件,字符流专门处理文本文件。
- 字节流的核心用法是文件读写和拷贝,通过字节数组读取可提升效率;字符流可避免编码问题,
readLine() 是读取文本的标准方式。
- 缓冲流通过缓冲区减少磁盘 IO 次数,大幅提升 IO 效率;转换流用于处理编码问题;对象流实现对象的序列化与反序列化。
- IO 流的最佳实践是优先使用缓冲流、及时关闭流资源、指定编码格式、使用
try-with-resources 管理资源。
- 实际开发中,需根据文件类型选择合适的 IO 流,处理异常情况,保证程序的健壮性。