Friday, September 10, 2021

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

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 in 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 directories even easier.

And, If you are new to the Java world then I also recommend you go through The Complete Java MasterClass on Udemy to learn Java in a better and more structured way. This is one of the best and up-to-date courses to learn Java online.




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 transfers. In the case of a write, data you want to write is placed in a buffer, which is passed to a channel then the channel reads that data from the buffer and writes it into the file.

Similarly, in the case of a read, a channel puts data in a buffer you provide 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 the 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 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 it 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.




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

Here is a 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 implements 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 a 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, provide abstraction, separating what to do from how to do it. 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]


Caution

Don't forget to flip the byte buffer after writing the contents of the object into it, because the file channel needs to read it in order to write data into RandomAccessFile. If you forget to call the flip() method before calling the FileChannel.write() then you end up writing nothing into the file.

Similarly, after reading data from the file into the buffer, flip it again so that you can read data from a buffer to the popular contents of an object. Many Java programmer does this mistake of not flipping after writing and ends up debugging hours because either nothing is written to file or nothing they can read from a file.

That's all about how to read/write a File using FileChannel and ByteBuffer in Java. In this demon, I have shown you how to read and write a RandomAccessFile using FileChannel and ByteBuffer, but you can apply the same technique to read any other text or binary file from the Java program.

Other Java IO tutorials you may like:
  • How to read the file in one line in Java 8? (solution)
  • How to read the file line by line in Java using BufferedReader and Scanner? (answer)
  • How to read/write an Excel file in Java? (program)
  • How to read CSV files in Java? (program)
  • How to append data into an existing file in Java? (answer)

Thanks for reading this article so far. If you like this Java FileChannel Tutorial and Example then please share with your friends and colleagues. If you have any questions or feedback, please ask. 

No comments :

Post a Comment