AI智能
改变未来

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


这是蜗牛互联网的第 112 期原创。

作者 | 白色蜗牛

来源 | 蜗牛互联网(ID: woniu_internet)

转载请联系授权(微信ID: 919201148)

本文大纲:

在上一篇文章 很清晰!带你图解 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,其实在浮点数的世界里容易被你忽略甚至用错的点还很多,比如判断两个浮点数是否相等,如果直接用

    ==

    是会让程序出错的。限于篇幅,另外蜗牛的认知还不够深,就没继续展开。

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

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

    我是蜗牛,大厂程序员,专注技术原创和个人成长,正在互联网上摸爬滚打。[strong]欢迎关注[/strong]我,和蜗牛一起成长,我们一起牛~下期见!

    赞(0) 打赏
    未经允许不得转载:爱站程序员基地 » Java 基本类型的各种运算,你真的了解了么?