Sun Java Solaris Communities My SDN Account Join SDN
 
Technical Articles and Tips

File Channels and Stack Trace Elements

 

Tech Tips archive


WELCOME to the Java Developer Connection (JDC) Tech Tips, May 07, 2002. This issue covers:

These tips were developed using Java 2 SDK, Standard Edition, v 1.4.

Pixel

FILE CHANNELS

Channels are a new Java library feature, part of the new I/O package (java.nio). The documentation for channels defines a channel like this:

A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.

The java.nio package has many important features in it, including buffers, file channels, other kinds of channels such as socket channels, and extensible interfaces. In this tip, the focus is on file channels only. An important benefit of using file channels is additional I/O functionality in your applications. This tip presents several examples of what this benefit means in practice.

Suppose that you have some legacy data in binary files, and this data includes some 16-bit numbers written by the following C program:

    /* cd1.c */
    
    #include <stdio.h>
    #include <assert.h>
    
    short data[] = {1234, 2345, 3456, 4567, 5678};
    
    #define SIZESHORT 2
    
    int main()
    {
        int i, j;
    
        /* open data file for writing */
    
        FILE* fp = fopen("data", "wb");
        assert(fp);
   
        /* write out each short value, low byte first */
    
        for (i = 0; i < sizeof(data) / sizeshort; i++) 
            {
            short item = data[i];
            for (j = 0; j < sizeshort; j++) {
                char c = (char)(item & 0xff);
                fputc(c, fp);
                item >>= 8;
            }
        }
    
        fclose(fp);
    
        return 0;
    }

This program writes five 16-bit values to a file. Each value is written as two bytes, with the low byte written first (this ordering of low byte first is referred to as "little-endian"). Compile, link, and run the C program to produce the data file.

How do you read this data using a Java program? Here's one way:

    import java.io.*;
    
    public class ChannelDemo0 {
        public static void main(String args[]) 
                                   throws IOException {
            FileInputStream fis = 
                           new FileInputStream("data");
            DataInputStream dis = 
                              new DataInputStream(fis);
            short s = dis.readShort();
            System.out.println(s);
            dis.close();
        }
    }

This program uses the DataInputStream class and the readShort method of that class. Unfortunately, however, if you run the ChannelDemo0 program, the result is:

-11772

This does not correspond to the first value (1234) written into the data file. The problem is that the C program writes the short values as low byte / high byte, and the readShort method expects high byte / low byte.

How do you solve this problem? Here's another approach, one that reads the values from the data file and computes their sum:

    import java.nio.*;
    import java.nio.channels.*;
    import java.io.*;
    
    public class ChannelDemo1 {
    
        // sum the values of short data items in a file
    
        static short sumFileContents(String fn) 
                                   throws IOException {
    
            // open input stream and get channel
    
            FileInputStream fis = 
                               new FileInputStream(fn);
            FileChannel fc = fis.getChannel();
    
            // map the file into a byte buffer
    
            MappedByteBuffer mbb = fc.map(
                      FileChannel.MapMode.READ_ONLY, 0, 
                      fc.size());
    
            // set byte order to be little-endian
    
            mbb.order(ByteOrder.LITTLE_ENDIAN);
    
            // get short view buffer of byte buffer
    
            ShortBuffer sb = mbb.asShortBuffer();
    
            // sum up the values
    
            short sum = 0;
            while (sb.hasRemaining()) {
                sum += sb.get();
            }
    
            // finish up
    
            fc.close();
            fis.close();
    
            return sum;
        }
    
        public static void main(String args[]) 
                                   throws IOException {
            short sum = sumFileContents("data");
            System.out.println(sum);
        }
    } 

The sumFileContents method first creates a FileInputStream, and then gets a file channel based on this stream. A similar approach is used for output streams (FileOutputStream) or for files that are open for both reading and writing (RandomAccessFile).

After the method gets the channel, it maps the data file into a MappedByteBuffer. This means that the buffer's content is exactly the file's content, so reading the buffer fetches bytes from the file and writing the buffer stores bytes into the file.

The byte buffer is then specified to be little-endian, changing the default order from big-endian. The method then creates a short "view buffer" on the byte buffer. The view buffer presents a view of the mapped byte buffer as a sequence of short (16-bit) values. The short values are each composed of two bytes, and the low-order byte is assumed to come first, so as to match what the C program writes. The view buffer is backed by the byte buffer. So what the method is doing is creating a view of the data file as a sequence of little-endian short values. The final step in the method is to sum up the values.

Run the ChannelDemo1 program. It should produce the result:

17280 

Let's go on and look at another example of mapping files. Suppose you want to reverse the bytes in a file. How can you do this? One simple way uses the following approach:

    import java.nio.*;
    import java.nio.channels.*;
    import java.io.*;
    
    public class ChannelDemo2 {
        public static void main(String args[]) 
                                   throws IOException {
    
            // check command-line argument
    
            if (args.length != 1) {
                System.err.println(
                              "missing file argument");
                System.exit(1);
            }
    
            // get channel
    
            RandomAccessFile raf =
                new RandomAccessFile(args[0], "rw");
            FileChannel fc = raf.getChannel();
    
            // map file to buffer
    
            MappedByteBuffer mbb = fc.map(
                     FileChannel.MapMode.READ_WRITE, 0, 
                     fc.size());
    
            // reverse bytes of file
     
            int len = (int)fc.size();
            for (
               int i = 0, j = len - 1; i < j; i++, j--) 
               {
                byte b = mbb.get(i);
                mbb.put(i, mbb.get(j));
                mbb.put(j, b);
               }
    
            // finish up
    
            fc.close();
            raf.close();
        }
    }

This program opens a channel based on a RandomAccessFile object, and maps the file for reading and writing. It then sets up a loop that exchanges bytes starting at the two ends of the buffer, and works toward the middle. Because the MappedByteBuffer is mapped to the file on disk, changes to the buffer are reflected in the file. If you enter the commands:

    javac ChannelDemo2.java

    java ChannelDemo2 ChannelDemo2.java

and then look at the text of ChannelDemo2.java, you will find that all of the bytes in ChannelDemo2.java are reversed. In other words, the first line of the file will be:

} 

and the last line of the file will be:

;*.oin.avaj tropmi

To restore the ChannelDemo2.java file to its original byte order, you need to enter the command:

    java ChannelDemo2 ChannelDemo2.java

Mapping a file can be very useful, but it doesn't necessarily free you from doing additional work. For example, suppose that you map a file to get convenient access to the last few bytes of the file. There is a certain amount of intrinsic work that you need to do in this case, such as doing a disk seek to the end of the file, and reading a file block into memory. There's no getting around this work. But mapping is certainly convenient, and sometimes it can be faster than the alternatives. For example, it can help you avoid system call overhead or buffer copies.

Let's look at several ways of copying one file to another using file channel features. The first approach is another example of mapped files:

    import java.nio.*;
    import java.nio.channels.*;
    import java.io.*;
    
    public class ChannelDemo3 {
        public static void main(String args[]) 
                                   throws IOException {
    
            // check command-line arguments
    
            if (args.length != 2) {
                System.err.println("missing filenames");
                System.exit(1);
            }
    
            // get channels
  
            FileInputStream fis = 
                          new FileInputStream(args[0]);
            FileOutputStream fos = 
                         new FileOutputStream(args[1]);
            FileChannel fcin = fis.getChannel();
            FileChannel fcout = fos.getChannel();
    
            // map input file
    
            MappedByteBuffer mbb = fcin.map(
                     FileChannel.MapMode.READ_ONLY, 0, 
                     fcin.size());
    
            // do the file copy
    
            fcout.write(mbb);
    
            // finish up
    
            fcin.close();
            fcout.close();
            fis.close();
            fos.close();
        }
    }


In this example, the input file channel is mapped to a buffer, and then that buffer is written to the output channel. Because the buffer represents the whole file, writing the buffer is equivalent to copying the file. So if you enter the commands:

    javac ChannelDemo3.java

    java ChannelDemo3 ChannelDemo3.java ChannelCopy3.java

It copies the ChannelDemo3 file to ChannelCopy3.java.

Another way of copying a file looks like this:

    import java.nio.*;
    import java.nio.channels.*;
    import java.io.*;
    
    public class ChannelDemo4 {
        public static void main(String args[]) 
                                   throws IOException {
    
            // check command-line arguments
    
            if (args.length != 2) {
                System.err.println("missing filenames");
                System.exit(1);
            }
    
            // get channels
    
            FileInputStream fis = 
                          new FileInputStream(args[0]);
            FileOutputStream fos = 
                         new FileOutputStream(args[1]);
            FileChannel fcin = fis.getChannel();
            FileChannel fcout = fos.getChannel();
    
            // do the file copy
    
            fcin.transferTo(0, fcin.size(), fcout);
    
            // finish up
    
            fcin.close();
            fcout.close();
            fis.close();
            fos.close();
        }
    }

The transferTo method transfers bytes from the source channel (fcin) to the specified target channel (fcout). The transfer is typically done without explicit user-level reads and writes of the channel. The documentation for the transferTo method says:

This method is potentially much more efficient than a simple loop that reads from this channel and writes to the target channel. Many operating systems can transfer bytes directly from the filesystem cache to the target channel without actually copying them.

In other words, transferTo may rely on special operating system features that support very fast file transfers.

What does a "regular" file copy look like using file channels? Here's an example:

    import java.io.*;
    import java.nio.*;
    import java.nio.channels.*;
   
    public class ChannelDemo5 {
        public static void main(String args[]) 
                                   throws IOException {
    
            // check command-line arguments
    
            if (args.length != 2) {
                System.err.println("missing filenames");
                System.exit(1);
            }
    
            // get channels
    
            FileInputStream fis = 
                           new FileInputStream(args[0]);
            FileOutputStream fos = 
                          new FileOutputStream(args[1]);
            FileChannel fcin = fis.getChannel();
            FileChannel fcout = fos.getChannel();
    
            // allocate buffer
    
            ByteBuffer buf = 
                        ByteBuffer.allocateDirect(8192);
    
            // do copy
    
            long size = fcin.size();
            long n = 0;
            while (n < size) {
                buf.clear();
                if (fcin.read(buf) < 0) {
                    break;
                }
                buf.flip();
                n += fcout.write(buf);
            }
    
            // finish up
    
            fcin.close();
            fcout.close();
            fis.close();
            fos.close();
        }
    }

This program copies one file to another, one buffer at a time. Before it reads each chunk of the input file into the buffer, the program "clears" the buffer. This makes the buffer ready for reading by setting the position to 0 and the limit to the capacity. Then, after each read, the program "flips" the buffer. This makes the buffer ready for writing by setting the limit to the current position and the current position to 0.

At this point, you've seen some of the new features offered by file channels. There are other features, such as locking, that are also important to learn about. Beyond the new features, file channels offer significant I/O performance gains. For more information about file channels, see New I/O APIs.

Pixel

STACK TRACE ELEMENTS

The standard Java library has long had a mechanism for displaying stack tracebacks using the Throwable.printStackTrace method. This method is used to dump the program context for uncaught exceptions. The trace is printed on the System.err stream or on a specified PrintStream or PrintWriter.

A new library feature gives you programmatic access to stack tracebacks. You can retrieve an array of StackTraceElement objects, with each object representing a single stack frame in a trace. Let's look at an example to see how this works:

    class A {
        B bref;
    
        void f() {
            bref.g();
        }
    }
    
    class B {
        void g() {}
    }
    
    class C {
        String str;
        int len = str.length();
    }
   
    public class TraceDemo1 {
    
        // dump a single stack trace element
    
        static void dumpTraceElement(
                               StackTraceElement ste) {
            System.err.println("filename = " +
                ste.getFileName());
            System.err.println("line number = " +
                ste.getLineNumber());
            System.err.println("class name = " +
                ste.getClassName());
            System.err.println("method name = " +
                ste.getMethodName());
            System.err.println("is native method = " +
                ste.isNativeMethod());
        }
    
        // dump an array of stack trace elements, 
        // most recent first
    
        static void dumpTrace(Throwable e) {

            // display exception

            System.err.println("Exception: " + e);
            System.err.println();
    
            // display traceback

            StackTraceElement ste[] = e.getStackTrace();
            for (int i = 0; i < ste.length; i++) {
                dumptraceelement(ste[i]);
                system.err.println();
            }
        }
    
        public static void main(string args[]) {
    
            // call a.f() and trigger an exception
    
            try {
                a aref = new a();
                aref.f();
            }
            catch (throwable e) {
    
                // display regular stack trace
    
                e.printstacktrace();
                system.err.println();
    
                // dump stack trace in custom format
    
                dumptrace(e);
            }
    
            system.err.println();
            system.err.println(
                     "==============================");
            system.err.println();
    
            // trigger an exception in initialization
    
            try {
                new c();
            }
            catch (throwable e) {
                dumptrace(e);
            }
        }
    }

In this example, the TraceDemo1 program first calls the A.f method. That method, in turn, calls B.g. Unfortunately, when B.g is called, the B object reference is null. This triggers an exception.

The first thing the program prints is the regular stack trace, which looks like this:

    java.lang.NullPointerException
        at A.f(TraceDemo1.java:5)
        at TraceDemo1.main(TraceDemo1.java:61)

Then it displays a customized stack traceback, with the exception name and a sequence of StackTraceElements:

    Exception: java.lang.NullPointerException
    
    filename = TraceDemo1.java
    line number = 5
    class name = A
    method name = f
    is native method = false
    
    filename = TraceDemo1.java
    line number = 61
    class name = TraceDemo1
    method name = main
    is native method = false

Notice that the filename, class name, and method name of the first StackTraceElement refer to the point of the exception. The exception occurred at line 5 of TraceDemo1.java, within method A.f.

The second part of the example shows what happens when an exception is thrown during instance initialization. In this example, the TraceDemo1 program creates a C object. When the C object is instantiated, it attempts to take the length of the str string. However, because str is never explicitely initialized, the reference to str is a null reference. Here's the output for that part of the example:

    Exception: java.lang.NullPointerException
    
    filename = TraceDemo1.java
    line number = 15
    class name = C
    method name = <init>
    is native method = false
    
    filename = TraceDemo1.java
    line number = 83 
    class name = TraceDemo1
    method name = main
    is native method = false


The point of the exception is line 15 of TraceDemo1.java, in the C class, in a method called <init>, which is a special name for instance initialization methods within the Java virtual machine*.

You can use the StackTraceElement feature to implement customized exception reporting and logging formats. An example of a customized logging format is one that limits the stack trace to a manageable depth. For example, if there are 500 stack frames, you might want to preserve the first ten and the last ten. The StackTraceElement information is part of the serialized representation of Throwable class instances, so it's available for deserialized objects.

For more information about stack trace elements, see the description of the StackTraceElement class. Also, see section 10.12, Threads and Exceptions, in "The Java Programming Language Third Edition" by Arnold, Gosling, and Holmes.

Pixel

IMPORTANT: Please read our Terms of Use and Privacy policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html

FEEDBACK
Comments? Send your feedback on the JDC Tech Tips to:
jdc-webmaster@sun.com

SUBSCRIBE/UNSUBSCRIBE
- To subscribe, go to the subscriptions page, choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".

- ARCHIVES
You'll find the JDC Tech Tips archives at:
http://java.sun.com/jdc/TechTips/index.html

- COPYRIGHT
Copyright 2002 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.

This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html

This issue of the JDC Tech Tips is written by Glen McCluskey.

JDC Tech Tips
May 07, 2002

Sun, Sun Microsystems, Java, and Java Developer Connection are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.

* As used in this document, the terms "Java virtual machine" or "JVM" mean a virtual machine for the Java platform.