The Way of the Java/Debugging

From Wikibooks, open books for an open world
Jump to navigation Jump to search

Debugging[edit | edit source]

There are a few different kinds of errors that can occur in a program, and it is useful to distinguish between them in order to track them down more quickly.

itemize

Compile-time errors are produced by the compiler and usually indicate that there is something wrong with the syntax of the program. Example: omitting the semi-colon at the end of a statement.

Run-time errors are produced by the run-time system if something goes wrong while the program is running. Most run-time errors are Exceptions. Example: an infinite recursion eventually causes a StackOverflowException.

Semantic errors are problems with a program that compiles and runs, but doesn't do the right thing. Example: an expression may not be evaluated in the order you expect, yielding an unexpected result.

itemize

compile-time error
run-time error
semantic error
error!compile-time
error!run-time
error!semantic
exception

The first step in debugging is to figure out which kind of error you are dealing with. Although the following sections are organized by error type, there are some techniques that are applicable in more than one situation.


Compile-time errors[edit | edit source]

The compiler is spewing error messages.[edit | edit source]

error messages
compiler

If the compiler reports 100 error messages, that doesn't mean there are 100 errors in your program. When the compiler encounters an error, it gets thrown off track for a while. It tries to recover and pick up again after the first error, but sometimes it fails, and it reports spurious errors.

In general, only the first error message is reliable. I suggest that you only fix one error at a time, and then recompile the program. You may find that one semi-colon ``fixes 100 errors. Of course, if you see several legitimate error messages, you might as well fix more than one bug per compilation attempt.


I'm getting a weird compiler message and it won't go away.[edit | edit source]

First of all, read the error message carefully. It is written in terse jargon, but often there is a kernel of information there that is carefully hidden.

If nothing else, the message will tell you where in the program the problem occurred. Actually, it tells you where the compiler was when it noticed a problem, which is not necessarily where the error is. Use the information the compiler gives you as a guideline, but if you don't see an error where the compiler is pointing, broaden the search.

Generally the error will be prior to the location of the error message, but there are cases where it will be somewhere else entirely. For example, if you get an error message at a method invocation, the actual error may be in the method definition.

If you are building the program incrementally, you should have a good idea about where the error is. It will be in the last line you added.

If you are copying code from a book, start by comparing your code to the book's code very carefully. Check every character. At the same time, remember that the book might be wrong, so if you see something that looks like a syntax error, it might be.

If you don't find the error quickly, take a breath and look more broadly at the entire program. Now is a good time to go through the whole program and make sure it is indented properly. I won't say that good indentation makes it easy to find syntax errors, but bad indentation sure makes it harder.

Now, start examining the code for the common syntax errors.

syntax

enumerate

Check that all parentheses and brackets are balanced and properly nested. All method definitions should be nested within a class definition. All program statements should be within a method definition.

Remember that upper case letters are not the same as lower case letters.

Check for semi-colons at the end of statements (and no semi-colons after squiggly-braces).

Make sure that any strings in the code have matching quotation marks (and that you use double-quotes, not single).

For each assignment statement, make sure that the type on the left is the same as the type on the right.

For each method invocation, make sure that the arguments you provide are in the right order, and have right type, and that the object you are invoking the method on is the right type.

If you are invoking a fruitful method, make sure you are doing something with the result. If you are invoking a void method, make sure you are not trying to do something with the result.

If you are invoking an object method, make sure you are invoking it on an object with the right type. If you are invoking a class method from outside the class where it is defined, make sure you specify the class name.

Inside an object method you can refer to the instance variables without specifying an object. If you try that in a class method, you will get a confusing message like, ``Static reference to non-static variable.

enumerate

If nothing works, move on to the next section...


I can't get my program to compile no matter what I do.[edit | edit source]

If the compiler says there is an error and you don't see it, that might be because you and the compiler are not looking at the same code. Check your development environment to make sure the program you are editing is the program the compiler is compiling. If you are not sure, try putting an obvious and deliberate syntax error right at the beginning of the program. Now compile again. If the compiler doesn't find the new error, there is probably something wrong with the way you set up the project.

Otherwise, if you have examined the code thoroughly, it is time for desperate measures. You should start over with a program that you can compile and then gradually add your code back.

itemize

Make a copy of the file you are working on. If you are working on Fred.java, make a copy called Fred.java.old.

Delete about half the code from Fred.java. Try compiling again.

itemize

If the program compiles now, then you know the error is in the other half. Bring back about half of the code you deleted and repeat.

If the program still doesn't compile, the error must be in this half. Delete about half of the code and repeat.

itemize

Once you have found and fixed the error, start bringing back the code you deleted, a little bit at a time.

itemize

This process is called ``debugging by bisection. As an alternative, you can comment out chunks of code instead of deleting them. For really sticky syntax problems, though, I think deleting is more reliable---you don't have to worry about the syntax of the comments, and by making the program smaller you make it more readable.

bisection!debugging by
debugging by bisection

Run-time errors[edit | edit source]

My program hangs.[edit | edit source]

infinite loop
infinite recursion
hanging

If a program stops and seems to be doing nothing, we say it is ``hanging. Often that means that it is caught in an infinite loop or an infinite recursion.

itemize

If there is a particular loop that you suspect is the problem, add a print statement immediately before the loop that says ``entering the loop and another immediately after that says ``exiting the loop.

Run the program. If you get the first message and not the second, you've got an infinite loop. Go to the section titled ``Infinite loop.

Most of the time an infinite recursion will cause the program to run for a while and then produce a StackOverflowException. If that happens, go to the section titled ``Infinite recursion.

If you are not getting a StackOverflowException, but you suspect there is a problem with a recursive method, you can still use the techniques in the infinite recursion section.

If neither of those things works, start testing other loops and other recursive methods.

If none of those things works, then it is possible that you don't understand the flow of execution in your program. Go to the section titled ``Flow of execution.

itemize


Infinite loop[edit | edit source]

If you think you have an infinite loop and think you know what loop is causing the problem, add a print statement at the end of the loop that prints the values of the variables in the condition, and the value of the condition.

For example,

verbatim

   while (x > 0 && y < 0) 
       // do something to x
       // do something to y
       System.out.println ("x: " + x);
       System.out.println ("y: " + y);
       System.out.println ("condition: " + (x > 0 && y < 0));
   

verbatim

Now when you run the program you will see three lines of output for each time through the loop. The last time through the loop, the condition should be false. If the loops keeps going, you will be able to see the values of x and y and you might figure out why they are not being updated correctly.


Infinite recursion[edit | edit source]

Most of the time an infinite recursion will cause the program to run for a while and then produce a StackOverflowException.

If you suspect that method is causing an infinite recursion, start by checking to make sure that there is a base case. In other words, there should be some condition that will cause the method to return without making a recursive invocation. If not, then you need to rethink the algorithm and identify a base case.

If there is a base case, but the program doesn't seem to be reaching it, add a print statement at the beginning of the method that prints the parameters. Now when you run the program you will see a few lines of output every time the method is invoked, and you will see the parameters. If the parameters are not moving toward the base case, you will get some ideas about why not.


Flow of execution[edit | edit source]
flow of execution

If you are not sure how the flow of execution is moving through your program, add print statements to the beginning of each method with a message like ``entering method foo, where foo is the name of the method.

Now when you run the program it will print a trace of each method as it is invoked.

It is often useful to print the parameters each method receives when it is invoked. When you run the program, check whether the parameters are reasonable, and check for one of the classic errors---providing parameters in the wrong order.


When I run the program I get an Exception.[edit | edit source]

Exception

If something goes wrong during run time, the Java run-time system prints a message that includes the name of the exception, the line of the program where the problem occurred, and a stack trace.

The stack trace includes the method that is currently running, and then the method that invoked it, and then the method that invoked that, and so on. In other words, it traces the path of method invocations that got you to where you are.

The first step is to examine the place in the program where the error occurred and see if you can figure out what happened.

description

[NullPointerException:] You tried to access an instance variable or invoke a method on an object that is currently null. You should figure out what variable is null and then figure out how it got to be that way.

Remember that when you declare a variable with an object type, it is initially null, until you assign a value to it. For example, this code causes a NullPointerException:

verbatim Point blank; System.out.println (blank.x); verbatim

[ArrayIndexOutOfBoundsException:] The index you are using to access an array is either negative or greater than array.length-1. If you can find the site where the problem is, add a print statement immediately before it to print the value of the index and the length of the array. Is the array the right size? Is the index the right value?

Now work your way backwards through the program and see where the array and the index come from. Find the nearest assignment statement and see if it is doing the right thing.

If either one is a parameter, go to the place where the method is invoked and see where the values are coming from.

[StackOverFlowException:] See ``Infinite recursion.

description


I added so many print statements I get inundated with output.[edit | edit source]

print statement
statement!print

One of the problems with using print statements for debugging is that you can end up buried in output. There are two ways to proceed: either simplify the output or simplify the program.

To simplify the output, you can remove or comment out print statements that aren't helping, or combine them, or format the output so it is easier to understand.

To simplify the program, there are several things you can do. First, scale down the problem the program is working on. For example, if you are sorting an array, sort a small array. If the program takes input from the user, give it the simplest input that causes the error.

Second, clean up the program. Remove dead code and reorganize the program to make it as easy to read as possible. For example, if you suspect that the error is in a deeply-nested part of the program, try rewriting that part with simpler structure. If you suspect a large method, try splitting it into smaller methods and test them separately.

Often the process of finding the minimal test case leads you to the bug. For example, if you find that a program works when the array has an even number of elements, but not when it has an odd number, that gives you a clue about what is going on.

Similarly, rewriting a piece of code can help you find subtle bugs. If you make a change that you think doesn't affect the program, and it does, that can tip you off.


Semantic errors[edit | edit source]

My program doesn't work.[edit | edit source]

In some ways semantic errors are the hardest, because the compiler and the run-time system provide no information about what is wrong. Only you know what the program was supposed to do, and only you know that it isn't doing it.

The first step is to make a connection between the program text and the behavior you are seeing. You need a hypothesis about what the program is actually doing. One of the things that makes this hard is that computers run so fast. You will often wish that you could slow the program down to human speed, but there is no straightforward way to do that, and even if there were, it is not really a good way to debug.

Here are some questions to ask yourself:

itemize

Is there something the program was supposed to do, but doesn't seem to be happening? Find the section of the code that performs that function and make sure it is executing when you think it should. Add a print statement to the beginning of the suspect methods.

Is something happening that shouldn't? Find code in your program that performs that function and see if it is executing when it shouldn't.

Is a section of code producing an effect that is not what you expected? Make sure that you understand the code in question, especially if it involves invocations to built-in Java methods. Read the documentation for the methods you invoke. Try out the methods by invoking the methods directly with simple test cases, and check the results.

itemize

In order to program, you need to have a mental model of how programs work. If your program that doesn't do what you expect, very often the problem is not in the program; it's in your mental model.

model!mental
mental model

The best way to correct your mental model is to break the program into its components (usually the classes and methods) and test each component independently. Once you find the discrepancy between your model and reality, you can solve the problem.

Of course, you should be building and testing components as you develop the program. If you encounter a problem, there should be only a small amount of new code that is not known to be correct.

Here are some common semantic errors that you might want to check for:

itemize

If you use the assignment operator, =, instead of the equality operator, ==, in the condition of an if, while or for statement, you might get an expression that is syntactically legal, but it doesn't do what you expect.

When you apply the equality operator, ==, to an object, it checks shallow equality. If you meant to check deep equality, you should use the equals method (or define one, for user-defined objects).

Some Java libraries expect user-defined objects to define methods like equals. If you don't define them yourself, you will inherit the default behavior from the parent class, which may not be what you want.

In general, inheritance can cause subtle semantic errors, because you may be executing inherited code without realizing it. Again, make sure you understand the flow of execution in your program.

itemize


I've got a big hairy expression and it doesn't do what I expect.[edit | edit source]

expression!big and hairy

Writing complex expressions is fine as long as they are readable, but they can be hard to debug. It is often a good idea to break a complex expression into a series of assignments to temporary variables.

For example:

verbatim rect.setLocation (rect.getLocation().translate

                    (-rect.getWidth(), -rect.getHeight()));

verbatim

Can be rewritten as

verbatim int dx = -rect.getWidth(); int dy = -rect.getHeight(); Point location = rect.getLocation(); Point newLocation = location.translate (dx, dy); rect.setLocation (newLocation); verbatim

The explicit version is easier to read, because the variable names provide additional documentation, and easier to debug, because we can check the types of the intermediate variables and display their values.

temporary variable
variable!temporary
order of evaluation
precedence

Another problem that can occur with big expressions is that the order of evaluation may not be what you expect. For example, if you are translating the expression

into Java, you might write

verbatim double y = x / 2 * Math.PI; verbatim

That is not correct, because multiplication and division have the same precedence, and are evaluated from left to right. So this expression computes .

A good way to debug expressions is to add parentheses to make the order of evaluation explicit.

verbatim double y = x / (2 * Math.PI); verbatim

Any time you are not sure of the order of evaluation, use parentheses. Not only will the program be correct (in the sense of doing what you intend); it will also be more readable for other people who haven't memorized the rules of precedence.


I've got a method that doesn't return what I expect.[edit | edit source]

return statement
statement!return

If you have a return statement with a complex expression, you don't have a chance to print the return value before returning. Again, you can use a temporary variable. For example, instead of

verbatim public Rectangle intersection (Rectangle a, Rectangle b)

   return new Rectangle (
       Math.min (a.x, b.x),
       Math.min (a.y, b.y),
       Math.max (a.x+a.width, b.x+b.width)-Math.min (a.x, b.x)
       Math.max (a.y+a.height, b.y+b.height)-Math.min (a.y, b.y) );

verbatim

You could write

verbatim public Rectangle intersection (Rectangle a, Rectangle b)

   int x1 = Math.min (a.x, b.x);
   int y2 = Math.min (a.y, b.y);
   int x2 = Math.max (a.x+a.width, b.x+b.width);
   int y2 = Math.max (a.y+a.height, b.y+b.height);
   Rectangle rect = new Rectangle (x1, y1, x2-x1, y2-y1);
   return rect;

verbatim

Now you have the opportunity to display any of the intermediate variables before returning.


My print statement isn't doing anything[edit | edit source]

print statement
statement!print

If your use the println method, the output gets displayed immediately, but if you use print (at least in some environments) the output gets stored without being displayed until the next newline character gets output. If the program terminates without producing a newline, you may never see the stored output.

If you suspect that this is happening to you, try changing all the print statements to println.


I'm really, really stuck and I need help[edit | edit source]

First of all, try getting away from the computer for a few minutes. Computers emit waves that affect the brain, causing the following symptoms:

itemize

Frustration and/or rage.

Superstitious beliefs (``the computer hates me) and magical thinking (``the program only works when I wear my hat backwards).

Random walk programming (the attempt to program by writing every possible program and choosing the one that does the right thing).

itemize

If you find yourself suffering from any of these symptoms, get up and go for a walk. When you are calm, think about the program. What is it doing? What are some possible causes of that behavior? When was the last time you had a working program, and what did you do next?

Sometimes it just takes time to find a bug. I often find bugs when I am away from the computer and I let my mind wander. Some of the best places to find bugs are trains, showers, and in bed, just before you fall asleep.


No, I really need help.[edit | edit source]

It happens. Even the best programmers occasionally get stuck. Sometimes you work on a program so long that you can't see the error. A fresh pair of eyes is just the thing.

Before you bring someone else in, make sure you have exhausted the techniques described here. You program should be as simple as possible, and you should be working on the smallest input that causes the error. You should have print statements in the appropriate places (and the output they produce should be comprehensible). You should understand the problem well enough to describe it concisely.

When you bring someone in to help, be sure to give them the information they need.

itemize

What kind of bug is it? Compile-time, run-time, or semantic?

If the bug occurs at compile-time or run-time, what is the error message, and what part of the program does it indicate?

What was the last thing you did before this error occurred? What were the last lines of code that you wrote, or what is the new test case that fails?

What have you tried so far, and what have you learned?

itemize

When you find the bug, take a second to think about what you could have done to find it faster. Next time you see something similar, you will be able to find the bug more quickly.

Remember, in this class the goal is not to make the program work. The goal is to learn how to make the program work. Program development plan

If you are spending a lot of time debugging, it is probably because you do not have an effective program development plan.

A typical, bad program development plan goes something like this:

enumerate

Write an entire method.

Write several more methods.

Try to compile the program.

Spend an hour finding syntax errors.

Spend an hour finding run time errors.

Spend three hours finding semantic errors.

enumerate

The problem, of course, is the first two steps. If you write more than one method, or even an entire method, before you start the debugging process, you are likely to write more code than you can debug.

If you find yourself in this situation, the only solution is to remove code until you have a working program again, and then gradually build the program back up. Beginning programmers are often unwilling to do this, because their carefully crafted code is precious to them. To debug effectively, you have to be ruthless!


Here is a better program development plan:

enumerate

Start with a working program that does something visible,

  like printing something.

Add a small number of lines of code at a time,

  and test the program after every change.

Repeat until the program does what it is supposed to do.

enumerate

After every change, the program should produce some visible effect that demonstrates the new code. This approach to programming can save a lot of time. Because you only add a few lines of code at a time, it is easy to find syntax errors. Also, because each version of the program produces a visible result, you are constantly testing your mental model of how the program works. If your mental model is erroneous, you will be confronted with the conflict (and have a chance to correct it) before you have written a lot of erroneous code.

One problem with this approach is that it is often difficult to figure out a path from the starting place to a complete and correct program.

I will demonstrate by developing a method called isIn that takes a String and a Vector, and that returns a boolean: true if the String appears in the list and false otherwise.

enumerate

The first step is to write the shortest possible method that will compile, run, and do something visible:

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn");
   return false;

verbatim

Of course, to test the method we have to invoke it. In main, or somewhere else in a working program, we need to create a simple test case.

We'll start with a case where the String appears in the vector (so we expect the result to be true).

verbatim public static void main (String[] args)

   Vector v = new Vector ();
   v.add ("banana");
   boolean test = isIn ("banana", v);
   System.out.println (test);

verbatim

If everything goes according to plan, this code will compile, run, and print the word isIn and the value false. Of course, the answer isn't correct, but at this point we know that the method is getting invoked and returning a value.

In my programming career, I have wasted way too much time debugging a method, only to discover that it was never getting invoked. If I had used this development plan, it never would have happened.

The next step is to check the parameters the method receives.

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   return false;

verbatim

The first print statement allows us to confirm that isIn is looking for the right word. The second statement prints a list of the elements in the vector.

To make things more interesting, we might add a few more elements to the vector:

verbatim public static void main (String[] args)

   Vector v = new Vector ();
   v.add ("apple");
   v.add ("banana");
   v.add ("grapefruit");
   boolean test = isIn ("banana", v);
   System.out.println (test);

verbatim

Now the output looks like this:

verbatim isIn looking for banana in the vector [apple, banana, grapefruit] verbatim

Printing the parameters might seem silly, since we know what they are supposed to be. The point is to confirm that they are what we think they are.


To traverse the vector, we can take advantage of the code from Section vector. In general, it is a great idea to reuse code fragments rather than writing them from scratch.

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
   
   return false;

verbatim

Now when we run the program it prints the elements of the vector one at a time. If all goes well, we can confirm that the loop examines all the elements of the vector.


So far we haven't given much thought to what this method is going to do. At this point we probably need to figure out an algorithm. The simplest algorithm is a linear search, which traverses the vector and compares each element to the target word.

Happily, we have already written the code that traverses the vector. As usual, we'll proceed by adding just a few lines at a time:

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           System.out.println ("found it");
       
   
   return false;

verbatim

As always, we use the equals method to compare Strings, not the == operator!

Again, I added a print statement so that when the new code executes it produces a visible effect.

At this point we are pretty close to working code. The next change is to return from the method if we find what we are looking for:

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           System.out.println ("found it");
           return true;
       
   
   return false;

verbatim

If we find the target word, we return true. If we get all the way through the loop without finding it, then the correct return value is false.

If we run the program at this point, we should get

verbatim isIn looking for banana in the vector [apple, banana, grapefruit] apple banana found it true verbatim


The next step is to make sure that the other test cases work correctly. First, we should confirm that the method returns false if the word in not in the vector.

Then we should check some of the typical troublemakers, like an empty vector (one with size 0) and a vector with a single element. Also, we might try giving the method an empty String.

As always, this kind of testing can help find bugs if there are any, but it can't tell you if the method is correct.

The penultimate step is to remove or comment out the print statements.

verbatim public static boolean isIn (String word, Vector v)

   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           return true;
       
   
   return false;

verbatim

Commenting out the print statements is a good idea if you think you might have to revisit this method later. But if this is the final version of the method, and you are convinced that it is correct, you should remove them.

Removing the comments allows you to see the code most clearly, which can help you spot any remaining problems.

If there is anything about the code that is not obvious, you should add comments to explain it. Resist the temptation to translate the code line by line. For example, no one needs this:

verbatim

       // if word equals s, return true
       if (word.equals (s)) 
           return true;
       

verbatim

You should use comments to explain non-obvious code, to warn about conditions that could cause errors, and to document any assumptions that are built into the code. Also, before each method, it is a good idea to write an abstract description of what the method does.


The final step is to examine the code and see if you can convince yourself that it is correct.

At this point we know that the method is syntactically correct, because it compiles.

To check for run time errors, you should find every statement that can cause an error and figure out what conditions cause the error.

The statements in this method that can produce a run time error are:

tabularl l v.size() & if v is null. word.equals (s) & if word is null. (String) v.get(i) & if v is null or i is out of

                         bounds, 
                       & or the th element of v is not
                         a String.

tabular

Since we get v and word as parameters, there is no way to avoid the first two conditions. The best we can do is check for them.

verbatim public static boolean isIn (String word, Vector v)

   if (v == null  word == null) return false;
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           return true;
       
   
   return false;

verbatim

In general, it is a good idea for methods to make sure their parameters are legal.

instanceof operator
operator!instanceof

The structure of the for loop ensures that i is always between 0 and v.size()-1. But there is no way to ensure that the elements of v are Strings. On the other hand, we can check them as we go along. The instanceof operator checks whether an object belongs to a class.

verbatim

   Object obj = v.get(i);
   if (obj instanceof String) 
       String s = (String) v.get(i);
   

verbatim

This code gets an object from the vector and checks whether it is a String. If it is, it performs the typecast and assigns the String to s.

As an exercise, modify isIn so that if it finds an element in the vector that is not a String, it skips to the next element.

If we handle all the problem conditions, we can prove that this method will not cause a run time error.

We haven't proven yet that the method is semantically correct, but by proceeding incrementally, we have avoided many possible errors. For example, we already know that the method is getting parameters correctly and that the loop traverses the entire vector. We also know that it is comparing Strings successfully, and returning true if it finds the target word. Finally, we know that if the loop exists, the target word cannot be in the vector.

Short of a formal proof, that is probably the best we can do.