«1. Обзор

В этой статье мы собираемся изучить один из ключевых дополнительных API нового ввода-вывода (NIO2) в Java 7 — API асинхронного файлового канала.

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

Вы также можете прочитать больше об операциях с файлами NIO.2 и операциях с путями — понимание этого облегчит понимание этой статьи.

Чтобы использовать асинхронные файловые каналы NIO2 в наших проектах, мы должны импортировать пакет java.nio.channels, так как он объединяет все необходимые классы:

import java.nio.channels.*;

2. AsynchronousFileChannel

В этом разделе мы мы рассмотрим, как использовать основной класс, который позволяет нам выполнять асинхронные операции с файлами, класс AsynchronousFileChannel. Чтобы создать его экземпляр, мы вызываем метод static open:

Path filePath = Paths.get("/path/to/file");

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
  filePath, READ, WRITE, CREATE, DELETE_ON_CLOSE);

Все значения перечисления берутся из StandardOpenOption.

Первым параметром открытого API является объект Path, представляющий расположение файла. Чтобы узнать больше об операциях пути в NIO2, перейдите по этой ссылке. Другие параметры составляют набор, определяющий параметры, которые должны быть доступны для возвращаемого файлового канала.

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

Path filePath = Paths.get("/path/to/file");

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
  filePath, StandardOpenOption.READ);

3. Чтение из файла

Как и все асинхронные операции в NIO2, чтение содержимого файла можно выполнить двумя способами. Использование Future и использование CompletionHandler. В каждом случае мы используем API чтения возвращаемого канала.

Внутри папки тестовых ресурсов maven или в исходном каталоге, если maven не используется, давайте создадим файл с именем file.txt, в начале которого будет только текст baeldung.com. Теперь мы покажем, как читать этот контент.

3.1. Подход к будущему

Во-первых, мы увидим, как асинхронно читать файл с помощью класса Future:

@Test
public void givenFilePath_whenReadsContentWithFuture_thenCorrect() {
    Path path = Paths.get(
      URI.create(
        this.getClass().getResource("/file.txt").toString()));
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      path, StandardOpenOption.READ);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    Future<Integer> operation = fileChannel.read(buffer, 0);

    // run other code as operation continues in background
    operation.get();
      
    String fileContent = new String(buffer.array()).trim();
    buffer.clear();

    assertEquals(fileContent, "baeldung.com");
}

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

Второй параметр — это тип long, указывающий позицию в файле, с которой следует начать чтение.

Метод возвращает сразу же, был ли файл прочитан или нет.

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

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

Если бы мы изменили параметр позиции в вызове API чтения с нуля на что-то другое, мы бы тоже увидели эффект. Например, седьмой символ в строке baeldung.com — g. Таким образом, изменение параметра position на 7 приведет к тому, что буфер будет содержать строку g.com.

3.2. Подход CompletionHandler

Далее мы увидим, как читать содержимое файла с помощью экземпляра CompletionHandler:

@Test
public void 
  givenPath_whenReadsContentWithCompletionHandler_thenCorrect() {
    
    Path path = Paths.get(
      URI.create( this.getClass().getResource("/file.txt").toString()));
    AsynchronousFileChannel fileChannel 
      = AsynchronousFileChannel.open(path, StandardOpenOption.READ);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    fileChannel.read(
      buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {

        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            // result is number of bytes read
            // attachment is the buffer containing content
        }
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {

        }
    });
}

В приведенном выше коде мы используем второй вариант API чтения. Он по-прежнему принимает ByteBuffer и начальную позицию операции чтения в качестве первого и второго параметров соответственно. Третий параметр — экземпляр CompletionHandler.

Первый общий тип обработчика завершения — это возвращаемый тип операции, в данном случае Integer, представляющий число прочитанных байтов.

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

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

«4. Запись в файл

Java NIO2 также позволяет выполнять операции записи в файл. Как и в случае с другими операциями, мы можем записывать в файл двумя способами. Использование Future и использование CompletionHandler. В каждом случае мы используем API записи возвращаемого канала.

Создать AsynchronousFileChannel для записи в файл можно следующим образом:

AsynchronousFileChannel fileChannel
  = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);

4.1. Особые замечания

Обратите внимание на опцию, переданную в открытый API. Мы также можем добавить еще одну опцию StandardOpenOption.CREATE, если мы хотим, чтобы файл, представленный путем, был создан, если он еще не существует. Другим распространенным параметром является StandardOpenOption.APPEND, который не перезаписывает существующее содержимое в файле.

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

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
  path, WRITE, CREATE, DELETE_ON_CLOSE);

Таким образом, мы укажем любой произвольный путь и будем уверены, что файл будет создан. После завершения теста созданный файл будет удален. Чтобы созданные файлы не удалялись после завершения теста, вы можете удалить последнюю опцию.

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

public static String readContent(Path file) {
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      file, StandardOpenOption.READ);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    Future<Integer> operation = fileChannel.read(buffer, 0);

    // run other code as operation continues in background
    operation.get();     

    String fileContent = new String(buffer.array()).trim();
    buffer.clear();
    return fileContent;
}

4.2. Подход к будущему

Для асинхронной записи в файл с использованием класса Future:

@Test
public void 
  givenPathAndContent_whenWritesToFileWithFuture_thenCorrect() {
    
    String fileName = UUID.randomUUID().toString();
    Path path = Paths.get(fileName);
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      path, WRITE, CREATE, DELETE_ON_CLOSE);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    buffer.put("hello world".getBytes());
    buffer.flip();

    Future<Integer> operation = fileChannel.write(buffer, 0);
    buffer.clear();

    //run other code as operation continues in background
    operation.get();

    String content = readContent(path);
    assertEquals("hello world", content);
}

Давайте посмотрим, что происходит в приведенном выше коде. Мы создаем случайное имя файла и используем его для получения объекта Path. Мы используем этот путь, чтобы открыть асинхронный файловый канал с ранее упомянутыми параметрами.

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

4.3. Подход CompletionHandler

Мы также можем использовать обработчик завершения, чтобы нам не приходилось ждать завершения операции в цикле while:

@Test
public void 
  givenPathAndContent_whenWritesToFileWithHandler_thenCorrect() {
    
    String fileName = UUID.randomUUID().toString();
    Path path = Paths.get(fileName);
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      path, WRITE, CREATE, DELETE_ON_CLOSE);

    ByteBuffer buffer = ByteBuffer.allocate(1024);
    buffer.put("hello world".getBytes());
    buffer.flip();

    fileChannel.write(
      buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {

        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            // result is number of bytes written
            // attachment is the buffer
        }
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {

        }
    });
}

Когда мы на этот раз вызываем API записи, единственный новый вещь — это третий параметр, в который мы передаем анонимный внутренний класс типа CompletionHandler.

Когда операция завершается, класс вызывает завершенный метод, в котором мы можем определить, что должно произойти.

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

В этой статье мы рассмотрели некоторые из наиболее важных функций API асинхронного файлового канала Java NIO2.

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