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);
}
}
}
Related Topics
Privacy Policy, Terms and Conditions, DMCA Policy and Compliant
Copyright © 2018-2023 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.