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 .