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

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

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

Задача: Наибольшая подстрока палиндром

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

ANDROMEDA 42

1. Обзор

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

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

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

К преимуществам FileChannel относятся:

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

3. Чтение с FileChannel

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

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

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.

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

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

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

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

Давайте посмотрим, как открыть 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. Чтение данных из FileChannel

Чтобы прочитать данные, мы можем использовать один из методов чтения .

Давайте посмотрим, как читать последовательность байтов. Мы будем использовать 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);

Следует отметить необходимость использования Charset для декодирования массива байтов в String .

Мы указываем Charset , с помощью которого байты были изначально закодированы. Без этого мы можем получить искаженный текст. В частности, многобайтовые кодировки, такие как 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 , мы должны открыть его.

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

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

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

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

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

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

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

Давайте посмотрим, как записать последовательность байтов, используя 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 . Мы используем FileChannel.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 , чтобы открыть файл как для чтения, так и для записи.

Мы также можем использовать режим FileChannel.MapMode.PRIVATE , в котором изменения не применяются к исходному файлу . ****

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 . Этот метод также принимает логический параметр для запроса разделяемой или монопольной блокировки.

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

11. Закрытие FileChannel

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

При необходимости мы можем закрыть FileChannel напрямую с помощью метода close :

channel.close();

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

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

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