1. Introduction
Although in Groovy we can work with I/O just as we do in Java, Groovy expands on Java’s I/O functionality with a number of helper methods.
In this tutorial, we’ll look at reading and writing files, traversing file systems and serializing data and objects via Groovy’s File extension methods.
Where applicable, we’ll be linking to our relevant Java articles for easy comparison to the Java equivalent.
2. Reading Files
Groovy adds convenient functionality for reading files in the form of the eachLine methods, methods for getting BufferedReaders and InputStreams, and ways to get all the file data with one line of code.
Java 7 and Java 8 have similar support for reading files in Java.
2.1. Reading with eachLine
When dealing with text files, we often need to read each line and process it. Groovy provides a convenient extension to java.io.File with the eachLine method:
def lines = []
new File('src/main/resources/ioInput.txt').eachLine { line ->
lines.add(line)
}
The closure provided to eachLine also has a useful optional line number. Let’s use the line number to get only specific lines from a file:
def lineNoRange = 2..4
def lines = []
new File('src/main/resources/ioInput.txt').eachLine { line, lineNo ->
if (lineNoRange.contains(lineNo)) {
lines.add(line)
}
}
By default, the line numbering starts at one. We can provide a value to use as the first line number by passing it as the first parameter to the eachLine method.
Let’s start our line numbers at zero:
new File('src/main/resources/ioInput.txt').eachLine(0, { line, lineNo ->
if (lineNoRange.contains(lineNo)) {
lines.add(line)
}
})
If an exception is thrown in eachLine, Groovy makes sure the file resource gets closed. Much like a try-with-resources or a try-finally in Java.
2.2. Reading with Reader
We can also easily get a BufferedReader from a Groovy File object. We can use withReader to get a BufferedReader to the file object and pass it to a closure:
def actualCount = 0
new File('src/main/resources/ioInput.txt').withReader { reader ->
while(reader.readLine()) {
actualCount++
}
}
As with eachLine, the withReader method will automatically close the resource when an exception is thrown.
Sometimes, we might want to have the BufferedReader object available. For example, we might plan to call a method that takes one as a parameter. We can use the newReader method for this:
def outputPath = 'src/main/resources/ioOut.txt'
def reader = new File('src/main/resources/ioInput.txt').newReader()
new File(outputPath).append(reader)
reader.close()
Unlike the other methods we’ve looked at so far, *we’re responsible for closing the BufferedReader resource when we acquire a *Buffered**Reader this way.**
2.3. Reading with InputStreams
Similar to withReader and newReader, *Groovy also provides methods for easily working with InputStreams*. Although we can read text with InputStreams and Groovy even adds functionality for it, InputStreams are most commonly used for binary data.
Let’s use withInputStream to pass an InputStream to a closure and read in the bytes:
byte[] data = []
new File("src/main/resources/binaryExample.jpg").withInputStream { stream ->
data = stream.getBytes()
}
If we need to have the InputStream object, we can get one using 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()
As with the BufferedReader, we need to close our InputStream resource ourselves when we use newInputStream, but not when using withInputStream.
2.4. Reading Other Ways
Let’s finish the subject of reading by looking at a few methods Groovy has for grabbing all the file data in one statement.
If we want the lines of our file in a List, we can use collect with an iterator it passed to the closure:
def actualList = new File('src/main/resources/ioInput.txt').collect {it}
To get the lines of our file into an array of Strings, we can use as String[]:
def actualArray = new File('src/main/resources/ioInput.txt') as String[]
For short files, we can get the entire contents in a String using text:
def actualString = new File('src/main/resources/ioInput.txt').text
And when working with binary files, there’s the bytes method:
def contents = new File('src/main/resources/binaryExample.jpg').bytes
3. Writing Files
Before we start writing to files, let’s set up the text we’ll be outputting:
def outputLines = [
'Line one of output example',
'Line two of output example',
'Line three of output example'
]
3.1. Writing with Writer
As with reading a file, we can also easily get a BufferedWriter out of a File object.
Let’s use withWriter to get a BufferedWriter and pass it to a closure:
def outputFileName = 'src/main/resources/ioOutput.txt'
new File(outputFileName).withWriter { writer ->
outputLines.each { line ->
writer.writeLine line
}
}
Using withWriter will close the resource should an exception occur.
Groovy also has a method for getting the BufferedWriter object. Let’s get a BufferedWriter using newWriter:
def outputFileName = 'src/main/resources/ioOutput.txt'
def writer = new File(outputFileName).newWriter()
outputLines.forEach {line ->
writer.writeLine line
}
writer.flush()
writer.close()
We’re responsible for flushing and closing our BufferedWriter object when we use newWriter.
3.2. Writing with Output Streams
If we’re writing out binary data, we can get an OutputStream using either withOutputStream or newOutputStream.
Let’s write some bytes to a file using withOutputStream:
byte[] outBytes = [44, 88, 22]
new File(outputFileName).withOutputStream { stream ->
stream.write(outBytes)
}
Let’s get an OutputStream object with newOutputStream and use it to write some bytes:
byte[] outBytes = [44, 88, 22]
def os = new File(outputFileName).newOutputStream()
os.write(outBytes)
os.close()
Similarly to InputStream, BufferedReader, and BufferedWriter, we’re responsible for closing the OutputStream ourselves when we use newOutputStream.
3.3. Writing with the << Operator
As writing text to files is so common, the << operator provides this feature directly.
Let’s use the << operator to write some simple lines of text:
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. Writing Binary Data with Bytes
We saw earlier in the article that we can get all the bytes out of a binary file simply by accessing the bytes field.
Let’s write binary data the same way:
def outputFileName = 'src/main/resources/ioBinaryOutput.bin'
def outputFile = new File(outputFileName)
byte[] outBytes = [44, 88, 22]
outputFile.bytes = outBytes
4. Traversing File Trees
Groovy also provides us with easy ways to work with file trees. In this section, we’re going to do that with eachFile, eachDir and their variants and the traverse method.
4.1. Listing Files with eachFile
Let’s list all of the files and directories in a directory using eachFile:
new File('src/main/resources').eachFile { file ->
println file.name
}
Another common scenario when working with files is the need to filter the files based on file name. Let’s list only the files that start with “io” and end in “.txt” using eachFileMatch and a regular expression:
new File('src/main/resources').eachFileMatch(~/io.*\.txt/) { file ->
println file.name
}
The eachFile and eachFileMatch methods only list the contents of the top-level directory. Groovy also allows us to restrict what the eachFile methods return by passing a FileType to the methods. The options are ANY, FILES, and DIRECTORIES.
Let’s recursively list all the files using eachFileRecurse and providing it with a FileType of FILES:
new File('src/main').eachFileRecurse(FileType.FILES) { file ->
println "$file.parent $file.name"
}
The eachFile methods throw an IllegalArgumentException if we provide them with a path to a file instead of a directory.
Groovy also provides the eachDir methods for working with only directories. We can use eachDir and its variants to accomplish the same thing as using eachFile with a FileType of DIRECTORIES.
Let’s recursively list directories with eachFileRecurse:
new File('src/main').eachFileRecurse(FileType.DIRECTORIES) { file ->
println "$file.parent $file.name"
}
Now, let’s do the same thing with eachDirRecurse:
new File('src/main').eachDirRecurse { dir ->
println "$dir.parent $dir.name"
}
4.2. Listing Files with Traverse
For more complicated directory traversal use cases, we can use the traverse method. It functions similarly to eachFileRecurse but provides the ability to return FileVisitResult objects to control the processing.
Let’s use traverse on our src/main directory and skip processing the tree under the groovy directory:
new File('src/main').traverse { file ->
if (file.directory && file.name == 'groovy') {
FileVisitResult.SKIP_SUBTREE
} else {
println "$file.parent - $file.name"
}
}
5. Working with Data and Objects
5.1. Serializing Primitives
In Java, we can use DataInputStream and DataOutputStream to serialize primitive data fields. Groovy adds useful expansions here as well.
Let’s set up some primitive data:
String message = 'This is a serialized string'
int length = message.length()
boolean valid = true
Now, let’s serialize our data to a file using withDataOutputStream:
new File('src/main/resources/ioData.txt').withDataOutputStream { out ->
out.writeUTF(message)
out.writeInt(length)
out.writeBoolean(valid)
}
And read it back in using 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()
}
Similar to the other with* methods, withDataOutputStream and withDataInputStream pass the stream to the closure and ensure it’s closed properly.
5.2. Serializing Objects
Groovy also builds upon Java’s ObjectInputStream and ObjectOutputStream to allow us to easily serialize objects that implement Serializable.
Let’s first define a class that implements Serializable:
class Task implements Serializable {
String description
Date startDate
Date dueDate
int status
}
Now let’s create an instance of Task that we can serialize to a file:
Task task = new Task(description:'Take out the trash', startDate:new Date(), status:0)
With our Task object in hand, let’s serialize it to a file using withObjectOutputStream:
new File('src/main/resources/ioSerializedObject.txt').withObjectOutputStream { out ->
out.writeObject(task)
}
Finally, let’s read our Task back in using withObjectInputStream:
Task taskRead
new File('src/main/resources/ioSerializedObject.txt').withObjectInputStream { is ->
taskRead = is.readObject()
}
The methods we used, withObjectOutputStream and withObjectInputStream, pass the stream to a closure and handle closing the resources appropriately, just as with seen with the other with* methods.
6. Conclusion
In this article, we explored functionality that Groovy adds onto existing Java File I/O classes. We used this functionality to read and write files, work with directory structures, and serialize data and objects.
We only touched on a few of the helper methods, so it’s worth digging into Groovy’s documentation to see what else it adds to Java’s I/O functionality.
The example code is available over on GitHub.