21、文件I/O:java.io.File扩展、读写文件、遍历目录、使用Kotlin的use函数管理资源
文件操作,说白了就是跟磁盘打交道。我做了这么多年项目,几乎每个应用都离不开读写文件。Kotlin 在这方面做得挺贴心,它没有重新发明轮子,而是在 Java 的 java.io.File 基础上做了很多扩展,让代码更简洁、更安全。
今天我们就来聊聊 Kotlin 里文件 I/O 的那些事。我会结合自己踩过的坑,把读写文件、遍历目录、资源管理这些核心操作讲透。
21.1 java.io.File 的 Kotlin 扩展
Kotlin 标准库给 java.io.File 加了不少扩展函数。你想想看,以前在 Java 里读文件内容,要写一堆 BufferedReader、InputStreamReader 的样板代码。现在呢?一行搞定。
核心扩展函数一览:
| 扩展函数 | 作用 | 返回值 |
|---|---|---|
readText() |
读取整个文件为字符串 | String |
readLines() |
按行读取,返回列表 | List<String> |
writeText() |
写入字符串到文件 | Unit |
appendText() |
追加字符串到文件末尾 | Unit |
forEachLine() |
逐行处理,避免内存溢出 | Unit |
useLines() |
自动管理资源,逐行处理 | T |
我个人习惯用 readText() 处理小文件,比如配置文件。但如果是几百兆的日志文件,千万别这么干——会把内存撑爆的。我曾经在项目里见过有人用 readText() 读一个 2GB 的日志文件,结果 OOM 了。嗯,那场面挺尴尬的。
21.2 读写文件的最佳实践
先看个最简单的例子:
import java.io.File
fun main() {
val file = File("example.txt")
// 写入
file.writeText("Hello, Kotlin!\n这是第二行")
// 读取
val content = file.readText()
println(content)
}
是不是很简洁?但这里有个坑——writeText() 默认使用 UTF-8 编码。如果你的系统环境不是 UTF-8,或者文件本身是 GBK 编码,就会乱码。
注意:读写文件时务必指定编码!尤其是在 Windows 环境下,很多中文文件是 GBK 编码的。
正确的做法是:
import java.io.File
fun main() {
val file = File("example.txt")
// 指定编码写入
file.writeText("你好,世界", Charsets.UTF_8)
// 指定编码读取
val content = file.readText(Charsets.UTF_8)
println(content)
}
对于大文件,我建议用 forEachLine() 或 useLines()。它们不会一次性把整个文件加载到内存,而是逐行处理。
import java.io.File
fun main() {
val file = File("large_log.txt")
// 逐行处理,适合大文件
file.forEachLine { line ->
if (line.contains("ERROR")) {
println("发现错误: $line")
}
}
}
21.3 遍历目录的几种方式
遍历目录也是家常便饭。Kotlin 提供了 walk() 和 walkTopDown() 等扩展函数,用起来很顺手。
目录遍历函数:
| 函数 | 遍历顺序 | 典型场景 |
|---|---|---|
walk() |
深度优先,默认先处理子目录 | 统计文件数量 |
walkTopDown() |
自上而下,先处理父目录 | 复制目录结构 |
walkBottomUp() |
自下而上,先处理子目录 | 删除目录树 |
listFiles() |
仅当前目录,不递归 | 列出当前目录文件 |
举个例子,统计一个目录下所有 .kt 文件的行数:
import java.io.File
fun main() {
val dir = File("src")
var totalLines = 0
dir.walk()
.filter { it.extension == "kt" }
.forEach { file ->
val lines = file.readLines().size
totalLines += lines
println("${file.name}: $lines 行")
}
println("总计: $totalLines 行")
}
我曾经在项目里用 walkBottomUp() 来删除临时目录。为什么要自下而上?因为如果先删父目录,子目录就删不掉了。这个坑我踩过一次,后来就记住了。
21.4 use 函数:资源管理的利器
说到资源管理,Java 的 try-with-resources 已经不错了。但 Kotlin 的 use 函数更灵活,它是 Closeable 的扩展函数,能自动关闭资源。
import java.io.BufferedReader
import java.io.FileReader
fun readFileWithUse(path: String): String {
return BufferedReader(FileReader(path)).use { reader ->
reader.readText()
}
// use 结束后,reader 自动关闭
}
use 的好处是:
- 无论正常结束还是抛出异常,资源都会被关闭
- 支持 lambda 表达式,代码更紧凑
- 可以链式调用
小技巧:如果你需要同时管理多个资源,可以嵌套使用 use,或者用 useBoth 这样的扩展函数(需要自己实现)。
看一个实际例子:复制文件内容,同时管理输入流和输出流。
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
fun copyFile(source: File, target: File) {
FileInputStream(source).use { input ->
FileOutputStream(target).use { output ->
input.copyTo(output)
}
}
// 两个流都会自动关闭
}
嗯,这里要注意:copyTo() 是 Kotlin 给 InputStream 加的扩展函数,它内部会处理缓冲区,不用自己写循环。
21.5 知识体系总览
下面这张图总结了本章的核心知识点,方便你快速回顾:
21.6 避坑指南
最后,分享几个我踩过的坑:
- 编码问题:我曾经在 Windows 上写了一个配置文件,用
writeText()保存,结果在 Linux 上读出来全是乱码。后来统一指定Charsets.UTF_8才解决。 - 大文件内存溢出:用
readText()读大文件,直接 OOM。后来改用forEachLine()逐行处理,问题解决。 - 目录遍历顺序:删除目录时用了
walkTopDown(),结果父目录先被删了,子目录删不掉。改成walkBottomUp()就好了。 - 资源未关闭:早期写 Java 代码时,经常忘记在
finally里关流。Kotlin 的use函数彻底解决了这个问题,我现在写文件操作必用use。
文件 I/O 看似简单,但细节决定成败。掌握了这些技巧,你写出来的代码会更健壮、更优雅。
公众号:蓝海资料掘金营,微信deep3321