Java BigDecimal e Dinheiro

Para o computador, 0.1 + 0.2 não é 0.3. Num jogo, ninguém percebe a diferença; numa loja, num banco ou numa folha de pagamento, um centavo errado é um problema sério. O Java resolve isso com a classe BigDecimal, que faz contas com números decimais exatos, e a sua irmã BigInteger, para inteiros de qualquer tamanho.

Você vai aprender
  • Por que o double erra centavos, com exemplos reais.
  • Criar um BigDecimal do jeito certo (new BigDecimal("0.1")) e o jeito errado (new BigDecimal(0.1)).
  • Fazer contas com add, subtract, multiply e divide.
  • Arredondar com setScale e RoundingMode.
  • A ArithmeticException da divisão que não termina, e como evitá-la.
  • Comparar valores com compareTo (e por que equals engana).
  • Calcular o fatorial de 50 com BigInteger.

O problema: double não sabe 0,1

Você já viu em double e float que o double guarda números em binário (base 2). Assim como 1/3 não tem representação exata em decimal (0,3333…), o número 0,1 não tem representação exata em binário: o double guarda o valor mais próximo possível, que é um pouquinho diferente. Na maioria das contas, o erro some ao imprimir. Nas contas com dinheiro, ele aparece:

public class Main {
    public static void main(String[] args) {
        System.out.println(0.1 + 0.2);
        System.out.println(0.1 + 0.2 == 0.3);
        double total = 0;
        for (int i = 0; i < 10; i++) {
            total += 0.10;
        }
        System.out.println("10 moedas de 10 centavos: " + total);
        System.out.println(1.10 - 1.00);
        System.out.println(4.35 * 100);
        System.out.println((int) (4.35 * 100));
    }
}
$ java Main.java
0.30000000000000004
false
10 moedas de 10 centavos: 0.9999999999999999
0.10000000000000009
434.99999999999994
434
  • 0.1 + 0.2 dá 0.30000000000000004, e por isso a comparação com 0.3 é false.
  • Dez moedas de R$ 0,10 não somam R$ 1,00. Some milhares de vendas assim e a diferença cresce.
  • R$ 4,35 em centavos, convertendo para int, vira 434: o (int) corta as casas decimais (veja Conversão de Tipos), e o valor era 434,99999…

Não é defeito do Java: toda linguagem que usa o padrão de ponto flutuante IEEE 754 (JavaScript, Python, C…) tem o mesmo comportamento. A regra dos programadores é antiga e simples: dinheiro não se guarda em double.


BigDecimal: decimais exatos

O BigDecimal ("decimal grande", do pacote java.math) guarda o número como nós escrevemos: os dígitos e a posição da vírgula. 0.1 é exatamente um décimo.

import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("0.1");
        BigDecimal b = new BigDecimal("0.2");
        System.out.println(a.add(b));
        System.out.println(new BigDecimal("0.1"));
        System.out.println(new BigDecimal(0.1));
        System.out.println(BigDecimal.valueOf(0.1));
        BigDecimal total = BigDecimal.ZERO;
        for (int i = 0; i < 10; i++) {
            total = total.add(new BigDecimal("0.10"));
        }
        System.out.println("10 moedas de 10 centavos: " + total);
    }
}
$ java Main.java
0.3
0.1
0.1000000000000000055511151231257827021181583404541015625
0.1
10 moedas de 10 centavos: 1.00
import java.math.BigDecimal;
A classe fica em java.math, que não é importado automaticamente.
new BigDecimal("0.1")
Criado a partir de um texto: exatamente 0,1. É o jeito certo. O texto usa ponto, como o Java.
a.add(b)
Soma. Não existe + para BigDecimal: as contas são métodos. Resultado: 0.3, exato.
new BigDecimal(0.1)
Criado a partir de um double. O double 0.1 já chega errado, e o BigDecimal mostra fielmente o valor que o double realmente tinha: 0.1000000000000000055511151231257827021181583404541015625. É o erro escondido do double, revelado.
BigDecimal.valueOf(0.1)
Se o valor já está num double, esta é a forma segura: ela passa pelo texto que o double mostraria ("0.1") e dá 0.1.
BigDecimal.ZERO
Constante pronta para o zero (também há ONE e TEN).
total = total.add(…)
Como a String, o BigDecimal é imutável: add devolve um BigDecimal novo. É preciso guardar o resultado.
Nunca new BigDecimal(0.1)

Criar a partir de um double traz junto o erro do double. Use new BigDecimal("0.1") (texto) ou BigDecimal.valueOf(0.1). Números inteiros não têm esse problema: new BigDecimal(3) e BigDecimal.valueOf(3) são exatos.

Missão: a soma das dez moedas dá 0.9999999999999999. Reescreva com BigDecimal: comece em BigDecimal.ZERO e, no laço, faça total = total.add(new BigDecimal("0.10")). A saída deve ser Total: 1.00.

public class Main {
    public static void main(String[] args) {
        double total = 0;
        for (int i = 0; i < 10; i++) {
            total += 0.10;
        }
        System.out.println("Total: " + total);
    }
}
import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        BigDecimal total = BigDecimal.ZERO;
        for (int i = 0; i < 10; i++) {
            total = total.add(new BigDecimal("0.10"));
        }
        System.out.println("Total: " + total);
    }
}

As quatro operações e a escala

A Ana vende 3 cadernos de R$ 19,90, dá 15% de desconto e parcela em 4 vezes:

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

public class Main {
    public static void main(String[] args) {
        BigDecimal preco = new BigDecimal("19.90");
        BigDecimal quantidade = new BigDecimal("3");
        BigDecimal subtotal = preco.multiply(quantidade);
        System.out.println("Subtotal: " + subtotal);
        BigDecimal desconto = subtotal.multiply(new BigDecimal("0.15"));
        System.out.println("Desconto de 15%: " + desconto);
        BigDecimal descontoArredondado = desconto.setScale(2, RoundingMode.HALF_UP);
        System.out.println("Desconto arredondado: " + descontoArredondado);
        BigDecimal total = subtotal.subtract(descontoArredondado);
        System.out.println("Total: " + total);
        BigDecimal parcela = total.divide(new BigDecimal("4"), 2, RoundingMode.HALF_UP);
        System.out.println("4 parcelas de: " + parcela);
        System.out.println("Escala do total: " + total.scale());
    }
}
$ java Main.java
Subtotal: 59.70
Desconto de 15%: 8.9550
Desconto arredondado: 8.96
Total: 50.74
4 parcelas de: 12.69
Escala do total: 2
OperaçãoMétodoNo exemplo
Somaa.add(b)
Subtraçãoa.subtract(b)59.70 - 8.96 = 50.74
Multiplicaçãoa.multiply(b)19.90 × 3 = 59.70
Divisãoa.divide(b, casas, arredondamento)50.74 ÷ 4 = 12.685 → 12.69
Negar, valor absolutoa.negate(), a.abs()
Potênciaa.pow(n)

A escala: quantas casas depois do ponto

Todo BigDecimal tem uma escala (scale): o número de casas decimais que ele guarda. 19.90 tem escala 2 (e o zero do fim é mantido: 59.70, não 59.7). Na multiplicação, as escalas se somam: 59.70 (2 casas) × 0.15 (2 casas) = 8.9550 (4 casas). Por isso, depois de multiplicar por uma porcentagem, arredonde para centavos.

setScale(2, RoundingMode.HALF_UP)
"Deixe com 2 casas, arredondando meio para cima": 8.9550 vira 8.96. Também devolve um BigDecimal novo.
divide(new BigDecimal("4"), 2, RoundingMode.HALF_UP)
Divisão já com o número de casas e a regra de arredondamento: 12.685 vira 12.69.
scale()
Devolve a escala: 2.

Os modos de arredondamento (RoundingMode)

Arredondar parece simples até alguém perguntar: "e o 5?". O RoundingMode (pacote java.math) tem as regras todas. Cinco valores arredondados para 2 casas, de cinco jeitos:

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

public class Main {
    public static void main(String[] args) {
        String[] valores = {"2.345", "2.355", "-2.345", "2.341", "2.349"};
        System.out.printf("%-8s %-8s %-9s %-8s %-6s %s%n", "valor", "HALF_UP", "HALF_EVEN", "DOWN", "UP", "FLOOR");
        for (String v : valores) {
            BigDecimal b = new BigDecimal(v);
            System.out.printf("%-8s %-8s %-9s %-8s %-6s %s%n", v,
                b.setScale(2, RoundingMode.HALF_UP),
                b.setScale(2, RoundingMode.HALF_EVEN),
                b.setScale(2, RoundingMode.DOWN),
                b.setScale(2, RoundingMode.UP),
                b.setScale(2, RoundingMode.FLOOR));
        }
    }
}
$ java Main.java
valor    HALF_UP  HALF_EVEN DOWN     UP     FLOOR
2.345    2.35     2.34      2.34     2.35   2.34
2.355    2.36     2.36      2.35     2.36   2.35
-2.345   -2.35    -2.34     -2.34    -2.35  -2.35
2.341    2.34     2.34      2.34     2.35   2.34
2.349    2.35     2.35      2.34     2.35   2.34
ModoRegraQuando usar
HALF_UPO 5 sobe (o da escola): 2,345 → 2,35O mais comum em preços e notas fiscais
HALF_EVENO 5 vai para o vizinho par: 2,345 → 2,34; 2,355 → 2,36"Arredondamento bancário": em muitas contas, os arredondamentos para cima e para baixo se compensam
DOWNCorta as casas (em direção ao zero)Truncar: "não cobrar fração de centavo"
UPSempre se afasta do zero se sobrou algo: 2,341 → 2,35Cobrar a fração (taxas mínimas)
FLOOR / CEILINGSempre para baixo / para cima na reta (diferem do DOWN/UP nos negativos)Regras específicas

Qual usar com dinheiro é uma decisão do negócio (ou da lei), não do programador. Na dúvida, HALF_UP é o que as pessoas esperam.


A divisão que não termina: ArithmeticException

Dividir R$ 100,00 por 4 dá exatamente 25,00. Mas por 3 dá 33,3333… para sempre. Como o BigDecimal promete ser exato, e essa dízima não tem fim, ele se recusa:

import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        BigDecimal conta = new BigDecimal("100.00");
        BigDecimal pessoas = new BigDecimal("3");
        System.out.println(conta.divide(new BigDecimal("4")));
        System.out.println(conta.divide(pessoas));
    }
}
$ java Main.java
25.00
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
	at java.base/java.math.BigDecimal.divide(BigDecimal.java:1810)
	at Main.main(Main.java:8)

"Expansão decimal sem fim; não há resultado decimal exato representável". O divide com um argumento só funciona quando a divisão termina. A solução é sempre dizer quantas casas e como arredondar: conta.divide(pessoas, 2, RoundingMode.HALF_UP) dá 33.33. E aí aparece outra pergunta do mundo real: 3 × 33,33 = 99,99. Quem paga o centavo que sobrou? A missão do fim da aula calcula isso.


Comparar: compareTo, não equals

import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("2.0");
        BigDecimal b = new BigDecimal("2.00");
        System.out.println("equals: " + a.equals(b));
        System.out.println("compareTo: " + a.compareTo(b));
        System.out.println("iguais? " + (a.compareTo(b) == 0));
        BigDecimal saldo = new BigDecimal("50.00");
        BigDecimal saque = new BigDecimal("80");
        if (saque.compareTo(saldo) > 0) {
            System.out.println("Saque maior que o saldo");
        }
        System.out.println("Maior: " + saldo.max(saque));
        System.out.println("Sem zeros: " + b.stripTrailingZeros());
    }
}
$ java Main.java
equals: false
compareTo: 0
iguais? true
Saque maior que o saldo
Maior: 80
Sem zeros: 2

2.0 e 2.00 têm o mesmo valor, mas escalas diferentes (1 e 2 casas). O equals do BigDecimal compara valor e escala, por isso dá false. O compareTo compara só o valor, e devolve um número:

a.compareTo(b)Quer dizerEquivale a (com números comuns)
negativoa é menora < b → a.compareTo(b) < 0
0valores iguaisa == b → a.compareTo(b) == 0
positivoa é maiora > b → a.compareTo(b) > 0

Truque para ler: escreva a comparação que você queria (saque > saldo), troque os dois lados por saque.compareTo(saldo) e compare com zero usando o mesmo sinal (> 0). O compareTo é o mesmo método que o Java usa para ordenar (veja Comparable e Comparator). E nada de <, > ou == direto entre BigDecimal: são objetos.


Mostrando dinheiro

O toString do BigDecimal usa ponto. Para mostrar ao usuário no formato brasileiro, use o NumberFormat da aula Formatar Texto, que aceita BigDecimal diretamente:

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

public class Main {
    public static void main(String[] args) {
        BigDecimal total = new BigDecimal("1234.5");
        NumberFormat real = NumberFormat.getCurrencyInstance(Locale.of("pt", "BR"));
        System.out.println(real.format(total));
        System.out.println(total.doubleValue());
        System.out.println(total.intValue());
    }
}
$ java Main.java
R$ 1.234,50
1234.5
1234

doubleValue() e intValue() convertem de volta para primitivos (o intValue corta as casas). Use-os só na saída, nunca para continuar fazendo contas com dinheiro. O printf("%.2f", total) também aceita BigDecimal.


BigInteger: inteiros sem limite

O long vai até 9.223.372.036.854.775.807 (veja int, long, short, byte). Passou disso, ele "dá a volta" e vira negativo, sem aviso. O fatorial cresce muito rápido (5! = 5 × 4 × 3 × 2 × 1 = 120) e mostra bem o problema. O BigInteger, também de java.math, cresce o quanto for preciso:

import java.math.BigInteger;

public class Main {
    public static void main(String[] args) {
        long fatorialLong = 1;
        BigInteger fatorial = BigInteger.ONE;
        for (int i = 1; i <= 50; i++) {
            fatorialLong *= i;
            fatorial = fatorial.multiply(BigInteger.valueOf(i));
            if (i == 20 || i == 21 || i == 50) {
                System.out.println(i + "! com long:       " + fatorialLong);
                System.out.println(i + "! com BigInteger: " + fatorial);
            }
        }
        System.out.println("Dígitos de 50!: " + fatorial.toString().length());
        System.out.println("2 elevado a 100: " + BigInteger.TWO.pow(100));
    }
}
$ java Main.java
20! com long:       2432902008176640000
20! com BigInteger: 2432902008176640000
21! com long:       -4249290049419214848
21! com BigInteger: 51090942171709440000
50! com long:       -3258495067890909184
50! com BigInteger: 30414093201713378043612608166064768844377641568960512000000000000
Dígitos de 50!: 65
2 elevado a 100: 1267650600228229401496703205376

Até 20!, os dois concordam. Em 21!, o long estoura (overflow) e mostra um número negativo absurdo; o BigInteger continua certo até 50!, um número de 65 dígitos. Os métodos são os mesmos do BigDecimal (add, subtract, multiply, divide, pow, compareTo), com BigInteger.valueOf(n) para criar a partir de um long e constantes ZERO, ONE, TWO, TEN. Ele é usado em criptografia, em cálculos científicos e em qualquer conta que possa passar do limite do long.


Qual tipo usar

SituaçãoTipo
Dinheiro, preços, impostos, jurosBigDecimal (criado com texto)
Medidas físicas, gráficos, estatística, jogosdouble: rápido, e o erro minúsculo não importa
Contagens, quantidades inteirasint ou long
Inteiros maiores que 9,2 × 1018BigInteger
Alternativa simples para dinheirolong com o valor em centavos (1990 = R$ 19,90). Funciona, mas porcentagens e divisões exigem cuidado

Quando dá errado

"bad operand types for binary operator '*'" e "double cannot be converted to BigDecimal"
import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        BigDecimal preco = new BigDecimal("10.50");
        BigDecimal total = preco * 2;
        BigDecimal frete = 5.00;
    }
}
$ java Main.java
Main.java:6: error: bad operand types for binary operator '*'
        BigDecimal total = preco * 2;
                                 ^
  first type:  BigDecimal
  second type: int
Main.java:7: error: incompatible types: double cannot be converted to BigDecimal
        BigDecimal frete = 5.00;
                           ^
2 errors
error: compilation failed

O que aconteceu: os operadores + - * / só funcionam com primitivos (e o + com String). E não existe "autoboxing" de double para BigDecimal.

Como resolver: preco.multiply(new BigDecimal("2")) (ou BigDecimal.valueOf(2)) e new BigDecimal("5.00").

O saldo não mudou
import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        BigDecimal saldo = new BigDecimal("100.00");
        saldo.add(new BigDecimal("50.00"));
        System.out.println("Saldo depois do depósito: " + saldo);
    }
}
$ java Main.java
Saldo depois do depósito: 100.00

O que aconteceu: BigDecimal é imutável; o add devolveu 150.00 e o resultado foi descartado.

Como resolver: saldo = saldo.add(new BigDecimal("50.00"));.

"Non-terminating decimal expansion"

O que aconteceu: um divide(x) sem escala, numa divisão que dá dízima (por 3, 6, 7…).

Como resolver: sempre que dividir dinheiro, informe as casas e o arredondamento: divide(x, 2, RoundingMode.HALF_UP).

Dois valores iguais deram equals falso

O que aconteceu: escalas diferentes (2.0 × 2.00, ou 10 × 10.00).

Como resolver: compare com a.compareTo(b) == 0.


Referência

CódigoO que faz
new BigDecimal("19.90")Cria a partir de texto (o jeito certo)
BigDecimal.valueOf(d)Cria a partir de um double ou long, com segurança
BigDecimal.ZERO, ONE, TENConstantes
add, subtract, multiplySoma, subtração, multiplicação (devolvem um novo)
divide(b, 2, RoundingMode.HALF_UP)Divisão com 2 casas
setScale(2, RoundingMode.HALF_UP)Arredonda para 2 casas
scale()Quantas casas decimais
a.compareTo(b)Negativo, 0 ou positivo
max, min, abs, negateMaior, menor, absoluto, negativo
stripTrailingZeros()Tira zeros do fim
doubleValue(), intValue()Converte para primitivo (só na saída)
BigInteger.valueOf(n), multiply, powInteiros sem limite

Exercícios

1. Criar do jeito certo

Complete para criar exatamente 0,1:

BigDecimal x = new BigDecimal();

2. Somar

Complete para somar b ao total, guardando o resultado:

total = total.;

3. Arredondar

Complete para deixar valor com 2 casas, arredondando o 5 para cima:

valor.setScale(2, RoundingMode.)

4. O que System.out.println(0.1 + 0.2); mostra?

5. new BigDecimal("5.0").equals(new BigDecimal("5.00")) dá…

6. Que linha lança ArithmeticException?

Missão no JShell: calcule 0.1 + 0.2 com BigDecimal, criando os dois números a partir de texto, e confira que o resultado é exatamente 0.3. (O JShell já importa java.math.) Depois, experimente new BigDecimal(0.1).

new BigDecimal("0.1").add(new BigDecimal("0.2"))

Missão final: a conta de R$ 100,00 será dividida entre 3 amigos. O programa quebra com ArithmeticException. Corrija a divisão para 2 casas com RoundingMode.HALF_UP e calcule a sobra: conta menos cada × pessoas. Saída esperada: Cada um paga: 33.33 e Sobra: 0.01.

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

public class Main {
    public static void main(String[] args) {
        BigDecimal conta = new BigDecimal("100.00");
        BigDecimal pessoas = new BigDecimal("3");
        BigDecimal cada = conta.divide(pessoas);
        System.out.println("Cada um paga: " + cada);
    }
}
import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        BigDecimal conta = new BigDecimal("100.00");
        BigDecimal pessoas = new BigDecimal("3");
        BigDecimal cada = conta.divide(pessoas, 2, RoundingMode.HALF_UP);
        BigDecimal sobra = conta.subtract(cada.multiply(pessoas));
        System.out.println("Cada um paga: " + cada);
        System.out.println("Sobra: " + sobra);
    }
}

Resumo

  • double guarda decimais em binário e erra frações como 0,1: 0.1 + 0.2 dá 0.30000000000000004. Não use para dinheiro.
  • BigDecimal é exato. Crie com texto (new BigDecimal("0.1")) ou BigDecimal.valueOf; nunca com new BigDecimal(0.1).
  • Contas são métodos (add, subtract, multiply, divide) e devolvem um objeto novo: guarde o resultado.
  • setScale(2, RoundingMode.HALF_UP) arredonda para centavos; escolha o RoundingMode de propósito.
  • divide sem escala lança ArithmeticException quando o resultado não termina: use divide(x, 2, RoundingMode.HALF_UP).
  • Compare com compareTo (o equals considera a escala: 2.0 ≠ 2.00).
  • BigInteger faz contas com inteiros de qualquer tamanho: 50! tem 65 dígitos.