Перейти к основному содержимому

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

· 8 мин. чтения

1. Обзор

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

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

2. Настройка

Настройка вашего проекта для использования файловых API — это просто вопрос импорта:

import java.nio.file.*;

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

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

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

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

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

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

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

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

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

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

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

Приведенный выше тест создает временный файл в каталоге 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 удаления . Для ясности следующий тест сначала проверяет, что файл еще не существует, затем создает его и подтверждает, что он теперь существует, и, наконец, удаляет его и подтверждает, что он больше не существует:

@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. Копирование файлов

Вы можете скопировать файл или каталог с помощью 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);
}

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

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

Вы можете переместить файл или каталог с помощью API перемещения . Во многом это похоже на операцию копирования . Если операция копирования аналогична операции копирования и вставки в системах с графическим интерфейсом, то перемещение аналогично операции вырезания и вставки :

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

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

@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 .