Java for You Java基础教程 【Java基础教程06】Java基本类型的各种运算

【Java基础教程06】Java基本类型的各种运算

73 / 100

本文大纲:

Java基本类型

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

整数运算

首先是整数的运算。

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

比较操作符

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

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

数字操作符

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

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

转换运算符

第三个是转换运算符。

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

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

隐式转换

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

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

public class TypeConvert {

    /**
     * from 公众号:蜗牛互联网
     *
     * @param args 入参
     */
    public static void main(String[] args) {

        // 数字 65 实际表示大写字母 A
        char charValue = 65;

        // 初始化的char
        System.out.println("initCharValue=" + charValue);

        // 加一
        charValue += 1;
        System.out.println("CharAddOneValue=" + charValue);

        // 自增符号 打印 B 后,charValue 的值已经是 C 了,也就是 67
        System.out.println("CharAddOneValue=" + charValue++);

        // 加法运算 输出 134,即 67+67=134
        System.out.println(charValue + charValue);

        // 往高精度自动转
        int intValue = charValue;
        System.out.println("intValue=" + intValue);

        long longValue = intValue;
        System.out.println("longValue=" + longValue);

        double doubleValue = intValue;
        System.out.println("doubleValue=" + doubleValue);

    }
}

以下是输出:

initCharValue=A
CharAddOneValue=B
CharAddOneValue=B
134
intValue=67
longValue=67
doubleValue=67.0

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

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

显示转换

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

你比如说以下代码:

// 高精度到低精度,走强转
int highIntValue = 129;
byte lowByteValue = (byte)highIntValue;

// 但强转后会出现精度丢失,比如这里会输出 -127
System.out.println(lowByteValue);

你会发现居然输出的是 -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位数值的无符号所得值,用原码表示所得值和用反码表示所得值。

数值 无符号所得值 用原码表示所得值 用反码表示所得值
0111 1111 127 127 127
0111 1110 126 126 126
0000 0010 2 2 2
0000 0001 1 1 1
0000 0000 0 0 0
1111 1111 255 −127 −0
1111 1110 254 −126 −1
1111 1101 253 −125 −2
1000 0001 129 -1 −126
1000 0000 128 -0 −127

(反码示例数据表)

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

数学表达:

1 - 1 = 1 + (-1) = 0;
1 - 2 = 1 + (-2) = (-1);

反码表达:

0000 0001 + 1111 1110 = 1111 1111(-0);//有问题
0000 0001 + 1111 1101 = 1111 1110(-1);//正确

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

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

补码

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

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

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

如下面这张表所示。

数值 无符号所得值 用原码表示所得值 用反码表示所得值 用补码表示所得值
0111 1111 127 127 127 127
0111 1110 126 126 126 126
0000 0010 2 2 2 2
0000 0001 1 1 1 1
0000 0000 0 0 0 0
1111 1111 255 −127 −0 -1
1111 1110 254 −126 −1 -2
1111 1101 253 −125 −2 -3
1000 0001 129 -1 −126 -127
1000 0000 128 -0 −127 -128

(补码示例数据表)

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

数学表达:

1 - 1 = 1 + (-1) = 0;
1 - 2 = 1 + (-2) = (-1);

补码表达:

    0000 0001 (1)
  + 1111 1111 (-1)
--------------
   10000 0000 (0)

    0000 0001 (1)
  + 1111 1110 (-2)
--------------
    1111 1111 (-1)

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

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

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

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

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

  1. 三码都是二进制表达
  2. 三码第一位是符号位,1 表示负数,0 表示正数,其余位是数值位
  3. 正数的三码都一样。
  4. 负数的反码是在原码基础上对非符号位取反,即负数反码=符号位+原码数值位取反
  5. 负数的补码是在反码基础上加一,即负数补码=反码+1
  6. 负数补码转原码是在补码基础上减一,然后对非符号位取反,即负数原码=(补码-1)&&数值位取反

了解原码、反码和补码的概念后,我们回到精度丢失的问题上,回顾下之前的代码:

// 高精度到低精度,走强转
int highIntValue = 129;
byte lowByteValue = (byte)highIntValue;

// 但强转后会出现精度丢失,比如这里会输出 -127
System.out.println(lowByteValue);

在上面代码中,我们知道,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。

以下代码会输出什么呢?

// 用二进制形式定义一个 int
int strAppendInt = 0b111;

System.out.println(strAppendInt);

// 字符串连接打印
System.out.println("字符串串联运算符测试,原定义为:0b111,打印值为:" + strAppendInt);

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

7
字符串串联运算符测试,原定义为:0b111,打印值为: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 位。

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

数值 浮点数二进制表示 说明
-16 1100 0001 1000 0000 0000 0000 0000 0000 第 1 位是符号位,1 表示负数。阶码位 为 131 – 127 = 4,即 2 =16,尾数部分为 1.0
16.35 0100 0001 1000 0010 1100 1100 1100 1101 第 1 位是符号位,0 表示正数。阶码位同上,尾数部分有效数字为 1.000 0010 1100 1100 1100 1101,转换成十进制为 1.021875,然后乘以 2 得到 16.35000038。你会发现,计算机实际存储的值可能和真值不同。
0.35 0011 1110 1011 0011 0011 0011 0011 0011 16.35 和 0.35 尾数不同
1.0 0011 1111 1000 0000 0000 0000 0000 0000 127-127=0 即 2=1,尾数部分为 1.0
0.9 0011 1111 0110 0110 0110 0110 0110 0110 126 -127 = -1 即 2=0.5,尾数部分有效数字为 1.11001100110011001100110,转成十进制为 1.7999999523162842,然后乘以 0.5 得到 0.899999976158142,你会发现 0.9 并不能用有限二进制位进行精确表示。

加减运算

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

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

1.0 – 0.9 运算过程说明

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

    public static void main(String[] args) {

        System.out.println(1.0f - 0.9f);
        
    }

答案是:0.100000024

0.100000024

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

1.0 的二进制为:  0011 1111 1000 0000 0000 0000 0000 0000
-0.9 的二进制为:1011 1111 0110 0110 0110 0110 0110 0110

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

浮点数 符号 阶码 尾数(实际值) 尾数补码
1.0 0 127 1000 0000 0000 0000 0000 0000 1000 0000 0000 0000 0000 0000
-0.9 1 126 1110 0110 0110 0110 0110 0110 0001 1001 1001 1001 1001 1010

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

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

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

符号位  尾数位
0  1000 0000 0000 0000 0000 0000
1  1000 1100 1100 1100 1100 1101
---------------------------------------
0  0000 1100 1100 1100 1100 1101

最左端是符号位,计算结果为 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,其实在浮点数的世界里容易被你忽略甚至用错的点还很多,比如判断两个浮点数是否相等,如果直接用 == 是会让程序出错的。限于篇幅,另外蜗牛的认知还不够深,就没继续展开。

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

图片

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

推荐阅读:

1120页的Leetcode算法刷题笔记,完整版PDF开放下载!

《Java 工程师成神之路》.pdf

更多内容请收藏:Java for You

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

作者: 蜗牛

联系我们

联系我们

公众号:蜗牛互联网

在线咨询: QQ交谈

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

微信扫一扫关注我们

关注微博
返回顶部