I was trying to make my own class for currencies using longs, but apparently I should use BigDecimal
instead. Could someone help me get started? What would be the best way to use BigDecimal
s for dollar currencies, like making it at least but no more than 2 decimal places for the cents, etc. The API for BigDecimal
is huge, and I don't know which methods to use. Also, BigDecimal
has better precision, but isn't that all lost if it passes through a double
? if I do new BigDecimal(24.99)
, how will it be different than using a double
? Or should I use the constructor that uses a String
instead?
问题:
回答1:
Here are a few hints:
- Use
BigDecimal
for computations if you need the precision that it offers (Money values often need this). - Use the
NumberFormat
class for display. This class will take care of localization issues for amounts in different currencies. However, it will take in only primitives; therefore, if you can accept the small change in accuracy due to transformation to adouble
, you could use this class. - When using the
NumberFormat
class, use thescale()
method on theBigDecimal
instance to set the precision and the rounding method.
PS: In case you were wondering, BigDecimal
is always better than double
, when you have to represent money values in Java.
PPS:
Creating BigDecimal
instances
This is fairly simple since BigDecimal
provides constructors to take in primitive values, and String
objects. You could use those, preferably the one taking the String
object. For example,
BigDecimal modelVal = new BigDecimal("24.455");
BigDecimal displayVal = modelVal.setScale(2, RoundingMode.HALF_EVEN);
Displaying BigDecimal
instances
You could use the setMinimumFractionDigits
and setMaximumFractionDigits
method calls to restrict the amount of data being displayed.
NumberFormat usdCostFormat = NumberFormat.getCurrencyInstance(Locale.US);
usdCostFormat.setMinimumFractionDigits( 1 );
usdCostFormat.setMaximumFractionDigits( 2 );
System.out.println( usdCostFormat.format(displayVal.doubleValue()) );
回答2:
I would recommend a little research on Money Pattern. Martin Fowler in his book Analysis pattern has covered this in more detail.
public class Money {
private static final Currency USD = Currency.getInstance("USD");
private static final RoundingMode DEFAULT_ROUNDING = RoundingMode.HALF_EVEN;
private final BigDecimal amount;
private final Currency currency;
public static Money dollars(BigDecimal amount) {
return new Money(amount, USD);
}
Money(BigDecimal amount, Currency currency) {
this(amount, currency, DEFAULT_ROUNDING);
}
Money(BigDecimal amount, Currency currency, RoundingMode rounding) {
this.currency = currency;
this.amount = amount.setScale(currency.getDefaultFractionDigits(), rounding);
}
public BigDecimal getAmount() {
return amount;
}
public Currency getCurrency() {
return currency;
}
@Override
public String toString() {
return getCurrency().getSymbol() + " " + getAmount();
}
public String toString(Locale locale) {
return getCurrency().getSymbol(locale) + " " + getAmount();
}
}
Coming to the usage:
You would represent all monies using Money
object as opposed to BigDecimal
. Representing money as big decimal will mean that you will have the to format the money every where you display it. Just imagine if the display standard changes. You will have to make the edits all over the place. Instead using the Money
pattern you centralize the formatting of money to a single location.
Money price = Money.dollars(38.28);
System.out.println(price);
回答3:
Or, wait for JSR-354. Java Money and Currency API coming soon!
回答4:
1) If you are limited to the double
precision, one reason to use BigDecimal
s is to realize operations with the BigDecimal
s created from the double
s.
2) The BigDecimal
consists of an arbitrary precision integer unscaled value and a non-negative 32-bit integer scale, while the double wraps a value of the primitive type double
in an object. An object of type Double
contains a single field whose type is double
3) It should make no difference
You should have no difficulties with the $ and precision. One way to do it is using System.out.printf
回答5:
Use BigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP)
when you want to round up to the 2 decimal points for cents. Be aware of rounding off error when you do calculations though. You need to be consistent when you will be doing the rounding of money value. Either do the rounding right at the end just once after all calculations are done, or apply rounding to each value before doing any calculations. Which one to use would depend on your business requirement, but generally, I think doing rounding right at the end seems to make a better sense to me.
Use a String
when you construct BigDecimal
for money value. If you use double
, it will have a trailing floating point values at the end. This is due to computer architecture regarding how double
/float
values are represented in binary format.
回答6:
Primitive numeric types are useful for storing single values in memory. But when dealing with calculation using double and float types, there is a problems with the rounding.It happens because memory representation doesn't map exactly to the value. For example, a double value is supposed to take 64 bits but Java doesn't use all 64 bits.It only stores what it thinks the important parts of the number. So you can arrive to the wrong values when you adding values together of the float or double type.
Please see a short clip https://youtu.be/EXxUSz9x7BM
回答7:
There is an extensive example of how to do this on javapractices.com. See in particular the Money
class, which is meant to make monetary calculations simpler than using BigDecimal
directly.
The design of this Money
class is intended to make expressions more natural. For example:
if ( amount.lt(hundred) ) {
cost = amount.times(price);
}
The WEB4J tool has a similar class, called Decimal
, which is a bit more polished than the Money
class.
回答8:
NumberFormat.getNumberInstance(java.util.Locale.US).format(num);
回答9:
I would be radical. No BigDecimal.
Here is a great article https://lemnik.wordpress.com/2011/03/25/bigdecimal-and-your-money/
Ideas from here.
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
testConstructors();
testEqualsAndCompare();
testArithmetic();
}
private static void testEqualsAndCompare() {
final BigDecimal zero = new BigDecimal("0.0");
final BigDecimal zerozero = new BigDecimal("0.00");
boolean zerosAreEqual = zero.equals(zerozero);
boolean zerosAreEqual2 = zerozero.equals(zero);
System.out.println("zerosAreEqual: " + zerosAreEqual + " " + zerosAreEqual2);
int zerosCompare = zero.compareTo(zerozero);
int zerosCompare2 = zerozero.compareTo(zero);
System.out.println("zerosCompare: " + zerosCompare + " " + zerosCompare2);
}
private static void testArithmetic() {
try {
BigDecimal value = new BigDecimal(1);
value = value.divide(new BigDecimal(3));
System.out.println(value);
} catch (ArithmeticException e) {
System.out.println("Failed to devide. " + e.getMessage());
}
}
private static void testConstructors() {
double doubleValue = 35.7;
BigDecimal fromDouble = new BigDecimal(doubleValue);
BigDecimal fromString = new BigDecimal("35.7");
boolean decimalsEqual = fromDouble.equals(fromString);
boolean decimalsEqual2 = fromString.equals(fromDouble);
System.out.println("From double: " + fromDouble);
System.out.println("decimalsEqual: " + decimalsEqual + " " + decimalsEqual2);
}
}
It prints
From double: 35.7000000000000028421709430404007434844970703125
decimalsEqual: false false
zerosAreEqual: false false
zerosCompare: 0 0
Failed to devide. Non-terminating decimal expansion; no exact representable decimal result.
How about storing BigDecimal into a database? Hell, it also stores as a double value??? At least, if I use mongoDb without any advanced configuration it will store BigDecimal.TEN
as 1E1
.
Possible solutions?
I came with one - use String to store BigDecimal in Java as a String into the database. You have validation, for example @NotNull
, @Min(10)
, etc... Then you can use a trigger on update or save to check if current string is a number you need. There are no triggers for mongo though.
Is there a built-in way for Mongodb trigger function calls?
There is one drawback I am having fun around - BigDecimal as String in Swagger defenition
I need to generate swagger, so our front-end team understands that I pass them a number presented as a String. DateTime for example presented as a String.
There is another cool solution I read in the article above... Use long to store precise numbers.
A standard long value can store the current value of the Unites States national debt (as cents, not dollars) 6477 times without any overflow. Whats more: it’s an integer type, not a floating point. This makes it easier and accurate to work with, and a guaranteed behavior.
Update
https://stackoverflow.com/a/27978223/4587961
Maybe in the future MongoDb will add support for BigDecimal. https://jira.mongodb.org/browse/SERVER-1393 3.3.8 seems to have this done.
It is an example of the second approach. Use scaling. http://www.technology-ebay.de/the-teams/mobile-de/blog/mapping-bigdecimals-with-morphia-for-mongodb.html