2.7 Avoiding Errors and Debugging

Avoiding Errors I

  1. In evaluating expressions, the order in which operations are performed is often critical. To be sure that operations are carried out in the correct order (and to help make the order clear to the reader), it is often a good .idea to use extra sets of parentheses in complex expressions. Casts, with their high precedence, are often causes of errors of this sort. For example, the expression (int) x*y will be evaluated by first casting x to an int and then multiplying the result by y.
  2. Another error that arises frequently with order of operations involves expressions of the form a/b*c. Since multiplication and division have equal precedence and are evaluated from left to right, such an expression will be evaluated as if it were written in the form (a/b) *c - not as a/ (b*c). In mathematics, the meaning of an expression such as is clear because the expression is two-dimensional. The corrresponding expression in Java, a/ (b*c ) is one-dimensional and hence more difficult to interpret correctly.
  3. Division operations can often cause errors because integer divisions produce integer values. For example, the expression (3/4) *8 has the value zero, not six. A cast is often a good way to get around such problems. The value of ((double)3/4) *8 is six (as a double).
  4. Because each numerical value is stored in a fixed number of bits while there are infinitely many real numbers, not all numbers can be represented exactly in a computer. This can cause a variety of errors.
    1. Roundoff errors occur when a decimal value cannot be represented exactly. We see this all the time when we try to write a fraction like 1/3 as a decimal. Simply writing digits will never give us the exact value of the fraction, no matter how many digits we write. You can also see this with a calculator if you use one to evaluate the expression: 3(1/3 + 100 - 100). In theory, the value should be exactly one but, because of roundoff error, the value displayed by the calculator will likely be something like 0.999999999. Similarly, in Java, the statement
      System.out.println(3.0*(1.0/3.0 + 100.0 - 100.0)); 
      will not print 1.0 but only something close to it. Try it yourself to see the result.
    2. Overflow errors occur when the magnitude of a value is larger than the capacity of the storage location. Again, we can see this with a calculator. By trying to evaluate the expression 100100 on a calculator, the result that is displayed will probably be E (for error) or something like that. In Java, double values can have magnitudes that are up to approximately 10308 but if we try to exceed that value we run into problems. If, for example, we try to print the value of the expression 1E300 * 1E300, the result that is printed will be Infinity since Java considers anything greater than its largest value to be infinite.
    3. Underflow errors occur when the magnitude of a floating point value is too close to zero. Java's double values can have magnitudes as small as (approximately) 10-323. Java considers a value smaller than this to be equal to zero. For example, the expression 1E-200 * 1E-200 will be stored as zero.

  5. For virtually all the problems that you will be asked to solve in this book, Java's integer and floating point types should be adequate, if used carefully. If you have a need for representations of larger values, Java provides the classes BigInteger and BigDecimal in the java.math package. Features of the BigInteger class are discussed on page 266.
  6. Rounding errors can cause problems in the conversion of floating point values to integers. For example, the value of the expression (int) (100*9.87) is 986, not 987. The reason for this is that 9.87 is stored in a form that is approximately equal to 9.869 999 999 999 998, a representation that is very slightly smaller than the actual value. When this is multiplied by 100, the result is 986.999 999 999 999 8 and, when this is cast as an int, the fractional part is lost. The remedy for such problems is to use Math.round in conversions. Here, if we write (int)Math.round(100*9.87), the result is 987. (The cast is used to convert the value returned by Math.round from a long to an int.)
  7. If you are evaluating a number of expressions that have some common elements, it is often a good idea to break up the calculations by first evaluating the common sub-expressions and then using these values in making the final calculations. This is both more efficient in computer time and likely to make the resulting code more readable (and hence less prone to errors). As an example, the area, A, of a triangle with sides a, b, and c is

    Rather than use this formula directly, it is a much better idea to first calculate the value of the semi-perimeter, s, of the triangle

    and then use this to determine the area

  8. Although Java's increment (++) and decrement (--) operators can be combined with other operators in expressions, it is not usually a good idea to do so. In your programming, you should always be aiming for maximal clarity rather than maximal compression.
  9. Although it may take a bit more writing, it is almost always better to create new variables for new purposes, rather than re-using old ones. For example, if we want to find the sum of x, y, and z, it is much clearer to write

    double total = x + y + z;
    than it is to write
    x += y + z; 

Debugging

  1. If a program is running but producing incorrect results, it is often useful to trace its progress to help you find out where things are going wrong. To perform a trace on a variable, note the points at which the variable is assigned values and print those values, along with some text that identifies the source of the value being printed.

    For example, to trace the value of the variable sum at some point, you could insert the following tracing statement in your program.
    System.out.println("At A, sum = " + sum); 
    The phrase "at A" identifies the location in the program. If sum changes value at some other point, you may want to insert another tracing statement that prints At B, sum = ...
  2. Many systems have debuggers - programs that can be used to trace variables easily. The operation of debuggers varies widely but any debugger should have facilities to

    If your system does have a debugger, it is well worth your while to learn how to use at least the basic features.

Exercise 2.7

    1. 6 / 4*2
    2. 2 * 3/2
    3. (int) 2.7*1. 8
    4. (int)2.7*(int)1.8
    5. (int)(2.7*1.8)
  1. For each expression, determine its mathematical value and then run a Java program that prints the value of the expression. Explain any differences.
    1. 3.0*(1.0/3.0 + 100.0 - 100.0)
    2. (5E305 + 7E307)*10
    3. 18 + 1E18 - 1E18
    4. 1E18 - 1E18 + 18
    5. 1E-200 * 1E-200 * lE200 * lE200
    6. 1E200 * lE200 * lE-200 * lE-200
  2. A quadratic equation of the form ax2 + bx + c = 0 has roots

    Write a fragment that efficiently determines the values of the roots (rootl and root2) of the quadratic equation with coefficients a, b, and c. Assume that all the variables have been declared as type double and that the equation has two real roots.
  3. Rewrite the statement
       m = n-- * ++p;  
    using three statements.