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,它还支持类似 printf
和 format
的格式化写入功能。
示例:
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 原始数据类型,比如 double
、int
等。
假设我们要写入 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。因为 DataOutputStream 将 double
以二进制形式存储,每个 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/catch 或 Try 结构:
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 的 FileReader
和 BufferedReader
:
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 查看。