Guida all'I / O in Groovy

1. Introduzione

Sebbene in Groovy possiamo lavorare con I / O proprio come facciamo in Java, Groovy espande le funzionalità di I / O di Java con una serie di metodi di supporto.

In questo tutorial, esamineremo la lettura e la scrittura di file, l'attraversamento di file system e la serializzazione di dati e oggetti tramite i metodi di estensione dei file di Groovy .

Laddove applicabile, collegheremo i nostri articoli Java pertinenti per un facile confronto con l'equivalente Java.

2. Lettura di file

Groovy aggiunge una comoda funzionalità per leggere i file sotto forma di metodi eachLine , metodi per ottenere BufferedReader e InputStream e modi per ottenere tutti i dati del file con una riga di codice.

Java 7 e Java 8 hanno un supporto simile per la lettura di file in Java.

2.1. Leggere con eachLine

Quando si ha a che fare con file di testo, spesso è necessario leggere ogni riga ed elaborarla. Groovy fornisce una comoda estensione a java.io.File con il metodo eachLine :

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

La chiusura fornita a eachLine ha anche un utile numero di riga opzionale. Usiamo il numero di riga per ottenere solo righe specifiche da un file:

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

Per impostazione predefinita, la numerazione delle righe inizia da uno. Possiamo fornire un valore da usare come primo numero di riga passandolo come primo parametro al metodo eachLine .

Iniziamo i nostri numeri di riga da zero:

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

Se viene generata un'eccezione in eachLine, Groovy si assicura che la risorsa del file venga chiusa . Proprio come una prova con risorse o una prova finale in Java.

2.2. Leggere con Reader

Possiamo anche ottenere facilmente un BufferedReader da un oggetto Groovy File . Possiamo usare withReader per ottenere un BufferedReader per l'oggetto file e passarlo a una chiusura:

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

Come con eachLine , il metodo withReader chiuderà automaticamente la risorsa quando viene generata un'eccezione.

A volte, potremmo voler avere l' oggetto BufferedReader disponibile. Ad esempio, potremmo pianificare di chiamare un metodo che ne accetta uno come parametro. Possiamo usare il metodo newReader per questo:

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

A differenza degli altri metodi esaminati finora, siamo responsabili della chiusura della risorsa BufferedReader quando acquisiamo un Buffered Reader in questo modo.

2.3. Leggere con InputStream s

Simile a withReader e newReader , Groovy fornisce anche metodi per facilmente lavorare con InputStream s . Sebbene possiamo leggere il testo con InputStream se Groovy aggiunge anche funzionalità per esso, InputStream sono più comunemente usati per i dati binari.

Usiamo withInputStream per passare un InputStream a una chiusura e leggere i byte:

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

Se dobbiamo avere l' oggetto InputStream , possiamo ottenerne uno usando 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()

Come con BufferedReader , dobbiamo chiudere noi stessi la nostra risorsa InputStream quando usiamo newInputStream, ma non quando usiamo withInputStream .

2.4. Leggere altri modi

Terminiamo l'argomento della lettura esaminando alcuni metodi che Groovy ha per acquisire tutti i dati del file in un'unica istruzione.

Se vogliamo che le linee del nostro file in un elenco , possiamo usare Collect con un iteratore si passò alla chiusura:

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

Per ottenere le righe del nostro file in un array di stringhe , possiamo usare come String [] :

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

Per file brevi, possiamo ottenere l'intero contenuto in una stringa utilizzando il testo :

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

E quando si lavora con file binari, c'è il metodo bytes :

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

3. Scrittura di file

Prima di iniziare a scrivere sui file, impostiamo il testo che emetteremo:

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

3.1. Scrivere con Writer

Come con la lettura di un file, possiamo anche ottenere facilmente un BufferedWriter da un oggetto File .

Usiamo withWriter per ottenere un BufferedWriter e passarlo a una chiusura:

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

L'utilizzo di withReader chiuderà la risorsa in caso di eccezione.

Groovy ha anche un metodo per ottenere l' oggetto BufferedWriter . Otteniamo un BufferedWriter usando 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) }

Infine, rileggiamo la nostra attività utilizzando withObjectInputStream :

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

I metodi che abbiamo usato, withObjectOutputStream e withObjectInputStream , passano il flusso a una chiusura e gestiscono la chiusura delle risorse in modo appropriato, proprio come con gli altri metodi * .

6. Conclusione

In questo articolo, abbiamo esplorato le funzionalità che Groovy aggiunge alle classi I / O di file Java esistenti. Abbiamo utilizzato questa funzionalità per leggere e scrivere file, lavorare con strutture di directory e serializzare dati e oggetti.

Abbiamo toccato solo alcuni dei metodi di supporto, quindi vale la pena scavare nella documentazione di Groovy per vedere cos'altro aggiunge alla funzionalità I / O di Java.

Il codice di esempio è disponibile su GitHub.