一、引言
借用《Effactive Java》這本書中的話,float和double類型的主要設計目標是為了科學計算和工程計算。他們執行二進制浮點運算,這是為了在廣域數值范圍上提供 較為精確的快速近似計算而精心設計的。然而,它們沒有提供完全精確的結果,所以不應該被用於要求精確結果的場合。但是,貨幣計算往往要求結果精確,這時候 可以使用int、long或BigDecimal。本文主要講述BigDecimal使用過程中的一些陷阱、建議和技巧。
二、不可變性
BigDecimal是不可變類,每一個操作(加減乘除等)都會返回一個新的對象, 下面以加法操作為例。
BigDecimal a =new BigDecimal("1.22");
System.out.println("construct with a String value: " + a);
BigDecimal b =new BigDecimal("2.22");
a.add(b);
System.out.println("a plus b is : " + a);
我們很容易會認為會輸出:
construct with a String value: 1.22
a plus b is :3.44
但實際上a plus b is : 1.22
下面我們就來分析一下加法操作的源碼
public BigDecimal add(BigDecimal augend) { long xs =this.intCompact; //整型數字表示的BigDecimal,例a的intCompact值為122 long ys = augend.intCompact;//同上 //初始化BigInteger的值,intVal為BigDecimal的一個BigInteger類型的屬性 BigInteger fst = (this.intCompact !=INFLATED) ?null :this.intVal; BigInteger snd =(augend.intCompact !=INFLATED) ?null : augend.intVal; int rscale =this.scale;//小數位數 long sdiff = (long)rscale - augend.scale;//小數位數之差 if (sdiff != 0) {//取小數位數多的為結果的小數位數 if (sdiff < 0) { int raise =checkScale(-sdiff); rscale =augend.scale; if (xs ==INFLATED ||(xs = longMultiplyPowerTen(xs,raise)) ==INFLATED) fst =bigMultiplyPowerTen(raise); }else { int raise =augend.checkScale(sdiff); if (ys ==INFLATED ||(ys =longMultiplyPowerTen(ys,raise)) ==INFLATED) snd = augend.bigMultiplyPowerTen(raise); } } if (xs !=INFLATED && ys !=INFLATED) { long sum = xs + ys; if ( (((sum ^ xs) &(sum ^ ys))) >= 0L)//判斷有無溢出 //返回使用BigDecimal的靜態工廠方法得到的BigDecimal實例 return BigDecimal.valueOf(sum,rscale); } if (fst ==null) fst =BigInteger.valueOf(xs);//BigInteger的靜態工廠方法 if (snd ==null) snd =BigInteger.valueOf(ys); BigInteger sum =fst.add(snd); //返回通過其他構造方法得到的BigDecimal對象 return (fst.signum == snd.signum) ?new BigDecimal(sum,INFLATED, rscale, 0) : new BigDecimal(sum,compactValFor(sum),rscale, 0); }
因為BigInteger與BigDecimal都是不可變的(immutable)的,在進行每一步運算時,都會產生一個新的對象,所以 a.add(b)雖然做了加法操作,但是a並沒有保存加操作后的值,正確的用法應該是a=a.add(b); 減乘除操作也是一樣的返回一個新的BigDecimal對象。
三、構造函數和valueOf方法
首先看如下一段代碼:
// use constructor BigDecimal(double)
BigDecimal aDouble =new BigDecimal(1.22);
System.out.println("construct with a double value: " + aDouble);
// use constructor BigDecimal(String)
BigDecimal aString = new BigDecimal("1.22");
System.out.println("construct with a String value: " + aString);
// use constructor BigDecimal.valueOf(double)
BigDecimal aValue = BigDecimal.valueOf(1.22);
System.out.println("use valueOf method: " + aValue);
你認為輸出結果會是什么呢?如果你認為第一個會輸出1.22,那么恭喜你答錯了,輸出結果如下:
construct with a double value: 1.2199999999999999733546474089962430298328399658203125
construct with a String value: 1.22
use valueOf method: 1.22
為什么會這樣呢?JavaDoc對於BigDecimal(double)有很詳細的說明:
1、參數類型為double的構造方法的結果有一定的不可預知性。有人可能認為在Java中new BigDecimal(0.1)所創建的BigDecimal的值正好等於 0.1(非標度值 1,其標度為 1),但是它實際上等於0.1000000000000000055511151231257827021181583404541015625。這是因 為0.1無法准確地表示為 double(或者說對於該情況,不能表示為任何有限長度的二進制小數)。這樣,傳入到構造方法的值不會正好等於 0.1(雖然表面上等於該值)。
2、另一方面,String 構造方法是完全可預知的:new BigDecimal("0.1") 將創建一個 BigDecimal,它的值正好等於期望的0.1。因此,比較而言,通常建議優先使用String構造方法。
3、當 double 必須用作BigDecimal的來源時,請注意,此構造方法提供了一個精確轉換;它不提供與以下操作相同的結果:先使用 Double.toString(double)方法將double轉換為String,然后使用BigDecimal(String)構造方法。要獲取 該結果,使用static valueOf(double)方法。
?? BigDecimal.valueOf(double) 使用由 Double.toString(double)方法提供的 double的標准化字符串表示形式( canonical string representation) 將 double 轉換成 BigDecimal 。這也是比較推薦的一種方式。
??
BigDecimal.valueOf(double)還有一個重載的方法 BigDecimal.valueOf(long),對於某些常用值(0到10) BigDecimal在內部做了緩存, 如果傳遞的參數值范圍為[0, 10], 這個方法直接返回緩存中相應的BigDecimal對象。
java源碼如下:
/** * Translates a {@code long} value into a {@code BigDecimal} * with a scale of zero. This {@literal "static factory method"} * is provided in preference to a ({@code long}) constructor * because it allows for reuse of frequently used * {@code BigDecimal} values. * * @param val value of the {@code BigDecimal}. * @return a {@code BigDecimal} whose value is {@code val}. */ public static BigDecimal valueOf(long val) { if (val >= 0 && val < zeroThroughTen.length) return zeroThroughTen[(int)val]; else if (val != INFLATED) return new BigDecimal(null, val, 0, 0); return new BigDecimal(INFLATED_BIGINT, val, 0, 0); } // Cache of common small BigDecimal values. private static final BigDecimal zeroThroughTen[] = { new BigDecimal(BigInteger.ZERO, 0, 0, 1), new BigDecimal(BigInteger.ONE, 1, 0, 1), new BigDecimal(BigInteger.valueOf(2), 2, 0, 1), new BigDecimal(BigInteger.valueOf(3), 3, 0, 1), new BigDecimal(BigInteger.valueOf(4), 4, 0, 1), new BigDecimal(BigInteger.valueOf(5), 5, 0, 1), new BigDecimal(BigInteger.valueOf(6), 6, 0, 1), new BigDecimal(BigInteger.valueOf(7), 7, 0, 1), new BigDecimal(BigInteger.valueOf(8), 8, 0, 1), new BigDecimal(BigInteger.valueOf(9), 9, 0, 1), new BigDecimal(BigInteger.TEN, 10, 0, 2), };
附上相應的測試代碼:
BigDecimal a1 = BigDecimal.valueOf(10);
BigDecimal a2 = BigDecimal.valueOf(10);
System.out.println(a1 == a2); // true
BigDecimal a3 = BigDecimal.valueOf(11);
BigDecimal a4 = BigDecimal.valueOf(11);
System.out.println(a3 == a4); // false
四、equals方法
BigDecimal.equals方法是有問題的.僅當你確定比較的值有着相同的標度時才可使用. 因此,當你校驗相等性時注意 - BigDecimal有一個標度,用於相等性比較. 而compareTo方法則會忽略這個標度(scale).
BigDecimal的equals方法源碼如下:
@Override public boolean equals(Object x) { // 必須是BigDecimal實例 if (!(x instanceof BigDecimal)) return false; BigDecimal xDec = (BigDecimal) x; if (x == this) return true; // 標度必須相同 if (scale != xDec.scale) return false; long s = this.intCompact; long xs = xDec.intCompact; if (s != INFLATED) { if (xs == INFLATED) xs = compactValFor(xDec.intVal); return xs == s; } else if (xs != INFLATED) return xs == compactValFor(this.intVal); return this.inflated().equals(xDec.inflated()); }
參見以下測試代碼:
// 打印false
System.out.println(new BigDecimal("0.0").equals(new BigDecimal("0.00")));
// 打印false
System.out.println(new BigDecimal("0.0").hashCode() == (new BigDecimal("0.00")).hashCode());
// 打印0
System.out.println(new BigDecimal("0.0").compareTo(new BigDecimal("0.00")));
五、對除法使用標度
BigDecimal對象的精度沒有限制。如果結果不能終止,divide方法將會拋出ArithmeticException, 如1 / 3 = 0.33333...。所以強烈推薦使用重載方法divide(BigDecimal d, int scale, int roundMode)指定標度和舍入模式來避免以上異常。
參見以下測試代碼:
//java.lang.ArithmeticException: Non-terminating decimal expansion;
//no exact representable decimal result.
try {
BigDecimal.valueOf(1).divide(BigDecimal.valueOf(3));
} catch (ArithmeticException ex) {
System.out.println(ex.getMessage());
}
// always use a scale and the rounding mode of your choice
// 0.33
System.out.println(BigDecimal.valueOf(1).divide(BigDecimal.valueOf(3), 2, BigDecimal.ROUND_HALF_UP));
六、總結
(1)商業計算使用BigDecimal。
(2)使用參數類型為String的構造函數,將double轉換成BigDecimal時用BigDecimal.valueOf(double),做 除法運算時使用重載的方法divide(BigDecimal d, int scale, int roundMode)。
(3)BigDecimal是不可變的(immutable)的,在進行每一步運算時,都會產生一個新的對象,所以在做加減乘除運算時千萬要保存操作后的值。
(4)盡量使用compareTo方法比較兩個BigDecimal對象的大小。
再來看看數字格式化輸出的概念:
有時候我需要將數字按照本地的風格習慣進行數字的顯示,可以用NumberFormat,
此類的定義如下:
public abstract class NumberFormat extends Format
MessageFormat 、DateFormat 、NumberFormat 是 Format 三個常用的子類,如果要想進一步完成一個好的國際化程序,則肯定需要同時使用這樣三個類完成,根據不同的國家顯示貸幣的形式。
此類還是在java.text 包中,所以直接導入此包即可。
import java.text.* ; public class NumberFormatDemo01{ public static void main(String args[]){ NumberFormat nf = null ; // 聲明一個NumberFormat對象 nf = NumberFormat.getInstance() ; // 得到默認的數字格式化顯示 System.out.println("格式化之后的數字:" + nf.format(10000000)) ; System.out.println("格式化之后的數字:" + nf.format(1000.345)) ; } };
在美國,"."是小數點,但在其它地方就不一定了。如何處理這個呢? java.text 包中的一些包可以處理這類問題。下面的簡單范例使用那些類解決上面提出的問題:
import java.text.NumberFormat;
import java.util.Locale;
public class DecimalFormat1 {
public static void main(String args[]) {
// 得到本地的缺省格式
NumberFormat nf1 = NumberFormat.getInstance();
System.out.println(nf1.format(1234.56));
// 得到德國的格式
NumberFormat nf2 = NumberFormat.getInstance(Locale.GERMAN);
System.out.println(nf2.format(1234.56));
} }
如果你在美國,運行程序后輸出: 1,234.56 1.234,56
換句話說,在不同的地方使用不同的習慣表示數字。 NumberFormat.getInstance() 方法返回NumberFormat的一個實例(實際上是NumberFormat具體的一個子類,例如DecimalFormat), 這適合根據本地設置格式化一個數字。你也可以使用非缺省的地區設置,例如德國。然后格式化方法根據特定的地區規則格式化數字。這個程序也可以使用一個簡單 的形式: NumberFormat.getInstance().format(1234.56) 但是保存一個格式然后重用更加有效。國際化是格式化數字時的一個大問題。 另一個是對格式的有效控制,例如指定小數部分的位數,下面是解決這個問題的一個簡單例子:
import java.text.DecimalFormat;
import java.util.Locale;
public class DecimalFormat2 {
public static void main(String args[]) {
// 得到本地的缺省格式
DecimalFormat df1 = new DecimalFormat("####.000");
System.out.println(df1.format(1234.56));
// 得到德國的格式
Locale.setDefault(Locale.GERMAN);
DecimalFormat df2 = new DecimalFormat("####.000");
System.out.println(df2.format(1234.56));
}
}
在這個例子中設置了數字的格式,使用像"####.000"的符號。這個模式意味着在小數點前有四個數字,如果不夠就空着,小數點后有三位數字,不足用0 補齊。
程序的輸出: 1234.560 1234,560
相似的,也可以控制指數形式的格式,例如:
import java.text.DecimalFormat;
public class DecimalFormat3 {
public static void main(String args[]) {
DecimalFormat df = new DecimalFormat("0.000E0000");
System.out.println(df.format(1234.56));
}
}
輸出: 1.235E0003
對於百分數:
import java.text.NumberFormat;
public class DecimalFormat4 {
public static void main(String args[]) {
NumberFormat nf = NumberFormat.getPercentInstance();
System.out.println(nf.format(0.47));
}
}
輸出: 47%
至此,你已經看到了格式化數字的幾個不同的技術。