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 里读文件内容,要写一堆 BufferedReaderInputStreamReader 的样板代码。现在呢?一行搞定。

核心扩展函数一览:

扩展函数 作用 返回值
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 知识体系总览

下面这张图总结了本章的核心知识点,方便你快速回顾:

Kotlin 文件 I/O 知识体系 File 扩展函数 读写文件 目录遍历 资源管理 (use) readText() writeText() readLines() appendText() 指定编码 (Charsets) 大文件逐行处理 walk() / walkTopDown() walkBottomUp() 自动关闭资源 核心原则:简洁、安全、高效

21.6 避坑指南

最后,分享几个我踩过的坑:

  • 编码问题:我曾经在 Windows 上写了一个配置文件,用 writeText() 保存,结果在 Linux 上读出来全是乱码。后来统一指定 Charsets.UTF_8 才解决。
  • 大文件内存溢出:readText() 读大文件,直接 OOM。后来改用 forEachLine() 逐行处理,问题解决。
  • 目录遍历顺序:删除目录时用了 walkTopDown(),结果父目录先被删了,子目录删不掉。改成 walkBottomUp() 就好了。
  • 资源未关闭:早期写 Java 代码时,经常忘记在 finally 里关流。Kotlin 的 use 函数彻底解决了这个问题,我现在写文件操作必用 use

文件 I/O 看似简单,但细节决定成败。掌握了这些技巧,你写出来的代码会更健壮、更优雅。


公众号:蓝海资料掘金营,微信deep3321