Перейти к основному содержимому

Руководство по вводу-выводу в Groovy

· 9 мин. чтения

1. Введение

Хотя в Groovy мы можем работать с вводом-выводом так же, как в Java, Groovy расширяет функциональные возможности ввода-вывода Java с помощью ряда вспомогательных методов.

В этом руководстве мы рассмотрим чтение и запись файлов, обход файловых систем и сериализацию данных и объектов с помощью методов расширения файлов Groovy.

Там, где это применимо, мы будем давать ссылки на наши соответствующие статьи по Java, чтобы упростить сравнение с эквивалентом Java.

2. Чтение файлов

Groovy добавляет удобную функциональность для чтения файлов в виде методов eachLine , методов получения BufferedReader и InputStream и способов получения всех данных файла с помощью одной строки кода.

Java 7 и Java 8 имеют аналогичную поддержку чтения файлов в Java .

2.1. Чтение с каждой строкой

При работе с текстовыми файлами нам часто приходится читать каждую строку и обрабатывать ее. Groovy предоставляет удобное расширение для java.io.File с помощью метода eachLine :

def lines = []

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

Замыкание, предоставляемое каждой строке , также имеет полезный необязательный номер строки. Давайте используем номер строки, чтобы получить только определенные строки из файла:

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

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

По умолчанию нумерация строк начинается с единицы. Мы можем указать значение для использования в качестве номера первой строки, передав его в качестве первого параметра методу eachLine .

Давайте начнем наши номера строк с нуля:

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

Если в каждой строке генерируется исключение , Groovy обеспечивает закрытие файлового ресурса . Очень похоже на try-with-resources или try-finally в Java.

2.2. Чтение с читателем

Мы также можем легко получить BufferedReader из объекта Groovy File . Мы можем использовать 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()

В отличие от других методов, которые мы рассматривали до сих пор, мы отвечаем за закрытие ресурса BufferedReader , когда таким образом получаем Buffered Reader .

2.3. Чтение с помощью InputStream s

Подобно withReader и newReader , Groovy также предоставляет методы для простой работы с InputStream s . Хотя мы можем читать текст с помощью InputStream s, а Groovy даже добавляет для этого функциональность, InputStream чаще всего используется для двоичных данных.

Давайте используем withInputStream для передачи InputStream замыканию и считывания байтов:

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 , нам нужно самим закрыть наш ресурс InputStream , когда мы используем newInputStream, но не при использовании withInputStream .

2.4. Чтение других способов

Давайте закончим тему чтения, рассмотрев несколько методов Groovy для захвата всех данных файла в одном выражении.

Если нам нужны строки нашего файла в списке , мы можем использовать collect с итератором , который он передал замыканию:

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

Чтобы получить строки нашего файла в массив Strings , мы можем использовать как String[] :

def actualArray = new File('src/main/resources/ioInput.txt') as 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. Пишу с писателем

Как и при чтении файла, мы также можем легко получить BufferedWriter из объекта File .

Давайте воспользуемся withWriter , чтобы получить BufferedWriter и передать его замыканию:

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

Использование withReader закроет ресурс в случае возникновения исключения.

В Groovy также есть метод получения объекта BufferedWriter . Давайте получим BufferedWriter , используя newWriter :

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

Мы несем ответственность за очистку и закрытие нашего объекта BufferedWriter при использовании newWriter .

3.2. Запись с выходными потоками

Если мы записываем двоичные данные, мы можем получить OutputStream , используя withOutputStream или newOutputStream .

Давайте запишем несколько байтов в файл, используя withOutputStream :

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

Давайте получим объект OutputStream с помощью newOutputStream и используем его для записи нескольких байтов:

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

Подобно InputStream , BufferedReader и BufferedWriter , мы сами отвечаем за закрытие OutputStream при использовании newOutputStream .

3.3. Запись с оператором <<

Поскольку запись текста в файлы очень распространена, оператор << обеспечивает эту функцию напрямую.

Давайте воспользуемся оператором << , чтобы написать несколько простых строк текста:

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 .

Запишем бинарные данные таким же образом:

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

4. Обход файловых деревьев

Groovy также предлагает простые способы работы с файловыми деревьями. В этом разделе мы собираемся сделать это с eachFile , eachDir и их вариантами, а также методом traverse .

4.1. Список файлов с каждым файлом

Давайте перечислим все файлы и каталоги в каталоге, используя eachFile :

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

Другим распространенным сценарием при работе с файлами является необходимость фильтрации файлов на основе имени файла. Давайте перечислим только те файлы, которые начинаются с «io» и заканчиваются на «.txt», используя eachFileMatch и регулярное выражение:

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

Методы eachFile и eachFileMatch отображают только содержимое каталога верхнего уровня. Groovy также позволяет нам ограничивать то, что возвращают методы eachFile , передавая им FileType . Возможные варианты: ЛЮБЫЕ , ФАЙЛЫ и КАТАЛОГИ .

Давайте рекурсивно перечислим все файлы, используя eachFileRecurse и предоставив ему FileType FILES :

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

Методы eachFile генерируют исключение IllegalArgumentException , если мы предоставляем им путь к файлу вместо каталога.

Groovy также предоставляет методы eachDir для работы только с каталогами. Мы можем использовать eachDir и его варианты, чтобы добиться того же, что и при использовании eachFile с типом файла 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

Для более сложных вариантов использования обхода каталога мы можем использовать метод обхода . Он работает аналогично eachFileRecurse, но предоставляет возможность возвращать объекты FileVisitResult для управления обработкой.

Давайте воспользуемся обходом нашего каталога 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 мы можем использовать DataInputStream и DataOutputStream для сериализации примитивных полей данных . 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* , withDataOutputStream и withDataInputStream передают поток замыканию и обеспечивают его правильное закрытие.

5.2. Сериализация объектов

Groovy также основывается на ObjectInputStream и ObjectOutputStream Java, что позволяет нам легко сериализовать объекты, реализующие 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)

Имея в руках наш объект Task , давайте сериализуем его в файл, используя 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()
}

Используемые нами методы withObjectOutputStream и withObjectInputStream передают поток замыканию и соответствующим образом обрабатывают закрытие ресурсов, как и в случае с другими методами with* .

6. Заключение

В этой статье мы рассмотрели функциональные возможности, которые Groovy добавляет к существующим классам файлового ввода-вывода Java. Мы использовали эту функциональность для чтения и записи файлов, работы со структурами каталогов и сериализации данных и объектов.

Мы коснулись лишь нескольких вспомогательных методов, поэтому стоит заглянуть в документацию Groovy, чтобы узнать, что еще он добавляет к функциям ввода-вывода Java.

Код примера доступен на GitHub .