Chapter 11

Reading Files

WHAT YOU’LL LEARN IN THIS CHAPTER

  • How to obtain a file channel for reading a file
  • How to use buffers in file channel read operations
  • How to read different types of data from a file
  • How to retrieve data from random positions in a file
  • How you can read from and write to the same file
  • How you can do direct data transfer between channels
  • What a memory-mapped file is and how you can access a memory-mapped file
  • What a file lock is and how you can lock all or part of a file

In this chapter you investigate how you read files containing basic types of data. You explore how to read files sequentially or at random and how you can open a file for both read and write operations.

FILE READ OPERATIONS

The process for reading a file parallels that of writing a file so if you are comfortable with writing files, this chapter is going to be easy. You have three ways for reading files, all provided by static methods in the java.nio.file.Files class:

  • The newInputStream() method returns an InputStream object, which you can use to read a binary file.
  • The newBufferedReader() method returns a BufferedReader object, which you can use to read a file containing character data.
  • The newByteChannel() method that you used in the Chapter 10 returns a reference to a FileChannel object as type SeekableByteChannel, which you can use to read a file when the READ open option is in effect. You can read binary or character data from the file.

I cover the first two briefly because they are quite simple and then concentrate on the third option, which uses a FileChannel object.

Reading a File Using an Input Stream

Here’s how you create an input stream to read a given file from the beginning:

    Path file = Paths.get(System.getProperty("user.home")).
                      resolve("Beginning Java Stuff").resolve("MyFile.txt");
// Make sure we have a directory...
 
try(BufferedInputStream in =
                        new BufferedInputStream(Files.newInputStream(file))){
  // read the file...
} catch(IOException e) {
  e.printStackTrace();
}
 

The first argument to the newInputStream() method is the path to the file. You can supply optional arguments following the first to specify the open options to be in effect when the file is opened. These are the options from the java.nio.file.StandardOpenOption enumeration that you saw in the previous chapter. The newInputStream() method assumes the READ open option if you do not specify any options. The method can throw the following exceptions:

  • IllegalArgumentException if you specify an invalid combination of options
  • UnsupportedOperationException if you specify an option that is not supported
  • IOException if an I/O error occurs while opening the file
  • SecurityException if the installed security manager determines that you are not permitted to read the file

Wrapping the InputStream object that the method returns in a java.io.BufferedInputStream object provides more efficient read operations. The stream has an internal buffer of a default size that is automatically filled by reading from the file as you read from the buffered stream. You can create a BufferedInputStream object with a buffer of a given size by specifying a second argument to the constructor of type int that determines the capacity of the stream buffer in bytes.

Buffered Stream Read Operations

You have two methods that read from a buffered input stream:

  • read() reads a single byte from the stream buffer and returns it as a value of type int. If the end-of-file (EOF) is reached, the method returns −1.
  • read(byte[] bytes, int offset, int length) reads up to length bytes from the stream and stores them in the bytes array starting at index position offset. Less than length bytes are read if there are fewer than this number of bytes available up to end-of-file. The method returns the actual number of bytes read as type int or −1 if no bytes were read because EOF was reached. The method throws a NullPointerException if the first argument is null and an IndexOutOfBoundsException if offset or length are invalid or if the bytes.length-offset is less than length.

Both methods throw an IOException if the stream has been closed or if an I/O error occurs.

You can skip over a given number of bytes in the file by calling the skip() method for the stream object. You specify the number of bytes that you want to skip as an argument of type long. The method returns the actual number of bytes skipped as a long value. The method throws an IOException if an I/O error occurs or if the underlying stream does not support seek, which means when mark() and reset() operations are not supported.

The available() method for a BufferedInputStream returns an estimate of the number of bytes you can read or skip over without blocking as a value of type int. A stream can be blocked if another thread of execution is reading from the same file.

Marking a Stream

You can mark the current position in a buffered stream by calling mark() for the stream object. The argument to this method is an integer specifying the number of bytes that can be read or skipped before the current mark is invalidated. You can return to the position in the stream that you have marked by calling the reset() method for the stream object. This enables you to read a section of a file repeatedly.

The markSupported() method for a BufferedInputStream object tests if the stream supports marking the stream. It returns true if mark() and reset() are supported and false otherwise.

Let’s try an example.

TRY IT OUT: Reading a Binary File

In this example, you read the fibonacci.bin file that you created in the Junk directory in the previous chapter.

image
import java.nio.file.*;
import java.nio.*;
import java.io.*;
 
public class StreamInputFromFile {
  public static void main(String[] args) {
 
    Path file = Paths.get(System.getProperty("user.home")).
                      resolve("Beginning Java Stuff").resolve("fibonnaci.bin");
 
    if(!Files.exists(file)) {
      System.out.println(file + " does not exist. Terminating program.");
      System.exit(1);
    }
 
    final int count = 6;                         // Number of values to be read each time
 
    // Buffer to hold count values
    ByteBuffer buf = ByteBuffer.allocate(8*count); 
 
    LongBuffer values = buf.asLongBuffer();
    byte[] bytes = buf.array();                 // Backing array for buf
    int totalRead = 0;                          // Total value read
    try(BufferedInputStream fileIn =
                          new BufferedInputStream(Files.newInputStream(file))){
      int numberRead = 0;
      while(true) {
        numberRead = fileIn.read(bytes, 0, bytes.length);
        if(numberRead == -1)                    // EOF reached
          break;
        totalRead += numberRead/8;              // Increment total
 
        for(int i = 0 ; i < numberRead/8 ; ++i)  // Access as many as there are
          System.out.format("%12d", values.get());
 
        System.out.println();                   // New line
        values.flip();                           // Reset for next input
      }
    System.out.format("%d  values read.%n", totalRead);
    } catch(IOException e) {
      System.err.println("Error writing file: " + file);
      e.printStackTrace();
    }
  }
}
 

StreamInputFromFile.java

I got the following output:

           0           1           1           2           3           5
           8          13          21          34          55          89
         144         233         377         610         987        1597
        2584        4181        6765       10946       17711       28657
       46368       75025      121393      196418      317811      514229
      832040     1346269     2178309     3524578     5702887     9227465
    14930352    24157817    39088169    63245986   102334155   165580141
   267914296   433494437   701408733  1134903170  1836311903  2971215073
  4807526976  7778742049
50  values read.
 

It looks as though I got the same number of values back from the file as I wrote, which is very encouraging.

How It Works

After creating the file path you verify that it does really exist. If the file does not exist, you end the program. After you are sure the file is there, you set up the buffers you use to read the file. A ByteBuffer is convenient here because you can pass the backing byte array to the read() method for the stream to read the data and use a view buffer for values of type long to retrieve the Fibonacci numbers. You create the ByteBuffer with a capacity for 6 values of type long because you can output this many on a single line. The totalRead variable accumulates the number of values that you read from the file.

You create a BufferedInputStream object that wraps the InputStream object that is returned by the getInputStream() method for efficient stream input operations. You don’t need to specify any open options for the file because the default option is READ.

You read the file in the indefinite while loop into the bytes array that backs buf. The loop is terminated when the read() method for the stream returns −1, indicating that EOF has been reach. You access the data that was read through the view buffer, values:

        for(int i = 0 ; i < numberRead/8 ; ++i)   // Access as many as there are
          System.out.format("%12d", values.get());
 

You can’t use the value that the hasRemaining() method for the view buffer returns to indicate how many values you have available because the position and limit for the view buffer are not updated when you read data into the bytes array. The read operation does not involve buf or values in any direct way and only the get() and put() methods for a buffer update the position and limit. However, numberRead does reflect the number of bytes actually read from the file, so dividing this by 8 gives you the number of values read for each operation.

Using get() for the view buffer does change its position so you need to call flip() for values to reset its state ready for the next loop iteration.

At the end of the try block, the stream and the file are closed automatically.

If you want to try marking the input stream to re-read it, you could replace the try block in the example with the following code:

      boolean markIt = true;
      while(true) {
        if(markIt)
          fileIn.mark(fileIn.available());
 
        numberRead = fileIn.read(bytes, 0, bytes.length);
        if(numberRead == -1)                        // EOF reached
          break;
 
        totalRead += numberRead/8;                  // Increment total
 
        for(int i = 0 ; i < numberRead/8 ; ++i)     // Read long buffer
          System.out.format("%12d", values.get());
 
        System.out.println();                       // New line
        values.flip();                              // Reset for next input
        if(markIt)
          fileIn.reset();
 
        markIt = !markIt;
      }
    System.out.format("%d  values read.%n", totalRead);
    } catch(IOException e) {
      System.err.println("Error writing file: " + file);
      e.printStackTrace();
    }
 

This uses a boolean variable, markIt, that flip-flops between true and false in the while loop. When markIt is true, you call mark() to mark the stream and after the input has been processed you call reset() to reset the stream to the mark. When markIt is false you don’t reset the stream to the mark. This has the effect of reading and processing each block of input twice. You might want to do this when you were unsure about the format of the data. You could read the input from the stream once and check key items to ascertain whether it had been read correctly. If it has not, you could read it again using a different type of view buffer.

Reading a File Using a Buffered Reader

You can create a BufferedReader object that can read a given character file by calling the static newBufferedReader() method defined by the java.nio.file.Files class. The first argument is a reference to a Path object specifying the file that is to be read and the second argument is a reference to a Charset object that is the charset to be used for converting the bytes to Unicode characters. I discussed Charset objects in the previous chapter so I won’t repeat it here. Here’s how you might create a buffered reader for reading a file specified by a Path object, path:

BufferedReader inFile = Files.newBufferedReader(path, Charset.forName("UTF-16"));
 

The newBufferedReader() method throws an IOException if an I/O error occurs when opening the file so it should be in a try block.

There are three methods that a BufferedReader object provides for reading the file:

  • read() reads a single character from the file and returns it as type int. The method returns -1 if EOF is read.
  • read(char[] chars, int offset, int length) attempts to read length characters from the file storing them in the chars array beginning at index position offset. The method returns the number of characters read or −1 if EOF is the first character read.
  • readLine() reads a line of text from the file that is terminated by a line separator character and returns it as type String. The method returns null if EOF was reached.

All three methods throw an IOException if an I/O error occurs.

You can skip over characters in the file by calling the skip() method for the buffered reader. You specify the number of characters to be skipped as an argument of type long. It returns the actual number of characters skipped and throws an IOException if an I/O error occurs. It throws an IllegalArgumentException if you supply a negative argument.

You have mark(), reset(), and markSupported() methods available for a buffered reader that work in the same way as for a buffered input stream. Let’s see a BufferedReader object in action.

TRY IT OUT: Reading a File Using a Buffered Reader

This example uses a BufferedReader object to read the Sayings.txt file that you wrote in the previous chapter using a BufferedWriter object:

image
import java.io.*;
import java.nio.file.*;
import java.nio.charset.Charset;
 
public class ReaderInputFromFile {
  public static void main(String[] args) {
    Path file = Paths.get(System.getProperty("user.home")).
                      resolve("Beginning Java Stuff").resolve("Sayings.txt");
    if(!Files.exists(file)) {
      System.out.println(file + " does not exist. Terminating program.");
      System.exit(1);;
    }
 
    try(BufferedReader fileIn =
                  new BufferedReader(Files.newBufferedReader(
                                           file, Charset.forName("UTF-16")))){
      String saying = null;              // Stores a saying
      int totalRead = 0;                 // Acumulates number of sayings
      // Read sayings until we reach reach EOF
      while((saying = fileIn.readLine()) != null) {
        System.out.println(saying);
        ++totalRead;                     // Increment count
      }
    System.out.format("%d  sayings read.%n", totalRead);
    } catch(IOException e) {
      System.err.println("Error writing file: " + file);
      e.printStackTrace();
    }
  }
}
 

ReaderInputFromFile.java

I got the following output:

A nod is as good as a wink to a blind horse.
Least said, soonest mended.
There are 10 kinds of people in the world, those that understand binary
                                                            and those that don't.
You can't make a silk purse out of a sow's ear.
Hindsight is always twenty-twenty.
Existentialism has no future.
Those who are not confused are misinformed.
Better untaught that ill-taught.
8  sayings read.
 

The similarity to what was written in the previous chapter is uncanny.

How It Works

Because each saying is terminated by a line separator, it’s easy to read the file back. The readLine() method for the BufferedReader object returns either a string, or null when EOF is read. Reading the file and storing the result happens in the loop expression. The while loop, therefore, continues until you reach the end of the file. The loop just writes each saying to standard output and increments the count of the number of lines read. It couldn’t be simpler!

READING A FILE USING A CHANNEL

You would use channels for file I/O when high-performance is a primary requirement and when you need multi-threaded to a file. That doesn’t apply in our simple examples but they will show you how you can work with a channel. You used the static newByteChannel() method from the Files class in the previous chapter to obtain a FileChannel object that writes a file. If you supply no explicit open option arguments, the READ option is the default. You can then use the channel to read data from the file into one or more buffers. For example:

Path file = Paths.get("D:/Junk/MyFile.txt");
// Make sure we have a directory...
try(ReadableByteChannel inCh = Files.newByteChannel(file)){
  // read the file...
} catch(IOException e) {
  e.printStackTrace(System.err);
}
 

This creates a reference to a FileChannel object in the inCh variable with the file that is specified by the file argument opened for reading. Of course, there’s no harm in explicitly specifying the READ option when you want to read a file.

File Channel Read Operations

The following three FileChannel read operations read bytes starting at the byte indicated by the current position in the file:

  • int read(ByteBuffer buf)): Tries to read buf.remaining() bytes (equivalent to limit-position bytes) from the file into the buffer, buf, starting at the buffer’s current position. The number of bytes read is returned, or −1 if the channel reaches the end-of-file during the operation. The buffer position is incremented by the number of bytes read and the buffer’s limit is left unchanged.
  • int read(ByteBuffer[] buffers)): Tries to read bytes into each of the buffers in the buffers array in sequence. Bytes are read into each buffer starting at the point defined by that buffer’s position. The number of bytes read into each buffer is defined by the remaining() method for that buffer. The read() method returns the total number of bytes read or −1 if the channel reaches the end-of-file during the operation. Each buffer’s position is incremented by the number of bytes read into it. Each buffer’s limit is unchanged.
  • int read(ByteBuffer[] buffers,int offset, int length)): This operations works in the same way as the previous method except that bytes are read starting with the buffer buffers[offset], up to and including the buffer buffers[offset+length-1]. This method throws an exception of type IndexOutOfBoundsException if offset or offset+length-1 are not valid index values for the buffers array.

As you can see, all three read() methods read data into one or more buffers of type ByteBuffer. The file position is incremented by the number of bytes read. Because you can use only ByteBuffer objects to receive the data read from the file, you can only read data as a series of bytes. How you interpret these bytes afterward, though, is up to you. View buffers provide you with a lot of possibilities.

These methods are distributed among the channel interfaces so the type you use to store the reference to the channel object can be important. The first read() method in the list that reads bytes into a single buffer is declared in the ReadableByteChannel interface, which the SeekableByteChannel interface extends along with the WritableByteChannel interface. The other two read() methods are declared in the ScatteringByteChannel interface that the FileChannel class implements. The newByteChannel() method returns a reference of type SeekableByteChannel, so if you want to use the two latter read() methods that work with arrays of buffers, you must cast the reference that these methods return to type FileChannel.

All three methods can throw exceptions of any of the following types:

  • NonReadableChannelException is thrown if the file is not opened for reading.
  • ClosedChannelException is thrown if the channel is closed.
  • AsynchronousCloseException is thrown if the channel is closed by another thread while the read operation is in progress.
  • ClosedByInterruptException is thrown if another thread interrupts the current thread while the read operation is in progress.
  • IOException is thrown if some other I/O error occurs.

The third read() method that enables you to read data into a subset of buffers from an array can also throw an IndexOutOfBoundsException if the index parameters are inconsistent or invalid.

The FileChannel object keeps track of the file’s current position. This is initially set to zero, which corresponds to the first byte available from the file. Each read operation increments the channel’s file position by the number of bytes read, so the next read operation starts at that point, assuming you don’t modify the file position by some other means. When you need to change the file position in the channel — to reread the file, for example — you just call the position() method for the FileChannel object, with the index position of the byte where you want the next read to start as the argument to the method. For example, with a reference to a FileChannel object stored in a variable inChannel, you could reset the file position to the beginning of the file with the following statements:

try (/* ... create channel inCh... */){
  // ...
  inCh.position(0);   // Set file position to first byte
 
} catch(IOException e) {
  e.printStackTrace();
}
 

This method throws a ClosedChannelException if the channel is closed or an IOException if some other error occurs, so you need to put the call in a try block. It can also throw an IllegalArgumentException if the argument you supply to the method is negative. IllegalArgumentException is a subclass of RuntimeException. You can legally specify a position beyond the end of the file, but a subsequent read operation just returns −1, indicating that the end-of-file has been reached.

Calling the position() method with no argument returns the current file position. You can use this to record a file position that you want to return to later in a variable. This version of the method can also throw exceptions of type ClosedChannelException and IOException, so you must put the call in a try block or make the calling method declare the exceptions in a throws clause.

The amount of data read from a file into a byte buffer is determined by the position and limit for the buffer when the read operation executes, as Figure 11-1 illustrates. Bytes are read into the buffer starting at the byte in the buffer given by its position; assuming sufficient bytes are available from the file, a total of limit-position bytes from the file are stored in the buffer.

You’ll see some other channel read() methods later that you can use to read data from a particular point in a file.

Reading a Text File

You can now attempt to read the charData.txt file that you wrote using the BufferStateTrace example in the previous chapter. You wrote this file as Unicode characters, so you must take this into account when interpreting the contents of the file. Of course, all files are read as a series of bytes. It’s how you interpret those bytes that determines whether or not you get something that makes sense.

Your first steps are to define a Path object encapsulating the file path and to create a FileChannel object using the process you saw at the beginning of this chapter.

You create a ByteBuffer object exactly as you saw previously when you were writing the file in Chapter 10. You know that you wrote 50 bytes at a time to the file — you wrote the string "Garbage in, garbage out. " that consists of 25 Unicode characters. However, it’s possible that you tried appending to the file an arbitrary number of times, so you should provide for reading as many Unicode characters as there are in the file. You can set up the ByteBuffer with exactly the right size for the data from a single write operation with the following statement:

ByteBuffer buf = ByteBuffer.allocate(50);
 

The code that you use to read from the file needs to allow for an arbitrary number of 25-character strings in the file. Of course, it must also allow for the end-of-file being reached while you are reading the file. You can read from the file into the buffer like this:

Path file = Paths.get(System.getProperty("user.home")).
                      resolve("Beginning Java Stuff").resolve("charData.txt");
ByteBuffer buf = ByteBuffer.allocate(50);
try (ReadableByteChannel inCh = file.newByteChannel()){
  while(inCh.read(buf) != -1) {
    // Code to extract the data that was read into the buffer...
    buf.clear();                       // Clear the buffer for the next read
  }
  System.out.println("EOF reached.");
} catch(IOException e) {
  e.printStackTrace();
}
 

The file is read by calling the read() method for the FileChannel object in the expression that you use for the while loop condition. The loop continues to read from the file until the read() method returns −1 when the end-of-file is reached. Within the loop you have to extract the data from the buffer, do what you want with it, and then clear the buffer to be ready for the next read operation.

Getting Data from the Buffer

After each read operation, the buffer’s position points to the byte following the last byte that was read. Before you attempt to extract any data from the buffer, you therefore need to flip the buffer to reset the buffer position back to the beginning of the data, and the buffer limit to the byte following the last byte of data that was read. One way to extract bytes from the buffer is to use the getChar() method for the ByteBuffer object. This retrieves a Unicode character from the buffer at the current position and increments the position by two. This could work like this:

buf.flip();
StringBuffer str = new StringBuffer(buf.remaining()/2);
while(buf.hasRemaining()) {
  str.append(buf.getChar());
}
System.out.println("String read: "+ str.toString());
 

This code would replace the comment in the previous fragment that appears at the beginning of the while loop. You first create a StringBuffer object in which you assemble the string. This is the most efficient way to do this — using a String object would result in the creation of a new String object each time you add a character to the string. Of course, because there’s no possibility of multiple threads accessing the string, you could use a StringBuilder object here instead of the StringBuffer object and gain a little more efficiency. The remaining() method for the buffer returns the number of bytes read after the buffer has been flipped, so you can just divide this by two to get the number of characters read. You extract characters one at a time from the buffer in the while loop and append them to the StringBuffer object. The getChar() method increments the buffer’s position by two each time, so eventually the hasRemaining() method returns false when all the characters have been extracted, and the loop ends. You then just convert the StringBuffer to a String object and output the string on the command line.

This approach works okay, but a better way is to use a view buffer of type CharBuffer. The toString() method for the CharBuffer object gives you the string that it contains directly. Indeed, you can boil the whole thing down to a single statement:

System.out.println("String read: " +
                   ((ByteBuffer)(buf.flip())).asCharBuffer().toString());
 

The flip() method returns a reference of type Buffer, so you have to cast it to type ByteBuffer to make it possible to call the asCharBuffer() method for the buffer object. This is necessary because the asCharBuffer() method is defined in the CharBuffer class, not in the Buffer class.

You can assemble these code fragments into a working example.

TRY IT OUT: Reading Text from a File

Here’s the code for the complete program to read the charData.txt file that you wrote in the previous chapter using the BufferStateTrace.java program:

image
import java.nio.file.*;
import java.nio.channels.ReadableByteChannel;
import java.io.IOException;
import java.nio.ByteBuffer;
 
public class ReadAString {
 
  public static void main(String[] args) {
 
    Path file = Paths.get(System.getProperty("user.home")).
                      resolve("Beginning Java Stuff").resolve("charData.txt");
    if(!Files.exists(file)) {
      System.out.println(file + " does not exist. Terminating program.");
      System.exit(1);;
    }
 
    ByteBuffer buf = ByteBuffer.allocate(50);
    try (ReadableByteChannel inCh = Files.newByteChannel(file)){
      while(inCh.read(buf) != -1) {
        System.out.print("String read: " +
                   ((ByteBuffer)(buf.flip())).asCharBuffer().toString());
        buf.clear();                      // Clear the buffer for the next read
      }
      System.out.println("EOF reached.");
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
}
 

ReadAString.java

When you compile and run this, you should get output something like the following:

String read: Garbage in, garbage out.
String read: Garbage in, garbage out.
String read: Garbage in, garbage out.
 
EOF reached.
 

The number of lines of output depends on how many times you ran the example that wrote the file.

How It Works

Nothing is new here beyond what I have already discussed. The file contains new line characters so the print() method generates the output that you see.

If you want to output the length of the file, you could add a statement to call the size() method for the inCh object:

System.out.println("File contains " + ((SeekableByteChannel)inCh).size() + " bytes.");
 

For this to compile you will need an import statement for java.nio.channels.SeekableByteChannel. The newByteChannel() method returns a reference of this type, but it was stored as ReadableByteChannel (a subinterface of SeekableByteChannel) and this type does not specify the size() method. Immediately before the while loop would be a good place to put it because the size() method can throw an IOException. You might also like to modify the code to output the buffer’s position and limit before and after the read. This shows quite clearly how these change when the file is read.

Of course, this program works because you have prior knowledge of how long the records are in the file. If you didn’t have this information, you would have to read a chunk of data from the file and then figure out where each record ended by looking for the character that appears at the end of each record. You get an idea of how you could deal with this kind of situation a little later in this chapter when I go into reading mixed data from a file.

Reading Binary Data

When you read binary data, you still read bytes from the file, so the process is essentially the same as you used in the previous example. You just use a different type of view buffer to access the data. To read the primes.bin file, you have some options for the size of the byte buffer. The number of bytes in the buffer should be a multiple of eight because a prime value is of type long, so as long as it’s a multiple of eight, you can make it whatever size you like. You could allocate a buffer to accommodate the number of primes that you want to output to the command line — six values, say. This would make accessing the data very easy because you need to set up a view buffer of type LongBuffer only each time you read from the file. One thing against this is that reading such a small amount of data from the file in each read operation might not be a very efficient way to read the file. However, in the interests of understanding the mechanics of this, let’s see how it would work anyway. The buffer would be created like this:

final int PRIMECOUNT = 6;                 // Number of primes to read at a time
final int LONG_BYTES = 8;                 // Number of bytes for type long
ByteBuffer buf = ByteBuffer.allocate(LONG_BYTES*PRIMECOUNT);
 

You can then read the primes in a while loop inside the try block that creates the channel using a view buffer of type LongBuffer. Calling the asLongBuffer() method for the ByteBuffer object, buf, creates the view buffer. The LongBuffer class offers you a choice of four get() methods for accessing values of type long in the buffer:

  • get() extracts a single value of type long from the buffer at the current position and returns it. The buffer position is then incremented by one.
  • get(int index) extracts a single value of type long from the buffer position specified by the argument and returns it. The current buffer position is not altered. Remember: The buffer position is in terms of values.
  • get(long[] values) extracts values.length values of type long from the buffer starting at the current position and stores them in the array values. The current position is incremented by the number of values retrieved from the buffer. The method returns a reference to the buffer as type LongBuffer. If insufficient values are available from the buffer to fill the array that you pass as the argument — in other words, limit-position is less than values.length — the method throws an exception of type BufferUnderflowException, no values are transferred to the array, and the buffer’s position is unchanged.
  • get(long[] values, int offset, int length) extracts length values of type long from the buffer starting at the current position and stores them in the values array, starting at values[offset]. The current position is incremented by the number of values retrieved from the buffer. The method returns a reference to the buffer as type LongBuffer. If there are insufficient values available from the buffer the method behaves in the same way as the previous version.

The BufferUnderflowException class is a subclass of RuntimeException, so you are not obliged to catch this exception.

You could access the primes in the buffer like this:

LongBuffer longBuf = ((ByteBuffer)(buf.flip())).asLongBuffer();
System.out.println();                        // Newline for the buffer contents
while(longBuf.hasRemaining()) {              // While there are values
  System.out.print("  " + longBuf.get());    // output them on the same line
}
 

If you want to collect the primes in an array, using the form of get() method that transfers values to an array is more efficient than writing a loop to transfer them one at a time, but you have to be careful. If the number of primes in the file is not a multiple of the array size, you must take steps to pick up the last few stragglers correctly. If you don’t, you see the BufferUnderflowException thrown by the get() method. Let’s see it working in an example.

TRY IT OUT: Reading a Binary File

You will read the primes six at a time into an array. Here’s the program:

image
import java.nio.file.*;
import java.nio.*;
import java.nio.channels.ReadableByteChannel;
import java.io.IOException;
 
public class ReadPrimes {
  public static void main(String[] args) {
    Path file = Paths.get(System.getProperty("user.home")).
                                 resolve("Beginning Java Stuff").resolve("primes.bin");
    if(!Files.exists(file)) {
      System.out.println(file + " does not exist. Terminating program.");
      System.exit(1);
    }
 
    final int PRIMECOUNT = 6;
    final int LONG_BYTES = 8;                 // Number of bytes for type long
    ByteBuffer buf = ByteBuffer.allocate(LONG_BYTES*PRIMECOUNT);
    long[] primes = new long[PRIMECOUNT];
    try (ReadableByteChannel inCh = Files.newByteChannel(file)){
      int primesRead = 0;
      while(inCh.read(buf) != -1) {
        LongBuffer longBuf = ((ByteBuffer)(buf.flip())).asLongBuffer();
        primesRead = longBuf.remaining();
        longBuf.get(primes, 0, primesRead);
 
        // List the primes read on the same line
        System.out.println();
        for(int i = 0 ; i < primesRead ; ++i) {
          System.out.printf("%10d", primes[i]);
        }
        buf.clear();                   // Clear the buffer for the next read
      }
      System.out.println("
EOF reached.");
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
}
 

ReadPrimes.java

You should get all the prime values, six to a line, followed by "EOF reached."

How It Works

The code to access the primes in the buffer avoids the possibility of buffer underflow altogether. You always transfer the number of values available in longBuf by using the remaining() method to determine the count so you can’t cause the BufferUnderflowException to be thrown.

A further possibility is that you could use a buffer large enough to hold all the primes in the file. You can work this out from the value returned by the size() method for the channel — which is the length of the file in bytes. You could do that like this:

    final int PRIMECOUNT = (int)inCh.size()/LONG_BYTES;
 

Of course, you also must alter the for loop that outputs the primes so it doesn’t attempt to put them all on the same line. There is a hazard with this though if you don’t know how large the file is. Unless your computer is unusually replete with memory, it could be inconvenient if the file contains the first billion primes.

Reading Mixed Data

The primes.txt file that you created in the previous chapter contains data of three different types. You have the string length as a binary value of type double, which is a strange choice for an integer but good experience, followed by the string itself describing the prime value, followed by the binary prime value as type long. Reading this file is a little trickier than it looks at first sight.

To start with you set up the Path object appropriately and obtain the channel for the file. Because, apart from the name of the file, this is exactly the same as in the previous example, I won’t repeat it here. Of course, the big problem is that you don’t know ahead of time exactly how long the strings are. You have two strategies to deal with this:

  • You can read the string length in the first read operation and then read the string and the binary prime value in the next. The only downside to this approach is that it’s not a particularly efficient way to read the file because you have many read operations that each read a very small amount of data.
  • You can set up a sizable byte buffer of an arbitrary capacity and just fill it with bytes from the file. You can then sort out what you have in the buffer. The problem with this approach is that the buffer’s contents may well end partway through one of the data items from the file. You have to do some work to detect this and figure out what to do next, but this is much more efficient than the first approach because you vastly reduce the number of read operations that are necessary to read the entire file.

Let’s try the first approach first because it’s easier.

To read the string length you need a byte buffer with a capacity to hold a single value of type double:

ByteBuffer lengthBuf = ByteBuffer.allocate(8);
 

You can create byte buffers to hold the string and the binary prime value, but you can only create the former after you know the length of the string. Remember, you wrote the string as Unicode characters so you must allow 2 bytes for each character in the original string. Here’s how you can define the buffers for the string and the prime value:

      ByteBuffer[] buffers = {
                           null,                  // Byte buffer to hold string
                           ByteBuffer.allocate(8) // Byte buffer to hold prime
                             };
 

The first element in the array is the buffer for the string. This is null because you redefine this as you read each record.

You need two read operations to get at all the data for a single prime record. A good approach would be to put both read operations in an indefinite loop and use a break statement to exit the loop when you hit the end-of-file. Here’s how you can read the file using this approach:

      while(true) {
        if(inCh.read(lengthBuf) == -1)        // Read the string length,
          break;                              // if its EOF exit the loop
 
        lengthBuf.flip();
 
        // Extract the length and convert to int
        strLength = (int)lengthBuf.getDouble();
 
        // Now create the buffer for the string
        buffers[0] = ByteBuffer.allocate(2*strLength);
 
        if(inCh.read(buffers) == -1) {  // Read the string & binary prime value
          // Should not get here!
          System.err.println("EOF found reading the prime string.");
          break;                            // Exit loop on EOF
        }
  // Output the prime data and clear the buffers for the next iteration...
}
 

After reading the string length into lengthBuf you can create the second buffer, buffers[0]. The getDouble() method for lengthBuf provides you with the length of the string. You get the string and the binary prime value the arguments to the output statement. Of course, if you manage to read a string length value, there ought to be a string and a binary prime following, so you have output an error message to signal something has gone seriously wrong if this turns out not to be the case.

Let’s see how it works out in practice.

TRY IT OUT: Reading Mixed Data from a File

Here’s the complete program code:

image
import java.nio.file.*;
import java.nio.channels.FileChannel;
import java.io.IOException;
import java.nio.ByteBuffer;
 
public class ReadPrimesMixedData {
  public static void main(String[] args) {
    Path file = Paths.get(System.getProperty("user.home")).
                         resolve("Beginning Java Stuff").resolve("primes.txt");
    if(!Files.exists(file)) {
      System.out.println(file + " does not exist. Terminating program.");
      System.exit(1);;
    }
 
    try (FileChannel inCh = (FileChannel)Files.newByteChannel(file)){
      ByteBuffer lengthBuf = ByteBuffer.allocate(8);
      int strLength = 0;                          // Stores the string length
 
      ByteBuffer[] buffers = {
                           null,                  // Byte buffer to hold string
                           ByteBuffer.allocate(8) // Byte buffer to hold prime
                             };
 
      while(true) {
        if(inCh.read(lengthBuf) == -1)            // Read the string length,
          break;                                  // if its EOF exit the loop
 
        lengthBuf.flip();
 
        // Extract the length and convert to int
        strLength = (int)lengthBuf.getDouble();
 
        // Now create the buffer for the string
        buffers[0] = ByteBuffer.allocate(2*strLength);
 
        if(inCh.read(buffers) == -1) {  // Read the string & binary prime value
          // Should not get here!
          System.err.println("EOF found reading the prime string.");
          break;                                  // Exit loop on EOF
        }
 
        System.out.printf(
                "String length: %3s  String: %-12s  Binary Value: %3d%n",
                strLength,
                ((ByteBuffer)(buffers[0].flip())).asCharBuffer().toString(),
                ((ByteBuffer)buffers[1].flip()).getLong());
 
        // Clear the buffers for the next read
        lengthBuf.clear();
        buffers[1].clear();
      }
      System.out.println("
EOF reached.");
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
}
 

ReadPrimesMixedData.java

The program reads the file that you wrote with PrimesToFile2.java in Chapter 10. You should get the following output:

String length:   9 String: prime = 2     Binary Value:   2
String length:   9 String: prime = 3     Binary Value:   3
String length:   9 String: prime = 5     Binary Value:   5
 

and so on down to the end:

String length:  11 String: prime = 523   Binary Value: 523
String length:  11 String: prime = 541   Binary Value: 541
 
EOF reached.
 

How It Works

Because you use the channel read() method that reads into an array of ByteBuffer objects, you cast the reference returned by the newByteChannel() method to type FileChannel. On each iteration of the loop that reads the file, you first read 8 bytes into lengthBuf because this is the length of the string that follows as a value of type double. Knowing the length of the string, you are able to create the second buffer, buffers[0], to accommodate the string.

The string and the binary prime value are obtained in the arguments to the printf() method:

        System.out.printf(
                "String length: %3s  String: %-12s  Binary Value: %3d%n",
                strLength,
                ((ByteBuffer)(buffers[0].flip())).asCharBuffer().toString(),
                ((ByteBuffer)buffers[1].flip()).getLong());

You obtain the string by calling the asCharBuffer() method to obtain a CharBuffer object from the original ByteBuffer and calling its toString() method to get the contents as a String object. It is necessary to flip the byte buffer before you do this and you have to cast the reference returned by the flip() method to type ByteBuffer in order to make this possible because flip() returns a reference of type Buffer. You use a similar mechanism to get the binary prime value.

Finally you clear the buffers for the string length and prime value ready for the next loop iteration.

Compacting a Buffer

Another approach to reading the primes.txt file is to read bytes from the file into a large buffer for efficiency and then figure out what is in it. Processing the data needs to take account of the possibility that the last data item in the buffer following a read operation may be incomplete — part of a double or long value or part of a string. The essence of this approach is therefore as follows:

1. Read from the file into a buffer.

2. Extract the string length, the string, and the binary prime value from the buffer repeatedly until no more complete sets of values are available.

3. Shift any bytes that are left over in the buffer back to the beginning of the buffer. These are some part of a complete set of the string length, the string, and the binary prime value. Go back to point 1 to read more from the file.

The buffer classes provide the compact() method for performing the operation you need in the third step here to shift the bytes that are left over in the buffer back to the beginning. An illustration of the action of the compact() method on a buffer is shown in Figure 11-2.

As you can see, the compacting operation copies everything in the buffer, which are the data elements from the buffer’s current position, up to but not including the buffer’s limit, to the beginning of the buffer. The buffer’s position is then set to the element following the last element that was copied, and the limit is set to the capacity. This is precisely what you want when you have worked partway through the data in an input buffer and you want to add some more data from the file. Compacting the buffer sets the position and limit such that the buffer is ready to receive more data. The next read operation using the buffer adds data at the end of what was left in the buffer.

TRY IT OUT: Reading into a Large Buffer

Here is a new version of the previous example that reads data into a large buffer:

image
import java.nio.file.*;
import java.nio.channels.ReadableByteChannel;
import java.io.IOException;
import java.nio.ByteBuffer;
 
public class ReadPrimesMixedData2 {
  public static void main(String[] args) {
    Path file = Paths.get(System.getProperty("user.home")).
                         resolve("Beginning Java Stuff").resolve("primes.txt");
    if(!Files.exists(file)) {
      System.out.println(file + " does not exist. Terminating program.");
      System.exit(1);
    }
 
    try(ReadableByteChannel inCh = Files.newByteChannel(file)) {
      ByteBuffer buf = ByteBuffer.allocateDirect(256);
      buf.position(buf.limit());     // Set the position for the loop operation
      int strLength = 0;             // Stores the string length
      byte[] strChars = null;        // Array to hold the bytes for the string
 
      while(true) {
        if(buf.remaining() < 8) {    // Verify enough bytes for string length
          if(inCh.read(buf.compact()) == -1)
            break;                          // EOF reached
          buf.flip();
         }
         strLength = (int)buf.getDouble();  // Get the string length
 
         // Verify enough bytes for complete string
         if(buf.remaining() < 2*strLength) {
           if(inCh.read(buf.compact()) == -1) {
            System.err.println("EOF found reading the prime string.");
            break;                          // EOF reached
           }
           buf.flip();
        }
        strChars = new byte[2*strLength];   // Array for string bytes
        buf.get(strChars);                  // Get the bytes
 
        if(buf.remaining()<8) {          // Verify enough bytes for prime value
          if(inCh.read(buf.compact()) == -1) {
            System.err.println("EOF found reading the binary prime value.");
            break;                          // EOF reached
          }
          buf.flip();
        }
 
        System.out.printf(
                     "String length: %3s  String: %-12s  Binary Value: %3d%n",
                     strLength,
                     ByteBuffer.wrap(strChars).asCharBuffer(),buf.getLong());
      }
 
      System.out.println("
EOF reached.");
 
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
}
 

ReadPrimesMixedData2.java

This should result in the same output as the previous example.

How It Works

I chose a buffer size that would guarantee that we do multiple read operations to read the complete file contents and that we would get bits of a complete record left at the end of the buffer.

All the work is done in the indefinite while loop. Before the loop executes you create a direct buffer with a capacity of 256 bytes by calling the allocateDirect() method. A direct buffer is faster if you are reading a lot of data from a file, as the data is transferred directly from the file to our buffer. The code within the loop determines whether there are data values left in the buffer by calling the remaining() method for the buffer object. You only do another file read when you have processed all the complete prime records that the buffer contains. This is when remaining() returns a value that is less than the number of bytes needed to accommodate the next data item. The default initial settings for the buffer are with the position at zero and the limit at the capacity, and this would suggest falsely that there is data in the buffer. Therefore you set the position to the limit initially so that the remaining() method returns zero at the beginning of the first loop iteration.

Within the loop you first check whether there are sufficient bytes for the double value that specifies the string length. On the first iteration, this is definitely not the case, so the compact() method is called to compact the buffer, and the reference to buf that is returned is passed to the read() method for inCh to read data from the file. You then flip the buffer and get the length of the string. Of course, data in the file should be in groups of three items — the string length, the string, the binary prime value — so the end-of-file should be detected trying to obtain the first of these by the read() method for the channel returning −1. In this case you exit the loop by executing a break statement.

Next you get the string itself, after checking that you have sufficient bytes left in the buffer. You should never find EOF, so you output an error message if EOF is detected. Finally, you obtain the binary prime value in a similar way and output the group of three data items. The loop continues until all data has been read and processed and EOF is recognized when you are looking for a string length value.

COPYING FILES

You have already seen in Chapter 9 how you can use the copy() method in the Files class to copy a file to another location. There’s another way using FileChannel objects. A FileChannel object connected to an input file can transfer data directly to a FileChannel object that is connected to an output file without involving explicit buffers.

The FileChannel class defines two methods for direct data transfer between channels that are particularly efficient.

  • transferTo(long position, long count, WritableByteChannel dst)Attempts to transfer count bytes from this channel to the channel dst. Bytes are read from this channel starting at the file position specified by position. The position of this channel is not altered by this operation, but the position of dst is incremented by the number of bytes written. Fewer than count bytes are transferred if this channel’s file has fewer than count bytes remaining, or if dst is non-blocking and has fewer than count bytes free in its system output buffer. The number of bytes transferred is returned as a value of type int.
  • transferFrom(ReadableByteChannel src, long position, long count)) Attempts to transfer count bytes to this channel from the channel src. Bytes are written to this channel starting at the file position specified by position. The position of this channel is not altered by the operation, but the position of src is incremented by the number of bytes read from it. If position is greater than the size of the file, then no bytes are transferred. Fewer than count bytes are transferred if the file corresponding to src has fewer than count bytes remaining in the file or if it is non-blocking and has fewer than count bytes free in its system input buffer. The number of bytes transferred is returned as a value of type int.

A channel that was opened for reading supports only the transferTo() method. Similarly, a channel that was opened for writing supports only the transferFrom() method. Both of these methods can throw any of the following flurry of exceptions:

  • IllegalArgumentException is thrown if either count or position is negative.
  • NonReadableChannelException is thrown if the operation attempts to read from a channel that was not opened for reading.
  • NonWritableChannelException is thrown if the operation attempts to write to a channel that was not opened for writing.
  • ClosedChannelException is thrown if either channel involved in the operation is closed.
  • AsynchronousCloseException is thrown if either channel is closed by another thread while the operation is in progress.
  • ClosedByInterruptException is thrown if another thread interrupts the current thread while the operation is in progress.
  • IOException is thrown if some other I/O error occurs.

The value of these methods lies in the potential for using the I/O capabilities of the underlying operating system directly. Where this is possible, the operation is likely to be much faster than copying from one file to another in a loop using the read() and write() methods you have seen.

A file copy program is an obvious candidate for trying out these methods.

TRY IT OUT: Direct Data Transfer between Channels

This example is a program that copies the file that is specified by a command-line argument. You copy the file to a backup file that you create in the same directory as the original. You create the name of the new file by appending "_backup" to the original filename as many times as necessary to form a unique filename. That operation is a good candidate for writing a helper method:

image
// Method to create a unique backup Path object under MS Windows
public static Path createBackupFilePath(Path file) {
   Path parent = file.getParent();
   String name = file.getFileName().toString();        // Get the file name
   int period = name.indexOf('.'),          // Find the extension separator
   if(period == -1) {                       // If there isn't one
     period = name.length();                // set it to the end of the string
   }
   String nameAdd = "_backup";              // String to be appended
 
   // Create a Path object that is a unique
    Path backup = parent.resolve(
              name.substring(0,period) + nameAdd + name.substring(period));
   while(Files.exists(backup)) {               // If the path already exists...
      name = backup.getFileName().toString();  // Get the current file name
      backup = parent.resolve(name.substring(0,period) +     // add _backup
                              nameAdd + name.substring(period));
     period += nameAdd.length();               // Increment separator index
   }
   return backup;
}
 

FileBackup.java

This method assumes the argument has already been validated as a real file. You extract the basic information you need to create the new file first — the parent directory path, the file name, and where the period separator is, if there is one. You then create a Path variable, backup, that you initialize using the original file path. The while loop continues as long as backup already exists as a file, and an instance of "_backup" is appended to the path until a unique filename is arrived at.

You can now write the main() method to use the createBackupFile() method to create the destination file for the file backup operation:

image
import static java.nio.file.StandardOpenOption.*;
import java.nio.file.*;
import java.nio.channels.*;
import java.io.IOException;
import java.util.EnumSet;
 
public class FileBackup {
  public static void main(String[] args) {
    if(args.length==0) {
      System.out.println("No file to copy. Application usage is:
" +
                                 "java -classpath . FileCopy "filepath"" );
      System.exit(1);
    }
    Path fromFile = Paths.get(args[0]);
    fromFile.toAbsolutePath();
 
    if(Files.notExists(fromFile)) {
      System.out.printf("File to copy, %s, does not exist.", fromFile);
      System.exit(1);
    }
 
    Path toFile = createBackupFilePath(fromFile);
    try (FileChannel inCh = (FileChannel)(Files.newByteChannel(fromFile));
         WritableByteChannel outCh = Files.newByteChannel(
                                       toFile, EnumSet.of(WRITE,CREATE_NEW))){
      int bytesWritten = 0;
      long byteCount = inCh.size();
      while(bytesWritten<byteCount) {
        bytesWritten += inCh.transferTo(bytesWritten,
                                        byteCount-bytesWritten,
                                        outCh);
      }
 
      System.out.printf(
             "File copy complete. %d bytes copied to %s%n", byteCount, toFile);
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
 
  // Code for createBackupFilePath() goes here...
}
 

FileBackup.java

You could try this out by copying the file containing the binary prime values. I supplied the path to my primes.bin file as the command-line argument and got the following output:

File copy complete. 800 bytes copied to C:UsersIvorBeginning Java Stuffprimes_backup.bin
 

You should be able to verify that the new file’s contents are identical to the original by inspecting it, or you could run the earlier example that reads a binary file with the file path to primes_backup.bin.

How It Works

You first obtain the command-line argument and create a Path object from it with the following code:

    if(args.length==0) {
      System.out.println("No file to copy. Application usage is:
"+
                         "java -classpath . FileCopy "filepath"" );
      System.exit(1);
    }
    Path fromFile = Paths.get(args[0]);
 

If there’s no command-line argument, you supply a message explaining how to use the program before exiting.

Next, you verify that this is a real file:

    if(Files.notExists(fromFile)) {
      System.out.printf("File to copy, %s, does not exist.", fromFile);
      System.exit(1);
    }
 

If it isn’t, there’s nothing you can do, so you bail out of the program.

Creating a Path object for the backup file is a piece of cake:

    Path toFile = createBackupFilePath(fromFile);
 

You saw how this helper method works earlier in this chapter.

Next, in the try block you create the channel for each file from the Path objects:

    try (FileChannel inCh = (FileChannel)(Files.newByteChannel(fromFile));
         WritableByteChannel outCh = Files.newByteChannel(
                                      toFile, EnumSet.of(WRITE,CREATE_NEW))){
 

There are two FileChannel objects that you want to be automatically closed, so you create both between the parentheses following the try keyword. The inCh file channel has the READ option specified by default. You specify the WRITE and CREATE_NEW options for outCh, which guarantees the backup file is a new file. If the backup file already exists, the getByteChannel() method would fail. However, this should not happen because you have already verified that the backup file path does not reference a file that already exists.

After you have the channel objects, you transfer the contents of the input file to the output file in the try block like this:

      int bytesWritten = 0;
      long byteCount = inCh.size();
      while(bytesWritten<byteCount) {
        bytesWritten += inCh.transferTo(bytesWritten,
                                        byteCount-bytesWritten,
                                        outCh);
      }
 

You copy the data using the transferTo() method for inCh. The chances are good that the transferTo() method transfers all the data in one go. The while loop is there just in case it doesn’t. The loop condition checks whether the number of bytes written is less than the number of bytes in the file. If it is, the loop executes another transfer operation for the number of bytes left in the file, with the file position specified as the number of bytes written so far.

RANDOM ACCESS TO A FILE

The FileChannel class defines read() and write() methods that operate at a specified position in the file:

  • read(ByteBuffer buf, long position) reads bytes from the file into buf in the same way as you have seen previously except that bytes are read starting at the file position specified by the second argument. The channel’s position is not altered by this operation. If position is greater than the number of bytes in the file, then no bytes are read.
  • write(ByteBuffer buf, long position) writes bytes from buf to the file in the same way as you have seen previously except that bytes are written starting at the file position specified by the second argument. The channel’s position is not altered by this operation. If position is less than the number of bytes in the file then bytes from that point are overwritten. If position is greater than the number of bytes in the file then the file size is increased to this point before bytes are written. In this case the bytes between the original end-of-file and where the new bytes are written contain junk values.

These methods can throw the same exceptions as the corresponding method that accepts a single argument; plus, they might throw an exception of type IllegalArgumentException if a negative file position is specified. Obviously, when you want to use a channel to read and write a file, you must specify both the READ and WRITE open options when you create the FileChannel object.

The SeekableByteChannel interface that the FileChannel class implements also declares an overload of the position() method that accepts an argument of type long. Calling the method sets the file position for the channel to the position specified by the argument. This version of the position() method returns a reference to the channel object as type FileChannel. The interface also declares the size() method that returns the number of bytes in the file to which the channel is connected. You could use this to decide on the buffer size, possibly reading the whole file in one go if it’s not too large.

Let’s try an example that demonstrates how you can access a file randomly.

TRY IT OUT: Reading a File Randomly

To show how easy it is to read from random positions in a file, the example extracts a random selection of values from our primes.bin file. Here’s the code:

image
import java.nio.file.*;
import java.nio.channels.FileChannel;
import java.io.IOException;
import java.nio.ByteBuffer;
 
public class RandomFileRead {
  public static void main(String[] args) {
    Path file = Paths.get(System.getProperty("user.home")).
                         resolve("Beginning Java Stuff").resolve("primes.bin");
    if(!Files.exists(file)) {
      System.out.println(file + " does not exist. Terminating program.");
      System.exit(1);
    }
 
    final int PRIMESREQUIRED = 10;
      final int LONG_BYTES = 8;             // Number of bytes for type long
    ByteBuffer buf = ByteBuffer.allocate(LONG_BYTES*PRIMESREQUIRED);
 
    long[] primes = new long[PRIMESREQUIRED];
    int index = 0;                          // Position for a prime in the file
 
    try (FileChannel inCh = (FileChannel)(Files.newByteChannel(file))){
      // Count of primes in the file
      final int PRIMECOUNT = (int)inCh.size()/LONG_BYTES;
 
      // Read the number of random primes required
      for(int i = 0 ; i < PRIMESREQUIRED ; ++i) {
        index = LONG_BYTES*(int)(PRIMECOUNT*Math.random());
        inCh.read(buf, index);              // Read the value
        buf.flip();
        primes[i] = buf.getLong();          // Save it in the array
        buf.clear();
      }
 
      // Output the selection of random primes 5 to a line in field width of 12
      int count = 0;                        // Count of primes written
      for(long prime : primes) {
        System.out.printf("%12d", prime);
        if(++count%5 == 0) {
          System.out.println();
        }
      }
 
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
}
 

RandomFileRead.java

When I ran this, I got the following output:

         359         107         383         109             7
         173         443         337          17           113
 

You should get something similar but not the same because the random number generator is seeded using the current clock time. The number of random selections is fixed, but you could easily add code for a value to be entered on the command line.

How It Works

You access a random prime in the file by generating a random position in the file with the expression 8*(int)(PRIMECOUNT*Math.random()). The value of index is a pseudo-random integer that can be from 0 to the number of primes in the file minus one, multiplied by 8 because each prime occupies 8 bytes. The prime is read from the file with the statement:

        inCh.read(buf, index);          // Read the value at index
 

This calls the read() method for the channel object that accepts a file position argument to specify from where in the file bytes are to be read. Because buf has a capacity of 8 bytes, only one prime is read each time. You store each randomly selected prime in an element of the primes array.

Finally, you output the primes five to a line in a field width of 12 characters.

You could also have used the read() method that accepts a single argument together with the position() method:

      for(int i = 0 ; i<PRIMESREQUIRED ; ++i) {
        index = LONG_BYTES*(int)(PRIMECOUNT*Math.random());
        inCh.position(index).read(buf);          // Read the value
        buf.flip();
        primes[i] = buf.getLong();               // Save it in the array
        buf.clear();
      }
 

Calling the position() method sets the file position to index then the read() method reads the file starting at that point.

The need to be able to access and update a file randomly arises quite often. Even with a simple personnel file, for example, you are likely to need the capability to update the address or the phone number for an individual. Assuming you have arranged for the address and phone number entries to be of a fixed length, you could update the data for any entry simply by overwriting it. If you want to read from and write to the same file you can just open the channel for reading and writing. Let’s try that, too.

TRY IT OUT: Reading and Writing a File Randomly

You can modify the previous example so that you overwrite each random prime that you retrieve from the primes_backup.bin file that you created earlier with the value 99999L to make it stand out from the rest. This messes up the primes_backup.bin file that you use here, but you can always run the program that copies files to copy primes.bin if you want to restore it. Here’s the code:

image
import static java.nio.file.StandardOpenOption.*;
import java.nio.file.*;
import java.nio.channels.SeekableByteChannel;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.EnumSet;
 
public class RandomReadWrite {
  public static void main(String[] args)
  {
    Path file = Paths.get(System.getProperty("user.home")).
                  resolve("Beginning Java Stuff").resolve("primes_backup.bin");
    if(!Files.exists(file)) {
      System.out.println(file + " does not exist. Terminating program.");
      System.exit(1);
    }
 
    final int PRIMESREQUIRED = 10;
    final int LONG_BYTES = 8;
    ByteBuffer buf = ByteBuffer.allocate(LONG_BYTES);
 
    long[] primes = new long[PRIMESREQUIRED];
    int index = 0;                             // Position for a prime in the file
    final long REPLACEMENT = 99999L;            // Replacement for a selected prime
 
    try (SeekableByteChannel channel = 
                          Files.newByteChannel(file, EnumSet.of(READ, WRITE))){
      final int PRIMECOUNT = (int)channel.size()/8;
      System.out.println("Prime count = "+PRIMECOUNT);
      for(int i = 0 ; i < PRIMESREQUIRED ; ++i) {
        index = LONG_BYTES*(int)(PRIMECOUNT*Math.random());
        channel.position(index).read(buf);     // Read at a random position
        buf.flip();                             // Flip the buffer
        primes[i] = buf.getLong();             // Extract the prime
        buf.flip();                             // Flip to ready for insertion
        buf.putLong(REPLACEMENT);              // Replacement into buffer
        buf.flip();                             // Flip ready to write
        channel.position(index).write(buf);    // Write the replacement to file
        buf.clear();                           // Reset ready for next read
      }
 
      int count = 0;                           // Count of primes written
      for(long prime : primes) {
        System.out.printf("%12d", prime);
        if(++count%5 == 0) {
          System.out.println();
        }
      }
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
}
 

RandomReadWrite.java

This outputs from the file a set of ten random prime selections that have been overwritten. If you want to verify that you have indeed overwritten these values in the file, you can run the ReadPrimes example that you wrote earlier in this chapter with the file name as "primes_backup.bin".

How It Works

All you had to do to write the file as well as read it was to create a Path object for the file and create a file channel that is opened for both reading and writing. You can read and write sequentially or at random. You read and write the file using essentially the same mechanism. You call the position() method for the channel object to set the file position and then you call the read() or write() method with buf as the argument.

You could have used the channel read() and write() methods that explicitly specify the position where the data is to be read or written as an argument. In this case you would need to cast channel to type FileChannel. One problem with the example as it stands is that some of the selections could be 99999L, which is patently not prime. I got some of these after running the example just three times. You could fix this by checking each value that you store in the primes array:

image
for(int i = 0 ; i < PRIMESREQUIRED ; ++i)
{
  while(true)
  {
    index = LONG_BYTES*(int)(PRIMECOUNT*Math.random());
      channel.position(index).read(buf);   // Read at a random position
    buf.flip();                             // Flip the buffer
    primes[i] = buf.getLong();             // Extract the prime
    if(primes[i] != REPLACEMENT) {
      break;                               // It's good so exit the inner loop
    } else {
      buf.clear();                         // Clear ready to read another
    }
  }
  buf.flip();                               // Flip to ready for insertion
  buf.putLong(REPLACEMENT);                // Replacement into buffer
  buf.flip();                               // Flip ready to write
    channel.position(index).write(buf);    // Write the replacement to file
  buf.clear();                             // Reset ready for next read
}
 

RandomReadWrite.java

This code fragment is commented out in the download. The while loop now continues if the value read from the file is the same as REPLACEMENT, so another random file position is selected. This continues until something other than the value of REPLACEMENT is found. Of course, if you run the example often enough, you won’t have enough primes in the file to fill the array, so the program loops indefinitely looking for something other than REPLACEMENT. You could deal with this in several ways. For example, you could count how many iterations have occurred in the while loop and bail out if it reaches the number of primes in the file. You could also inspect the file first to see whether there are sufficient primes in the file to fill the array. If there are exactly 10, you can fill the array immediately. I leave it to you to fill in these details.

MEMORY-MAPPED FILES

A memory-mapped file is a file that has its contents mapped into an area of virtual memory in your computer. This enables you to reference or update the data in the file directly without performing any explicit file read or write operations on the physical file yourself. When you reference a part of the file that is not actually in real memory, it is automatically brought in by your operating system. The memory that a file maps to might be paged in or out by the operating system, just like any other memory in your computer, so its immediate availability in real memory is not guaranteed. Because of the potentially immediate availability of the data it contains, a memory-mapped file is particularly useful when you need to access the file randomly. Your program code can reference the data in the file just as though it were all resident in memory.

Mapping a file into memory is implemented by a FileChannel object. The map() method for a FileChannel object returns a reference to a buffer of type MappedByteBuffer that maps to a specified part of the channel’s file:

MappedByteBuffer map(int mode, long position, long size)
 

This method maps a region of the channel’s file to a buffer of type MappedByteBuffer and returns a reference to the buffer. The file region that is mapped starts at position in the file and is of length size bytes. The first argument, mode, specifies how the buffer’s memory may be accessed and can be any of the following three constant values, defined in the MapMode class, which is a static nested class of the FileChannel class:

  • MapMode.READ_ONLY: This is valid if the channel was opened for reading the file. In this mode the buffer is read-only. If you try to modify the buffer’s contents, an exception of type ReadOnlyBufferException is thrown.
  • MapMode.READ_WRITE: This is valid if the channel was for both reading and writing. You can access and change the contents of the buffer and any changes to the contents are eventually be propagated to the file. The changes might or might not be visible to other users who have mapped the same file.
  • MapMode.PRIVATE: This mode is for a “copy-on-write” mapping. This option for mode is also valid only if the channel was open for both reading and writing. You can access or change the buffer, but changes are not propagated to the file and are not visible to users who have mapped the same file. Private copies of modified portions of the buffer are created and used for subsequent buffer accesses.

When you access or change data in the MappedByteBuffer object that is returned when you call the map() method, you are effectively accessing the file that is mapped to the buffer. After you have called the map() method, the file mapping and the buffer that you have established are independent of the FileChannel object. You can close the channel, and the mapping of the file into the MappedByteBuffer object is still valid and operational.

Because the MappedByteBuffer class is a subclass of the ByteBuffer class, you have all the ByteBuffer methods available for a MappedByteBuffer object. This implies that you can create view buffers for a MappedByteBuffer object, for instance.

The MappedByteBuffer class defines three methods of its own to add to those inherited from the ByteBuffer class:

TABLE 11-1: MappedByteBuffer Class Methods

METHOD DESCRIPTION
force() If the buffer was mapped in MapMode.READ_WRITE mode, this method forces any changes that you make to the buffer’s contents to be written to the file and returns a reference to the buffer. For buffers created with other access modes, this method has no effect.
load() Tries on a “best efforts” basis to load the contents of the buffer into memory and returns a reference to the buffer.
isLoaded() Returns true if it is likely that this buffer’s contents are available in physical memory and false otherwise.

The load() method is dependent on external operating system functions executing to achieve the desired result, so the result cannot in general be guaranteed. Similarly, when you get a true return from the isLoaded() method, this is an indication of a probable state of affairs rather than a guarantee. This doesn’t imply any kind of problem. It just means that accessing the data in the mapped byte buffer may take longer that you might expect in some instances.

Unless the file is large, using a mapped byte buffer is typically slower than using the read() and write() methods for a channel. Using a memory-mapped file through a MappedByteBuffer is simplicity itself though, so let’s try it.

TRY IT OUT: Using a Memory-Mapped File

You will access and modify the primes_backup.bin file using a MappedByteBuffer, so you might want to rerun the file copy program to restore it to its original condition. Here’s the code:

image
import static java.nio.file.StandardOpenOption.*;
import static java.nio.channels.FileChannel.MapMode.READ_WRITE;
import java.nio.file.*;
import java.nio.channels.FileChannel;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.util.EnumSet;
 
public class MemoryMappedFile {
  public static void main(String[] args) {
    Path file = Paths.get(System.getProperty("user.home")).
                  resolve("Beginning Java Stuff").resolve("primes_backup.bin");
    if(!Files.exists(file)) {
      System.out.println(file + " does not exist. Terminating program.");
      System.exit(1);
    }
 
    final int PRIMESREQUIRED = 10;
    final int LONG_BYTES = 8;
    long[] primes = new long[PRIMESREQUIRED];
 
    int index = 0;                          // Position for a prime in the file
    final long REPLACEMENT = 999999L;        // Replacement for a selected prime
 
    try {
      FileChannel channel =
            (FileChannel)(Files.newByteChannel(file, EnumSet.of(READ, WRITE)));
      final int PRIMECOUNT = (int)channel.size()/LONG_BYTES;
      MappedByteBuffer buf = channel.map(
                                       READ_WRITE, 0L, channel.size()).load();
      channel.close();                      // Close the channel
 
      for(int i = 0 ; i < PRIMESREQUIRED ; ++i) {
        index = LONG_BYTES*(int)(PRIMECOUNT*Math.random());
        primes[i] = buf.getLong(index);
        buf.putLong(index, REPLACEMENT);
      }
      int count = 0;                        // Count of primes written
      for(long prime : primes) {
        System.out.printf("%12d", prime);
        if(++count%5 == 0) {
          System.out.println();
        }
      }
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
}
 

MemoryMappedFile.java

This should output ten randomly selected primes, but some or all of the selections may turn out to be 99999L, the value of REPLACEMENT, if you have not refreshed the contents of primes_backup.bin.

How It Works

The statements of interest are those that are different to the previous example.

You have an import statement for the MappedByteBuffer class name, and you import the static member of the MapMode nested class to the FileChannel class that you use in the code.

You get the file channel with the following statement:

      FileChannel channel =
            (FileChannel)(Files.newByteChannel(file, EnumSet.of(READ, WRITE)));
 

This stores the reference that the newByteChannel() method as type FileChannel because you want to call the map() method that the FileChannel class defines. Opening the channel for both reading and writing is essential here because you want to access and change the contents of the MappedByteBuffer object. Notice that the program uses a regular try block instead of a try block with resources. This is because you want to close the channel as soon as the memory mapping is established so as to demonstrate that it works independently from the channel.

You create and load a MappedByteBuffer object with the statement:

      MappedByteBuffer buf =
                        channel.map(READ_WRITE, 0L, channel.size()).load();
 

The buffer is created with the READ_WRITE mode specified, which permits the buffer to be accessed and modified. The buffer maps to the entire file because you specify the start file position as zero, and the length that is mapped is the length of the file. The map() method returns a reference to the MappedByteBuffer object that is created, and you use this to call its load() method to request that the contents of the file be loaded into memory immediately. The load() method also returns the same buffer reference, and you store that in buf.

Note that you are not obliged to call the load() method before you access the data in the buffer. If the data is not available when you try to access it through the MappedByteBuffer object, it is loaded for you. Try running the example with the call to load() removed. It should work the same as before.

The next statement closes the file channel because it is no longer required:

      channel.close();                      // Close the channel
 

It is not essential to close the channel, but doing so demonstrates that memory-mapped file operations are independent of the channel after the mapping has been established.

Inside the for loop, you retrieve a value from the buffer at a random position, index:

primes[i] = buf.getLong(index);
 

You have no need to execute any explicit read() operations. The file contents are available directly through the buffer and any read operations that need to be executed to make the data you are accessing available are initiated automatically.

Next, you change the value at the position from which you retrieved the value that you store in primes[i]:

      buf.putLong(index, REPLACEMENT);
 

This statement changes the contents of the buffer, and this change is subsequently written to the file at some point. When this occurs depends on the underlying operating system.’

Finally, you output the contents of the primes array. You have been able to access and modify the contents of the file without having to execute any explicit I/O operations on the file. For large files where you are transferring large amounts of data, this is potentially much faster than using explicit read and write operations. How much faster depends on how efficiently your operating system handles memory-mapped files and whether the way in which you access the data results in a large number of page faults.

Memory-mapped files have one risky aspect that you need to consider, and you will look at that in the next section.

Locking a File

You need to take care that an external program does not modify a memory-mapped file that you are working with, especially if the file could be truncated externally while you are accessing it. If you try to access a part of the file through a MappedByteBuffer that has become inaccessible because a segment has been chopped off the end of the file by another program, then the results are somewhat unpredictable. You may get a junk value back that your program may not recognize as such, or an exception of some kind may be thrown. You can acquire a lock on the file to try to prevent this sort of problem. A shared lock allows several processes to have concurrent read access to the file. An exclusive lock gives you exclusive access to the file; no one else can access the file while you have the exclusive lock. A file lock simply ensures your right of access to the file and might also inhibit the ability of others to change or possibly access the file as long as your lock is in effect. This facility is available only if the underlying operating system supports file locking.

image

WARNING Some operating systems may prevent the use of memory mapping for a file if a mandatory lock on the file is acquired and vice versa. In such situations using a file lock with a memory-mapped file does not work.

A lock on a file is encapsulated by an object of the java.nio.channels.FileLock class. The lock() method for a FileChannel object tries to obtain an exclusive lock on the channel’s file. Acquiring an exclusive lock on a file ensures that another program cannot access the file at all, and is typically used when you want to write to a file, or when any modification of the file by another process causes you problems. Here’s one way to obtain an exclusive lock on a file specified by a Path object, file:

try (FileChannel fileChannel =
            (FileChannel)(Files.newByteChannel(file, EnumSet.of(READ, WRITE)));
     FileLock fileLock = channel.lock()){
 
  // Work with the locked file... 
 
} catch (IOException e) {
  e.printStackTrace();
}
 

The lock() method attempts to acquire an exclusive lock on the channel’s entire file so that no other program or thread can access the file while this channel holds the lock. A prerequisite for obtaining an exclusive lock is that the file has been opened for both reading and writing. If another program or thread already has a lock on the file, the lock() method blocks (i.e., does not return) until the lock on the file is released and can be acquired by this channel. The lock that is acquired is owned by the channel, channel, and is automatically released when the channel is closed. The FileLock class implements the AutoClosable interface so its close() method is called at the end of the try block to release the lock. You can also release the lock on a file by explicitly calling the release() method for the FileLock object.

You can call the isValid() method for a FileLock object to determine whether it is valid. A return value of true indicates a valid lock; otherwise, false is returned indicating that the lock is not valid. Note that once created, a FileLock object is immutable. It also has no further effect on file access after it has been invalidated. If you want to lock the file a second time, you must acquire a new lock.

The previous fragment hangs until a lock on the file is acquired. Having your program hang until a lock is acquired is not an ideal situation because it is quite possible a file could be locked permanently — at least until the computer is rebooted. This could be because a programming error in another program has locked the file, in which case your program hangs indefinitely. The tryLock() method for a channel offers an alternative way of requesting a lock that does not block. It either returns a reference to a valid FileLock object or returns null if the lock could not be acquired. This gives your program a chance to do something else or retire gracefully:

try (FileChannel fileChannel =
           (FileChannel)(Files.newByteChannel(file, EnumSet.of(READ, WRITE))){
FileLock  fileLock = channel.tryLock();
  if(fileLock == null) {
    System.out.println("The file's locked - again!! Oh, I give up...");
    System.exit(1);
  }
 
  // Work with the locked file...
} catch (IOException e) {
  e.printStackTrace();
}
 

You will later see a better response to a lock than this in an example, but you should get the idea.

Locking Part of a File

Overloaded versions of the lock() and tryLock() methods enable you to specify just the part of the file you want to obtain a lock on so you don’t lock the whole file and enable you to request a shared lock:

  • lock(long position, long size, boolean shared) Requests a lock on the region of this channel’s file starting at position and of length size. If the last argument is true, the lock requested is a shared lock. If it is false, the lock requested is an exclusive lock. If the lock cannot be obtained for any reason, the method blocks until the lock can be obtained or the channel is closed by another thread.
  • tryLock(long position, long size, boolean shared) Works in the same way as the previous method, except that null is returned if the requested lock cannot be acquired. This avoids the potential for hanging your program indefinitely.

The effect of a shared lock is to prevent an exclusive lock being acquired by another program that overlaps the region that is locked. However, a shared lock does permit another program to acquire a shared lock on a region of the file that may overlap the region to which the original shared lock applies. This implies that more than one program may be accessing the same region of the file, so the effect of a shared lock is simply to ensure that your code is not prevented from doing whatever it is doing by some other program with a shared lock on the file. Some operating systems do not support shared locks, in which case the request is always treated as an exclusive lock regardless of what you requested. Microsoft Windows 7 supports shared locks.

Note that a single Java Virtual Machine (JVM) does not allow overlapping locks, so different threads running on the same JVM cannot have overlapping locks on a file. However, the locks within two or more JVMs on the same computer can overlap. If another program changing the data in a file would cause a problem for you then the safe thing to do is to obtain an exclusive lock on the file you are working with. If you want to test for the presence of an overlapping lock, you can call the overlaps() method for your lock object.

Practical File Locking Considerations

You can apply file locks in any context, not just with memory-mapped files. The fact that all or part of a file can be locked by a program means that you cannot ignore file locking when you are writing a real-world Java application that may execute in a context where file locking is supported. You need to include at least shared file locks for regions of a file that your program uses. In some instances, though, you should use exclusive locks because external changes to a file’s contents can still be a problem even when the parts you are accessing cannot be changed. As I’ve said, you can obtain an exclusive lock only on a channel that is open for both reading and writing; a NonReadableChannelException or NonWritableChannelException is thrown, as appropriate, if you have opened the file just for input or just for output. This means that if you really must have an exclusive lock on a file, you have to have opened it for reading and writing.

You don’t need to obtain a lock on an entire file. Generally, if it is likely that other programs will be using the same file concurrently, it is not reasonable practice to lock everyone else out, unless it is absolutely necessary, such as a situation in which you may be performing a transacted operation that must either succeed or fail entirely. Circumstances where it would be necessary are when the correctness of your program result is dependent on the entire file’s contents not changing. If you were computing a checksum for a file, for example, you need to lock the entire file. Any changes made while your checksum calculation is in progress are likely to make it incorrect.

Most of the time it is quite sufficient to lock the portion of the file you are working with and then release it when you are done with it. You can get an idea of how you might do this in the context of the program that lists the primes from the primes.bin file.

TRY IT OUT: Using a File Lock

You will lock the region of the primes.bin file that you intend to read and then release it after the read operation is complete. You will use the tryLock() method because it does not block and try to acquire the lock again if it fails to return a reference to a FileLock object. To do this sensibly you need to be able to pause the current thread rather than roaring round a tight loop frantically calling the tryLock() method. I bring forward a capability from Chapter 16 to do this for you. You can pause the current thread by 200 milliseconds with the following code:

try {
  Thread.sleep(200);    // Wait for 200 milliseconds
 
} catch(InterruptedException e) {
  e.printStackTrace();
}
 

The static sleep() method in the Thread class causes the current thread to sleep for the number of milliseconds specified by the argument. While the current thread is sleeping, other threads can execute, so whoever has a lock on our file has a chance to release it.

Here’s the code for the complete example:

image
import java.nio.file.*;
import java.nio.channels.*;
import java.io.IOException;
import java.nio.*;
 
public class LockingPrimesRead {
  public static void main(String[] args) {
    Path file = Paths.get(System.getProperty("user.home")).
                  resolve("Beginning Java Stuff").resolve("primes.bin");
    final int PRIMECOUNT = 6;
    final int LONG_BYTES = 8;
    ByteBuffer buf = ByteBuffer.allocate(LONG_BYTES*PRIMECOUNT);
    long[] primes = new long[PRIMECOUNT];
 
    try (FileChannel inCh = (FileChannel)(Files.newByteChannel(file))){
      int primesRead = 0;
      FileLock inLock = null;
 
      // File reading loop
      while(true) {
        int tryLockCount = 0;
 
        // Loop to get a lock on the file region you want to read
        while(true) {
          inLock = inCh.tryLock(inCh.position(), buf.remaining(), true);
          if(inLock != null) {            // If you have a lock
           System.out.println("
Acquired file lock.");
           break;                         // exit the loop
          }
 
          if(++tryLockCount >= 100) {     // If you've tried too often
            System.out.printf("Failed to acquire lock after %d tries." +
                                           "Terminating...%n", tryLockCount);
            System.exit(1);               // end the program
          }
 
          // Wait 200 msec before the next try for a file lock
          try {
                Thread.sleep(200);        // Wait for 200 milliseconds
          } catch(InterruptedException e) {
              e.printStackTrace();
          }
        }
 
        // You have a lock so now read the file
        if(inCh.read(buf) == -1) {
          break;
        }
        inLock.release();                 // Release lock as read is finished
        System.out.println("Released file lock.");
 
        LongBuffer longBuf = ((ByteBuffer)(buf.flip())).asLongBuffer();
        primesRead = longBuf.remaining();
        longBuf.get(primes,0, longBuf.remaining());
        for(int i = 0 ; i < primesRead ; ++i) {
          if(i%6 == 0) {
            System.out.println();
          }
          System.out.printf("%12d", primes[i]);
        }
        buf.clear();                      // Clear the buffer for the next read
      }
 
      System.out.println("
EOF reached.");
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
}
 

LockingPrimesRead.java

This outputs primes from the file just as the ReadPrimes example does, but interspersed with comments showing where you acquire and release the file lock.

How It Works

The overall while loop for reading the file is now indefinite because you need to obtain a file lock before reading the file. You attempt to acquire the file lock in the inner while loop with the following statement:

          inLock = inCh.tryLock(inCh.position(), buf.remaining(), true);
 

This requests a shared lock on buf.remaining() bytes in the file starting with the byte at the current file position. You can’t get an exclusive lock on a file unless it has been opened for both reading and writing, and this doesn’t apply here. Acquiring a shared lock on just the part of the file that you want to read ensures that other programs are not prevented from accessing the file, but the bit you are working with cannot be changed externally. Another program cannot acquire an exclusive overlapping lock, but it can acquire a shared overlapping lock.

You have to test the value returned by the tryLock() method for null to determine whether you have obtained a lock or not. The if statement that does this is quite simple:

          if(inLock != null) {            // If you have a lock
           System.out.println("
Acquired file lock.");
           break;                         // exit the loop
          }
 

If inLock is not null, you have a lock on the file, so you exit the loop to acquire the lock. If inLock is null, you then check how often you have tried to acquire a lock and failed:

          if(++tryLockCount >= 100) {     // If you've tried too often
            System.out.printf("Failed to acquire lock after %d tries. " +
                                           "Terminating...%n", tryLockCount);
            System.exit(1);               // end the program
          }
 

The only reason for the String concatenation here is that the string won’t fit in the width of the page. If you have already tried 100 times to obtain a lock, you give up and exit the program. If it’s fewer tries than this, you’re prepared to give it another try, but first you pause the current thread:

          try {
                Thread.sleep(200);        // Wait for 200 milliseconds
          } catch(InterruptedException e) {
              e.printStackTrace();
          }
 

This pauses the current thread for 200 milliseconds, which provides an opportunity for the program that has an exclusive lock on the file to release it. After returning from the sleep() method, the while loop continues for another try at acquiring a lock.

After you have acquired a lock, you read the file in the usual way and release the lock:

        if(inCh.read(buf) == -1) {
          break;
        }
        inLock.release();                 // Release lock as read is finished
        System.out.println("Released file lock.");
 

By releasing the lock immediately after reading the file, you ensure that the amount of time the file is blocked is a minimum. Of course, if the read() method returns −1 because EOF has been reached, you won’t call the release() method for the FileLock object here because you exit the outer loop. However, after exiting the outer while loop you exit the try block, which closes the channel, and closing the channel releases the lock.

SUMMARY

In this chapter, I discussed the various ways in which you can read basic types of data from a file. You can now transfer data of any of the basic types to or from a file. In the next chapter you will learn how to transfer objects of class types that you have defined to or from a file.

EXERCISES

You can download the source code for the examples in the book and the solutions to the following exercises from www.wrox.com.

1. Write a program to read back the contents of the files written by the first exercise in the previous chapter and output the proverb length that was read and the proverb itself for each of the proverbs.

2. Extend the ReadPrimes example that you produced in this chapter to optionally display the nth prime, when n is entered from the keyboard.

3. Extend the ReadPrimes program further to output a given number of primes, starting at a given number. For example, output 15 primes starting at the 30th. The existing capabilities should be retained.

4. Write a program that outputs the contents of a file to the command line as groups of eight hexadecimal digits with five groups to a line, each group separated from the next by a space.

5. Write a program that allows either one or more names and addresses to be entered from the keyboard and appended to a file, or the contents of the file to be read back and output to the command line.

6. Modify the previous example to store an index to the name and address file in a separate file. The index file should contain each person’s second name, plus the position where the corresponding name and address can be found in the name and address file. Provide support for an optional command argument allowing a person’s second name to be entered. When the command-line argument is present, the program should then find the name and address and output it to the command line.

image

• WHAT YOU LEARNED IN THIS CHAPTER

TOPIC CONCEPT
File Input/Output All file input and output is in terms of bytes. When you read bytes from a file, you must then interpret the bytes as the types of data items that were written to the file.
Accessing a File You can obtain a FileChannel object connected to a file for reading, writing, or both by calling the static newByteChannel() method in the Files class for the Path object that encapsulates the path to the file. The method returns a reference of type SeekableByteChannel.
Reading and Writing a File The argument to the newByteChannel() method determines whether you open the file for reading, writing, or both. The argument can be one or more of the constants defined by the StandardOpenOption enumeration.
File Read Operations You call the read() method for a FileChannel object connected to a file to read() from the file.
Reading Data from a File The data that you read from a file is stored in a buffer of type ByteBuffer.
Interpreting Data from a File You can call one of the get methods for a ByteBuffer object to retrieve bytes from the buffer interpreted as a given basic type.
Using View Buffers A view buffer interprets the contents of a ByteBuffer as a sequence of data items of a given type. You can use view buffers to interpret the bytes read from a file into a ByteBuffer as any basic type other than boolean. You obtain a view buffer corresponding to a ByteBuffer object for a given data type by calling the appropriate ByteBuffer class method.
Memory-Mapped Files A memory-mapped file enables you to access data in the file as though it were resident in memory. You access a memory-mapped file through a MappedByteBuffer object that you obtain by calling the map() method for a FileChannel object.
File Locking You can lock all or part of the contents of a file by acquiring a file lock. A file lock can be an exclusive lock that locks the entire file for your use, or it can be a shared lock that locks part of a file while you are accessing it.
Exclusive File Locks You can obtain an exclusive lock only on a file you have opened for both reading and writing. You obtain an exclusive lock by calling the lock() method for the FileChannel object that is connected to the file.
Shared File Locks You obtain a shared file lock by calling the tryLock() method for the FileChannel object that is connected to the file.
image
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.98.111