Magic with Merlin: The ins and outs of Merlin's new I/O buffers

Learn how to manipulate J2SE 1.4's new I/O package

A series of buffer classes underpins the Java 2 Platform Standard Edition's new I/O (NIO) packages. The classes' data containers form the foundation of other NIO operations like nonblocking reads over socket channels. In this month's Magic with Merlin, resident Java programming wizard John Zukowski shows how to manipulate those data buffers for such tasks as reading/writing primitives and working with memory-mapped files. In a later article, he'll expand on the concepts presented here to work with the socket channels.

Share:

John Zukowski (jaz@zukowski.net), President, JZ Ventures, Inc.

Author photoJohn Zukowski conducts strategic Java consulting with JZ Ventures, Inc. and serves as the resident guru for a number of jGuru's community-driven Java FAQs. His latest books are Learn Java with JBuilder 6 from Apress and Mastering Java 2: J2SE 1.4 from Sybex. Contact John at jaz@zukowski.net.



25 March 2003

Also available in Japanese

With Java 2 Platform Standard Edition (J2SE) 1.4 came numerous changes to the Java platform's I/O handling capabilities. Instead of just the previous J2SE releases' continual support for stream-based I/O operations with chaining from stream to stream, Merlin adds new capabilities in what is dubbed the New I/O classes (NIO), which now reside in the java.nio package.

I/O performs input and output operations to transfer data from things like files or the system console into or out of an application. (See Resources for additional information on Java I/O.)

Buffer foundations

The abstract Buffer class forms the foundation of the java.nio package's buffer support. Buffer works like an in-memory RandomAccessFile for reading and writing primitive data types. Like a RandomAccessFile, with Buffer the next operation you perform (read/write) happens at your position. Performing either operation changes that position, so doing a read after a write doesn't read what was just written, but what is after what was just written. Buffer offers four indicators for access to the linear structure (from highest value to lowest):

  • capacity(): Indicates the buffer's size
  • limit(): Tells you how many bytes you've stuffed into the buffer so far, or lets you change that limit with :limit(int newLimit):
  • position(): Tells you the current location to perform the next read/write operation
  • mark(): Lets you remember a position for later resetting with reset()

A buffer's basic operations are get() and put(); those methods, however, are type-specific in subclasses. Having said that, let's examine a quick example to demonstrate writing and reading a char from the same buffer. In Listing 1, the flip() method swaps the limit and position, then sets the position to zero, discarding the mark, letting you read what was just written:

Listing 1. Reading/writing example
import java.nio.*;
...

CharBuffer buff = ...;
buff.put('A');
buff.flip();
char c = buff.get(); 
System.out.println("An A: " + c);

Now let's examine the specific Buffer subclasses.


Buffer types

Merlin features seven specific Buffer types, one for each primitive data type (except boolean):

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

I'll discuss an eighth type, MappedByteBuffer, available for memory-mapped files, later in this article. If you must work with something other than primitives, you can acquire bytes from a ByteBuffer, then convert them into an Object or whatever else.

As previously mentioned, each buffer includes get() and put() methods that offer type-safe versions. Typically, these get() and put() methods are overloaded. For example, with CharBuffer you can get() the next char, get(int index) the char at a specific position, or get(char[] destination) a bunch of chars. Static methods can also create the buffers, because no constructors exist. So, sticking with the CharBuffer example, you can convert a String object to a CharBuffer with CharBuffer.wrap(aString). To demonstrate, Listing 2 takes the first command-line argument, converts it to a CharBuffer, and displays each char in the argument:

Listing 2. CharBuffer demonstration
import java.nio.*;

public class ReadBuff {
  public static void main(String args[]) {
     if (args.length != 0) {
      CharBuffer buff = CharBuffer.wrap(args[0]);
      for (int i=0, n=buff.length(); i<n; i++) {
        System.out.println(i + " : " + buff.get());
      }
    }
  }
}

Notice that I use get(), not get(index). I do so because after each get() operation the position is moved, so you needn't manually state which position to retrieve.


Direct versus nondirect

Now that you've seen the typical buffer, let's examine the difference between direct and nondirect buffers. When you create a buffer, you can request a direct buffer, which costs more to create than nondirect buffers but lets the runtime environment use quicker native I/O operations directly upon the buffer. Because of the added creation cost, use direct buffers only for long-lived buffers, not short, one-time, throwaway buffers. Furthermore, you can create direct buffers only at the ByteBuffer level, where you then must convert the Buffer to the more specific type if you want to treat the contents as a different type. To demonstrate, Listing 3 duplicates the code in Listing 2's behavior, but uses a direct buffer:

Listing 3. List network interfaces
import java.nio.*;

public class ReadDirectBuff {
  public static void main(String args[]) {
     if (args.length != 0) {
      String arg = args[0];
      int size = arg.length();
      ByteBuffer byteBuffer = ByteBuffer.allocateDirect(size*2);
      CharBuffer buff = byteBuffer.asCharBuffer();
      buff.put(arg);
      buff.rewind();
      for (int i=0, n=buff.length(); i<n; i++) {
        System.out.println(i + " : " + buff.get());
      }
    }
  }
}

In the code above, notice that you can't just wrap the String into a direct ByteBuffer. You first must create the buffer, fill it, then rewind the position to the beginning so you can read from the start. Also remember that chars are twice as wide as bytes, hence the size*2 in the example.


Memory-mapped files

The eighth Buffer type, MappedByteBuffer, is simply a special ByteBuffer. MappedByteBuffer maps a region of a file directly in memory. Typically, that region comprises the entire file, although it could map a portion. You must, therefore, specify what part of the file to map. Moreover, as with the other Buffer objects, no constructor exists here; you must ask the java.nio.channels.FileChannel for its map() method to get a MappedByteBuffer. Further, without delving too far into channels, you can just get the FileChannel from a FileInputStream or a FileOutputStream with the getChannel() method. Listing 4 demonstrates MappedByteBuffer by reading the text file's contents in which the filename is passed in from the command line:

Listing 4. Read a memory-mapped text file
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;


public class ReadFileBuff {
  public static void main(String args[]) throws IOException {
     if (args.length != 0) {
      String filename = args[0];
      FileInputStream fis = new FileInputStream(filename);
      FileChannel channel = fis.getChannel();
      int length = (int)channel.size();
      MappedByteBuffer byteBuffer =
        channel.map(FileChannel.MapMode.READ_ONLY, 0, length);
      Charset charset = Charset.forName("ISO-8859-1");
      CharsetDecoder decoder = charset.newDecoder();
      CharBuffer charBuffer = decoder.decode(byteBuffer);
      for (int i=0, n=charBuffer.length(); i<n; i++) {
        System.out.print(charBuffer.get());
      }
    }
  }
}

As I explained in "Character sets" (see Resources), because the file has content, you must tell the system how to convert the bytes to characters. Hence the need to use Charset.


Conclusion

J2SE's new I/O package, underpinned by Buffer, fundamentally changes the way Java technology handles I/O operations. After reading this article, you should understand NIO, from basic get and put operations to reading a memory-mapped file. That, however, should not end your NIO education. Use the resources mentioned here to read up on I/O in the Java platform. In a future article, I'll relate the concepts presented here to working with the socket channels.

Resources

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=10788
ArticleTitle=Magic with Merlin: The ins and outs of Merlin's new I/O buffers
publish-date=03252003