I want to revisit and expand upon a point that I made in the previous article. When you are debugging code that you don't understand, it is often helpful to eliminate things that are logical impossibilities. Look at the following code:
package nodebugger;
public class Logical {
public static void main(String[] args) {
String x = "ABCDEF";
int index = 5;
index = one(index);
index = two(index);
char a = getAChar(x, index);
System.out.println("a = " + a);
}
private static int one(int i) {
int rv = i * 2;
rv = rv + 3;
rv = rv / 4;
return rv;
}
private static int two(int i) {
if (i < 0) return i;
return i - 6;
}
private static char getAChar(String x, int i) {
if (i >= x.length()) {
i = x.length() - 1;
}
if (i % 3 == 0) {
return x.charAt(i);
}
return x.charAt(-i);
}
}
When this code runs, you get the following exception:
Exception in thread "main" java.lang.StringIndexOutOfBoundsException
at java.lang.String.charAt(String.java:418)
at nodebugger.Logical.getAChar(Logical.java:30)
at nodebugger.Logical.main(Logical.java:9)
This is admittedly silly code, but I use it to make a point about logical impossibilities.
Eliminating the Impossible
First of all, we are failing with a StringIndexOutOfBoundsException. As in the previous article, we find that this means an index was either negative or else it was greater than or equal to the length of the String. When we first glance at the "getAChar" method, we see something suspicious on line 32:
return x.charAt(-i);
However, the exception shows that we were at line 30 in this method. Since the method contains no loops (or any other construct that could cause execution to move backwards), we know that we must not have gotten to line 32.
Understanding Boundaries for Variable Values
So, let's look at the line in our code that is part of the failure.
return x.charAt(i);
Thus, "i" must either be negative, or else it is greater than or equal to the length of the String "x". So, let's look at how "i" gets here:
private static char getAChar(String x, int i) {
if (i >= x.length()) {
i = x.length() - 1;
}
if (i % 3 == 0) {
return x.charAt(i);
}The variable "i" is a parameter to this method. However, look at what happens on lines 26 and 27. If "i" is greater than or equal to the length of the String "x", then it gets set to the position of the last character in the String. Nothing else happens to "i" before it is used on line 30, so clearly, "i" is not too big - it is bounded by the length of the String. Therefore, it must have been negative.
How Did We Get This Value?
The "i" parameter must be negative, but how could it have been? Well, let's look at all the places that that parameter is set in the calling method:
int index = 5;
index = one(index);
index = two(index);
char a = getAChar(x, index);
The parameter "i" comes from variable "index" in the main routine. The initialization on line 6 sets "index" to a positive number, so that clearly isn't the immediate problem. Then we see that it is the target of assignments on lines 7 and 8, where it passes the "index" variable as a parameter, then the same variable receives the return values from routines "one" and "two". So, let's look at those routines.
Method "one" looks like this:
private static int one(int i) {
int rv = i * 2;
rv = rv + 3;
rv = rv / 4;
return rv;
}This routine looks like it is doing some complicated things to the return value, but look at them in order. Line 14 takes the initial parameter (which is the "index" variable from the main method) and multiplies it by two. Line 15 adds to it, and line 15 divides it. Notice, however, that there is no place where we could possibly introduce a negative value unless we started with one. There is no subtraction, no adding of negative values, no complementing (e.g. making negative). Therefore, this routine can not be the source of a negative number.
Finishing Up Debugging
Method "two" looks like this:
private static int two(int i) {
if (i < 0) return i;
return i - 6;
}Notice that line 21 could return a negative value, but only if the value was already negative. We have proven that this could not be the case, so we skip past that line. On line 22, though, we see that there is a subtraction. This could give us a negative value. Since this is the last change to the value before it gets passed to the "getAChar" method, this must be the line that made it negative - there is no other place where it could have happened.
Conclusion - Logical Impossibilities Are Your Friends
The point of this silly example is that, when a variable has an unexpected value, it is important to understand the possible range of values for that variable. Working back from the statement where things went wrong, you can use each statement in the program to put boundaries on the value. In this case, we started with a value that was either negative ot too big for a String, then proved that it must have been negative. We used that to eliminate an entire method from consideration, and eventually got to the specific line where the bad value was introduced. Note that this did not necessarily identify the actual bug - all it did is help us to understand how we could have gotten to the point of failure. However, once we have identified how we got the bad value, we now can look at the code that leads up to that point and figure out whether one part of the code made invalid assumptions about how other parts worked (which is usually the cause of bugs when dealing with larger programs).
