«1. Введение

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

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

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

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

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

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)
}

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

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. Чтение с помощью Reader

Мы также можем легко получить 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, когда мы таким образом получаем BufferedReader.

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

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

Давайте используем 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}

Чтобы получить строки нашего файла в массив строк, мы можем использовать as 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. Запись с помощью Writer

Как и при чтении файла, мы также можем легко получить 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. Запись с потоками вывода

Если мы записываем двоичные данные, мы можем получить поток вывода, используя либо 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. Запись с помощью оператора \u003c\u003c

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

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

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. Запись двоичных данных с помощью байтов

Ранее в статье мы видели, что мы можем получить все байты из двоичного файла, просто обратившись к полю байтов.

Давайте запишем двоичные данные таким же образом:

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

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

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

4.1. Список файлов с помощью eachFile

Давайте перечислим все файлы и каталоги в каталоге, используя 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.