Saturday, May 17, 2014

Getting familiar with Java FileChannel

When I was studying into lucene 4.8.0 codebase, one particular code that stumble upon was the use of FileChannel. So today, I'm spending time to play around this class FileChannel.

So you would ask, why use FileChannel instead of BufferedWriter?

From the FileChannel documentation:

In addition to the familiar read, write, and close operations of byte channels, this class defines the following file-specific operations:

Bytes may be read or written at an absolute position in a file in a way that does not affect the channel's current position.

A region of a file may be mapped directly into memory; for large files this is often much more efficient than invoking the usual read or write methods.

Updates made to a file may be forced out to the underlying storage device, ensuring that data are not lost in the event of a system crash.

Bytes can be transferred from a file to some other channel, and vice versa, in a way that can be optimized by many operating systems into a very fast transfer directly to or from the filesystem cache.

A region of a file may be locked against access by other programs.

That sounds interesting, and to understand better, we will start to write code using class FileChannel. Below is one I wrote and explanation come after.
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;

public class FileChannelTest {

public static void main(String[] args) throws IOException {

try {
File aFile = new File("test.txt");
FileChannel fc = FileChannel.open(aFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ);
System.out.println("initialized is Open " + fc.isOpen()); // true

String data = "hello orld";
ByteBuffer buf = ByteBuffer.allocate(data.length());
buf.put(data.getBytes());
buf.flip();

System.out.println("initial size " + fc.size()); // 0
System.out.println("initial position " + fc.position()); // 0
fc.write(buf);
System.out.println("after write size " + fc.size()); // 10
System.out.println("after write position " + fc.position()); //10

ByteBuffer dst = ByteBuffer.allocate(200);
fc.read(dst, 0);
System.out.println("initial read " + new String(dst.array(), "UTF-8")); // hello orld

ByteBuffer newData = ByteBuffer.wrap("world\ndelete me".getBytes());
fc.write(newData, 6);
fc.position(21);

dst.clear();
fc.read(dst, 0);
System.out.println("read second write " + new String(dst.array(), "UTF-8")); //hello world
//delete me
System.out.println("after second write size " + fc.size()); // 21
System.out.println("after second write pos " + fc.position()); // 21

fc.truncate(12);
System.out.println("after truncate size " + fc.size()); // 12
System.out.println("after truncate pos " + fc.position()); // 12
dst = ByteBuffer.allocate(200);
fc.read(dst, 0);
System.out.println("after truncate " + new String(dst.array(), "UTF-8"));

newData.clear();
newData = ByteBuffer.wrap("a new line of text\n".getBytes());
fc.write(newData);
System.out.println("after second write size " + fc.size()); // 31
System.out.println("after second write pos " + fc.position()); // 31

fc.force(true);
fc.close();

System.out.println("after close " + fc.isOpen()); // false

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

}

It's a simple single threaded class. So we start with a File object with a test file. Then we create FileChannel object where we create test.txt if is not exists, then write and read it.

To dip our toe into the water, we start by checking if the file Channel is open. In order to write, we need ByteBuffer. We construct a new ByteBuffer object with the data length and write the data into the buffer array. In order for the channel to write this ByteBuffer, you must call the method flip().

We checked what is the current FileChannel size and position. Initially, they are zero. After data is written, note that size and position has been increased to 10. So in order to check the file channel written, we can invoke the method read(), hence the next few statements in the code.

To hold the data read from the file channel object, we create a new ByteBuffer object called dst, with a capacity of 200, so we can fit 200 bytes of data. We read the file channel object starting from file position 0 into the dst object. As expected in the print out, hello orld is written into file channel object. Interesting! It means we can write based on position we want to specify and that is exactly the next few lines of code did, we write "world\ndelete me" using method write but with position specify. If you notice carefully, unlike method write(), write() did not update file channel position, hence we are setting it explicitly to advance to position 21, that is, last position of character in the file. With this second write, we change the original data (hello orld) to (hello world\ndelete me), this overwrite the existing string than appending.

We check the file channel object after second write, we corrected the typo in the string and the position and size is as expected (21). Now we truncate the file channel up to 12bytes. That is, we start to truncate from position 0 until 12, which the byte array containing "hello world\n" survive and the remaining will be truncated. so the remaining size is 12 and position is set to 12 as well. As verified from the print out, delete me is no longer exist in the file channel.

As the file position is set end of file, we can simulate append effect by just writing and that is exactly what the next lines of code did. To ensure data and metadata is flush to the block device, we called force with parameter true. We end this example with invoking close method and check after that file channel is no longer opened.

When file channel object is created, we added open option, StandardOpenOption.READ is because when read into the file channel, this bit has to be set or else you will get exception. That's it about learning FileChannel.

No comments:

Post a Comment