Classe Dinheiro específica: aberta para sugestões

Povo,

Desenvolvi uma classe que representa dinheiro. Gostaria da sugestões de vocês, mas tenham em mente que:

1 - não preciso trabalhar com mais de um tipo de moeda.
2 - não preciso trabalhar com uma sacola (bag) de moedas.
3 - não quero utilizar library de terceiros (mas as sugestões são bem-vindas).

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

import javax.persistence.Embeddable;

@Embeddable
public class Dinheiro implements Serializable, Comparable<Dinheiro> {

	private static final long serialVersionUID = 1L;
	
	private static final String mensagemDinheiroNull = "Dinheiro não pode ser null";

	private static MathContext CONTEXTO = new MathContext(15,
			RoundingMode.HALF_UP);

	private static int DECIMAIS = 2;
	private static int MENOR = -1;
	private static int IGUAL = 0;
	private static int MAIOR = 1;

	public static Dinheiro ZERO = new Dinheiro(0.00);

	private BigDecimal montante = BigDecimal.ZERO;

	protected Dinheiro() {
		this.montante = this.montante.setScale(Dinheiro.DECIMAIS,
				RoundingMode.HALF_UP);
	}

	public Dinheiro(String dinheiro) {
		if (dinheiro == null)
			throw new NullPointerException(Dinheiro.mensagemDinheiroNull);
		this.montante = new BigDecimal(dinheiro);
		if (this.montante.scale() > Dinheiro.DECIMAIS) {
			throw new IllegalArgumentException("Número de decimais superior a "
					+ Dinheiro.DECIMAIS);
		}
	}

	public Dinheiro(BigDecimal montante) {
		if (montante == null)
			throw new NullPointerException("Montante não pode ser null");
		if (montante.scale() > Dinheiro.DECIMAIS) {
			throw new IllegalArgumentException("Número de decimais superior a "
					+ Dinheiro.DECIMAIS);
		}

		this.montante = montante.setScale(Dinheiro.DECIMAIS,
				RoundingMode.HALF_UP);
	}

	public Dinheiro(double montante) {

		String[] partes = String.valueOf(montante).split("\\.");
		if (partes.length == 2 && partes[1].length() > Dinheiro.DECIMAIS) {
			throw new IllegalArgumentException("Número de decimais superior a "
					+ Dinheiro.DECIMAIS);
		}

		this.montante = BigDecimal.valueOf(montante).setScale(
				Dinheiro.DECIMAIS, RoundingMode.HALF_UP);
	}

	public Dinheiro(long montante) {
		this.montante = BigDecimal.valueOf(montante).setScale(
				Dinheiro.DECIMAIS, RoundingMode.HALF_UP);
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((montante == null) ? 0 : montante.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Dinheiro other = (Dinheiro) obj;
		if (montante == null) {
			if (other.montante != null)
				return false;
		} else if (montante.compareTo(other.montante) != 0)
			return false;
		return true;
	}

	@Override
	public String toString() {
		return "Dinheiro [montante=" + montante + "]";
	}

	public BigDecimal getMontante() {
		return this.montante;
	}

	public Dinheiro somar(Dinheiro valorSoma) {
		if (valorSoma == null)
			throw new NullPointerException(Dinheiro.mensagemDinheiroNull);
		return new Dinheiro(this.montante
				.add(valorSoma.getMontante(), CONTEXTO).setScale(DECIMAIS,
						RoundingMode.HALF_UP));
	}

	public Dinheiro subtrair(Dinheiro valorSubtrair) {
		if (valorSubtrair == null)
			throw new NullPointerException(Dinheiro.mensagemDinheiroNull);
		return new Dinheiro(this.montante.subtract(valorSubtrair.getMontante(),
				CONTEXTO).setScale(DECIMAIS, RoundingMode.HALF_UP));
	}

	public Dinheiro multiplicar(long valorMultiplicador) {
		return new Dinheiro(this.montante.multiply(
				BigDecimal.valueOf(valorMultiplicador), CONTEXTO).setScale(
				DECIMAIS, RoundingMode.HALF_UP));
	}

	public Dinheiro multiplicar(double valorMultiplicador) {
		return new Dinheiro(this.montante.multiply(
				BigDecimal.valueOf(valorMultiplicador), CONTEXTO).setScale(
				DECIMAIS, RoundingMode.HALF_UP));
	}

	public Dinheiro dividir(long valorDivisor) {
		return new Dinheiro(this.montante.divide(
				BigDecimal.valueOf(valorDivisor), CONTEXTO).setScale(DECIMAIS,
				RoundingMode.HALF_UP));
	}

	public Dinheiro dividir(double valorDivisor) {
		return new Dinheiro(this.montante.divide(
				BigDecimal.valueOf(valorDivisor), CONTEXTO).setScale(DECIMAIS,
				RoundingMode.HALF_UP));
	}

	public boolean isZero() {
		return this.montante.compareTo(BigDecimal.ZERO) == IGUAL;
	}

	public boolean isMaior(Dinheiro dinheiro) {
		return this.montante.compareTo(dinheiro.getMontante()) == MAIOR;
	}

	public boolean isMenor(Dinheiro dinheiro) {
		return this.montante.compareTo(dinheiro.getMontante()) == MENOR;
	}

	public boolean isMaiorIgual(Dinheiro dinheiro) {
		return (this.montante.compareTo(dinheiro.getMontante()) == MAIOR
				|| this.montante.compareTo(dinheiro.getMontante()) == IGUAL);
	}

	public boolean isMenorIgual(Dinheiro dinheiro) {
		return (this.montante.compareTo(dinheiro.getMontante()) == MENOR
				|| this.montante.compareTo(dinheiro.getMontante()) == IGUAL);
	}

	public Dinheiro percentual(double perc) {

		return new Dinheiro(this.montante
				.multiply(BigDecimal.valueOf(perc), CONTEXTO)
				.divide(BigDecimal.valueOf(100))
				.setScale(DECIMAIS, RoundingMode.HALF_UP));
	}

	public int dividirParaInteiro(double divisor) {
		return this.montante.divideToIntegralValue(BigDecimal.valueOf(divisor))
				.intValue();
	}

	public int dividirParaInteiro(long divisor) {
		return this.montante.divideToIntegralValue(BigDecimal.valueOf(divisor))
				.intValue();
	}

	public int dividirParaInteiro(BigDecimal divisor) {
		return this.montante.divideToIntegralValue(divisor).intValue();

	}

	public long dividirParaLong(double divisor) {
		return this.montante.divideToIntegralValue(BigDecimal.valueOf(divisor))
				.longValue();
	}

	public long dividirParaLong(long divisor) {
		return this.montante.divideToIntegralValue(BigDecimal.valueOf(divisor))
				.longValue();
	}

	public long dividirParaLong(BigDecimal divisor) {
		return this.montante.divideToIntegralValue(divisor).longValue();

	}

	public double doubleValue() {
		return this.montante.doubleValue();
	}

	public long longValue() {
		return this.montante.longValue();
	}

	public Dinheiro inverter() {
		return new Dinheiro(this.getMontante().negate(Dinheiro.CONTEXTO));
	}

	public Dinheiro max(Dinheiro dinheiro) {
		return new Dinheiro(this.montante.max(dinheiro.montante));
	}

	public Dinheiro min(Dinheiro dinheiro) {
		return new Dinheiro(this.montante.min(dinheiro.montante));
	}

	@Override
	public int compareTo(Dinheiro dinheiro) {
		if (dinheiro == null)
			throw new NullPointerException(Dinheiro.mensagemDinheiroNull);
		return this.montante.compareTo(dinheiro.getMontante());
	}
}

http://www.idinews.com/sourcecode/MoneyJava.html

Olá! Estou utilizando essa classe Dinheiro em meus projetos. Alterei uma coisa fundamental nessa classe, o arredondamento das operações matemáticas. Não podemos arredondar o resultado a cada operação, o correto é arredondar somente quando queremos que a calculadora “cuspa” o resultado. Segue abaixo minhas alterações:

package any;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Locale;

public class Dinheiro extends Number implements Serializable, Comparable<Dinheiro>{

	private static final long serialVersionUID = 1L;
	
	private static final String mensagemDinheiroNull = "Dinheiro não pode ser null";

	private static MathContext CONTEXTO = new MathContext(15,
			RoundingMode.HALF_UP);

	private static int DECIMAIS = 2;
	private static int MENOR = -1;
	private static int IGUAL = 0;
	private static int MAIOR = 1;

	public static Dinheiro ZERO = new Dinheiro(0.00);

	private BigDecimal rawMontante = BigDecimal.ZERO;
	
	private Locale ptBr = new Locale("pt", "BR");
	private NumberFormat moedaFormat =  NumberFormat.getCurrencyInstance(ptBr);  //para moedas
	
	
	protected Dinheiro() {
		
	}

	public Dinheiro(String dinheiro) {
		if (dinheiro == null)
			throw new NullPointerException(Dinheiro.mensagemDinheiroNull);
		this.rawMontante = new BigDecimal(dinheiro);
	}

	public Dinheiro(BigDecimal rawMontante) {
		if (rawMontante == null)
			throw new NullPointerException("Montante nao pode ser null");
		
		this.rawMontante = rawMontante;
	}

	public Dinheiro(double rawMontante) {
		
		this.rawMontante = BigDecimal.valueOf(rawMontante);
	}

	public Dinheiro(long rawMontante) {
		this.rawMontante = BigDecimal.valueOf(rawMontante);
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((this.getMontante() == null) ? 0 : this.getMontante().hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Dinheiro other = (Dinheiro) obj;
		if (this.getMontante() == null) {
			if (other.getMontante() != null)
				return false;
		} else if (this.getMontante().compareTo(other.getMontante()) != 0)
			return false;
		return true;
	}

	@Override
	public String toString() {
		return this.getMontante().toString();
	}

	public String printAsMoney(){
						
		return moedaFormat.format(this.getMontante());
	}
	
	public BigDecimal getMontante() {
		return this.rawMontante.setScale(DECIMAIS, RoundingMode.HALF_UP);
	}

	public BigDecimal getRawMontante() {
		return this.rawMontante;
	}

	public Dinheiro somar(Dinheiro valorSoma) {
		if (valorSoma == null)
			throw new NullPointerException(Dinheiro.mensagemDinheiroNull);
		return new Dinheiro(this.rawMontante.add(valorSoma.getRawMontante(), CONTEXTO));
	}

	public Dinheiro subtrair(Dinheiro valorSubtrair) {
		if (valorSubtrair == null)
			throw new NullPointerException(Dinheiro.mensagemDinheiroNull);
		return new Dinheiro(this.rawMontante.subtract(valorSubtrair.getRawMontante(),CONTEXTO));
	}

	public Dinheiro multiplicar(long valorMultiplicador) {
		return new Dinheiro(this.rawMontante.multiply(BigDecimal.valueOf(valorMultiplicador), CONTEXTO));
	}

	public Dinheiro multiplicar(double valorMultiplicador) {
		return new Dinheiro(this.rawMontante.multiply(BigDecimal.valueOf(valorMultiplicador), CONTEXTO));
	}

	public Dinheiro dividir(long valorDivisor) {
		return new Dinheiro(this.rawMontante.divide(BigDecimal.valueOf(valorDivisor), CONTEXTO));
	}

	public Dinheiro dividir(double valorDivisor) {
		return new Dinheiro(this.rawMontante.divide(BigDecimal.valueOf(valorDivisor), CONTEXTO));
	}

	public boolean isZero() {
		return this.getMontante().compareTo(BigDecimal.ZERO) == IGUAL;
	}

	public boolean isMaior(Dinheiro dinheiro) {
		return this.getMontante().compareTo(dinheiro.getMontante()) == MAIOR;
	}

	public boolean isMenor(Dinheiro dinheiro) {
		return this.getMontante().compareTo(dinheiro.getMontante()) == MENOR;
	}

	public boolean isMaiorIgual(Dinheiro dinheiro) {
		return (this.getMontante().compareTo(dinheiro.getMontante()) == MAIOR
				|| this.getMontante().compareTo(dinheiro.getMontante()) == IGUAL);
	}

	public boolean isMenorIgual(Dinheiro dinheiro) {
		return (this.getMontante().compareTo(dinheiro.getMontante()) == MENOR
				|| this.getMontante().compareTo(dinheiro.getMontante()) == IGUAL);
	}

	public Dinheiro percentual(double perc) {

		return new Dinheiro(this.rawMontante
				.multiply(BigDecimal.valueOf(perc), CONTEXTO)
				.divide(BigDecimal.valueOf(100), CONTEXTO));
				
	}

	public int dividirParaInteiro(double divisor) {
		return this.rawMontante.divideToIntegralValue(BigDecimal.valueOf(divisor))
				.intValue();
	}

	public int dividirParaInteiro(long divisor) {
		return this.rawMontante.divideToIntegralValue(BigDecimal.valueOf(divisor))
				.intValue();
	}

	public int dividirParaInteiro(BigDecimal divisor) {
		return this.rawMontante.divideToIntegralValue(divisor).intValue();

	}

	public long dividirParaLong(double divisor) {
		return this.rawMontante.divideToIntegralValue(BigDecimal.valueOf(divisor))
				.longValue();
	}

	public long dividirParaLong(long divisor) {
		return this.rawMontante.divideToIntegralValue(BigDecimal.valueOf(divisor))
				.longValue();
	}

	public long dividirParaLong(BigDecimal divisor) {
		return this.rawMontante.divideToIntegralValue(divisor).longValue();

	}

	public double doubleValue() {
		return this.getMontante().doubleValue();
	}

	public long longValue() {
		return this.getMontante().longValue();
	}

	@Override
	public int intValue() {
		return this.getMontante().intValue();
	}
	
	@Override
	public float floatValue() {
		return this.getMontante().floatValue();
	}
	
	public Dinheiro inverter() {
		return new Dinheiro(this.getRawMontante().negate(Dinheiro.CONTEXTO));
	}

	public Dinheiro max(Dinheiro dinheiro) {
		return new Dinheiro(this.getMontante().max(dinheiro.getMontante()));
	}

	public Dinheiro min(Dinheiro dinheiro) {
		return new Dinheiro(this.getMontante().min(dinheiro.getMontante()));
	}

	@Override
	public int compareTo(Dinheiro dinheiro) {
		if (dinheiro == null)
			throw new NullPointerException(Dinheiro.mensagemDinheiroNull);
		return this.getMontante().compareTo(dinheiro.getMontante());
	}

}

Na verdade o ideal é nem armazenar em BigDecimal e, sim em BigInteger. É mais fácil considerar os centavos, que é a menor unidade do nosso dinheiro.

A divisão também deveria retornar uma lista. 10 reais divididos por 3 pessoas daria R$3,33 ; R$3,33 e R$3,34.

Armazenar em um BigInteger também deixa a classe bem genérica, pois nem toda moeda utiliza parte fracionária (o Iene, por exemplo, não tem). Quando o valor for mostrado para o usuário a conversão de centavos pra reais seria feita.

É mais ou menos como os modos de arredondamento das calculadoras científicas, no display ela mostra o valor arredondado, mas, internamente, ela armazena o valor mais preciso que ela consegue.