1. 程式人生 > >Java中使用long型別實現精確的四則運算

Java中使用long型別實現精確的四則運算

引子

Effective Java 2nd Edition 第48條建議:如果需要精確的答案,請避免使用float和doble。float和double型別主要是為了科學計算和工程計算而設計的。他們執行二進位制制浮點運算(binary floating-point arithmetic),這是為了在廣泛的數值範圍上提供較為精確的快速近似計算而精心設計的。然而,他們並沒有提供完全精確的結果,所以不應該被用於需要精確結果的場合。float和double型別尤其不適合用於貨幣計算,因為要讓float或double精確的表示0.1(或者10的任何其他負數次方值)是不可能的。解決這個問題的正確辦法是使用BigDecimal、int或long進行貨幣計算。

1. float和double為何是近似計算

float和double無法精確計算是由他們的設計決定的。詳細資訊請參考下面這兩篇篇博文:

對浮點數的IEEE 754表示法不熟悉的同學,可以參考下面的博文:
細說浮點數
IEEE 754 浮點數的表示精度探討

2. 使用long實現精確計算

本例子實現了使用long型別來完成浮點數的精確的加減乘,由於除法本身很多是不能整除的,所以對除法的精確計算沒有想到好的方法。
由於long的表示範圍為 -2^63 —– 2^63 - 1,即十進位制的: -9223372036854775808 —–9223372036854775807
所以對於使用long完成精確計算必須保證計算結果及其中間結果不超過long的範圍。對於比較大的數,使用BigDecimal靠譜。

上程式碼:

package effectivejava.chapter8;

import java.math.BigDecimal;

/**
 * 
 * 本例子實現了使用long型別來完成浮點數的精確的加減乘,由於除法本身很多是不能整除的,所以對除法的精確計算沒有想到好的方法。
 * 由於long的表示範圍為-2^63 ----- 2^63 - 1,即十進位制的: -9223372036854775808-----9223372036854775807
 * 所以對於使用long完成精確計算必須保證計算結果及其中間結果不超過long的範圍。對於比較大的數,使用BigDecimal靠譜。
 */
public class ExactComputationByLong { /** * 1. 獲取兩個小數的右移小數點之後的long值及移動位數; 2. 統一移動位數為兩個之中大的那個; 3. 相加; 4. 將小數點左移到相加的數中。 */ public static String add(String strA, String strB) { ExactLong exactA = new ExactLong(strA); ExactLong exactB = new ExactLong(strB); int maxScale = getMaxScale(exactA, exactB); changeToMaxSacle(exactA, maxScale); changeToMaxSacle(exactB, maxScale); long addNum = exactA.getData() + exactB.getData(); String resultInit = getDataFromLong(addNum, maxScale); return getResultRemoveLastZeros(resultInit); } /** * 過程與加法相同。 */ public static String subtract(String strA, String strB) { ExactLong exactA = new ExactLong(strA); ExactLong exactB = new ExactLong(strB); int maxScale = getMaxScale(exactA, exactB); changeToMaxSacle(exactA, maxScale); changeToMaxSacle(exactB, maxScale); long subNum = exactA.getData() - exactB.getData(); String resultInit = getDataFromLong(subNum, maxScale); return getResultRemoveLastZeros(resultInit); } /** * 過程與加法相似,唯一不同是移動位數為maxScale * 2。 */ public static String multiply(String strA, String strB) { ExactLong exactA = new ExactLong(strA); ExactLong exactB = new ExactLong(strB); if (exactA.getData() == 0 || exactB.getData() == 0) { return "0"; } int maxScale = getMaxScale(exactA, exactB); changeToMaxSacle(exactA, maxScale); changeToMaxSacle(exactB, maxScale); long subNum = exactA.getData() * exactB.getData(); String resultInit = getDataFromLong(subNum, maxScale * 2); return getResultRemoveLastZeros(resultInit); } /** * 對於除法,我沒有想到好的方法來精確計算。而且除法本身有很多不是整除的,所以除法一般都要確定一個精度,然後再計算。水平有限,這裡就用BigDecimal的一個實現代替吧☺ */ public static String divide(String strA, String strB) { return new BigDecimal(strA).divide(new BigDecimal(strB), BigDecimal.ROUND_DOWN).toPlainString(); } private static void changeToMaxSacle(ExactLong exactData, int maxScale) { if (maxScale != exactData.getScale()) { exactData.setData((long) (exactData.getData() * Math.pow(10, maxScale - exactData.getScale()))); exactData.setScale(maxScale); } } private static int getMaxScale(ExactLong exactA, ExactLong exactB) { int maxScale = exactA.getScale() > exactB.getScale()?exactA.getScale():exactB.getScale(); return maxScale; } /** * 將dataL小數點左移scale位 */ private static String getDataFromLong(long dataL, int scale) { String dataStr = dataL + ""; if (scale < 0) { throw new IllegalArgumentException("The degree must greater than 0!!"); } if (scale == 0) { return dataStr; } if (dataStr.length() > scale) { return dataStr.substring(0, dataStr.length() - scale) + "." + dataStr.substring(dataStr.length() - scale); } else { StringBuilder dataSB = new StringBuilder("0."); for (int i = 0;i < scale - dataStr.length();i++) { dataSB.append("0"); } dataSB.append(dataStr); return dataSB.toString(); } } /** * 去掉小數點之後多餘的0 */ private static String getResultRemoveLastZeros(String resultInit) { int index = resultInit.length(); // 去處小數位最後可能的多個0 for (int i = resultInit.length() - 1;i >= 0;i--) { if (resultInit.charAt(i) != '0') { index = i + 1; break; } } return resultInit.substring(0, index); } public static void main(String[] args) { String strA = "0.001234560"; String strB = "10.2345"; String strAdd = add(strA, strB); System.out.println("Add:\n" + strA + "\n" + strB + "\n" + strAdd); String strSub = subtract(strA, strB); System.out.println("Sub:\n" + strA + "\n" + strB + "\n" + strSub); String strMult = multiply(strA, strB); System.out.println("Mult:\n" + strA + "\n" + strB + "\n" + strMult); String sttDiv = divide(strA, strB); System.out.println("Div:\n" + strA + "\n" + strB + "\n" + sttDiv); } } /** * 將一個字串表示的浮點數用long表示,scale表示小數點右移的位數。 */ class ExactLong { private long data; private int scale; public ExactLong(String dataStr) { if (dataStr.indexOf(".") == -1) {// 如123456 data = Long.parseLong(dataStr); scale = 0; return; } if (dataStr.indexOf(".") != dataStr.lastIndexOf(".")) {// 如123.456.7 throw new IllegalArgumentException(dataStr + " can not cast to long!"); } else {// 如12345.678 scale = dataStr.length() - dataStr.indexOf(".") - 1; long beforePoint = Long.parseLong(dataStr.substring(0, dataStr.indexOf(".")));// 小數點之前的數 long afterPoint = Long.parseLong(dataStr.substring(dataStr.indexOf(".") + 1, dataStr.length()));// 小數點之後的數 data = (long) (beforePoint * Math.pow(10, scale) + afterPoint); } } public long getData() { return data; } public void setData(long data) { this.data = data; } public int getScale() { return scale; } public void setScale(int scale) { this.scale = scale; } }