JAVA / DATES, NUMBERS, REGEX AND CORE LIBRARIES
Math utilities, rounding and exact decimal arithmetic
Compute exact decimal results in Java with BigDecimal, scale and RoundingMode, and know when Math's rounding helpers behave differently.
What you will learn
- Represent money as BigDecimal built from a String, or as long minor units
- Pass a scale and RoundingMode to divide(), or it throws on 1/3
- Compare amounts with compareTo; equals also compares scale, so 2.50 != 2.5
- Round once at the end, and know Math.round(-2.5) is -2 but HALF_UP gives -3
Understanding Math utilities, rounding and exact decimal arithmetic
A double is a binary fraction: a 53-bit significand multiplied by a power of two. Values like 0.1 or 19.99 are not finite sums of negative powers of two, so what gets stored is the nearest representable double, and every operation on it rounds again. Printing hides this because Java prints the shortest decimal that maps back to the same double, which is why 0.1 looks clean until you add it to 0.2 and see 0.30000000000000004. The individual errors are tiny but they do not reliably cancel, so long chains of price arithmetic drift.
BigDecimal drops the base-two model and stores a value as an arbitrary-precision integer plus a scale, where the value is unscaledValue times ten to the power of minus scale. Since 19.99 is exactly 1999 times 10^-2, decimal literals are exact, and addition, subtraction and multiplication stay exact: add takes the larger scale, multiply adds the two scales together. Division is the exception, because a quotient like 1/3 needs infinitely many digits, so divide with no rounding argument throws instead of quietly truncating. That refusal is the whole point: you are forced to say how many digits you want and which way ties go.
Rounding is a decision, not a formatting detail. Math.round(x) is defined as floor(x + 0.5), so it lifts -2.5 to -2, while RoundingMode.HALF_UP moves it away from zero to -3, and a plain cast to int just truncates toward zero. HALF_UP matches most billing and human conventions; HALF_EVEN avoids the upward bias you accumulate when millions of ties all go the same direction, which is why it is the mode used by the MathContext.DECIMAL32/64/128 presets. Apply setScale at the boundary where a number becomes a stored or displayed amount, not after every intermediate step.
import java.math.BigDecimal;
import java.math.RoundingMode;
public class ExactMoney {
public static void main(String[] args) {
System.out.println("double 0.1 + 0.2 = " + (0.1 + 0.2));
System.out.println("decimal 0.1 + 0.2 = " + new BigDecimal("0.1").add(new BigDecimal("0.2")));
BigDecimal price = new BigDecimal("19.99");
BigDecimal net = price.multiply(new BigDecimal("3"));
BigDecimal gross = net.multiply(new BigDecimal("1.075"));
System.out.println("net = " + net);
System.out.println("gross = " + gross + " (scale " + gross.scale() + ")");
System.out.println("billed = " + gross.setScale(2, RoundingMode.HALF_UP));
System.out.println("Math.round(2.5) = " + Math.round(2.5));
System.out.println("Math.round(-2.5) = " + Math.round(-2.5));
System.out.println("HALF_UP(-2.5) = " + new BigDecimal("-2.5").setScale(0, RoundingMode.HALF_UP));
System.out.println("HALF_EVEN(2.5) = " + new BigDecimal("2.5").setScale(0, RoundingMode.HALF_EVEN));
}
}Exact decimal arithmetic means leaving base two behind: BigDecimal is an integer plus a scale, so every result carries a chosen number of decimal digits and every inexact division needs an explicit rounding mode.
Worked examples
Division needs a scale
Shows why divide() throws without rounding, and the difference between a fixed scale and a fixed precision.
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
public class Dividing {
public static void main(String[] args) {
BigDecimal one = new BigDecimal("1");
BigDecimal three = new BigDecimal("3");
try {
System.out.println(one.divide(three));
} catch (ArithmeticException e) {
System.out.println("caught: " + e.getMessage());
}
System.out.println(one.divide(three, 5, RoundingMode.HALF_UP));
System.out.println(one.divide(three, new MathContext(5)));
System.out.println(new BigDecimal("10").divide(new BigDecimal("4")));
}
}Example explained
Line 1one.divide(three) asks for an exact quotient, and 1/3 has no finite decimal form, so BigDecimal throws rather than silently picking a precision.
Line 2divide(three, 5, RoundingMode.HALF_UP) fixes five digits after the decimal point, producing 0.33333.
Line 3new MathContext(5) fixes five significant digits instead; the two agree here but diverge for values above 1, where significant digits are spent on the integer part.
Line 410 divided by 4 terminates, so no rounding argument is needed, and the exact result is returned with scale 1 as 2.5.
Scale is part of a BigDecimal's identity
Demonstrates equals versus compareTo, the double constructor trap, and how stripTrailingZeros can produce scientific notation.
import java.math.BigDecimal;
public class Scales {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("2.50");
BigDecimal b = new BigDecimal("2.5");
System.out.println("equals: " + a.equals(b));
System.out.println("compareTo: " + (a.compareTo(b) == 0));
System.out.println("scales: " + a.scale() + " and " + b.scale());
System.out.println(new BigDecimal(0.1));
System.out.println(BigDecimal.valueOf(0.1));
System.out.println(new BigDecimal("100.00").stripTrailingZeros());
System.out.println(new BigDecimal("100.00").stripTrailingZeros().toPlainString());
}
}Example explained
Line 1equals compares unscaled value and scale together, so 2.50 and 2.5 are unequal objects even though they are numerically the same amount.
Line 2compareTo ignores scale and returns 0, which is the comparison you almost always want for amounts.
Line 3new BigDecimal(0.1) copies the double's exact binary value, all 55 decimal digits of it, while BigDecimal.valueOf(0.1) goes through Double.toString and gives you 0.1.
Line 4stripTrailingZeros turns 100.00 into unscaled 1 with scale -2, and toString then prints 1E+2; toPlainString suppresses the exponent.
Integer division, negative remainders and overflow
Contrasts / and % with Math.floorDiv/floorMod, and silent int wraparound with Math.addExact.
public class IntMath {
public static void main(String[] args) {
System.out.println("-7 / 2 = " + (-7 / 2));
System.out.println("-7 % 2 = " + (-7 % 2));
System.out.println("floorDiv(-7,2)= " + Math.floorDiv(-7, 2));
System.out.println("floorMod(-7,2)= " + Math.floorMod(-7, 2));
System.out.println("MAX_VALUE + 1 = " + (Integer.MAX_VALUE + 1));
try {
System.out.println(Math.addExact(Integer.MAX_VALUE, 1));
} catch (ArithmeticException e) {
System.out.println("caught: " + e.getMessage());
}
}
}Example explained
Line 1Integer / truncates toward zero, so -7 / 2 is -3, and % is defined to keep the sign of the dividend, giving -1.
Line 2Math.floorDiv rounds toward negative infinity (-4), and Math.floorMod always matches the divisor's sign (1), which is what cyclic indexes and wrap-around clock arithmetic need.
Line 3Integer.MAX_VALUE + 1 wraps around to Integer.MIN_VALUE with no warning at all.
Line 4Math.addExact performs the same addition but throws ArithmeticException with the message "integer overflow" instead of wrapping.
Important notes
BigDecimal is immutable; add, multiply and setScale return new instances, so a bare statement like total.add(item); computes a value and throws it away.
For plain integers remember that / truncates toward zero and % follows the dividend's sign: reach for Math.floorDiv and Math.floorMod when you need wrap-around behaviour, and Math.addExact or Math.multiplyExact when silent overflow would be a bug.
Common mistakes
Writing new BigDecimal(0.1) instead of new BigDecimal("0.1"): the object carries the double's exact binary value, so totals grow long tails of digits and a later setScale can tip a boundary case the wrong way.
Rounding after every intermediate step instead of once at the end: each rounding discards a fractional cent, and the accumulated drift makes a summed invoice differ by cents from the same invoice computed on unrounded values.
Comparing amounts with equals: new BigDecimal("2.50").equals(new BigDecimal("2.5")) is false, so HashSet and HashMap lookups and assertEquals in tests fail on values that are numerically identical.
Try it yourself
Change, predict, then run
Add 0.1 ten times into a double and ten times into a BigDecimal, print both totals and their difference as a BigDecimal. Then price three items at 4.35 each with 8.25% tax, rounding to cents with HALF_UP exactly once, and print the amount billed.
Open the Java workspaceCheck your understanding
Three line items cost 4.35 each and tax is 8.25%. Why can rounding the tax on each line to cents and summing give a different total from taxing the summed amount and rounding once?
- BigDecimal multiplication is approximate when one operand is a percentage.
- The scale of a product is the maximum of the two operand scales, so digits are lost.
- Each rounding discards a fractional-cent remainder, and three separate remainders need not add up to the single remainder of the unrounded total.
- HALF_UP is biased upward; using HALF_EVEN would make the two totals identical.
Show answer
BigDecimal multiplication is exact and the product's scale is the sum of the operand scales, so nothing is lost until you round; the discrepancy is created entirely by where rounding happens, since three discarded fractions can sum to more or less than one cent. HALF_EVEN only changes which way ties break and reduces long-run bias, so option 4 is wrong: per-line and per-total rounding can still disagree under any mode.