Dinero en Java con BigDecimal: IVA, redondeo y formato en pesos

Con double, 0.1 + 0.2 da 0.30000000000000004, y en una factura eso es un error. Para dinero, Java tiene BigDecimal: números decimales exactos, con el redondeo bajo tu control. Aquí calculamos una factura con IVA y la mostramos en pesos mexicanos.

Una factura con IVA

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.List;
import java.util.Locale;

record Linea(String concepto, int cantidad, BigDecimal precio) {
    BigDecimal importe() { return precio.multiply(BigDecimal.valueOf(cantidad)); }
}

final BigDecimal IVA = new BigDecimal("0.16");

void main() {
    var lineas = List.of(
        new Linea("Teclado", 2, new BigDecimal("449.90")),
        new Linea("Mouse", 1, new BigDecimal("199.99")),
        new Linea("Cable USB-C", 3, new BigDecimal("89.50")));

    BigDecimal subtotal = lineas.stream().map(Linea::importe).reduce(BigDecimal.ZERO, BigDecimal::add);
    BigDecimal iva = subtotal.multiply(IVA).setScale(2, RoundingMode.HALF_UP);
    BigDecimal total = subtotal.add(iva);

    var pesos = NumberFormat.getCurrencyInstance(Locale.of("es", "MX"));
    lineas.forEach(l -> IO.println("%-12s %2d x %10s = %11s".formatted(
        l.concepto(), l.cantidad(), pesos.format(l.precio()), pesos.format(l.importe()))));
    IO.println("Subtotal: " + pesos.format(subtotal));
    IO.println("IVA 16 %: " + pesos.format(iva));
    IO.println("Total:    " + pesos.format(total));
}
salidaTeclado       2 x    $449.90 =     $899.80
Mouse         1 x    $199.99 =     $199.99
Cable USB-C   3 x     $89.50 =     $268.50
Subtotal: $1,368.29
IVA 16 %: $218.93
Total:    $1,587.22

Las reglas de oro

  • Crea los valores desde texto: new BigDecimal("449.90"). Desde un double heredas su error: new BigDecimal(0.1) vale 0.1000000000000000055511151231257827…
  • Los BigDecimal no cambian: add o multiply devuelven un número nuevo. subtotal.add(iva); suelto no hace nada; hay que guardar el resultado.
  • Redondea a propósito con setScale(2, RoundingMode.HALF_UP), el redondeo «de la escuela» (0.5 sube). HALF_EVEN, el de los bancos, reparte mejor los errores en sumas largas.
  • Compara con compareTo, no con equals: para equals, 2.0 y 2.00 son distintos (está en las rarezas).

Dividir siempre con escala

import java.math.BigDecimal;
import java.math.RoundingMode;

void main() {
    var cuenta = new BigDecimal("1000.00");
    var personas = BigDecimal.valueOf(3);
    try {
        cuenta.divide(personas);
    } catch (ArithmeticException e) {
        IO.println("sin escala: " + e.getMessage());
    }
    var cadaUno = cuenta.divide(personas, 2, RoundingMode.HALF_UP);
    var sobra = cuenta.subtract(cadaUno.multiply(personas));
    IO.println("cada uno paga " + cadaUno + " y sobran " + sobra);
}
salidasin escala: Non-terminating decimal expansion; no exact representable decimal result.
cada uno paga 333.33 y sobran 0.01

1000 entre 3 no tiene un resultado decimal exacto, y BigDecimal se niega a inventarlo: hay que decirle cuántos decimales y cómo redondear. Y ojo con el centavo que sobra o falta al repartir: en un sistema de cobros, alguien tiene que llevarlo.

Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.