1. 概述

在本教程中,我们将深入探讨 Scala 中的文件 IO 操作,包括从文件中读取数据和向文件中写入数据。

我们还会指出一些在进行文件 IO 时应当避免的常见错误,以及未正确处理这些操作可能引发的问题。

2. 写入文件

Scala 本身并不提供原生的文件写入能力,因此在 Scala 中写入文件完全依赖于 Java 类库。得益于 Scala 与 Java 的互操作性,这并不是问题,但在调用 Java 代码时需要注意一些细节,否则容易出错。

下面我们将通过 Java 类来演示几种在 Scala 中写入文件的方式。

2.1. 使用 FileWriter

FileWriter 是最简洁的写入方式之一,它支持写入字符、字符串等数据类型,语法简洁明了。

示例代码如下:

val fileWriter = new FileWriter(new File("/tmp/hello.txt"))
fileWriter.write("hello there")
fileWriter.close()

⚠️ 务必记得关闭 FileWriter,否则写入内容可能不会完整保存。关闭操作会将缓冲区内容刷新到磁盘。

2.2. 使用 PrintWriter

PrintWriter 同样是一个简洁的写入工具,相比 FileWriter,它还支持类似 printfformat 的格式化写入功能。

示例:

val writer = new PrintWriter(new File("data.txt"))
val s = "big"
val numberOfLines = 3000000
writer.printf("This is a %s program with %d of code", s, new Integer(numberOfLines))
writer.close()

当然,Scala 也提供了 f"" 插值器,可以更方便地进行格式化。

2.3. 使用 DataOutputStream

DataOutputStream 用于高效写入 Java 原始数据类型,比如 doubleint 等。

假设我们要写入 10,000 个 double 值,使用 PrintWriter

val printWriter = new PrintWriter(new FileOutputStream(new File("data.txt")))
val random = Random
for (_ <- 1 to 10000) {
  printWriter.write(random.nextDouble().toString)
}
printWriter.close()

文件大小约为 150KB。

而使用 DataOutputStream

val random = Random
val dataOutputStream = new DataOutputStream(new FileOutputStream(new File("data.txt")))
for (_ <- 1 to 10000){
  dataOutputStream.writeDouble(random.nextDouble())
}
dataOutputStream.close()

此时文件大小为 80KB。因为 DataOutputStreamdouble 以二进制形式存储,每个 double 占 8 字节。

对比字符串形式:

val pi = Math.PI
val dataOutputStream = new DataOutputStream(new FileOutputStream(new File("data.txt")))

dataOutputStream.writeDouble(pi)
val piLengthDouble = dataOutputStream.size() // 8

dataOutputStream.flush()

val piLengthString = pi.toString.getBytes.length // 17

DataOutputStream 更适合处理大量原始数据类型。

2.4. 写入文件时的异常处理

由于 Scala 将所有异常视为运行时异常,不像 Java 那样强制处理,因此在使用 Java IO 类时,很容易忽略异常。

例如,尝试向一个只读文件写入:

touch data.txt && chmod 444 data.txt

然后运行:

val fileWriter = new FileWriter(new File("data.txt"))
fileWriter.write("Hello World!")
fileWriter.close()

会抛出异常:

java.io.FileNotFoundException: data.txt (Permission denied)

为避免这种情况,应使用 try/catchTry 结构:

Try {
   val fileWriter = new FileWriter(new File("data.txt"))
   fileWriter.write("Hello World!")
   fileWriter.close()
 }.toEither match {
   case Left(ex) =>
     // handle exception: ex
   case Right(_) =>
     // write operation was successful
 }

✅ 这样可以保证运行时异常被捕获并处理。

2.5. 高效写入文件

如果频繁进行小量写入操作,建议使用 BufferedWriter 来减少系统调用。

示例:

val bufferedPrintWriter = new BufferedWriter(new PrintWriter(new File("data.txt")))

for (_ <- 1 to 10000) {
  bufferedPrintWriter.write(random.nextDouble().toString)
}
bufferedPrintWriter.close()

默认缓冲区大小为 8KB,数据先写入缓冲区,满后再一次性刷盘,大大减少系统调用次数。

对于 DataOutputStream,可以使用 BufferedOutputStream 包裹 FileOutputStream

val bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("data.txt"))
val dataOutputStream = new DataOutputStream(bufferedOutputStream)

3. 读取文件

Scala 提供了原生的文件读取能力,即 scala.io.Source.fromFile 方法。

示例:

val fileName = "data.txt"
scala.io.Source.fromFile(fileName).getLines().foreach { line =>
  // do something with line
}

getLines() 返回一个 Iterator[String],支持多种换行符(\r\n, \r, \n)。

3.1. 使用 for-comprehension 读取

val fileName = "data.txt"
for (lines <- scala.io.Source.fromFile(fileName).getLines()) {
  // do something with lines
}

⚠️ 一定要记得关闭 BufferedSource,否则可能导致资源泄露。

val fileName = "data.txt"
val bufferedSource = scala.io.Source.fromFile(fileName)
for (lines <- bufferedSource.getLines()) {
  // do something with lines
}
bufferedSource.close()

3.2. 读取时的异常处理

读取文件时同样可能抛出运行时异常,推荐使用 Try 结构处理:

Try {
    val bufferedSource = scala.io.Source.fromFile(fileName)
    for (lines <- bufferedSource.getLines()) {
      // do something with lines
    }
    bufferedSource
  }.toEither match {
    case Left(error) =>
      // handle error
    case Right(bufferedSource) =>
      bufferedSource.close()
  }

3.3. 高效读取大文件

getLines() 返回的是惰性迭代器,不会一次性将整个文件加载进内存。

如果需要一次性读取全部内容:

val fileName = "data.txt"
val bufferedSource = scala.io.Source.fromFile(fileName)
val text = bufferedSource.getLines().mkString
bufferedSource.close()

⚠️ 对于大文件,不建议使用 mkString,避免 OutOfMemoryError

3.4. 使用 Java API 读取文件

也可以使用 Java 的 FileReaderBufferedReader

val fileReader = new BufferedReader(new FileReader(fileName))
def handleRead(line: String): Unit = {
  // handle line
  val newLine = fileReader.readLine()
  if (newLine != null)
    handleRead(newLine)
}
handleRead(fileReader.readLine())
fileReader.close()

这里使用了尾递归方式读取每一行。

4. 总结

本文详细介绍了如何在 Scala 中进行文件读写操作,包括:

✅ 使用 Java 类进行文件写入
✅ 使用 Scala 原生方法读取文件
✅ 异常处理机制
✅ 高效读写技巧(Buffered)
✅ 资源关闭的重要性

所有示例代码可在 GitHub 查看。


原始标题:File IO in Scala