IBM Skip to main content
Search for:   within 
      Search help  
     IBM home  |  Products & services  |  Support & downloads   |  My account

developerWorks > Java technology
developerWorks
Magic with Merlin: The ins and outs of Merlin's new I/O buffers
Discuss70KBe-mail it!
Contents:
Buffer foundations
Buffer types
Direct versus nondirect
Memory-mapped files
Conclusion
Resources
About the author
Rate this article
Related content:
Merlin brings nonblocking I/O to the Java platform
Working XML: Wrestling with Java NIO
Turning streams inside out, Part 2: Optimizing internal Java I/O
Magic with Merlin series
Subscriptions:
dW newsletters
dW Subscription
(CDs and downloads)
Learn how to manipulate J2SE 1.4's new I/O package

Level: Introductory

John Zukowski (mailto:jaz@zukowski.net?cc=&subject=The ins and outs of Merlin's new I/O buffers)
President, JZ Ventures, Inc.
25 March 2003

Column iconA 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.

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

About the author
John Zukowski John 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 mailto:jaz@zukowski.net?Subject=Magic with Merlin.


Discuss70KBe-mail it!

What do you think of this document?
Killer! (5) Good stuff (4) So-so; not bad (3) Needs work (2) Lame! (1)

Send us your comments or click Discuss to share your comments with others.



developerWorks > Java technology
developerWorks
  About IBM  |  Privacy  |  Terms of use  |  Contact