Статьи

Работа с файлами и каталогами в NIO.2

В предыдущих статьях я обсуждал создание ( создание файлов и каталогов ) и выбор (просмотр и фильтрация содержимого каталогов ) файлов и каталогов. Последний логичный шаг — выяснить, что мы можем с ними сделать и как. Это часть библиотеки, которая была значительно переработана. Обновления в этой области включают гарантию атомарности определенных операций, улучшения API, оптимизацию производительности, а также введение надлежащей иерархии исключений, которая заменила методы boolean возврата из предыдущих версий библиотеки ввода-вывода.

Открытие файла

Прежде чем приступить к чтению и записи в файл, нам необходимо рассмотреть один общий принцип этих операций — способ открытия файлов. Способ открытия файлов напрямую влияет на результаты этих операций, а также на их производительность. Давайте взглянем на стандартные параметры открытия файлов, содержащиеся в enum java.nio.file.StandardOpenOption :

Стандартные открытые опции
Значение Описание
APPEND Если файл открыт для доступа WRITE, то байты будут записаны в конец файла, а не в его начало.
CREATE Создайте новый файл, если он не существует.
CREATE_NEW Создайте новый файл, если файл уже существует.
DELETE_ON_CLOSE Удалить по закрытию.
DSYNC Требует, чтобы каждое обновление содержимого файла было записано синхронно на базовое устройство хранения.
READ Открыть для чтения.
SPARSE Разреженный файл.
SYNC Требует, чтобы каждое обновление содержимого файла или метаданных было записано синхронно на базовое устройство хранения.
TRUNCATE_EXISTING Если файл уже существует и он открыт для доступа WRITE, его длина сокращается до 0.
WRITE Открыть для записи.

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

Чтение файла

Когда дело доходит до чтения файлов, NIO.2 предлагает несколько способов сделать это — каждый со своими плюсами и минусами. Эти подходы заключаются в следующем:

  • Чтение файла в байтовый массив
  • Использование небуферизованных потоков
  • Использование буферизованных потоков

Давайте посмотрим на первый вариант. Class Files предоставляет метод readAllBytes для выполнения именно этого. Чтение файла в байтовый массив кажется довольно простым действием, но это может подойти только для очень ограниченного диапазона файлов. Поскольку мы помещаем весь файл в память, мы должны учитывать его размер. Использование этого метода целесообразно только тогда, когда мы пытаемся читать небольшие файлы, и это можно сделать мгновенно. Это довольно простая операция, представленная в следующем фрагменте кода:

01
02
03
04
05
06
07
08
09
10
11
12
Path filePath = Paths.get("C:", "a.txt");
 
if (Files.exists(filePath)) {
    try {
        byte[] bytes = Files.readAllBytes(filePath);
        String text = new String(bytes, StandardCharsets.UTF_8);
 
        System.out.println(text);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

Приведенный выше код сначала считывает файл в байтовый массив, а затем создает строковый объект, содержащий содержимое указанного файла, со следующим выводом:

1
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sit amet justo nec leo euismod porttitor. Vestibulum id sagittis nulla, eu posuere sem. Cras commodo, massa sed semper elementum, ligula orci malesuada tortor, sed iaculis ligula ligula et ipsum.

Когда нам нужно прочитать содержимое файла в виде строки, мы можем использовать приведенный выше код. Однако это решение не так уж чисто, и мы можем использовать readAllLines из класса Files чтобы избежать этой неуклюжей конструкции. Этот метод служит удобным решением для чтения файлов, когда нам нужен построчно читаемый человеком вывод. Использование этого метода еще раз довольно просто и очень похоже на предыдущий пример (применяются те же ограничения):

01
02
03
04
05
06
07
08
09
10
11
12
13
Path filePath = Paths.get("C:", "b.txt");
 
if (Files.exists(filePath)) {
    try {
        List<String> lines = Files.readAllLines(filePath, StandardCharsets.UTF_8);
 
        for (String line : lines) {
            System.out.println(line);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

Со следующим выводом:

1
2
3
4
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aliquam sit amet justo nec leo euismod porttitor.
Vestibulum id sagittis nulla, eu posuere sem.
Cras commodo, massa sed semper elementum, ligula orci malesuada tortor, sed iaculis ligula ligula et ipsum.

Чтение файла с использованием потоков

Переходя к более сложным подходам, мы всегда можем использовать старые добрые потоки, как это было в предыдущих версиях библиотеки. Поскольку это хорошо известное основание, я только покажу, как получить экземпляры этих потоков. Прежде всего, мы можем извлечь экземпляр InputStream из класса Files , вызвав метод newInputStream . Как обычно, можно дополнительно поиграть с шаблоном декоратора и сделать из него буферизованный поток. Или для удобства используйте метод newBufferedReader . Оба метода возвращают экземпляр потока, который является простым старым объектом java.io

1
2
3
4
5
6
7
8
Path filePath1 = Paths.get("C:", "a.txt");
Path filePath2 = Paths.get("C:", "b.txt");
 
InputStream is = Files.newInputStream(filePath1);
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
 
BufferedReader reader = Files.newBufferedReader(filePath2, StandardCharsets.UTF_8);

Запись в файл

Запись в файл аналогична процессу чтения в ряде инструментов, предоставляемых библиотекой NIO.2, поэтому давайте просто рассмотрим:

  • Запись байтового массива в файл
  • Использование небуферизованных потоков
  • Использование буферизованных потоков

Еще раз давайте сначала рассмотрим опцию байтового массива. Неудивительно, что у класса Files есть две версии метода write . Либо мы пишем байты из массива, либо из строк текста, нам необходимо сосредоточиться на StandardOpenOptions поскольку выбор этих модификаторов может зависеть от обоих методов. По умолчанию, когда метод StandardOpenOption не передается методу, метод write ведет себя так, как если бы присутствовали параметры CREATE , TRUNCATE_EXISTING и WRITE (как указано в Javadoc). Сказав это, пожалуйста, остерегайтесь использования версии метода write по умолчанию (без открытых опций), поскольку он либо создает новый файл, либо изначально обрезает существующий файл до нулевого размера. Файл автоматически закрывается по окончании записи — как после успешной записи, так и после возникновения исключения. Когда дело доходит до размеров файлов, применяются те же ограничения, что и в readAllBytes .

В следующем примере показано, как записать байтовый массив в файл. Обратите внимание на отсутствие какого-либо метода проверки из-за поведения метода write по умолчанию. Этот пример может быть запущен несколько раз с двумя разными результатами. Первый запуск создает файл, открывает его для записи и записывает байты из bytes массива в этот файл. Любой последующий вызов этого кода сотрет файл и запишет содержимое массива bytes в этот пустой файл. Оба запуска приведут к закрытому файлу с текстом «Hello world!» написано в первой строке.

1
2
3
4
5
6
7
8
Path newFilePath = Paths.get("/home/jstas/a.txt");
byte[] bytes = new byte[] {0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21};
 
try {
    Files.write(newFilePath, bytes);
} catch(IOException e) {
    throw new RuntimeException(e);
}

Когда нам нужно писать строки вместо байтов, мы можем преобразовать строку в байтовый массив, однако есть и более удобный способ сделать это. Просто подготовьте список строк и передайте его для write метода. Обратите внимание на использование двух StandardOpenOption в следующем примере. Используя их для опций, я уверен, что файл присутствует (если он не существует, он создается) и способ добавить данные в этот файл (таким образом, не теряя ранее записанные данные). Весь пример довольно прост, посмотрите:

01
02
03
04
05
06
07
08
09
10
11
12
13
Path filePath = Paths.get("/home/jstas/b.txt");
 
List<String> lines = new ArrayList<>();
lines.add("Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
lines.add("Aliquam sit amet justo nec leo euismod porttitor.");
lines.add("Vestibulum id sagittis nulla, eu posuere sem.");
lines.add("Cras commodo, massa sed semper elementum, ligula orci malesuada tortor, sed iaculis ligula ligula et ipsum.");
 
try {
    Files.write(filePath, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
    throw new RuntimeException(e);
}

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

Может быть не очень хорошая идея работать с байтовыми массивами, когда дело касается больших файлов. Это когда приходят потоки. Подобно чтению главы, я не собираюсь объяснять потоки или как их использовать. Я бы предпочел сосредоточиться на способе получить их экземпляры. Class Files предоставляет метод newOutputStream который принимает StandardOpenOption для настройки поведения потоков. По умолчанию, когда метод StandardOpenOption не передается методу, метод write потоков ведет себя так, как если бы присутствовали параметры CREATE , TRUNCATE_EXISTING и WRITE (как указано в Javadoc). Этот поток не буферизуется, но с небольшим количеством магии декоратора вы можете создать экземпляр BufferedWriter . Чтобы противостоять этому неудобству, NIO.2 поставляется с методом newBufferWriter который сразу создает экземпляр буферизованного потока. Оба способа показаны в следующем фрагменте кода:

1
2
3
4
5
6
7
8
Path filePath1 = Paths.get("/home/jstas/c.txt");
Path filePath2 = Paths.get("/home/jstas/d.txt");
 
OutputStream os = Files.newOutputStream(filePath1);
OutputStreamWriter osw = new OutputStreamWriter(os);
BufferedWriter bw = new BufferedWriter(osw);
 
BufferedWriter writer = Files.newBufferedWriter(filePath2, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);

Копирование и перемещение файлов и каталогов

Копирование файлов и каталогов

Одной из наиболее полезных функций NIO.2 является обновленный способ обработки копирования и перемещения файлов и каталогов. Чтобы все было в порядке, дизайнеры решили OpenOption два родительских (маркерных) интерфейса в новый API файловой системы: OpenOption и CopyOption (оба интерфейса из пакета java.nio.file ). OpenOption StandardOpenOption упомянутое в предыдущей главе, реализует интерфейс OpenOption . Интерфейс CopyOption с другой стороны, имеет две реализации, одну из которых мы уже встречали в посте о ссылках в NIO.2 . Некоторые из вас могут вспомнить перечисление LinkOption которое называется методами управления реализацией, связанными с операциями, связанными со ссылками. Однако есть и другая реализация — перечисление StandardCopyOption из пакета java.nio.file . Еще раз, мы представляем еще одно перечисление — используемое для управления операциями копирования. Поэтому, прежде чем мы перейдем к какому-либо коду, рассмотрим, чего мы можем достичь, используя различные варианты копирования.

Стандартные параметры копирования
Значение Описание
ATOMIC_MOVE Переместите файл как элементарную операцию файловой системы.
COPY_ATTRIBUTES Скопируйте атрибуты в новый файл.
REPLACE_EXISTING Замените существующий файл, если он существует.

Использование этих опций для управления операциями ввода-вывода довольно элегантно, а также просто. Поскольку мы пытаемся скопировать файл, ATOMIC_MOVE не имеет особого смысла в использовании (вы все равно можете его использовать, но в итоге вы получите java.lang.UnsupportedOperationException: Unsupported copy option ). Class Files предоставляет 3 варианта метода copy для разных целей:

  • copy(InputStream in, Path target, CopyOption... options)
    • Копирует все байты из входного потока в файл.
  • copy(Path source, OutputStream out)
    • Копирует все байты из файла в выходной поток.
  • copy(Path source, Path target, CopyOption... options)
    • Скопируйте файл в целевой файл.

Прежде чем мы перейдем к какому-либо коду, я считаю, что полезно понять наиболее важные поведенческие особенности метода copy (последний вариант из трех выше). Метод copy ведет себя следующим образом (на основе Javadoc):

  • По умолчанию копирование не выполняется, если целевой файл уже существует или является символической ссылкой.
  • Если источник и цель — один и тот же файл, метод завершается без копирования файла. (для дальнейшей информации проверьте метод isSameFile класса Files )
  • Атрибуты файла не обязательно копировать в целевой файл.
  • Если исходный файл является каталогом, то он создает пустой каталог в целевом местоположении (записи в каталоге не копируются).
  • Копирование файла не является атомарной операцией.
  • Пользовательские реализации могут принести новые конкретные параметры.

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

1
2
3
4
5
6
7
8
Path source = Paths.get("/home/jstas/a.txt");
Path target = Paths.get("/home/jstas/A/a.txt");
 
try {
    Files.copy(source, target, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    throw new RuntimeException(e);
}

Здесь нет больших сюрпризов — код копирует исходный файл с его атрибутами. Если вы чувствуете, что я забыл о (не пустых) каталогах, позвольте мне заверить вас, что я этого не сделал. Также можно использовать NIO.2 для копирования, перемещения или удаления заполненных каталогов, но об этом я расскажу в следующем посте, поэтому вам придется подождать пару дней.

Перемещение файлов и каталогов

Когда дело доходит до перемещения файлов, нам снова нужно иметь возможность указать параметры, управляющие процессом move метода из класса Files . Здесь мы используем StandardCopyOptions упомянутые в предыдущей главе. Два соответствующих параметра — ATOMIC_MOVE и REPLACE_EXISTING . Прежде всего, давайте начнем с некоторых основных характеристик, а затем перейдем к примеру кода:

  • По умолчанию метод move не выполняется, если целевой файл уже существует.
  • Если источник и цель — один и тот же файл, метод завершается без перемещения файла. (для дальнейшей информации проверьте метод isSameFile класса Files )
  • Если источником является символическая ссылка, то сама ссылка перемещается.
  • Если исходный файл является каталогом, он должен быть пустым для перемещения.
  • Атрибуты файла не требуется перемещать.
  • Перемещение файла может быть настроено как атомарная операция, но не обязательно.
  • Пользовательские реализации могут принести новые конкретные параметры.

Код довольно прост, поэтому давайте посмотрим на следующий фрагмент кода:

1
2
3
4
5
6
7
8
Path source = Paths.get("/home/jstas/b.txt");
Path target = Paths.get("/home/jstas/A/b.txt");
 
try {
    Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch(IOException e) {
    throw new RuntimeException(e);
}

Как и ожидалось, код перемещает исходный файл в элементарной операции.

Удаление файлов и каталогов

Последняя часть этой статьи посвящена удалению файлов и каталогов. Удаление файлов, опять же, довольно просто с двумя возможными методами для вызова (как обычно, из класса Files ):

  • public static void delete(Path path)
  • public static boolean deleteIfExists(Path path)

Одинаковые правила регулируют оба метода:

  • По умолчанию метод удаления завершается с ошибкой DirectoryNotEmptyException когда файл является каталогом и не является пустым.
  • Если файл является символической ссылкой, то сама ссылка удаляется.
  • Удаление файла не может быть атомарной операцией.
  • Файлы не могут быть удалены, если они открыты или используются JVM или другим программным обеспечением.
  • Пользовательские реализации могут принести новые конкретные параметры.
01
02
03
04
05
06
07
08
09
10
11
Path newFile = Paths.get("/home/jstas/c.txt");
Path nonExistingFile = Paths.get("/home/jstas/d.txt");
 
try {
    Files.createFile(newFile);
    Files.delete(newFile);
 
    System.out.println("Any file deleted: " + Files.deleteIfExists(nonExistingFile));
} catch(IOException e) {
    throw new RuntimeException(e);
}

С выходом:

1
Any file deleted: false
Ссылка: Работа с файлами и каталогами в NIO.2 от нашего партнера JCG Якуба Стаса в блоге