Java Module 2 – Variables, types and operators
Floating point, rounding and BigDecimal
Why 0.1 + 0.2 prints 0.30000000000000004 in Java, how to compare doubles and handle NaN, and how to compute money with BigDecimal and a RoundingMode.
What you will learn
- Explain why 0.1 + 0.2 is not exactly 0.3 in double arithmetic
- Compare doubles safely and handle NaN, infinities and -0.0
- Predict what Math.round, Math.rint, %.2f and a cast to int do to a double
- Calculate money with BigDecimal using an explicit scale and RoundingMode
- Split a tax-inclusive price so that its two parts always add up to it
Before you start
What you’ll learn · Before you start
What you will learn
- Explain why 0.1 + 0.2 is not exactly 0.3 in double arithmetic
- Compare doubles safely and handle NaN, infinities and -0.0
- Predict what Math.round, Math.rint, %.2f and a cast to int do to a double
- Calculate money with BigDecimal using an explicit scale and RoundingMode
- Split a tax-inclusive price so that its two parts always add up to it
Before you start
On this page
In Java, 0.1 + 0.2 prints 0.30000000000000004, and 0.1 + 0.2 == 0.3 is false. Nothing is broken: a double is
an IEEE 754: The standard for binary floating-point arithmetic that float and double follow: float is binary32 with 24 significant bits, double is binary64 with 53, and every operation rounds its exact result to the nearest representable value. binary64 number with 53 significant bits, and a decimal fraction such as 0.1
cannot be written in base 2 with finitely many digits, so Java rounds it to the closest double it can represent. The
tiny differences that follow are normal and predictable.
For measurements and science that is fine, as long as you compare doubles with a tolerance and know what infinity and
NaN mean. For money it is not: amounts must be exact and must round the way an invoice says, and that is the job of
java.math.BigDecimal, a decimal number with a scale and a rounding mode that you choose. You will see the stored
values on JDK 25, step through the sum that misses 0.3, compare and round doubles safely, and split a price into its
base and its tax so that the parts always add up.
A double holds the nearest binary fraction
In base 2 the places after the point are worth 1/2, 1/4, 1/8, 1/16 and so on. A value such as 0.75, which is 1/2
plus 1/4, needs two of those places and is stored exactly. One tenth can never be built from them, because 10 is 2 × 5
and no power of two divides by 5, so its binary digits repeat for ever and the double keeps only the first 53
significant bits. new BigDecimal(double) converts a double without any rounding, so it prints the value the double
really holds:
// What a double really holds: new BigDecimal(double) shows every digit of the stored value.
void main() {
IO.println("0.1 + 0.2 = " + (0.1 + 0.2));
IO.println("0.1 + 0.2 == 0.3 : " + (0.1 + 0.2 == 0.3));
IO.println("stored 0.1 = " + new BigDecimal(0.1));
IO.println("stored 0.2 = " + new BigDecimal(0.2));
IO.println("their exact sum = " + new BigDecimal(0.1).add(new BigDecimal(0.2)));
IO.println("0.1 + 0.2 holds = " + new BigDecimal(0.1 + 0.2));
IO.println("stored 0.3 = " + new BigDecimal(0.3));
IO.println("stored 19.99 = " + new BigDecimal(19.99));
double sum = 0;
for (int i = 0; i < 10; i++) sum += 0.1;
IO.println("ten 0.1s added = " + sum);
IO.println("gap after 1.0 = " + Math.ulp(1.0));
long big = 9_007_199_254_740_993L; // 2 to the power 53, plus 1
IO.println(big + " as a double = " + (double) big);
} from session import run, version
version()
run("""
java Stored.java
""") Output
# openjdk version "25.0.4.1" LTS $ java Stored.java 0.1 + 0.2 = 0.30000000000000004 0.1 + 0.2 == 0.3 : false stored 0.1 = 0.1000000000000000055511151231257827021181583404541015625 stored 0.2 = 0.200000000000000011102230246251565404236316680908203125 their exact sum = 0.3000000000000000166533453693773481063544750213623046875 0.1 + 0.2 holds = 0.3000000000000000444089209850062616169452667236328125 stored 0.3 = 0.299999999999999988897769753748434595763683319091796875 stored 19.99 = 19.989999999999998436805981327779591083526611328125 ten 0.1s added = 0.9999999999999999 gap after 1.0 = 2.220446049250313E-16 9007199254740993 as a double = 9.007199254740992E15
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stored_session.py
The double for 0.1 is a little above 0.1 and the double for 0.2 a little above 0.2, so their sum lands on the double
just above 0.3, while the literal 0.3 is stored as the double just below it. Two different doubles, so == is
false. Step through the four values the run printed around 0.3:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 0.1, stored. One tenth has no exact binary form, so 0.1 is stored as the nearest double, 0.1000000000000000055…, a little above 0.1.
- 0.2, stored. 0.2 is stored the same way, as 0.2000000000000000111…, also a little above.
- The exact sum. Added exactly, the two stored values make 0.3000000000000000166…, the line their exact sum of the run. That number is not a double either.
- Two neighbours. The doubles on each side of it are 0.2999999999999999888… and 0.3000000000000000444…, about 5.6 × 10⁻¹⁷ apart, and the exact sum lies exactly halfway between them.
- The tie is broken. An addition rounds its exact result to the nearest double, and a tie goes to the neighbour whose last bit is 0, here the one above. So 0.1 + 0.2 holds 0.3000000000000000444…, which prints as 0.30000000000000004.
- 0.3 is the other one. The literal 0.3 is stored as its own nearest double, the one below, as the line stored 0.3 shows. Two different doubles, so 0.1 + 0.2 == 0.3 is false.
Adding 0.1 ten times misses 1.0 the same way. Math.ulp(1.0) prints 2.220446049250313E-16: that is the
ulp: Unit in the last place: the gap between a floating-point number and the next larger one of its type. Math.ulp(1.0) is 2.220446049250313E-16, and the gap grows with the size of the number. of 1.0, how far it is from its neighbour above, and the reason a double carries roughly 15
or 16 decimal digits that you can trust. Whole numbers are exact only up to 2⁵³: the last line shows
9,007,199,254,740,993 coming back as …992, the nearest double.
Printing hides all this. Double.toString writes the fewest digits that still identify the double uniquely, so the
double for 0.1 comes out as 0.1 rather than its 55 exact digits after the point; the difference shows only once
arithmetic lands on a neighbouring double, as 0.1 + 0.2 does.
Key idea
A double holds the binary fraction nearest to the decimal you wrote, so most decimals are a little off before any arithmetic starts, and every operation rounds its exact result to a double again.
Checkpoint
Check: What a double stores
JDK 17 made arithmetic identical everywhere, and JDK 19 changed the printing
Two releases changed Java’s floating point, and code written for older JDKs still shows both. Walk through how the rules moved:
- Java 1.0
IEEE 754 and strict results from the start
float and double were IEEE 754 binary32 and binary64 numbers, and every floating-point expression gave exactly the same result on every computer.
- Java SE 1.2
A looser default mode, and strictfp strictfp added
To make arithmetic on the x87 unit of x86 processors cheaper, the default rules were loosened, and the strictfp modifier was added for code that needed the original, exactly reproducible results.
- JDK 17
Strict everywhere again strictfp redundant
JEP 306 made every floating-point expression strict. Arithmetic results are bit for bit the same on every platform, strictfp changes nothing, and javac warns that it is not required.
- JDK 19
The shortest digits when printing printing changed
Double.toString and Float.toString print the shortest decimal that identifies the value. 2e23 prints as 2.0E23 where JDK 17 printed 1.9999999999999998E23; the double itself is the same.
The timeline has 4 events, from Java 1.0 to JDK 19.
Floating-point arithmetic in Java, the operators such as + and * and the conversions between types, gives
bit-for-bit identical results on every platform. Before JDK 17 that was guaranteed only inside code marked strictfp;
JEP 306 made every floating-point expression strict, so the modifier no longer changes anything and javac says so:
// strictfp was needed before JDK 17 for results that are the same on every machine; now it changes nothing.
strictfp double area(double radius) {
return Math.PI * radius * radius;
}
void main() {
IO.println(area(2.5));
} from session import run, version
version()
run("""
javac Strict.java
java Strict
""") Output
# openjdk version "25.0.4.1" LTS
$ javac Strict.java
Strict.java:2: warning: [strictfp] as of release 17, all floating-point expressions are evaluated strictly and 'strictfp' is not required
strictfp double area(double radius) {
^
1 warning
$ java Strict
19.634954084936208
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 strict_session.py
You can delete strictfp from old code without changing a single result. The methods of Math, such as Math.sin,
are a different matter: their documentation allows the last bit to differ from one platform to another, while
StrictMath.sin is defined to give the same result everywhere.
Double.toString has followed the shortest-digits rule exactly since JDK 19. Older releases sometimes printed a
longer string than needed, with different last digits. The same class, run on JDK 17 and then on JDK 25:
public class Shortest {
public static void main(String[] args) {
System.out.println(2e23 + " " + 1e23 + " " + 0.1);
}
} from session import run, version
version("17")
run("java Shortest.java", jdk="17")
version()
run("java Shortest.java") Output
# openjdk version "17.0.20.1" $ java Shortest.java 1.9999999999999998E23 9.999999999999999E22 0.1 # openjdk version "25.0.4.1" LTS $ java Shortest.java 2.0E23 1.0E23 0.1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shortest_session.py
The doubles are the same on both; only the printed text changed. If you compare printed doubles in tests, for example after an upgrade from JDK 17, expect a few of them to look different.
JDK 17 and JDK 19
From JDK 17, strictfp changes no result and javac warns that it is not required. From JDK 19, 2e23 prints as
2.0E23, where JDK 17 printed 1.9999999999999998E23 for the same double.
Checkpoint
Check: Strict results and shortest printing
Compare computed doubles with a tolerance
A computed double usually carries some rounding error, so testing it with == demands more precision than the
arithmetic delivered. Test whether the two values are near enough to each other instead. Java’s standard library has
no ready-made method for this, so write a small one:
// Comparing computed doubles: a relative tolerance, plus an absolute one for values near zero.
boolean close(double a, double b, double relTol, double absTol) {
return Math.abs(a - b) <= Math.max(relTol * Math.max(Math.abs(a), Math.abs(b)), absTol);
}
void main() {
double total = 0.1 + 0.2;
double balance = 0.1 + 0.2 - 0.3; // should be zero
IO.println("total == 0.3 : " + (total == 0.3));
IO.println("close(total, 0.3) : " + close(total, 0.3, 1e-9, 0));
IO.println("balance = " + balance);
IO.println("close(balance, 0), rel : " + close(balance, 0, 1e-9, 0));
IO.println("close(balance, 0), abs : " + close(balance, 0, 1e-9, 1e-9));
IO.println("difference from 0.3 = " + Math.abs(total - 0.3));
IO.println("relative allowance, 0.3 = " + 1e-9 * Math.max(Math.abs(total), 0.3));
IO.println("relative allowance, 0 = " + 1e-9 * Math.max(Math.abs(balance), 0));
} from session import run, version
version()
run("""
java Close.java
""") Output
# openjdk version "25.0.4.1" LTS $ java Close.java total == 0.3 : false close(total, 0.3) : true balance = 5.551115123125783E-17 close(balance, 0), rel : false close(balance, 0), abs : true difference from 0.3 = 5.551115123125783E-17 relative allowance, 0.3 = 3.0000000000000005E-10 relative allowance, 0 = 5.551115123125783E-26
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 close_session.py
close measures the difference against the bigger of the two magnitudes. With relTol at 1e-9, two values pass
when they agree to roughly nine significant digits, whether they are tiny or huge. The idea breaks down next to zero:
a relative test scales the allowed difference by the size of the numbers, and when the expected value is 0 the
allowed difference shrinks to nothing. So balance, which ought to be 0 but holds 5.6 × 10⁻¹⁷, is rejected. Step
through the three calls with the numbers the last three lines of the run printed:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- total against 0.3. close(total, 0.3, 1e-9, 0) starts with the difference: total is 0.30000000000000004, so |total − 0.3| is 5.551115123125783E-17, as the run printed.
- The relative allowance. The allowance grows with the numbers compared. 1e-9 times the larger of the two, about 0.3, is 3.0000000000000005E-10, millions of times more than the difference.
- Close. absTol is 0 here, so the relative allowance decides, and the difference is far below it. close(total, 0.3) is true.
- balance against 0. balance should be 0 and holds 5.551115123125783E-17. Against 0 the larger size is that tiny number itself, so the relative allowance shrinks to 5.551115123125783E-26.
- Not close. With absTol at 0 the allowance is smaller than the difference, so close(balance, 0, 1e-9, 0) is false. A relative test cannot pass a value that ought to be 0.
- An absolute allowance. close(balance, 0, 1e-9, 1e-9) adds an absolute allowance of 1e-9, a difference accepted whatever the sizes. Now the larger allowance is 1e-9, and the test is true.
The absTol argument covers the case near zero: it is a difference you accept whatever the sizes. Pick it from what
the numbers mean; for lengths in metres, 1e-9 is one nanometre.
Testing a computed double with ==
if (total == 0.3) is false for total = 0.1 + 0.2, although the arithmetic did nothing wrong. Write
close(total, 0.3, 1e-9, 1e-9), with tolerances chosen for what the numbers mean.
Checkpoint
Check: Comparing with a tolerance
Floating point never throws: infinity, NaN and minus zero
Floating-point arithmetic never throws. A result that is too large becomes infinity, and an operation with no sensible answer gives NaN: Not a Number: the double or float that 0.0 / 0.0 and Math.sqrt(-1) give. Every comparison with NaN is false, itself included, so it is found with Double.isNaN(x), never with ==., “not a number”:
// Infinity, NaN and the two zeros: double arithmetic never throws.
void main() {
double nan = 0.0 / 0.0;
IO.println("1.0 / 0 = " + 1.0 / 0 + ", -1.0 / 0 = " + -1.0 / 0);
IO.println("0.0 / 0.0 = " + nan + ", Math.sqrt(-1) = " + Math.sqrt(-1));
IO.println("nan == nan : " + (nan == nan) + ", Double.isNaN(nan): " + Double.isNaN(nan));
IO.println("Double.compare(nan, nan) = " + Double.compare(nan, nan)
+ ", Double.valueOf(nan).equals(nan): " + Double.valueOf(nan).equals(nan));
IO.println("0.0 == -0.0 : " + (0.0 == -0.0) + ", Double.compare(0.0, -0.0) = " + Double.compare(0.0, -0.0)
+ ", 1 / -0.0 = " + 1 / -0.0);
IO.println("MAX_VALUE * 2 = " + Double.MAX_VALUE * 2 + ", MAX_VALUE + 1 == MAX_VALUE: " + (Double.MAX_VALUE + 1 == Double.MAX_VALUE));
} from session import run, version
version()
run("""
java Specials.java
""") Output
# openjdk version "25.0.4.1" LTS $ java Specials.java 1.0 / 0 = Infinity, -1.0 / 0 = -Infinity 0.0 / 0.0 = NaN, Math.sqrt(-1) = NaN nan == nan : false, Double.isNaN(nan): true Double.compare(nan, nan) = 0, Double.valueOf(nan).equals(nan): true 0.0 == -0.0 : true, Double.compare(0.0, -0.0) = 1, 1 / -0.0 = -Infinity MAX_VALUE * 2 = Infinity, MAX_VALUE + 1 == MAX_VALUE: true
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 specials_session.py
1.0 / 0isInfinity, while the integer division1 / 0throws.Double.MAX_VALUE * 2overflows toInfinity, andDouble.MAX_VALUE + 1is stillDouble.MAX_VALUE, because 1 is far smaller than the gap between doubles there.0.0 / 0.0andMath.sqrt(-1)are NaN. NaN compares false with everything, itself included, sonan == nanis false; test for it withDouble.isNaN.Double.compareandDouble.valueOf(nan).equalstreat NaN as equal to itself, which keeps sorting and hash maps working.-0.0 == 0.0is true, but the two zeros differ:1 / -0.0is negative infinity, andDouble.compare(0.0, -0.0)is 1. A sort puts -0.0 before 0.0.
The run shows two ways of comparing that disagree on exactly these values. Read the two cases side by side:
| Compared | == says |
Double.compare says |
|---|---|---|
| NaN with NaN | false, not equal | 0, equal |
| 0.0 with -0.0 | true, equal | 1, 0.0 is the larger |
== follows IEEE 754, which a sort or a hash map cannot use; Double.compare and equals give every double one
place in a single order.
Checkpoint
Check: Infinity, NaN and minus zero
Each way of rounding a double has its own rule
Math.round, Math.rint, Math.floor, Math.ceil, %.2f and a cast to int each turn a double into fewer digits,
and no two of them follow the same rule. The program runs them on the same values:
// Ways to round a double, and the one that quietly cuts instead.
void main() {
IO.println("Math.round(2.5) = " + Math.round(2.5) + ", Math.round(-2.5) = " + Math.round(-2.5));
IO.println("Math.rint(2.5) = " + Math.rint(2.5) + ", Math.rint(3.5) = " + Math.rint(3.5));
IO.println("Math.floor(-2.5) = " + Math.floor(-2.5) + ", Math.ceil(-2.5) = " + Math.ceil(-2.5));
IO.println("%.2f of 2.675 = " + String.format("%.2f", 2.675));
IO.println("stored 2.675 = " + new BigDecimal(2.675));
IO.println("new BigDecimal(2.675), 2 places, HALF_UP = " + new BigDecimal(2.675).setScale(2, RoundingMode.HALF_UP));
IO.println("BigDecimal.valueOf(2.675), 2 places, HALF_UP = " + BigDecimal.valueOf(2.675).setScale(2, RoundingMode.HALF_UP));
IO.println("0.57 * 100 = " + 0.57 * 100 + ", (int) gives " + (int) (0.57 * 100) + ", Math.round gives " + Math.round(0.57 * 100));
} from session import run, version
version()
run("""
java Rounding.java
""") Output
# openjdk version "25.0.4.1" LTS $ java Rounding.java Math.round(2.5) = 3, Math.round(-2.5) = -2 Math.rint(2.5) = 2.0, Math.rint(3.5) = 4.0 Math.floor(-2.5) = -3.0, Math.ceil(-2.5) = -2.0 %.2f of 2.675 = 2.68 stored 2.675 = 2.67499999999999982236431605997495353221893310546875 new BigDecimal(2.675), 2 places, HALF_UP = 2.67 BigDecimal.valueOf(2.675), 2 places, HALF_UP = 2.68 0.57 * 100 = 56.99999999999999, (int) gives 56, Math.round gives 57
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rounding_session.py
Math.roundreturns the nearestlongand sends a tie upwards, toward positive infinity:Math.round(2.5)is 3 andMath.round(-2.5)is -2.Math.rintsends a tie to the even neighbour, so 2.5 becomes 2.0 and 3.5 becomes 4.0.Math.floorrounds down andMath.ceilrounds up.String.format("%.2f", 2.675)gives2.68. Java’s formatter rounds the shortest decimal form of the double, the digitsDouble.toStringprints, with the half-up rule, so it sees “2.675” and rounds up. The exact stored value is a little below 2.675, so rounding that value gives 2.67, which is whatnew BigDecimal(2.675)does, whileBigDecimal.valueOf(2.675)starts from the digits “2.675” and gives 2.68.0.57 * 100is 56.99999999999999, and a cast tointcuts the fraction off, giving 56. UseMath.roundwhen a floating-point result has to become a whole number.
The third point is the one that surprises people. Step through what each of the three calls reads of 2.675 before it rounds:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The literal. 2.675 in the code is a decimal, but the double nearest it is 2.67499999999999982…, a little below 2.675, as the line stored 2.675 of the run shows.
- One double, two readings. Every rounding below starts from that one double. What differs is what it reads of it, the exact stored value or the short digits Double.toString prints for it, 2.675.
- %.2f. String.format rounds the shortest decimal form, the digits 2.675, with the half-up rule. The third decimal is a 5, so it rounds up and prints 2.68.
- The exact value. new BigDecimal(2.675) copies the stored value exactly, 2.67499999…, and rounding that to 2 places with HALF_UP gives 2.67, because its third decimal is a 4.
- From the digits. BigDecimal.valueOf(2.675) goes through Double.toString, so it starts from the digits 2.675 and rounds to 2.68, the same answer as %.2f.
- Choose what is rounded. When a rule says how to round, start from the decimal itself, new BigDecimal("2.675") or BigDecimal.valueOf, never from new BigDecimal(double), which rounds the binary value.
Checkpoint
Check: Rounding a double
BigDecimal keeps decimals exact with a scale and a rounding mode
A BigDecimal stores a whole number of any size, its unscaled value, and a
scale: The number of digits after the decimal point of a BigDecimal: 19.99 has scale 2. A BigDecimal is its unscaled whole number times 10 to the power minus its scale, so 2.0 and 2.00 have the same value but different scales., the number of digits after the decimal point. The diagram shows both parts
for 19.99, 2.0 and 2.00:
A BigDecimal stores a whole number and how many digits stand after the point
Text description of the diagram
A BigDecimal's value is its unscaled value times 10 to the power minus its scale. Three examples:
- `new BigDecimal("19.99")`: unscaled value 1999, scale 2, so the value is exactly 19.99.
- `new BigDecimal("2.0")`: unscaled value 20, scale 1, value 2.0.
- `new BigDecimal("2.00")`: unscaled value 200, scale 2, value 2.00.
The last two have the same value and different scales. `compareTo` looks at the value only, so it says they are equal; `equals` also compares the scale, so it says they are not.
Because it counts in powers of 10, every decimal amount you can write is exact. Addition, subtraction and multiplication are always exact; division and rounding need you to say how many digits to keep and which rounding mode: The rule a BigDecimal follows when it must drop digits, given as a RoundingMode: HALF_UP sends a tie away from zero, HALF_EVEN to the even neighbour, DOWN cuts toward zero, and UNNECESSARY throws if any digit would be lost. to use:
Java (Money.java) · Money.java
// Decimal arithmetic with BigDecimal: exact sums, an explicit scale and rounding mode for division.
void main() {
var price = new BigDecimal("19.99");
IO.println("3 × 19.99 = " + price.multiply(BigDecimal.valueOf(3)));
IO.println("0.1 + 0.2 = " + new BigDecimal("0.1").add(new BigDecimal("0.2")));
IO.println("new BigDecimal(0.1) = " + new BigDecimal(0.1));
IO.println("BigDecimal.valueOf(0.1) = " + BigDecimal.valueOf(0.1));
var bill = new BigDecimal("100.00");
try {
IO.println(bill.divide(BigDecimal.valueOf(3)));
} catch (ArithmeticException e) {
IO.println("100.00 / 3: " + e.getMessage());
}
var share = bill.divide(BigDecimal.valueOf(3), 2, RoundingMode.DOWN);
var last = bill.subtract(share.multiply(BigDecimal.valueOf(2)));
IO.println("three shares: " + share + " + " + share + " + " + last);
var a = new BigDecimal("2.345");
IO.println("2.345 HALF_UP " + a.setScale(2, RoundingMode.HALF_UP) + ", HALF_EVEN " + a.setScale(2, RoundingMode.HALF_EVEN)
+ ", DOWN " + a.setScale(2, RoundingMode.DOWN));
} Java (Equality.java) · Equality.java
// equals compares value and scale; compareTo compares value only.
void main() {
var two = new BigDecimal("2.0");
var twoAgain = new BigDecimal("2.00");
IO.println("scales: " + two.scale() + " and " + twoAgain.scale());
IO.println("equals: " + two.equals(twoAgain) + ", compareTo: " + two.compareTo(twoAgain));
IO.println("in a HashSet: " + new HashSet<>(List.of(two, twoAgain)).size() + " elements");
IO.println("in a TreeSet: " + new TreeSet<>(List.of(two, twoAgain)).size() + " element");
IO.println("after stripTrailingZeros: " + two.stripTrailingZeros().equals(twoAgain.stripTrailingZeros()));
} from session import run, version
version()
run("""
java Money.java
java Equality.java
""") Output
# openjdk version "25.0.4.1" LTS $ java Money.java 3 × 19.99 = 59.97 0.1 + 0.2 = 0.3 new BigDecimal(0.1) = 0.1000000000000000055511151231257827021181583404541015625 BigDecimal.valueOf(0.1) = 0.1 100.00 / 3: Non-terminating decimal expansion; no exact representable decimal result. three shares: 33.33 + 33.33 + 33.34 2.345 HALF_UP 2.35, HALF_EVEN 2.34, DOWN 2.34 $ java Equality.java scales: 1 and 2 equals: false, compareTo: 0 in a HashSet: 2 elements in a TreeSet: 1 element after stripTrailingZeros: true
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 money_session.py
- Build a BigDecimal from a string,
new BigDecimal("19.99"), or from a whole number.new BigDecimal(0.1)copies the double exactly, error and all;BigDecimal.valueOf(0.1)goes throughDouble.toStringand gives 0.1. - Divide with a scale and a rounding mode.
bill.divide(three)throws, because 100.00 / 3 has no finite decimal form.divide(three, 2, RoundingMode.DOWN)gives 33.33, and the last share takes what is left, 33.34, so the three shares add up to the bill. - Choose the rounding mode on purpose.
HALF_UPis the school rule;HALF_EVEN, sometimes called banker’s rounding, sends ties to the even neighbour, so that rounding many values does not drift upwards;DOWNcuts. The rule for an invoice or a tax comes from the business or the law, so check which one applies. equalscompares the scale too. 2.0 has scale 1 and 2.00 scale 2, soequalssays they differ, whilecompareTocompares values and returns 0. AHashSet, which usesequals, keeps both; aTreeSet, which usescompareTo, keeps one. Compare amounts withcompareTo, or bring them to one scale first.
The table puts the three ways of storing an amount side by side:
| Criterion | double | BigDecimal | long in cents |
|---|---|---|---|
| Stores | 53 significant bits and a binary exponent | a whole number and a decimal scale, any size | a whole number of the smallest unit |
| Exact for | halves, quarters and whole numbers up to 2⁵³ | every decimal amount | every amount in whole cents |
| Speed and memory | fast, 8 bytes | slower, an object per value | fast, 8 bytes |
| Rounding | IEEE 754 round to nearest, every operation | only where you ask, with the mode you choose | integer division cuts; you round by hand |
| When to choose | Measurements, science, graphics and statistics | Prices, taxes, interest and anything an invoice shows | Fast totals of money where every amount is whole cents |
The rule of thumb
Use double for measurements, BigDecimal built from strings for any amount a rule says how to round, and a long
of cents for fast totals of whole cents.
Checkpoint
Check: BigDecimal, scale and rounding mode
A tax must be what is left of the price
A price that includes a sales tax, VAT or GST has to be split into the price before tax and the tax itself. It is tempting to compute both parts and round each one. The run below does that at a rate of 12 % for every price from 0.01 to 1,000.00:
// Rounding the base and the tax separately: how often do they still add up to the price?
void main() {
var hundred = BigDecimal.valueOf(100);
var rate = BigDecimal.valueOf(12);
var divisor = hundred.add(rate);
int wrong = 0;
BigDecimal first = null;
for (int cents = 1; cents <= 100_000; cents++) {
var price = BigDecimal.valueOf(cents, 2);
var base = price.multiply(hundred).divide(divisor, 2, RoundingMode.HALF_UP);
var tax = price.multiply(rate).divide(divisor, 2, RoundingMode.HALF_UP);
if (base.add(tax).compareTo(price) != 0) {
wrong++;
if (first == null) first = price;
}
}
IO.println(wrong + " of 100000 prices from 0.01 to 1000.00 do not add up at 12 %");
var base = first.multiply(hundred).divide(divisor, 2, RoundingMode.HALF_UP);
var tax = first.multiply(rate).divide(divisor, 2, RoundingMode.HALF_UP);
IO.println("the first is " + first + ": base " + base + " + tax " + tax + " = " + base.add(tax));
var exactBase = first.multiply(hundred).divide(divisor, MathContext.DECIMAL64);
var exactTax = first.multiply(rate).divide(divisor, MathContext.DECIMAL64);
IO.println("its exact parts: base " + exactBase + " + tax " + exactTax);
} from session import run, version
version()
run("""
java NaiveSplit.java
""") Output
# openjdk version "25.0.4.1" LTS $ java NaiveSplit.java 3571 of 100000 prices from 0.01 to 1000.00 do not add up at 12 % the first is 0.14: base 0.13 + tax 0.02 = 0.15 its exact parts: base 0.125 + tax 0.015
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 naive_session.py
For 3,571 of the 100,000 prices, the two rounded parts do not add up to the price, the first already at 0.14. Step through that first price, from its exact parts to the cent that appears from nowhere:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The first price that fails. 0.14 is the first price of the run whose two rounded parts do not add up. It includes a tax of 12 %, so it is 112 % of the price before tax.
- Both parts, exactly. Exactly, the base is 0.14 × 100 / 112 = 0.125 and the tax 0.14 × 12 / 112 = 0.015, as the last line of the run printed, and 0.125 + 0.015 is 0.14.
- Two ties. Both exact parts end in a 5 at the third decimal place, so HALF_UP rounds each one up, the base to 0.13 and the tax to 0.02.
- One cent too many. Rounded separately, the parts make 0.13 + 0.02 = 0.15, one cent more than the price. 3,571 of the 100,000 prices of the run go wrong in this way.
- Round one part only. The fix rounds the base alone and makes the tax what is left, 0.14 − 0.13 = 0.01. The two parts then add up to the price every time, which the exercise below checks for every price up to 1,000.00.
The fix is to round one part and make the other what is left, which the exercise below asks you to write.
GST-inclusive prices
A GST-inclusive price splits into its taxable value and the tax: at 18 %, ₹118.00 is ₹100.00 plus ₹18.00 of tax. The rate depends on what is sold and is revised on the recommendation of the GST Council, so pass it in as a parameter instead of writing it into the code.
Checkpoint
Check: Splitting an amount so the parts add up
Practise: split a tax-inclusive price
Write split(inclusive, ratePercent), which returns the price before tax and the tax, both with two decimal places.
Round the base with RoundingMode.HALF_UP, then make the tax whatever is left, so that the two always add up to the
price. The last check tries every price from 0.01 to 1,000.00 at 12 %:
// Exercise: split(inclusive, ratePercent) splits a price that includes a sales tax (VAT or GST) into the price
// before tax and the tax, both with 2 decimal places. Round the base with HALF_UP, then make the tax what is left,
// so that base + tax is always exactly the price. Write it, then run this file: every check must say ok.
record Split(BigDecimal base, BigDecimal tax) {}
Split split(BigDecimal inclusive, int ratePercent) {
return new Split(BigDecimal.ZERO, BigDecimal.ZERO); // replace this line
}
int failures = 0;
void check(String price, int rate, String base, String tax) {
Split s = split(new BigDecimal(price), rate);
boolean passed = s.base().equals(new BigDecimal(base)) && s.tax().equals(new BigDecimal(tax));
if (!passed) failures++;
IO.println((passed ? "ok " : "FAILED ") + price + " at " + rate + " %: " + s.base() + " + " + s.tax());
}
void main() {
check("118.00", 18, "100.00", "18.00");
check("99.99", 18, "84.74", "15.25");
check("1000.00", 5, "952.38", "47.62");
check("100", 18, "84.75", "15.25");
check("59.00", 0, "59.00", "0.00");
check("0.00", 18, "0.00", "0.00");
check("-118.00", 18, "-100.00", "-18.00");
int bad = 0;
for (int cents = 1; cents <= 100_000; cents++) {
var price = BigDecimal.valueOf(cents, 2);
Split s = split(price, 12);
if (s.base().add(s.tax()).compareTo(price) != 0) bad++;
}
if (bad > 0) failures++;
IO.println((bad == 0 ? "ok " : "FAILED ") + "base + tax is the price for every price from 0.01 to 1000.00 (" + bad + " wrong)");
IO.println(failures + " of 8 checks failed");
if (failures > 0) System.exit(1);
} As given, the method returns two zeros:
from session import run, version
version()
run("""
!java TaxSplit.java
""") Output
# openjdk version "25.0.4.1" LTS $ java TaxSplit.java FAILED 118.00 at 18 %: 0 + 0 FAILED 99.99 at 18 %: 0 + 0 FAILED 1000.00 at 5 %: 0 + 0 FAILED 100 at 18 %: 0 + 0 FAILED 59.00 at 0 %: 0 + 0 FAILED 0.00 at 18 %: 0 + 0 FAILED -118.00 at 18 %: 0 + 0 FAILED base + tax is the price for every price from 0.01 to 1000.00 (100000 wrong) 8 of 8 checks failed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py
First hint: the base is inclusive × 100 / (100 + rate), and divide(divisor, 2, RoundingMode.HALF_UP) gives it two
decimal places. The checks compare both parts with equals, which looks at the scale too, and
inclusive.subtract(base) keeps the base’s two places: even a price written as “100” gives the tax 15.25.
With all eight checks at ok, practise the seven ideas of this lesson, from stored values to the tax split.
Questions you miss come back before the end, and each one explains its answer as soon as you check it:
Practice that adapts to you
Practise this lesson
20 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · Why should money not be stored in a double?
A double is a binary fraction, so most decimal amounts, such as 0.10 or 19.99, are stored as the nearest binary
value rather than exactly, and each operation rounds again. Totals then miss by tiny amounts, such as
0.30000000000000004, comparisons with == fail, and rounding for display can give a different cent from the one an
invoice rule expects. Use BigDecimal, built from strings, with an explicit scale and RoundingMode, or a long that
counts the smallest unit, such as cents. Keep double for measurements, where a tiny relative error does not matter.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Prices are BigDecimal values read from several suppliers. A deduplication step puts them in a HashSet and keeps both 19.9 and 19.90, while a TreeSet of the same prices keeps one. Explain, and say how you would store and compare prices.
-
Mid to senior
A metrics job adds ten million readings of 0.1 with a double in a for loop and gets 999999.9998389754 instead of 1,000,000. Explain where the error comes from, how it grows, and how you would get the right total.
Key takeaways
- A
doubleis an IEEE 754 binary64 number; most decimal fractions are stored as the nearest binary value, so0.1 + 0.2is 0.30000000000000004. - Since JDK 17 floating-point arithmetic gives identical results everywhere and
strictfpdoes nothing; since JDK 19Double.toStringprints the shortest decimal that identifies the double. - Compare computed doubles with a tolerance, relative plus absolute near zero; test NaN with
Double.isNaN, and remember that==andDouble.comparedisagree on NaN and on -0.0. Math.roundsends ties up,Math.rintto even;%.2frounds the printed decimal digits half up; a cast tointcuts the fraction off.- Use
BigDecimalfor money: build it from strings, divide with a scale and aRoundingMode, compare withcompareTo, becauseequalsalso compares the scale. - When a total is split, round one part and make the other what is left, so the parts always add up.
Terms in this lesson
- IEEE 754
- The standard for binary floating-point arithmetic that float and double follow: float is binary32 with 24 significant bits, double is binary64 with 53, and every operation rounds its exact result to the nearest representable value.
- NaN
- Not a Number: the double or float that 0.0 / 0.0 and Math.sqrt(-1) give. Every comparison with NaN is false, itself included, so it is found with Double.isNaN(x), never with ==.
- rounding mode
- The rule a BigDecimal follows when it must drop digits, given as a RoundingMode: HALF_UP sends a tie away from zero, HALF_EVEN to the even neighbour, DOWN cuts toward zero, and UNNECESSARY throws if any digit would be lost.
- scale
- The number of digits after the decimal point of a BigDecimal: 19.99 has scale 2. A BigDecimal is its unscaled whole number times 10 to the power minus its scale, so 2.0 and 2.00 have the same value but different scales.
- ulp
- Unit in the last place: the gap between a floating-point number and the next larger one of its type. Math.ulp(1.0) is 2.220446049250313E-16, and the gap grows with the size of the number.
References
- The Java Language Specification, Java SE 25: 4.2.3 Floating-Point Types and Values (Oracle)
- The Java Language Specification, Java SE 25: 4.2.4 Floating-Point Operations (Oracle)
- IEEE Standard for Floating-Point Arithmetic (IEEE 754) (IEEE)
- JEP 306: Restore Always-Strict Floating-Point Semantics (OpenJDK)
- JDK 19 release notes: Double.toString(double) and Float.toString(float) may return slightly different results (Oracle)
- JDK-4511638: Double.toString(double) sometimes produces incorrect results (fixed in JDK 19) (OpenJDK)
- Double (Java SE 25 API) (Oracle)
- Math.round, Math.rint, Math.ulp and the accuracy of Math methods (Java SE 25 API) (Oracle)
- StrictMath (Java SE 25 API) (Oracle)
- Formatter, floating-point conversions (Java SE 25 API) (Oracle)
- BigDecimal (Java SE 25 API) (Oracle)
- RoundingMode (Java SE 25 API) (Oracle)
- MathContext (Java SE 25 API) (Oracle)
- Goods and Services Tax Council (GST Council, Government of India)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress