进制的转换与计算

我们都知道,计算机内部是通过二进制来进行计算,他无法识别二进制数据以外的任何数据

首先列举五个简单的Demo

一:
   int a = 3;
   int b = 4;
   a / b;
   //0

二:
   double (a / b);
   //0.0

 三:
   int a = 3;
   float b = (float) 4.0;
   double v = a / b;//0.75

 四:
   double a = (1.2 - 0.4) / 0.1;//得到的是7.9999999
   int a = (int) ((1.2 - 0.4) / 0.1);//得到的是7
   double a = 0.8 / 0.1;//得到的是8.0,这里只是似乎不会出问题,注意只是似乎不会出现问题;

 五:
      float a = (float) (1.0 / 3.0);//0.33333334
      double b = (double) a;//0.3333333432674408

很多朋友可能看到Demo4和Demo5的时候就会产生疑惑,那么为什么会造成这种问题呢?

1
2
3
4
5
6
其实计算机在做计算的时候,是把程序都转换成二进制来计算。
我们的浮点数(注意,这里不能叫做小数,因为计算机没有小数这种说法)
是十进制,在转换成二进制的时候,浮点数参与了计算,那么转换的过程就会不可预知。
就是在这个过程中,发生了精度的丢失。
那么为什么有些浮点计算会得到准确的结果呢?
应该是碰巧那个计算的二进制与 十进制之间能够准确转换。

首先我们需要对基础知识进行一波回顾

进制转换问题

二进制的转换要分两种

1 十进制转换成二进制

1.1 十进制转二进制(整数型)

例如 十进制的10 转换成二进制就是1010(不断除2,取余,逆序排列)

1.2 十进制转二进制(小数型)

这里就需要把十进制的整数部分和小数部分进行拆分
例如 10.1转换成二进制,得到的结果就是

10依然是转换成1010
而小数部分的计算,则是不断乘2,取整(只需要整数部分),顺序排列

很明显,这里如果拿值直接进行换算的话,就会造成精度丢失。

2 二进制转十进制

2.1 二进制转换十进制(整数)

例如 二进制的1010转换成十进制就是10(根据位数*2的次数相加)

2.2二进制转换十进制(小数)

例如 二进制的1101.01转换成十进制
整数部分的计算不变
小数部分,不断除以2
image.png

3 二进制的运算方法

3.1 加法

0+0=0
0+1=1+0=1
1+1=0 (遇2进位)
1+1+1=1 (遇2进位,再加1为1)

3.2 减法

0-0=0
1-1=0
1-0=1
0-1=1 (二进制借位为2,再减1,得1)

3.3 乘法(参考十进制的乘法)

0×0=0
0×1=1×0=0
1×1=1

3.4 除法(参考十进制的除法)

知道了这里的原理的话,那么我们上面的demo就很好解释

题目讲解

Demo1

1
2
3
4
5
6
int a=3;==>转换二进制就是011
int b=4;==>转换成二进制就是100
011
+ 100
------------------
111==>转换成十进制就是7

Demo2

1
因为a/b还是为int 7,转换成double 就是7.0

Demo3

1
2
int a=3;==>转换二进制就是011
float b = (float) 4.0==>转换成二进制就是100

Demo4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    double a = (1.2 - 0.4) / 0.1;//得到的是7.9999999
float a=(float)((1.2 - 0.4) / 0.1);//得到8.0
//理论上应该是报错才对。结果确是进了一位。
int a = (int) ((1.2 - 0.4) / 0.1);//得到的是7
double a = 0.8 / 0.1;//得到的是8.0,这里只是似乎不会出问题,注意只是似乎不会出现问题;

1.2转换成二进制就是1.001100110011001100110011001100110011001100110011.....
因为double的取值范围是:2的-1074次方~2的1024次方-1,64位
最后一位肯定是0

0.4转换成二进制是0.011001100110011001100110011001100110011001100......

又因为double是精确到小数点16位
二者想减得到的值就是0.7999999999999999
而0.1的二进制是0.00011001100110011001100110011001100110011001101......
相除,得到的double显示出来的值 就是7.999999999999999
又因为
float的取值范围是2的-149次方~2的128次方-1,二进制位数是32位
double强转成float,造成精度丢失,得到8.0
int的取值范围是-2的31次方~2的31次方-1,二进制位数是32位
double强转成int,造成精度丢失,得到8

所以 float和double只是用于科学计算。
float是精确到小数点8位
如果尾数是0的话,则忽略不计。
double多少个字节。内存里面保存了多少位的字节,最后一位一定是一个0,但是显示的话,只是16位。

Demo5

1
2
3
4
5
6
7
8
9
int a=1;
float b =(float)3.0;
double v=a/b;//0.3333333432674408
float a = (float) (a/b);//0.33333334
那么 这里的计算顺序就是
int a=1先转换成(float)1.0,再由1.0/3.0
1.0转换成二进制是1
3.0转换成二进制是11
1除以11,商为0.09090909....(略),中间的余数虽然为1,但是最后的余数一定是0

所以 在当计算时,如果是float类型,那么就会得到六位数小数,而double类型,则会得到16位小数

最后总结:

1
2
3
4
浮点类型的数,只适合做科学计算。
有的时候,可能我们会踩狗屎运,没有遇到这些问题,但是要注意的,那只是碰巧而已。
如果是在实际项目中遇到一些业务需求,例如,金额的四舍五入,加减乘除,分期付款,优惠券等
那么我们就需要用到BigDecimal类(专门用于商业计算)来进行计算

注意事项:

1
2
3
4
5
6
举例:Math.round(4.015*100)/100.0;
//得到4.01,因为math只能保留两位小数,不能四舍五入
DecimalFormat也不能四舍五入。
new DecimalFormat("0.00").format(4.015)
//得到的也只是4.01
商业计算等精确计算中,我们只能用BigDecimal来进行计算。

当然在使用的时候,注意封装。没必要每次都去new。

BigDecimal简易封装代码

import java.math.BigDecimal;

/**
* 由于Java的简单类型不能够精确的对浮点数进行运算,这个工    具类提供精
* 确的浮点数运算,包括加减乘除和四舍五入。
*/

public class Arith{
//默认除法运算精度
private static final int DEF_DIV_SCALE = 10;
//这个类不能实例化
private Arith(){
}

/**
 * 提供精确的加法运算。
 * @param v1 被加数
 * @param v2 加数
 * 两个参数的和
 */
public static double add(double v1,double v2){
    BigDecimal b1 = new BigDecimal(Double.toString(v1));
    BigDecimal b2 = new BigDecimal(Double.toString(v2));
    return b1.add(b2).doubleValue();
}
/**
 * 提供精确的减法运算。
 * @param v1 被减数
 * @param v2 减数
 *  两个参数的差
 */
public static double sub(double v1,double v2){
    BigDecimal b1 = new BigDecimal(Double.toString(v1));
    BigDecimal b2 = new BigDecimal(Double.toString(v2));
    return b1.subtract(b2).doubleValue();
}
/**
 * 提供精确的乘法运算。
 * @param v1 被乘数
 * @param v2 乘数
 * 两个参数的积
 */
public static double mul(double v1,double v2){
    BigDecimal b1 = new BigDecimal(Double.toString(v1));
    BigDecimal b2 = new BigDecimal(Double.toString(v2));
    return b1.multiply(b2).doubleValue();
}

/**
 * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
 * 小数点以后10位,以后的数字四舍五入。
 * @param v1 被除数
 * @param v2 除数
 *  两个参数的商
 */
public static double div(double v1,double v2){
    return div(v1,v2,DEF_DIV_SCALE);
}

/**
 * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
 * 定精度,以后的数字四舍五入。
 * @param v1 被除数
 * @param v2 除数
 * @param scale 表示表示需要精确到小数点以后几位。
 * 两个参数的商
 */
public static double div(double v1,double v2,int scale){
    if(scale<0){
        throw new IllegalArgumentException(
            "The scale must be a positive integer or zero");
    }
    BigDecimal b1 = new BigDecimal(Double.toString(v1));
    BigDecimal b2 = new BigDecimal(Double.toString(v2));
    return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
}

/**
 * 提供精确的小数位四舍五入处理。
 * @param v 需要四舍五入的数字
 * @param scale 小数点后保留几位
 * 四舍五入后的结果
 */
public static double round(double v,int scale){

    if(scale<0){
        throw new IllegalArgumentException(
            "The scale must be a positive integer or zero");
    }
    BigDecimal b = new BigDecimal(Double.toString(v));
    BigDecimal one = new BigDecimal("1");
    return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
  }
};
举一个我们项目遇到的真实问题
一件商品的价格是10.5449万元RMB
业务上如果要保留两位小数,四舍五入,显示就是10.54万元。
就会造成一个问题,用户在线下进行实际购买时,会多出49元,那用户肯定是不乐意的。
如果UI上显示"约10.54",那也不太好,用户心理不平衡。
所以这里的最佳业务就是 小数点第三位大于0,则进一位。
利用BigDecimal也可以完成。
igding wechat
2018 依计行事 持续精进
坚持原创技术分享,您的支持将鼓励我继续创作!