Java IO 流
Java IO 流:字节流、字符流、缓冲流一网打尽
学习日期:2026-06-16 难度:⭐⭐⭐⭐ 核心
前言
IO(Input/Output)流是 Java 中处理数据输入输出的核心机制。读文件、写文件、网络传输、控制台交互……凡是涉及数据流动的地方,都有 IO 流的身影。
IO 流类很多、体系复杂,初学者容易绕晕。这篇把 IO 流的核心概念、常用流类、典型用法和常见坑点一次说清楚。
一、IO 流总览
1.1 什么是"流"
流(Stream)是一组有顺序的、有起点和终点的字节集合,是对数据传输的抽象。就像水管里的水一样,数据从一端流向另一端。
1.2 流的分类
表格
| 分类角度 | 类型 | 说明 |
|---|---|---|
| 流向 | 输入流(Input) | 从外部读到内存(读文件、读网络) |
| 输出流(Output) | 从内存写到外部(写文件、发数据) | |
| 数据单元 | 字节流 | 以字节为单位,8 位,可处理任何数据(文本、图片、视频等) |
| 字符流 | 以字符为单位,16 位,专门处理文本(自动处理编码) | |
| 功能 | 节点流 | 直接连接数据源(FileInputStream、FileReader) |
| 处理流(装饰器) | 包装节点流,提供更强大的功能(BufferedReader、ObjectOutputStream) |
1.3 四大基类
所有流都继承自以下四个抽象基类:
plaintext
9
1
2
3
4
输入流 输出流
字节流 InputStream OutputStream
字符流 Reader Writer
💡 命名规律:字节流以 Stream 结尾,字符输入流以 Reader 结尾,字符输出流以 Writer 结尾。
二、File 类
在讲流之前,先认识一下 File 类——它表示文件或目录的路径信息,不负责文件内容的读写。
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 创建 File 对象
File file = new File("test.txt"); // 相对路径
File file2 = new File("D:/docs", "test.txt"); // 父路径 + 子路径
File file3 = new File(new File("D:/docs"), "test.txt");
// 判断方法
file.exists(); // 是否存在
file.isFile(); // 是否是文件
file.isDirectory(); // 是否是目录
file.isHidden(); // 是否隐藏
file.canRead(); // 是否可读
file.canWrite(); // 是否可写
// 获取信息
file.getName(); // 文件名
file.getPath(); // 路径名
file.getAbsolutePath(); // 绝对路径
file.length(); // 文件大小(字节)
file.lastModified(); // 最后修改时间(毫秒)
// 创建和删除
file.createNewFile(); // 创建文件(文件已存在返回false)
file.mkdir(); // 创建单级目录
file.mkdirs(); // 创建多级目录
file.delete(); // 删除文件/空目录
// 目录遍历
File dir = new File("D:/docs");
String[] names = dir.list(); // 获取子文件/目录名数组
File[] files = dir.listFiles(); // 获取子文件/目录File对象数组
// 带过滤器的遍历(只拿 .txt 文件)
File[] txtFiles = dir.listFiles(f -> f.getName().endsWith(".txt"));
⚠️ 常见坑:
delete()只能删除空目录或文件,非空目录删不掉listFiles()如果路径不存在或不是目录,返回null,要做非空判断- 路径分隔符建议用
File.separator代替硬编码的/或\,保证跨平台
三、字节流
3.1 文件字节流:FileInputStream / FileOutputStream
最基础的文件字节流,直接操作字节。
文件读取:
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 方式一:单个字节读(效率低,不推荐)
try (FileInputStream fis = new FileInputStream("test.txt")) {
int data;
while ((data = fis.read()) != -1) { // read() 返回 -1 表示读完
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
// 方式二:字节数组读(推荐!效率高)
try (FileInputStream fis = new FileInputStream("test.txt")) {
byte[] buffer = new byte[1024]; // 1KB 缓冲区
int len;
while ((len = fis.read(buffer)) != -1) { // len 是实际读到的字节数
String str = new String(buffer, 0, len);
System.out.print(str);
}
} catch (IOException e) {
e.printStackTrace();
}
文件写入:
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 覆盖写入(每次写都会清空原有内容)
try (FileOutputStream fos = new FileOutputStream("test.txt")) {
fos.write("Hello Java IO!".getBytes());
fos.write('\n');
byte[] data = "你好".getBytes();
fos.write(data, 0, data.length); // 写数组的一部分
fos.flush(); // 刷新缓冲区,强制写出
} catch (IOException e) {
e.printStackTrace();
}
// 追加写入(第二个参数 true 表示追加)
try (FileOutputStream fos = new FileOutputStream("test.txt", true)) {
fos.write("追加内容".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
⚠️ 重要:用完流一定要
close()释放资源!上面的写法用了 try-with-resources(Java 7+),会自动关闭流,不用手动写 finally。
3.2 文件拷贝实战
public class FileCopy {
public static void main(String[] args) {
String src = "source.jpg";
String dest = "copy.jpg";
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192]; // 8KB 缓冲区
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
System.out.println("拷贝完成!");
} catch (IOException e) {
System.err.println("拷贝失败:" + e.getMessage());
}
}
}
3.3 字节缓冲流:BufferedInputStream / BufferedOutputStream
内部有一个缓冲区数组(默认 8KB),读写时先操作缓冲区,减少 IO 次数,大幅提升性能。
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("src.mp4"));
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("dest.mp4"))) {
byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
💡 装饰器模式:缓冲流是处理流,需要包装一个节点流。它不改变流的本质,只是在外面套了一层缓冲功能。
3.4 数据流:DataInputStream / DataOutputStream
可以直接读写基本数据类型和字符串,不用自己转字节。
// 写入
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream("data.dat"))) {
dos.writeInt(100);
dos.writeDouble(3.14);
dos.writeBoolean(true);
dos.writeUTF("你好"); // 写入字符串(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();
}
3.5 对象流:ObjectInputStream / ObjectOutputStream
可以把 Java 对象直接写入文件或从文件读取,这就是序列化和反序列化。
序列化的前提:对象所属的类必须实现 Serializable 接口(标记接口,没有方法)。
// 实体类必须实现 Serializable
class Person implements Serializable {
// 序列化版本号(建议显式声明,避免类修改后反序列化失败)
private static final long serialVersionUID = 1L;
private String name;
private int age;
// transient 关键字:修饰的属性不参与序列化
private transient String password;
// 构造方法、getter/setter 省略
}
// 序列化(写对象到文件)
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("person.dat"))) {
oos.writeObject(new Person("张三", 20, "123456"));
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化(从文件读对象)
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("person.dat"))) {
Person p = (Person) ois.readObject();
System.out.println(p.getName() + " " + p.getAge());
// password 是 transient,反序列化后为 null
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
⚠️ 序列化注意事项:
- 要序列化的类必须实现
Serializable接口- 类的所有属性也必须是可序列化的(基本类型和 String 默认可以)
transient修饰的属性不参与序列化,反序列化后是默认值- 建议显式声明
serialVersionUID,否则类修改后自动生成的 ID 会变,导致反序列化失败- 序列化保存的是对象的状态,不保存类的方法
四、字符流
字节流处理文本时可能会有乱码问题(因为一个字符可能占多个字节),字符流专门用来处理文本。
4.1 字符编码
- ASCII:英文用 1 个字节
- GBK:中文 2 字节,英文 1 字节,Windows 默认
- UTF-8:中文 3 字节,英文 1 字节,互联网通用标准
- UTF-16:所有字符都是 2 字节(Java char 用的就是这个)
💡 乱码的本质:编码和解码用的字符集不一致。
4.2 文件字符流:FileReader / FileWriter
底层还是字节流,只是自动帮你做了字符集转换。默认使用平台字符集(中文 Windows 是 GBK)。
// 读取文件
try (FileReader 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();
}
// 写入文件
try (FileWriter fw = new FileWriter("test.txt")) {
fw.write("Hello 你好");
fw.write('\n');
fw.append("追加内容"); // append 和 write 效果差不多
fw.flush();
} catch (IOException e) {
e.printStackTrace();
}
4.3 字符缓冲流:BufferedReader / BufferedWriter
字符缓冲流除了有缓冲功能,还提供了按行读写的便利方法。
// 按行读取
try (BufferedReader br = new BufferedReader(
new FileReader("test.txt"))) {
String line;
while ((line = br.readLine()) != null) { // 读一行,返回 null 表示读完
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
// 按行写入
try (BufferedWriter bw = new BufferedWriter(
new FileWriter("test.txt"))) {
bw.write("第一行");
bw.newLine(); // 换行(跨平台,比直接写 \n 好)
bw.write("第二行");
bw.newLine();
} catch (IOException e) {
e.printStackTrace();
}
💡 小技巧:Java 11+ 新增了更简单的方式:
java
9
1
2
3
4
5
6
7
8
9
// 读取整个文件为字符串
String content = Files.readString(Path.of("test.txt"), StandardCharsets.UTF_8);
// 读取所有行
List<String> lines = Files.readAllLines(Path.of("test.txt"));
// 写字符串到文件
Files.writeString(Path.of("test.txt"), "Hello World");
4.4 转换流:InputStreamReader / OutputStreamWriter
字符流和字节流之间的桥梁,可以指定字符集。
// 从文件读取,指定 UTF-8 编码
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("test.txt"),
StandardCharsets.UTF_8))) { // 指定编码
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
// 写入文件,指定 GBK 编码
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("gbk.txt"),
Charset.forName("GBK"))) {
osw.write("你好");
} catch (IOException e) {
e.printStackTrace();
}
⚠️ FileReader/FileWriter 的局限:它们只能用默认字符集,要指定编码必须用转换流。
五、标准输入输出流
Java 提供了三个标准流:
表格
| 变量 | 类型 | 说明 |
|---|---|---|
System.in |
InputStream | 标准输入流,默认从键盘输入 |
System.out |
PrintStream | 标准输出流,默认输出到控制台 |
System.err |
PrintStream | 标准错误流,默认输出到控制台(红色) |
从控制台读取输入
// 方式一:用 Scanner(最方便,推荐)
Scanner scanner = new Scanner(System.in);
System.out.print("请输入姓名:");
String name = scanner.nextLine(); // 读一行
System.out.print("请输入年龄:");
int age = scanner.nextInt(); // 读整数
System.out.println(name + " " + age);
scanner.close();
// 方式二:用 BufferedReader(传统写法)
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
String s = br.readLine();
// 方式三:System.in 直接读(太底层,麻烦)
int b = System.in.read(); // 只读一个字节
六、打印流:PrintStream / PrintWriter
打印流只负责输出,功能很强大:可以打印各种类型、自动刷新、永不抛 IOException。
// PrintStream(字节打印流)
try (PrintStream ps = new PrintStream(
new FileOutputStream("log.txt"), true)) { // true=自动刷新
ps.println("Hello");
ps.println(123);
ps.println(3.14);
ps.printf("Name: %s, Age: %d%n", "张三", 20); // 格式化输出
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// PrintWriter(字符打印流,更常用)
try (PrintWriter pw = new PrintWriter(
new FileWriter("output.txt"), true)) {
pw.println("第一行");
pw.printf("数值:%d%n", 100);
} catch (IOException e) {
e.printStackTrace();
}
💡
System.out就是一个 PrintStream。
七、常用 IO 流选择指南
表格
| 需求 | 推荐流 |
|---|---|
| 读取文本文件 | BufferedReader + FileReader / InputStreamReader |
| 写入文本文件 | BufferedWriter + FileWriter / OutputStreamWriter |
| 读取二进制文件(图片、视频、音频) | BufferedInputStream + FileInputStream |
| 写入二进制文件 | BufferedOutputStream + FileOutputStream |
| 读写基本数据类型 | DataInputStream / DataOutputStream |
| 读写 Java 对象 | ObjectInputStream / ObjectOutputStream |
| 按行读文本 | BufferedReader.readLine() |
| 格式化输出文本 | PrintWriter / PrintStream |
| 需要指定字符编码 | InputStreamReader / OutputStreamWriter |
| 简单文件读写(Java 11+) | Files.readString() / Files.writeString() |
八、实战练习
练习1:文件拷贝工具类
/**
* 文件拷贝工具类(支持任意文件类型)
*/
public class FileCopyUtils {
private static final int BUFFER_SIZE = 8192; // 8KB
/**
* 拷贝文件
* @param srcPath 源文件路径
* @param destPath 目标文件路径
* @return 是否拷贝成功
*/
public static boolean copyFile(String srcPath, String destPath) {
File src = new File(srcPath);
if (!src.exists() || !src.isFile()) {
System.err.println("源文件不存在");
return false;
}
// 确保目标目录存在
File dest = new File(destPath);
File parentDir = dest.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(src));
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(dest))) {
byte[] buffer = new byte[BUFFER_SIZE];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return true;
} catch (IOException e) {
System.err.println("拷贝异常:" + e.getMessage());
return false;
}
}
public static void main(String[] args) {
boolean success = copyFile("source.jpg", "backup/copy.jpg");
System.out.println(success ? "拷贝成功" : "拷贝失败");
}
}
练习2:统计 Java 文件的代码行数
public class CodeLineCounter {
public static int countLines(String filePath) {
int count = 0;
try (BufferedReader br = new BufferedReader(
new FileReader(filePath))) {
while (br.readLine() != null) {
count++;
}
} catch (IOException e) {
e.printStackTrace();
}
return count;
}
// 统计目录下所有 Java 文件的总行数
public static int countLinesInDir(String dirPath) {
int total = 0;
File dir = new File(dirPath);
if (!dir.exists() || !dir.isDirectory()) {
return 0;
}
File[] files = dir.listFiles(f ->
f.isDirectory() || f.getName().endsWith(".java"));
if (files == null) return 0;
for (File file : files) {
if (file.isFile()) {
int lines = countLines(file.getAbsolutePath());
System.out.println(file.getName() + ": " + lines + " 行");
total += lines;
} else {
total += countLinesInDir(file.getAbsolutePath());
}
}
return total;
}
public static void main(String[] args) {
int total = countLinesInDir("src");
System.out.println("总代码行数:" + total);
}
}
练习3:用户登录系统(对象序列化)
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() { return username; }
public boolean checkPassword(String pwd) {
return password.equals(pwd);
}
}
public class UserSystem {
private static final String DATA_FILE = "users.dat";
private Map<String, User> users = new HashMap<>();
// 注册
public boolean register(String username, String password) {
if (users.containsKey(username)) {
return false;
}
users.put(username, new User(username, password));
saveUsers();
return true;
}
// 登录
public boolean login(String username, String password) {
User user = users.get(username);
return user != null && user.checkPassword(password);
}
// 保存用户数据到文件
private void saveUsers() {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(DATA_FILE))) {
oos.writeObject(users);
} catch (IOException e) {
e.printStackTrace();
}
}
// 从文件加载用户数据
@SuppressWarnings("unchecked")
private void loadUsers() {
File file = new File(DATA_FILE);
if (!file.exists()) return;
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(file))) {
users = (Map<String, User>) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
UserSystem system = new UserSystem();
system.loadUsers();
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("1.注册 2.登录 3.退出");
int choice = sc.nextInt();
sc.nextLine();
if (choice == 3) break;
System.out.print("用户名:");
String username = sc.nextLine();
System.out.print("密码:");
String password = sc.nextLine();
if (choice == 1) {
if (system.register(username, password)) {
System.out.println("注册成功!");
} else {
System.out.println("用户名已存在!");
}
} else if (choice == 2) {
if (system.login(username, password)) {
System.out.println("登录成功!欢迎 " + username);
} else {
System.out.println("用户名或密码错误!");
}
}
}
sc.close();
}
}
九、总结速查表
流的分类速记
表格
| 类型 | 输入 | 输出 | 缓冲区版本 |
|---|---|---|---|
| 字节(文件) | FileInputStream | FileOutputStream | BufferedInputStream / BufferedOutputStream |
| 字符(文件) | FileReader | FileWriter | BufferedReader / BufferedWriter |
| 字节数组 | ByteArrayInputStream | ByteArrayOutputStream | - |
| 字符数组 | CharArrayReader | CharArrayWriter | - |
| 字符串 | StringReader | StringWriter | - |
| 管道 | PipedInputStream | PipedOutputStream | - |
| 数据 | DataInputStream | DataOutputStream | - |
| 对象 | ObjectInputStream | ObjectOutputStream | - |
| 打印 | - | PrintStream / PrintWriter | - |
| 转换 | InputStreamReader | OutputStreamWriter | - |
编码习惯
- 优先用缓冲流:BufferedXxx 比裸流快很多
- 用 try-with-resources:自动关闭流,不用写 finally
- 文本用字符流,二进制用字节流:别搞混
- 指定字符集:避免平台默认编码导致的乱码
- read() 返回 -1 表示结束:这是约定,记住它
- 用完 flush + close:尤其是输出流,避免数据留在缓冲区没写入
常见问题
表格
| 问题 | 原因 | 解决 |
|---|---|---|
| 中文乱码 | 编码解码字符集不一致 | 统一用 UTF-8,用转换流指定编码 |
| FileNotFoundException | 文件路径错了,或权限不够 | 检查路径、文件是否存在、权限 |
| 反序列化失败 | serialVersionUID 不匹配 | 显式声明 serialVersionUID |
| 大文件内存溢出 | 一次性把整个文件读进内存 | 用缓冲流分批次读写 |
| 文件夹删不掉 | 目录非空 | 先递归删子文件,再删目录 |
IO 流是 Java 的基础但重要的内容,记住"四大基类 + 装饰器模式 + 缓冲流提升性能"这条主线,再多写几个练习,很快就能熟练掌握。
更多推荐
所有评论(0)