You add three test scores, divide by three, and print the result. The program prints 82. The number that should have come out was 82.33. Nothing raised an error and nothing warned you. The compiler accepted every line, and 82 is a believable score, which is exactly why it went unnoticed.
This article uses one small file. You will compile it, run it, look at the wrong number, change one thing, and run it again.
Key takeaways
int / intis integer division: the fraction is cut off, not rounded, so247 / 3is82.%returns what the division dropped:247 % 3is1.- Casting the result (
(double) (sum / count)) prints82.0, because the division already happened inint. - Cast one operand (
(double) sum / count) to divide in floating point and get82.33333333333333. - A
doublecannot hold every decimal, since0.1 + 0.2is0.30000000000000004, which is why money usesBigDecimal. - A test input that divides evenly, like 246 by 3, hides the bug in both versions.
The program that prints 82
Save this as Average.java. It holds three scores, adds them in a loop, and divides the sum by the length of the array.
// Average.java
public class Average {
public static void main(String[] args) {
int[] scores = {80, 75, 92};
int sum = 0;
for (int score : scores) {
sum += score;
}
int average = sum / scores.length;
System.out.println("sum: " + sum);
System.out.println("average: " + average);
}
}
Compile it and run it with two commands:
javac Average.java
java Average
You should see this:
sum: 247
average: 82
The sum is right, because 80 + 75 + 92 is 247. The average is not, because 247 / 3 is 82.33. The line int average = sum / scores.length; reads like the textbook formula, so a reviewer sees a sum divided by a count and moves on. The output is a whole number in a plausible range. Nothing about 82 looks broken.
What / does when both sides are int
The JVM decides the type of a division from its two operands. Here sum is an int and scores.length is an int, so / performs integer division and the result is an int. An int has no room for a fraction, so the fraction is dropped.
Dropped is not the same as rounded. It is cut off. 247 / 3 is 82, and 5 / 2 is 2, where rounding would have given you 3. The program never sees the .33 at all.
The % operator hands you the part that was dropped. 247 % 3 is 1: three goes into 247 eighty-two times, and one is left over. Put the pair together and nothing is lost, since 82 * 3 + 1 is 247.
Cast before the division, not after
The obvious repair is to ask for a double. Change the line to this:
double average = (double) (sum / scores.length);
It compiles without a warning and prints 82.0. That looks like a double, and a double looks precise, but it is not the average. The parentheses make the division run first, in int, and the result is 82. The cast then widens 82 to 82.0. The damage was done before the cast got a turn.
The cast has to sit on an operand: (double) sum / scores.length. A cast binds tighter than /, so sum becomes a double first. With one double on the left, Java widens the other operand to match and divides in floating point. The result is 82.33333333333333.
Here is the fixed file. It changes the type of average, moves the cast onto sum, and adds one line that prints the remainder explained above.
// Average.java
public class Average {
public static void main(String[] args) {
int[] scores = {80, 75, 92};
int sum = 0;
for (int score : scores) {
sum += score;
}
double average = (double) sum / scores.length;
System.out.println("sum: " + sum);
System.out.println("average: " + average);
System.out.println("remainder: " + sum % scores.length);
}
}
Run the same two commands:
sum: 247
average: 82.33333333333333
remainder: 1
Precedence and the rest of the operators
Multiplication, division and remainder bind tighter than addition and subtraction. So 1 + 2 * 3 is 7, while (1 + 2) * 3 is 9. Parentheses change which operation runs first. The cast bug is the same kind of mistake: the order Java evaluates things in decides the answer.
Integer division shows up outside averages too. 10 / 3 is 3. Make one operand a double and 10.0 / 3 is 3.3333333333333335. One .0 on one side turns a whole number into a long fraction.
Look at both long results again. 82.33333333333333 and 3.3333333333333335 do not end the way you would write them on paper. That leads to the one thing this article does not settle.
What this does not cover
A double cannot hold every decimal exactly. It stores binary fractions, and a value like 0.1 has no exact binary form. Add two of them and you can see the gap: 0.1 + 0.2 is 0.30000000000000004.
For money, that gap matters, and Java has BigDecimal for it. We will not use it here. An average of scores is not money. The error sits far past any digit a reader of a score would care about, so double is the right tool for this program.
Before the next article
Change one score so the three add up to 246: use 80, 75 and 91. Run the starter version, then the fixed version. This is what I measured. The truncating version prints 82, and the cast version prints 82.0. They agree, and neither answer looks wrong, because 246 divides by 3 exactly.
A data set that divides evenly hides the bug. Your task is to explain, in your own words, why the bug comes back the moment the sum is not divisible by the count.
If a result looks right on one input, which input would make it wrong?