Java Nio FileChannel Example
The FileChannel is a Java Nio Channel implementation for working with a file. It facilitates reading, writing, mapping and manipulating a file.
The examples in this tutorial will be demonstrated via test cases with no explicit Charset specified when encoding and decoding text from ByteBuffers.
1. Introduction
The FileChannel has a rich hierarchy of interfaces extending to it a diverse set of behaviors and provides a handle to access a rich set of meta data properties of the file including time stamps for creation, modification, size etc.
FileChannels are safe for multi-threaded use and will allow certain operations (those that use explicit positions) to execute concurrently while those that involve the intrinsic position
marker will execute serially.
The view of the underlying file is guaranteed to be consistent with other views of the same file within the same Java process (Threads) but the same guarantee is not extended to other concurrently executing programs.
A FileChannel is created by executing one of the open(..)
methods of the FileChannel class or by calling getChannel(...)
on any either FileInputStream, FileOutputStream or RandomAccessFile. The position
marker of the FileChannel and ultimately the state of the FileChannel is coupled to the state of the class on which getChannel(...)
was called.
Opening a FileChannel via one of the open(...)
methods allows for the provision of a mode in which the Channel is opened. This is encapsulated very nicely for us via the StandardOpenOption enumeration.
If we obtained the FileChannel via the getChannel(...)
method of FileInputStream then the FileChannel is opened for reading and will throw a NonWritableChannelException if you attempt to write to it, the same bodes for opening a FileChannel with a StandardOpenOption#READ and attempting to write to it. If we obtained a FileChannel via the getChannel(...)
method of FileOutputStream then the FileChannel is opened for writing and reading. We need to be explicit about our StandardOpenOption’s when getting a FileChannel via one of the open(...)
methods.
2. Technologies used
The example code in this article was built and run using:
- Java 1.8.101 (1.8.x will do fine)
- Maven 3.3.9 (3.3.x will do fine)
- Spring source tool suite 4.6.3 (Any Java IDE would work)
- Ubuntu 16.04 (Windows, Mac or Linux will do fine)
3. Reading
Reading from a FileChannel
@Test public void viaFileInputStream() throws IOException { final StringBuilder data = new StringBuilder(); final FileInputStream fis = new FileInputStream(new File(SRC)); try (FileChannel channel = fis.getChannel()) { readFromChannel(channel, data); } finally { if (!Objects.isNull(fis)) { fis.close(); } } assertEquals("Invalid content", CONTENTS.toString(), data.toString().trim()); } @Test public void readOk() throws IOException { final StringBuilder data = new StringBuilder(); try (FileChannel channel = FileChannel.open(Paths.get(SRC), StandardOpenOption.READ)) { readFromChannel(channel, data); } assertEquals("Invalid content", CONTENTS.toString(), data.toString().trim()); } @Test(expected = NoSuchFileException.class) public void readNoSuchFile() throws IOException { final ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); try (FileChannel channel = FileChannel.open(Paths.get("/tmp/nosuchfile.txt"), StandardOpenOption.READ)) { channel.read(buffer); } } @Test(expected = NonWritableChannelException.class) public void tryWrite() throws IOException { try (FileChannel channel = FileChannel.open(Paths.get(SRC), StandardOpenOption.READ)) { channel.write(ByteBuffer.wrap("Trying to write this text".getBytes())); } } @Test(expected = NonWritableChannelException.class) public void tryWriteViaFileInputStream() throws IOException { final FileInputStream fis = new FileInputStream(new File(SRC)); try (FileChannel channel = fis.getChannel()) { channel.write(ByteBuffer.wrap("Trying to write this text".getBytes())); } finally { if (!Objects.isNull(fis)) { fis.close(); } } } private void readFromChannel(final FileChannel channel, final StringBuilder data) throws IOException { final ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); int bytes = channel.read(buffer); while (bytes >= 0) { if (bytes > 0) { transferTo(buffer, data); } buffer.clear(); bytes = channel.read(buffer); } }
- line 2: defines a method for obtaining a FileChannel from a FileInputStream and reading all the bytes from the FileChannel
- line 18: defines a method for obtaining a FileChannel via one of the static factory methods
open(...)
on the FileChannel class and reading all the bytes from the FileChannel - line 28: defines a method for attempting to open and read a file that does not exist
- lines 37 & 44: define methods for attempting to write to a FileChannel that was opened exclusively for reading via the StandardOpenOption#READ or form calling
getChannel(...)
on the FileInputStream - lines 56-67: defines a method for reading the bytes from a FileChannel into a finite sized ByteBuffer
4. Writing
Writing to a FileChannel
@Test public void viaFileOutuputStream() throws IOException { final ByteBuffer buffer = ByteBuffer.wrap(CONTENTS.getBytes()); final FileOutputStream fos = new FileOutputStream(new File(TARGET)); int bytes = 0; try (FileChannel channel = fos.getChannel()) { bytes = writeToChannel(channel, buffer); } finally { if (!Objects.isNull(fos)) { fos.close(); } } assertTrue("Invalid amount of bytes written", CONTENTS.getBytes().length == bytes); } @Test public void writeOk() throws IOException { final ByteBuffer buffer = ByteBuffer.wrap(CONTENTS.getBytes()); int bytes = 0; try (FileChannel channel = FileChannel.open(Paths.get(TARGET), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) { bytes = writeToChannel(channel, buffer); } assertTrue("Invalid amount of bytes written", CONTENTS.getBytes().length == bytes); } @Test(expected = NoSuchFileException.class) public void writeNoSuchFile() throws IOException { try (FileChannel channel = FileChannel.open(Paths.get("/tmp/nosuchfile.txt"), StandardOpenOption.WRITE)) { } } private int writeToChannel(final FileChannel channel, final ByteBuffer buffer) throws IOException { int bytes = 0; while (buffer.hasRemaining()) { bytes += channel.write(buffer); } return bytes; }
- line 2: defines a method that gets a FileChannel from a FileOutputStream and writes some content to the FileChannel
- line 19: defines a method that gets a FileChannel via the
open(...)
method and writes some content to the FileChannel. Two StandardOpenOption settings are specified indicating that the file should be created and that the FileChannel should be opened inwrite
mode - line 31: defines a method that attempts to write to a file that does not exist
- lines 36-43: defines a method that writes the contents of the given ByteBuffer to the given FileChannel and returns the number of bytes that was written
5. Transferring
Transferring data between two FileChannels
@Test public void transfer() throws IOException { try (FileChannel in = FileChannel.open(Paths.get(SRC), StandardOpenOption.READ, StandardOpenOption.CREATE); FileChannel out = FileChannel.open(Paths.get(TARGET), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) { long bytes = 0; while(bytes < in.size()) { bytes += out.transferFrom(in, bytes, in.size()); } out.force(true); assertTrue("All bytes not transfered", in.size() == bytes); } }
line 2: defines a method that opens two FileChannels, one for reading from and one for writing to. The transferFrom(...)
method is called on the target FileChannel which allows us to transfer all the bytes from the source FileChannel to the target FileChannel. It’s important to do this in a loop as not all the bytes are guaranteed to be transferred in a single call.
The transferFrom(...)
and transferTo(...)
methods exploit various OS optimizations (if available) and should probably be preferred when working with ReadableByteChannel and WritableByteChannel instances.
6. Mapping
Mapping the contents of a File into memory
@Test public void mapReadonly() throws IOException { try (FileChannel channel = FileChannel.open(Paths.get(SRC), StandardOpenOption.READ)) { final MappedByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, channel.size()); byte[] tmp = new byte[buffer.remaining()]; buffer.get(tmp); assertTrue("Buffer not direct", buffer.isDirect()); assertEquals("Content does not match", CONTENTS.toString(), new String(tmp).trim()); } } @Test(expected = ReadOnlyBufferException.class) public void mapReadonlyBufferException() throws IOException { try (FileChannel channel = FileChannel.open(Paths.get(SRC), StandardOpenOption.READ)) { final MappedByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, channel.size()); buffer.putChar('x'); } } @Test public void mapWrite() throws IOException { try (FileChannel channel = FileChannel.open(Paths.get(SRC), StandardOpenOption.READ, StandardOpenOption.WRITE)) { final MappedByteBuffer buffer = channel.map(MapMode.READ_WRITE, 0, channel.size()); buffer.clear(); buffer.put(new StringBuilder(CONTENTS).reverse().toString().getBytes()); channel.force(true); } try (FileChannel channel = FileChannel.open(Paths.get(SRC), StandardOpenOption.READ)) { final MappedByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, channel.size()); byte[] tmp = new byte[buffer.remaining()]; buffer.get(tmp); assertEquals("Contents does not match reverse", new StringBuilder(CONTENTS).reverse().toString(), new String(tmp)); } }
- line 2: defines a method that maps the contents (entire) of a file into memory in
read only mode
. - line 15: defines a method that attempts to mutate a
MappedByteBuffer
that was opened inREAD_ONLY
mode - line 24: defines a method that hat maps the entire contents of a file into memory and mutates the contents of it in memory
This type of behavior is especially useful with large files where a portion of a file (specified by byte ranges) can be mapped into memory and utilized at any given point in time. The FileChannel.MapMode specifies the following modes PRIVATE
READ_ONLY
READ_WRITE
which need to correlate with the FileChannels StandardOpenOption mode otherwise a NonWritableChannelException or a NonReadableChannelException will be raised.
The MapMode in concert with the StandardOpenOptions allow finer granularity with permissible operations when mapping a files contents into memory.
7. Locking
Taking out a lock on a file region
@Test public void exclusiveLock() throws IOException { try (FileChannel channel = FileChannel.open(Paths.get(SRC), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); FileLock lock = channel.lock();) { assertTrue("Lock is not exclusive", !lock.isShared()); } } @Test public void sharedLock() throws IOException { try (FileChannel channel = FileChannel.open(Paths.get(SRC), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); FileLock lock = channel.lock(0, channel.size(), true)) { assertTrue("Lock is not shared", lock.isShared()); } } @Test(expected = OverlappingFileLockException.class) public void testOverlappingLock() { final CountDownLatch innerThreadLatch = new CountDownLatch(1); final CountDownLatch testThreadLatch = new CountDownLatch(1); try (FileChannel channel = FileChannel.open(Paths.get(SRC), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) { new Thread() { public void run() { try { channel.lock(); innerThreadLatch.countDown(); testThreadLatch.await(); } catch (OverlappingFileLockException | IOException | InterruptedException e) { throw new RuntimeException("Unable to get lock on file for overlapping lock test", e); } } }.start(); innerThreadLatch.await(); channel.lock(); } catch (InterruptedException | IOException e) { throw new RuntimeException(e); } finally { testThreadLatch.countDown(); } } @Test public void lockDifferentRegions() throws IOException { try (FileChannel channel = FileChannel.open(Paths.get(SRC), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); FileLock lock1 = channel.lock(0, 5, true); FileLock lock2 = channel.lock(5, channel.size(), true)) { assertFalse("Locks same", lock1 == lock2); } } @Test(expected = OverlappingFileLockException.class) public void overlappingLockSameThread() throws IOException { try (FileChannel channel = FileChannel.open(Paths.get(SRC), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); FileLock lock1 = channel.lock(0, channel.size(), true); FileLock lock2 = channel.lock(0, channel.size(), true)) { } }
- line 2 & 10: defines a method for testing the
shared
property of a FileLock - line 18 & 55: defines a method for testing for an OverlappingFileLockException from different and the same Threads
- line 46: defines a method for showing that two different FileLocks can be taken out on two different regions (byte range) of the same file
An aside about FileLocks. Strictly not to be used as a sole means of concurrency in the JVM, however in concert with normal synchronization measures can prove effective when trying to control file region access. Inside a JVM process no two FileLocks can lock the same region.
FileLocks can be exclusive or shared:
- Exclusive: prevents other processes / threads from acquiring any FileLock (shared or exclusive) on the same region of a file
- Shared: prevents other processes / threads from acquiring an exclusive FileLock on the same region of a file but allows other processes / threads to acquire a shared FileLock on the same region of a file
8. Test cases
The project is a maven project and can be run from the command line by executing the following: mvn clean install
. Alternatively one can run the code from within Eclipse.
9. Summary
In this example we learnt how to read from a file via a FileChannel, how to write to a file via a FileChannel, how to map part of a file into memory from a FileChannel, how to transfer data from one file to another via a FileChannel and how to lock regions of a file from a FileChannel.
10. Download the Source Code
This was a Java Nio FileChannel Example
You can download the full source code of this example here: Java Nio FileChannel Example