Программы, которые мы пишем, так или иначе должны взаимодействовать с пользователем и внешней средой — операционной системой, устройствами компьютера, сетью Интернет. Простейшим способом взаимодействия является ввод с консоли и вывод на консоль, но сейчас такой способ применяется крайне редко. Более совершенным способом взаимодействия являются файлы — многие программы берут из них настроечную или входную информацию, и используют их для сохранения результатов своей работы или различных настроек. Например, всевозможные редакторы позволяют открывать файлы в определённом формате, просматривать и/или изменять их, сохранять файлы на диске компьютера или в сети Интернет.

В библиотеке Java внутри пакета java.io имеется ряд типов, обеспечивающих возможность работы с файлами, а библиотека Котлина дополняет их некоторыми удобными возможностями. Как обычно, рассмотрим часть этих возможностей на примере.

Краеугольный тип, используемый для работы с файлами в Котлине — тип java.io.File. В соответствии с названием, он предназначен для различных операций с файлами; объект этого типа соответствует какому-либо реальному файлу, чаще всего находящемуся на жёстком диске. Для создания файла используется специальная функция-конструктор: File(inputName) или File(outputName) в примере. Если в аргументе конструктора указано только имя файла — поиск файла происходит в текущей директории, а если аргумент содержим также путь к файлу — то в директории, указанной этим путём. Специфика конструктора заключается в том, что его имя совпадает с типом объекта, которую он создаёт, и он имеет результат соответствующего типа. Более подробно мы поговорим о конструкторах в следующем уроке.

Обмен данными с файлом может происходить в режиме чтения либо в режиме записи. В режиме чтения информации, заданное имя должно соответствовать уже существующему файлу. Один из способов получения информации из файла — вызов функции file.readLines(). Результат вызова — список строк, из которых состоит файл. Каждый Stringв этом списке соответствует одной строке файла, строки файла разделяются символом «возврат каретки» и / или «новая строка».

В режиме записи информации, заданное имя может не соответствовать существующему файлу — в этом случае он будет создан. Для записи информации, необходимо создать один из объектов, обеспечивающих такую возможность. В примере, таким объектом является val writer = File(outputName).bufferedWriter() — то есть необходимо вызвать функцию bufferedWriter() на получателе, соответствующем исходному файлу. Как видно из текста примера, writer(писатель) имеет функции writer.newLine() (добавление в файл новой строки), writer.write(string) (добавление в файл заданной строки) и writer.close() (закрытие писателя, выполняется строго ПОСЛЕ выполнения всех остальных действий и фиксирует итоговое состояние файла).

Мы перечислили все файловые операции, присутствующие в исходном примере. Внутри цикла for, каждая из строк файла разбивается по пробелам на слова, с этой целью используется Regex("\\s+"). В currentLineLengthнакапливается длина текущей строки ВЫХОДНОГО файла. Если в текущей строке достаточно места для очередного слова ВХОДНОГО файла, слово добавляется в текущую строку, в противном случае в файл добавляется перевод строки и слово добавляется в новую строку. Пустые строки входного файла, как и сказано в задании, переносятся в выходной файл без изменений.

За занавесом: чтение из файла

Пакет java.io позволяет работать с файлами на трёх разных уровнях:

  1. Уровень отдельных байт. В этом случае файл воспринимается как массив или, точнее, как поток байт. Поток, в отличие от массива, можно только перебирать, с сильно ограниченными возможностями по возвращению назад. Для этой цели имеется тип java.io.InputStream.
  2. Уровень символов. В этом случае файл воспринимается уже как поток символов типа Char, то есть каждые несколько байт файла превращаются в определённый символ — с учётом заданной кодировки файла. Для этой цели имеется тип java.io.InputStreamReader, который внутри себя использует InputStream для чтения байт.
  3. Уровень строк. На этом уровне файл воспринимается как набор строк String, составленных из символов по определённым правилам — чаще всего используется разделение по отдельным строкам файла. Эту роль выполняет тип java.io.BufferedReader, использующий внутри себя InputStreamReader для чтения символов.

При программировании на Java каждый из этих объектов приходится создавать отдельно — вначале InputStream, потом InputStreamReader и, наконец, BufferedReader. Библиотека Котлина позволяет создать любой из этих объектов сразу, используя файл-получатель:

  1. file.inputStream() создаёт байтовый поток.
  2. file.reader() создаёт читатель символов, используя кодировку по умолчанию. file.reader(Charset.forName("CP1251")) создаёт писатель с заданной кодировкой (в данном случае CP1251).
  3. Наконец, file.bufferedReader() создаёт буферизованный читатель строк. Опять-таки, может быть задана нужная кодировка, иначе используется кодировка по умолчанию.

Набор функций у данных трёх объектов различается. У всех у них есть функция close(), закрывающая исходный файл в конце работы с потоком. Также, у них имеется функция высшего порядка use { …​ }, выполняющая описанные в лямбде действия и закрывающая файл в конце своей работы автоматически. Скажем, исходный пример можно было бы переписать с помощью use так:

Здесь исходный BufferedWriter в лямбде становится параметром it. Заметим, что при использовании use исходный файл будет закрыт как при корректном завершении функции, так и при возникновении исключения.

Кроме этого, каждый объект обладает своими методами для чтения информации:

  1. inputStream.read() читает из InputStream очередной байт, возвращая его в виде результата типа Int. Если файл закончен, результат этой функции будет -1. inputStream.read(byteArray) читает сразу несколько байт, записывая их в массив байт (число прочитанных байт равно размеру массива). inputStream.read(byteArray, offset, length)записывает в byteArray length байт, начиная с индекса offset.
  2. reader.read() читает из InputStreamReader очередной символ, возвращая его в виде результата типа Int. Здесь используется именно Int, а не Char, так как, во-первых, символ в общем случае может не поместиться в двухбайтовые тип и, во-вторых, чтобы вернуть -1 в случае неудачи. Есть аналогичные методы для чтения символьного массива (НЕ строки) с возможным указанием смещения и числа символов — см. выше про байтовый массив.
  3. bufferedReader.readLine() читает из BufferedReader очередную строку (до перевода строки). bufferedReader.readLines() читает сразу же все строки. Есть ряд других методов для работы со строками по отдельности.

Следует отметить, что все функции чтения информации могут бросить исключение IOException в том случае, если чтение по какой-либо причине невозможно (например, если файл не существует или недоступен).

В примере, мы вообще не создавали bufferedReader, а использовали функцию file.readLines(). Она создаёт bufferedReader внутри себя и обращается к его функции readLines(). После чтения последней строки файл закрывается.

За занавесом: запись в файл

Запись в файл использует те же три уровня: байты OutputStream, символы OutputStreamWriter и строки BufferedWriter. Для записи байт либо символов используются функции write, аргументом которых может являться целое число (в котором хранится байт или код символа) или массив (опять-таки байт или символов). Эти функции не имеют результата и бросают IOException, если файл недоступен для записи. BufferedWriter может использовать функцию write также для записи строк. Как и все три вида потоков чтения, потоки записи необходимо закрывать после использования с помощью close() или use { …​ }.

Сверх этого, для записи часто используется так называемый поток печати PrintStream. В Котлине его можно создать из файла, используя функцию file.printStream(). Поток печати расширяет обычный байтовый поток рядом дополнительных возможностей:

  1. printStream.println(…​) — вывод заданной строки или строкового представления с последующим переходом на новую строку.
  2. printStream.print(…​) — то же, но без перехода на новую строку.
  3. printStream.format(formatString, …​) — форматированный вывод (происходит по принципу, описанном в разделе 6).

Упражнения

Откройте файл srс/lesson7/task1/Files.kt в проекте KotlinAsFirst. Он содержит ряд задач, каждая из которых предполагает наличие входного и/или выходного файла. Решите хотя бы одну-две из имеющихся задач, используя описанные в этом разделе приёмы. Обратите внимание на задачи, помеченные как «Сложная» или «Очень сложная», попробуйте решить одну из них.

Протестируйте свою реализацию, используя тесты из test/lesson7/task1/Tests.kt. Обратите внимание, что тесты используют готовые входные файлы, расположенные в директории input нашего проекта. Убедитесь, что тесты успешно проходят, обязательно создайте два-три дополнительных теста. Постарайтесь внутри этих тестов проверить какие-либо необычные ситуации, которые могут возникнуть в выбранной вами задаче.

Добавить комментарий