IO是什么?
IO = Input/Output = 输入/输出 = 数据的进和出
比喻:你的程序就像一个房间,IO就是门——数据从门外进来(Input),从门内出去(Output)
读文件 = 数据从硬盘输入到程序(Input)
写文件 = 数据从程序输出到硬盘(Output)
网络请求 = 数据从网络输入,响应从程序输出
方向参考系永远是程序本身:站在程序的角度看,进来的是Input,出去的是Output
一、流的分类
“流”(Stream)这个比喻非常形象:数据像水一样,从一端流到另一端,你可以一点一点地接
按方向分
| 方向 | 类 | 说明 |
|---|---|---|
| 输入流 | InputStream / Reader |
数据流进程序(读) |
| 输出流 | OutputStream / Writer |
数据流出程序(写) |
按数据类型分
| 类型 | 基类 | 单位 | 适合场景 |
|---|---|---|---|
| 字节流 | InputStream / OutputStream |
byte(1字节) | 图片、视频、压缩包等二进制文件 |
| 字符流 | Reader / Writer |
char(2字节) | 文本文件(.txt, .java, .html) |
口诀:给人看的用字符流,给机器看的用字节流(来自 进制与编码 里的口诀)
完整分类对比表
| 字节流 | 字符流 | |
|---|---|---|
| 输入(读) | InputStream |
Reader |
| 输出(写) | OutputStream |
Writer |
| 文件读写 | FileInputStream / FileOutputStream |
FileReader / FileWriter |
| 缓冲 | BufferedInputStream / BufferedOutputStream |
BufferedReader / BufferedWriter |
| 桥接(字节↔字符) | — | InputStreamReader / OutputStreamWriter |
| 操作单位 | 1 byte | 1 char(2 bytes) |
| 是否处理编码 | ❌ 不管编码 | ✅ 自动处理编码 |
二、字节流:InputStream / OutputStream
字节流是最底层的流,一次搬运一个字节(或一个字节数组),不关心编码,什么文件都能读写
FileInputStream:从文件读字节
1 |
|
FileOutputStream:向文件写字节
1 |
|
实战:用字节流复制文件 ⭐
这是IO流最经典的操作——把一个文件的内容搬到另一个文件
1 |
|
⚠️ 常见坑
fos.write(buffer) 而不是 fos.write(buffer, 0, len) → 最后一次读取可能不满1024字节,多写了垃圾数据
忘记 close() → 数据可能还在缓冲区没写入文件,用 try-with-resources 就不会忘
三、字符流:Reader / Writer
字符流专门处理文本,会自动帮你处理编码问题(一个中文字符不管占几个字节,读出来就是一个 char)
FileReader / FileWriter:读写文本文件
1 |
|
InputStreamReader / OutputStreamWriter:桥接流(字节↔字符) ⭐
这两个类是字节流和字符流之间的桥梁,最大的价值是可以指定编码
在 进制与编码 中我们学过,乱码 = 编码和解码方案不一致。桥接流就是让你显式告诉Java用什么编码
1 |
|
FileReader 和 InputStreamReader 的关系
FileReader 其实就是 InputStreamReader 的简化版,用系统默认编码
new FileReader("a.txt") 等价于 new InputStreamReader(new FileInputStream("a.txt"), 系统默认编码)
需要指定编码时,必须用 InputStreamReader
四、缓冲流:性能提升利器
为什么要缓冲?
比喻:你要从井里打100桶水
没缓冲 = 每次只端一杯水,跑100趟 → 慢死
有缓冲 = 每次装满一桶再搬,跑几趟就够了 → 快得多
每次调用 read() / write() 都要和操作系统交互(系统调用),这很慢
缓冲流在内部维护一个 缓冲区(默认8KB),攒够一批再统一读/写,大大减少系统调用次数
字节缓冲流:BufferedInputStream / BufferedOutputStream
1 |
|
字符缓冲流:BufferedReader / BufferedWriter ⭐
BufferedReader 多了一个杀手级方法:readLine() —— 一次读一行
1 |
|
缓冲流对比表
| 无缓冲 | 有缓冲 | |
|---|---|---|
| 字节流 | FileInputStream / FileOutputStream |
BufferedInputStream / BufferedOutputStream |
| 字符流 | FileReader / FileWriter |
BufferedReader / BufferedWriter |
| 额外方法 | 无 | readLine()(BufferedReader)、newLine()(BufferedWriter) |
| 性能 | 慢(频繁系统调用) | 快(批量读写) |
⚠️ 常见坑
BufferedWriter 写完后忘记 flush() → 数据还在缓冲区,文件里没内容。不过 close() 会自动 flush(),所以用 try-with-resources 就没问题
readLine() 不包含换行符,如果你要把读到的内容原样写回去,别忘了加 newLine()
五、File类:操作文件和目录
File 类不是用来读写文件内容的,而是操作文件/目录本身——创建、删除、查询信息
就像文件管理器:你可以看到文件的名字、大小、位置,但看不到里面写了什么
常用方法一览
| 方法 | 作用 | 返回值 |
|---|---|---|
exists() |
文件/目录是否存在 | boolean |
isFile() |
是不是文件 | boolean |
isDirectory() |
是不是目录 | boolean |
getName() |
获取文件名 | String |
getAbsolutePath() |
获取绝对路径 | String |
length() |
文件大小(字节数) | long |
createNewFile() |
创建新文件 | boolean |
mkdir() |
创建单级目录 | boolean |
mkdirs() |
创建多级目录(推荐) | boolean |
delete() |
删除文件或空目录 | boolean |
list() |
列出目录下的文件名 | String[] |
listFiles() |
列出目录下的File对象 | File[] |
代码示例
1 |
|
递归遍历:列出目录下所有文件(包含子目录)
1 |
|
⚠️ 常见坑
new File("xxx") 不会创建文件!只是创建了一个路径的引用对象
mkdir() vs mkdirs():前者只能创建一级目录,路径中间的目录不存在就失败;后者能创建整条路径
delete() 只能删空目录,非空目录要先递归删里面的文件
listFiles() 如果路径不存在或不是目录,返回 null 而不是空数组——不判空会 NPE
六、try-with-resources 和流
流用完必须关闭,否则会占用系统资源(文件句柄),严重时导致”Too many open files”错误
在 try-catch-finally 中我们学过 try-with-resources 语法——这就是专门为IO流设计的
对比:手动关闭 vs 自动关闭
1 | // ❌ 老写法:手动close,又臭又长,容易忘 |
多个流一起关闭
1 | // 多个流用分号隔开,都会自动关闭 |
能放进 try() 里的条件
必须实现 AutoCloseable 接口(所有IO流都实现了)
数据库连接(Connection)、网络连接等也实现了,都能用
七、⭐ 安全角度:路径遍历漏洞(Path Traversal)
这是Web开发中非常常见的安全漏洞,学IO流必须知道
攻击原理
如果你的程序根据用户输入的文件名来读取文件:
1 | // ❌ 危险代码! |
.. 是什么?
.. 表示上级目录,../.. 就是往上跳两级
攻击者通过多个 .. 跳出你的应用目录,访问服务器上的任意文件
防御方法 ⭐
1 |
|
防御清单
| 方法 | 说明 |
|---|---|
getCanonicalPath() |
解析真实路径(去掉 ..),然后判断是否在允许目录内 |
| 白名单校验 | 只允许合法字符(字母、数字、.、-、_) |
Path.normalize() |
NIO方式标准化路径 |
| 绝不信任用户输入 | 永远不要直接拼接用户输入到文件路径 |
八、NIO基础(了解)
NIO = New IO,Java 1.4 引入,Java 7 又大幅增强(NIO.2)
NIO vs 传统IO对比
| 传统IO | NIO | |
|---|---|---|
| 模型 | 流(Stream) | 通道(Channel)+ 缓冲区(Buffer) |
| 阻塞 | 阻塞(read时线程卡住等数据) | 可以非阻塞 |
| 处理方式 | 面向流:一个字节一个字节地流过 | 面向缓冲区:先把数据装到Buffer里再处理 |
| 适合场景 | 简单的文件读写 | 高并发网络编程(如Netty底层就是NIO) |
| 代码复杂度 | 简单 | 复杂 |
Files / Paths 工具类(Java 7+):日常最实用 ⭐
Java 7 引入的 java.nio.file.Files 和 java.nio.file.Paths,比传统IO简洁太多
1 |
|
Files 常用方法速查
| 方法 | 作用 | 对应传统方式 |
|---|---|---|
Files.readAllLines(path) |
读所有行到List | BufferedReader + readLine循环 |
Files.readString(path) |
读成字符串(Java 11+) | 手动拼StringBuilder |
Files.write(path, bytes) |
写字节数组 | FileOutputStream |
Files.writeString(path, str) |
写字符串(Java 11+) | FileWriter |
Files.copy(src, dest) |
复制文件 | 手写输入输出流循环 |
Files.move(src, dest) |
移动/重命名 | File.renameTo() |
Files.delete(path) |
删除 | File.delete() |
Files.exists(path) |
是否存在 | File.exists() |
Files.size(path) |
文件大小 | File.length() |
日常建议
简单的文件读写,优先用 Files 工具类,代码少、可读性好
需要逐行处理大文件、需要指定编码时,还是用 BufferedReader
高并发网络编程才需要深入 Channel/Buffer/Selector 那一套
九、IO流选择指南总结
给人看的用字符流,给机器看的用字节流(再强调一遍,来自 进制与编码)
| 需求 | 推荐方案 |
|---|---|
| 读写文本文件 | BufferedReader / BufferedWriter |
| 读写二进制文件(图片、视频) | BufferedInputStream / BufferedOutputStream |
| 需要指定编码 | InputStreamReader / OutputStreamWriter(桥接流) |
| 简单读写小文件 | Files.readAllLines() / Files.write()(NIO) |
| 复制文件 | Files.copy()(NIO)或字节流 + buffer |
| 操作文件属性(创建/删除/列表) | File 类或 Files / Paths(NIO) |
十、常见坑汇总
| 坑 | 现象 | 原因 | 解决 |
|---|---|---|---|
| 字节流读中文乱码 | 输出乱码 | 中文是多字节,逐字节读会拆开 | 用字符流或指定编码 |
| 文件写入不完整 | 文件比预期小 | 没 flush() 或没 close() |
用 try-with-resources |
write(buffer) 多写数据 |
文件末尾多了垃圾 | 最后一次读取不满buffer | 用 write(buffer, 0, len) |
listFiles() NPE |
空指针异常 | 路径不存在返回null | 先判空 |
mkdir() 失败 |
返回false | 中间目录不存在 | 用 mkdirs() |
| 路径遍历漏洞 | 被读取任意文件 | 直接拼接用户输入 | 校验canonicalPath |
| 编码不匹配 | 读出乱码 | 写GBK读UTF-8 | 全链路统一UTF-8(参考 进制与编码) |
十一、练习题
练习1:统计文件中某个字符出现的次数
读取一个文本文件,统计字符 e 出现了多少次
提示:用 FileReader 或 BufferedReader,逐字符读取判断
练习2:文件复制工具
写一个方法 copyFile(String src, String dest),能复制任意类型的文件
要求:使用缓冲字节流,输出复制耗时
练习3:目录统计
给定一个目录路径,递归统计:总共多少个文件、多少个子目录、总大小多少字节
练习4:逐行读取并编号
读取一个Java源文件,每行前面加上行号后写到新文件中
例如:1: public class Hello {
练习5(安全):实现一个安全的文件读取方法
方法签名:String safeReadFile(String basePath, String userInput)
要求:防御路径遍历攻击,只允许读取basePath目录内的文件
| 上一章 | 目录 | 下一章 |
|---|---|---|
| 序列化与Serializable | java基础 | Lambda表达式 |