Рубрики
Без рубрики

Руководство по файловому каналу Java

Узнайте, как использовать API-интерфейсы Java NIO FileChannel для более эффективного чтения и записи файлов.

Автор оригинала: baeldung.

1. Обзор

В этом кратком руководстве мы рассмотрим класс FileChannel , предоставленный в библиотеке Java NIO . Мы обсудим как читать и записывать данные с помощью FileChannel и ByteBuffer .

Мы также рассмотрим преимущества использования FileChannel и некоторые другие функции обработки файлов.

2. Преимущества FileChannel

Преимущества FileChannel включают в себя:

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

3. Чтение с помощью файлового канала

FileChannel работает быстрее, чем стандартный ввод-вывод, когда мы читаем большой файл.

Следует отметить, что, хотя часть Java NIO , FileChannel операции блокируются и не имеют неблокирующего режима.

3.1. Чтение файла с помощью FileChannel

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

Hello world

Этот тест считывает файл и проверяет, что он был прочитан нормально:

@Test
public void givenFile_whenReadWithFileChannelUsingRandomAccessFile_thenCorrect() 
  throws IOException {
    try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r");
        FileChannel channel = reader.getChannel();
        ByteArrayOutputStream out = new ByteArrayOutputStream()) {

        int bufferSize = 1024;
        if (bufferSize > channel.size()) {
           bufferSize = (int) channel.size();
        }
        ByteBuffer buff = ByteBuffer.allocate(bufferSize);

        while (channel.read(buff) > 0) {
            out.write(buff.array(), 0, buff.position());
            buff.clear();
        }
        
     String fileContent = new String(out.toByteArray(), StandardCharsets.UTF_8);
 
     assertEquals("Hello world", fileContent);
    }
}

Здесь мы считываем байты из файла с помощью FileChannel , RandomAccessFile и ByteBuffer.

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

Однако операции, которые предоставляют явные позиции канала, могут выполняться одновременно без блокировки.

3.2. Открытие файлового канала

Чтобы прочитать файл с помощью FileChannel , мы должны открыть его.

Давайте посмотрим, как открыть файловый канал с помощью RandomAccessFile :

RandomAccessFile reader = new RandomAccessFile(file, "r");
FileChannel channel = reader.getChannel();

Режим “r” указывает, что канал “открыт только для чтения”. Следует отметить, что закрытие RandomAccessFile также закроет связанный канал.

Далее мы увидим открытие FileChannel для чтения файла с помощью FileInputStream :

FileInputStream fin= new FileInputStream(file);
FileChannel channel = fin.getChannel();

Аналогично, закрытие FileInputStream также закрывает связанный с ним канал.

3.3. Чтение данных из файлового канала

Для чтения данных мы можем использовать один из методов read .

Давайте посмотрим, как читать последовательность байтов. Мы будем использовать ByteBuffer для хранения данных:

ByteBuffer buff = ByteBuffer.allocate(1024);
int noOfBytesRead = channel.read(buff);
String fileContent = new String(buff.array(), StandardCharsets.UTF_8);

assertEquals("Hello world", fileContent);

Далее мы рассмотрим, как читать последовательность байтов, начиная с позиции файла:

ByteBuffer buff = ByteBuffer.allocate(1024);
int noOfBytesRead = channel.read(buff, 5);
String fileContent = new String(buff.array(), StandardCharsets.UTF_8);
assertEquals("world", fileContent);

Мы должны отметить необходимость кодировки для декодирования массива байтов в Строку .

Мы указываем Кодировку , с помощью которой байты были первоначально закодированы. Без него , мы можем получить искаженный текст. В частности, многобайтовые кодировки, такие как UTF-8 и UTF-16 , могут не декодировать произвольный раздел файла, так как некоторые многобайтовые символы могут быть неполными.

4. Запись с помощью FileChannel

4.1. Запись в файл с помощью FileChannel

Давайте рассмотрим, как писать с помощью FileChannel :

@Test
public void whenWriteWithFileChannelUsingRandomAccessFile_thenCorrect()   
  throws IOException {
    String file = "src/test/resources/test_write_using_filechannel.txt";
    try (RandomAccessFile writer = new RandomAccessFile(file, "rw");
        FileChannel channel = writer.getChannel()){
        ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
 
        channel.write(buff);
 
     // verify
     RandomAccessFile reader = new RandomAccessFile(file, "r");
     assertEquals("Hello world", reader.readLine());
     reader.close();
    }
}

4.2. Открытие файлового канала

Чтобы записать в файл с помощью FileChannel , мы должны открыть его.

Давайте посмотрим, как открыть файловый канал с помощью RandomAccessFile :

RandomAccessFile writer = new RandomAccessFile(file, "rw");
FileChannel channel = writer.getChannel();

Режим “rw” указывает, что канал “открыт для чтения и записи”.

Давайте также посмотрим, как открыть файловый канал с помощью FileOutputStream :

FileOutputStream fout = new FileOutputStream(file);
FileChannel channel = fout.getChannel();

4.3. Запись данных с помощью FileChannel

Для записи данных с помощью FileChannel мы можем использовать один из методов write .

Давайте посмотрим, как записать последовательность байтов, используя ByteBuffer для хранения данных:

ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
channel.write(buff);

Далее мы рассмотрим, как записать последовательность байтов, начиная с позиции файла:

ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
channel.write(buff, 5);

5. Текущее Положение

FileChannel позволяет нам получить и изменить положение, в котором мы читаем или пишем.

Давайте посмотрим, как получить текущую позицию:

long originalPosition = channel.position();

Далее давайте посмотрим, как установить позицию:

channel.position(5);
assertEquals(originalPosition + 5, channel.position());

6. Получите размер файла

Давайте посмотрим, как использовать метод FileChannel.size , чтобы получить размер файла в байтах:

@Test
public void whenGetFileSize_thenCorrect() 
  throws IOException {
    RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r");
    FileChannel channel = reader.getChannel();

    // the original file size is 11 bytes.
    assertEquals(11, channel.size());

    channel.close();
    reader.close();
}

7. Усечение файла

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

@Test
public void whenTruncateFile_thenCorrect() 
  throws IOException {
    String input = "this is a test input";

    FileOutputStream fout = new FileOutputStream("src/test/resources/test_truncate.txt");
    FileChannel channel = fout.getChannel();

    ByteBuffer buff = ByteBuffer.wrap(input.getBytes());
    channel.write(buff);
    buff.flip();

    channel = channel.truncate(5);
    assertEquals(5, channel.size());

    fout.close();
    channel.close();
}

8. Принудительное обновление файла в хранилище

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

channel.force(true);

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

9. Загрузите раздел файла в память

Давайте посмотрим, как загрузить раздел файла в память с помощью FileChannel.map . Мы используем Файловый канал.MapMode.READ_ONLY для открытия файла в режиме только для чтения:

@Test
public void givenFile_whenReadAFileSectionIntoMemoryWithFileChannel_thenCorrect() 
  throws IOException { 
    try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r");
        FileChannel channel = reader.getChannel();
        ByteArrayOutputStream out = new ByteArrayOutputStream()) {

        MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY, 6, 5);

        if(buff.hasRemaining()) {
          byte[] data = new byte[buff.remaining()];
          buff.get(data);
          assertEquals("world", new String(data, StandardCharsets.UTF_8));	
        }
    }
}

Аналогично, мы можем использовать FileChannel.MapMode.READ_WRITE , чтобы открыть файл как в режиме чтения, так и в режиме записи.

Мы также можем использовать Файловый канал.MapMode.ЗАКРЫТЫЙ режим, в котором изменения не применяются к исходному файлу .

10. Блокировка раздела файла

Давайте разберемся, как заблокировать раздел файла, чтобы предотвратить одновременный доступ к разделу с помощью метода FileChannel.tryLock :

@Test
public void givenFile_whenWriteAFileUsingLockAFileSectionWithFileChannel_thenCorrect() 
  throws IOException { 
    try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "rw");
        FileChannel channel = reader.getChannel();
        FileLock fileLock = channel.tryLock(6, 5, Boolean.FALSE )){
 
        //do other operations...
 
        assertNotNull(fileLock);
    }
}

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

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

11. Закрытие файлового канала

Наконец, когда мы закончим использовать файловый канал , мы должны закрыть его. В наших примерах мы использовали try-with-resources .

При необходимости мы можем закрыть файловый канал непосредственно с помощью метода close :

channel.close();

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

В этом уроке мы видели, как использовать FileChannel для чтения и записи файлов . Кроме того, мы изучили, как читать и изменять размер файла и его текущее местоположение для чтения/записи, а также рассмотрели, как использовать Файловые каналы в параллельных или критически важных приложениях.

Как всегда, исходный код примеров доступен на GitHub .