如果要求精确的答案,请避免使用float和double

1. 概述

float和double类型的主要设计目的是为了科学计算和工程计算。它们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们没有提供完全精确的结果,所以不应该被用于要求精确结果的场合。float和double类型对于货币计算尤为不合适,因为要让一个float或者double精确地表示0.1(或者10的任何负数次方值)是不可能的,比如System.out.println(2.0-1.1)将会打印0.899999999999999,而不是你所希望的0.9,这种舍入错误产生的原因是浮点数实际上是用二进制系统实现的,而分数1/10在二进制系统中没有精确的表示,其道理就如同在十进制系统中无法精确表示1/3一样;再比如0.5在二进制系统中有精确表示,而0.55在二进制系统中没有精确表示。

许多数都是无法在有限的n内完全精确的表示出来的,我们只能利用更大的n值来更精确的表示这个数,但更大的n也只是更加逼近精确值而无法得到精确值。例如

  1. public static void main(String[] args) {
  2.     System.out.println("100.5 - 100                 = " + (100.5 - 100));
  3.     System.out.println("100.5F - 100F               = " + (100.5F - 100F));
  4.     System.out.println("100.55 - 100.00             = " + (100.55 - 100.00));
  5.     System.out.println("100.55F - 100.00F           = " + (100.55F - 100.00F));
  6.     System.out.println("new Double(100.55 - 100.00) = " + new Double(100.55 - 100.00));
  7.     System.out.println("new Float(100.55 - 100.00)  = " + new Float(100.55 - 100.00));
  8.     System.out.println("(double)(100.55 - 100.00)   = " + (double)(100.55 - 100.00));
  9.     System.out.println("(float)(100.55 - 100.00)    = " + (float)(100.55 - 100.00));
  10.     
  11.     System.out.println("2.0 - 1.1               = " + (2.0 - 1.1));
  12.     System.out.println("2.0F - 1.1F             = " + (2.0F - 1.1F));
  13.     System.out.println("new Double(2.0 - 1.1)   = " + new Double(2.0 - 1.1));
  14.     System.out.println("new Float(2.0 - 1.1)    = " + new Float(2.0 - 1.1));
  15.     System.out.println("(double)(2.0 - 1.1)     = " + (double)(2.0 - 1.1));
  16.     System.out.println("(float)(2.0 - 1.1)      = " + (float)(2.0 - 1.1));
  17.     System.out.println("1.0 - 0.9               = " + (1.0 - 0.9));
  18.     System.out.println("1.0F - 0.9F             = " + (1.0F - 0.9F));
  19.     System.out.println("new Double(1.0 - 0.9)   = " + new Double(1.0 - 0.9));
  20.     System.out.println("new Float(1.0 - 0.9)    = " + new Float(1.0 - 0.9));
  21.     System.out.println("(double)(1.0 - 0.9)     = " + (double)(1.0 - 0.9));
  22.     System.out.println("(float)(1.0 - 0.9)      = " + (float)(1.0 - 0.9));
  23.     /*
  24.      * 输出结果:
  25.      * 100.5 - 100                 = 0.5
  26.      * 100.5F - 100F               = 0.5
  27.      * 100.55 - 100.00             = 0.5499999999999972
  28.      * 100.55F - 100.00F           = 0.55000305
  29.      * new Double(100.55 - 100.00) = 0.5499999999999972
  30.      * new Float(100.55 - 100.00)  = 0.55 (将一个Double转换为Float自然会有精度丢失, 所以舍入为0.55)
  31.      * (double)(100.55 - 100.00)   = 0.5499999999999972
  32.      * (float)(100.55 - 100.00)    = 0.55 (将一个double转换为float自然会有精度丢失, 所以舍入为0.55)
  33.      * 
  34.      * 2.0 - 1.1                = 0.8999999999999999
  35.      * 2.0F - 1.1F              = 0.9 (这里用Float可以得到正确值, 我不知道原因, 你去问cpu吧. 只要记住这是一个巧合就可以了)
  36.      * new Double(2.0 - 1.1)    = 0.8999999999999999
  37.      * new Float(2.0 - 1.1)     = 0.9 (将一个Double转换为Float自然会有精度丢失, 所以舍入为0.9)
  38.      * (double)(2.0 - 1.1)      = 0.8999999999999999
  39.      * (float)(2.0 - 1.1)       = 0.9 (将一个double转换为float自然会有精度丢失, 所以舍入为0.9)
  40.      * 
  41.      * 1.0 - 0.9                = 0.09999999999999998
  42.      * 1.0F - 0.9F              = 0.100000024
  43.      * new Double(1.0 - 0.9)    = 0.09999999999999998
  44.      * new Float(1.0 - 0.9)     = 0.1 (将一个Double转换为Float自然会有精度丢失, 所以舍入为0.1)
  45.      * (double)(1.0 - 0.9)      = 0.09999999999999998
  46.      * (float)(1.0 - 0.9)       = 0.1 (将一个double转换为float自然会有精度丢失, 所以舍入为0.1)
  47.      * */
  48. }

2. 为什么浮点数会丢失精度(浮点数没办法用十进制来精确表示)

(1)十进制与二进制转换

要归咎于CPU表示浮点数的方法:2.4的二进制表示并非就是精确的2.4,而是最为接近2.3999999999999999;原因在于浮点数由两部分组成:指数和尾数,浮点数的值实际上是由cpu的某个数学公式计算得到的,所以您所遇到的精度损失会在任何操作系统和编程环境中遇到。

(2)类型失配

您可能混合了浮点数和双精度浮点数类型。请确定您在进行数学运算的时候所有的数据类型全部相同。注意:float类型的变量只有7位的精度,而double类型的变量有15位的精度。

3. 解决方法

(1)java.lang.Math中的public static int round(float a)和public static long round(double a)不能解决问题; java.lang.Object --> java.text.Format --> java.text.NumberFormat --> java.text.DecimalFormat 也不能解决问题
(2)使用BigDecimal、int或long(将小数转换为整数再进行计算,例如:将以美元为单位的货币计算改为以美分为单位的货币计算)。java.lang.Object --> java.lang.Number -- Direct Known Subclasses: BigDecimal, Byte, Double, Float, Integer, Long, Short

BigDecimal常用构造方法:

public BigDecimal(double val) --> Translates a double into a BigDecimal which is the exact decimal representation of the double's binary floating-point value.

public BigDecimal(String val) --> Translates the string representation of a BigDecimal into a BigDecimal

通常情况下,上面的那一个使用起来要方便一些。我们可能想都不想就用上了,会有什么问题呢?等到出了问题的时候,才发现上面哪个够造方法的详细说明中有这么一段。

The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.

The String constructor, on the other hand, is perfectly predictable: writing new BigDecimal("0.1") creates a BigDecimal which is exactly equal to 0.1, as one would expect. Therefore, it is generally recommended that the String constructor be used in preference to this one.

When a double must be used as a source for a BigDecimal, note that this constructor provides an exact conversion; it does not give the same result as converting the double to a String using the Double.toString(double) method and then using the BigDecimal(String) constructor. To get that result, use the static valueOf(double) method.

原来我们如果需要精确计算,非要用String来够造BigDecimal不可!在《Effective Java》一书中的例子也是用String来够造BigDecimal的,但是书上却没有强调这一点,这也许是一个小小的失误吧。

使用BigDecimal加法运算步骤:(1)将两个浮点数转为String,然后构造成BigDecimal;(2)在其中一个上调用add()方法,传入另一个作为参数,然后把运算的结果(BigDecimal)再转换为浮点数。这样比较繁琐,我们可以自己写一个工具类Arith来简化操作:

  1. package edu.hust.test;
  2. import java.math.BigDecimal;
  3. /**
  4.  * 由于Java的简单类型不能够精确的对浮点数进行运算,这个工具类提供精确的浮点数运算,包括加减乘除和四舍五入。
  5.  * 所以提供以下静态方法:
  6.  * public static double add(double v1,double v2)
  7.  * public static double sub(double v1,double v2)
  8.  * public static double mul(double v1,double v2)
  9.  * public static double div(double v1,double v2)
  10.  * public static double div(double v1,double v2,int scale)
  11.  * public static double round(double v,int scale)
  12.  * 
  13.  */
  14. public class Arith {
  15.     //默认除法运算精度
  16.     private static final int DEF_DIV_SCALE = 10;
  17.     //这个类不能实例化
  18.     private Arith(){}
  19.     /**
  20.      * 提供精确的加法运算。
  21.      * @param v1 被加数
  22.      * @param v2 加数
  23.      * @return 两个参数的和
  24.      */
  25.     public static double add(double v1,double v2){
  26.         BigDecimal b1 = new BigDecimal(Double.toString(v1));
  27.         BigDecimal b2 = new BigDecimal(Double.toString(v2));
  28.         return b1.add(b2).doubleValue();
  29.     }
  30.     /**
  31.      * 提供精确的减法运算。
  32.      * @param v1 被减数
  33.      * @param v2 减数
  34.      * @return 两个参数的差
  35.      */
  36.     public static double sub(double v1,double v2){
  37.         BigDecimal b1 = new BigDecimal(Double.toString(v1));
  38.         BigDecimal b2 = new BigDecimal(Double.toString(v2));
  39.         return b1.subtract(b2).doubleValue();
  40.     } 
  41.     /**
  42.      * 提供精确的乘法运算。
  43.      * @param v1 被乘数
  44.      * @param v2 乘数
  45.      * @return 两个参数的积
  46.      */
  47.     public static double mul(double v1,double v2){
  48.         BigDecimal b1 = new BigDecimal(Double.toString(v1));
  49.         BigDecimal b2 = new BigDecimal(Double.toString(v2));
  50.         return b1.multiply(b2).doubleValue();
  51.     }
  52.     /**
  53.      * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到小数点以后10位,以后的数字四舍五入。
  54.      * @param v1 被除数
  55.      * @param v2 除数
  56.      * @return 两个参数的商
  57.      */
  58.     public static double div(double v1,double v2){
  59.         return div(v1,v2,DEF_DIV_SCALE);
  60.     }
  61.     /**
  62.      * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指定精度,以后的数字四舍五入。
  63.      * @param v1 被除数
  64.      * @param v2 除数
  65.      * @param scale 表示表示需要精确到小数点以后几位。
  66.      * @return 两个参数的商
  67.      */
  68.     public static double div(double v1,double v2,int scale){
  69.         if(scale<0){
  70.             throw new IllegalArgumentException("The scale must be a positive integer or zero");
  71.         }
  72.         BigDecimal b1 = new BigDecimal(Double.toString(v1));
  73.         BigDecimal b2 = new BigDecimal(Double.toString(v2));
  74.         return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
  75.     }
  76.     /**
  77.      * 提供精确的小数位四舍五入处理。
  78.      * @param v 需要四舍五入的数字
  79.      * @param scale 小数点后保留几位
  80.      * @return 四舍五入后的结果
  81.      */
  82.     public static double round(double v,int scale){
  83.         if(scale<0){
  84.             throw new IllegalArgumentException("The scale must be a positive integer or zero");
  85.         }
  86.         BigDecimal b = new BigDecimal(Double.toString(v));
  87.         BigDecimal one = new BigDecimal("1");
  88.         return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
  89.     }
  90. }

使用BigDecimal缺点:要创建对象并占用内存;其floatValue()方法有时会精度丢失而造成计算错误(所以一般小数计算尽量使用double)。

最后补充一点:也正因为此,浮点数不能用于循环判断,否则容易造成死循环。

<think>好的,我现在需要处理用户的问题:“会计核算 Java 结合的面试题答案”。用户之前问过后端系统安全性,现在转向了会计Java结合的面试题,可能是在准备相关职位的面试,比如财务软件开发或者企业ERP系统的开发岗位。 首先,我需要理解会计核算Java结合的具体应用场景。会计软件通常涉及复杂的业务逻辑、数据处理、合规性要求,以及高精度的计算。Java作为后端语言,常用于这类系统,所以面试题可能会围绕设计模式、数据处理、框架使用、事务管理、安全性等方面展开。 接下来,我需要确定用户的需求。他们可能希望了解常见的面试问题及其答案,以便准备面试。需要涵盖基础知识、设计模式、框架应用、数据库处理、安全性、测试以及实际场景题。 考虑到用户之前问的是后端安全性,可能他们有一定技术背景,但需要更具体的领域知识。因此,答案不仅要展示Java技术点,还要结合会计领域的特殊性,比如财务计算的准确性、审计追踪、合规性等。 需要分门别类整理问题,比如基础知识、设计模式、框架相关、数据库、安全、测试实际场景。每个问题需要简明扼要,答案也要准确,突出Java在会计系统中的实际应用。 同时,要注意可能用户没直接提到的点,比如事务管理在会计系统中的重要性,或者如何处理高精度计算避免浮点数误差,这些都是会计软件中常见的问题,Java中的BigDecimal类处理这类问题。 另外,面试题可能涉及设计模式,比如策略模式处理不同的会计策略,工厂模式创建不同的财务凭证,这些都需要结合会计业务来解释。 还需要考虑合规性安全性,比如数据加密、审计日志,这些在会计系统中至关重要,结合Java的实现方式,如Spring Security、AOP记录日志等。 测试方面,会计软件对准确性要求极高,单元测试、集成测试必须严谨,MockitoJUnit的使用是重点,同时数据验证边界条件测试也是常见考点。 最后,实际场景问题需要举例说明如何设计模块或解决典型问题,比如汇率计算、多币种处理,展示解决问题的思路Java技术的应用。 要确保回答结构清晰,覆盖全面,同时每个问题点都结合实际例子,让用户能够理解如何将Java技术应用于会计系统开发中,满足面试准备的需求。</think>以下是一些结合 **会计核算** 与 **Java** 的常见面试题及其参考答案,涵盖技术实现、设计模式、业务逻辑等方向: --- ### **1. 基础问题** #### **Q1: 在会计系统中,金额计算为什么不能用 `double` 或 `float`?Java 中如何解决?** - **答案**: `double` `float` 使用二进制浮点数表示,无法精确表示十进制小数(如 `0.1`),会导致累计误差,而会计系统对金额精度要求极高。 **解决方案**: 使用 `BigDecimal` 类进行金额计算,并指定精确的舍入模式(如 `ROUND_HALF_UP`)精度位数。 #### **Q2: 如何设计一个会计凭证(Accounting Voucher)的 Java 类?** - **答案**: ```java public class AccountingVoucher { private String voucherId; // 凭证编号 private LocalDate date; // 凭证日期 private List<Entry> entries; // 借贷分录 private String description; // 描述 // 必须满足借贷平衡:总借方金额 = 总贷方金额 } public class Entry { private String accountCode; // 科目编码 private BigDecimal debit; // 借方金额 private BigDecimal credit; // 贷方金额 } ``` -- ### **2. 设计模式与业务逻辑** #### **Q3: 会计系统需要支持多种会计准则(如 IFRS、GAAP),如何用 Java 实现灵活切换?** - **答案**: 使用 **策略模式(Strategy Pattern)**: ```java public interface AccountingStandard { BigDecimal calculateDepreciation(Asset asset); } public class IFRS implements AccountingStandard { /* 实现IFRS规则 */ } public class GAAP implements AccountingStandard { /* 实现GAAP规则 */ } // 上下文类动态切换策略 public class AccountingService { private AccountingStandard strategy; public void setStrategy(AccountingStandard strategy) { ... } } ``` #### **Q4: 如何保证会计凭证的借贷平衡(Debit = Credit)?** - **答案**: - 在 `AccountingVoucher` 类的 `addEntry` 方法中,每次添加分录时校验总借方总贷方是否相等。 - 使用工厂模式(Factory Pattern)确保创建凭证时必须满足平衡条件: ```java public class VoucherFactory { public static AccountingVoucher createVoucher(List<Entry> entries) { if (!isBalanced(entries)) throw new IllegalArgumentException("借贷不平!"); return new AccountingVoucher(entries); } } ``` --- ### **3. 框架与数据库** #### **Q5: 使用 Spring Boot 时,如何设计会计科目的增删改查(CRUD)接口?** - **答案**: - 定义 `Account` 实体类,标注 JPA 注解(如 `@Entity`)。 - 创建 `AccountRepository` 接口继承 `JpaRepository`。 - 通过 `AccountController` 暴露 RESTful API,并使用 DTO 隔离实体与接口。 **关键点**: - 校验科目编码的唯一性。 - 处理父子科目层级关系(如树形结构)。 #### **Q6: 在数据库中存储金额时,推荐什么字段类型?Java 如何映射?** - **答案**: - **数据库**:使用 `DECIMAL(precision, scale)`(如 `DECIMAL(15,2)`),确保精度。 - **Java 映射**:通过 Hibernate 注解 `@Column(precision=15, scale=2)`,字段类型为 `BigDecimal`。 --- ### **4. 安全与审计** #### **Q7: 如何记录会计凭证的修改审计日志?** - **答案**: - 使用 **AOP(面向切面编程)** 拦截 `updateVoucher` 方法。 - 记录操作人、时间、旧值、新值到审计表。 - 示例代码: ```java @Aspect public class AuditAspect { @AfterReturning(pointcut = "execution(* updateVoucher(..))", returning = "voucher") public void logAudit(AccountingVoucher voucher) { // 将审计信息写入数据库或日志文件 } } ``` #### **Q8: 如何防止会计数据被篡改?** - **答案**: - 数据库层面:使用事务(`@Transactional`)保证原子性。 - 业务层面:对敏感操作(如金额修改)添加审批流程。 - 技术层面:通过哈希签名(如 SHA-256)验证数据完整性。 --- ### **5. 测试与验证** #### **Q9: 如何对会计系统的计算逻辑进行单元测试?** - **答案**: 使用 JUnit AssertJ 验证计算结果的精确性: ```java @Test void testInterestCalculation() { BigDecimal principal = new BigDecimal("1000.00"); BigDecimal rate = new BigDecimal("0.05"); BigDecimal expectedInterest = new BigDecimal("50.00"); BigDecimal actualInterest = accountingService.calculateInterest(principal, rate); assertThat(actualInterest).isEqualByComparingTo(expectedInterest); } ``` #### **Q10: 如何处理多币种会计场景?** - **答案**: - 定义 `Currency` 类,包含币种代码(如 USD、CNY)汇率。 - 在分录中记录原币金额本币金额。 - 使用第三方库(如 Joda-Money)简化货币计算。 --- ### **6. 实际场景题** #### **Q11: 设计一个“应收账款”模块,如何用 Java 实现账龄分析(Aging Analysis)?** - **答案**: - 数据库表设计:记录每笔应收账款的日期、金额、客户等信息。 - Java 逻辑:根据当前日期计算账龄区间(如 0-30天、31-60天),分组统计金额。 - 示例代码: ```java public Map<String, BigDecimal> calculateAging(List<Receivable> receivables) { Map<String, BigDecimal> agingMap = new HashMap<>(); LocalDate today = LocalDate.now(); receivables.forEach(r -> { long days = ChronoUnit.DAYS.between(r.getDueDate(), today); String range = getAgingRange(days); // 根据天数返回区间 agingMap.merge(range, r.getAmount(), BigDecimal::add); }); return agingMap; } ``` --- ### **总结** - **核心考察点**: 1. **业务理解**:会计规则(如借贷平衡、科目体系)。 2. **技术能力**:Java 精度处理、设计模式、框架使用。 3. **系统设计**:扩展性、安全性、审计追踪。 - **加分项**:熟悉财务开源库(如 Apache OFBiz)、对会计准则(如权责发生制)的理解。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值