Введение в файловый API Java NIO2

1. Обзор

В этой статье мы собираемся сосредоточиться на новых API ввода-вывода в платформе Java - NIO2 - для выполнения основных манипуляций с файлами .

Файловые API в NIO2 представляют собой одну из основных новых функциональных областей платформы Java, которая поставляется с Java 7, в частности подмножество нового API файловой системы наряду с API-интерфейсами Path.

2. Настроить

Настройка вашего проекта для использования File APIs - это всего лишь вопрос импорта:

import java.nio.file.** ;

Поскольку примеры кода в этой статье, вероятно, будут работать в разных средах, давайте разберемся с домашним каталогом пользователя, который будет действителен во всех операционных системах:

private static String HOME = System.getProperty("user.home");

Класс Files является одной из основных точек входа пакета java.nio.file . Этот класс предлагает богатый набор API для чтения, записи и управления файлами и каталогами. Методы класса Files работают с экземплярами объектов Path .

3. Проверка файла или каталога

У нас может быть экземпляр Path , представляющий файл или каталог в файловой системе. Независимо от того, существует ли этот файл или каталог, на который он указывает, он доступен или нет, можно подтвердить файловой операцией.

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

Чтобы проверить, существует ли файл, мы используем API exists :

@Test
public void givenExistentPath__whenConfirmsFileExists__thenCorrect() {
    Path p = Paths.get(HOME);

    assertTrue(Files.exists(p));
}

Чтобы проверить, что файл не существует, мы используем API notExists :

@Test
public void givenNonexistentPath__whenConfirmsFileNotExists__thenCorrect() {
    Path p = Paths.get(HOME + "/inexistent__file.txt");

    assertTrue(Files.notExists(p));
}

Мы также можем проверить, является ли файл обычным файлом, таким как myfile.txt или просто каталогом, мы используем API isRegularFile :

@Test
public void givenDirPath__whenConfirmsNotRegularFile__thenCorrect() {
    Path p = Paths.get(HOME);

    assertFalse(Files.isRegularFile(p));
}

Существуют также статические методы для проверки прав доступа к файлам. Чтобы проверить, доступен ли файл для чтения, мы используем API isReadable :

@Test
public void givenExistentDirPath__whenConfirmsReadable__thenCorrect() {
    Path p = Paths.get(HOME);

    assertTrue(Files.isReadable(p));
}

Чтобы проверить, доступно ли это для записи, мы используем API isWritable :

@Test
public void givenExistentDirPath__whenConfirmsWritable__thenCorrect() {
    Path p = Paths.get(HOME);

    assertTrue(Files.isWritable(p));
}

Аналогично, чтобы проверить, является ли он исполняемым:

@Test
public void givenExistentDirPath__whenConfirmsExecutable__thenCorrect() {
    Path p = Paths.get(HOME);
    assertTrue(Files.isExecutable(p));
}

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

@Test
public void givenSameFilePaths__whenConfirmsIsSame__thenCorrect() {
    Path p1 = Paths.get(HOME);
    Path p2 = Paths.get(HOME);

    assertTrue(Files.isSameFile(p1, p2));
}

4. Создание файлов

API файловой системы обеспечивает однострочные операции для создания файлов.

Чтобы создать обычный файл, мы используем API createFile и передаем ему объект Path , представляющий файл, который мы хотим создать.

Все элементы имени в пути должны существовать, кроме имени файла, в противном случае мы получим IOException:

@Test
public void givenFilePath__whenCreatesNewFile__thenCorrect() {
    String fileName = "myfile__" + UUID.randomUUID().toString() + ".txt";
    Path p = Paths.get(HOME + "/" + fileName);
    assertFalse(Files.exists(p));

    Files.createFile(p);

    assertTrue(Files.exists(p));
}

В приведенном выше тесте, когда мы сначала проверяем путь, он не существует, затем после операции createFile он обнаруживается, что он существует.

Чтобы создать каталог, мы используем API createDirectory :

@Test
public void givenDirPath__whenCreatesNewDir__thenCorrect() {
    String dirName = "myDir__" + UUID.randomUUID().toString();
    Path p = Paths.get(HOME + "/" + dirName);
    assertFalse(Files.exists(p));

    Files.createDirectory(p);

    assertTrue(Files.exists(p));
    assertFalse(Files.isRegularFile(p));
    assertTrue(Files.isDirectory(p));
}

Эта операция требует, чтобы все элементы имени в пути существовали, если нет, мы также получаем IOException :

@Test(expected = NoSuchFileException.class)
public void givenDirPath__whenFailsToCreateRecursively__thenCorrect() {
    String dirName = "myDir__" + UUID.randomUUID().toString() + "/subdir";
    Path p = Paths.get(HOME + "/" + dirName);
    assertFalse(Files.exists(p));

    Files.createDirectory(p);
}

Однако, если мы хотим создать иерархию каталогов одним вызовом, мы используем метод createDirectories . В отличие от предыдущей операции, когда он обнаруживает пропущенные элементы имени в пути, он не генерирует IOException , он создает их рекурсивно, приводя к последнему элементу:

@Test
public void givenDirPath__whenCreatesRecursively__thenCorrect() {
    Path dir = Paths.get(
      HOME + "/myDir__" + UUID.randomUUID().toString());
    Path subdir = dir.resolve("subdir");
    assertFalse(Files.exists(dir));
    assertFalse(Files.exists(subdir));

    Files.createDirectories(subdir);

    assertTrue(Files.exists(dir));
    assertTrue(Files.exists(subdir));
}

5. Создание временных файлов

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

Новый API файловой системы предоставляет специальные операции для этой цели.

API createTempFile выполняет эту операцию. Он принимает объект пути, префикс файла и суффикс файла:

@Test
public void givenFilePath__whenCreatesTempFile__thenCorrect() {
    String prefix = "log__";
    String suffix = ".txt";
    Path p = Paths.get(HOME + "/");

    Files.createTempFile(p, prefix, suffix);

    assertTrue(Files.exists(p));
}

Эти параметры достаточны для требований, которые требуют этой операции. Однако, если вам нужно указать конкретные атрибуты файла, существует четвертый переменный параметр arguments.

Приведенный выше тест создает временный файл в каталоге HOME , предварительно ожидая и добавляя предоставленные строки префикса и суффикса соответственно. В итоге мы получим имя файла, например log 8821081429012075286.txt__. Длинная числовая строка генерируется системой.

Однако если мы не предоставим префикс и суффикс, тогда имя файла будет включать только длинную числовую строку и расширение .tmp по умолчанию:

@Test
public void givenPath__whenCreatesTempFileWithDefaults__thenCorrect() {
    Path p = Paths.get(HOME + "/");

    Files.createTempFile(p, null, null);

    assertTrue(Files.exists(p));
}

Приведенная выше операция создает файл с именем, похожим на 8600179353689423985.tmp .

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

@Test
public void givenNoFilePath__whenCreatesTempFileInTempDir__thenCorrect() {
    Path p = Files.createTempFile(null, null);

    assertTrue(Files.exists(p));
}

В Windows это будет по умолчанию что-то вроде C: \ Users \ user \ AppData \ Local \ Temp \ 6100927974988978748.tmp .

Все вышеперечисленные операции можно адаптировать для создания каталогов, а не обычных файлов, используя createTempDirectory вместо createTempFile .

6. Удаление файла

Чтобы удалить файл, мы используем API delete . Для ясности следующий тест сначала гарантирует, что файл еще не существует, а затем создает его и подтверждает, что он теперь существует, и, наконец, удаляет его и подтверждает, что он больше не существует:

@Test
public void givenPath__whenDeletes__thenCorrect() {
    Path p = Paths.get(HOME + "/fileToDelete.txt");
    assertFalse(Files.exists(p));
    Files.createFile(p);
    assertTrue(Files.exists(p));

    Files.delete(p);

    assertFalse(Files.exists(p));
}

Однако, если файл не существует в файловой системе, операция удаления завершится с ошибкой IOException :

@Test(expected = NoSuchFileException.class)
public void givenInexistentFile__whenDeleteFails__thenCorrect() {
    Path p = Paths.get(HOME + "/inexistentFile.txt");
    assertFalse(Files.exists(p));

    Files.delete(p);
}

Мы можем избежать этого сценария, используя deleteIfExists , который завершается с ошибкой в ​​случае, если файл не существует. Это важно, когда несколько потоков выполняют эту операцию, и нам не нужно сообщение об ошибке просто потому, что поток выполнил операцию раньше, чем текущий поток, который вышел из строя:

@Test
public void givenInexistentFile__whenDeleteIfExistsWorks__thenCorrect() {
    Path p = Paths.get(HOME + "/inexistentFile.txt");
    assertFalse(Files.exists(p));

    Files.deleteIfExists(p);
}

При работе с каталогами, а не с обычными файлами, мы должны помнить, что операция удаления по умолчанию не работает рекурсивно. Поэтому, если каталог не пустой, он потерпит неудачу с IOException :

@Test(expected = DirectoryNotEmptyException.class)
public void givenPath__whenFailsToDeleteNonEmptyDir__thenCorrect() {
    Path dir = Paths.get(
      HOME + "/emptyDir" + UUID.randomUUID().toString());
    Files.createDirectory(dir);
    assertTrue(Files.exists(dir));

    Path file = dir.resolve("file.txt");
    Files.createFile(file);

    Files.delete(dir);

    assertTrue(Files.exists(dir));
}

7. Копирование файлов

Вы можете скопировать файл или каталог, используя copy API:

@Test
public void givenFilePath__whenCopiesToNewLocation__thenCorrect() {
    Path dir1 = Paths.get(
      HOME + "/firstdir__" + UUID.randomUUID().toString());
    Path dir2 = Paths.get(
      HOME + "/otherdir__" + UUID.randomUUID().toString());

    Files.createDirectory(dir1);
    Files.createDirectory(dir2);

    Path file1 = dir1.resolve("filetocopy.txt");
    Path file2 = dir2.resolve("filetocopy.txt");

    Files.createFile(file1);

    assertTrue(Files.exists(file1));
    assertFalse(Files.exists(file2));

    Files.copy(file1, file2);

    assertTrue(Files.exists(file2));
}

Копирование завершится неудачно, если целевой файл существует, если не указана опция REPLACE EXISTING__:

@Test(expected = FileAlreadyExistsException.class)
public void givenPath__whenCopyFailsDueToExistingFile__thenCorrect() {
    Path dir1 = Paths.get(
      HOME + "/firstdir__" + UUID.randomUUID().toString());
    Path dir2 = Paths.get(
      HOME + "/otherdir__" + UUID.randomUUID().toString());

    Files.createDirectory(dir1);
    Files.createDirectory(dir2);

    Path file1 = dir1.resolve("filetocopy.txt");
    Path file2 = dir2.resolve("filetocopy.txt");

    Files.createFile(file1);
    Files.createFile(file2);

    assertTrue(Files.exists(file1));
    assertTrue(Files.exists(file2));

    Files.copy(file1, file2);

    Files.copy(file1, file2, StandardCopyOption.REPLACE__EXISTING);
}

Однако при копировании каталогов содержимое не копируется рекурсивно. Это означает, что если /baeldung содержит файлы /articles.db и /hors.db , то при копировании /baeldung в новое место будет создан пустой каталог.

8. Перемещение файлов

Вы можете переместить файл или каталог, используя move API. Это во многом похоже на операцию copy . Если операция копирования аналогична операции copy и paste в системах с графическим интерфейсом, то move аналогична операции cut and paste :

@Test
public void givenFilePath__whenMovesToNewLocation__thenCorrect() {
    Path dir1 = Paths.get(
      HOME + "/firstdir__" + UUID.randomUUID().toString());
    Path dir2 = Paths.get(
      HOME + "/otherdir__" + UUID.randomUUID().toString());

    Files.createDirectory(dir1);
    Files.createDirectory(dir2);

    Path file1 = dir1.resolve("filetocopy.txt");
    Path file2 = dir2.resolve("filetocopy.txt");
    Files.createFile(file1);

    assertTrue(Files.exists(file1));
    assertFalse(Files.exists(file2));

    Files.move(file1, file2);

    assertTrue(Files.exists(file2));
    assertFalse(Files.exists(file1));
}

Операция move завершается ошибкой, если целевой файл существует, если не указана опция REPLACE EXISTING , как мы делали с операцией copy__:

@Test(expected = FileAlreadyExistsException.class)
public void givenFilePath__whenMoveFailsDueToExistingFile__thenCorrect() {
    Path dir1 = Paths.get(
      HOME + "/firstdir__" + UUID.randomUUID().toString());
    Path dir2 = Paths.get(
      HOME + "/otherdir__" + UUID.randomUUID().toString());

    Files.createDirectory(dir1);
    Files.createDirectory(dir2);

    Path file1 = dir1.resolve("filetocopy.txt");
    Path file2 = dir2.resolve("filetocopy.txt");

    Files.createFile(file1);
    Files.createFile(file2);

    assertTrue(Files.exists(file1));
    assertTrue(Files.exists(file2));

    Files.move(file1, file2);

    Files.move(file1, file2, StandardCopyOption.REPLACE__EXISTING);

    assertTrue(Files.exists(file2));
    assertFalse(Files.exists(file1));
}

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

В этой статье мы узнали о файловых API в новом API файловой системы (NIO2), который был поставлен как часть Java 7, и увидели большинство важных файловых операций в действии.

Примеры кода, использованные в этой статье, можно найти в статье Github project .