热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

为什么不建议你使用实数作为HashMap的key?

点击关注公众号,实用技术文章及时了解来源:blog.csdn.netqq_30219017articledetails796894921.起因让我关注到

点击关注公众号,实用技术文章及时了解e47ff9e1931b346480b1a06f20447a4a.png

来源:blog.csdn.net/qq_30219017/article/details/79689492

1.起因

让我关注到这一点的起因是一道题:牛客网上的max-points-on-a-line

题目是这么描述的:

Given n points on a 2D plane, find the maximum number of points that lie on the same straight line.

大意就是给我一些点的X,Y坐标,找到过这些点最多的直线,输出这条线上的点数量

于是我就敲出了以下的代码:

import java.util.HashMap;
import java.util.Map;//class Point {
//    int x;
//    int y;
//    Point(int a, int b) { x = a; y = b; }
//}public class Solution {public int maxPoints(Point[] points) {if (points.length <&#61; 2) {return points.length;}int max &#61; 2;for (int i &#61; 0; i < points.length - 1; i&#43;&#43;) {Map map &#61; new HashMap<>(16);// 记录垂直点数; 当前和Points[i]在一条线上的最大点数; 和Points[i]垂直的点数int ver &#61; 0, cur, dup &#61; 0;for (int j &#61; i &#43; 1; j < points.length; j&#43;&#43;) {if (points[j].x &#61;&#61; points[i].x) {if (points[j].y !&#61; points[i].y) {ver&#43;&#43;;} else {dup&#43;&#43;;}} else {float d &#61; (float)((points[j].y - points[i].y) / (double) (points[j].x - points[i].x));map.put(d, map.get(d) &#61;&#61; null ? 1 : map.get(d) &#43; 1);}}cur &#61; ver;for (int v : map.values()) {cur &#61; Math.max(v, cur);}max &#61; Math.max(max, cur &#43; dup &#43; 1);}return max;}
}

这段代码在天真的我看来是没啥问题的&#xff0c;可就是没办法过&#xff0c;经过长久的排查错误&#xff0c;我写了以下代码加在上面的代码里运行

public static void main(String[] args) {int[][] vals &#61; {{2,3},{3,3},{-5,3}};Point[] points &#61; new Point[3];for (int i&#61;0; i}

它输出的&#xff0c;竟然是2

也就是说&#xff0c;它认为(3-3) / (3-2) 和 (3-3) / (-5-2) 不同? 什么鬼…

经过debug,发现上述结果分别是0.0和-0.0

0.0 难道不等于 -0.0 ?

这时我心里已经一阵卧槽了&#xff0c;不过我还是写了验证代码:

System.out.println(0.0 &#61;&#61; -0.0);

结果是True&#xff0c;没问题啊&#xff0c;我凌乱了……

一定是java底层代码错了! 我没错……

又是一阵debug,我找到了这条语句:

map.put(d, map.get(d) &#61;&#61; null ? 1 : map.get(d) &#43; 1);

我觉得map.get()很有问题, 它的源代码是这样的:

public V get(Object key) {Node e;return (e &#61; getNode(hash(key), key)) &#61;&#61; null ? null : e.value;
}

唔&#xff0c;先获得hash()是吧&#xff0c;那我找到了它的hash函数:

static final int hash(Object key) {int h;return (key &#61;&#61; null) ? 0 : (h &#61; key.hashCode()) ^ (h >>> 16);
}

再来&#xff0c;这里是要比较h 和key的hashCode是吧,那我们去看hashCode()函数

public native int hashCode();

这是一个本地方法&#xff0c;看不到源码了&#xff0c;唔&#xff0c;&#xff0c;那我就使用它看看吧&#xff0c;测试一下不就好了吗,我写了以下的测试代码:

public static void main(String[] args) {System.out.println(0.0 &#61;&#61; -0.0);System.out.println(new Float(0.0).hashCode() &#61;&#61; new Float(-0.0).hashCode());}

结果竟然是True和False !!!

这个源头终于找到了, 0.0 和 -0.0 的hashCode值是不同的 !

经过一番修改&#xff0c;我通过了这道题(其实精度也会有问题&#xff0c;应该使用BigDecimal的&#xff0c;不过牛客网要求没那么高。后来我想了想只有把直线方程写成Ax&#43;By&#43;C&#61;0的形式才能完全避免精度问题)

接下来&#xff0c;探讨下实数的hashCode()函数是个啥情况:

2.实数的hashCode()

  • 在程序执行期间&#xff0c;只要equals方法的比较操作用到的信息没有被修改&#xff0c;那么对这同一个对象调用多次&#xff0c;hashCode方法必须始终如一地返回同一个整数。

  • 如果两个对象根据equals方法比较是相等的&#xff0c;那么调用两个对象的hashCode方法必须返回相同的整数结果。

  • 如果两个对象根据equals方法比较是不等的&#xff0c;则hashCode方法不一定得返回不同的整数。——《effective java》

那么我们来看看&#xff0c;0.0和-0.0调用equals方法是否相等:

System.out.println(new Float(0.0).equals(0.0f));
System.out.println(new Float(0.0).equals((float) -0.0));

输出是True 和 False

好吧&#xff0c;二者调用equals() 方法不相等&#xff0c;看来是满足了书里说的逻辑的

那我们看看Float底层equals函数咋写的:

public boolean equals(Object obj) {return (obj instanceof Float)&& (floatToIntBits(((Float)obj).value) &#61;&#61; floatToIntBits(value));
}

哦&#xff0c;原来是把Float转换成Bits的时候发生了点奇妙的事&#xff0c;于是我找到了一切的源头:

/*** Returns a representation of the specified floating-point value* according to the IEEE 754 floating-point "single format" bit* layout.** 

Bit 31 (the bit that is selected by the mask* {&#64;code 0x80000000}) represents the sign of the floating-point* number.* Bits 30-23 (the bits that are selected by the mask* {&#64;code 0x7f800000}) represent the exponent.* Bits 22-0 (the bits that are selected by the mask* {&#64;code 0x007fffff}) represent the significand (sometimes called* the mantissa) of the floating-point number.** 

If the argument is positive infinity, the result is* {&#64;code 0x7f800000}.** 

If the argument is negative infinity, the result is* {&#64;code 0xff800000}.** 

If the argument is NaN, the result is {&#64;code 0x7fc00000}.** 

In all cases, the result is an integer that, when given to the* {&#64;link #intBitsToFloat(int)} method, will produce a floating-point* value the same as the argument to {&#64;code floatToIntBits}* (except all NaN values are collapsed to a single* "canonical" NaN value).** &#64;param   value   a floating-point number.* &#64;return the bits that represent the floating-point number.*/public static int floatToIntBits(float value) {int result &#61; floatToRawIntBits(value);// Check for NaN based on values of bit fields, maximum// exponent and nonzero significand.if (((result & FloatConsts.EXP_BIT_MASK) &#61;&#61;FloatConsts.EXP_BIT_MASK) &&(result & FloatConsts.SIGNIF_BIT_MASK) !&#61; 0)result &#61; 0x7fc00000;return result;}

这文档挺长的&#xff0c;也查了其它资料&#xff0c;看了半天终于搞懂了

就是说Java浮点数的语义一般遵循IEEE 754二进制浮点算术标准。IEEE 754标准提供了浮点无穷&#xff0c;负无穷&#xff0c;负零和NaN&#xff08;非数字&#xff09;的定义。在使用Java过程中&#xff0c;一些特殊的浮点数通常会让大家很迷惑

当浮点运算产生一个非常接近0的负浮点数时&#xff0c;会产生“-0.0”&#xff0c;而这个浮点数不能正常表示

我们可以输出一波0.0和-0.0的数据:

System.out.println(Float.floatToIntBits((float) 0.0));
System.out.println(Float.floatToIntBits((float) -0.0));
System.out.println(Float.floatToRawIntBits(0.0f));
System.out.println(Float.floatToRawIntBits((float)-0.0));

结果:

0
-2147483648
0
-2147483648

就是说&#xff0c;存储-0.0, 竟然用的是0x80000000

这也是我们熟悉的Integer.MIN_VALUE

3.总结

java中浮点数的表示比较复杂&#xff0c;特别是牵涉到-0.0, NaN, 正负无穷这种&#xff0c;所以不适宜用来作为Map的key, 因为可能跟我们预想的不一致

推荐&#xff1a;

主流Java进阶技术&#xff08;学习资料分享&#xff09;

cf262e2c13ca38f795ecb37a821da689.png

PS&#xff1a;因为公众号平台更改了推送规则&#xff0c;如果不想错过内容&#xff0c;记得读完点一下“在看”&#xff0c;加个“星标”&#xff0c;这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧&#xff01;



推荐阅读
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • Python爬虫中使用正则表达式的方法和注意事项
    本文介绍了在Python爬虫中使用正则表达式的方法和注意事项。首先解释了爬虫的四个主要步骤,并强调了正则表达式在数据处理中的重要性。然后详细介绍了正则表达式的概念和用法,包括检索、替换和过滤文本的功能。同时提到了re模块是Python内置的用于处理正则表达式的模块,并给出了使用正则表达式时需要注意的特殊字符转义和原始字符串的用法。通过本文的学习,读者可以掌握在Python爬虫中使用正则表达式的技巧和方法。 ... [详细]
  • 本文介绍了在iOS开发中使用UITextField实现字符限制的方法,包括利用代理方法和使用BNTextField-Limit库的实现策略。通过这些方法,开发者可以方便地限制UITextField的字符个数和输入规则。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Final关键字的含义及用法详解
    本文详细介绍了Java中final关键字的含义和用法。final关键字可以修饰非抽象类、非抽象类成员方法和变量。final类不能被继承,final类中的方法默认是final的。final方法不能被子类的方法覆盖,但可以被继承。final成员变量表示常量,只能被赋值一次,赋值后值不再改变。文章还讨论了final类和final方法的应用场景,以及使用final方法的两个原因:锁定方法防止修改和提高执行效率。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
author-avatar
田景撩人_108
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有