1. Overview
In this quick tutorial, we’ll look at the FileChannel class provided in the Java NIO library. We’ll discuss how to read and write data using FileChannel and ByteBuffer.
We’ll also explore the advantages of using FileChannel and some of its other file manipulation features.
2. Advantages of FileChannel
The advantages of FileChannel include:
- Reading and writing at a specific position in a file
- Loading a section of a file directly into memory, which can be more efficient
- We can transfer file data from one channel to another at a faster rate
- We can lock a section of a file to restrict access by other threads
- To avoid data loss, we can force writing updates to a file immediately to storage
3. Reading with FileChannel
FileChannel performs faster than standard I/O when we read a large file.
We should note that although part of Java NIO, FileChannel operations are blocking and do not have a non-blocking mode.
3.1. Reading a File Using FileChannel
Let’s understand how to read a file using FileChannel on a file that contains:
Hello world
This test reads the file and checks it was read ok:
@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);
}
}
Here we read bytes from the file using FileChannel, RandomAccessFile, and ByteBuffer.
We should also note that multiple concurrent threads can use FileChannels safely. However, only one thread at a time is allowed an operation that involves updating a channel’s position or changing its file size. This blocks other threads attempting a similar operation until the previous operation completes.
However, operations that provide explicit channel positions can run concurrently without being blocked.
3.2. Opening a FileChannel
In order to read a file using FileChannel, we must open it.
Let’s see how to open a FileChannel using RandomAccessFile:
RandomAccessFile reader = new RandomAccessFile(file, "r");
FileChannel channel = reader.getChannel();
Mode ‘r’ indicates that the channel is ‘open for reading’ only. We should note that closing a RandomAccessFile will also close the associated channel.
Next, we’ll see opening a FileChannel to read a file using FileInputStream:
FileInputStream fin= new FileInputStream(file);
FileChannel channel = fin.getChannel();
Similarly, closing a FileInputStream also closes the channel associated with it.
3.3. Reading Data from a FileChannel
To read the data, we can use one of the read methods.
Let’s see how to read a sequence of bytes. We’ll use a ByteBuffer to hold the data:
ByteBuffer buff = ByteBuffer.allocate(1024);
int noOfBytesRead = channel.read(buff);
String fileContent = new String(buff.array(), StandardCharsets.UTF_8);
assertEquals("Hello world", fileContent);
Next, we’ll see how to read a sequence of bytes, starting at a file position:
ByteBuffer buff = ByteBuffer.allocate(1024);
int noOfBytesRead = channel.read(buff, 5);
String fileContent = new String(buff.array(), StandardCharsets.UTF_8);
assertEquals("world", fileContent);
We should note the need for a Charset to decode a byte array into String.
We specify the Charset with which the bytes were originally encoded. Without it*,* we may end up with garbled text. In particular, multi-byte encodings like UTF-8 and UTF-16 may not be able to decode an arbitrary section of the file, as some of the multi-byte characters may be incomplete.
4. Writing with FileChannel
4.1. Writing into a File Using FileChannel
Let’s explore how to write using 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. Opening a FileChannel
In order to write into a file using FileChannel, we must open it.
Let’s see how to open a FileChannel using RandomAccessFile:
RandomAccessFile writer = new RandomAccessFile(file, "rw");
FileChannel channel = writer.getChannel();
Mode ‘rw’ indicates that the channel is ‘open for reading and writing’.
Let’s also see how to open a FileChannel using FileOutputStream:
FileOutputStream fout = new FileOutputStream(file);
FileChannel channel = fout.getChannel();
4.3. Writing Data with FileChannel
To write data with a FileChannel, we can use one of the write methods.
Let’s see how to write a sequence of bytes, using a ByteBuffer to store the data:
ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
channel.write(buff);
Next, we’ll see how to write a sequence of bytes, starting at a file position:
ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
channel.write(buff, 5);
5. Current Position
FileChannel allows us to get and change the position at which we are reading or writing.
Let’s see how to get the current position:
long originalPosition = channel.position();
Next, let’s see how to set the position:
channel.position(5);
assertEquals(originalPosition + 5, channel.position());
6. Get the Size of a File
Let’s see how to use the FileChannel.size method to get the size of a file in bytes:
@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. Truncate a File
Let’s understand how to use the FileChannel.truncate method to truncate a file to given size in bytes:
@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 File Update into Storage
An operating system may cache file changes for performance reasons, and data may be lost if the system crashes. To force file content and metadata to write to disk continuously we can use the force method:
channel.force(true);
This method is guaranteed only when the file resides on a local device.
9. Load a Section of a File into Memory
Let’s see how to load a section of a file in memory using FileChannel.map. We use FileChannel.MapMode.READ_ONLY to open the file in read-only mode:
@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));
}
}
}
Similarly, we can use FileChannel.MapMode.READ_WRITE to open the file into both read and write mode.
We can also use FileChannel.MapMode.PRIVATE mode, where changes do not apply to the original file.**
10. Lock a Section of a File
Let’s understand how to lock a section of a file to prevent concurrent access of a section using the FileChannel.tryLock method:
@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);
}
}
The tryLock method attempts to acquire a lock on the file section. If the requested file section is already blocked by another thread, it throws an OverlappingFileLockException exception. This method also takes a boolean parameter to request either a shared lock or an exclusive lock.
We should note that some operating systems may not allow a shared lock, defaulting instead to an exclusive lock.
11. Closing a FileChannel
Finally, when we are done using a FileChannel, we must close it. In our examples we have used try-with-resources.
If necessary, we can close the FileChannel directly with the close method:
channel.close();
12. Conclusion
In this tutorial, we’ve seen how to use FileChannel to read and write files. In addition, we’ve explored how to read and change the file size and its current read/write location and looked at how to use FileChannels in concurrent or data critical applications.
As always, the source code for the examples is available over on GitHub.