【java解惑】java中那些反常识的小知识

一、Q:请为 i == i + 1 ;  提供一个声明使得条件成立。

 分析:一个数字永远不会等于它自己加 1对吧!如果这个数字是无穷大的又会怎样呢?Java 强制要求使用IEEE二进制浮点数算术标准IEEE 754,它可以让你用一个 double 或 float 来表示无穷大。无穷大加 1 还是无穷大。如果 i 在声明为无穷大那么i == i + 1 就成立。


    A:可以用任何被计算为无穷大的浮点算术表达式来声明 i ,例如double i = 1.0 / 0.0; 不过最好是能够利用标准类库提供的常量double i = Double.POSITIVE_INFINITY;

总结:事实上不必将 i 声明为无穷大也可以使条件成立,任何足够大的浮点数都可以实现这一目的。例如double i = 1.0e40;这样做之所以可以起作用是因为一个浮点数值越大,它和其后继数值之间的间隔就越大。浮点数的这种分布是用固定数量的有效位来表示它们的必然结果。对一个足够大的浮点数加 1 不会改变它的值,因为 1 不足以“填补它与其后继者之间的空隙”。浮点数操作返回的是最接近其精确的数学结果的浮点数值。一旦毗邻的浮点数值之间的距离大于 2,那么对其中的一个浮点数值加 1 将不会产生任何效果,因为其结果没有达到两个数值之间的一半。对于 float 类型加 1 不会产生任何效果的最小级数是 2^25,即 33,554,432;而对于 double 类型最小级数是 2^54,大约是 1.8*  10^16。毗邻的浮点数值之间的距离被称为一个 ulp,它是“最小单位unit in the last place”的首字母缩写词。在5.0版中引入了 Math.ulp 方法来计算 float或 double 数值的 ulp。

总之用一个 double 或一个 float 数值来表示无穷大是可以的。大多数人在第一次听到这句话时多少都会有一点吃惊,可能是因为我们无法用任何整数类型来表示无穷大的原因。

第二点将一个很小的浮点数加到一个很大的浮点数上时将不会改变大的浮点数的值。这过于违背直觉了,因为对实际的数字来说这是不成立的。我们应该记住二进制浮点算术只是对实际算术的一种近似。




二、Q:请为 i!=i  提供一个声明使得条件成立。

 分析:一个数字总是等于它自己对吧?!但是 IEEE 754 浮点算术保留了一个特殊的值用来表示一个不是数字的数量,这个值就是 NaN(“ 不是一个数字Not a Number” 的缩写)。对于所有没有良好的数字定义的浮点计算,例如 0.0/0.0其值都是它。规范中描述到NaN 不等于任何浮点数值包括它自身在内。因此如果 i 在被初始化为 NaN那么 i != i 就成立。

A:可以用任何计算结果为 NaN 的浮点算术表达式来初始化 i, 例如double i = 0.0 / 0.0;同样为了表达清晰可以使用标准类库提供的常量double i = Double.NaN;

 总结: NaN 还有其他的惊人之处。任何浮点操作只要它的一个或多个操作数为 NaN,那么其结果为 NaN。这条规则是非常合理的,但是它却具有奇怪的结果。 例如下面的程序将打印 false:

class Test {
 public static void main(String[]  args) {
 double i = 0.0 / 0.0;
 System.out.println(i - i == 0);
 }
}

这条计算 NaN 的规则所基于的原理是,一旦一个计算产生了 NaN,它就被损坏了,没有任何更进一步的计算可以修复这样的损坏。NaN 值有意使受损的计算继续执行下去直到方便处理这种情况的地方为止。

 总之float 和 double 类型都有一个特殊的 NaN 值用来表示不是数字的数量。对于涉及 NaN 值的计算,其规则很简单也很明智,但是这些规则的结果可能是违背直觉的。



三、Q:请为 i!=1+0; 提供一个声明使得条件成立。但是不能像上题一样使用浮点数。

    分析:与前一个题一样这个谜题初看起来是不可能实现的。毕竟一个数字总是等于它自身加上 0,你被禁止使用浮点数,因此不能使用 NaN。而在整数类型中没有 NaN 的等价物。我们必然可以得出这样的结论,即 i 的类型必须是非数值类型的,并且这其中存在着解谜方案。唯一对 + 操作符有定义的非数值类型就是 String。+ 操作符被重载了,对于 String 类型它执行的不是加法而是字符串连接。如果在连接中的某个操作数具有非 String 的类型,那么这个操作数就会在连接之前转换成字符串。

    A:事实上i 可以被初始化为任何值,只要它是 String 类型的即可。例如String i="搜索微信公众号ape_it";


    总结:操作符重载是很容易令人误解的。在本谜题中的加号看起来是表示一个加法,但是通过为变量 i 选择合适的类型即 String我们让它执行了字符串连接操作。甚至是因为变量被命名为 i都使得本题更加容易令人误解,因为 i通常被当作整型变量名而被保留的。对于程序的可读性来说好的变量名、方法名和类名至少与好的注释同等重要。



四、Q:请提供一个对 i 的声明,将下面的循环转变为一个无限循环:

while (i != 0) {
 i >>>= 1;
}

   

     分析:>>>=是对应于无符号右移操作符的赋值操作符。0 被从左移入到由移位操作而空出来的位上,即使被移位的负数也是如此。

    为了使移位合法,i 必须是一个整数类型byte、char、short、int 或 long。无符号右移操作符把 0 从左边移入。因此看起来这个循环执行迭代的次数与最大的整数类型所占据的位数相同即 64 次。怎样才能将它转变为一个无限循环呢?解决本谜题的关键在于,>>>=是一个复合赋值操作符。复合赋值操作符包括*=、/=、%=、+=、-=、 <<=、 >>=、 >>>=、&=、 ^=和| =。有关混合操作符的一个不幸的事实是,它们可能会自动地执行窄化原始类型转换。这种转换把一种数字类型转换成了另一种更缺乏表示能力的类型。窄化原始类型转换可能会丢失级数的信息或者是数值的精度。

    A:假设你在循环的前面放置了下面的声明 short i = -1; 因为 i 的初始值(short)0xffff是非 0 的,所以循环体会被执行。在执行移位操作时,第一步是将 i 提升为 int 类型。所有算数操作都会对short、byte和 char 类型的操作数执行这样的提升。这种提升是一个拓宽原始类型转换,因此没有任何信息会丢失。这种提升执行的是符号扩展,因此所产生的 int 数值是0xffffffff。然后这个数值右移 1 位,但不使用符号扩展,因此产生了 int数值 0x7fffffff。最后这个数值被存回到 i 中。为了将 int 数值存入 short变量,Java 执行的是可怕的窄化原始类型转换,它直接将高 16 位截掉。这样就只剩下(short)0xffff 了,我们又回到了开始处。循环的第二次以及后续的迭代行为都是一样的,因此循环将永远不会终止。


    总结:如果你将 i 声明为一个 short 或 byte 变量,并且初始化为任何负数,那么这种行为也会发生。如果你声明 i 为一个 char,那么你将无法得到无限循环,因为char 是无符号的,所以发生在移位之前的拓宽原始类型转换不会执行符号扩展。总之,不要在 short、byte 或 char 类型的变量之上使用复合赋值操作符。因为这样的表达式执行的是混合类型算术运算,它容易造成混乱。更糟的是,它们执行将隐式地执行会丢失信息的窄化转型,其结果是灾难性的。




五、Q:请为 i <= j && j <= i && i != j ;提供一个声明使得条件成立。

   

    分析:如果 i <= j 并且 j <= i,i 不是肯定等于 j 吗?这一属性对实数肯定有效。 事实上,它是如此地重要,以至于它有这样的定义: 实数上的<=关系是反对称的。Java 的<=操作符在 5.0 版之前是反对称的,但是这从 5.0 版之后就不再是了 。

 在5.0 版之前,Java 的数字比较操作符(<、 <=、 >和>=)要求它们的两个操作数都是原始数字类型的(byte、char、short、int、long、float 和 double)。但是在 5.0 版中,规范作出了修改,新规范描述道:每一个操作数的类型必须可以转换成原始数字类型。

    在 5.0 版中,自动包装(auto-boxing)和自动反包装(auto-unboxing)被添加到了 Java 语言中。<=操作符在原始数字类型集上仍然是反对称的,但是现在它还被应用到了被包装的数字类型上。(被包装的数字类型有: Byte、 Character、 Short、Integer、 Long、 Float 和 Double。)<=操作符在这些类型的操作数上不是反对称的,因为 Java 的判等操作符(==和!=)在作用于对象引用时,执行的是引用的比较,而不是值的比较。

    A:下面的声明赋予表达式(i <= j && j <= i && i != j)的值为 true:

Integer i = new Integer(0);
Integer j = new Integer(0);

前两个子表达式(i <= j 和 j <= i)在 i 和 j 上执行解包转换,并且在数字上比较所产生的 int 数值。i 和 j 都表示 0,所以这两个子表达式都被计算为 true。第三个子表达式(i != j)在对象引 用 i 和 j 上执行标识比较,因为它们都初始化为一个新的 Integer 实例,因此,第三个子表达式同样也被计算为 true。

    

    总结:当两个操作数都是被包装的数字类型时,数值比较操作符和判等操作符的行为存在着根本的差异:数值比较操作符执行的是值比较,而判等操作符执行的是引用标识的比较。




注:本【java解惑】系列均是博主阅读《java解惑》原书后将原书上的讲解和例子部分改编然后写成博文进行发布的。所有例子均亲自测试通过并共享在github上。通过这些例子激励自己惠及他人。同时本系列所有博文会同步发布在博主个人微信公众号搜索“爱题猿”或者“ape_it”方便大家阅读。如果文中有任何侵犯原作者权利的内容请及时告知博主以便及时删除如果读者对文中的内容有异议或者问题欢迎通过博客留言或者微信公众号留言等方式共同探讨。

源代码地址https://github.com/rocwinger/java-disabuse

时间: 2024-10-07 06:12:27

【java解惑】java中那些反常识的小知识的相关文章

【java解惑】java中那些反常识的小知识(续)

六.Q:请为 i!=0 && i ==-1 提供一个声明,使得其成立. 分析:在布尔表达式(i != 0 && i == -i)中,一元减号操作符作用于 i,这意味着它的类型必须是数字型的:一元减号操作符作用于一个非数字型操作数是非法的.因此,我们要寻找一个非 0 的数字型数值,它等于它自己的负值.NaN 不能满足这个属性,因为它不等于任何数值,因此,i 必须表示一个实际的数字.肯定没有任何数字满足这样的属性吗?嗯, 没有任何实数具有这种属性,但是没有任何一种 Java 数

关于C语言中的一些宏的小知识

关于C语言中的一些宏的小知识 1.##和# c语言中,##表示把两个宏参数贴合在一起,即,#define call(x,y) x##y ,执行call(x,y)结果为xy,例如,int x=2,y=5;int xy=90;printf("%d\n",call(x,y));//结果为90 ##被称为连接符,用来将两个宏参数连接为一个宏参数.而单个#的功能是将其后面的宏参数进行字符串化操作,简单地说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号,使其成为字符串. 以上引自百度知

面试过程中,你知道这些小知识,将事半功倍

在平时我们面试的过程总总会遇到一些面试官问一些平时我们不怎么注意的问题,当问出来的时候,面试者通常都是一脸懵逼,这和我想象中的不一样阿,怎么不按照套路出牌,当然一些小知识更能体现出你的细心和好学,以下分析20个面试中的小知识,共勉~.·整理的这份PDF有从基础到进阶.含有BATJ.字节跳动面试专题,算法专题,高端技术专题,混合开发专题,java面试专题,Android,Java小知识,到性能优化.线程.View.OpenCV.NDK等应有尽有.还有辅之相关的视频+学习笔记 (更多完整项目下载.未

拿不到想要的offer,只缘身在CV中,关于一些面试小知识

在平时我们面试的过程总总会遇到一些面试官问一些平时我们不怎么注意的问题,当问出来的时候,面试者通常都是一脸懵逼,这和我想象中的不一样阿,怎么不按照套路出牌,当然一些小知识更能体现出你的细心和好学,以下分析20个面试中的小知识,共勉~.·整理的这份PDF有从基础到进阶.含有BATJ.字节跳动面试专题,算法专题,高端技术专题,混合开发专题,java面试专题,Android,Java小知识,到性能优化.线程.View.OpenCV.NDK等应有尽有.还有辅之相关的视频+学习笔记 (更多完整项目下载.未

java 删除字符串中的反斜杠\

Java中有时候会打印出来会含有反斜杠(\)的字符串,我们需要删除时,可以使用 replace() 或 replaceAll() 但是要注意的是replaceAll()里面用的是正则表达式,所以一个斜扛要写4个,即在这个函数里,\\\\代表\(是个反斜杠代表打印出的一个斜杠) 用str.replaceAll( “\\\\”,  “”);就实现了消除str中的所有反斜杠.

java常量池中基本数据类型包装类的小陷阱

想必大部分学过java的人都应该做过这种题目: 1 public class Test { 2 public static void main(String[] args) { 3 //第一个字符串 4 String s1="hello"; 5 6 //第二个字符串 7 String s2="hello"; 8 9 //比较s1和s2是否相同 10 System.out.println(s1==s2); 11 12 13 /** 14 * 修改变量 15 */ 16

在 Java SE 6 中监视和诊断性能问题

Java™ Platform, Standard Edition 6 (Java SE) 专注于提升性能,提供的增强工具可以管理和监视应用程序以及诊断常见的问题.本文将介绍 Java SE 平台中监视和管理的基本知识,并提供 Java SE 6 中相关增强的详细信息. Java SE 6 对性能进行了深入研究,使用增强的工具管理和监视应用程序并且诊断常见问题.这些改进包括: 监视和管理 API 增强 正式支持增强的图形监视工具 JConsole 提供增强的 Java 虚拟机(JVM)测试工具 本

java中replaceAll反斜杠\

java中replaceAll反斜杠\ String s=new String("this is a \\"); s.replaceAll("\\","back slash"); 使用上面的代码会导致 java.util.regex.PatternSyntaxException: Unexpected internal error near index 1 \ ^ 错误原来是因为要使用replaceAll("\\\\",&qu

java 和 JS(javaScript)中的反斜杠正则转义

首先说下为什么要转义.在正则中有些字符有特殊含义的,比方说 * 可以前一子表达式的任意次,. 表示除"\r\n"之外的任何单个字符,+ 表示前一子表达式的一次或多次,等.而在有些情况下,需要正则验证这些字符,比方说要验证加减乘除四个运算符号,为了告诉编译器,你要验证的是一个字符而不是正则表达式,就要对这些有特殊含义的字符进行转义,这样就有了转义这一操作. 在java 和javascipt中,都是用反斜杠"\"进行转义,然后两种语言的用法不尽相同,下面举例说明: 1.