入门篇-其之五-Java运算符(上)

一元运算符之正负号

Java支持多种一元运算符,一元运算符中的“一元”是指一个操作数。我们初中学过的正负号就属于一元运算符,因为正负号后面只有一个数字。

正数使用+表示,其中+可以省略;负数使用-表示。如果变量的值是数值类型,也可以在变量前面加上正负号。

/**
 * 正负号的表示
 *
 * @author iCode504
 * @date 2023-10-06 19:49
 */
public class PlusAndMinusSign {
    public static void main(String[] args) {
        int intValue1 = 20;    // 正数,加号可忽略
        int intValue2 = -40;    // 负数
        System.out.println("intValue1 = " + intValue1);
        System.out.println("intValue2 = " + intValue2);

        // 变量的前面也可以加上正负号
        int intValue3 = 40;
        int intValue4 = -intValue3;
        System.out.println("intValue3 = " + intValue3);
        System.out.println("intValue4 = " + intValue4);

        // 加上符号的变量也可以参与运算,以下两个变量相乘得到的结果是相同的
        int intValue5 = intValue3 * intValue4;  // 推荐写法
        int intValue6 = intValue3 * -intValue3;     // 不推荐,可读性变差
        System.out.println("intValue5 = " + intValue5);
        System.out.println("intValue6 = " + intValue6);

        // 负数前面加上负号为正数(负负得正)
        int intValue7 = -(-20);
        int intValue8 = -intValue4;     // intValue4本身的值就是负数
        System.out.println("intValue7 = " + intValue7);
        System.out.println("intValue8 = " + intValue8);
    }
}

运行结果:

image-20231006200450682

根据intValue7intValue8的输出结果我们可以得知,负号可以改变数值的正负,正数加了负号变负数,负数加负号可以变正数(负负得正)。

编写代码不推荐int intValue6 = intValue3 * -intValue3;这种写法,虽然能得到预期结果,但是右侧计算的表达式可读性变差,可能会造成误解。

算数运算符

算术运算符的基本使用

在大多数编程语言中,算术运算符基本上由+、减-、乘*、除/、取余%(也称“取模”,也就是两个数相除的余数)组成,以上五个运算符在Java中也完全适用。

/**
 * 算术运算符--加减乘除、取余
 *
 * @author iCode504
 * @date 2023-10-08 7:01
 */
public class MathOperators1 {
    public static void main(String[] args) {
        int intValue1 = 22;
        int intValue2 = 5;

        // 加减乘除运算
        int result1 = intValue1 + intValue2;
        System.out.println("intValue1 + intValue2 = " + result1);
        int result2 = intValue1 - intValue2;
        System.out.println("intValue1 - intValue2 = " + result2);
        int result3 = intValue1 * intValue2;
        System.out.println("intValue1 * intValue2 = " + result3);
        // 两个整除相除,只保留整数部分,不会进行四舍五入操作
        int result4 = intValue1 / intValue2;
        System.out.println("intValue1 / intValue2 = " + result4);
        // 两个整数取余:22对5取余得到的结果是2
        int result5 = intValue1 % intValue2;
        System.out.println("intValue1 % intValue2 = " + result5);
    }
}

运行结果:

image-20231008071109661

两个整数运算得到的结果是整数,两个浮点数运算得到的结果是浮点数,整数和浮点数进行运算时得到的结果是浮点数(因为整数类型会自动提升为浮点类型)。

/**
 * 整数和浮点数的运算、byte/short/char类型的运算
 *
 * @author iCode504
 * @date 2023-09-28 15:47:46
 */
public class MathOperators2 {
    public static void main(String[] args) {
        // 定义两个变量intValue1,intValue2并赋值
        int intValue1 = 20;
        int intValue2 = 40;
        // 直接输出intValue1和intValue2相加的和
        // 注意:下方输出时,需要对要计算的表达式加上括号,防止intValue1和intValue2转换成字符串类型
        System.out.println("intValue1 + intValue2 = " + (intValue1 + intValue2));

        System.out.println("----------分割线----------");
        // byte、short、char进行运算时,会自动提升为int类型计算。
        // 如果转换成想要的小范围数据类型,需要进行强制类型转换
        byte byteValue = 30;
        short shortValue = 50;
        char charValue = 30;
        // 错误写法:
        // byte byteValue1 = byteValue + shortValue;
        // 正确写法: 将计算的结果转换成小范围数据类型。注意:强制类型转换时需要考虑到数据溢出的问题。
        byte byteValue1 = (byte) (byteValue + shortValue);
        short shortValue1 = (short) (shortValue + charValue);
        char charValue1 = (char) (byteValue + charValue);       // 得到的结果是Unicode字符表中对应的字符
        System.out.println("byteValue1 = " + byteValue1);
        System.out.println("shortValue1 = " + shortValue1);
        System.out.println("charValue1 = " + charValue1);
        System.out.println("----------分割线----------");
        // 浮点数参与计算:整数会自动提升为浮点类型
        double doubleValue1 = 0.1;
        double doubleValue2 = 0.2;
        int intValue3 = 30;
        System.out.println("doubleValue1 + intValue3 = " + (doubleValue1 + intValue3));
        System.out.println("doubleValue1 + doubleValue2 = " + (doubleValue1 + doubleValue2));
    }
}

运行结果:

image-20231008072150038

浮点数计算为什么不准确?

从上述结果我们发现一个问题,double类型的值0.10.2相加得到的结果并不是0.3,而是0.30000000000000004,为什么?

假设有两个浮点数0.10.2,如果两个值赋值给float类型和double类型,相加计算是不是0.3?

我们使用Java代码来测试一下:

/**
 * 浮点数0.1和0.2分别使用float类型和double类型计算
 *
 * @author iCode504
 * @date 2023-10-06 17:00:21
 */
public class DecimalCalculation1 {
    public static void main(String[] args) {
        // float类型相加计算
        float floatValue1 = 0.1f;
        float floatValue2 = 0.2f;
        System.out.println("floatValue1 + floatValue2 = " + (floatValue1 + floatValue2));
        // double类型相加计算
        double doubleValue1 = 0.1;
        double doubleValue2 = 0.2;
        System.out.println("doubleValue1 + doubleValue2 = " + (doubleValue1 + doubleValue2));
        
        double doubleValue3 = 0.5;
        double doubleValue4 = 0.8;
        System.out.println("doubleValue3 + doubleValue4 = " + (doubleValue3 + doubleValue4));
    }
}

运行结果:

image-20231006193753117

此时发现一个问题:doubleValue1 + doubleValue2 = 0.30000000000000004并没有得到我们预期的结果,为什么?

事实上,0.1 + 0.2的结果在大多数编程语言中进行运算时也会得到上述结果,点我查看

众所周知,计算机在底层计算使用的是二进制。无论是整数还是浮点数都会转换成二进制数进行运算。以下是小数转为二进制数运算的基本流程

flowchart LR 十进制数 --> 二进制数 --> 科学计数法形式表示二进制数 --> 指数补齐 --> 二进制数相加 --> 还原成十进制数

十进制小数转为二进制小数

小数转为二进制数的规则是:将小数乘以2,然后取整数部分作为二进制数的一部分,然后再将小数部分继续乘以2,再取整数部分,以此类推,直到小数部分为0所达到的精度。

将0.2转换成二进制:

\[0.2 \times 2 = 0.4 \to 取整数部分0 \]

\[0.4 \times 2 = 0.8 \to 取整数部分0 \]

\[0.8 \times 2 = 1.6 \to 取整数部分1 \]

\[0.6 \times 2 = 1.2\to取整数部分1 \]

\[0.2 \times 2 = 0.4\to整数部分为0 \]

此时我们发现,我们对得到的小数怎么乘以2,小数位永远都不是0。因此,使用计算器计算0.2得到的二进制数字为

\[0.00110011...(无限循环0011) \]

同理,0.1转换成二进制数是:

\[0.000110011...(无限循环0011) \]

二进制小数转为科学计数法表示

当然,计算机不能存储无限循环小数。Java的double是双精度浮点类型,64位,因此在存储时使用64位存储double浮点数。要想表示尽可能大的数据,就需要使用到科学计数法来表示数据。

十进制和二进制数都可以转换成相应的科学计数法来表示。

十进制的科学计数法的表示方式是整数只留个位数,且个位数主要是1到9,通过乘以10的指数来表示。例如:89999用科学计数法表示为\(8.9999\times10^4\),0.08586用十进制科学计数法表示为\(8.586\times10^{-2}\)

二进制的科学计数法的表示方式和十进制的类似。它的个位数使用1来表示,通过乘以2的指数来表示

例如,0.1的二进制数转换成科学计数法表示,小数点需要向右移动4位得到整数部分1;同理,0.2需要向右移动3位。因此0.1和0.2的二进制用科学计数法表示如下:

\[1.10011...\times2^{-4}(0011无限循环) \]

\[1.10011...\times2^{-3}(0011无限循环) \]

科学计数法的数据转成二进制表示

Java的double类型是双精度浮点数,IEEE 754标准对64位浮点数做出了如下的规定:

  • 最高1位是符号位,0表示正号,1表示负号。
  • 其后面的11位用来存储科学计数法中指数的二进制。以上述二进制科学计数法为例,这11位数字存储的就是-4的二进制。
  • 剩下的52位存储二进制科学计数法中小数点的后52位。以上述二进制科学计数法为例,存储的就是10011...之后的52位数字。
image-20231009084527660

既然内存已经给出了11位用于表示指数。那么转换成十进制数默认范围就是\([0, 2^{11}]\),即\([0,2048]\)。但此时还有一个问题,以上述的二进制科学计数法为例,它的指数是-4,是负数,如何表示负数?需要在11位的头部在单独拿出一位来表示吗?

并不是,IEEE 754标准将指数为0的基数定为1023(1是1024,相当于存储\([-1023,1024]\)范围的数),指数-4会转换成1023 - 4 = 1019,再将1019转换成二进制:1111111011,前面我们说过,指数为11位,需要在前面补零,得到的结果为:01111111011

剩下的52位也需要处理,但是二进制科学计数法的小数部分也是一个无限循环小数。此时就需要进行舍入计算,0舍1入(类似四舍五入),舍入计算会让数据丢失精度

此时得到的0.1的二进制:

\[0\ 01111111011\ 1001100110011001100110011001100110011001100110011010 \]

0.2的二进制如下:

\[0\ 01111111100\ 1001100110011001100110011001100110011001100110011010 \]

此时需要对二进制科学计数法提取公因数,为了减少精度损失,遵循小指数转换成大指数的原则。这里较大的指数是-3,因此需要将0.1的二进制科学计数法再乘以2,得到结果如下:

\[0\ 01111111011\ (0.)100110011001100110011001100110011001100110011001101 \]

0.1原有的最后一位需要舍去,让给小数点前的0。此时0.1和0.2的二进制的指数均为-3、

此时0.1+0.2的小数部分得到的结果是:

\[10.0110011001100110011001100110011001100110011001100111 \]

指数补齐

根据上述结果,我们会发现两个问题:

  • 整数部分不符合科学计数法的规则。
  • 二进制数整体得到的结果超过52位。

首先需要将将结果转换成二进制科学计数法,小数点向左移动一位(相当于乘以2):

\[1.00110011001100110011001100110011001100110011001100111 \]

指数部分也需要加1,因为指数由-3(1020)变为-2(1021)

\[01111111101 \]

根据0舍1入的原则,将超出52位的小数部分做舍入计算,得到的结果为:

\[0\ 01111111101\ (1.)0011001100110011001100110011001100110011001100110100 \]

还原成十进制数

将二进制科学计数法转换成正常的二进制数,原有的指数是-2,还原时小数点需向左移动两位:

\[0.010011001100110011001100110011001100110011001100110100 \]

再转换为十进制为:

\[0.30000000000000004 \]

经过上述的复杂推导,我们可以总结出一个结论:使用基本数据类型的浮点数进行运算并不准确(尤其是在金融货币领域对小数点精度要求比较高的不能使用)。那么,有什么办法可以解决浮点数计算不准确的问题?

方法一(现阶段推荐):转换成整数计算,得到结果再除以10的n次方

还是以0.1 + 0.2为例,我们可以转换成整数计算,整数计算的结果再除以10,示例代码如下:

/**
 * 浮点数计算: 计算0.1 + 0.2的精确结果
 *
 * @author iCode504
 * @date 2023-10-09 18:13:35
 */
public class DecimalCalculation2 {
    public static void main(String[] args) {
        double doubleValue1 = 0.1;
        double doubleValue2 = 0.2;
        // 将doubleValue1和doubleValue2转换成整数
        int tempValue1 = (int) (doubleValue1 * 10);
        int tempValue2 = (int) (doubleValue2 * 10);
        int tempResult = tempValue1 + tempValue2;
        double result = (double) tempResult / 10;
        System.out.println("result = " + result);
    }
}

运行结果:

image-20231009222458278

此时能得到精确的结果。

方法二:使用BigDecimal类(这个类后续会讲到,小白可以直接跳过)精确运算

import java.math.BigDecimal;

/**
 * 使用BigDecimal类精确计算浮点数
 *
 * @author iCode504
 * @date 2023-10-09 22:26
 */
public class DecimalCalculation3 {
    public static void main(String[] args) {
        double doubleValue1 = 0.1;
        double doubleValue2 = 0.2;

        // 将double类型的值转换成字符串
        String doubleValueString1 = String.valueOf(doubleValue1);
        String doubleValueString2 = String.valueOf(doubleValue2);

        // 使用BigDecimal类进行运算
        BigDecimal decimal1 = new BigDecimal(doubleValueString1);
        BigDecimal decimal2 = new BigDecimal(doubleValueString2);
        BigDecimal resultDecimal = decimal1.add(decimal2);
        double result = resultDecimal.doubleValue();
        System.out.println("result = " + result);
    }
}

运行结果:

image-20231009223129893

负数的除法和取余规则

负数的除法规则:两个负数相除得到的结果是正数,正数除以负数或者负数除以整数结果是负数

/**
 * 负数的除法运算
 *
 * @author iCode504
 * @date 2023-10-07 19:57
 */
public class DivideOperators {
    public static void main(String[] args) {
        int intValue1 = 20;
        int intValue2 = -10;
        int intValue3 = 5;
        int intValue4 = -5;

        // 情况一:被除数为正数,除数为负数,得到的结果是负数
        int result1 = intValue1 / intValue2;
        System.out.println("result1 = " + result1);

        // 情况二:被除数为负数,除数为正数,得到的结果是负数
        int result2 = intValue2 / intValue3;
        System.out.println("result2 = " + result2);

        // 情况三:被除数和除数都是负数,得到的结果是正数
        int result3  = intValue2 / intValue4;
        System.out.println("result3 = " + result3);
    }
}

运行结果:

image-20231007221818423

负数的取余规则:被除数如果是正数,求余的结果就是正数;反之,结果为负数

/**
 * 负数的取余运算
 *
 * @author iCode504
 * @date 2023-10-07 22:12
 */
public class ModOperators {
    public static void main(String[] args) {
        int intValue1 = 20;
        int intValue2 = -13;
        int intValue3 = 7;
        int intValue4 = -3;

        // 情况一:被除数为正数,除数为负数,得到的结果是正数
        int result1 = intValue1 % intValue2;
        System.out.println("result1 = " + result1);

        // 情况二:被除数为负数,除数为正数,得到的结果是负数
        int result2 = intValue2 % intValue3;
        System.out.println("result2 = " + result2);

        // 情况三:被除数和除数都是负数,得到的结果是负数
        int result3 = intValue2 % intValue4;
        System.out.println("result3 = " + result3);
    }
}

运行结果:

image-20231007222009322

赋值运算符

赋值运算符=

我们知道,创建Java变量的一般语法是:数据类型 变量名 = 变量值。其中=是赋值运算符,它的作用是将右侧的值赋值给左边的变量

  • 变量值一般是:常量、已经赋值的变量名或者是可以计算出新数值的表达式。
  • 赋值运算符=左侧的变量名唯一。

基本数据类型的变量可以直接赋值,因为基本数据类型保存的是实际值。

/**
 * 赋值运算符 = 的基本使用
 *
 * @author iCode504
 * @date 2023-10-06 6:40
 */
public class AssignmentOperator1 {
    public static void main(String[] args) {
        // 将20赋值给number1
        int number1 = 20;
        System.out.println("number1 = " + number1);
        // 将已经赋值的变量名number1赋值给number2
        int number2 = number1;
        System.out.println("number2 = " + number2);
        // 可以计算出新数值的表达式赋值给新变量
        int number3 = 30 + 40;
        System.out.println("number3 = " + number3);
        int number4 = number1 + number2;
        System.out.println("number4 = " + number4);
    }
}

运算结果:

image-20231006064803996

number1number2的输出结果可知:变量number1存储的值20赋值给了number2,此时number2的值也是20。

变量number3number4右侧是可以计算的表达式,即30 + 40能够直接计算出结果,前面已经赋值的number1 + number2也能计算出结果。

引用数据类型存储的是一个地址值引用。例如:ObjectString是类,属于引用数据类型。此时我们创建这两个类型的对象并赋值给变量,然后直接输出变量。

/**
 * 赋值运算符--引用数据类型变量赋值并输出
 *
 * @author iCode504
 * @date 2023-10-06 23:50
 */
public class AssignmentOperator2 {
    public static void main(String[] args) {
        // 第一组:创建两个Object对象分别赋值给object1和object2
        Object object1 = new Object();
        Object object2 = new Object();
        // 输出两个地址值
        System.out.println("object1 = " + object1);
        System.out.println("object2 = " + object2);

        System.out.println("--------------------");
        // 第二组:让object1指向object2
        object2 = object1;
        System.out.println("object1 = " + object1);
        System.out.println("object2 = " + object2);

        System.out.println("--------------------");
        // 第三组:创建两个String对象分别赋值给string1和string2
        String string1 = new String();
        String string2 = new String();
        System.out.println("string1 = " + string1);
        System.out.println("string2 = " + string2);
    }
}

运行结果:

image-20231007065141822

前两组输出结果的格式我们发现,它们是以java.lang.Object@和变量在物理内存中的地址(十六进制数)。

  • 其中java.lang.Object叫做全限定类名。全限定类名是指当前类所属的包名(包名会在后续文章中讲到)和类名组成。Object是类名,java.langObject类所在的包名。
  • @后面的就是变量在内存中的存储地址。如果你使用上述命令将代码输出,那么得到的地址值和上述的内容不同,因为变量的地址值是内存随机分配的。

第一组的object1object2分别创建了Object对象,相当于在栈内存和堆内存中分别开辟了两块不同的空间,栈内存中存储的变量地址和堆内存中开辟的内存地址一一对应,因此object1object2的地址值不同。第一组的object1object2在内存的表现形式如下:

image-20231007071809712

第二组,我们发现object1赋值给了object2,在栈内存中的表现形式是当前变量object2的地址值赋值给object1。原来object2在堆内存中创建的对象不再被引用,虚拟机后续会对此对象进行回收。

image-20231007215157582

我们发现第三组两个String对象的输出结果什么都看不到,它们也是引用数据类型,难道不输出地址值吗?事实上,在源码层面,String做了进一步处理。

我们使用new String()创建对象时,会调用String的构造器(构造器,也叫做构造方法,后续会讲到),打开源码观察这个构造器:

image-20231007220335076

在调用空参构造器时就已经初始化一个空字符串值了,因此我们在输出String对象时输出的是空字符串,此时我们看不到任何内容就显得比较合理了。

其他赋值运算符

假设有一个int类型变量intValue的值是20,此时我在此基础上再加上20再赋值给intValue,得到的表达式如下:

int intValue = 20;
intValue = intValue + 20;		// 此时intValue的结果为40

Java给我们提供了+=运算符可以简化当前的代码intValue = intValue + 20;,使用+=可以简化成如下形式:

int intValue = 20;
intValue += 20;		// 得到的结果也是40,相当于intValue = intValue + 20;

除了+=以外,-=*=/=%=的作用机制和+=完全相同。

赋值运算符 说明 使用
+= 加并赋值运算符:先相加,得到的结果再赋值 i = i + 20可以简写成i += 20
-= 减并赋值运算符:先相减,得到的结果再赋值 i = i - 20可以简写成i -= 20
*= 乘并赋值运算符:先相乘,得到的结果再赋值 i = i * 20可以简写成i *= 20
/= 除并赋值运算符:先相除,得到的结果再赋值 i = i / 20可以简写成i /= 20
%= 取余并赋值运算符:先取余,得到的结果再赋值 i = i % 20可以简写成i %= 20

以下是5个运算符在代码中的应用:

/**
 * 其他赋值运算符+=、-=、*=、/=和%=的使用
 *
 * @author iCode504
 * @date 2023-10-07 20:14
 */
public class AssignmentOperator3 {
    public static void main(String[] args) {
        int intValue1 = 20;
        int intValue2 = 30;
        int intValue3 = 40;
        int intValue4 = 50;
        int intValue5 = 60;

        intValue1 += 30;
        intValue2 -= 40;
        intValue3 *= 50;
        intValue4 /= 10;
        intValue5 %= 7;
        System.out.println("intValue1 = " + intValue1);
        System.out.println("intValue2 = " + intValue2);
        System.out.println("intValue3 = " + intValue3);
        System.out.println("intValue4 = " + intValue4);
        System.out.println("intValue5 = " + intValue5);
    }
}

运行结果:

image-20231007203213625

byteshortchar三者使用上述赋值运算符时,不需要进行强制类型转换:

/**
 * byte、short、char使用赋值运算符
 *
 * @author iCode504
 * @date 2023-10-07 20:34
 */
public class AssignmentOperator4 {
    public static void main(String[] args) {
        byte byteValue1 = 30;
        byte byteValue2 = 40;
        short shortValue = 10;
        char charValue = 'a';

        byteValue1 += byteValue2;
        System.out.println("byteValue1 = " + byteValue1);
        byteValue1 += 10;
        System.out.println("byteValue2 = " + byteValue2);

        charValue += byteValue1;
        shortValue += charValue;
        byteValue2 += shortValue;
        System.out.println("charValue = " + charValue);
        System.out.println("shortValue = " + shortValue);
        System.out.println("byteValue2 = " + byteValue2);
    }
}

运行结果:

image-20231007211517616

使用赋值运算符的优势包括:

1. 简洁性:使用+=可以在一行内同时完成加法计算和赋值操作,让代码更加简洁。例如:i += 20就是i = i + 20的简化写法(其他赋值运算符亦同理)。

2. 性能优势:在某些情况下,赋值运算符要比单独的加法和赋值操作更快。

总的来说,使用赋值运算符可以增加代码的简洁性,提高性能,并使代码更易于阅读和理解。


参考资料:

0.1 + 0.2为什么不等于0.3?

0.1+0.2为什么不等于0.3,以及怎么等于0.3

0.1 + 0.2 为什么不等于 0.3???

posted @ 2023-10-09 22:47  iCode504  阅读(88)  评论(0编辑  收藏  举报