We will now begin giving some examples of techniques you can use to debug without actually having access to the system where the problem occurred. This example shows techniques for dealing with a common type of problem: the NullPointerException.
Note that software engineers who have significant experience with diagnosing customer issues may find this posting a bit obvious, but I think it's still a good introduction to the concepts.
Example Code
Here's the example code:
public class DebugExample {
public String doSomething(String s) {
return s.replace('l', 'L');
}
public static final class C1 {
private DebugExample _de;
public DebugExample getDe() {
return _de;
}
public void setDe(final DebugExample de) {
_de = de;
}
public String save(DebugExample de) {
return de.doSomething("Hello");
}
}
public static final class C2 {
public String x(C1 c1) {
return c1.getDe().doSomething(null); /* Line 25 */
}
}
public static void main(String[] args) {
DebugExample de = new DebugExample();
de.doSomething("Hello");
C1 c1 = new C1();
c1.save(de);
C2 c2 = new C2();
System.out.println(c2.x(c1)); /* Line 35 */
}
}
Scenario
Imagine that you have a customer bug report, saying that when they run the above code, then get the following:
Exception in thread "main" java.lang.NullPointerException
at DebugExample$C2.x(DebugExample.java:25)
at DebugExample.main(DebugExample.java:35)
You don't have any other information. So, what do you do?
Become the Computer
The best way to approach NullPointerExceptions (NPEs) is to put yourself in the position of the computer. You want to try to look at the evidence you have, and then use logic to work backwards from the point of the exception to see how you could possibly have gotten to that point with a null variable.
In this case, the exception occurred at line 25. Line 25 looks like this:
return c1.getDe().doSomething(null); /* Line 25 */
So, how could this line produce a NPE? Working from the end of the line, your eye might be drawn to the null being passed to the “doSomething()” method. However, that could not be the problem. If it were, then the NPE would have occurred inside the “doSomething()” method. But it did not – it occurred on the line in the “x()” method. Even if the null could cause a problem (which it definitely would in this case), it was not the cause of this problem.
Causes for NullPointerExceptions
There are only two possible ways that line 25 could cause an NPE: If the “c1” variable was null, or if the “getDe()” method returned a null. Remember that an NPE means that Java tried to use an object reference to call a method on that object, but the object reference was null. So, the last method in any chain of method calls can not be the cause of a NPE that occurs directly on that line.
First Possibility
So, let’s look at those two possible causes. Could the “c1” variable be null? Well, “c1” is a parameter to the method:
public String x(C1 c1) {
There are no intervening lines that make any changes to “c1”, so if it is null, there had to be a null passed in the “x()” method call. So, let’s look at the previous line in the stack traceback. It says that the call to “x()” came from line 35:
C1 c1 = new C1();
c1.save(de);
C2 c2 = new C2();
System.out.println(c2.x(c1)); /* Line 35 */
As you can see, the parameter to the “x()” method is also called “c1”, and it is most definitely not null. The only assignment to it is where it is assigned to “new C1()”. There is no way that a constructor can return a null, so the “c1” variable is not null. Novices might be tempted to look into the “save()” method on the C1 class to see if there is anything that could be null there, but the internal state of the object that “c1” refers to does not matter. Remember, for Java to throw an NPE, the object reference itself must be null – if the reference itself is not null, then an NPE could not be thrown from that line.
Second Possibility
So, this means that the only way we could have gotten a NPE from line 25 is if the “getDe()” method returned a null. We have proven that there is no other way this could have happened. This is a key part of this kind of debugging – you have to not waste time chasing after issues that could not possibly have occurred.
So, let’s look at the “getDe() method:
public DebugExample getDe() {
return _de;
}
So, “_de” is a member variable (a.k.a. “field”) of the C1 class. Let’s look at its declaration:
private DebugExample _de;
Now we’re getting somewhere. There’s no initializer for this variable, which means that its value starts out as null. So now we should look for assignments to that variable. Scanning the source, we find only one such assignment:
public void setDe(final DebugExample de) {
_de = de;
}
OK, so somebody must call this method before the call to “getDe()” takes place. Scanning the source, though, there are no callers to this method. That, then, is the reason for the NPE – the “getDe()” method is being called without a previous call to “setDe()”.
Aftermath
Finding the problem is the first part – now you need to determine how to fix the problem. After reading the code, you notice that the C1 class has a method called “save()” that takes an instance of a DebugExample object as a parameter. It calls a method on that object, but it does not do anything else with it. From the method name (“save”), you can reasonably guess that the original writer of this code was planning to save the reference to the DebugExample object parameter. After fixing that, you find that the null you originally looked at on line 25 is indeed a problem, and you fix that as well.
Conclusions
The art of debugging is especially challenging when you don’t have access to the system where the failure occurs. It is necessary to become the computer, using logic to look for take the actual symptoms and reverse-engineer what must have been true to cause those symptoms. In doing this kind of work, it is key to not waste time looking at things that you have already proven could not be true.
