java-foundations 18 min read

Type Casting & Floating-Point Precision

How to force decimal division, why 0.1 + 0.2 ≠ 0.3, and how printf brings order to floating-point chaos

Reading: Reges & Stepp: Ch. 2 §2.2–2.3

Part 1 — The Truncation Problem

Java divides two int values using integer division: it computes the quotient and throws away the remainder.

int total = 397;
int count = 5;
double average = total / count;   // 79 stored as 79.0 — WRONG!

Step by step:

  1. total / count — both are int → integer division → 79 (remainder 2 is discarded)
  2. 79 is stored in average as 79.0

The double declaration did not help. The truncation happened before the assignment.

This is the per-operation rule: Java evaluates each operation in isolation based on the types present at that moment. The fact that the result will be stored in a double is irrelevant — the division already happened with two ints.


Part 2 — The cast Operator

A cast explicitly converts a value from one type to another. The syntax: (targetType) value.

double average = (double) total / count;

What happens:

  1. (double) total — converts total from int 397 to double 397.0
  2. 397.0 / count — double divided by int → Java promotes count to 5.0
  3. 397.0 / 5.0 = 79.4

The cast attaches to the value immediately to its right. It does not apply to the whole expression.

Cast placement is critical

int total = 397, count = 5;

double a = (double) total / count;      // 79.4  ← CORRECT
double b = total / (double) count;      // 79.4  ← also correct
double c = (double) (total / count);    // 79.0  ← WRONG! divides first, then casts
double d = total / count;               // 79.0  ← WRONG! no cast at all

Line C is the common trap: you remembered to cast, but put the parentheses around the whole division. The integer division 397 / 5 = 79 happens first, then (double) 79 = 79.0. The truncation already happened before the cast got there.

Rule: Cast one of the operands, before the division. Either operand works:

(double) total / count     // cast the numerator
total / (double) count     // cast the denominator
What about other casts?

You can cast between any numeric types:

double price = 9.99;
int truncated = (int) price;       // 9 — decimal part discarded, no rounding
long bigNumber = 9876543210L;
int narrowed  = (int) bigNumber;   // may lose data if value doesn't fit

// char ↔ int (each char has a numeric code)
char letter = 'A';
int code = (int) letter;           // 65
char back = (char) 66;             // 'B'

For CSCD 210, you primarily need (double) to force decimal division. The others are here for completeness.


Part 3 — Type Promotion Rules

When two different numeric types appear in an expression, Java automatically promotes the smaller type to the larger before computing.

Promotion hierarchy (smaller → larger): byte → short → int → long → float → double

Promotion is per-operation, not per-expression:

System.out.println(10 / 4);            // 2    — int/int, no promotion
System.out.println(10 / 4 + 3.0);     // 5.0  — (2) + 3.0: int 2 promoted to 2.0
System.out.println(10.0 / 4 + 3);     // 5.5  — (2.5) + 3: int 3 promoted to 3.0
System.out.println((double) 10 / 4);  // 2.5  — 10.0/4: 4 promoted to 4.0

Line 2 is the key: 10 / 4 evaluates to 2 (integer division, truncated) before 3.0 is ever seen. The 3.0 cannot undo the truncation that already happened. To get 5.5, you need 10.0 / 4 + 3.0 or (double) 10 / 4 + 3.0.

Predict Before You Run
What does System.out.println(7 / 2 * 1.0) print?
A3.5
B3.0
C3
D3.50
Answer: B. Left-to-right at the same precedence level: 7 / 2 first — both ints → 3 (truncated). Then 3 * 1.0 — int × double → 3.0. The double arrived too late to prevent the truncation. To get 3.5, cast first: (double) 7 / 2 * 1.0.

Part 4 — Floating-Point Representation

double stores numbers in binary (base 2). Most decimal fractions — including 0.1 and 0.2 — cannot be represented exactly in binary, the same way 1/3 cannot be written exactly in decimal.

System.out.println(0.1 + 0.2);         // 0.30000000000000004 — not 0.3!
System.out.println(0.1 + 0.2 == 0.3);  // false

This is not a Java bug. It happens in every language that uses the IEEE 754 standard for floating-point: C, Python, JavaScript, all of them.

The error is tiny — about 5 × 10⁻¹⁷ — but == is exact. Even a difference of 0.00000000000000004 causes == to return false.

What this means for your code

Never compare doubles with ==:

double result = 0.1 + 0.2;

// WRONG: almost always false due to tiny rounding error
if (result == 0.3) { ... }

// CORRECT: check if they're "close enough"
if (Math.abs(result - 0.3) < 0.000001) { ... }

Use printf to hide the noise in output:

double average = 79.4;
System.out.println(average);           // 79.4 — clean in this case
System.out.println(0.1 + 0.2);        // 0.30000000000000004 — ugly
System.out.printf("%.2f%n", 0.1+0.2); // 0.30 — printf rounds for display

printf rounds the value for display purposes only. The underlying double still has the imprecision — it just doesn’t show.


Part 5 — printf for Controlled Precision

System.out.printf(formatString, values...) formats output with exact control over decimal places, column widths, and alignment.

Format specifiers

Specifier Type Example Output
%.2f double printf("%.2f", 79.4) 79.40
%.0f double printf("%.0f", 15.0) 15
%d int printf("%d", 397) 397
%s String printf("%s", "Alice") Alice
%n newline (platform newline)

%.2f means: print a floating-point number (f) rounded to 2 decimal places (.2). Java pads trailing zeros: 79.4 becomes 79.40.

Multiple values in one call

double average = 79.4;
int passingCount = 4;
System.out.printf("Average: %.2f — %d passing%n", average, passingCount);
// Average: 79.40 — 4 passing

Values are substituted left-to-right: first %.2f gets average, then %d gets passingCount.

%n vs \n

Use %n inside printf instead of \n. %n outputs the platform-correct line separator (important on Windows), while \n hardcodes Unix-style endings. Both work on macOS/Linux, but %n is the correct choice in printf.


Part 6 — Lab 5 CP3 Pattern

int total = 397;   // accumulated with += in CP1
int count = 5;

// Cast before dividing — not after
double average = (double) total / count;   // 397.0 / 5 = 79.4

// printf formats to exactly 2 decimal places
System.out.printf("Class average: %.2f%n", average);   // Class average: 79.40

Without (double): 397 / 5 = 79 → average stored as 79.0 → printf shows 79.00. The test checks for 79.40 — if you see 79.00, the cast is missing or in the wrong place.

The floating-point imprecision note from CP3:

// This is NOT the class average — just demonstrating the precision issue
double tenth = 0.1 + 0.2;
System.out.println(tenth);                  // 0.30000000000000004
System.out.printf("%.2f%n", tenth);         // 0.30 — printf rounds for display

What You Learned

  • (double) cast before a division forces decimal arithmetic: (double) total / 5 gives 79.4, not 79
  • Cast placement matters: (double) (total / count) casts after truncation — wrong order, wrong answer
  • Type promotion is per-operation: 10 / 4 + 3.0 = 5.0 because 10/4 truncates before 3.0 is seen
  • 0.1 + 0.2 is not exactly 0.3 in any language — use Math.abs() for equality, printf for display
  • printf("%.2f", value) rounds to 2 decimal places and pads trailing zeros: 79.479.40

What Comes Next

Next: loops — instead of writing total += s1; total += s2; ... five times, you write total += score once inside a loop and let the loop handle the repetition. The accumulation pattern from lesson-2c runs exactly the same way inside a loop.