Aritmética Exata com BigDecimal e BigInteger
Objective
double e long são rápidos porque são tipos binários de largura fixa — e é exatamente por isso que são a ferramenta errada para dinheiro, impostos ou chaves criptográficas. Um double não consegue representar 0.1 de forma alguma, e um long para em 9.223.372.036.854.775.807. java.math.BigDecimal e java.math.BigInteger trocam velocidade por duas garantias que os tipos primitivos não conseguem dar: precisão arbitrária (o número cresce tanto quanto a memória permitir) e aritmética decimal exata sob uma política de arredondamento que você escolhe explicitamente, em vez de uma que o hardware escolhe por você. Ambos são classes imutáveis semelhantes a tipos de valor, então toda operação retorna um novo objeto.
Use Cases
- Aritmética monetária — preços, totais de fatura, impostos, juros — onde uma fração de centavo perdida por arredondamento binário é um bug de reconciliação ou um achado de auditoria.
- Qualquer coisa com uma regra de arredondamento legalmente exigida (arredondamento bancário, arredondamento de IVA, escalas específicas de moeda) que precisa estar declarada no código, não herdada do IEEE 754.
- Criptografia e teoria dos números: inteiros do tamanho usado em RSA, exponenciação modular, geração de primos prováveis — tudo muito além de
long. - Fazer parsing e reemitir dados decimais (JSON, CSV, colunas
NUMERICde banco de dados) sem mudar o valor ou seu número de casas decimais. - Combinatória e resultados exatos com números grandes — fatoriais,
2^4096, números de Fibonacci grandes — onde o overflow silenciosamente daria a volta em umlong.
Deep Dive
Por que double perde valores decimais
Um double armazena uma fração binária. 0.1 é uma fração periódica na base 2, então é armazenada como o valor representável mais próximo, e o erro fica visível assim que você soma:
javaSystem.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // false
double sum = 0;
for (int i = 0; i < 10; i++) sum += 0.1;
System.out.println(sum); // 0.9999999999999999BigDecimal armazena os dígitos que você realmente escreveu, na base 10, então o mesmo cálculo é exato:
javaBigDecimal sum = BigDecimal.ZERO;
for (int i = 0; i < 10; i++) sum = sum.add(new BigDecimal("0.1"));
System.out.println(sum); // 1.0Note sum = sum.add(...). BigDecimal é imutável; um simples sum.add(x); calcula um valor e o descarta:
javaBigDecimal balance = new BigDecimal("10.00");
balance.add(new BigDecimal("5")); // return value discarded
System.out.println(balance); // 10.00O modelo de BigDecimal: valor não escalonado e escala
Um BigDecimal é exatamente duas coisas: um inteiro de precisão arbitrária (unscaledValue(), um BigInteger) e uma scale() de 32 bits. O valor é unscaledValue × 10^-scale:
javaBigDecimal d = new BigDecimal("12.3400");
System.out.println(d.unscaledValue()); // 123400
System.out.println(d.scale()); // 4
System.out.println(d.precision()); // 6 (total significant digits)A escala faz parte da identidade do objeto, e é por isso que equals() distingue 2.0 de 2.00 enquanto compareTo() não:
javaBigDecimal a = new BigDecimal("2.0");
BigDecimal b = new BigDecimal("2.00");
System.out.println(a.equals(b)); // false — different scales
System.out.println(a.compareTo(b)); // 0 — same numeric valuePara dinheiro isso é um recurso: 2.00 carrega "duas casas decimais" como dado, e setScale() é como você normaliza isso.
javaBigDecimal price = new BigDecimal("2.345");
System.out.println(price.setScale(2, RoundingMode.HALF_UP)); // 2.35
System.out.println(price.setScale(2, RoundingMode.HALF_EVEN)); // 2.34Construindo um corretamente
new BigDecimal(double) é exato — e esse é o problema. Ele registra fielmente a aproximação binária que o double já carrega, todos os 55 dígitos dela:
javaSystem.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625
System.out.println(BigDecimal.valueOf(0.1)); // 0.1
System.out.println(new BigDecimal("0.1")); // 0.1BigDecimal.valueOf(double) passa por Double.toString(), então dá o decimal curto que uma pessoa teria digitado. O construtor com String nunca envolve um double de forma alguma — prefira-o sempre que o valor vier originalmente de texto (o corpo de uma requisição, uma célula de CSV, um arquivo de configuração). BigInteger tem a mesma divisão: BigInteger.valueOf(long) para valores que cabem, o construtor com String para qualquer coisa maior.
javaBigInteger big = new BigInteger("3419229223372036854775807");
System.out.println(Long.MAX_VALUE); // 9223372036854775807
System.out.println(big); // 3419229223372036854775807Divisão obriga você a declarar uma política de arredondamento
add, subtract e multiply sempre têm uma resposta decimal exata, então as formas com um único argumento são seguras. Divisão frequentemente não tem, e a divide() de um argumento se recusa a adivinhar:
javaBigDecimal.ONE.divide(new BigDecimal("3"));
// ArithmeticException: Non-terminating decimal expansion;
// no exact representable decimal result.Forneça uma escala alvo mais um RoundingMode, ou um MathContext que fixe o número de dígitos significativos:
javaBigDecimal.ONE.divide(new BigDecimal("3"), 5, RoundingMode.HALF_UP);
// 0.33333
BigDecimal.ONE.divide(new BigDecimal("3"), MathContext.DECIMAL64);
// 0.3333333333333333RoundingMode (em java.math, compartilhado com setScale) tem oito constantes: UP, DOWN, CEILING, FLOOR, HALF_UP, HALF_DOWN, HALF_EVEN e UNNECESSARY. HALF_EVEN é o arredondamento bancário — arredonda empates em direção ao vizinho par para que uma longa sequência de arredondamentos não derive para cima, e é por isso que é o padrão em todo preset de MathContext exceto UNLIMITED. UNNECESSARY afirma que nenhum arredondamento deveria ser necessário e lança exceção se for:
javanew BigDecimal("2.5").setScale(0); // ArithmeticException: Rounding necessaryMathContext empacota uma precisão (dígitos significativos) com um RoundingMode, e todo método aritmético tem uma sobrecarga que aceita um: DECIMAL32, DECIMAL64, DECIMAL128 espelham os formatos decimais do IEEE 754 (7, 16 e 34 dígitos, todos HALF_EVEN), e MathContext.UNLIMITED significa "exato, ou lance exceção".
BigInteger: inteiros ilimitados, aritmética modular e de primos
BigInteger cobre os operadores inteiros mais a teoria dos números que a criptografia de chave pública precisa:
javaBigInteger.valueOf(2).pow(4096).bitLength(); // 4097
BigInteger.valueOf(48).gcd(BigInteger.valueOf(18)); // 6
BigInteger.valueOf(1000).sqrt(); // 31 (floor)
BigInteger.valueOf(4).modPow(BigInteger.valueOf(13),
BigInteger.valueOf(497)); // 445modPow é a operação por trás do RSA, e não é o mesmo que pow().mod() — ela nunca materializa o intermediário astronomicamente grande. A primalidade é probabilística: isProbablePrime(certainty) e nextProbablePrime() podem reportar um composto como primo com probabilidade menor que 1 - 1/2^certainty, enquanto probablePrime(bitLength, random) gera um candidato novo de um determinado tamanho:
javaBigInteger p = BigInteger.probablePrime(2048, new SecureRandom());
System.out.println(p.isProbablePrime(100)); // trueAs constantes ZERO, ONE, TWO e TEN existem em ambas as classes (BigInteger.TWO desde o Java 9, BigDecimal.TWO desde o Java 19), e ambas implementam Comparable, então ordenam e funcionam em um TreeMap sem um comparador.
Voltando sem perder dados silenciosamente
Os métodos xxxValue() são conversões restritivas (narrowing) e são discretos sobre isso. Um BigInteger além de Double.MAX_VALUE vira Infinity; um BigDecimal com fração é truncado:
javaSystem.out.println(BigInteger.TEN.pow(400).doubleValue()); // Infinity
System.out.println(new BigDecimal("2.99").intValue()); // 2As variantes ...Exact — intValueExact(), longValueExact(), toBigIntegerExact() — falham de forma ruidosa em vez disso:
javanew BigDecimal("2.50").intValueExact();
// ArithmeticException: Rounding necessaryA impressão tem uma armadilha equivalente: toString() pode mudar para notação científica, toPlainString() nunca faz isso.
javaBigDecimal x = new BigDecimal("600.0").stripTrailingZeros();
System.out.println(x); // 6E+2
System.out.println(x.toPlainString()); // 600Onde java.math para: sem complexo, sem tipo racional
java.math tem só duas classes numéricas. Não existe um tipo complexo, racional, matriz ou sem sinal embutido — para isso você escreve uma pequena classe de valor ou adota uma dependência de biblioteca (o Complex, BigFraction do Apache Commons Math). Desde o Java 16, um record torna a versão feita à mão praticamente de graça, e a imutabilidade vem junto:
javapublic record Complex(double re, double im) {
public Complex plus(Complex o) { return new Complex(re + o.re, im + o.im); }
public Complex times(Complex o) {
return new Complex(re * o.re - im * o.im, re * o.im + im * o.re);
}
public double magnitude() { return Math.hypot(re, im); }
}
var c = new Complex(3, 5).times(new Complex(2, -2));
System.out.println(c); // Complex[re=16.0, im=4.0]Note a nomeação plus/times: Java não tem sobrecarga de operadores, então cada um desses tipos — incluindo BigDecimal — é usado por meio de chamadas de método, e a.add(b).multiply(c) é o melhor que a sintaxe consegue oferecer.
Trade-offs
- Correção custa velocidade e alocação — toda operação aloca um novo objeto e roda em software em vez de uma instrução de CPU, então
BigDecimalé ordens de magnitude mais lento quelongoudouble. Código monetário de alto volume frequentemente armazena unidades mínimas inteiras (centavos) em umlonge recorre aBigDecimalsó nas fronteiras onde escala e arredondamento acontecem. equals()ecompareTo()discordam, então coleções baseadas em hash e ordenadas discordam também —BigDecimaldeliberadamente quebra a expectativa usual de "consistente com equals", e2.0versus2.00é uma chave duplicada silenciosa em umHashMap.javavar values = List.of(new BigDecimal("2.0"), new BigDecimal("2.00")); new HashSet<>(values).size(); // 2 — equals() sees two values new TreeSet<>(values).size(); // 1 — compareTo() sees onenew BigDecimal(double)importa exatamente o erro do qual você fugiu — o construtor é exato sobre um valor que já estava errado, então umdoubleem qualquer ponto anterior à conversão já perdeu a precisão.javanew BigDecimal(1.1).multiply(new BigDecimal("3")); // 3.300000000000000266453525910037569701671600341796875 new BigDecimal("1.1").multiply(new BigDecimal("3")); // 3.3- Divisão sem arredondamento lança exceção em vez de aproximar — mais seguro que uma resposta errada, mas significa que toda
divide()na base de código precisa tomar uma decisão de política, e uma sobrecarga faltando é umaArithmeticExceptionem produção em vez de um erro de compilação.javanew BigDecimal("10").divide(new BigDecimal("3")); // ArithmeticException - Imutabilidade torna resultados descartados invisíveis — nada avisa quando um valor de retorno é descartado, diferente do erro de compilação que você teria ao usar mal um operador de atribuição.java
BigDecimal total = new BigDecimal("0.00"); total.add(new BigDecimal("9.99")); // silently does nothing - A escala é ilimitada, e o custo também —
BigIntegercresce com o valor e a precisão deBigDecimalcresce a cadamultiply, então aritmética exata encadeada sobre entrada fornecida pelo usuário pode consumir quantidades surpreendentes de memória e CPU.MathContextem cada operação, ou umsetScale()periódico, limita isso. - A legibilidade sofre — uma fórmula escrita em chamadas de método
BigDecimalé materialmente mais difícil de ler e revisar do que a mesma fórmula em operadores, o que é um argumento real para manter a camada de aritmética exata fina e bem testada em vez de espalhá-la pelo modelo de domínio.