zoukankan      html  css  js  c++  java
  • 老板说用 float 存储金额的损失从工资里扣!

    作者:何甜甜在吗

    juejin.im/post/5c08db5ff265da611e4d7417

    公司最近在做交易系统,交易系统肯定是要和钱打交道的,和钱有关,自然而然很容易想到用float存储,但是使用float存储金额做的计算是近似计算。

    老板说:「用float做计算造成公司损失的钱都往你工资里扣。

    哼,扣工资就扣工资。但还是得静下心来想想为什么不能用float。


    为什么不能使用float存储金额

    首先看个例子:FloatTest.java

    public class FloatTest {
        public static void main(String[] args) {
            float f1 = 6.6f;
            float f2 = 1.3f;
            System.out.println(f1 + f2);
        }
    }

    结果:7.8999996 和自己口算的值竟然不一样

    计算机只认识0和1,所有类型的计算首先会转化为二进制的计算。


    从计算机二进制角度计算 6.6 + 1.3 的过程

    float底层存储

    计算是由CPU来完成的,CPU表示浮点数由三部分组成 分为三个部分,符号位(sign),指数部分(exponent)和有效部分(fraction, mantissa)。其中float总共占用32位,符号位,指数部分,有效部分各占1位,8位,23位。


    二进制的转化

    对于实数,转化为二进制分为两部分,第一部分整数部分,第二部分是小数部分。整数部分计算二进制大家都很熟悉。

    整数部分的计算:6转化为二进制

    所以6最终的二进制为110

    小数部分的计算

    将小数乘以2,取整数部分作为二进制的值,然后再将小数乘以2,再取整数部分,以此往复循环。

    0.6转化为二进制

    …进入循环,循环体为1001 所以0.6转化为二进制为0.10011001… 6.6转化为二进制为110.10011001…

    规约化

    通过规约化将小数转为规约形式,类似科学计数法,就是保证小数点前面有一个有效数字。在二进制里面,就是保证整数位是一个1。110.10011001规约化为:1.1010011001*2^2。

    指数偏移值

    指数偏移值 = 固定值 + 规约化的指数值 固定值=2^(e-1)-1,其中的e为存储指数部分的比特位数,前面提到的float为8位。所以float中规定化值为127 6.6的二进制值规约化以后为1.1010011001*2^2,指数是2,所以偏移值就是127+2=129,转换为二进制就是10000001。

    拼接6.6

    6.6为正数,符号位为0,指数部分为偏移值的二进制10000001,有效部分为规约形式的小数部分,取小数的前23位即10100110011001100110011,最后拼接到一起即 01000000110100110011001100110011。

    到这里已经大致可以知道float为什么不精确了,首先在存储的时候就会造成精度损失了,在这里小数部分的二进制是循环的,但是仍然只能取前23位。double造成精度损失的原因也是如此。推荐阅读:金融系统中正确的金额计算及存储方式

    求和

    原来如此



    不能使用float那用什么类型存储金额?

    使用int 数据库存储的是金额的分值,显示的时候在转化为元。Java中的运算神器BigDecimal,这篇也推荐看下。

    使用decimal mysql中decimal存储类型的使用

    column_name  decimal(P,D);

    D:代表小数点后的位数 P:有效数字数的精度,小数点也算一位 测试例子 数据表的创建:

    CREATE TABLE `test_decimal` (
      `id` int(11) NOT NULL,
      `amount` decimal(10,2) NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

    对应的DAO层代码:TestDecimalDao.java

    /**
     * @description dao层
     *
     * @author JoyHe
     * @date 2018/11/05
     * @version 1.0
     */  
    @Repository  
    public interface TestDecimalDao {
        @Select("select * from test_decimal where id = #{id}")
        TestDecimal getTestDecimal(int id);
    }

    测试类:TestDecimalDaoTest.java   

    /**
     * @description 测试类
     *
     * @author JoyHe
     * @date 2018/11/05
     * @version 1.0
     */  
    public class TestDecimalDaoTest extends BaseTest {
        @Resource  
        private TestDecimalDao testDecimalDao;
    
        @Test  
        public void test() {
            TestDecimal testDecimal1 =   testDecimalDao.getTestDecimal(1);
            TestDecimal testDecimal2 =   testDecimalDao.getTestDecimal(2);
            BigDecimal result =   testDecimal1.getAmount().add(testDecimal2.getAmount());
            System.out.println(result.floatValue());
        }
    }

    说明:jdbcType为decimal转化为javaType为BigDecimal 测试结果:

    是符合预期的7.9

    使用decimal存储类型的缺点

    1、占用存储空间。

    浮点类型在存储同样范围的值时,通常比decimal使用更少的空间

    2、使用decimal计算效率不高  

    因为使用decimal时间和空间开销较大,选用int作为数据库存储格式比较合适,可以同时避免浮点存储计算的不精确和decimal的缺点。对于存储数值较大或者保留小数较多的数字,数据库存储结构可以选择bigint。

    END

    【推荐阅读

    瞬间几千次的重复提交,我用SpringBoot+Redis扛住了

    Java项目构建:统一结果,统一异常,统一日志

    微信支付软件架构,这也太牛逼了!

    “上中台吗?会送命的那种!”

    SpringBoot+RabbitMQ(保证消息100%投递成功并被消费)

    一个基于 Spring Boot 的项目骨架

    一口气说出 9 种分布式 ID 生成方式,面试官有点懵了

  • 相关阅读:
    Codeforces 845E Fire in the City 线段树
    Codeforces 542D Superhero's Job dp (看题解)
    Codeforces 797F Mice and Holes dp
    Codeforces 408D Parcels dp (看题解)
    Codeforces 464D World of Darkraft
    Codeforces 215E Periodical Numbers 容斥原理
    Codeforces 285E Positions in Permutations dp + 容斥原理
    Codeforces 875E Delivery Club dp
    Codeforces 888F Connecting Vertices 区间dp (看题解)
    Codeforces 946F Fibonacci String Subsequences dp (看题解)
  • 原文地址:https://www.cnblogs.com/Java-Road/p/13067378.html
Copyright © 2011-2022 走看看