Java工程师带的新人,脸皮厚啊,你竟然使用=比较浮点数?

我在九朝古都洛阳的一家公司工作敲代码的同时带两个新人,其中一个就是
经常犯错,被我写到文章里的小王。

不过,他的心态一直很不错,他不觉得被我批评有什么丢人的,反而每次读完我的文章后觉得自己又升级了。因此,我觉得小王大有前途,再这么干个一两年,老板要是觉得我的性价比低了,没准就把我辞退留下小王了。一想到这,我竟然枯燥一笑了。

image

那天,我闲来无聊,就准备偷偷 review 一下小王的代码,看能不能鸡蛋里挑点骨头,没想到,还真的被我挑到了。

double d1 = .0;
for (int i = 1; i <= 11; i++) {
    d1 += .1;
}

double d2 = .1 * 11;

System.out.println(d1 == d2)

他这段代码蛮炫技的,其实,尤其是 .0、.1 的写法,我平常都老实巴交的写成0.0、0.1,从来没想着要把小数点前面的 0 省略。 按照正常的逻辑来看,d1 在经过 11 次循环加 .1 后的结果应该是 1.1,d2 通过 .1 乘以 11 后的结果也应该是 1.1,最后打印出来的结果应该是 true,对吧?小王应该也是这么期待的,我觉得。 但我当时硬是没忍住我的暴脾气,破口大骂:“我擦,小王,你竟然敢用 == 比较浮点数,这不是找刺激吗?”

image

如果有读者也觉得输出结果是 true 的话,可以把上面这段代码在本地运行一下,输出的结果一定会出乎你的意料。
false

对,false,我没骗你。如何正确地比较浮点数(单精度的 float 和双精度的 double),不单单是 Java 特定的问题,很多编程语言的初学者也会遇到同样的问题。在计算机的内存中,存储浮点数时使用的是 IEEE 754 标准,就会有精度的问题,至于实际上的存储转换过程,这篇文章不做过多的探讨。
(主要是我太菜了,探讨的过程很枯燥,一点都不有趣,严谨地理论推导就交给那些真正的技术大佬们吧,我就不献丑了。)

[图片上传失败...(image-4336b5-1595230479715)]

同学们只需要知道,存储和转换的过程中浮点数容易引起一些较小的舍入误差,正是这个原因,导致在比较浮点数的时候,不能使用“==”操作符——要求严格意义上的完全相等。

这套视频教程 是我从去年年底到今年年初,根据市场技术栈需求录制的,非常系统完整,相信可以快速带你走进Java编程的世界。

学习资料和
实战项目练习:https://pan.baidu.com/s/1y4mc-gJYKrtQOXOoDBuymg

需要的话可以在评论回复 :需要

再来看一下小王的代码,我们把 d1 和 d2 打印出来,看看它们的值到底是什么。

d1:1.0999999999999999
d2:1.1
复制代码
 怪不得“==”的时候输出 false,原来 d1 的值有一些误差,并不是我们预期的 1.1。既然“==”不能用来比较浮点数,那么小王就得挨骂,这逻辑讲得通吧?
 那这个问题该怎么解决呢?
 对于浮点数的存储和转化问题,我表示无能为力,这是实在话,计算机的底层问题,驾驭不了。但是,可以通过一些折中的办法,比如说允许两个值之间有点误差(指定一个阈值),小到 0.000000…..1,具体多少个  0 懒得数了,反正特别小,那么我们就认为两个浮点数是相等的。
 第一种方案就是使用 Math.abs() 方法来计算两个浮点数之间的差异,如果这个差异在阈值范围之内,我们就认为两个浮点数是相等。
 final double THRESHOLD = .0001;

double d1 = .0;
for (int i = 1; i <= 11; i++) {
    d1 += .1;
}

double d2 = .1 * 11;

if(Math.abs(d1-d2) < THRESHOLD) {
    System.out.println("d1 和 d2 相等");
} else {
    System.out.println("d1 和 d2 不等");
}
复制代码
 Math.abs() 方法用来返回 double 的绝对值,如果 double 小于 0,则返回 double 的正值,否则返回 double。也就是说,abs() 后的结果绝对大于 0,如果结果小于阈值(THRESHOLD),我们就认为 d1 和 d2 相等。
 第二种解决方案就是使用 BigDecimal 类,可以指定要舍入的模式和精度,这样就可以解决舍入的误差。
 可以使用 BigDecimal 类的 compareTo() 方法对两个数进行比较,该方法将会忽略小数点后的位数,怎么理解这句话呢?比如说 2.0 和 2.00 的位数不同,但它俩的值是相等的。
 如果 a 小于 b,则该方法返回 -1,如果相等,则返回 0,否则返回 -1。
 注意,千万不要使用 equals() 方法对两个 BigDecimal 对象进行比较,这是因为 equals() 方法会考虑位数,如果位数不同,则会返回 false,尽管数学值是相等的。
 BigDecimal a = new BigDecimal("2.00");
BigDecimal b = new BigDecimal("2.0");

System.out.println(a.equals(b));
System.out.println(a.compareTo(b) == 0);
复制代码
 a.equals(b) 的结果就为 false,因为 2.00 和 2.0 小数点后的位数不同,但 a.compareTo(b) == 0 的结果就为 true,因为 2.00 和 2.0 在数学层面的值的确是相等的。
 compareTo() 方法比较的过程非常严谨,感兴趣的同学可以查看一下源码,其中位数不同的时候,会执行以下方法进行比较。
 private int compareMagnitude(BigDecimal val) {
 // Match scales, avoid unnecessary inflation
 long ys = val.intCompact;
 long xs = this.intCompact;
 if (xs == 0)
 return (ys == 0) ? 0 : -1;
 if (ys == 0)
 return 1;

 long sdiff = (long)this.scale - val.scale;
 if (sdiff != 0) {
 // Avoid matching scales if the (adjusted) exponents differ
 long xae = (long)this.precision() - this.scale;   // [-1]
 long yae = (long)val.precision() - val.scale;     // [-1]
 if (xae < yae)
 return -1;
 if (xae > yae)
 return 1;
 if (sdiff < 0) {
 // The cases sdiff <= Integer.MIN_VALUE intentionally fall through.
 if ( sdiff > Integer.MIN_VALUE &&
                    (xs == INFLATED ||
                            (xs = longMultiplyPowerTen(xs, (int)-sdiff)) == INFLATED) &&
                    ys == INFLATED) {
                BigInteger rb = bigMultiplyPowerTen((int)-sdiff);
 return rb.compareMagnitude(val.intVal);
            }
        } else { // sdiff > 0
 // The cases sdiff > Integer.MAX_VALUE intentionally fall through.
 if ( sdiff <= Integer.MAX_VALUE &&
                    (ys == INFLATED ||
                            (ys = longMultiplyPowerTen(ys, (int)sdiff)) == INFLATED) &&
                    xs == INFLATED) {
                BigInteger rb = val.bigMultiplyPowerTen((int)sdiff);
 return this.intVal.compareMagnitude(rb);
            }
        }
    }
 if (xs != INFLATED)
 return (ys != INFLATED) ? longCompareMagnitude(xs, ys) : -1;
 else if (ys != INFLATED)
 return 1;
 else
 return this.intVal.compareMagnitude(val.intVal);
}

好了,现在让我们使用 BigDecimal 来解决精度问题吧。

BigDecimal d1 = new BigDecimal("0.0");
BigDecimal pointOne = new BigDecimal("0.1");
for (int i = 1; i <= 11; i++) {
d1 = d1.add(pointOne);
}

BigDecimal d2 = new BigDecimal("0.1");
BigDecimal eleven = new BigDecimal("11");
d2 = d2.multiply(eleven);

System.out.println("d1 = " + d1);
System.out.println("d2 = " + d2);

System.out.println(d1.compareTo(d2));

程序输出的结果如下:
d1 = 1.1
d2 = 1.1
0

d1 和 d2 都为 1.1,所以 compareTo() 的结果就为 0,表示两个值是相等的。
总结一下,在遇到浮点数的时候,千万不要使用“==”操作符来进行比较,因为有精度问题。要么使用阈值来忽略舍入的问题,要么使用 BigDecimal 来替代 double 或者 float。

ouble 或者 float。
等会我就把这篇文章发给小王看看,同学们顺手点个赞 ,让小王不再感到那么孤单寂寞和冷。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353