Tuesday 28 March 2017

The New I/O Packages - Java Tutorials ( Page 2 of 2 )

Here is how the program works. First, a file is opened by using the FileInputStream constructor and a reference to that object is assigned to fIn. Next, a channel connected to the file is obtained by calling getChannel( ) on fIn and the size of the file is obtained by calling size( ). The program then calls the allocate( ) method of ByteBuffer to allocate a buffer that will hold the contents of the file when it is read. A byte buffer is used because FileChannel operates on bytes. A reference to this buffer is stored in mBuf. The contents of the file are then read into mBuf through a call to read( ). Next, the buffer is rewound through a call to rewind( ). This call is necessary because the current position is at the end of the buffer after the call to read( ). It must be reset to the start of the buffer in order for the bytes in mBuf to be read by calling get( ). Because mBuf is a byte buffer, the values returned by get( ) are bytes. They are cast to char so that the file can be displayed as text. (Alternatively, it is possible to create a buffer that encodes the bytes into characters, and then read that buffer.) The program ends by closing the channel and the file.

A second, and often easier way to read a file is to map it to a buffer. The advantage to this approach is that the buffer automatically contains the contents of the file. No explicit read operation is necessary. To map and read the contents of a file, follow this general procedure. First, open the file using FileInputStream. Next, obtain a channel to that file by calling getChannel( ) on the file object. Then, map the channel to a buffer by calling map( ) on the FileChannel object. The map( ) method is shown here:

      MappedByteBuffer map(FileChannel.MapMode how,
                                              long pos, long size) throws IOException

The map( ) method causes the data in the file to be mapped into a buffer in memory. The value in how determines what type of operations are allowed. It must be one of these values.
  • MapMode.READ 
  • MapMode.READ_WRITE 
  • MapMode.PRIVATE

For reading a file, use MapMode.READ. To read and write, use MapeMode.READ_WRITE. MapMode.PRIVATE causes a private copy of the file to be made and changes to the buffer do not affect the underlying file. The location within the file to begin mapping is specified by pos and the number of bytes to map are specified by size. A reference to this buffer is returned as a MappedByteBuffer, which is a subclass of ByteBuffer. Once the file has been mapped to a buffer, you can read the file from that buffer.

The following program reworks the first example so that it uses a mapped file.

  // Use a mapped file to read a text file.
  import java.io.*;
  import java.nio.*;
  import java.nio.channels.*;

  public class MappedChannelRead {
    public static void main(String args[]) {
      FileInputStream fIn;
      FileChannel fChan;
      long fSize;
      MappedByteBuffer mBuf;

      try {
        // First, open an file for input.
        fIn = new FileInputStream("test.txt");

        // Next, obtain a channel to that file.
        fChan = fIn.getChannel();

        // Get the size of the file.
        fSize = fChan.size();

        // Now, map the file into a buffer.
        mBuf = fChan.map(FileChannel.MapMode.READ_ONLY,
                         0, fSize);

        // Read bytes from the buffer.
        for(int i=0; i < fSize; i++)
          System.out.print((char)mBuf.get());

        fChan.close(); // close channel
        fIn.close(); // close file
      } catch (IOException exc) {
        System.out.println(exc);
        System.exit(1);
      }
    }
  }

As before, the file is opened by using the FileInputStream constructor and a reference to that object is assigned to fIn. A channel connected to the file is obtained by calling getChannel( ) on fIn, and the size of the file is obtained. Then the entire file is mapped into memory by calling map( ) and a reference to the buffer is stored in mBuf. The bytes in mBuf are read by calling get( ).

Writing to a File
There are several ways to write to a file through a channel. Again, we will look at two. First, you can write data to an output file through a channel, by using explicit write operations. Second, if the file is opened for read/write operations, you can map the file to a buffer and then write to that buffer. Changes to the buffer will automatically be reflected in the file. Both ways are described here.

To write to a file through a channel using explicit calls to write( ), follow these steps. First, open the file for output. Then, allocate a byte buffer, put the data you want to write into that buffer, and then called write( ) on the channel. The following program demonstrates this procedure. It writes the alphabet to a file called test.txt.

  // Write to a file using the new I/O.
  import java.io.*;
  import java.nio.*;
  import java.nio.channels.*;

  public class ExplicitChannelWrite {
    public static void main(String args[]) {
      FileOutputStream fOut;
      FileChannel fChan;
      ByteBuffer mBuf;
  
      try {
        fOut = new FileOutputStream("test.txt");

        // Get a channel to the output file.
        fChan = fOut.getChannel();

        // Create a buffer.
        mBuf = ByteBuffer.allocateDirect(26);

        // Write some bytes to the buffer.
        for(int i=0; i<26; i++)
          mBuf.put((byte)('A' + i));

        // Rewind the buffer so that it can written.
        mBuf.rewind();

        // Write the buffer to the output file.
        fChan.write(mBuf);

        // close channel and file.
        fChan.close();
        fOut.close();
      } catch (IOException exc) {
        System.out.println(exc);
        System.exit(1);
      }
    }
  }

The call to rewind( ) on mBuf is necessary in order to reset the current position to zero after data has been written to mBuf. Remember, each call to put( ) advances the current position. Therefore, it is necessary for the current position to be reset to the start of the buffer before calling write( ). If this is not done, write( ) will think that there is no data in the buffer.

To write to a file using a mapped file, follow these steps. First, open the file for read/write operations. Next, map that file to a buffer by calling map( ). Then, write to the buffer. Because the buffer is mapped to the file, any changes to that buffer are automatically reflected in the file. Thus, no explicit write operations to the channel are necessary. Here is the preceding program reworked so that a mapped file is used. Notice that the file is opened as a RandomAccessFile. This is necessary to allow the file to be read and written.

  // Write to a mapped file.
  import java.io.*;
  import java.nio.*;
  import java.nio.channels.*;

  public class MappedChannelWrite {
    public static void main(String args[]) {
      RandomAccessFile fOut;
      FileChannel fChan;
      ByteBuffer mBuf;

      try {
        fOut = new RandomAccessFile("test.txt", "rw");

        // Next, obtain a channel to that file.
        fChan = fOut.getChannel();

        // Then, map the file into a buffer.
        mBuf = fChan.map(FileChannel.MapMode.READ_WRITE,
                         0, 26);

        // Write some bytes to the buffer.
        for(int i=0; i<26; i++)
          mBuf.put((byte)('A' + i));

        // close channel and file.
        fChan.close();
        fOut.close();
      } catch (IOException exc) {
        System.out.println(exc);
        System.exit(1);
      }
    }
  }

As you can see, there are no explicit write operations to the channel, itself. Because mBuf is mapped to the file, changes to mBuf are automatically reflected in the underlying file.


Copying a File Using the New I/O

The new I/O system simplifies some types of file operations. For example, the following program copies a file. It does so by opening an input channel to the source file and an output channel to the target file. It then writes the mapped input buffer to the output file in a single operation. As you will find, the part of the program that actually copies the file is substantially shorter.

  // Copy a file using NIO.
  import java.io.*;
  import java.nio.*;
  import java.nio.channels.*;

  public class NIOCopy {
    public static void main(String args[]) {
      FileInputStream fIn;
      FileOutputStream fOut;
      FileChannel fIChan, fOChan;
      long fSize;
      MappedByteBuffer mBuf;

      try {
        fIn = new FileInputStream(args[0]);
        fOut = new FileOutputStream(args[1]);

        // Get channels to the input and output files.
        fIChan = fIn.getChannel();
        fOChan = fOut.getChannel();

        // Get the size of the file.
        fSize = fIChan.size();

        // Map the input file to a buffer.
        mBuf = fIChan.map(FileChannel.MapMode.READ_ONLY,
                          0, fSize);

        // Write the buffer to the output file.
        fOChan.write(mBuf); // this copies the file

        // Close the channels and files.
        fIChan.close();
        fIn.close();
        fOChan.close();
        fOut.close();
      } catch (IOException exc) {
        System.out.println(exc);
        System.exit(1);
      } catch (ArrayIndexOutOfBoundsException exc) {
        System.out.println("Usage: Copy from to");
        System.exit(1);
      }
    }
  }

Because the input file is mapped to mBuf, it contains the entire source file. Thus, the call to write( ) copies all of mBuf to the target file. This, of course, means that the target file is an identical copy of the source file.


Is NIO the Future of I/O Handling?

The new I/O APIs offer an exciting new way to think about and handle some types of file operations. Because of this it is natural to ask the question, “Is NIO the future of I/O handling?” Unfortunately, at the time of this writing, this question cannot be answered. Certainly, channels and buffers offer a clean way of thinking about I/O. However, they also add another layer of abstraction. Furthermore, the traditional stream-based approach is both well-understood, and widely used. As explained at the outset, channel-based I/O is currently designed to supplement, not replace the standard I/O mechanisms defined in java.io. In this role, the channel/buffer approach used by the NIO APIs succeeds admirably. Whether the new approach will someday supplant the traditional approach, only time and usage patterns will tell.

No comments:

Post a Comment