本文中使用到的工具是JDK 8,需要安装的小伙伴请点击右侧连接查看教程:点我查看安装JDK8教程。
一、一元运算符之正负号
Java支持多种一元运算符,一元运算符中的“一元”是指一个操作数。我们初中学过的正负号就属于一元运算符,因为正负号后面只有一个数字。
正数使用+
表示,其中+
可以省略;负数使用-
表示。如果变量的值是数值类型,也可以在变量前面加上正负号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
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; System.out.println("intValue7 = " + intValue7); System.out.println("intValue8 = " + intValue8); } }
|
运行结果:

根据intValue7
和intValue8
的输出结果我们可以得知,负号可以改变数值的正负,正数加了负号变负数,负数加负号可以变正数(负负得正)。
编写代码不推荐int intValue6 = intValue3 * -intValue3;
这种写法,虽然能得到预期结果,但是右侧计算的表达式可读性变差,可能会造成误解。
二、算数运算符
2.1 算术运算符的基本使用
在大多数编程语言中,算术运算符基本上由**加+
、减-
、乘*
、除/
、取余%
**(也称“取模”,也就是两个数相除的余数)组成,以上五个运算符在Java中也完全适用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
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); int result5 = intValue1 % intValue2; System.out.println("intValue1 % intValue2 = " + result5); } }
|
运行结果:

两个整数运算得到的结果是整数,两个浮点数运算得到的结果是浮点数,整数和浮点数进行运算时得到的结果是浮点数(因为整数类型会自动提升为浮点类型)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
public class MathOperators2 { public static void main(String[] args) { int intValue1 = 20; int intValue2 = 40; System.out.println("intValue1 + intValue2 = " + (intValue1 + intValue2));
System.out.println("----------分割线----------"); byte byteValue = 30; short shortValue = 50; char charValue = 30; byte byteValue1 = (byte) (byteValue + shortValue); short shortValue1 = (short) (shortValue + charValue); char charValue1 = (char) (byteValue + charValue); 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)); } }
|
运行结果:

2.2 浮点数计算为什么不准确?
从上述结果我们发现一个问题,double
类型的值0.1
和0.2
相加得到的结果并不是0.3
,而是0.30000000000000004
,为什么?
假设有两个浮点数0.1
和0.2
,如果两个值赋值给float
类型和double
类型,相加计算是不是0.3?
我们使用Java代码来测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
public class DecimalCalculation1 { public static void main(String[] args) { float floatValue1 = 0.1f; float floatValue2 = 0.2f; System.out.println("floatValue1 + floatValue2 = " + (floatValue1 + floatValue2)); 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)); } }
|
运行结果:

此时发现一个问题:doubleValue1 + doubleValue2 = 0.30000000000000004
并没有得到我们预期的结果,为什么?
事实上,0.1 + 0.2
的结果在大多数编程语言中进行运算时也会得到上述结果,点我查看
众所周知,计算机在底层计算使用的是二进制。无论是整数还是浮点数都会转换成二进制数进行运算。以下是小数转为二进制数运算的基本流程
flowchart LR
十进制数 --> 二进制数 --> 科学计数法形式表示二进制数 --> 指数补齐 --> 二进制数相加 --> 还原成十进制数
2.2.1 十进制小数转为二进制小数
小数转为二进制数的规则是:将小数乘以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)
$$
2.2.2 二进制小数转为科学计数法表示
当然,计算机不能存储无限循环小数。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无限循环)
$$
2.2.3 科学计数法的数据转成二进制表示
Java的double类型是双精度浮点数,IEEE 754标准对64位浮点数做出了如下的规定:
- 最高1位是符号位,0表示正号,1表示负号。
- 其后面的11位用来存储科学计数法中指数的二进制。以上述二进制科学计数法为例,这11位数字存储的就是-4的二进制。
- 剩下的52位存储二进制科学计数法中小数点的后52位。以上述二进制科学计数法为例,存储的就是
10011...
之后的52位数字。
既然内存已经给出了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
$$
2.2.4 指数补齐
根据上述结果,我们会发现两个问题:
- 整数部分不符合科学计数法的规则。
- 二进制数整体得到的结果超过52位。
首先需要将将结果转换成二进制科学计数法,小数点向左移动一位(相当于乘以2):
$$
1.00110011001100110011001100110011001100110011001100111
$$
指数部分也需要加1,因为指数由-3(1020)变为-2(1021)
$$
01111111101
$$
根据0舍1入的原则,将超出52位的小数部分做舍入计算,得到的结果为:
$$
0\ 01111111101\ (1.)0011001100110011001100110011001100110011001100110100
$$
2.2.5 还原成十进制数
将二进制科学计数法转换成正常的二进制数,原有的指数是-2,还原时小数点需向左移动两位:
$$
0.010011001100110011001100110011001100110011001100110100
$$
再转换为十进制为:
$$
0.30000000000000004
$$
经过上述的复杂推导,我们可以总结出一个结论:使用基本数据类型的浮点数进行运算并不准确(尤其是在金融货币领域对小数点精度要求比较高的不能使用)。那么,有什么办法可以解决浮点数计算不准确的问题?
方法一(现阶段推荐):转换成整数计算,得到结果再除以10的n次方。
还是以0.1 + 0.2为例,我们可以转换成整数计算,整数计算的结果再除以10,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public class DecimalCalculation2 { public static void main(String[] args) { double doubleValue1 = 0.1; double doubleValue2 = 0.2; int tempValue1 = (int) (doubleValue1 * 10); int tempValue2 = (int) (doubleValue2 * 10); int tempResult = tempValue1 + tempValue2; double result = (double) tempResult / 10; System.out.println("result = " + result); } }
|
运行结果:

此时能得到精确的结果。
方法二:使用BigDecimal
类(这个类后续会讲到,小白可以直接跳过)精确运算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import java.math.BigDecimal;
public class DecimalCalculation3 { public static void main(String[] args) { double doubleValue1 = 0.1; double doubleValue2 = 0.2;
String doubleValueString1 = String.valueOf(doubleValue1); String doubleValueString2 = String.valueOf(doubleValue2);
BigDecimal decimal1 = new BigDecimal(doubleValueString1); BigDecimal decimal2 = new BigDecimal(doubleValueString2); BigDecimal resultDecimal = decimal1.add(decimal2); double result = resultDecimal.doubleValue(); System.out.println("result = " + result); } }
|
运行结果:

2.6 负数的除法和取余规则
负数的除法规则:两个负数相除得到的结果是正数,正数除以负数或者负数除以整数结果是负数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
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); } }
|
运行结果:

负数的取余规则:被除数如果是正数,求余的结果就是正数;反之,结果为负数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
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); } }
|
运行结果:

三、赋值运算符
3.1 赋值运算符=
我们知道,创建Java变量的一般语法是:数据类型 变量名 = 变量值。其中=
是赋值运算符,它的作用是将右侧的值赋值给左边的变量。
- 变量值一般是:常量、已经赋值的变量名或者是可以计算出新数值的表达式。
- 赋值运算符
=
左侧的变量名唯一。
基本数据类型的变量可以直接赋值,因为基本数据类型保存的是实际值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public class AssignmentOperator1 { public static void main(String[] args) { int number1 = 20; System.out.println("number1 = " + number1); 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); } }
|
运算结果:

由number1
和number2
的输出结果可知:变量number1
存储的值20赋值给了number2
,此时number2
的值也是20。
变量number3
和number4
右侧是可以计算的表达式,即30 + 40
能够直接计算出结果,前面已经赋值的number1 + number2
也能计算出结果。
引用数据类型存储的是一个地址值引用。例如:Object
和String
是类,属于引用数据类型。此时我们创建这两个类型的对象并赋值给变量,然后直接输出变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
public class AssignmentOperator2 { public static void main(String[] args) { Object object1 = new Object(); Object object2 = new Object(); System.out.println("object1 = " + object1); System.out.println("object2 = " + object2);
System.out.println("--------------------"); object2 = object1; System.out.println("object1 = " + object1); System.out.println("object2 = " + object2);
System.out.println("--------------------"); String string1 = new String(); String string2 = new String(); System.out.println("string1 = " + string1); System.out.println("string2 = " + string2); } }
|
运行结果:

前两组输出结果的格式我们发现,它们是以java.lang.Object
、@
和变量在物理内存中的地址(十六进制数)。
- 其中
java.lang.Object
叫做全限定类名。全限定类名是指当前类所属的包名(包名会在后续文章中讲到)和类名组成。Object
是类名,java.lang
是Object
类所在的包名。
@
后面的就是变量在内存中的存储地址。如果你使用上述命令将代码输出,那么得到的地址值和上述的内容不同,因为变量的地址值是内存随机分配的。
第一组的object1
和object2
分别创建了Object对象,相当于在栈内存和堆内存中分别开辟了两块不同的空间,栈内存中存储的变量地址和堆内存中开辟的内存地址一一对应,因此object1
和object2
的地址值不同。第一组的object1
和object2
在内存的表现形式如下:
第二组,我们发现object1
赋值给了object2
,在栈内存中的表现形式是当前变量object2
的地址值赋值给object1
。原来object2
在堆内存中创建的对象不再被引用,虚拟机后续会对此对象进行回收。
我们发现第三组两个String
对象的输出结果什么都看不到,它们也是引用数据类型,难道不输出地址值吗?事实上,在源码层面,String
做了进一步处理。
我们使用new String()
创建对象时,会调用String
的构造器(构造器,也叫做构造方法,后续会讲到),打开源码观察这个构造器:
在调用空参构造器时就已经初始化一个空字符串值了,因此我们在输出String
对象时输出的是空字符串,此时我们看不到任何内容就显得比较合理了。
3.2 其他赋值运算符
假设有一个int
类型变量intValue
的值是20,此时我在此基础上再加上20再赋值给intValue
,得到的表达式如下:
1 2
| int intValue = 20; intValue = intValue + 20;
|
Java给我们提供了+=
运算符可以简化当前的代码intValue = intValue + 20;
,使用+=
可以简化成如下形式:
1 2
| int intValue = 20; 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个运算符在代码中的应用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
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); } }
|
运行结果:

byte
、short
、char
三者使用上述赋值运算符时,不需要进行强制类型转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
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); } }
|
运行结果:

使用赋值运算符的优势包括:
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???