Main Page

Previous Next

Writing To a File

To start with, we will be using the simplest write() method for a file channel that writes the data contained in a single ByteBuffer object to a file. The number of bytes written to the file is determined by the buffer's position and limit when the write() method executes. Bytes will be written starting with the byte at the buffer's current position. The number of bytes written is limit-position, which is the number returned by the remaining() method for the buffer object. The write() method returns the number of bytes written as a value type int.

A channel write() operation can throw any of five different exceptions:

Exception

Description

NonWritableChannelException

Thrown if the channel was not opened for writing.

ClosedChannelException

Thrown if the channel is closed. Calling the close() method for the file channel will close the channel, as will calling the close() method for the file stream.

AsynchronousCloseException

Thrown if another thread closes the channel while the write operation is in progress.

ClosedByInterruptException

Thrown if another thread interrupts the current thread while the write operation is in progress.

IOException

Thrown if some other I/O error occurs.

The first of these is a subclass of RuntimeException, so you do not have to catch this exception. The other four are subclasses of IOException, which must be caught, so the write() method call must be in a try block. If you want to react specifically to one or other of these last four exceptions, you will need to add a catch block for that specific type. Otherwise you can just include a single catch block for type IOException to catch all four types of exception. For instance if you have set up a ByteBuffer, buf, ready to be written, you might code the write operation like this:

File aFile = new File("C:/Beg Java Stuff/myFile.text");
// Place to store an output stream reference
FileOutputStream outputFile = null;
try {
  // Create the stream opened to write
  outputFile = new FileOutputStream(aFile); 
} catch (FileNotFoundException e) {
  e.printStackTrace(System.err);
}
// Get the channel for the file
FileChannel outputChannel = outputFile.getChannel();

try {
  outputChannel.write(buf);
} catch (IOException e) {
  e.printStackTrace(System.err);
}

A write() method for a channel will only return when the write operation is complete, but this does not guarantee that the data has actually been written to the file. Some of the data may still reside in the native I/O buffers. If the data you are writing is critical and you want to minimize the risk of losing it in the event of a system crash, you can force all outstanding output operations to a file that were previously executed by the channel to be completed by calling the force() method for the FileChannel object like this:

try {
  outputChannel.force();
} catch (IOException e) {
  e.printStackTrace(System.err);
}

The force() method will throw a ClosedChannelException if the channel is closed, or an IOException if some other I/O error occurs. Note that the force() method only guarantees that all data will be written for a local storage device.

Only one write operation can be in progress for a given file channel at any time. If you call write() while a write() operation initiated by another thread is in progress, your call to the write() method will block until the write that is in progress has been completed.

File Position

The position of a file is the index position of where the next byte is to be read or written. The first byte in a file is at position zero so the value for a file's position is the offset of the next byte from the beginning. Don't mix this up with a buffer's position that we discussed earlier – the two are quite independent, but of course, they are connected. When you write a buffer to a file using the write() method we discussed in the previous section, the byte in the buffer at the buffer's current position will be written to the file at its current position:

Click To expand

The file channel object maintains a record of the current position in the file. If the file stream was created to append to the file by using a FileOutputStream constructor with the append mode argument as true, then the file position recorded by the channel for the file will start out at the byte following the last byte. Otherwise, the initial file position will be the first byte of the file. The file position will generally be incremented by the number of bytes written each time you write to the file. There is one exception to this. The FileChannel class defines a special write() method that does the following:

Method

Description

write(ByteBuffer buf, long position)

This writes the buffer, buf, to the file at the position specified by the second argument, and not the file position recorded by the channel. Bytes from the buffer are written starting at the buffer's current position, and buf.remaining() bytes will be written. This does not update the channel's file position.

This method can throw any of the following exceptions:

Exception

Description

IllegalArgumentException

Thrown if you specify a negative value for the file position.

NonWritableChannelException

Thrown if the file was not opened for writing.

ClosedChannelException

Thrown if the channel is closed.

AsynchronousCloseException

Thrown if another thread closes the channel while the write operation is in progress.

ClosedByInterruptException

Thrown if another thread interrupts the current thread while the write operation is in progress.

IOException

If any other I/O error occurs.

You might use this method in a sequence of writes to update a particular part of the file without disrupting the primary sequence of write operations. For example, you might record a count of the number of records in a file at the beginning. As you add new records to the file, you could update the count at the beginning of the file without changing the file position recorded by the channel, which would be pointing to the end of the file where new data is to be written.

You can find out what the current file position is by calling the position() method for the FileChannel object. This returns the position as type long rather than type int since it could conceivably be a large file with a lot more than two billion bytes in it. You can also set the file position by calling a position method for the FileChannel object, with an argument of type long specifying a new position. For instance, if we have a reference to a file channel stored in a variable outputChannel, we could alter the file position with the statements:

try {
  outputChannel.position(fileChannel.position() – 100);
} catch (IOException e) {
  e.printStackTrace(System.err);
}

This moves the current file position back by 100 bytes. This could be because we have written 100 bytes to the file and want to reset the position so we can rewrite it. The method must be in a try block because it can throw an exception of type IOException if an I/O error occurs.

You can set the file position beyond the end of the file. If you then write to the file, the bytes between the previous end of the file and the new position will contain junk values. If you try to read from a position beyond the end of the file, an end-of-file condition will be returned immediately.

When you are finished with writing a file you should close it by calling the close() method for the file stream object. This will close the file and the file channel. A FileChannel object defines its own close() method that will close the channel but not the file. It's time to start exercising your disk drive. Let's try an example.

Try It Out – Using a Channel to Write a String to a File

We will write the string "Garbage in, garbage out\n" to a file, charData.txt, that we will create in the directory Beg Java Stuff on drive C:. If you want to write to a different drive and/or directory, just change the program accordingly. Here is the code:

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;

public class WriteAString {
  public static void main(String[] args) {
    String phrase = new String("Garbage in, garbage out\n");
    String dirname = "C:/Beg Java Stuff";   // Directory name
    String filename = "charData.txt";       // File name

    File dir = new File(dirname);           // File object for directory

    // Now check out the directory
    if (!dir.exists()){                      // If directory does not exist
     
      if (!dir.mkdir()){                    // ...create it
       
        System.out.println("Cannot create directory: " + dirname);
        System.exit(1);
      } 
    } else if (!dir.isDirectory()) {
      System.err.println(dirname + " is not a directory");
      System.exit(1);
    } 

    // Create the filestream
    File aFile = new File(dir, filename);
    FileOutputStream outputFile = 
      null;                          // Place to store the stream reference
    try {
      outputFile = new FileOutputStream(aFile, true);
      System.out.println("File stream created successfully.");
    } catch (FileNotFoundException e) {
      e.printStackTrace(System.err);
    } 

    // Create the file output stream channel
    FileChannel outChannel = outputFile.getChannel();
    
    ByteBuffer buf = ByteBuffer.allocate(1024);
    System.out.println("New buffer:           position = " + buf.position()
                       + "\tLimit = " + buf.limit() + "\tcapacity = "
                       + buf.capacity());

    // Load the data into the buffer
    for (int i = 0; i < phrase.length(); i++) {
      buf.putChar(phrase.charAt(i));
    }

    System.out.println("Buffer after loading: position = " + buf.position()
                       + "\tLimit = " + buf.limit() + "\tcapacity = "
                       + buf.capacity());
    buf.flip();                // Flip the buffer ready for file write
    System.out.println("Buffer after flip:    position = " + buf.position() 
                       + "\tLimit = " + buf.limit() + "\tcapacity = " 
                       + buf.capacity());

    // Write the file
    try {
      outChannel.write(buf);   // Write the buffer to the file channel
      outputFile.close();      // Close the output stream & the channel
      System.out.println("Buffer contents written to file.");
    } catch (IOException e) {
      e.printStackTrace(System.err);
    }
    System.exit(0);
  }
}

There is some command-line output from the program to trace what is going on. After you have compiled and run this program, you should see the output:

File stream created successfully.
New buffer:           position = 0      Limit = 1024    capacity = 1024
Buffer after loading: position = 48     Limit = 1024    capacity = 1024
Buffer after flip:    position = 0      Limit = 48      capacity = 1024
Buffer contents written to file.

You can inspect the contents of the file, charData.txt, using a plain text editor. They will look something like the following.

 G a r b a g e   i n ,   g a r b a g e   o u t 

There are spaces between the characters, because we are writing Unicode characters to the file, and two bytes are written for each character in the original string. Your text editor may represent the first of each byte pair as something other than spaces, or possibly not at all, as they are bytes that contain zero. You might even find that your plain text editor will only display the first 'G'. If so, try to find another editor. If you run the example several times, the phrase will be appended to the file for each execution of the program.

Important 

Don't be too hasty deleting this or other files we will write later in this chapter, as we will reuse some of them in the next chapter when we start exploring how to read files.

How It Works

We have defined three String objects:

  • phrase – The string that we will write to the file

  • dirname – The name of the directory we will create

  • filename – The name of the file

In the try block, we first create a File object to represent the directory. If this directory does not exist, the exists() method will return false and the mkdir() method for dir will be called to create it. If the exists() method returns true , we must make sure that the File object represents a directory, and not a file.

Having established the directory one way or another, we create a File object, aFile, to represent the path to the file. We use this object to create a FileOutputStream object that will append data to the file. Omitting the second argument to the FileOutputStream constructor or specifying it as false would make the file stream overwrite any existing file contents. The file stream has to be created in a try block because the constructor can throw a FileNotFoundException. Once we have a FileOutputStream object, we call its getChannel() method to obtain a reference to the channel that we will use to write the file.

The next step is to create a ByteBuffer object and load it up with the characters from the string. We create a buffer with a capacity of 1024 bytes. This is so we can see clearly the difference between the capacity and the limit after flipping. We could have created a buffer exactly the size required with the statement:

    ByteBuffer buf = ByteBuffer.allocate(2*phrase.length());

You can see how the position, limit, and capacity values change from the output. We use the putChar() method for the buffer object to transfer the characters one at a time in a loop and then output the information about the buffer status again. The limit is still as it was but the position has increased by the number of bytes written.

Finally, we write the contents of the buffer to the file. You can see here how flipping the buffer before the operation sets up the limit and position ready for writing the data to the file.

The FileChannel object has a method, size(), which will return the length of the file in bytes as a value of type, long. You could try this out by adding the following statement immediately after the statement that writes the buffer to the channel:

System.out.println("The file contains " + outChannel.size() + " bytes.");

You should see that 48 bytes are written to the file each time, since phrase contains 24 characters. The size() method returns the total number of bytes in the file so the number will grow by 48 each time you run the program.

Using a View Buffer

The code in the previous example is not the only way of writing the string to the buffer. We could have used a view buffer, like this:

    ByteBuffer buf = ByteBuffer.allocate(1024);
    CharBuffer charBuf = buf.asCharBuffer();
    charBuf.put(phrase);                     // Transfer string to buffer
    buf.limit(2*charBuf.position());         // Update byte buffer limit

    // Create the file output stream channel
    FileChannel outChannel = outputFile.getChannel();

    // Write the file
    try {
      outChannel.write(buf);    // Write the buffer to the file channel
      outputFile.close();       // Close the output stream & the channel
    } catch(IOException e) {
      e.printStackTrace(System.err);
    }

Transferring the string via a view buffer of type CharBuffer is much simpler. The only fly in the ointment is that the backing ByteBuffer has no knowledge of this. The position for buf is still sitting firmly at zero with the limit as the capacity, so flipping it won't set it up ready to write to the channel. However, all we have to do is to set the limit corresponding to the number of bytes we transferred to the view buffer.

Of course, if we were writing the file for use by some other program, writing Unicode characters could be very inconvenient if the other program environment did not understand it. Let's see how we would write the data as bytes in the local character encoding.

Try It Out – Writing a String as Bytes

We will strip out the directory validation to keep the code shorter:

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;

public class WriteAStringAsBytes {
  public static void main(String[] args) {

    String phrase = new String("Garbage in, garbage out\n");
    String dirname = "C:/Beg Java Stuff";   // Directory name
    String filename = "byteData.txt";

    File aFile = new File(dirname, filename);

    // Create the file output stream
    FileOutputStream file = null;
    try {
      file = new FileOutputStream(aFile, true);
    } catch (FileNotFoundException e) {
      e.printStackTrace(System.err);
    } 
    FileChannel outChannel = file.getChannel();
    ByteBuffer buf = ByteBuffer.allocate(phrase.length());
    byte[] bytes = phrase.getBytes();

    buf.put(bytes);
    buf.flip();

    try {
      outChannel.write(buf);
      file.close();   // Close the output stream & the channel
    } catch (IOException e) {
      e.printStackTrace(System.err);
    }
  }
}

If you run this a couple of times and look into the byteData.txt file with your plain text editor, you should find:

Garbage in, garbage out
Garbage in, garbage out

There are no gaps between the letters this time because the Unicode character were converted to bytes in the default character encoding on your system by the getBytes() method for the string.

How It Works

We create the file stream and the channel essentially as in the previous example. This time the buffer is created with the precise amount of space we need. Since we will be writing each character as a single byte, the buffer capacity only needs to be the length of the string, phrase.

We convert the string to a byte array in the local character encoding using the getBytes() method defined in the String class. We transfer the contents of the array to the buffer using the relative put() method for the channel. After a quick flip of the buffer, we use the channel's write() method to write the buffer's contents to the file.

We could have written the conversion of the string to an array plus the sequence of operations with the buffer and the channel write operation much more economically, if less clearly, like this:

  outChannel.write(buf.put(myStr.getBytes()).flip());

This makes use of the fact that the buffer methods we are using here return a reference to the buffer so we can chain them together.

Writing Varying Length Strings to a File

So far, the strings we have written to the file have all been of the same length. It is very often the case that you will want to write a series of strings of different lengths to a file. In this case, if you want to recover the strings from the file, you need to provide some information in the file that allows the beginning and/or end of each string to be determined. One possibility is to write the length of each string to the file immediately preceding the string itself.

To do this, we can get two view buffers from the byte buffer we will use to write the file, one of type IntBuffer and the other of type CharBuffer. Let's see how we can use that in another example that writes strings of various lengths to a file.

Try It Out – Writing Multiple Strings to a File

We will just write a series of useful proverbs to a file. Here's the code:

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;

public class WriteProverbs {
  public static void main(String[] args) {
    String dirName = "c:/Beg Java Stuff";   // Directory for the output file
    String fileName = "Proverbs.txt";       // Name of the output file
    String[] sayings = {
      "Indecision maximizes flexibility.",
      "Only the mediocre are always at their best.",
      "A little knowledge is a dangerous thing.",
      "Many a mickle makes a muckle.",
      "Who begins too much achieves little.",
      "Who knows most says least.",
      "A wise man sits on the hole in his carpet."
    };
    File aFile = new File(dirName, fileName);

    FileOutputStream outputFile = null;
    try {
      outputFile = new FileOutputStream(aFile, true);
    } catch (FileNotFoundException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    } 
    FileChannel outChannel = outputFile.getChannel();

    // Create a buffer to accommodate the longest string + its length value
    int maxLength = sayings[0].length();
    for (int i = 1; i < sayings.length; i++) { 
      if (maxLength < sayings[i].length()) 
        maxLength = sayings[i].length ();
    }

    ByteBuffer buf = ByteBuffer.allocate(2 * maxLength + 4);

    // Write the file
    try {
      for (int i = 0; i < sayings.length; i++) {
        buf.putInt(sayings[i].length()).asCharBuffer().put(sayings[i]);
        buf.position(buf.position() + 2 * sayings[i].length()).flip();
        outChannel.write(buf);   // Write the buffer to the file channel
        buf.clear();
      }
      outputFile.close();        // Close the output stream & the channel
      System.out.println("Proverbs written to file.");
    } catch (IOException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    }
    System.exit(0);
  }
}

When you execute this it should produce the rather terse output:

Proverbs written to file.

You can check the veracity of this assertion by inspecting the contents of the file with a plain text editor.

How It Works

The program writes the strings from the array, sayings, to the file.

We create a String array, sayings[], that contains seven proverbs that are written to the stream in the for loop. We put the length of each proverb in the buffer using the putInt() method for the ByteBuffer object. We then use a view buffer of type CharBuffer to transfer the string to the buffer. The contents of the view buffer will start at the current position for the byte buffer. This corresponds to the byte immediately following the string length.

Transferring the string into the view buffer only causes the view buffer's position to be updated. The byte buffer's position is still pointing back at the byte following the string length where the first character of the string was written. We therefore have to increment the position for the byte buffer by twice the number of characters in the string before flipping it to make it ready to be written to the file.

The first time you run the program, the file doesn't exist, so it will be created. You can then look at the contents. If you run the program again, the same proverbs will be appended to the file, so there will be a second set. Alternatively, you could modify the sayings[] array to contain different proverbs the second time around. Each time the program runs, the data will be added at the end of the existing file.

After writing the contents of the byte buffer to the file, we call its clear() method to reset the position to zero and the limit back to the capacity. This makes it ready for transferring the data for the next proverb on the next iteration. Remember that it doesn't change the contents of the buffer though.

As we will write a program to read the proverbs.txt file back in the next chapter, you should leave it on your disk.

Direct and Indirect Buffers

When you allocate a byte buffer by calling the allocate() method for the ByteBuffer class, you get an indirect buffer. An indirect buffer is not used by the native I/O operations. Data to be written to a file has to be copied to an intermediate buffer in memory before the write operation can take place. Similarly, after a read operation the data is copied from the input buffer used by your operating system to the indirect buffer that you allocate.

Of course, with small buffers and limited amounts of data being read, using an indirect buffer doesn't add much overhead. With large buffers and lots of data, it can make a significant difference though. In this case, you can use the allocateDirect() method in the ByteBuffer class to allocate a direct buffer. The JVM will try to make sure that the native I/O operation makes use of the direct buffer, thus avoiding the overhead of the data copying process. The allocation and de-allocation of a direct buffer carries its own overhead, which may outweigh any advantages gained if the buffer size and data volumes are small.

You can test for a direct buffer by calling its isDirect() method. This will return true if it is a direct buffer and false otherwise.

You could try this out by making a small change to the previous example. Just replace the statement:

    ByteBuffer buf = ByteBuffer.allocate(2 * maxLength + 4);

with the following two statements:

    ByteBuffer buf = ByteBuffer.allocateDirect(2 * maxLength + 4);
    System.out.println("Buffer is "+ (buf.isDirect()?"":"not")+"direct.");

This will output a line telling you whether the program is working with a direct buffer or not. It will produce the following output:

Buffer is direct.
Proverbs written to file.

Writing Numerical Data to a File

Let's see how we could set up our primes-generating program from Chapter 4 to write primes to a file instead of outputting them. We will base the new code on the MorePrimes version of the program. Ideally, we could add a command-line argument to specify how many primes we want. This is not too difficult. Here's how the code will start off:

public class PrimesToFile {
  public static void main(String[] args) {
    int primesRequired = 100;   // Default prime count
    if (args.length > 0) {
      try {
        primesRequired = Integer.valueOf(args[0]).intValue();
      } catch (NumberFormatException e) {
        System.out.println("Prime count value invalid. Using default of "
                           + primesRequired);
      }
      // Code to generate the primes...

      // Code to write the file...
    }
  }
}

Here, if we don't find a command-line argument that we can convert to an integer, we just use a default count of 100.

We can now generate the primes with code similar to that in Chapter 4 as follows:

      long[] primes = new long[primesRequired];   // Array to store primes
      primes[0] = 2;                              // Seed the first prime
      primes[1] = 3;                              // and the second
      // Count of primes found – up to now, which is also the array index
      int count = 2;
      // Next integer to be tested
      long number = 5;

      outer:
      for (; count < primesRequired; number += 2) {

        // The maximum divisor we need to try is square root of number
        long limit = (long) Math.ceil(Math.sqrt((double) number));

        // Divide by all the primes we have up to limit
        for (int i = 1; i < count && primes[i] <= limit; i++) 
          if (number % primes[i] == 0)          // Is it an exact divisor?
            continue outer;                      // yes, try the next number

        primes[count++] = number;                // We got one!
      }

Now all we need to do is add the code to write the primes to the file. Let's put this into a working example.

Try It Out – Writing Primes to a File

Here's the complete example with the additional code to write the file shown shaded:

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;

public class PrimesToFile {
  public static void main(String[] args) {
    int primesRequired = 100;   // Default count
    if (args.length > 0) {
      try {
        primesRequired = Integer.valueOf(args[0]).intValue();
      } catch (NumberFormatException e) {
        System.out.println("Prime count value invalid. Using default of "
                           + primesRequired);
      }
    }

      long[] primes = new long[primesRequired];   // Array to store primes
      primes[0] = 2;                              // Seed the first prime
      primes[1] = 3;                              // and the second
      // Count of primes found – up to now, which is also the array index
      int count = 2;
      // Next integer to be tested
      long number = 5;

      outer:
      for (; count < primesRequired; number += 2) {

        // The maximum divisor we need to try is square root of number
        long limit = (long) Math.ceil(Math.sqrt((double) number));

        // Divide by all the primes we have up to limit
        for (int i = 1; i < count && primes[i] <= limit; i++) 
          if (number % primes[i] == 0)           // Is it an exact divisor?
            continue outer;                      // yes, try the next number

        primes[count++] = number;                // We got one!
      }

    File aFile = new File("C:/Beg Java Stuff/primes.bin");
    FileOutputStream outputFile = null;
    try {
      outputFile = new FileOutputStream(aFile);   // Create the file stream
    } catch (FileNotFoundException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    } 
    FileChannel file = 
      // Get the channel from the stream
      outputFile.getChannel();
    final int BUFFERSIZE = 100;                // Byte buffer size
    ByteBuffer buf = ByteBuffer.allocate(BUFFERSIZE);
    LongBuffer longBuf = buf.asLongBuffer();   // View buffer for type long

    // Count of primes written to file
    int primesWritten = 0;

    while (primesWritten < primes.length) {
      longBuf.put(primes,              // Array to be written
                  primesWritten,       // Index of 1st element to be written
                  Math.min(longBuf.capacity(),  
                        primes.length – primesWritten)); // Element count
      buf.limit(8 * longBuf.position());
      try {
        file.write(buf);
        primesWritten += longBuf.position();
      } catch (IOException e) {
        e.printStackTrace(System.err);
        System.exit(1);
      } 
      longBuf.clear();
      buf.clear();
    } 

    try {
      System.out.println("File written is " + file.size() + " bytes.");
      outputFile.close();   // Close the file and its channel
    } catch (IOException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    }
    System.exit(0);
  }
}

This produces the output:

File written is 800 bytes.

This looks reasonable since we wrote 100 values of type long as binary data and they are 8 bytes each.

How It Works

We create a FileOutputStream object and obtain the channel in the way we have already seen. Since we did not specify we wanted to append to the file when we created the stream object, the file will be overwritten each time we run the program.

We create the ByteBuffer object with a capacity of 100 bytes. I chose this value so that it is not an exact multiple of 8 – the number of bytes occupied by each prime value. This makes the problem more interesting. You can change the buffer size by change the value set for BUFFERSIZE.

The primes will be transferred to the buffer through a view buffer of type LongBuffer that we obtain from the original buffer. Since the buffer is too small to hold all the primes, we have to load it and write the primes to the file in a loop.

The primesWritten variable counts how many primes we have written to the file so we can use this to control the while loop that writes the primes to the file. The loop continues as long as primesWritten is less than the number of elements in the primes array. The number of primes that the LongBuffer object can hold corresponds to longBuf.capacity().We can transfer this number of primes to the buffer as long as there is that many left in the array still to be written to the file, so we do the transfer of a block of primes to the buffer like this:

      longBuf.put(primes, primesWritten, 
                  Math.min(longBuf.capacity(),
                           primes.length – primesWritten));

The first argument to the put() method is the array that is the source of the data and the second argument is the index position of the first element to be transferred. The third argument will be the capacity of the buffer as long as there are more than that number of primes still in the array. If there is less than this number on the last iteration, we will transfer primes.length-primesWritten values to the buffer.

Since we are using a relative put operation, loading the view buffer will change the position for that buffer to reflect the number of values transferred to it. However, the backing byte buffer that we use in the channel write operation will still have its limit and position unchanged. We therefore set the limit for the byte buffer with the statement:

      buf.limit(8 * longBuf.position());

Since each prime occupies 8 bytes, multiplying the position value for the view buffer by 8 gives us the number of bytes occupied in the primary buffer. We then go ahead and write that buffer to the file and increment primesWritten by the position value for the view buffer, since this will be the number of primes that were written. Before the next iteration we call clear() for both buffers to reset their limits and positions to their original states – 0 and the capacity respectively. When we have written all the primes, the loop will end and we output the length of the file before closing it.

Since this file contains binary data, we will not want to view it except perhaps for debugging purposes.

Writing Mixed Data to a File

Sometimes, you may want to write more than one kind of data to a file. You may want to mix integers with floating-point values with text perhaps. One way to do this is to use multiple view buffers. We can illustrate the principle of how this works by outputting some text along with each binary prime value in the previous example. Rather than taking the easy route by just writing the same text for each prime value, let's add a character representation of the prime preceding each binary value. We'll add something like prime = xxx ahead of each binary value. The first point to keep in mind is that if we ever want to read the file successfully, we can't just dump strings of varying lengths in it. You would have no way to tell where the text ended and where the binary data began. We either have to fix the length of the string or provide data in the file that specifies the length of the string. We will therefore choose to write the data corresponding to each prime as three successive data items:

  1. A count of the length of the string as binary value (it would sensibly be an integer type but we'll make it type double since we need the practice)

  2. The string representation of the prime value: prime = xxx

  3. The prime as a binary value of type long

The basic prime calculation will not change at all, so we only need to update the shaded code at the end in the previous example that writes the file.

The basic strategy we will adopt is to create a byte buffer, and then create a series of view buffers that map the three different kinds of data into it. A simple approach would be to write the data for one prime at a time, so let's try that first. Setting up the file stream and the channel will be more or less exactly the same:

    File aFile = new File("C:/Beg Java Stuff/primes.txt");
    FileOutputStream outputFile = null;
    try {
      outputFile = new FileOutputStream(aFile);
    } catch (FileNotFoundException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    } 
    FileChannel file = outputFile.getChannel();

The file extension has been changed to .txt to differentiate it from the original binary file that we wrote. We will want to make use of both the binary file and this file when we are looking into file read operations.

The byte buffer has to be large enough to hold the double value that counts the characters in the string, the string itself, plus the long value for the prime. The original byte buffer with 100 bytes capacity will be plenty big enough so let's go with that:

    final int BUFFERSIZE = 100;   // Buffer size in bytes
    ByteBuffer buf = ByteBuffer.allocate(BUFFERSIZE);

We need to create three view buffers from the byte buffer, one that will hold the double value for the count, one for the string, and one for the binary prime value, but there is a problem.

Click To expand

The length of the string will depend on the number of decimal digits in the prime value, so we don't know where it ends. This implies we can't map the last buffer to a fixed position. We are going to have to set this buffer up dynamically inside the file-writing loop after we figure out how long the string for the prime is. We can set up the first two view buffers outside the loop though:

    DoubleBuffer doubleBuf = buf.asDoubleBuffer();
    buf.position(8);
    CharBuffer charBuf = buf.asCharBuffer();

The first buffer that will hold the string length as type double will map to the beginning of the byte buffer, buf. The view buffer into which we will place the string needs to map to the position in buf immediately after the space required for the double value – 8 bytes from the beginning of buf in other words. Remember that the first element in a view buffer maps to the current position in the byte buffer. Thus, we can just set the position for buf to 8 before creating the view buffer, charBuf. All that's now needed is the loop to load up the first two view buffers, create the third view buffer and load it, and then write the file. Let's put the whole thing together as a working example.

Try It Out – Using Multiple View Buffers

The code for the loop is shaded in the following complete program:

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;

public class PrimesToFile2 {
  public static void main(String[] args) {
    int primesRequired = 100;   // Default count
    if (args.length > 0) {
      try {
        primesRequired = Integer.valueOf(args[0]).intValue();
      } catch (NumberFormatException e) {
        System.out.println("Prime count value invalid. Using default of "
                           + primesRequired);
      } 
    } 

    long[] primes = new long[primesRequired];   // Array to store primes
    primes[0] = 2;                              // Seed the first prime
    primes[1] = 3;                              // and the second
    // Count of primes found – up to now, which is also the array index
    int count = 2;
    long number = 5;                            // Next integer to be tested

    outer:
    for (; count < primesRequired; number += 2) {

      // The maximum divisor we need to try is square root of number
      long limit = (long) Math.ceil(Math.sqrt((double) number));

      // Divide by all the primes we have up to limit
      for (int i = 1; i < count && primes[i] <= limit; i++) 
        if (number % primes[i] == 0)    // Is it an exact divisor?
          continue outer;                // yes, try the next number

      primes[count++] = number;          // We got one!
    }

    File aFile = new File("C:/Beg Java Stuff/primes.txt");
    FileOutputStream outputFile = null;
    try {
      outputFile = new FileOutputStream(aFile);
    } catch (FileNotFoundException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    } 
    FileChannel file = outputFile.getChannel();
    final int BUFFERSIZE = 100;   // Buffer size in bytes
    ByteBuffer buf = ByteBuffer.allocate(BUFFERSIZE);

    DoubleBuffer doubleBuf = buf.asDoubleBuffer();
    buf.position(8);
    CharBuffer charBuf = buf.asCharBuffer();
    LongBuffer longBuf = null;
    String primeStr = null;

    for (int i = 0; i < primes.length; i++) {
      primeStr = "prime = " + primes[i];          // Create the string
      doubleBuf.put(0,(double) primeStr.length());// Store the string length
      charBuf.put(primeStr);                      // Store the string
      buf.position(2 * charBuf.position() + 8);   // Position for 3rd buffer
      longBuf = buf.asLongBuffer();               // Create the buffer
      longBuf.put(primes[i]);              // Store the binary long value
      buf.position(buf.position() + 8);    // Set position after last value
      buf.flip();                          // and flip
      try {
        file.write(buf);                   // Write the buffer as before.
      } catch (IOException e) {
        e.printStackTrace(System.err);
        System.exit(1);
      } 
      buf.clear();
      doubleBuf.clear();
      charBuf.clear();
    } 
    try {
      System.out.println("File written is " + file.size() + " bytes.");
      outputFile.close();   // Close the file and its channel
    } catch (IOException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    } 
    System.exit(0);
  }
}

This should produce the output:

File written is 3742 bytes.

How It Works

We only need to discuss the loop. We first create the string because we need to know its length so we can put that in the buffer first. We insert the length as type double in the view buffer, longBuf. We can then put the string into charBuf as this buffer already maps to the elements 8 along from the start of buf. Next, we update the position in buf to the element following the string. This will allow us to map longBuf to the byte buffer correctly. After creating the third view buffer, longBuf, we load the prime value. We then update the position for buf to the byte following this value. This will be the position as previously set plus 8. Finally we flip buf to set the position and limit for writing, and then the channel writes to the file.

If you inspect the file with a plain text editor you should get an idea of what is in the file. You should be able to see the Unicode strings separated by the binary values we have written to the file.

This writes the file one prime at a time, so it's not going to be very efficient. It would be better to use a larger buffer and load it with multiple primes. Let's see how we can do that.

Try It Out – Multiple Records in a Buffer

We will be loading the byte buffer using three different view buffers repeatedly to put as many primes into the buffer as we can. The basic idea is illustrated below:

Click To expand

We will just show the new code that replaces the code in the previous example that allocates the buffers and writes the file:

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;

public class PrimesToFile3 {
  public static void main(String[] args) {

    // Code as in the previous example...

    final int BUFFERSIZE = 1024;   // Buffer size in bytes – bigger!
    ByteBuffer buf = ByteBuffer.allocate(BUFFERSIZE);
    String primeStr = null;
    int primesWritten = 0;
    while (primesWritten < primes.length) {
      while (primesWritten < primes.length) {
        primeStr = "prime = " + primes[primesWritten];
        if ((buf.position() + 2 * primeStr.length() + 16) > buf.limit()) {
          break;
        }
        buf.asDoubleBuffer().put(0, (double) primeStr.length());
        buf.position(buf.position() + 8);
        buf.position(buf.position()
                     + 2 * buf.asCharBuffer().put(primeStr).position());
        buf.asLongBuffer().put(primes[primesWritten++]);
        buf.position(buf.position() + 8);
      } 
      buf.flip();
      try {
        file.write(buf);
      } catch (IOException e) {
        e.printStackTrace(System.err);
        System.exit(1);
      }
      buf.clear();
    }
    try {
      System.out.println("File written is " + file.size() + " bytes.");
      outputFile.close();   // Close the file and its channel
    } catch (IOException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    }
    System.exit(0);
  }
}

You should get the same output as for the previous example here.

How It Works

To start with, we just create a byte buffer with a capacity of 1024 bytes. All the view buffers are created inside the inner while loop. Both loops end when primesWritten, which counts the number of primes written to the file, reaches the length of the primes array. The inner loop loads up the buffer and the outer loop writes the contents of the buffer to the file.

The first step in the inner loop is to create the prime string. This makes it possible to check whether there is enough free space in the byte buffer to accommodate the string plus the two binary values – the string length as type double and the prime itself of type long. If there isn't enough space, the break statement will be executed so the inner loop will end, and the channel will write the buffer contents to the file after flipping it. After the buffer has been written, the buffer's clear() method is called to reset the position to 0 and the limit to the capacity.

When there is space in the byte buffer, the inner loop loads the buffer starting with the statement:

        buf.asDoubleBuffer().put(0, (double)primeStr.length());

This creates a view buffer of type DoubleBuffer and calls its put() method to transfer the length of the string to the buffer. We don't save the view buffer reference because we will need a different one on the next iteration – one that maps to the position in the byte buffer following the data we are transferring for the current prime.

The next statement increments the position of the byte buffer, by the number of bytes in the string length value. We then execute the statement:

        buf.position(buf.position()
                     + 2 * buf.asCharBuffer().put(primeStr).position());

This statement is a little complicated so let's dissect it. The expression for the argument to the position() method within the parentheses executes first. This calculates the new position for buf as the current position, given by buf.position(), plus the value resulting from the expression:

2 * buf.asCharBuffer().put(primeStr).position()

The subexpression, buf.asCharBuffer(), creates a view buffer of type CharBuffer. The put() method for this is called to transfer primeStr to the buffer, and this returns a reference to the CharBuffer object. This is used to call its position() method that will return the position after transferring the string, so multiplying this by 2 gives the number of bytes occupied by the string in buf. Thus, the position for buf is updated to the point following the string.

The last step in the loop is to execute the statements:

        buf.asLongBuffer().put(primes[primesWritten++]);
        buf.position(buf.position() + 8);

The first statement here transfers the binary prime value to the buffer via a view buffer of type LongBuffer and increments the count of the number of primes written to the file. The second statement updates the position for buf to the next available byte. The inner while loop then continues with the next iteration to load the data for the next prime into the buffer. This will continue until there is insufficient space for another prime, whereupon the inner loop will end, and the buffer will be written to the file.

Gathering-Write Operations

We will look at one further file channel output capability before we try to read a file – the ability to transfer data to a file from several buffers in sequence in a single write operation. This is called a gathering-write operation. The advantage of this capability is that it avoids the necessity to copy information into a single buffer before writing it to a file. A gathering-write operation is one side of what are called scatter-gather I/O operations. We will look into the other side – the scattering-read operation – in the next chapter.

Just to remind you, a file channel has two methods that can perform a gathering-write operation:

Method

Description

write(ByteBuffers[] buffers)

Writes bytes from each of the buffers in the array buffers, to the file in sequence, starting at the channel's current file position.

write(ByteBuffers[] buffers,

int offset,

int length)

Writes data to the file starting at the channel's current file position from buffers[offset] to buffers[offset+length-1] inclusive and in sequence.

Both these methods can throw the same five exceptions as the write method for a single ByteBuffer object. The second of these methods can also throw an IndexOutOfBoundsException if offset or offset+length-1 is not a legal index value for the array buffers.

The data that is written from each buffer to the file is determined from that buffer's position and limit in the way we have seen. One obvious application of the gathering-write operation is when you are reading data from several different files into a number of buffers, and you want to merge the data into a single file. We can demonstrate how it works by using a variation on our primes writing program.

Try It Out – The Gathering Write

To simulate conditions where a gathering write could apply, we will set up the string length, the string itself, and the binary prime value in separate byte buffers. We will also write the prime string as bytes in the local encoding.

Here's the code:

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;

public class GatheringWrite {
  public static void main(String[] args) {
    int primesRequired = 100;   // Default count
    if (args.length > 0) {
      try {
        primesRequired = Integer.valueOf(args[0]).intValue();
      } catch (NumberFormatException e) {
        System.out.println("Prime count value invalid. Using default of "
                           + primesRequired);
      }
    } 

    long[] primes = new long[primesRequired];   // Array to store primes
    primes[0] = 2;                              // Seed the first prime
    primes[1] = 3;                              // and the second
    // Count of primes found – up to now, which is also the array index
    int count = 2;
    long number = 5;                            // Next integer to be tested

    outer:
    for (; count < primesRequired; number += 2) {

      // The maximum divisor we need to try is square root of number
      long limit = (long) Math.ceil(Math.sqrt((double) number));

      // Divide by all the primes we have up to limit
      for (int i = 1; i < count && primes[i] <= limit; i++) {
        if (number % primes[i] == 0) {   // Is it an exact divisor?
          continue outer;                // yes, try the next number

        }
      }
      primes[count++] = number;          // We got one!
    }

    File aFile = new File("C:/Beg Java Stuff/primes.txt");
    FileOutputStream outputFile = null;
    try {
      outputFile = new FileOutputStream(aFile);
    } catch (FileNotFoundException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    }
    FileChannel file = outputFile.getChannel();

    // Array of buffer references
    ByteBuffer[] buffers = new ByteBuffer[3];
    buffers[0] = ByteBuffer.allocate(8);   // To hold a double value
    buffers[2] = ByteBuffer.allocate(8);   // To hold a long value

    String primeStr = null;
    for (int primesWritten = 0; primesWritten < primes.length; 
         primesWritten++) {
      primeStr = "prime = " + primes[primesWritten];
      buffers[0].putDouble((double) primeStr.length()).flip();
      buffers[1] = ByteBuffer.allocate(primeStr.length());
      buffers[1].put(primeStr.getBytes()).flip();
      buffers[2].putLong(primes[primesWritten]).flip();
      try {
        file.write(buffers);
      } catch (IOException e) {
        e.printStackTrace(System.err);
        System.exit(1);
      } 
      buffers[0].clear();
      buffers[2].clear();
    }

    try {
      System.out.println("File written is " + file.size() + " bytes.");
      outputFile.close();   // Close the file and its channel
    } catch (IOException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    }

    System.out.println("File closed");
    System.exit(0);
  }
}

When you execute this, it should produce the output:

File written is 2671 bytes.
File closed

The length of the file is considerably less than before, since we are writing the string as bytes rather than Unicode characters. The only part of the code that is different is shaded so we'll concentrate on that.

How It Works

We will be using three byte buffers: one for the string length, one for the string itself, and one for the binary prime value:

    // Array of buffer references
    ByteBuffer[] buffers = new ByteBuffer[3];

We therefore create a ByteBuffer[] array with three elements to hold references to these. The buffers holding the string length and the prime value are fixed in length so we are able to create those straight away to hold 8 bytes each:

    buffers[0] = ByteBuffer.allocate(8);   // To hold a double value
    buffers[2] = ByteBuffer.allocate(8);   // To hold a long value

We have to create dynamically, the buffer to hold the string, inside the for loop that iterates over all the prime values we have in the primes array.

After assembling the prime string, we transfer the length to the first buffer:

      buffers[0].putDouble((double) primeStr.length()).flip();

Note that we flip the buffer in the same statement after the data value has been transferred, so it is set up ready to be written to the file.

Next, we create the buffer to accommodate the string, load the byte array equivalent of the string and flip the buffer:

      buffers[1] = ByteBuffer.allocate(primeStr.length());
      buffers[1].put(primeStr.getBytes()).flip();

All of the put() methods for the byte buffers we are using here automatically update the buffer position, so we can flip each buffer as soon as the data is loaded. An alternative to allocating this byte buffer directly to accommodate the byte array from the string, is to call the static wrap() method in the ByteBuffer class that wraps a byte array. We could achieve the same as the previous two statements with the single statement:

      buffers[1] = ByteBuffer.wrap(primeStr.getBytes());

Since the wrap() method creates a buffer with a capacity the same as the length of the array, and with the position set to zero and the limit to the capacity, we don't need to flip the buffer – it is already in a state to be written.

The three buffers are ready so we write the array of buffers to the file like this:

      try {
        file.write(buffers);
      } catch (IOException e) {
        e.printStackTrace(System.err);
        System.exit(1);
      }

Finally, we ready the first and third buffers for the next iteration by calling the clear() method for each of them:

      buffers[0].clear();
      buffers[2].clear();

Of course, the second buffer is recreated on each iteration, so there is no need to clear it. Surprisingly easy wasn't it?

Previous Next
JavaScript Editor Java Tutorials Free JavaScript Editor