1. 简介

虽然在 Groovy 中我们可以像 Java 一样处理 I/O 操作,但 Groovy 在 Java 的 I/O 功能基础上扩展了许多辅助方法

在本教程中,我们将探讨如何使用 Groovy 提供的 File 扩展方法来读写文件、遍历文件系统以及序列化数据和对象。

在适当的地方,我们会链接到相关的 Java 文章,方便对比 Java 中的等效实现。

2. 文件读取

Groovy 为 读取文件 提供了便捷的功能,包括 eachLine 方法、获取 BufferedReaderInputStream 的方法,以及一行代码读取整个文件内容的方法。

Java 7 和 Java 8 也提供了类似的 Java 文件读取支持

2.1. 使用 eachLine 进行读取

处理文本文件时,我们通常需要逐行读取并处理每一行。Groovy 为 java.io.File 提供了一个便捷的扩展方法 eachLine

def lines = []

new File('src/main/resources/ioInput.txt').eachLine { line ->
    lines.add(line)
}

传递给 eachLine 的闭包还可以获取一个可选的行号参数。我们可以利用这个行号来读取文件中的特定行:

def lineNoRange = 2..4
def lines = []

new File('src/main/resources/ioInput.txt').eachLine { line, lineNo ->
    if (lineNoRange.contains(lineNo)) {
        lines.add(line)
    }
}

默认行号从 1 开始。我们可以通过将第一个参数传递给 eachLine 方法来指定起始行号。

例如,让行号从 0 开始:

new File('src/main/resources/ioInput.txt').eachLine(0, { line, lineNo ->
    if (lineNoRange.contains(lineNo)) {
        lines.add(line)
    }
})

⚠️ 如果在 eachLine 中抛出异常,Groovy 会确保文件资源被正确关闭,类似 Java 中的 try-with-resourcestry-finally

2.2. 使用 Reader 进行读取

我们也可以轻松地从 Groovy 的 File 对象中获取一个 BufferedReader。使用 withReader 可以将 BufferedReader 传递给闭包:

def actualCount = 0
new File('src/main/resources/ioInput.txt').withReader { reader ->
    while(reader.readLine()) {
        actualCount++
    }
}

eachLine 一样,withReader 方法在抛出异常时会自动关闭资源。

有时我们可能需要直接获取 BufferedReader 对象,比如我们要调用一个接受该对象的方法。这时可以使用 newReader 方法:

def outputPath = 'src/main/resources/ioOut.txt'
def reader = new File('src/main/resources/ioInput.txt').newReader()
new File(outputPath).append(reader)
reader.close()

⚠️ 与前面的方法不同,当我们通过 newReader 获取 BufferedReader 时,需要我们手动关闭资源

2.3. 使用 InputStream 进行读取

类似于 withReadernewReaderGroovy 也提供了方便处理 InputStream 的方法。虽然我们也可以通过 InputStream 读取文本,但 InputStream 更常用于处理二进制数据。

使用 withInputStreamInputStream 传递给闭包并读取字节:

byte[] data = []
new File("src/main/resources/binaryExample.jpg").withInputStream { stream ->
    data = stream.getBytes()
}

如果需要获取 InputStream 对象,可以使用 newInputStream

def outputPath = 'src/main/resources/binaryOut.jpg'
def is = new File('src/main/resources/binaryExample.jpg').newInputStream()
new File(outputPath).append(is)
is.close()

⚠️ 与 BufferedReader 类似,使用 newInputStream 时需要我们手动关闭资源,而使用 withInputStream 则不需要。

2.4. 其他读取方式

我们再来看几种 Groovy 提供的一行读取整个文件内容的方法。

如果想将文件内容按行读入一个 List,可以使用 collect 方法:

def actualList = new File('src/main/resources/ioInput.txt').collect {it}

将文件内容读入 String 数组:

def actualArray = new File('src/main/resources/ioInput.txt') as String[]

读取整个文件内容为 String(适用于短文件):

def actualString = new File('src/main/resources/ioInput.txt').text

处理二进制文件时,可以使用 bytes 方法:

def contents = new File('src/main/resources/binaryExample.jpg').bytes

3. 文件写入

在开始 文件写入 之前,我们先准备一些要写入的文本:

def outputLines = [
    'Line one of output example',
    'Line two of output example',
    'Line three of output example'
]

3.1. 使用 Writer 进行写入

与读取文件类似,我们也可以轻松从 File 对象获取 BufferedWriter

使用 withWriter 获取 BufferedWriter 并传递给闭包:

def outputFileName = 'src/main/resources/ioOutput.txt'
new File(outputFileName).withWriter { writer ->
    outputLines.each { line ->
        writer.writeLine line
    }
}

如果在闭包中抛出异常,withWriter 会自动关闭资源。

我们也可以使用 newWriter 获取 BufferedWriter 对象:

def outputFileName = 'src/main/resources/ioOutput.txt'
def writer = new File(outputFileName).newWriter()
outputLines.forEach {line ->
    writer.writeLine line
}
writer.flush()
writer.close()

⚠️ 使用 newWriter 时,我们需要手动调用 flushclose 方法。

3.2. 使用 Output Stream 进行写入

如果要写入二进制数据,可以使用 withOutputStreamnewOutputStream 获取 OutputStream

使用 withOutputStream 写入字节:

byte[] outBytes = [44, 88, 22]
new File(outputFileName).withOutputStream { stream ->
    stream.write(outBytes)
}

使用 newOutputStream 获取 OutputStream 并写入:

byte[] outBytes = [44, 88, 22]
def os = new File(outputFileName).newOutputStream()
os.write(outBytes)
os.close()

⚠️ 与 InputStreamBufferedReaderBufferedWriter 一样,使用 newOutputStream 时我们需要手动关闭资源。

3.3. 使用 << 操作符写入

由于写入文本到文件非常常见,Groovy 的 << 操作符提供了直接支持。

使用 << 操作符写入简单的文本行:

def ln = System.getProperty('line.separator')
def outputFileName = 'src/main/resources/ioOutput.txt'
new File(outputFileName) << "Line one of output example${ln}" + 
  "Line two of output example${ln}Line three of output example"

3.4. 使用 bytes 写入二进制数据

✅ 我们在前面看到可以通过访问 bytes 字段轻松读取二进制文件的所有字节。

同样地,我们可以用类似方式写入二进制数据:

def outputFileName = 'src/main/resources/ioBinaryOutput.bin'
def outputFile = new File(outputFileName)
byte[] outBytes = [44, 88, 22]
outputFile.bytes = outBytes

4. 遍历文件树

Groovy 还提供了方便处理文件树的方法。本节将使用 eachFileeachDir 及其变体,以及 traverse 方法进行演示。

4.1. 使用 eachFile 列出文件

使用 eachFile 列出目录中的所有文件和子目录:

new File('src/main/resources').eachFile { file ->
    println file.name
}

在处理文件时,经常需要根据文件名进行过滤。使用 eachFileMatch 和正则表达式只列出以 “io” 开头、以 “.txt” 结尾的文件:

new File('src/main/resources').eachFileMatch(~/io.*\.txt/) { file ->
    println file.name
}

⚠️ eachFileeachFileMatch 只列出顶层目录的内容。我们还可以通过传递 FileType 参数来限制返回类型,可选值有 ANYFILESDIRECTORIES

使用 eachFileRecurse 递归列出所有文件,并指定只返回文件类型:

new File('src/main').eachFileRecurse(FileType.FILES) { file ->
    println "$file.parent $file.name"
}

⚠️ 如果传递给 eachFile 的路径是一个文件而非目录,会抛出 IllegalArgumentException

Groovy 还提供了专门处理目录的 eachDir 方法。我们可以使用 eachDir 及其变体实现与 eachFile 配合 FileType.DIRECTORIES 相同的效果。

使用 eachFileRecurse 递归列出目录:

new File('src/main').eachFileRecurse(FileType.DIRECTORIES) { file ->
    println "$file.parent $file.name"
}

使用 eachDirRecurse 实现相同功能:

new File('src/main').eachDirRecurse { dir ->
    println "$dir.parent $dir.name"
}

4.2. 使用 traverse 进行复杂遍历

对于 更复杂的目录遍历 需求,我们可以使用 traverse 方法。它类似于 eachFileRecurse,但允许返回 FileVisitResult 对象以控制处理流程。

使用 traverse 遍历 src/main 目录,并跳过 groovy 子目录的处理:

new File('src/main').traverse { file ->
   if (file.directory && file.name == 'groovy') {
        FileVisitResult.SKIP_SUBTREE
    } else {
        println "$file.parent - $file.name"
    }
}

5. 数据与对象处理

5.1. 基本类型序列化

在 Java 中,我们可以使用 DataInputStreamDataOutputStream序列化基本类型数据。Groovy 在此之上也提供了扩展。

准备一些基本类型数据:

String message = 'This is a serialized string'
int length = message.length()
boolean valid = true

使用 withDataOutputStream 序列化数据到文件:

new File('src/main/resources/ioData.txt').withDataOutputStream { out ->
    out.writeUTF(message)
    out.writeInt(length)
    out.writeBoolean(valid)
}

使用 withDataInputStream 读取数据:

String loadedMessage = ""
int loadedLength
boolean loadedValid

new File('src/main/resources/ioData.txt').withDataInputStream { is ->
    loadedMessage = is.readUTF()
    loadedLength = is.readInt()
    loadedValid = is.readBoolean()
}

与其他 with* 方法类似,withDataOutputStreamwithDataInputStream 会自动关闭资源。

5.2. 对象序列化

Groovy 在 Java 的 ObjectInputStreamObjectOutputStream 基础上扩展,使得序列化实现 Serializable 的对象变得非常简单

首先定义一个实现 Serializable 的类:

class Task implements Serializable {
    String description
    Date startDate
    Date dueDate
    int status
}

创建一个 Task 实例并准备序列化:

Task task = new Task(description:'Take out the trash', startDate:new Date(), status:0)

使用 withObjectOutputStream 序列化对象:

new File('src/main/resources/ioSerializedObject.txt').withObjectOutputStream { out ->
    out.writeObject(task)
}

使用 withObjectInputStream 读回对象:

Task taskRead

new File('src/main/resources/ioSerializedObject.txt').withObjectInputStream { is ->
    taskRead = is.readObject()
}

withObjectOutputStreamwithObjectInputStream 会自动处理资源关闭。

6. 总结

在本文中,我们探讨了 Groovy 在 Java 文件 I/O 类基础上添加的功能。我们使用这些功能读写文件、处理目录结构以及序列化数据和对象。

我们只介绍了部分辅助方法,建议深入阅读 Groovy 的文档 了解更多扩展内容。

示例代码可在 GitHub 获取。


原始标题:Guide to I/O in Groovy