0

0

整理分享Java语言表达式的五个谜题

WBOY

WBOY

发布时间:2022-05-19 11:49:28

|

2651人浏览过

|

来源于CSDN

转载

本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于语言表达式的相关问题,其中包括了奇数性、找零、长整数等等内容,下面一起来看一下,希望对大家有帮助。

整理分享Java语言表达式的五个谜题

推荐学习:《java视频教程

谜题一:奇数性

下面的方法意图确定它那唯一的参数是否是一个奇数。这个方法能够正确运转吗?

public static boolean isOdd(int i){ 
returni%2==1:
}

        奇数可以被定义为被2整除余数为1的整数。表达式i%2计算的是i整除2时所产生的余数,因此看起来这个程序应该能够正确运转。遗憾的是,它不能;它在四分之一的时间里返回的都是错误的答案。

立即学习Java免费学习笔记(深入)”;

        为什么是四分之一?因为在所有的int数值中,有一半都是负数,而isOdd方法对于对所有负奇数的判断都会失败。在任何负整数上调用该方法都回返回false,不管该整数是偶数还是奇数。这是Java对取余操作符(%)的定义所产生的后果。该操作符被定义为对于所有的int数值a和所有的非零int数值b,都满足下面的恒等式:

(a/b)*b+(a%b)==a

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETkDnl5vogIzkuI3oqIDnrJHogIzkuI3or63nmoTmtYXkvKQ,size_20,color_FFFFFF,t_70,g_se,x_16

         换句话说,如果你用b整除a,将商乘以b,然后加上余数,那么你就得到了最初的值a。该恒等式具有正确的含义,但是当与Java的截尾整数整除操作符相结合时,它就意味着:当取余操作返回一个非零的结果时,它与左操作数具有相同的正负符号。

        当i是一个负奇数时,i%2等于-1而不是1,因此isOdd方法将错误地返回false。为了防止这种意外,请测试你的方法在为每一个数值型参数传递负数、零和正数数值时,其行为是否正确。这个问题很容易订正。只需将i%2与0而不是与1比较,并且反转比较的含义即可:

public static boolean isOdd(inti){ 
returni%2!=0;
}

        如果你正在在一个性能临界(performance-critical)环境中使用isOdd方法,那么用位操作符AND(&)来替代取余操作符会显得更好:

public static boolean isOdd(inti){ 
return(i&1)!=0;
}

        总之,无论你何时使用到了取余操作符,都要考虑到操作数和结果的符号。该操作符的行为在其操作数非负时是一目了然的,但是当一个或两个操作数都是负数时,它的行为就不那么显而易见

了。

谜题二:找零时刻

请考虑下面这段话所描述的问题:

        Tom在一家汽车配件商店购买了一个价值$1.10的火花塞,但是他钱包中都是两美元一张的钞票。如果他用一张两美元的钞票支付这个火花塞,那么应该找给他多少零钱呢?

下面是一个试图解决上述问题的程序,它会打印出什么呢?

public class Change{
public static void main(String args[]){ 
Systemoutprintln(2.00-1.10);
}
}

        你可能会很天真地期望该程序能够打印出0.90但是它如何才能知道你想要打印小数点后两位小数呢?

        如果你对在DoubletoString文档中所设定的将 double类型的值转换为字符串的规则有所了解你就会知道该程序打印出来的小数,是足以将 double类型的值与最靠近它的临近值区分出来的最短的小数,它在小数点之前和之后都至少有一位。因此,看起来,该程序应该打印0.9是合理的。

这么分析可能显得很合理,但是并不正确。如果你运行该程序,你就会发现它打印的是:

0.8999999999999999

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETkDnl5vogIzkuI3oqIDnrJHogIzkuI3or63nmoTmtYXkvKQ,size_20,color_FFFFFF,t_70,g_se,x_16

         问题在于1.1这个数字不能被精确表示成为一个 double,因此它被表示成为最接近它的double值。该程序从2中减去的就是这个值。遗憾的是,这个计算的结果并不是最接近0.9的double值。表示结果的double值的最短表示就是你所看到的打印出来的那个可恶的数字。

        更一般地说,问题在于并不是所有的小数都可以用二进制浮点数来精确表示的。

        如果你正在用的是JDK5.0或更新的版本,那么你可能会受其诱惑,通过使用printf工具来设置输出精度的方订正该程序:

//拙劣的解决方案-仍旧是使用二进制浮点数 

System.out.printf("%.2f%n",2.00-1.10);

        这条语句打印的是正确的结果,但是这并不表示它就是对底层问题的通用解决方案:它使用的仍日是二进制浮点数的double运算。浮点运算在一个范围很广的值域上提供了很好的近似,但是它通常不能产生精确的结果。二进制浮点对于货币计算是非常不适合的,因为它不可能将0.1-或者10的其它任何次负幂--精确表示为一个长度有限的二进制小数解决该问题的一种方式是使用某种整数类型,例如int或long,并且以分为单位来执行计算。如果你采纳了此路线,请确保该整数类型大到足够表示在程序中你将要用到的所有值。对这里举例的谜题来说,int就足够了。下面是我们用int类型来以分为单位表示货币值后重写的println语句。这个版本将打印出正确答案90分:

Systemoutprintln((200-110)+"cents")

        解决该问题的另一种方式是使用执行精确小数运算的BigDecimal。它还可以通过JDBC与SQL DECIMAL类型进行互操作。这里要告诫你一点:一定要用BigDecimal(String)构造器,而千万不要用BigDecimal(double)。后一个构造器将用它的参数的精确”值来创建一个实例:new BigDecimal(1)将返回一个表示0100000000000000055511151231257827021181583404541015625BigDecimal。通过正确使用BigDecimal,程序就可以打印出我们所期望的结果0.90:

import java.math.BigDecimal; 
public class Changel {
public static void main(String args[]){
System.out.println(newBigDecimal(2.00")
subtract(new BigDecimal("1.10")));
}
}

        这个版本并不是十分地完美,因为Java并没有为 BigDecimal提供任何语言上的支持。使用

BigDecimal的计算很有可能比那些使用原始类型的计算要慢一些,对某些大量使用小数计算的程序来说,这可能会成为问题,而对大多数程序来说,这显得一点也不重要。

Nanonets
Nanonets

基于AI的自学习OCR文档处理,自动捕获文档数据

下载

        总之,在需要精确答案的地方,要避免使用 float和double;对于货币计算,要使用int、long或BigDecimal。对于语言设计者来说,应该考虑对小数运算提供语言支持。一种方式是提供对操作符重载的有限支持,以使得运算符可以被塑造为能够对数值引用类型起作用,例如BigDecimal。另一种方式是提供原始的小数类型,就像COBOL与PL/I所作的一样。

谜题三:长整数

        这个谜题之所以被称为长整除是因为它所涉及的程序是有关两个long型数值整除的。被除数表示的是一天里的微秒数;而除数表示的是一天里的毫秒数。这个程序会打印出什么呢?

public class Longpision{
public static void main(String args[]){
final long MICROS PER DAY=24*60*60*1000*1000;
final long MILLIS PER DAY=24*60*60*1000;
Systemoutprintln(MICROS PER DAY/ MILLIS PER DAY);
}
}

        这个谜题看起来相当直观。每天的毫秒数和每天的微秒数都是常量。为清楚起见,它们都被表示成积的形式。每天的微秒数是(24小时/天*60分钟/小时*60秒/分钟*1000毫秒/秒*1000微秒/毫秒)。而每天的毫秒数的不同之处只是少了最后一个因子1000。当你用每天的毫秒数来整除每天的微秒数时,除数中所有的因子都被约掉了,只剩下1000,这正是每毫秒包含的微秒数。

        除数和被除数都是long类型的,long类型大到了可以很容易地保存这两个乘积而不产生溢出。因此,看起来程序打印的必定是1000。遗憾的是,它打印的是5。这里到底发生了什么呢?

        问题在于常数MICROS PER DAY的计算确实”溢出了。尽管计算的结果适合放入long中,并且其空间还有富余,但是这个结果并不适合放入 int中。这个计算完全是以int运算来执行的,并且只有在运算完成之后,其结果才被提升到long,而此时已经太迟了:计算已经溢出了,它返回的是一个小了200倍的数值。从int提升到 long是一种拓宽原始类型转换(widening primitive conversion),它保留了(不正确的)数值。这个值之后被MILLIS PER DAY整除,而MILLIS PER DAY的计算是正确的,因为它适合int运算。这样整除的结果就得到了5。

        那么为什么计算会是以int运算来执行的呢?为所有乘在一起的因子都是int数值。当你将两个int数值相乘时,你将得到另一个int数值。Java不具有目标确定类型的特性,这是一种语言特性,其含义是指存储结果的变量的类型会影响到计算所使用的类型。

        通过使用long常量来替代int常量作为每一个乘积的第一个因子,我们就可以很容易地订正这个程序。这样做可以强制表达式中所有的后续计算都用long运作来完成。尽管这么做只在MICROS PER DAY表达式中是必需的,但是在两个乘积中都这么做是一种很好的方式。相似地,使用long作为乘积的“第一个”数值也并不总是必需的,但是这么做也是一种很好的形式。在两个计算中都以long数值开始可以很清楚地表明它们都不会溢出。下面的程序将打印出我们所期望的1000:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETkDnl5vogIzkuI3oqIDnrJHogIzkuI3or63nmoTmtYXkvKQ,size_20,color_FFFFFF,t_70,g_se,x_16

public class Longpision{

public static void main(String args[)

final long MICROS PER DAY=24L*60*60*1000*1000:

final long MILLIS PER DAY=24L*60*60*1000;

SystemoutprintlnMICROS PER DAY MILLIS PER DAY);

}

}

        这个教训很简单:当你在操作很大的数字时,千万要提防溢出--它可是一个缄默杀手。即使用来保存结果的变量已显得足够大,也并不意味着要产生结果的计算具有正确的类型。当你拿不准时,就使用long运算来执行整个计算。

        语言设计者从中可以吸取的教训是:也许降低默溢出产生的可能性确实是值得做的一件事。这可以通过对不会产生缄默溢出的运算提供支持来实现。程序可以抛出一个异常而不是直接溢出。就像Ada所作的那样,或者它们可以在需要的时候自动地切换到一个更大的内部表示上以防止溢出,就像Lisp所作的那样。这两种方式都可能会遭受与其相关的性能方面的损失。降低缄默溢出的另一种方式是支持目标确定类型,但是这么做会显著地增加类型系统的复杂度。

谜题四:初级问题

        得啦,前面那个谜题是有点棘手,但它是有关整除的,每个人都知道整除是很麻烦的。那么下面的程序只涉及加法,它又会打印出什么呢?

public class Elementary{

public static void main(String]args) {

Systemoutprintln(12345+54321);

}

}

        从表面上看,这像是一个很简单的谜题--简单到不需要纸和笔你就可以解决它。加号的左操作数的各个位是从1到5升序排列的,而右操作数是降序排列的。因此,相应各位的和仍然是常数,程序必定打印66666。对于这样的分析,只有一个问题:当你运行该程序时,它打印出的是17777。难道是Java对打印这样的非常数字抱有偏见吗?不知怎么的,这看起来并不像是一个合理的解释。

        事物往往有别于它的表象。就以这个问题为例,它并没有打印出我们想要的输出。请仔细观察+操作符的两个操作数,我们是将一个int类型的12345加到了long类型的54321上。请注意左操作数开头的数字1和右操作数结尾的小写字母1之间的细微差异。数字1的水平笔划(称为“臂(arm)”)和垂直笔划(称为“茎(stem)”)之间是一个锐角,而与此相对照的是,小写字母l的臂和茎之间是一个直角。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETkDnl5vogIzkuI3oqIDnrJHogIzkuI3or63nmoTmtYXkvKQ,size_20,color_FFFFFF,t_70,g_se,x_16

         在你大喊“恶心!”之前,你应该注意到这个问题确实已经引起了混乱,这里确实有一个教训:在 long型字面常量中,一定要用大写的L,千万不要用小写的1。这样就可以完全掐断这个谜题所产生的混乱的源头。

System.out.println(12345+5432L);

        相类似的,要避免使用单独的一个1字母作为变量名。例如,我们很难通过观察下面的代码段来判断它到底是打印出列表1还是数字1。

List l=new ArrayList<String>() ;

l.add("Foo");

System.outprintln(1);

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETkDnl5vogIzkuI3oqIDnrJHogIzkuI3or63nmoTmtYXkvKQ,size_17,color_FFFFFF,t_70,g_se,x_16

         总之,小写字母l和数字1在大多数打字机字体中都是几乎一样的。为避免你的程序的读者对二者产生混淆,千万不要使用小写的1来作为long型字面常量的结尾或是作为变量名。Java从C编程语言中继承良多,包括long型字面常量的语法。也许当初允许用小写的1来编写long型字面常量本身就是一个错误。

谜题五:十六进制的趣事

下面的程序是对两个十六进制(hex)字面常量进行相加,然后打印出十六进制的结果。这个程序会打印出什么呢?

public class JoyOfHex{

public static void main(String[] args){ 

System.out.println(

Long.toHexString(0x100000000L+0xcafebabe));

}

}

        看起来很明显,该程序应该打印出1cafebabe。毕竟,这确实就是十六进制数字10000000016与 cafebabe16的和。该程序使用的是long型运算,它可以支持16位十六进制数,因此运算溢出是不可能的。

        然而,如果你运行该程序,你就会发现它打印出来的是cafebabe,并没有任何前导的1。这个输出表示的是正确结果的低32位,但是不知何故第33位丢失了。

        看起来程序好像执行的是int型运算而不是long型运算,或者是忘了加第一个操作数。这里到底发生了什么呢?

        十进制字面常量具有一个很好的属性,即所有的十进制字面常量都是正的,而十六进制和八进制字面常量并不具备这个属性。要想书写一个负的十进制常量,可以使用一元取反操作符(-)连接一个十进制字面常量。以这种方式,你可以用十进制来书写任何int或long型的数值,不管它是正的还是负的,并且负的十进制常数可以很明确地用一个减号符号来标识。但是十六进制和八进制字面常量并不是这么回事,它们可以具有正的以及负的数值。如果十六进制和八进制字面常量的最高位被置位了,那么它们就是负数。在这个程序中,数字Oxcafebabe是一个int常量,它的最高位被置位了,所以它是一个负数。它等于十进制数值-889275714。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETkDnl5vogIzkuI3oqIDnrJHogIzkuI3or63nmoTmtYXkvKQ,size_20,color_FFFFFF,t_70,g_se,x_16

         该程序执行的这个加法是一种“混合类型的计算(mixed-type computation)左操作数是long类型的,而右操作数是int类型的。为了执行该计算,Java将int类型的数值用拓宽原始类型转换提升为一个long类型,然后对两个long类型数值相加。因为int是一个有符号的整数类型,所以这个转换执行的是符合扩展:它将负的int类型的数值提升为一个在数值上相等的long类型数值。这个加法的右操作数0xcafebabe被提升为了long类型的数值0xffffffffcafebabeL。这个数值之后被加到了左操作数0x100000000L上。当作为int类型来被审视时,经过符号扩展之后的右操作数的高32位是-1,而左操作数的高32位是1,将这两个数相加就得到了0,这也就解释为什么在程序输出中前导1丢失了。下面所示是用手写的加法实现。(在加法上面的数字是进位。)

1111111

0xffffffffcafebabeL

+0x0000000100000000L

0x00000000cafebabeL

        订正该程序非常简单,只需用一个long十六进制字面常量来表示右操作数即可。这就可以避免了具有破坏力的符号扩展,并且程序也就可以打印出我们所期望的结果1cafebabe:

public class JoyOfHex{

public static void main(String[] args){ 

System.outprintln(

LongtoHexString(0x100000000L+0xcafebabeL));

}

}

        这个谜题给我们的教训是:混合类型的计算可能会产生混淆,尤其是十六进制和八进制字面常量无需显式的减号符号就可以表示负的数值。为了避免这种窘境,通常最好是避免混合类型的计算。对于语言的设计者们来说,应该考虑支持无符号的整数类型,从而根除符号扩展的可能性。可能会有这样的争辩:负的十六进制和八进制字面常量应该被禁用,但是这可能会挫伤程序员,他们经常使用十六进制字面常量来表示那些符号没有任何重要含义的数值。

推荐学习:《java视频教程

相关文章

java速学教程(入门到精通)
java速学教程(入门到精通)

java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

Python异步编程与Asyncio高并发应用实践
Python异步编程与Asyncio高并发应用实践

本专题围绕 Python 异步编程模型展开,深入讲解 Asyncio 框架的核心原理与应用实践。内容包括事件循环机制、协程任务调度、异步 IO 处理以及并发任务管理策略。通过构建高并发网络请求与异步数据处理案例,帮助开发者掌握 Python 在高并发场景中的高效开发方法,并提升系统资源利用率与整体运行性能。

46

2026.03.12

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

178

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

51

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

92

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

102

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

227

2026.03.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

532

2026.03.04

AI安装教程大全
AI安装教程大全

2026最全AI工具安装教程专题:包含各版本AI绘图、AI视频、智能办公软件的本地化部署手册。全篇零基础友好,附带最新模型下载地址、一键安装脚本及常见报错修复方案。每日更新,收藏这一篇就够了,让AI安装不再报错!

171

2026.03.04

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.4万人学习

C# 教程
C# 教程

共94课时 | 11.3万人学习

Java 教程
Java 教程

共578课时 | 81.5万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号