HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

In the course of teaching my Essential Java course, my student's attitudes towards low-level exceptions always amaze me. Most notably, my student's instinctive abhorence of ArrayIndexOutOfBoundsExceptions and NullPointerExceptions. They just hate these types of exceptions, and even the most experienced programmers who are learning Java for the first time will tell me, "A good program will never have array exceptions or null reference exceptions in it."

Typically these students will write code that goes like this:

Try {
// some array operation
} (catch (ArrayIndexOutOfBoundsException aioob) {
// Something's horribly wrong!
return;
} catch (NullPointerException npe) {
// Yikes!
return;
}

You probably recognize the pattern and I bet you use it yourself. Well, I'm writing this month to show you how this is actually wrong. There are better ways to use your exceptions, ways that are more optimized, make your program run faster and make your program more stable.

I guess I can understand the assumptions my students are making. They figure that the whole point of array exceptions is to tell you that you've run past the end (or, using a negative index, tried to go previous to the beginning) of an array. In the C/C++ world especially, this almost inevitably will lead to disastrous results. Random memory will be written over, or random values will be read from memory, which is almost never a good thing.

But in Java, we have this great mechanism already in place that will detect array overruns for us. Rather than just bail from a program or applet, why not use these exceptions to our advantage? For example, look at Listing 1. This listing is a relatively simple implementation of an expandable array class.

What's unique about this implementation is the lack of any sort of pro-active array re-allocation code. You would expect there to be some sort of If..Then statement at the beginning of the add() method. IF the array is not large enough to hold one more Object reference, THEN re-allocate the array and try again. Instead, I've implemented the add() method to blindly add a new element to the end of an array using an incremented counter. If the array has not been allocated at all yet, then this will cause a NullPointerException to be thrown. If the array does exist, but is not large enough to hold one more Object reference, then an ArrayIndexOutOfBoundsException is thrown.

In either case, I've set up exception handlers to deal appropriately with either type of exception. In the event an ArrayIndexOutOfBounds exception is thrown, my handler will re-allocate the array so that it is large enough to hold the new array element. If a NullPointerExcetpion is thrown, then I have my handle allocate an initial array.

There are two distinct advantages to implementing my ExpandableArray class this way. First, the code is easier to follow. The absence of any If..Then blocks in my add() method means the "meat" of the method is the two lines of code in the try block. Reallocation of the array and initial allocation of the array are clearly delineated from each other in separate catch blocks.

The second advantage is that this code will run faster than any implementation that uses an initial If..Then check. There are simply fewer atomic bytecode operations to perform in this implementation. There is some overhead associated with catching exceptions that is not present in the If..Then implementation, but by setting the class' increment constant sufficiently high, then exceptions will not be thrown all that often.

Use of ArrayIndexOutOfBoundsExceptions to control arrays is a good thing, but it is only applicable where arrays are present. Many storage mechanisms don't use arrays at all. Instead, they use node structures in a graph. Examples include a linked list, a binary tree, a graph, and so on. For these types of structures, you can use exceptions also to make your structure work faster.

Listing 2 is my implementation of a BinaryTree class. Note that this implementation assumes your objects implement a Comparable interface, which allows you to compare two Comparable objects to determine which is less than, and which is greater than, the other. Details of the implementation of this interface for your specific classes are left to the reader. Note also that the nodes in this binary tree are nothing more than three-element arrays of Objects. The first element is a Comparable object, the second is a left-child reference and the third is a right-child reference. For such a simple storage class, I didn't think I needed to make a Node class in addition to the BinaryTree class.

What's missing from this class' implementation of an add() method, just like in the ExpandableArray class, is any sort if ence to a right-child or left-child. In this case, I know I've come to a leaf, and the new node should be created and a reference to it made.

The add() method uses recursion to find its way through the tree to the appropriate spot for a new node. Recursion is nice because it allows us, with few lines of code, to do some pretty complex operations. In a deep tree, the call stack is going to be pretty large. Unravelling the call stack after an insertion takes place can be a somewhat expensive operation. For example, if the recursion is 14 layers deep, then unravelling the call stack will take at least 14 individual "return" operations to the VM. 100 layers deep would mean 100 separate return operations.

Luckily, we can use exceptions to cut the unravelling of the stack into a single step. A much faster step. Listing 3 is my implementation of a fast-unravelling BinaryTree class' private overloaded Add() and public add() methods. The private overloaded Add() method is the workhorse of the two. Its implementation is pretty much the same as the implementation of the add() method in Listing 2. One slight difference: When a new node is added to the BinaryTree, the overloaded Add() method throws a new Exception object.

To see how this is helpful, take a look at the public add() method implementation in Listing 3. The implementation calls the other Add() method within a try/catch block. The idea is that add() will catch the Exception object thrown after a new node has been added to the BinaryTree. Instead of 14 (or 100, or whatever) separate "return" operations on an interpreted VM level, an optimised VM will be able to unravel the stack in a single step. This will make adding new items to the tree faster.

I hope these three examples start fertilizing your mind with new ways to use exceptions in Java. The Exception mechanism is so much more than just a fancy error handler. It is another tool you can use to make better code. That is, better organized code, more stable code and faster code.

I'm sure I'm just touching upon all the unique and powerful ways you could use exceptions to make your programming task easier, better, faster and more stable. I encourage you to experiment with using Exceptions in novel ways within your Java code. Please e-mail me with your code examples, and I'll add a section to either the next Tips and Tricks column or the one after that with the best examples sent to me. Really, send your ideas because we'd all like to become better Java programmers.

About the Author
Brian Maso is a programming consultant working out of Portland, OR. He is an instructor of DevelopMentors "Essential Java" course for programming professionals and is the co-author of The Waite Group Press's recent release, "The Java API SuperBible" and Osborne/McGraw-Hill's "The Visual J++ Handbook". Before Java, he spent five years corralled in the MS Windows branch of programming, working for such notables as the Hearst Corp., First DataBank and Intel. Readers are encouraged to contact Brian via e-mail with any comments or questions at [email protected]

	

Listing 1: An ExpandableArray class implementation

class ExpandableArray {
    private Object[] aObjs = null;
    private int counter = 0;
    private static final int increment = 10;

    public void add(Object o) {
        do {
            try {
                aObjs[counter++] = o;
                return;
            } catch (NullPointerException npe) {
                // Need to allocate array
                aObjs = new Object[increment];
                counter--;
            } catch (ArrayIndexOutOfBounds aioob) {
                // Need to expand the array
               Object[] ao = new Object[aObjs.length + increment];
                System.arraycopy(aObjs, 0, ao, 0, aObjs.length);
                aObjs = ao;
                counter--;
            }
        } while(true);
    }

    public Object get(int index) {
        try {
            return aObjs[index];
        } catch(Exception e) {
            // Either unallocated array or index illegal
            return null;
        }
    }
}

Listing 2: A BinaryTree class

class BinaryTree {
    Object[] head;

    public void add(Comparable c) {
        try {
            add(c, head);
        } catch (NullPointerException npe) {
            head = new Object[3];
            head[0] = c;
        }
    }

    private void add(Comparable c, Object[] node) {
        if(c.lessThan((Comparable)node[0]))
            try {
                add(c, (Object[])node[1]);
            } catch (NullPointerException npel) {
                node[1] = new Object[3];
                ((Object[])node[1])[0] = c;
            }
        else
            try {
                add(c, (Object[])node[2]);
            } catch (NullPointerException npel) {
                node[2] = new Object[3];
                ((Object[])node[2])[0] = c;
            }
        }
    }
}

Listing 3: New overloaded add() methods for fast unravelling after recursion

public void add(Comparable c) {
    try {
        add(c, head);
    } catch (NullPointerException npe) {
        head = new Object[3];
        head[0] = c;
    } catch (Exception e) {
        // Thrown to unravel the stack
    }
}

private void add(Comparable c, Object[] node)
        throws Exception {
    if(c.lessThan((Comparable)node[0]))
        try {
            add(c, (Object[])node[1]);
        } catch (NullPointerException npel) {
            node[1] = new Object[3];
            ((Object[])node[1])[0] = c;
            throw new Exception();
        }
    else
        try {
            add(c, (Object[])node[2]);
        } catch (NullPointerException npel) {
            node[2] = new Object[3];
            ((Object[])node[2])[0] = c;
            throw new Exception();
        }
    }
}


 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.