Java for You Java Java 基本类型的各种运算,你真的了解了么?

Java 基本类型的各种运算,你真的了解了么?

本文大纲: 在上一篇文章 很清晰!带你图解 Java 程序的结构,变量和类型 里,我们知道 Java 的基本类型分整型类型,浮点型类型和布尔类型三种。那针对不同的类型,Java 提供的运算能力也是各有不同,本篇文章就分析下 Java 基本类

#####

本文大纲:

在上一篇文章 很清晰!带你图解 Java 程序的结构,变量和类型 里,我们知道 Java 的基本类型分整型类型,浮点型类型和布尔类型三种。那针对不同的类型,Java 提供的运算能力也是各有不同,本篇文章就分析下 Java 基本类型里的各种运算是怎么回事。

整数运算

首先是整数的运算。

Java 提供了很多操作符,这些操作符可以作用于整数值上。

比较操作符

第一个是比较操作符,它的结果是 boolean 类型的值。包括

  • 数字比较运算符: 和 >=。
  • 小于,小于等于,大于,大于,大于等于
  • 数字相等运算符:== 和 !=。
  • 等于,不等于
  • 数字操作符

    第二个是数字操作符,它的结果是 int 或 long 类型的值。包括

  • 一元正负运算符:+ 和 -。
  • 正,负
  • 乘法运算符:*, / 和 % 。
  • 乘,除,取模
  • 加法运算符:+ 和 -。
  • 加,减
  • 递增运算符:++。
  • 加一
  • 递减运算符:–。
  • 减一
  • 有符合和无符号的移位操作符:> 和 >>>。
  • > :右移,正数右移,高位补0,负数右移,高位补1。
  • >>> :无符号右移,高位补0,不区分正数负数。
  • 按位求补运算符:~。
  • 整数按位运算符:&, ^ 和 |。
  • 转换运算符

    第三个是转换运算符。

    在学习转换之前,我们先了解下 Java 基本类型的精度高低顺序,从低到高的话,就是 `byte->short->char->int->long->float->double` 。

    低精度的类型转高精度,Java 是怎么处理呢?

    隐式转换

    这种情况其实本质不会损失精度,因此 Java 会进行类型的自动转换,也叫隐式类型转换。

    比如以下这段代码,它的输出你能猜到么?

    以下是输出:

    你会发现,`char ` 类型会转换为其对应的 ASCII 码, ` byte、char、short ` 参与运算时会自动转为 ` int `,但 ` += ` 和 ` ++ ` 不会转 ` int ` 。多种类型混合运算的时候,会自动转成精度最大的类型。这个类型可以覆盖到浮点数,但不能和布尔类型发生转换。

    自动转换 Java 就帮忙做掉了,不需要我们代码里显式声明。

    显示转换

    另外就是,高精度转低精度,这种情况下就需要强制转换了,也叫显式转换。

    你比如说以下代码:

    你会发现居然输出的是 -127,而不是 129。这是怎么回事呢?

    原来是 Java 在做高精度到低精度类型转换的过程中,丢失了精度。至于精度为什么会丢,为什么打印出来是另外一个值,我们需要先明确一个计算机基础知识。

    那就是计算机存储 Java 数字类型时,它在内存中的数据是以什么形式存在的?

    这就要涉及到原码,反码和补码的概念了。

    原码

    原码是未经更改的码。它由最左边的符号位和二进制数构成。符号位是 0 表示正数,符号位是 1 表示负数。符号位是哪一位,由计算机的位数决定。比如数字 6 在 8 位计算机中原码的表示就是:0000 0110。它的优点就是简单直观,可以直接表示数,所以你看到程序打印的值都是原码,无非是我们这里做了下二进制到十进制的转换。

    但原码也有缺点,就是不能直接参与运算,容易出错。你比如在数学上, ` 1+(-1)=0 `,但在二进制中

    ` 00000001+10000001=10000010 `,换算成十进制就是 -2,显然不符合预期。

    于是有人就提出了反码。

    反码

    反码是正数不变,负数取反的码。正数的反码和原码一样,负数的反码需要保留最左边符号位,然后将原码数值位按照每位取反得到。

    比如数字6在 8 位计算机中反码就是它的原码:0000 0110。数字(-6)在计算机中反码就是:1111 1001。以下图表是更多的原码例子,列出了 8位数值的无符号所得值,用原码表示所得值和用反码表示所得值。

    (反码示例数据表)

    反码就解决了原码进行减法运算时计算错误的问题,虽然反码解决方案也有一定缺陷,我们看下反码是怎么做的。

    数学表达:

    反码表达:

    这说明反码在进行减法运算时,大部分场景是正确的,只有在结果为 0 时,可能会带负号。0 还能带负号,理解起来真的是怪怪的,这其实是反码天然的缺陷。从上面 (反码示例数据表)中我们就可以看出,反码的表示范围包括了 -127 到 -0 以及 0 到 127,总共 256 个数。它把 0 也区分了正负,这显然是不符合逻辑的!

    为了解决这个问题,补码就出现了。

    补码

    补码是正数不变,负数取反补一的码。正数的补码和原码一样,负数的补码需要保留最左边符号位,然后将原码数值位按照每位取反再加一。

    不同于反码系统中 0 有两种表示方式,补码系统的 0 就只有一种表示方式,就是数字 0 本身。

    从反码角度上定义补码,正数的补码和反码一样,负数的补码就是它的反码加一。

    如下面这张表所示。

    (补码示例数据表)

    补码的这种表示方式很适合计算机处理,依然是上面的减法问题,我们看下补码是怎么做的。

    数学表达:

    补码表达:

    第一个结果 10000 0000 看上去似乎是错的,因为已经超过八个比特,不过若忽略掉(从右开始数)第 9 个比特,结果是 0000 0000(0)。这次的计算结果依然是 0,但和反码计算结果相比,就没了负号。

    对照补码示例数据表,我们也可以看出,补码的表示范围包括了 -128 到 0 再到 127,总共 256 个数。

    补码这样设计,使符号位能与有效值部分一起参与运算,从而简化运算规则,同时也把减法运算转换为加法运算,进一步简化了计算机中运算器的线路设计。

    基于这样的优势,补码也就成为了计算机数据存储的最常用的方式。而我们看到 Java 程序打印输出的值都是计算机把补码转成了原码显示的,反码是中间的过渡。

    原码、反码和补码可谓是计算机领域的三架“码”车,它们共同支撑了数据在计算机中存储与表达的形式,它们之间的关系如下:

  • 三码都是二进制表达。
  • 三码第一位是符号位,1 表示负数,0 表示正数,其余位是数值位。
  • 正数的三码都一样。
  • 负数的反码是在原码基础上对非符号位取反,即负数反码=符号位+原码数值位取反。
  • 负数的补码是在反码基础上加一,即负数补码=反码+1。
  • 负数补码转原码是在补码基础上减一,然后对非符号位取反,即负数原码=(补码-1)&&数值位取反。
  • 了解原码、反码和补码的概念后,我们回到精度丢失的问题上,回顾下之前的代码:

    在上面代码中,我们知道,int 类型数据是 32位,byte 类型数据为 8 位,Java 把 int 类型数据转成 byte 类型数据时,实质上是截取 int 后 8 位存到 byte 中。

    int 类型的 129 三码一致,都为:0000 0000 0000 0000 0000 0000 1000 0001。计算机中存的是补码。

    从 int 转换 byte,截取后 8 位为:1000 0001。得到的数据为依然是补码。

    我们按负数补码转原码的公式,会发现其原码为:补码(1000 0001)–> 反码(1000 0000)–> 原码(1111 1111)。即 1111 1111 就是 (byte)highIntValue 的结果。

    转换成十进制就是 lowByteValue=-(64+32+16+8+4+2+1)=-127。

    是不是恍然大悟了?计算机奇怪的现象其实也是有迹可循的!

    字符串串联运算符

    第四个是字符串串连运算符:+。

    当给定一个 String 操作数和一个整数操作数时,这个运算符就会把整数操作数转换为表示其十进制形式的 String,将两个字符串串联起来,生成一个新创建的 String。

    以下代码会输出什么呢?

    没错,程序会打印 7 以及和一段字符串的拼接。

    浮点数运算

    讲完了整数运算,我们再来看看浮点数运算。

    浮点数在计算机中的存储方式遵循 IEEE 754 浮点数的计数

    浮点数运算和整数运算相比,只能进行加减乘除的数值运算,不能做位运算。不过浮点数在计算机里表示的范围会比较大,32 位的 float 都比 64 位的 long 精度大!但它也有个缺点,就是浮点数有时候不能精确表示。

    IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。

    Java 常用单精度和双精度,所以我们只讨论这两种浮点格式。

    科学计数法

    说到浮点数,就不得不说科学计数法!

    科学计数法的出现,是用来表示一个极大或极小数,像四亿亿这样的数字,用整数也可以表示,但你要真写的话,都不知道写到猴年马月,而且可读性也很差,不科学!于是科学计数法就应运而生,简单清晰地表达这样的数字。

    科学计数法由符号、有效数字和指数三个部分组成。现实世界的数字规则是十进制,从 0 到 9,指数以 10 为底。计算机世界的二极管只有通电和断电两种状态,那对应过来就是二进制。

    浮点数表示

    十进制的科学计数法要求有效数字的整数部分必须在 [1,9] 的区间内,而二进制的整数部分区间就只能是 [1],由于是确定的一个信息,为了节省成本,计算机就省去了对这个 1 的存储。32 位 float 单精度浮点数格式如下,黄色部分就是 1.xxx 后边的小数部分。

    我们介绍下浮点数格式的结构,它分为三个部分。

    【符号位】在最高二进制位上分配 1 位表示浮点数的符号,0 表示正数,1 表示负数。

    【阶码位】相当于科学计数法的指数。在符号位右侧分配 8 位用来存储指数,IEEE754 标准规定阶码位存储的是指数对应的移码,而不是指数的原码或补码。

    所谓移码,就是将一个真值在数轴上正向平移一个偏移量后得到的。也就是这 8 个绿色格子直接计算,得到的结果减去 127,就是实际的指数。

    【尾数位】相当于科学计数法的有效数字。最右侧分配连续的 23 位用来存储有效数字,IEEE754 标准规定尾数以原码表示,规格化表示省略 1.,double 双精度浮点数的指数是 11 位,尾数部分是 52 位。

    常用浮点数的规格化表示如表所示:

    加减运算

    在数学中,进行两个小数的加减运算时,首先要将小数点对齐,然后同位数进行加减运算。对采用科学计数法表示的数做加减法运算时,想让小数点对齐,就要确保指数一样,然后再将有效数字按照正常的数进行加减运算。具体操作如下:

  • 零值检测。阶码和尾数全为 0,即零值,有零值参与可以直接出结果。
  • 对阶操作。通过阶码比较,确定小数点位置是否对齐。IEEE 754 规定对阶的移动方向为向右移动,即选择阶码小的数进行操作。
  • 尾数求和。尾数按位相加求和,负数的话先转补码再运算。
  • 结果规格化。计算的结果可能不符合规格化形式,此时要将其规格化。尾数位向右移动是右规,尾数位向左移动是左规。
  • 结果舍入。对阶或右规过程中,最右端被移出的位会被丢弃,造成结果精度损失。为减少精度损失,要先将移出的数据先保存,叫保护位,等到规格化后再根据保护位进行舍入处理。

1.0 – 0.9 运算过程说明

你知道 1.0 – 0.9 的值是多少么?

答案是:0.100000024

我们分析下计算机的计算过程。

1.0 的二进制为: 0011 1111 1000 0000 0000 0000 0000 0000

-0.9 的二进制为:1011 1111 0110 0110 0110 0110 0110 0110

我们对这两个浮点数分别拆解下:

尾数最左端有个隐藏位,所以我们尾数实际值最高位都补 1。后续计算都基于实际的尾数位进行。

先进行对阶。1.0 的阶码是 127,-0.9 的阶码是 126。比较阶码大小后需要右移 -0.9 尾数的补码,使其阶码变为 127,同时高位补 1,那移动后的结果就是 10001 1001 1001 1001 1001 101。

然后进行尾数求和。基于补码按位相加即可,注意符号位也要参与运算。

最左端是符号位,计算结果为 0,尾数位计算结果为 0000 1100 1100 1100 1100 1101。

接着进行规范化。按照规范,尾数最高位必须是 1,因此要将结果向左移动 4 位,同时阶码要减 4。移动后的阶码等于 123(二进制为 1111011),尾数为 1100 1100 1100 1100 1101 0000。再隐藏尾数最高位,进而变为 100 1100 1100 1100 1101 0000。

那最终得到的结果的符号为 0,阶码为 1111011,尾数为 100 1100 1100 1100 1101 0000,三部分组合起来就是 1.0 – 0.9 的结果,对于的十进制就是 0.100000024。

为了方便大家理解上述步骤,蜗牛画了个图帮助大家记忆。

布尔运算

讲完了浮点数运算,我们看下最后一种运算:布尔运算。我这里分了两种,逻辑运算符和条件运算符。

逻辑运算符

逻辑运算符有 ` &, |, !, ^, ||, && `,分别是与、或、非、异或,短路或和短路与。参与运算的是布尔值,输出结果也是布尔值。

条件运算符

然后是条件运算符,类似这种格式:type identifier = boolean-expression? true-res : false-res。这就是所谓的三元表达式,三元分别是布尔运算表达式,布尔运算值为 true 时的结果值,布尔运算值为 false 时的结果值。

例如: int b = a > 10? 10 : a,在 a 是 99 的时候就返回了 10,在 a 是 6 的时候就返回了 a 本身也就是 6。

小结

本文介绍了 Java 基本类型的三大类运算,包括整数运算,浮点数运算和布尔运算,在讲解各种运算的过程中,也引出了计算机的一些基础知识,像原码,反码,补码这类,也举例说明了一些你平时可能不会注意到的问题,比如 1.0 减去 0.9 在计算机的世界里居然不是整整的 0.1,其实在浮点数的世界里容易被你忽略甚至用错的点还很多,比如判断两个浮点数是否相等,如果直接用 ` == ` 是会让程序出错的。限于篇幅,另外蜗牛的认知还不够深,就没继续展开。

这篇文章断断续续也写了一周多,看似简单的运算符,真正想分享的时候,才知道自己知之甚少,边学习边分享。真的是知道的越多,不知道的也就越多。不过这也是好处,只有知道自己在认知上的不足,才能去做弥补,从而看到更广阔的世界。按认知力漏斗来看,已经处于第二层了,需要继续精进。

写文不易,欢迎读者朋友点赞和转发,感谢你们!

动手验证

# 看进程各线程的内存 / CPU 占用
top -H -p <pid>

# 某个端口被谁占着
lsof -i :8080

# 抓一次 HTTP 请求内容
sudo tcpdump -A -i any 'tcp port 8080' -c 50

延伸阅读:官方与权威资料

相关阅读(站内)

常见问题

Q:G1 GC 和 CMS 垃圾回收器有什么区别?

G1 把堆拆成多个 Region,用标记-整理避免内存碎片,目标是可预测的停顿时间;CMS 用标记-清除,回收快但会产生碎片。JDK 9 起 G1 成为默认收集器,JDK 14 移除 CMS。

Q:G1 的 Remembered Set 是干什么的?

Region 化之后跨 Region 的引用都要记账,Remembered Set 记录其他 Region 指向本 Region 的指针,回收某个 Region 时不用扫全堆,这是 G1 停顿可控的关键之一。

Q:怎么调优 G1 的停顿时间?

三步走:先把 -XX:MaxGCPauseMillis 设在合理区间(别贪低,太小会疯狂扩容堆)、再看 GC 日志里的 Mixed GC 频率定位大对象、用 -XX:InitiatingHeapOccupancyPercent 控制并发标记起手时机。

Q:为什么 Java 的 String 要设计成不可变?

不可变才能安全共享与缓存哈希值,也能安全地放进字符串常量池;一旦可变,其他线程持有引用时修改会破坏哈希表与缓存的一致性。

Q:Java 里 == 和 equals 的区别是什么?

== 比较引用地址,equals 比较对象内容。String、Integer 等包装类重写了 equals,自定义对象不重写 equals 时,equals 等价于 ==。


本文首发于 java4u.cn,转载请注明出处。

关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。

本文由 java4u.cn 发布,可自由转载、引用,但需署名作者且注明文章出处(作者:白色蜗牛,出处:java4u.cn)。如转载至微信公众号,请在文末添加作者公众号二维码。 https://java4u.cn/java_base/3035.html

作者: 蜗牛

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们

联系我们

公众号:蜗牛互联网

在线咨询: QQ交谈

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部