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():

1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more