Java Tip 124: Trace your steps in Java 1.4

Locate runtime code with getStackTrace()

I don't know about you, but I really like to know where I am. Being a guy, I'm never lost, but sometimes I just don't know where I am. Some places, such as malls, have maps with "You Are Here" indicators. Similarly, Java now lets us figure out our location with the system's help. In this tip, I'll show you how to extract this location information from the system in a consistent and reliable manner.

I firmly believe the runtime system should provide enough metadata about the system itself so programs can make better decisions and complete tasks. Java has been able to introspect and reflect on classes for some time, but until now it has lacked the simple ability to map runtime code back to its position in the source code file. The pre-Java 1.4 solution was to manually parse an exception's stack trace. Now, with Java 1.4, we have a better solution.

Pull apart a stack trace?

The pre-Java 1.4 workaround to gathering location information was to manually parse an exception's printStackTrace() output. Here's an example of a simple stack trace:

java.lang.Throwable
        at boo.hoo.StackTrace.bar(StackTrace.java:223)
        at boo.hoo.StackTrace.foo(StackTrace.java:218)
        at boo.hoo.StackTrace.main(StackTrace.java:54)

Pulling the above code apart is not a major parsing problem. But how about this?

java.lang.Throwable
        at
boo.hoo.StackTrace$FirstNested$SecondNested.<init>(StackTrace.java:267)
        at boo.hoo.StackTrace$FirstNested.<init>(StackTrace.java:256)
        at boo.hoo.StackTrace.<init>(StackTrace.java:246)
        at boo.hoo.StackTrace.main(StackTrace.java:70)

Ugh. What does all that weird goobley-guk really mean, and why on earth should I have to parse it? Obviously, the system already tracks that location information, since it's able to build those stack traces. So why isn't that location information available directly? Well, with Java 1.4, it finally is.

In addition, keep in mind that in the face of JIT (just-in-time) compilers and dynamic, optimizing compilers like Sun Microsystems' HotSpot, file and line number information may not exist. The goal of "performance or bust" can certainly be bothersome.

Java 1.4 Throwable to the rescue!

After tolerating years of complaints, Sun Microsystems has finally extended the java.lang.Throwable class with the getStackTrace() method. getStackTrace() returns an array of StackTraceElements, where each StackTraceElement object provides the means to more or less directly extract location information.

To acquire that mapping information, you still create a Throwable instance at the point of interest in your code:

    //...
    public static void main (String[] args)
        {
        Throwable ex = new Throwable();
        //...

This code positions that point at the start of main().

Of course, it's useless to just gather that information without doing something with it. For this tip, we will use each underlying method of the StackTraceElements to extract and display all the information we can.

The example program, StackTrace.java, shows how to extract the location information with several examples. You will need the J2SE (Java 2 Platform, Standard Edition) 1.4 SDK to compile and run the example program.

To extract and display the mapping information, the example code uses a helper method, displayStackTraceInformation(), with the following basic usage idiom:

    //...
    public void crashAndBurnout()
        {
        //...
        displayStackTraceInformation (new Throwable());
        //...
        }
    //...

The displayStackTraceInformation() code is pretty straightforward:

    public static boolean displayStackTraceInformation (Throwable ex,
                                                        boolean displayAll)
        {
        if (null == ex)
            {
            System.out.println ("Null stack trace reference! Bailing...");
            return false;
            }
        System.out.println ("The stack according to printStackTrace():\n");
        ex.printStackTrace();
        System.out.println ("");
        StackTraceElement[] stackElements = ex.getStackTrace();
        if (displayAll)
            {
            System.out.println ("The " + stackElements.length +
                                " element" +
                                ((stackElements.length == 1) ? "": "s") +
                                " of the stack trace:\n");
            }
        else
            {
            System.out.println ("The top element of a " +
                                stackElements.length +
                                " element stack trace:\n");
            }
        
        for (int lcv = 0; lcv < stackElements.length; lcv++)
            {
            System.out.println ("Filename: " +
                                stackElements[lcv].getFileName());
            System.out.println ("Line number: " +
                                stackElements[lcv].getLineNumber());
            String className = stackElements[lcv].getClassName();
            String packageName = extractPackageName (className);
            String simpleClassName = extractSimpleClassName (className);
            System.out.println ("Package name: " +
                                ("".equals (packageName)?
                                "[default package]" : packageName));
            System.out.println ("Full class name: " + className);
            System.out.println ("Simple class name: " + simpleClassName);
            System.out.println ("Unmunged class name: " +
                                unmungeSimpleClassName (simpleClassName));
            System.out.println ("Direct class name: " +
                                extractDirectClassName (simpleClassName));
            System.out.println ("Method name: " +
                                stackElements[lcv].getMethodName());
            System.out.println ("Native method?: " +
                                stackElements[lcv].isNativeMethod());
            System.out.println ("toString(): " +
                                stackElements[lcv].toString());
            System.out.println ("");
            if (!displayAll)
                return true;
            }
        System.out.println ("");
        return true;
        }       // End of displayStackTraceInformation().

Basically, we call getStackTrace() on the passed-in Throwable, and then loop through the individual StackTraceElements, extracting as much mapping information as possible.

Note the bit of cruft the displayAll parameter introduces. displayAll lets the calling site decide whether or not to display all the StackTraceElements or just the stack's topmost element. The example program uses the displayAll parameter to restrict the output to a reasonable amount.

Most of the stack trace information is directly useful. For example, the StackTraceElement.getMethodName() returns a string that contains the method name, while StackTraceElement.getFileName() returns a string with the original source filename. Read the StackTraceElement Javadoc for the complete list of methods.

Class names galore!

As you probably noticed, the displayStackTraceInformation() code uses several additional helper methods to pull apart the value returned by StackTraceElement.getClassName(). Those helper methods are needed because StackTraceElement.getClassName() returns the class's fully qualified name, and StackTraceElement has no other methods to provide the underlying parts of the fully qualified class name. We'll learn about each additional helper method by working through the various example uses of displayStackTraceInformation().

Default vs. named packages

Given the fully qualified class name, extractPackageName() gives the name of the package in which the class resides:

    public static String extractPackageName (String fullClassName)
        {
        if ((null == fullClassName) || ("".equals (fullClassName)))
            return "";
        int lastDot = fullClassName.lastIndexOf ('.');
        if (0 >= lastDot)
            return "";
        
        return fullClassName.substring (0, lastDot);
        }

Basically, extractPackageName extracts everything preceding the last dot in the fully qualified class name. That preceding information just happens to be the package's name.

Note: You can comment/uncomment the package statement at the top of StackTrace.java to explore the difference between running the example program in the default, unnamed package versus running it in the boo.hoo package. For example, when uncommented, the display of the topmost stack element for the call to bar() from foo() from main() should look like this:

Filename: StackTrace.java
Line number: 227
Package name: boo.hoo
Full class name: boo.hoo.StackTrace
Simple class name: StackTrace
Unmunged class name: StackTrace
Direct class name: StackTrace
Method name: bar
Native method?: false
toString(): boo.hoo.StackTrace.bar(StackTrace.java:227)

Alternatively, if you comment out the package statement, then the above stack element should resemble this:

Filename: StackTrace.java
Line number: 227
Package name: [default package]
Full class name: StackTrace
Simple class name: StackTrace
Unmunged class name: StackTrace
Direct class name: StackTrace
Method name: bar
Native method?: false
toString(): StackTrace.bar(StackTrace.java:227)

Can class names ever be simple?

The next helper method we use is extractSimpleClassName(). As you'll see, that method's results are not necessarily simple, but I want to clearly distinguish this simplified class name from the fully qualified class name.

Basically, extractSimpleClassName() complements extractPackageName():

    public static String extractSimpleClassName (String fullClassName)
        {
        if ((null == fullClassName) || ("".equals (fullClassName)))
            return "";
        int lastDot = fullClassName.lastIndexOf ('.');
        if (0 > lastDot)
            return fullClassName;
        
        return fullClassName.substring (++lastDot);
        }

In other words, extractSimpleClassName() returns everything after the last dot (.) from the fully qualified class name. For example, from the same call to bar() above, we see that the simple class name is just StackTrace, whether or not the code is part of the default package or a named package.

We get more interesting results when we turn our attention to nested classes. In the example program, I created two levels of nested, named classes (FirstNested and FirstNested.SecondNested) along with an additional, anonymous inner class (inside FirstNested.SecondNested).

The whole nested usage starts with:

    public StackTrace (boolean na)
        {
        StackTrace.FirstNested nested = new StackTrace.FirstNested();
        }

Note that the boolean parameter (na) means nothing. I just added it since other constructors need to be distinguished.

Here are the nested classes:

    public class FirstNested 
        {
        public FirstNested()
            {
            StackTrace.displayStackTraceInformation (new Throwable());
            StackTrace.FirstNested.SecondNested yan =
                new StackTrace.FirstNested.SecondNested();
            
            System.out.println ("Dumping from inside hogwash():");
            yan.hogwash();
            }
        public class SecondNested
            {
            public SecondNested()
                {
                StackTrace.displayStackTraceInformation (new Throwable());
                }
            public void hogwash()
                {
                StackTrace.displayStackTraceInformation (new Throwable());
                Whackable whacked = new Whackable()
                        {
                        public void whack()
                            {
                            StackTrace.displayStackTraceInformation
                                (new Throwable());
                            }
                        };      // End of anonymous member class.
                whacked.whack();
                }       // End of hogwash().
            }   // End of FirstNested.SecondNexted member class.
        }       // End of FirstNested member class.

The topmost stack element for SecondNested's constructor looks like this:

Filename: StackTrace.java
Line number: 267
Package name: boo.hoo
Full class name: boo.hoo.StackTrace$FirstNested$SecondNested
Simple class name: StackTrace$FirstNested$SecondNested
Unmunged class name: StackTrace.FirstNested.SecondNested
Direct class name: SecondNested
Method name: <init>
Native method?: false
toString():
boo.hoo.StackTrace$FirstNested$SecondNested.<init>(StackTrace.java:267)

You can see that the simple class name isn't quite so simple in this case. The nested classes are distinguished from the higher-level nested classes and from the top-level class by using the dollar sign character ($). So, technically, the "simple" name of the second nested class is StackTrace$FirstNested$SecondNested.

I have provided the unmungeSimpleClassName() method to replace the dollar signs with periods for completeness.

Since I'm stubborn, I still wanted to get the truly simple class name, so I created extractDirectClassName():

    public static String extractDirectClassName (String simpleClassName)
        {
        if ((null == simpleClassName) || ("".equals (simpleClassName)))
            return "";
        int lastSign = simpleClassName.lastIndexOf ('$');
        if (0 >s lastSign)
            return simpleClassName;
        return simpleClassName.substring (++lastSign);
        }

The extractDirectClassName() method takes a simple class name and extracts everything after the last dollar sign character, if it exists. If the class isn't nested, then the direct class name is the simple class name. For example, from the multiply nested class example above, you can see that the StackTrace$FirstNested$SecondNested's direct class name is just SecondNested.

Some interesting warts

Constructors are not explicitly named in Java. All constructor methods are implicitly given the name <init>. This is shown in the previous example.

Anonymous inner classes are actually constructed as children of the top-level class in which they are nested, regardless of nesting level. From the multiply nested example above, note that the anonymous inner class is nested inside SecondNested. However, here is the topmost stack element:

Filename: StackTrace.java
Line number: 282
Package name: boo.hoo
Full class name: boo.hoo.StackTrace
Simple class name: StackTrace
Unmunged class name: StackTrace.1
Direct class name: 1
Method name: whack
Native method?: false
toString(): boo.hoo.StackTrace.whack(StackTrace.java:282)

See how the compiler gave the anonymous inner class a numeric direct class name immediately under the top-level class StackTrace? However, note that the array of stack trace elements shows all the nesting; if you want to know the nesting of an anonymous class relative to its actual nesting in the source code, then you must step through the stack elements above it and figure it out yourself. This is left as an exercise for the reader.

Location, location, location!

Well, there you have it: everything you need to know to find where you are and how you got there -- well, at least in your Java code. When coding, this trick is useful for simple logging and tracing needs, but I suggest you check out full-blown logging packages, such as the new Java Logging API or Log4J, which handle more details than just gathering information for you. And one final piece of advice: When traveling, do remember to stop and ask for directions. Your traveling companions may not enjoy your "Zen driving" experiment.

John D. Mitchell is the contributing editor of JavaWorld's Tips 'N Tricks column. John is also the founder and chief architect of Non, Inc. -- a technological business risk-management consulting practice.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies