Chapter: Java The Complete Reference - The Java Library - Input/Output: Exploring java.io

| Study Material, Lecturing Notes, Assignment, Reference, Wiki description explanation, brief detail |

Use NIO for Channel-Based I/O

An important use of NIO is to access a file via a channel and buffers. The following sections demonstrate some techniques that use a channel to read from and write to a file.

Use NIO for Channel-Based I/O

An important use of NIO is to access a file via a channel and buffers. The following sections demonstrate some techniques that use a channel to read from and write to a file.

Reading a File via a Channel

 

There are several ways to read data from a file using a channel. Perhaps the most common way is to manually allocate a buffer and then perform an explicit read operation that loads that buffer with data from the file. It is with this approach that we begin.

 

Before you can read from a file, you must open it. To do this, first create a Path that describes the file. Then use this Path to open the file. There are various ways to open the file depending on how it will be used. In this example, the file will be opened for byte-based input via explicit input operations. Therefore, this example will open the file and establish a channel to it by calling Files.newByteChannel( ). The newByteChannel( ) method has this general form:

 

static SeekableByteChannel newByteChannel(Path path, OpenOption ... how) throws IOException

 

It returns a SeekableByteChannel object, which encapsulates the channel for file operations. The Path that describes the file is passed in path. The how parameter specifies how the file will be opened. Because it is a varargs parameter, you can specify zero or more comma-separated arguments. (The valid values were discussed earlier and shown in Table 21-5.) If no arguments are specified, the file is opened for input operations. SeekableByteChannel is an interface that describes a channel that can be used for file operations. It is implemented by the FileChannel class. When the default file system is used, the returned object can be cast to FileChannel. You must close the channel after you have finished with it. Since all channels, including FileChannel, implement AutoCloseable, you can use a try-with-resources statement to close the file automatically instead of calling close( ) explicitly. This approach is used in the examples.

 

Next, you must obtain a buffer that will be used by the channel either by wrapping an existing array or by allocating the buffer dynamically. The examples use allocation, but the choice is yours. Because file channels operate on byte buffers, we will use the allocate( ) method defined by ByteBuffer to obtain the buffer. It has this general form:

 

static ByteBuffer allocate(int cap)

 

Here, cap specifies the capacity of the buffer. A reference to the buffer is returned.

 

After you have created the buffer, call read( ) on the channel, passing a reference to the buffer. The version of read( ) that we will use is shown next:

 

int read(ByteBuffer buf) throws IOException

Each time it is called, read( ) fills the buffer specified by buf with data from the file. The reads are sequential, meaning that each call to read( ) reads the next buffer’s worth of bytes from the file. The read( ) method returns the number of bytes actually read. It returns –1 when there is an attempt to read at the end of the file.

 

The following program puts the preceding discussion into action by reading a file called test.txt through a channel using explicit input operations:

 

// Use Channel I/O to read a file. Requires JDK 7 or later.

 

import java.io.*; import java.nio.*;

import java.nio.channels.*; import java.nio.file.*;

 

public class ExplicitChannelRead {

 

public static void main(String args[]) { int count;

 

Path filepath = null;

 

    First, obtain a path to the file. try {

 

filepath = Paths.get("test.txt"); } catch(InvalidPathException e) {

 

System.out.println("Path Error " + e); return;

 

}

 

    Next, obtain a channel to that file within a try-with-resources block. try ( SeekableByteChannel fChan = Files.newByteChannel(filepath) )

{

 

    Allocate a buffer.

 

ByteBuffer mBuf = ByteBuffer.allocate(128);

 

do {

 

// Read a buffer.

 

count = fChan.read(mBuf);

 

// Stop when end of file is reached.

if(count != -1) {

 

    //Rewind the buffer so that it can be read.

    mBuf.rewind();

 

    //Read bytes from the buffer and show

 

    //them on the screen as characters.

 

    for(int i=0; i < count; i++)

System.out.print((char)mBuf.get());

 

}

 

} while(count != -1);

 

System.out.println();

 

} catch (IOException e) { System.out.println("I/O Error " + e);

}

 

}

 

}

 

Here is how the program works. First, a Path object is obtained that contains the relative path to a file called test.txt. A reference to this object is assigned to filepath. Next, a channel connected to the file is obtained by calling newByteChannel( ), passing in filepath. Because no open option is specified, the file is opened for reading. Notice that this channel is the object managed by the try-with-resources statement. Thus, the channel is automatically closed when the block ends. 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 reference to this buffer is stored in mBuf. The contents of the file are then read, one buffer at a time, into mBuf through a call to read( ). The number of bytes read is stored in count. 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( ). (Recall that get( ) is defined by ByteBuffer.) Because mBuf is a byte buffer, the values returned by get( ) are bytes. They are cast to char so 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.) When the end of the file has been reached, the value returned by read( ) will be –1. When this occurs, the program ends, and the channel is automatically closed.

As a point of interest, notice that the program obtains the Path within one try block and then uses another try block to obtain and manage a channel linked to that path. Although there is nothing wrong, per se, with this approach, in many cases, it can be streamlined so that only one try block is needed. In this approach, the calls to Paths.get( ) and newByteChannel( ) are sequenced together. For example, here is a reworked version of the program that uses this approach:

 

 

// A more compact way to open a channel. Requires JDK 7 or later.

 

import java.io.*; import java.nio.*;

 

import java.nio.channels.*; import java.nio.file.*;

 

public class ExplicitChannelRead {

 

public static void main(String args[]) { int count;

 

    //Here, the channel is opened on the Path returned by Paths.get().

 

    There is no need for the filepath variable.

 

try (

SeekableByteChannel fChan = Files.newByteChannel(Paths.get("test.txt")) )

 

{

 

// Allocate a buffer.

 

ByteBuffer mBuf = ByteBuffer.allocate(128);

 

do {

 

// Read a buffer.

count = fChan.read(mBuf);

// Stop when end of file is reached.

if(count != -1) {

 

    //Rewind the buffer so that it can be read.

    mBuf.rewind();

 

    //Read bytes from the buffer and them on the screen as characters.

 

    for(int i=0; i < count; i++)

 

System.out.print((char)mBuf.get());

 

}

} while(count != -1);

 

System.out.println();

 

} catch(InvalidPathException e) { System.out.println("Path Error " + e);

 

} catch (IOException e) { System.out.println("I/O Error " + e);

}

 

}

 

}

 

In this version, the variable filepath is not needed and both exceptions are handled by the same try statement. Because this approach is more compact, it is the approach used

in the rest of the examples in this chapter. Of course, in your own code, you may encounter situations in which the creation of a Path object needs to be separate from the acquisition of a channel. In these cases, the previous approach can be used.

Another way to read a file is to map it to a buffer. The advantage 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, obtain a Path object that encapsulates the file as previously described. Next, obtain a channel to that file by calling Files.newByteChannel( ), passing in the Path and casting the returned object to FileChannel. As explained, newByteChannel( ) returns a SeekableByteChannel. When using the default file system, this object can be cast to FileChannel. Then, map the channel to a buffer by calling map( ) on the channel. The map( ) method is defined by FileChannel. This is why the cast to FileChannel is needed. The map( ) function 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:


For reading a file, use MapMode.READ_ONLY. To read and write, use MapMode.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. Here is an example that illustrates this approach:

 

// Use a mapped file to read a file. Requires JDK 7 or later.

 

import java.io.*; import java.nio.*;

import java.nio.channels.*; import java.nio.file.*;

 

public class MappedChannelRead {

 

public static void main(String args[]) {

// Obtain a channel to a file within a try-with-resources block. try ( FileChannel fChan =

(FileChannel) Files.newByteChannel(Paths.get("test.txt")) )

 

{

 

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

 

    Now, map the file into a buffer.

 

MappedByteBuffer mBuf = fChan.map(FileChannel.MapMode.READ_ONLY, 0, fSize);

 

// Read and display bytes from buffer.

for(int i=0; i < fSize; i++)

System.out.print((char)mBuf.get());

 

System.out.println();

 

} catch(InvalidPathException e) { System.out.println("Path Error " + e);

} catch (IOException e) {

 

System.out.println("I/O Error " + e);

 

}

 

}

 

}

 

In the program, a Path to the file is created and then opened via newByteChannel( ). The channel is cast to FileChannel and stored in fChan. Next, the size of the file is obtained by calling size( ) on the channel. Then, the entire file is mapped into memory by calling map( ) on fChan and a reference to the buffer is stored in mBuf. Notice that mBuf is declared as a reference to a MappedByteBuffer. The bytes in mBuf are read by calling get( ).

 

Writing to a File via a Channel

 

As is the case when reading from a file, there are also several ways to write data to a file using a channel. We will begin with one of the most common. In this approach, you manually allocate a buffer, write data to that buffer, and then perform an explicit write operation to write that data to a file.

 

Before you can write to a file, you must open it. To do this, first obtain a Path that describes the file and then use this Path to open the file. In this example, the file will be opened for byte-based output via explicit output operations. Therefore, this example will open the file and establish a channel to it by calling Files.newByteChannel( ). As shown in the previous section, the newByteChannel( ) method has this general form:

static SeekableByteChannel newByteChannel(Path path, OpenOption ... how) throws IOException

It returns a SeekableByteChannel object, which encapsulates the channel for file operations. To open a file for output, the how parameter must specify StandardOpenOption.WRITE.

If you want to create the file if it does not already exist, then you must also specify StandardOpenOption.CREATE. (Other options, which are shown in Table 21-5, are also available.) As explained in the previous section, SeekableByteChannel is an interface that describes a channel that can be used for file operations. It is implemented by the FileChannel class. When the default file system is used, the return object can be cast to FileChannel. You must close the channel after you have finished with it.

Here is one way to write to a file through a channel using explicit calls to write( ). First, obtain a Path to the file and then open it with a call to newByteChannel( ), casting the result to FileChannel. Next, allocate a byte buffer and write data to that buffer. Before the data

is written to the file, call rewind( ) on the buffer to set its current position to zero. (Each output operation on the buffer increases the current position. Thus, it must be reset prior to writing to the file.) Then, call write( ) on the channel, passing in the buffer. The following program demonstrates this procedure. It writes the alphabet to a file called test.txt.

 

// Write to a file using NIO. Requires JDK 7 or later.

 

import java.io.*; import java.nio.*;

 

import java.nio.channels.*; import java.nio.file.*;

 

public class ExplicitChannelWrite {

 

public static void main(String args[]) {

 

// Obtain a channel to a file within a try-with-resources block.

try ( FileChannel fChan = (FileChannel)

 

Files.newByteChannel(Paths.get("test.txt"),

 

StandardOpenOption.WRITE, StandardOpenOption.CREATE) )

 

{

 

// Create a buffer.

 

ByteBuffer mBuf = ByteBuffer.allocate(26);

 

    //Write some bytes to the buffer.

for(int i=0; i<26; i++)

 

mBuf.put((byte)('A' + i));

 

    //Reset the buffer so that it can be written.

    mBuf.rewind();

 

    //Write the buffer to the output file.

 

    fChan.write(mBuf);

 

} catch(InvalidPathException e) { System.out.println("Path Error " + e);

 

} catch (IOException e) { System.out.println("I/O Error: " + e); System.exit(1);

 

}

 

}

 

}

 

It is useful to emphasize an important aspect of this program. As mentioned, after data is written to mBuf, but before it is written to the file, a call to rewind( ) on mBuf is made. This is necessary in order to reset the current position to zero after data has been written to mBuf. Remember, each call to put( ) on mBuf 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.

Another way to handle the resetting of the buffer between input and output operations is to call flip( ) instead of rewind( ). The flip( ) method sets the value of the current position to zero and the limit to the previous current position. In the preceding example, because the capacity of the buffer equals its limit, flip( ) could have been used instead of rewind( ). However, the two methods are not interchangeable in all cases.

In general, you must reset the buffer between read and write operations. For example, assuming the preceding example, the following loop will write the alphabet to the file three times. Pay special attention to the calls to rewind( ).

 

for(int h=0; h<3; h++) {

 

    //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 be written.

    mBuf.rewind();

 

    //Write the buffer to the output file.

 

    fChan.write(mBuf);

 

    //Rewind the buffer so that it can be written to again.

 

    mBuf.rewind();

 

}

 

Notice that rewind( ) is called between each read and write operation.

 

One other thing about the program warrants mentioning: When the buffer is written to the file, the first 26 bytes in the file will contain the output. If the file test.txt was preexisting, then after the program executes, the first 26 bytes of test.txt will contain the alphabet, but the remainder of the file will remain unchanged.

Another way to write to a file is to map it to a buffer. The advantage to this approach is that the data written to the buffer will automatically be written to the file. No explicit write operation is necessary. To map and write the contents of a file, we will use this general procedure. First, obtain a Path object that encapsulates the file and then create a channel to that file by calling Files.newByteChannel( ), passing in the Path. Cast the reference returned by newByteChannel( ) to FileChannel. Next, map the channel to a buffer by calling map( ) on the channel. The map( ) method was described in detail in the previous section. It is summarized here for your convenience. Here is its general form:

 

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. For writing to a file, how must be MapMode.READ_WRITE. 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. Once the file has been mapped to a buffer, you can write data to that buffer, and it will automatically be written to the file. Therefore, no explicit write operations to the channel are necessary.

 

Here is the preceding program reworked so that a mapped file is used. Notice that in the call to newByteChannel( ), the open option StandardOpenOption.READ has been added. This is because a mapped buffer can either be read-only or read/write. Thus, to write to the mapped buffer, the channel must be opened as read/write.

 

// Write to a mapped file. Requires JDK 7 or later.

 

import java.io.*; import java.nio.*;

import java.nio.channels.*; import java.nio.file.*;

 

public class MappedChannelWrite {

 

public static void main(String args[]) {

 

// Obtain a channel to a file within a try-with-resources block.

try ( FileChannel fChan = (FileChannel)

Files.newByteChannel(Paths.get("test.txt"),

 

StandardOpenOption.WRITE,

 

StandardOpenOption.READ, StandardOpenOption.CREATE) )

{

 

// Then, map the file into a buffer.

 

MappedByteBuffer 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));

 

} catch(InvalidPathException e) { System.out.println("Path Error " + e);

} catch (IOException e) { System.out.println("I/O Error " + e);

}

 

}

 

}

 

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 NIO

 

NIO simplifies several types of file operations. Although we can’t examine them all, an example will give you an idea of what is available. The following program copies a file using a call to a single NIO method: copy( ), which is a static method defined by Files. It has several forms. Here is the one we will be using:

 

static Path copy(Path src, Path dest, CopyOption ... how) throws IOException

 

The file specified by src is copied to the file specified by dest. How the copy is performed is specified by how. Because it is a varargs parameter, it can be missing. If specified, it can be one or more of these values, which are valid for all file systems:


Other options may be supported, depending on the implementation.

 

The following program demonstrates copy( ). The source and destination files are specified on the command line, with the source file specified first. Notice how short the program is. You might want to compare this version of the file copy program to the one found in Chapter 13. As you will find, the part of the program that actually copies the file is substantially shorter in the NIO version shown here.

 

// Copy a file using NIO. Requires JDK 7 or later.

import java.io.*;

 

import java.nio.*;

 

import java.nio.channels.*;

import java.nio.file.*;

public class NIOCopy {

public static void main(String args[]) {

 

if(args.length != 2) { System.out.println("Usage: Copy from to"); return;

 

}

 

try {

 

Path source = Paths.get(args[0]); Path target = Paths.get(args[1]);

 

// Copy the file.

 

Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

 

} catch(InvalidPathException e) { System.out.println("Path Error " + e);

 

} catch (IOException e) { System.out.println("I/O Error " + e);

}

 

}

 

}

 


Study Material, Lecturing Notes, Assignment, Reference, Wiki description explanation, brief detail


Copyright © 2018-2020 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.