JavaIO流详解
Java IO 流详解
本章位置:Java 核心基础
前置知识:面向对象、异常机制、集合框架
下一章:多线程
学习目标:理解 Java IO 的基本体系,掌握文件与目录操作、字节流、字符流、缓冲流、转换流、对象流以及Path / Files等常用 API,并能够独立完成文本读写、文件复制和目录处理。
一、什么是 IO
IO 是:
Input / Output
也就是:
输入 / 输出
程序运行时,数据不可能永远只存在内存中。
实际开发经常需要:
读取文件
写入文件
上传图片
下载文件
读取配置
保存日志
网络传输
读取数据库返回的数据
这些都属于广义的:
IO 操作
二、什么是输入和输出
判断输入和输出时,要站在:
程序
的角度。
例如:
文件 → Java 程序
表示:
程序读取文件
属于:
Input
输入
而:
Java 程序 → 文件
表示:
程序把数据写入文件
属于:
Output
输出
可以记:
数据进入程序
→ 输入
数据离开程序
→ 输出
三、IO 的常见应用
Java 项目中常见 IO:
读取 txt 文本
读取 properties 配置
上传头像
保存图片
导入 Excel
导出 Excel
生成 PDF
文件下载
日志输出
网络通信
对象序列化
后面学习:
Servlet
SpringBoot
文件上传
网络编程
时都会继续使用 IO。
四、Java IO 的核心分类
传统 Java IO 可以先按照:
数据单位
分成:
字节流
字符流
再按照方向分:
输入流
输出流
组合起来就是:
IO
│
├─ 字节流
│ ├─ InputStream
│ └─ OutputStream
│
└─ 字符流
├─ Reader
└─ Writer
这 4 个类是理解 IO 的基础。
五、四大抽象父类
| 类型 | 输入 | 输出 |
|---|---|---|
| 字节流 | InputStream |
OutputStream |
| 字符流 | Reader |
Writer |
其中:
InputStream
OutputStream
Reader
Writer
都是抽象类。
平时真正使用的是它们的:
具体子类
六、字节流和字符流怎么选
字节流
适合:
任何类型的数据
例如:
图片
视频
音频
PDF
ZIP
Word
Excel
二进制文件
常见:
FileInputStream
FileOutputStream
BufferedInputStream
BufferedOutputStream
字符流
主要适合:
文本数据
例如:
txt
Java 源码
配置文件
JSON 文本
CSV 文本
常见:
FileReader
FileWriter
BufferedReader
BufferedWriter
七、最简单的选择原则
如果处理:
图片
视频
压缩包
PDF
使用:
字节流
如果处理:
纯文本
优先考虑:
字符流
但是字符流最终底层同样需要处理字节和字符编码之间的转换。
八、File 类
在学习流之前,需要先认识:
java.io.File
File 表示:
文件路径
或目录路径
注意:
File 对象不等于文件内容本身。
它更像是:
对某一个文件或目录路径的描述
九、创建 File 对象
例如:
import java.io.File;
File file =
new File(
"D:\\data\\test.txt"
);
Windows 路径中的:
\
在 Java 字符串里需要转义。
所以:
D:\data\test.txt
要写成:
"D:\\data\\test.txt"
十、相对路径和绝对路径
绝对路径
例如:
D:\data\test.txt
包含完整位置。
相对路径
例如:
data/test.txt
它是相对于:
当前程序工作目录
寻找。
十一、不要过度依赖 Windows 反斜杠
Java 可以使用:
/
表示路径:
File file =
new File(
"data/test.txt"
);
跨平台代码中通常更清晰。
现代 Java 还更推荐:
Path
后面会讲。
十二、File 常用方法
常用:
exists()
isFile()
isDirectory()
getName()
getPath()
getAbsolutePath()
length()
mkdir()
mkdirs()
createNewFile()
delete()
listFiles()
十三、exists()
判断路径是否存在:
if (file.exists()) {
System.out.println(
"文件存在"
);
}
十四、isFile() 和 isDirectory()
判断普通文件:
file.isFile();
判断目录:
file.isDirectory();
十五、getName()
例如:
File file =
new File(
"D:/data/test.txt"
);
System.out.println(
file.getName()
);
输出:
test.txt
十六、getAbsolutePath()
获取绝对路径:
System.out.println(
file.getAbsolutePath()
);
在排查:
相对路径找不到文件
时非常有用。
十七、length()
获取文件大小:
long size =
file.length();
单位:
字节
十八、mkdir()
创建单层目录:
File dir =
new File(
"data"
);
dir.mkdir();
十九、mkdirs()
创建多级目录:
File dir =
new File(
"data/upload/avatar"
);
dir.mkdirs();
如果中间目录不存在,也会尝试创建。
实际开发中:
mkdirs()
使用更方便。
二十、createNewFile()
创建空文件:
File file =
new File(
"data/test.txt"
);
file.createNewFile();
这个方法可能抛:
IOException
二十一、delete()
删除:
file.delete();
注意:
删除目录时
通常要求目录为空
另外:
delete()
不会像系统回收站那样帮你恢复。
使用时要谨慎。
二十二、listFiles()
获取目录下的文件:
File dir =
new File(
"data"
);
File[] files =
dir.listFiles();
遍历:
if (files != null) {
for (File item : files) {
System.out.println(
item.getName()
);
}
}
二十三、递归遍历目录
例如:
public static void printFiles(
File file
) {
if (file == null
|| !file.exists()) {
return;
}
if (file.isFile()) {
System.out.println(
file.getAbsolutePath()
);
return;
}
File[] files =
file.listFiles();
if (files == null) {
return;
}
for (File child : files) {
printFiles(child);
}
}
这里用到了:
递归
二十四、InputStream
InputStream 是:
字节输入流
的抽象父类。
常见子类:
FileInputStream
BufferedInputStream
ObjectInputStream
DataInputStream
二十五、OutputStream
OutputStream 是:
字节输出流
抽象父类。
常见:
FileOutputStream
BufferedOutputStream
ObjectOutputStream
DataOutputStream
二十六、FileInputStream
用于:
从文件读取字节
例如:
FileInputStream input =
new FileInputStream(
"data/test.txt"
);
注意:
文件不存在时
可能抛 FileNotFoundException
二十七、read() 一次读一个字节
例如:
int data =
input.read();
为什么返回:
int
而不是:
byte
因为还需要额外表示:
-1
也就是:
已经读到文件末尾
二十八、读取整个文件:一次一个字节
try (
FileInputStream input =
new FileInputStream(
"data/test.txt"
)
) {
int data;
while (
(data = input.read())
!= -1
) {
System.out.print(
(char) data
);
}
}
这种写法:
可以工作
但效率较低
因为每次只读取:
1 个字节
二十九、read(byte[])
更常见:
byte[] buffer =
new byte[1024];
int length =
input.read(buffer);
返回:
本次真正读取到的字节数量
读到末尾:
-1
三十、使用缓冲数组读取文件
try (
FileInputStream input =
new FileInputStream(
"data/test.txt"
)
) {
byte[] buffer =
new byte[1024];
int len;
while (
(len = input.read(buffer))
!= -1
) {
System.out.write(
buffer,
0,
len
);
}
}
三十一、为什么不能每次都输出整个 buffer
错误:
while (
input.read(buffer) != -1
) {
System.out.write(buffer);
}
最后一次读取时:
可能没有填满整个数组
数组剩余区域还保存着上一次的数据。
因此必须记录:
len
然后只处理:
0 ~ len - 1
三十二、FileOutputStream
用于:
向文件写入字节
例如:
FileOutputStream output =
new FileOutputStream(
"data/output.txt"
);
三十三、write(int)
写一个字节:
output.write(65);
对应 ASCII:
A
三十四、write(byte[])
例如:
String text =
"Hello Java";
byte[] bytes =
text.getBytes(
StandardCharsets.UTF_8
);
output.write(bytes);
三十五、覆盖写入和追加写入
默认:
new FileOutputStream(
"data/test.txt"
);
通常会:
覆盖原文件
如果想追加:
new FileOutputStream(
"data/test.txt",
true
);
第二个参数:
true
表示:
append
追加
三十六、字节流复制文件
例如复制图片:
try (
FileInputStream input =
new FileInputStream(
"data/a.jpg"
);
FileOutputStream output =
new FileOutputStream(
"data/b.jpg"
)
) {
byte[] buffer =
new byte[8192];
int len;
while (
(len = input.read(buffer))
!= -1
) {
output.write(
buffer,
0,
len
);
}
}
这就是最经典的:
文件复制
三十七、为什么图片必须优先用字节流
图片:
不是文本字符
如果用字符流读取图片:
会涉及错误字符解码
可能导致文件损坏。
所以:
图片、视频、PDF、ZIP
→ 字节流
三十八、BufferedInputStream
给普通字节输入流增加:
缓冲能力
例如:
BufferedInputStream input =
new BufferedInputStream(
new FileInputStream(
"data/a.jpg"
)
);
三十九、BufferedOutputStream
BufferedOutputStream output =
new BufferedOutputStream(
new FileOutputStream(
"data/b.jpg"
)
);
四十、缓冲流文件复制
try (
BufferedInputStream input =
new BufferedInputStream(
new FileInputStream(
"data/a.jpg"
)
);
BufferedOutputStream output =
new BufferedOutputStream(
new FileOutputStream(
"data/b.jpg"
)
)
) {
byte[] buffer =
new byte[8192];
int len;
while (
(len = input.read(buffer))
!= -1
) {
output.write(
buffer,
0,
len
);
}
}
四十一、flush()
输出流存在缓冲时:
数据可能暂时在内存缓冲区
flush():
output.flush();
表示:
把缓冲中的数据尽快写出去
但如果正确使用:
try-with-resources
关闭流时一般也会进行必要处理。
四十二、close()
IO 资源使用完应该关闭:
input.close();
output.close();
否则可能导致:
文件句柄泄漏
文件一直被占用
资源无法释放
现代 Java 推荐:
try-with-resources
自动关闭。
四十三、try-with-resources
基本结构:
try (
资源1;
资源2
) {
// 使用资源
}
这些资源必须实现:
AutoCloseable
IO 流基本都支持。
四十四、为什么推荐 try-with-resources
传统代码:
InputStream input = null;
try {
input = ...
} finally {
if (input != null) {
input.close();
}
}
不仅代码多,
而且:
关闭资源本身也可能发生异常
现在优先:
try (
InputStream input = ...
) {
}
更加清晰。
四十五、字符编码
文本最终存入磁盘时:
字符必须转换成字节
读取时:
字节还需要重新转换成字符
这就涉及:
字符编码
常见:
UTF-8
GBK
ISO-8859-1
四十六、乱码为什么产生
例如文件实际使用:
UTF-8
保存。
但是程序读取时按照:
GBK
解释字节。
结果:
同一组字节
按照错误规则解码
就会出现:
乱码
可以简单记:
写入编码
和
读取编码
要一致
四十七、StandardCharsets
现代 Java 推荐:
StandardCharsets.UTF_8
例如:
text.getBytes(
StandardCharsets.UTF_8
);
需要:
import java.nio.charset
.StandardCharsets;
优点:
不用手写 "UTF-8"
也避免拼写错误
四十八、Reader
Reader:
字符输入流
用于读取:
字符
常见:
FileReader
BufferedReader
InputStreamReader
四十九、Writer
Writer:
字符输出流
常见:
FileWriter
BufferedWriter
OutputStreamWriter
PrintWriter
五十、FileReader
用于:
读取文本文件
例如:
FileReader reader =
new FileReader(
"data/test.txt"
);
五十一、FileReader.read()
一次读取一个字符:
int data =
reader.read();
同样:
-1 表示结束
五十二、字符流读取文本
try (
FileReader reader =
new FileReader(
"data/test.txt"
)
) {
int ch;
while (
(ch = reader.read())
!= -1
) {
System.out.print(
(char) ch
);
}
}
五十三、read(char[])
可以一次读取多个字符:
char[] buffer =
new char[1024];
int len =
reader.read(buffer);
五十四、FileWriter
写文本:
try (
FileWriter writer =
new FileWriter(
"data/test.txt"
)
) {
writer.write(
"Hello Java"
);
}
五十五、FileWriter 追加
new FileWriter(
"data/test.txt",
true
);
表示:
追加写入
五十六、BufferedReader
BufferedReader 是非常常用的文本读取类。
最大的常用优势:
可以按行读取
方法:
readLine()
五十七、readLine()
例如:
try (
BufferedReader reader =
new BufferedReader(
new FileReader(
"data/test.txt"
)
)
) {
String line;
while (
(line = reader.readLine())
!= null
) {
System.out.println(line);
}
}
注意:
readLine()
读到文件末尾返回 null
五十八、readLine() 不包含换行符
假设文件:
Java
MySQL
Spring
每次:
readLine()
得到:
Java
而不是:
Java\n
所以输出时通常:
System.out.println(line);
五十九、BufferedWriter
例如:
try (
BufferedWriter writer =
new BufferedWriter(
new FileWriter(
"data/test.txt"
)
)
) {
writer.write("Java");
writer.newLine();
writer.write("Spring");
}
六十、newLine()
推荐:
writer.newLine();
而不是到处写:
"\n"
因为:
不同操作系统的换行符历史上可能不同
六十一、转换流
两个重要类:
InputStreamReader
OutputStreamWriter
它们负责:
字节流
和
字符流
之间的转换
同时可以指定:
字符编码
六十二、InputStreamReader
结构:
字节输入流
↓
InputStreamReader
↓
字符输入
例如:
InputStreamReader reader =
new InputStreamReader(
new FileInputStream(
"data/test.txt"
),
StandardCharsets.UTF_8
);
六十三、指定 UTF-8 读取文本
推荐组合:
try (
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
new FileInputStream(
"data/test.txt"
),
StandardCharsets.UTF_8
)
)
) {
String line;
while (
(line = reader.readLine())
!= null
) {
System.out.println(line);
}
}
六十四、OutputStreamWriter
结构:
字符
↓
OutputStreamWriter
↓
字节输出流
例如:
OutputStreamWriter writer =
new OutputStreamWriter(
new FileOutputStream(
"data/test.txt"
),
StandardCharsets.UTF_8
);
六十五、指定 UTF-8 写文件
try (
BufferedWriter writer =
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(
"data/test.txt"
),
StandardCharsets.UTF_8
)
)
) {
writer.write(
"你好 Java"
);
}
六十六、FileReader/FileWriter 和编码
较新的 Java 版本已经提供了一些:
带 Charset 参数
的构造方式,
但在很多老代码和教材里,
经常通过:
InputStreamReader
OutputStreamWriter
显式指定编码。
无论使用哪一种,
重点是:
明确编码
避免依赖系统默认编码
六十七、PrintWriter
PrintWriter 非常适合:
输出文本
支持:
print()
println()
printf()
例如:
try (
PrintWriter writer =
new PrintWriter(
"data/result.txt",
StandardCharsets.UTF_8
)
) {
writer.println(
"姓名:张三"
);
writer.println(
"成绩:90"
);
}
六十八、DataInputStream / DataOutputStream
用于按照 Java 基本类型格式:
读取 / 写入
例如:
writeInt()
writeDouble()
writeBoolean()
writeUTF()
六十九、DataOutputStream 示例
try (
DataOutputStream output =
new DataOutputStream(
new FileOutputStream(
"data/data.bin"
)
)
) {
output.writeInt(100);
output.writeDouble(3.14);
output.writeUTF(
"Java"
);
}
七十、DataInputStream
读取顺序必须和写入顺序对应:
try (
DataInputStream input =
new DataInputStream(
new FileInputStream(
"data/data.bin"
)
)
) {
int number =
input.readInt();
double value =
input.readDouble();
String text =
input.readUTF();
}
如果顺序不一致:
数据会被错误解释
七十一、对象序列化
对象保存在 JVM 内存中。
有时希望:
把对象转换成字节
保存到文件
或者通过网络传输
这个过程叫:
序列化
英文:
Serialization
七十二、反序列化
把:
字节数据
重新恢复成:
Java 对象
叫:
反序列化
英文:
Deserialization
七十三、Serializable
Java 原生序列化通常要求类实现:
Serializable
例如:
import java.io.Serializable;
public class Student
implements Serializable {
private String name;
private int age;
}
Serializable 是:
标记接口
本身没有需要实现的方法。
七十四、ObjectOutputStream
用于序列化对象:
try (
ObjectOutputStream output =
new ObjectOutputStream(
new FileOutputStream(
"data/student.dat"
)
)
) {
Student student =
new Student(
"张三",
20
);
output.writeObject(
student
);
}
七十五、ObjectInputStream
反序列化:
try (
ObjectInputStream input =
new ObjectInputStream(
new FileInputStream(
"data/student.dat"
)
)
) {
Student student =
(Student)
input.readObject();
System.out.println(
student
);
}
七十六、serialVersionUID
序列化类经常定义:
private static final long
serialVersionUID =
1L;
用于:
版本兼容标识
例如:
public class Student
implements Serializable {
private static final long
serialVersionUID =
1L;
}
七十七、transient
如果某个字段:
不希望被 Java 原生序列化
可以使用:
transient
例如:
private transient String password;
反序列化后这个字段通常恢复默认值。
七十八、Java 原生序列化在现代项目中的位置
需要知道:
ObjectInputStream
ObjectOutputStream
Serializable
因为:
老项目
JDK API
面试
框架底层
都可能遇到。
但现代 Web 项目进行数据交换时更常见:
JSON
例如:
Jackson
Fastjson2
Gson
因此不要把:
Java 原生序列化
理解成现代接口传输的默认方案。
七十九、Path
现代 Java 文件 API 中非常重要:
java.nio.file.Path
相对于老的:
File
很多操作会更自然。
八十、创建 Path
例如:
import java.nio.file.Path;
Path path =
Path.of(
"data",
"test.txt"
);
相比手写:
data/test.txt
更加适合跨平台拼接路径。
八十一、Path 常用方法
getFileName()
getParent()
toAbsolutePath()
resolve()
normalize()
八十二、resolve()
拼接路径:
Path dir =
Path.of("data");
Path file =
dir.resolve(
"test.txt"
);
得到:
data/test.txt
八十三、Files
java.nio.file.Files 是现代 Java 文件操作非常常用的工具类。
常见:
exists()
createFile()
createDirectories()
readString()
writeString()
readAllLines()
copy()
move()
delete()
deleteIfExists()
size()
list()
walk()
八十四、Files.exists()
Path path =
Path.of(
"data/test.txt"
);
boolean exists =
Files.exists(path);
八十五、Files.createDirectories()
Path dir =
Path.of(
"data/upload/avatar"
);
Files.createDirectories(
dir
);
如果父目录不存在:
会一起创建
很常用。
八十六、Files.readString()
读取整个文本文件:
String content =
Files.readString(
Path.of(
"data/test.txt"
),
StandardCharsets.UTF_8
);
对于:
小型文本文件
非常方便。
八十七、Files.writeString()
Files.writeString(
Path.of(
"data/test.txt"
),
"Hello Java",
StandardCharsets.UTF_8
);
八十八、读取所有行
List<String> lines =
Files.readAllLines(
Path.of(
"data/test.txt"
),
StandardCharsets.UTF_8
);
适合:
文件不大
因为会一次性把内容放入内存。
八十九、大文件不要随便 readAllLines()
假设:
一个日志文件几 GB
如果:
Files.readAllLines(...)
可能占用大量内存。
大文件更适合:
流式读取
BufferedReader
Files.lines()
九十、Files.lines()
try (
Stream<String> lines =
Files.lines(
Path.of(
"data/test.txt"
),
StandardCharsets.UTF_8
)
) {
lines.forEach(
System.out::println
);
}
这里使用到:
Stream
你前面的课程体系后续也会专门学。
九十一、Files.copy()
复制文件:
Files.copy(
Path.of(
"data/a.jpg"
),
Path.of(
"data/b.jpg"
)
);
九十二、覆盖复制
目标文件已经存在时,
可以:
Files.copy(
source,
target,
StandardCopyOption
.REPLACE_EXISTING
);
九十三、Files.move()
移动:
Files.move(
source,
target
);
同一目录下改文件名也可以理解为:
移动
九十四、Files.delete()
删除:
Files.delete(path);
不存在时:
会抛异常
九十五、Files.deleteIfExists()
Files.deleteIfExists(
path
);
如果不存在:
通常不会因为“不存在”而报错
更适合很多清理操作。
九十六、Files.size()
long size =
Files.size(path);
单位:
字节
九十七、Files.list()
列出:
目录第一层内容
例如:
try (
Stream<Path> stream =
Files.list(
Path.of(
"data"
)
)
) {
stream.forEach(
System.out::println
);
}
九十八、Files.walk()
递归遍历目录:
try (
Stream<Path> stream =
Files.walk(
Path.of(
"data"
)
)
) {
stream.forEach(
System.out::println
);
}
相比自己写:
File + 递归
更加方便。
九十九、传统 IO 和 NIO 怎么选
现在开发中:
简单文件操作
优先考虑:
Path
Files
例如:
读取整个小文本
复制文件
创建目录
删除文件
如果需要:
流式读取大文件
自己控制缓冲
处理网络流
装饰流组合
逐块读写
仍然会大量使用:
InputStream
OutputStream
Reader
Writer
Buffered...
两套并不是互相替代,
而是:
一起使用
一百、NIO 是什么
NIO:
New IO
Java 1.4 开始引入。
常见核心:
Buffer
Channel
Path
Files
Selector
其中:
Path / Files
日常文件开发很常用。
而:
Channel / Selector
更多会在:
网络编程
Netty
高并发 IO
中继续深入。
一百零一、ByteArrayInputStream
有时数据已经在内存中:
byte[]
但某个 API 需要:
InputStream
可以使用:
ByteArrayInputStream
例如:
byte[] data =
"Java"
.getBytes(
StandardCharsets.UTF_8
);
InputStream input =
new ByteArrayInputStream(
data
);
一百零二、ByteArrayOutputStream
用于:
把输出内容先写到内存 byte[]
例如:
ByteArrayOutputStream output =
new ByteArrayOutputStream();
output.write(
"Java"
.getBytes(
StandardCharsets.UTF_8
)
);
byte[] bytes =
output.toByteArray();
一百零三、内存流的实际用途
常见:
生成文件但暂时不落盘
图片处理
HTTP 返回文件
流之间转换
测试代码
例如 SpringBoot 下载接口中:
内存生成 Excel
再写到响应流
就可能类似这个思想。
一百零四、文件上传的 IO 思路
假设浏览器上传:
avatar.jpg
后端大致过程:
浏览器
↓
HTTP 请求
↓
服务器拿到文件流
↓
读取字节
↓
保存到本地 / OSS / COS
所以文件上传本质上仍然离不开:
IO
一百零五、文件下载的 IO 思路
大致:
服务器文件
↓
InputStream
↓
HTTP Response OutputStream
↓
浏览器
后面学习 Servlet / SpringBoot 时会看到类似:
response.getOutputStream();
一百零六、不要一次性把超大文件全部读入内存
不推荐大文件:
byte[] data =
Files.readAllBytes(path);
如果文件:
几 GB
可能导致:
内存不足
OutOfMemoryError
大文件应该:
边读
边写
即:
流式处理
一百零七、缓冲区大小
常见:
byte[] buffer =
new byte[8192];
为什么不是越大越好?
因为:
缓冲越大
内存占用越大
性能并不会无限提升
大多数普通场景:
4KB
8KB
16KB
等都很常见。
不要为了“优化”随便创建:
几百 MB 缓冲数组
一百零八、复制文件完整封装
public static void copyFile(
Path source,
Path target
)
throws IOException {
Path parent =
target.getParent();
if (parent != null) {
Files.createDirectories(
parent
);
}
try (
InputStream input =
Files.newInputStream(
source
);
OutputStream output =
Files.newOutputStream(
target
)
) {
byte[] buffer =
new byte[8192];
int len;
while (
(len = input.read(buffer))
!= -1
) {
output.write(
buffer,
0,
len
);
}
}
}
一百零九、其实 Files.copy 更简单
如果只是:
普通文件复制
可以:
Files.copy(
source,
target,
StandardCopyOption
.REPLACE_EXISTING
);
所以:
学习底层流复制
是为了理解 IO
实际开发
优先选择简单可靠 API
一百一十、文本复制案例
try (
BufferedReader reader =
Files.newBufferedReader(
Path.of(
"data/source.txt"
),
StandardCharsets.UTF_8
);
BufferedWriter writer =
Files.newBufferedWriter(
Path.of(
"data/target.txt"
),
StandardCharsets.UTF_8
)
) {
String line;
while (
(line = reader.readLine())
!= null
) {
writer.write(line);
writer.newLine();
}
}
一百一十一、Files.newBufferedReader()
比:
new BufferedReader(
new InputStreamReader(
new FileInputStream(...)
)
)
更简洁。
例如:
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
);
一百一十二、Files.newBufferedWriter()
BufferedWriter writer =
Files.newBufferedWriter(
path,
StandardCharsets.UTF_8
);
现代代码中非常好用。
一百一十三、StandardOpenOption
写文件时可以控制:
创建
覆盖
追加
例如:
Files.writeString(
path,
"Hello\n",
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND
);
一百一十四、常见 StandardOpenOption
CREATE
文件不存在则创建
CREATE_NEW
只允许创建新文件
APPEND
追加
TRUNCATE_EXISTING
清空原内容后写
WRITE
写
READ
读
一百一十五、Properties 文件和 IO
后面配置文件中经常看到:
application.properties
Java 自带:
Properties
可以读写简单:
key=value
文件。
一百一十六、读取 Properties
假设:
app.properties
内容:
username=admin
port=8080
代码:
Properties properties =
new Properties();
try (
InputStream input =
Files.newInputStream(
Path.of(
"app.properties"
)
)
) {
properties.load(input);
}
读取:
String username =
properties.getProperty(
"username"
);
一百一十七、写 Properties
Properties properties =
new Properties();
properties.setProperty(
"username",
"admin"
);
properties.setProperty(
"port",
"8080"
);
try (
OutputStream output =
Files.newOutputStream(
Path.of(
"app.properties"
)
)
) {
properties.store(
output,
"application config"
);
}
一百一十八、类路径资源和文件系统路径不是一回事
这是后面 MyBatis / SpringBoot 经常遇到的坑。
例如:
src/main/resources
中的:
mybatis-config.xml
打包之后可能进入:
classpath
它不一定还是一个普通:
File
所以读取类路径资源时经常使用:
ClassLoader
一百一十九、ClassLoader.getResourceAsStream()
例如:
InputStream input =
Demo.class
.getClassLoader()
.getResourceAsStream(
"config.properties"
);
适合读取:
classpath 下的资源
一百二十、为什么开发时能 File 读取,打 Jar 后却失败
例如:
new File(
"src/main/resources/a.txt"
);
IDEA 中可能能用。
但是项目打成:
jar
部署后:
src/main/resources
这个源码目录已经不存在。
所以:
类路径资源不要简单写死源码目录。
后面 Spring 中还会学:
ClassPathResource
一百二十一、文件路径安全
如果用户输入:
../../../../windows/system.ini
然后程序直接:
baseDir.resolve(userInput)
可能造成:
目录穿越
Path Traversal
文件上传下载功能需要特别注意。
一百二十二、路径 normalize()
例如:
Path base =
Path.of(
"uploads"
)
.toAbsolutePath()
.normalize();
Path target =
base.resolve(
userInput
)
.normalize();
然后检查:
if (!target.startsWith(base)) {
throw new IllegalArgumentException(
"非法文件路径"
);
}
这是一种基础防护思路。
一百二十三、上传文件名不能完全信任用户
例如上传文件:
../../evil.exe
不能直接:
把原始文件名当服务器路径
实际项目常见做法:
服务器生成 UUID 文件名
保留经过校验的扩展名
限制文件大小
限制 MIME 类型
必要时检查真实文件内容
一百二十四、不要只靠后缀判断文件类型
例如:
virus.exe
改名成:
photo.jpg
后缀并不能保证真实内容。
真正安全的文件上传需要结合:
MIME
文件魔数
内容检测
大小限制
存储隔离
当前阶段先建立安全意识。
一百二十五、常见错误:忘记关闭流
错误:
FileInputStream input =
new FileInputStream(
"a.txt"
);
// 使用后不关闭
可能造成:
资源泄漏
文件被占用
句柄耗尽
推荐:
try-with-resources
一百二十六、常见错误:文本乱码
原因通常:
写入 UTF-8
读取使用其他编码
推荐:
明确使用 StandardCharsets.UTF_8
一百二十七、常见错误:复制文件只 write(buffer)
错误:
while (
input.read(buffer) != -1
) {
output.write(buffer);
}
最后一块数据可能错误。
正确:
int len;
while (
(len = input.read(buffer))
!= -1
) {
output.write(
buffer,
0,
len
);
}
一百二十八、常见错误:字符流复制图片
不应该:
FileReader reader =
new FileReader(
"a.jpg"
);
图片应该:
字节流
一百二十九、常见错误:readLine() 判断 -1
错误:
while (
reader.readLine()
!= -1
) {
}
readLine() 返回:
String
文件末尾:
null
正确:
String line;
while (
(line = reader.readLine())
!= null
) {
}
一百三十、常见错误:用 available() 判断文件结束
有些初学代码:
while (
input.available() > 0
) {
}
不要把:
available()
当成通用:
文件剩余字节数
判断方式。
标准读取循环仍然应该根据:
read()
返回:
-1
判断结束。
一百三十一、常见错误:频繁 flush()
例如每写一个字符都:
writer.flush();
虽然可以,
但会减少缓冲带来的性能优势。
通常:
需要立即发送
或阶段性保证写出
时才手动 flush。
一百三十二、常见错误:吞 IOException
不推荐:
try {
} catch (
IOException e
) {
}
正确做法应该至少:
记录日志
转换异常
返回错误
或继续抛出
一百三十三、常见错误:直接 throws Exception
例如:
public void read()
throws Exception {
}
虽然省事,
但太宽泛。
如果明确:
IOException
建议:
throws IOException
更加清晰。
一百三十四、常见错误:绝对路径写死
例如:
"D:\\Users\\zhangsan\\Desktop\\data.txt"
换电脑:
直接失效
项目中通常应该使用:
配置项
相对数据目录
环境变量
对象存储
等方式管理路径。
一百三十五、常见错误:把大文件一次性读进 String
例如:
String content =
Files.readString(
hugeFile
);
如果文件非常大:
会占用大量内存
大文件应该:
按行
按块
流式处理
一百三十六、File 和 Path 对比
| 对比 | File |
Path |
|---|---|---|
| 所属 | java.io |
java.nio.file |
| API 年代 | 较早 | 较新 |
| 路径操作 | 基础 | 更丰富 |
| 配套工具 | File 自己的方法 | Files |
| 新代码 | 仍可用 | 通常更推荐 |
一百三十七、IO 类的装饰器模式
你会发现很多写法:
new BufferedReader(
new InputStreamReader(
new FileInputStream(
"a.txt"
)
)
)
为什么一层套一层?
因为 Java IO 大量使用:
装饰器模式
Decorator
不同对象分别提供:
FileInputStream
从文件读取字节
InputStreamReader
字节转字符
BufferedReader
增加缓冲和 readLine()
最后组合起来得到:
我们真正需要的能力
一百三十八、不要被“套娃”写法吓到
看到:
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
new FileInputStream(
file
),
UTF_8
)
);
可以从里向外读:
FileInputStream
读取文件字节
↓
InputStreamReader
按照 UTF-8 转字符
↓
BufferedReader
加缓冲并支持按行读取
这样就很好理解。
一百三十九、综合案例:统计文本行数
Path path =
Path.of(
"data/article.txt"
);
int lineCount = 0;
try (
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
) {
while (
reader.readLine()
!= null
) {
lineCount++;
}
}
System.out.println(
"总行数:"
+ lineCount
);
一百四十、综合案例:统计文本单词数
Path path =
Path.of(
"data/article.txt"
);
int wordCount = 0;
try (
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
) {
String line;
while (
(line = reader.readLine())
!= null
) {
String trimmed =
line.trim();
if (trimmed.isEmpty()) {
continue;
}
String[] words =
trimmed.split(
"\\s+"
);
wordCount +=
words.length;
}
}
System.out.println(
"单词数量:"
+ wordCount
);
一百四十一、综合案例:日志过滤
假设日志:
INFO 用户登录
ERROR 数据库连接失败
INFO 查询成功
ERROR Redis 超时
要求把:
ERROR
行写入另一个文件。
Path source =
Path.of(
"data/app.log"
);
Path target =
Path.of(
"data/error.log"
);
try (
BufferedReader reader =
Files.newBufferedReader(
source,
StandardCharsets.UTF_8
);
BufferedWriter writer =
Files.newBufferedWriter(
target,
StandardCharsets.UTF_8
)
) {
String line;
while (
(line = reader.readLine())
!= null
) {
if (
line.startsWith(
"ERROR"
)
) {
writer.write(line);
writer.newLine();
}
}
}
一百四十二、综合案例:复制整个目录的思路
步骤:
1. 遍历源目录
2. 计算每个文件相对源目录的位置
3. 在目标目录创建对应目录
4. 普通文件执行 Files.copy()
可以借助:
Files.walk()
完成。
一百四十三、目录复制基础示例
Path source =
Path.of(
"data/source"
);
Path target =
Path.of(
"data/backup"
);
try (
Stream<Path> paths =
Files.walk(source)
) {
for (
Path current :
paths.toList()
) {
Path relative =
source.relativize(
current
);
Path targetPath =
target.resolve(
relative
);
if (
Files.isDirectory(
current
)
) {
Files.createDirectories(
targetPath
);
} else {
Files.copy(
current,
targetPath,
StandardCopyOption
.REPLACE_EXISTING
);
}
}
}
这里只用于理解思路。
真实大型目录复制还需要考虑:
权限
软链接
异常恢复
大文件
覆盖策略
一百四十四、综合案例:简单用户数据保存
例如:
List<String> users =
List.of(
"001,张三,20",
"002,李四,21",
"003,王五,19"
);
保存:
Path path =
Path.of(
"data/users.csv"
);
Files.write(
path,
users,
StandardCharsets.UTF_8
);
得到简单 CSV:
001,张三,20
002,李四,21
003,王五,19
一百四十五、读取简单 CSV
List<String> lines =
Files.readAllLines(
path,
StandardCharsets.UTF_8
);
for (String line : lines) {
String[] values =
line.split(",");
String id =
values[0];
String name =
values[1];
int age =
Integer.parseInt(
values[2]
);
}
注意:
真正 CSV 可能包含引号、逗号转义、换行等复杂规则。
复杂 CSV 不应该长期依赖:
split(",")
而应该考虑成熟 CSV 库。
一百四十六、综合案例:生成验证码文件
Random random =
new Random();
StringBuilder code =
new StringBuilder();
for (int i = 0;
i < 6;
i++) {
code.append(
random.nextInt(10)
);
}
Files.writeString(
Path.of(
"data/code.txt"
),
code.toString(),
StandardCharsets.UTF_8
);
这里把前面学过的:
Random
StringBuilder
Files
结合起来。
一百四十七、综合案例:备份配置文件
Path source =
Path.of(
"config",
"application.properties"
);
Path backup =
Path.of(
"backup",
"application.properties.bak"
);
Files.createDirectories(
backup.getParent()
);
Files.copy(
source,
backup,
StandardCopyOption
.REPLACE_EXISTING
);
一百四十八、IO 中异常怎么处理
IO 常见:
IOException
FileNotFoundException
NoSuchFileException
AccessDeniedException
不要一律:
catch (Exception e)
如果当前方法不能真正解决:
可以 throws IOException
交给上层。
一百四十九、IO 业务中保留异常 cause
例如:
try {
Files.copy(
source,
target
);
} catch (
IOException e
) {
throw new RuntimeException(
"文件复制失败",
e
);
}
不要只:
throw new RuntimeException(
"失败"
);
否则原始原因丢失。
一百五十、文件读取工具方法
public static String readText(
Path path
) {
try {
return Files.readString(
path,
StandardCharsets.UTF_8
);
} catch (
IOException e
) {
throw new RuntimeException(
"读取文件失败:"
+ path,
e
);
}
}
一百五十一、文件写入工具方法
public static void writeText(
Path path,
String content
) {
try {
Path parent =
path.getParent();
if (parent != null) {
Files.createDirectories(
parent
);
}
Files.writeString(
path,
content,
StandardCharsets.UTF_8
);
} catch (
IOException e
) {
throw new RuntimeException(
"写入文件失败:"
+ path,
e
);
}
}
一百五十二、学习 IO 时最容易混淆的地方
重点区分:
Input / Output
字节流 / 字符流
File / 文件内容
File / Path
Reader / InputStream
Writer / OutputStream
BufferedReader / FileReader
覆盖 / 追加
UTF-8 / 默认编码
read() 的 -1
readLine() 的 null
一百五十三、四大流必须记住
InputStream
字节输入
OutputStream
字节输出
Reader
字符输入
Writer
字符输出
一百五十四、常用字节流
FileInputStream
FileOutputStream
BufferedInputStream
BufferedOutputStream
ByteArrayInputStream
ByteArrayOutputStream
DataInputStream
DataOutputStream
ObjectInputStream
ObjectOutputStream
一百五十五、常用字符流
FileReader
FileWriter
BufferedReader
BufferedWriter
InputStreamReader
OutputStreamWriter
PrintWriter
一百五十六、现代文件 API 重点
Path
Files
StandardCharsets
StandardOpenOption
StandardCopyOption
一百五十七、常用 Files 方法
建议熟练:
Files.exists()
Files.createDirectories()
Files.readString()
Files.writeString()
Files.readAllLines()
Files.copy()
Files.move()
Files.deleteIfExists()
Files.size()
Files.newInputStream()
Files.newOutputStream()
Files.newBufferedReader()
Files.newBufferedWriter()
Files.list()
Files.walk()
一百五十八、IO 选择速查
读取图片
InputStream
写图片
OutputStream
读取文本
BufferedReader
或 Files.readString
写文本
BufferedWriter
或 Files.writeString
按行读取大文本
BufferedReader
复制普通文件
Files.copy
创建目录
Files.createDirectories
遍历目录
Files.list
Files.walk
指定字符编码
StandardCharsets.UTF_8
一百五十九、练习题 1:创建文件和目录
创建:
data/student
目录。
然后创建:
data/student/info.txt
一百六十、练习题 2:写入文本
将:
姓名:张三
年龄:20
专业:软件工程
写入:
student.txt
要求:
UTF-8
一百六十一、练习题 3:按行读取
读取:
student.txt
逐行输出。
要求使用:
BufferedReader
一百六十二、练习题 4:复制图片
把:
a.jpg
复制成:
b.jpg
分别尝试:
字节流
Files.copy()
一百六十三、练习题 5:统计文本
统计一个 txt 文件:
总行数
非空行数
字符数量
一百六十四、练习题 6:筛选日志
读取:
app.log
把包含:
ERROR
的行保存到:
error.log
一百六十五、练习题 7:Properties
创建:
config.properties
保存:
username
password
port
然后使用 Java 读取。
一百六十六、练习题 8:对象序列化
创建 Student:
name
age
score
实现:
Serializable
将对象保存到文件,
然后重新读取。
一百六十七、练习题 9:文件列表
输入一个目录,
输出目录下:
所有普通文件
一百六十八、练习题 10:递归统计文件数量
统计某个目录及子目录中:
文件总数
目录总数
分别尝试:
File + 递归
Files.walk()
一百六十九、练习题 11:文件大小排序
遍历目录中的所有普通文件,
保存到:
List<Path>
按照:
文件大小从大到小
排序。
这个练习可以复习:
IO
集合
Comparator
一百七十、练习题 12:简单文件备份工具
需求:
输入源目录
输入备份目录
递归复制所有文件
目标目录不存在自动创建
一百七十一、本章核心知识结构
Java IO
│
├─ 文件路径
│ ├─ File
│ └─ Path
│
├─ 字节流
│ ├─ InputStream
│ │ ├─ FileInputStream
│ │ └─ BufferedInputStream
│ │
│ └─ OutputStream
│ ├─ FileOutputStream
│ └─ BufferedOutputStream
│
├─ 字符流
│ ├─ Reader
│ │ ├─ FileReader
│ │ ├─ InputStreamReader
│ │ └─ BufferedReader
│ │
│ └─ Writer
│ ├─ FileWriter
│ ├─ OutputStreamWriter
│ └─ BufferedWriter
│
├─ 特殊流
│ ├─ DataInputStream
│ ├─ DataOutputStream
│ ├─ ObjectInputStream
│ ├─ ObjectOutputStream
│ ├─ ByteArrayInputStream
│ └─ ByteArrayOutputStream
│
└─ NIO 文件工具
├─ Files
├─ StandardCharsets
├─ StandardOpenOption
└─ StandardCopyOption
一百七十二、本章必须掌握
至少应该能够独立完成:
1. 区分输入和输出
2. 区分字节流和字符流
3. 使用 File / Path 表示路径
4. 创建和判断文件目录
5. 使用 FileInputStream 读取文件
6. 使用 FileOutputStream 写文件
7. 使用字节流复制图片
8. 使用 BufferedReader 按行读取文本
9. 使用 BufferedWriter 写文本
10. 理解字符编码和乱码原因
11. 使用 UTF-8 指定编码
12. 理解转换流
13. 正确使用 try-with-resources
14. 理解序列化和反序列化
15. 使用 Path / Files 进行现代文件操作
16. 使用 Files.copy() 复制文件
17. 使用 Files.walk() 遍历目录
18. 知道大文件不能一次性全部读入内存
19. 理解 classpath 资源和普通文件路径区别
20. 知道文件上传下载的基本 IO 原理
一百七十三、和下一章多线程的衔接
IO 操作有一个非常重要的特点:
很多 IO 操作都需要等待
例如:
读取磁盘
上传文件
下载文件
调用网络接口
等待客户端数据
如果一个线程:
一直等待 IO
这段时间 CPU 可能并没有真正执行多少计算。
因此实际程序中会进一步涉及:
多个任务同时执行
线程调度
异步任务
线程安全
线程池
这就进入下一章:
Java 多线程
IO 和多线程以后还会结合出:
阻塞 IO
非阻塞 IO
NIO
异步 IO
网络服务器
Netty
当前阶段先把:
文件
流
编码
资源关闭
Path / Files
掌握好,再继续进入多线程。