一元运算符之正负号
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);
}
}
运行结果:
根据intValue7
和intValue8
的输出结果我们可以得知,负号可以改变数值的正负,正数加了负号变负数,负数加负号可以变正数(负负得正)。
编写代码不推荐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);
}
}
运行结果:
两个整数运算得到的结果是整数,两个浮点数运算得到的结果是浮点数,整数和浮点数进行运算时得到的结果是浮点数(因为整数类型会自动提升为浮点类型)。
/**
* 整数和浮点数的运算、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));
}
}
运行结果:
浮点数计算为什么不准确?
从上述结果我们发现一个问题,double
类型的值0.1
和0.2
相加得到的结果并不是0.3
,而是0.30000000000000004
,为什么?
假设有两个浮点数0.1
和0.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));
}
}
运行结果:
此时发现一个问题:doubleValue1 + doubleValue2 = 0.30000000000000004
并没有得到我们预期的结果,为什么?
事实上,0.1 + 0.2
的结果在大多数编程语言中进行运算时也会得到上述结果,点我查看
众所周知,计算机在底层计算使用的是二进制。无论是整数还是浮点数都会转换成二进制数进行运算。以下是小数转为二进制数运算的基本流程
flowchart LR
十进制数 --> 二进制数 --> 科学计数法形式表示二进制数 --> 指数补齐 --> 二进制数相加 --> 还原成十进制数
十进制小数转为二进制小数
小数转为二进制数的规则是:将小数乘以2,然后取整数部分作为二进制数的一部分,然后再将小数部分继续乘以2,再取整数部分,以此类推,直到小数部分为0所达到的精度。
将0.2转换成二进制:
0.2×2=0.4→取整数部分00.2 times 2 = 0.4 to 取整数部分00.2×2=0.4→取整数部分0
0.4×2=0.8→取整数部分00.4 times 2 = 0.8 to 取整数部分00.4×2=0.8→取整数部分0
0.8×2=1.6→取整数部分10.8 times 2 = 1.6 to 取整数部分10.8×2=1.6→取整数部分1
0.6×2=1.2→取整数部分10.6 times 2 = 1.2to取整数部分10.6×2=1.2→取整数部分1
0.2×2=0.4→整数部分为00.2 times 2 = 0.4to整数部分为00.2×2=0.4→整数部分为0
此时我们发现,我们对得到的小数怎么乘以2,小数位永远都不是0。因此,使用计算器计算0.2得到的二进制数字为
0.00110011...(无限循环0011)0.00110011...(无限循环0011)0.00110011...(无限循环0011)
同理,0.1转换成二进制数是:
0.000110011...(无限循环0011)0.000110011...(无限循环0011)0.000110011...(无限循环0011)
二进制小数转为科学计数法表示
当然,计算机不能存储无限循环小数。Java的double
是双精度浮点类型,64位,因此在存储时使用64位存储double
浮点数。要想表示尽可能大的数据,就需要使用到科学计数法来表示数据。
十进制和二进制数都可以转换成相应的科学计数法来表示。
十进制的科学计数法的表示方式是整数只留个位数,且个位数主要是1到9,通过乘以10的指数来表示。例如:89999用科学计数法表示为8.9999×1048.9999times10^48.9999×104,0.08586用十进制科学计数法表示为8.586×10−28.586times10^{-2}8.586×10−2。
二进制的科学计数法的表示方式和十进制的类似。它的个位数使用1来表示,通过乘以2的指数来表示。
例如,0.1的二进制数转换成科学计数法表示,小数点需要向右移动4位得到整数部分1;同理,0.2需要向右移动3位。因此0.1和0.2的二进制用科学计数法表示如下:
1.10011...×2−4(0011无限循环)1.10011...times2^{-4}(0011无限循环)1.10011...×2−4(0011无限循环)
1.10011...×2−3(0011无限循环)1.10011...times2^{-3}(0011无限循环)1.10011...×2−3(0011无限循环)
科学计数法的数据转成二进制表示
Java的double类型是双精度浮点数,IEEE 754标准对64位浮点数做出了如下的规定:
- 最高1位是符号位,0表示正号,1表示负号。
- 其后面的11位用来存储科学计数法中指数的二进制。以上述二进制科学计数法为例,这11位数字存储的就是-4的二进制。
- 剩下的52位存储二进制科学计数法中小数点的后52位。以上述二进制科学计数法为例,存储的就是
10011...
之后的52位数字。
既然内存已经给出了11位用于表示指数。那么转换成十进制数默认范围就是[0,211][0, 2^{11}][0,211],即[0,2048][0,2048][0,2048]。但此时还有一个问题,以上述的二进制科学计数法为例,它的指数是-4,是负数,如何表示负数?需要在11位的头部在单独拿出一位来表示吗?
并不是,IEEE 754标准将指数为0的基数定为1023(1是1024,相当于存储[−1023,1024][-1023,1024][−1023,1024]范围的数),指数-4会转换成1023 - 4 = 1019
,再将1019转换成二进制:1111111011,前面我们说过,指数为11位,需要在前面补零,得到的结果为:01111111011。
剩下的52位也需要处理,但是二进制科学计数法的小数部分也是一个无限循环小数。此时就需要进行舍入计算,0舍1入(类似四舍五入),舍入计算会让数据丢失精度。
此时得到的0.1的二进制:
0 01111111011 10011001100110011001100110011001100110011001100110100 01111111011 10011001100110011001100110011001100110011001100110100 01111111011 1001100110011001100110011001100110011001100110011010
0.2的二进制如下:
0 01111111100 10011001100110011001100110011001100110011001100110100 01111111100 10011001100110011001100110011001100110011001100110100 01111111100 1001100110011001100110011001100110011001100110011010
此时需要对二进制科学计数法提取公因数,为了减少精度损失,遵循小指数转换成大指数的原则。这里较大的指数是-3,因此需要将0.1的二进制科学计数法再乘以2,得到结果如下:
0 01111111011 (0.)1001100110011001100110011001100110011001100110011010 01111111011 (0.)1001100110011001100110011001100110011001100110011010 01111111011 (0.)100110011001100110011001100110011001100110011001101
0.1原有的最后一位需要舍去,让给小数点前的0。此时0.1和0.2的二进制的指数均为-3、
此时0.1+0.2的小数部分得到的结果是:
10.011001100110011001100110011001100110011001100110011110.011001100110011001100110011001100110011001100110011110.0110011001100110011001100110011001100110011001100111
指数补齐
根据上述结果,我们会发现两个问题:
- 整数部分不符合科学计数法的规则。
- 二进制数整体得到的结果超过52位。
首先需要将将结果转换成二进制科学计数法,小数点向左移动一位(相当于乘以2):
1.001100110011001100110011001100110011001100110011001111.001100110011001100110011001100110011001100110011001111.00110011001100110011001100110011001100110011001100111
指数部分也需要加1,因为指数由-3(1020)变为-2(1021)
011111111010111111110101111111101
根据0舍1入的原则,将超出52位的小数部分做舍入计算,得到的结果为:
0 01111111101 (1.)00110011001100110011001100110011001100110011001101000 01111111101 (1.)00110011001100110011001100110011001100110011001101000 01111111101 (1.)0011001100110011001100110011001100110011001100110100
还原成十进制数
将二进制科学计数法转换成正常的二进制数,原有的指数是-2,还原时小数点需向左移动两位:
0.0100110011001100110011001100110011001100110011001101000.0100110011001100110011001100110011001100110011001101000.010011001100110011001100110011001100110011001100110100
再转换为十进制为:
0.300000000000000040.300000000000000040.30000000000000004
经过上述的复杂推导,我们可以总结出一个结论:使用基本数据类型的浮点数进行运算并不准确(尤其是在金融货币领域对小数点精度要求比较高的不能使用)。那么,有什么办法可以解决浮点数计算不准确的问题?
方法一(现阶段推荐):转换成整数计算,得到结果再除以10的n次方。
还是以0.1 + 0.2为例,我们可以转换成整数计算,整数计算的结果再除以10,示例代码如下:
/**
* 浮点数计算: 计算0.1 + 0.2的精确结果
*
* @author ZhaoCong
* @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);
}
}
运行结果:
此时能得到精确的结果。
方法二:使用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);
}
}
运行结果:
负数的除法和取余规则
负数的除法规则:两个负数相除得到的结果是正数,正数除以负数或者负数除以整数结果是负数。
/**
* 负数的除法运算
*
* @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);
}
}
运行结果:
负数的取余规则:被除数如果是正数,求余的结果就是正数;反之,结果为负数。
/**
* 负数的取余运算
*
* @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);
}
}
运行结果:
赋值运算符
赋值运算符=
我们知道,创建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);
}
}
运算结果:
由number1
和number2
的输出结果可知:变量number1
存储的值20赋值给了number2
,此时number2
的值也是20。
变量number3
和number4
右侧是可以计算的表达式,即30 + 40
能够直接计算出结果,前面已经赋值的number1 + number2
也能计算出结果。
引用数据类型存储的是一个地址值引用。例如:Object
和String
是类,属于引用数据类型。此时我们创建这两个类型的对象并赋值给变量,然后直接输出变量。
/**
* 赋值运算符--引用数据类型变量赋值并输出
*
* @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);
}
}
运行结果:
前两组输出结果的格式我们发现,它们是以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
对象时输出的是空字符串,此时我们看不到任何内容就显得比较合理了。
其他赋值运算符
假设有一个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);
}
}
运行结果:
byte
、short
、char
三者使用上述赋值运算符时,不需要进行强制类型转换:
/**
* 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);
}
}
运行结果:
使用赋值运算符的优势包括:
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???