Tuesday, 12 January 2016

Reading/Writing to/from Files using FileChannel and ByteBuffer in Java

In the past, I have talked about RandomAccessFile and how it can be used for doing faster IO in Java, and in this Java NIO tutorial, we are going to see how to use read/write data from using FileChannel and ByteBuffer.

Channel provides an alternate way to read data from a file, it provides better performance than InputStream or OutputStream. It can also be opened in blocking and non-blocking mode. Though, FileChannles are read/write channels and they are always blocking, they cannot be put into non-blocking mode. The RandomAccessFile class treats a file as an array of Bytes.

You can write your data in any position of the Array and you can read from any position. To do that, it uses a pointer that holds the current position and provides several methods like seek() to move that pointer. Once you are at the right position, you can get the FileChannel from RandomAccessFile and starting reading data from a file. By the way, JDK 7 also introduced NIO 2, which makes dealing with files and directory even easier.

How to read/write files using FileChannel and ByteBuffer


Before start coding, let’s revise the basic concept of Channel and Buffer in Java NIO. In one word, buffers work with the channel. Channels are the tube through which data is transferred and buffers are the source and target of those data transfer. In the case of a write, data you want to write is placed in a buffer, which is passed to a channel than channel read that data from the buffer and write into the file.
Similarly in case of a read, a channel puts data in a buffer you provide from a file, network or any other source. Since the same buffer is used for reading and writing i.e. you write data into the buffer but channel reads it for writing into the file, you must call the flip() method once you are done writing into the buffer. The flip() method changes the pointers and allows you to read data from the buffer. There are three types of the buffer in Java, direct, non-direct and mapped buffer. We will use the direct byte buffer in this example.

Steps to read/write data using FileChannel and Buffer

Here is the step by step guide to starting reading data from a file using RandomAccessFile, FileChannel, and ByteBuffer:

  1. Open the file you want to read/write using RandomAccessFile in read/write mode.
  2. Call the getChannel() method of RandomAccessFile to get the FileChannel. The position of the returned channel will always be equal to this object’s file-pointer offset as returned by the getFilePointer() method.
  3. Create a ByteBuffer using ByteBuffer.allocate() method.
  4. Store the data into ByteBuffer using various put() method e.g. putInt(), putLong().
  5. Flip the Buffer so that Channel can read data from the buffer and write into a file. The flip() method changes the pointers and allows you to read data from the buffer.
  6. Call the write() method of FileChannel.
  7. Close the FileChannel
  8. Close the RandomAccessFile.

Another important point to note is that you can use the same buffer for reading and writing, but you need to flip it. Now, let’s see a sample Java program to read/write data from files using FileChannel and ByteBuffer in Java. After Memory Mapped File, this is the second fastest way to read and write from a file in Java.

Reading/Writing to/from Files using FileChannel and ByteBuffer in Java

Java Program to read/writes from file using FileChannel and ByteBuffer


Here is sample program to demonstrate how you can read and write data from a file (can be binary or text file) using FileChannel and ByteBuffer class. I have also used abstraction to create an interface called Persistable, which provides two methods persist() and recover(). Any object which implement this interface can be saved and loaded, but how do you save and load them is left to the implementor i.e. you can use Chanel and Buffer like we have done or you can use the old approach to read/write file in Java.

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * Java Program to read and write on RandomAccessFile in Java
 * using FileChannle and ByteBuffer.
 *
 * @author Javin
 */
public class FileChannelDemo {

    public static void main(String args[]) {

        Tablet ipad = new Tablet("Apple", true, 1000);
        System.out.println("Writing into RandomAcessFile : " + ipad);
        write("tablet.store", ipad);

        Tablet fromStore = new Tablet();
        read("tablet.store", fromStore);
        System.out.println("Object read from RandomAcessFile : " + fromStore);

    }

    /*
     * Method to write data into File using FileChannel and ByteBuffeer
     */
    public static void write(String filename, Persistable object) {
        try {
            // Creating RandomAccessFile for writing
            RandomAccessFile store = new RandomAccessFile("tablet", "rw");

            // getting FileChannel from file
            FileChannel channel = store.getChannel();

            // creating and initializing ByteBuffer for reading/writing data
            ByteBuffer buffer = ByteBuffer.allocate(2048);

            // an instance of Persistable writing into ByteBuffer
            object.persist(buffer);

            // flip the buffer for writing into file
            buffer.flip();
            int numOfBytesWritten = channel.write(buffer); // writing into File
            System.out.println("number of bytes written : " + numOfBytesWritten);
            channel.close(); // closing file channel
            store.close(); // closing RandomAccess file

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /*
     * Method to read data from File using FileChannel and ByteBuffeer
     */
    public static void read(String filename, Persistable object) {
        try {
            // Opening RandomAccessFile for reading data
            RandomAccessFile store = new RandomAccessFile("tablet", "rw");

            // getting file channel
            FileChannel channel = store.getChannel();

            // preparing buffer to read data from file
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            // reading data from file channel into buffer
            int numOfBytesRead = channel.read(buffer);
            System.out.println("number of bytes read : " + numOfBytesRead);

            // You need to filp the byte buffer before reading
            buffer.flip();

            // Recovering object
            object.recover(buffer);

            channel.close();
            store.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Our interface to abstract reading and writing mechanism. This is also the actual use of the interface, to provide abstraction, separating what to do from how to do. Like this interface just say persist and recover, doesn’t say how you do that.

interface Persistable {

    public void persist(ByteBuffer buffer);

    public void recover(ByteBuffer buffer);
}

Concrete class to implement Persistable to make them readable and writable:

class Tablet implements Persistable {

    private String brand;
    private boolean isCellular;
    private long cost; // in US Dollars

    public Tablet() {
        brand = "";
    }

    public Tablet(String brand, boolean isCellular, long cost) {
        this.brand = brand;
        this.isCellular = isCellular;
        this.cost = cost;
    }

    public final String getBrand() {
        return brand;
    }

    public final boolean isCellular() {
        return isCellular;
    }

    public final long getCost() {
        return cost;
    }

    public final void setBrand(String brand) {
        this.brand = brand;
    }

    public final void setCellular(boolean isCellular) {
        this.isCellular = isCellular;
    }

    public final void setCost(long cost) {
        this.cost = cost;
    }

    @Override
    public void persist(ByteBuffer buffer) {
        byte[] strBytes = brand.getBytes();
        buffer.putInt(strBytes.length);
        buffer.put(strBytes, 0, strBytes.length);
        buffer.put(isCellular == true ? (byte) 1 : (byte) 0);
        buffer.putLong(cost);
    }

    @Override
    public void recover(ByteBuffer buffer) {
        int size = buffer.getInt();
        byte[] rawBytes = new byte[size];
        buffer.get(rawBytes, 0, size);
        this.brand = new String(rawBytes);
        this.isCellular = buffer.get() == 1 ? true : false;
        this.cost = buffer.getLong();
    }

    @Override
    public String toString() {
        return "Tablet [brand=" + brand + ", isCellular=" + isCellular + ", cost=" + cost + "]";
    }

}

Output:
Writing into RandomAcessFile : Tablet [brand=Apple, isCellular=true, cost=1000]
number of bytes written : 18
number of bytes read : 1024
Object read from RandomAcessFile : Tablet [brand=Apple, isCellular=true, cost=1000]