6.170 / Spring 2001 / Lab 5: Debugging, Part Deux

Handout B5

Contents:


Purpose

The purpose of this lab is familiarize you with a certain class of common compile-time problems, and to equip you with tools to help debug run-time errors. The focus of this lab is not on coding.

Compile-time problems

Compile-time errors should be the easiest ones to debug, since javac immediately prints out the errors it encountered. However, the messages can be difficult to interpret, especially when you first encounter them. One of the most common is cannot resolve symbol, and can be caused by any of the following:

Packages

For a package to be well-formed, the package's parent directory must exist in your CLASSPATH environment variable -- for 6.170 this is already taken care of because our recommended modifications modify $CLASSPATH to include /mit/$USER/6.170 by default. This is why your problem sets must be in directories named psN under your 6.170 directory.

Additionally, each file in the package must have a declaration of the form:

    package psN;
which allows a class to know its own namespace, among other things. For example, in your Problem Set 2 class GeoSegment, you might have made a reference to the class GeoPoint. The compiler has a list of packages you've imported via the import construct (let's say java.util), as well as the class's own package (ps2). It looks in each of these packages for the GeoPoint class until it finds it, or issues an error.

Thus, you will get a cannot resolve symbol error from javac if you all of your classes for a particular project are not consistently declared as part of the same package. Some permutations of this error are shown in the exercises. As a quick example of this error:

    Foo.java:5: cannot resolve symbol
    symbol  : class Bar  
    location: class package1.Foo
        private Bar barbarbar;
                ^

The error message tells you that the compiler believes it's compiling a class "Foo" in package "package1". For some reason, it's encountered a naked class without a package, most likely because there's no class "Bar" in the same package so it can't walk through the search order as described above.

Misspelling

Another common cause of this error is by misspelling a variable or class or field name:

    OptimizedLinkedList.java:9: cannot resolve symbol
    symbol  : variable theTal
    location: class OptimizedLinkedList
	    theTal = null;
            ^

In this case the intended field is called theTail.

Scope

You'll also see this error if you have code which tries to reference a variable that was declared in another scope, like:

    ...
    try {
        double ran = Math.random();
        if (ran > .5) {
            throw new Exception("random number too large");
        }
    } catch (Exception e) {
        e.printStackTrace(System.err);
    }

    double result = ran;       // <<<<<< "ran" is out of scope here
    System.out.println("Random number is " + result);
    ...

The variable ran is only valid inside the try block, and so it becomes an unresolved symbol if you try to access it outside the block (unless you also have another variable called ran in the outer scope).

Casting

An interesting side-effect of the type-genericity of Java 2's Collections classes is that the return type for all producers is Object, since that's the lowest-common-denominator class of anything that can be stored in a Collection.

Therefore, from the compiler's point of view, Vector.get() always returns an object, regardless of the fact that you, the programmer, know that at run-time the Vector will only contain Integers, for instance. If you have a piece of code like:

    ...
    Vector vec = new Vector();
    vec.add(new Integer(5));
    int a = vec.get(0).intValue();
    ...
it will yield the compile-time error:
    Foo.java:13: cannot resolve symbol
    symbol  : method intValue  ()
    location: class java.lang.Object
	    int a = vec.get(0).intValue();
                          ^

The unresolved symbol error stems from the fact that the compiler only knows that vec.get(0) returns an element of compile-time type Object, which does not have method intValue() defined for it. The tip-off is that the "location:" is showing java.lang.Object instead of java.lang.Integer. You need to tell the compiler what the run-time type will be so it can find the correct method.

Note that you must be certain that what you are casting will actually have that type at run-time. If it does not, you'll have a ClassCastException. Inversely, if you have a ClassCastException at a particular line, you can debug it quickly by adding debugging code like:

    ...
    Object testclass = vec.get(0);
    System.err.println("At line 13 of Foo.java: " + testclass.getClass().getName() +
                             ": " + testclass);
    int a = ((Integer)vec.get(0)).intValue(); // <<<<<< ClassCastException happens here sometimes
    ...
This will print out the Vector elements' classes and use their toString() method (which is dispatched at run-time to the appropriate method in the run-time class).

Debugging Tools

To debug effectively, you need to:
  1. Isolate a particular test case (input) where the expected output does not match the actual output
  2. Identify the inputs into your system for that case.
  3. Identify the method were the error occurs, or where you can first observe the symptoms
  4. Understand the problem

JUnit provides a good testing harness for step 1 (finding a test case that exhibits the error), provided you have good test cases. Here are some tools to help you debug.

toString()

Every class has a toString method, although it might only inherit Object's, which isn't too exciting. Writing a custom toString should have no impact on the correctness of your code, but will help you understand the state of an object if you need it for debugging. Printing the object's class in this string is often useful.

During debugging, it may be helpful to to use a toString method that prints every field in your class, potentially with a field-name label. Since this may expose your internal variables to clients, you should disable the verbosity of the string for final versions of your code.

For example, your GeoSegment class from Problem Set 2 might output something like:

GeoSegment: Memorial Dr.
   p1: GeoPoint: (42358333, -71060278)
   p2: GeoPoint: (42358333, -71079857)

Note that in this case, it would be most modular if GeoPoint had it's own toString method that is called to generate "GeoPoint: ..." in the last two lines.

Remember that in the following three cases:

Object bat = getNextObjectOfRandomClass();
System.out.print("bat: "); System.out.println(bat);
System.out.println("bat: " + bat);
System.out.println("bat: " + bat.toString());
an equivalent string is printed out. The middle case is usually most convenient.

Method entry checks

Often a method might be called with parameters that satisfy its "@requires" clause, but you still want to get a peek at the values to see how they might interact with other parts of your code. For instance:
Entering method doSomething(Vector a, Object b, Word c)
a: [ "lala", "looloo" ]
b: null
c: Word: word: "nothing"
In this case we can clearly see that parameter "b" is null on this particular invocation, and that "a" only holds 2 elements.

The entire method signature is included in the output to isolate which particular method is being called, since it can be ambiguous with Java's method overloading, where multiple methods can share the same name but take different parameters

Exceptions

By now you've had some experience throwing and catching Exceptions. As a quick review, any Exception class that you define will be of the form:
    public class MyException extends Exception {
        public MyException() {
            super();
        }
        public MyException(String s) {
            super(s);
        }
    }
Unchecked exceptions only differ in that you extend RuntimeException

A combination of the two techniques above can be applied when you get a back trace. Although you can tell the location of the error, you might not immediately be able to tell the conditions under which the error is occuring. Before each line in your trace, do a variable dump of what's being passed as parameters to the method in the trace. Note: doing this in addition to method entry dumps will give you the same information twice, which might be too verbose.

This technique is more useful when your code mostly works, and you're trying to debug a specific exception, unlike if you need method entry checks to look at method invocation in every part of your code.

JDB

jdb is the Java debugger. It provides essentially all of the above-mentioned functionality, but at a much lower level. jdb also allows you to step through program execution one statement at a time.

jdb isn't discussed in detail here, but it is an alternative tool that can help you. More information about jdb can be found at Sun's site.

CheckRep() methods

Writing a checkRep() method can be hugely useful in tracing down the first time your object's internal representation invariant is broken. The method should verify make an assertion (like a JUnit assertion, or your own kind) that the RI is satisfied.

Inserting these calls at the beginning and end of public methods can make your code robust, since it will be far less likely that your internal state will become invalid without you realizing it.


Setup

Execute the following commands to set up for the exercises:
    athena% mkdir ~/6.170/lab5
    athena% cp -r /mit/6.170/www/labs/lab5/assignment/* ~/6.170/lab5/
    athena% cd ~/6.170/lab5

Exercises

Exercise 1

Reproduce the follow cannot resolve symbol errors using the provided files Class1.java and Class2.java which should be placed in ~/6.170/lab5/symbol/. We will use the package lab5.symbol, which works equivalently to a top-level package as described earlier, except it's one level nested.

Class1.java:5: cannot access Class2
bad class file: ./Class2.class
class file contains wrong class: lab5.symbol.Class2
Please remove or make sure it appears in the correct
subdirectory of the classpath.
    private Class2 c2;
            ^
Class1.java:5: cannot resolve symbol
symbol  : class Class2  
location: class lab5.symbols.Class1
    private Class2 c2;
            ^
Class1.java:8: cannot resolve symbol
symbol  : constructor Class2  ()
location: class lab5.symbol.Class2
	c2 = new Class2();
             ^
./Class2.java:8: cannot resolve symbol
symbol  : constructor Class1  ()
location: class lab5.symbol.Class1
	c1 = new Class1();
             ^

To reproduce these errors, you'll need to modify one or both of the source files, and run:

    athena% javac Class1.java
in your ~/6.170/lab5/symbol directory. If you get other errors, ignore them as long as you can reproduce the stated error.

Exercise 2

Implement a toString method for DictionaryEntry and Dictionary classes which prints out their respective fields. How will you print out multiple definitions or entries? (Hint: look at java.util.AbstractCollection.toString())

The Javadoc for these classes can be found here.

Exercise 3

Add method entry parameter dumps for every public method in Dictionary. For this exercise, only add System.out.println()'s which print the method signature or the parameters to the method.

Exercise 4

Compile the source files in your ~/6.170/lab5 directory and run the DictionaryLoader class.
     athena% javac *.java
     athena% java lab5.DictionaryLoader
Read through the logs during this invocation. The DictionaryLoader assembles a dictionary, using the DictionaryEntry class you modified. If you want to store this output, you can do:
     athena% java lab5.DictionaryLoader > dictionary.out
     athena% emacs dictionary.out
There are at least 3 suspicious entries that you should identify.

These examples are fairly contrived, since they clearly violate the requires clause. In defensive coding, you would make a check for these violations explicitly. However, dumping out values can help in determining coverage and execution paths, to better understand the average usage of your code.