3.7运算符
数学运算是计算机的基本用途之一,Java提供了非常丰富的运算符来支持。我们根据运算的特点和性质,把运算符划分为几组:基本算数运算符、自增自减运算符、关系运算符、位运算符、逻辑运算符、赋值运算符、其他运算符。下面分别介绍。
3.7.1基本算数运算符
在Java中,采用+、-、*、/、%来表示加、减、乘、除、取余(取模),这种运算小学就学过,无需多讲,列表举例如下:
运算非常简单,但是还是有一些问题需要注意,下面分别用实例来说明。
3.7.1.1类型变化
我们看一段代码:
1 public static void main(String[] args) { 2 int a1 = 15; 3 double a2 = 15; 4 int b = 2; 5 int c = 0; 6 System.out.println("整数运算:"); 7 System.out.println("a1 + b = " + (a1 + b)); 8 System.out.println("a1 - b = " + (a1 - b)); 9 System.out.println("a1 * b = " + (a1 * b)); 10 System.out.println("a1 / b = " + (a1 / b)); 11 System.out.println("a1 % b = " + (a1 % b)); 12 System.out.println("浮点数运算:"); 13 System.out.println("a2 + b = " + (a2 + b)); 14 System.out.println("a2 - b = " + (a2 - b)); 15 System.out.println("a2 * b = " + (a2 * b)); 16 System.out.println("a2 / b = " + (a2 / b)); 17 System.out.println("a2 % b = " + (a2 % b)); 18 }
运行结果为:
整数运算: a1 + b = 17 a1 - b = 13 a1 * b = 30 a1 / b = 7 a1 % b = 1 浮点数运算: a2 + b = 17.0 a2 - b = 13.0 a2 * b = 30.0 a2 / b = 7.5 a2 % b = 1.0
我们看到,整数15/2=7,而浮点数15/7=7.5。在Java中,参与运算的2个数有浮点数时,就会自动将非浮点数变成浮点数来运算。
下面为了节省篇幅,就不再分别列出代码和结果了。
3.7.1.2被0除问题
0.0 / 0 = NaN 1.0 / 0 = Infinity -1.0 / 0 = -Infinity 1 / 0 = Exception in thread "main" java.lang.ArithmeticException: / by zero at ch03.JibenYunsuanfu.main(JibenYunsuanfu.java:16)
我们看到,浮点数0除以0,得到NaN;正负浮点数除以0得到正负无穷大;整数除以0会抛出异常。
3.7.1.3原码反码补码
我们知道,Java的整型和浮点型都是有范围的,如果运算结果超过范围怎么办呢?我们知道int型的最大值是214783647,假如我们+1会得到什么结果呢?结果为:
2147483647 + 1 = -2147483648
说明这个问题原因之前,得先学习原码、反码、补码的相关知识。
3.7.1.3.1原码
我们现实生活当中,可以用正负号来表示正负数,但是计算机中只有0和1,怎么表示正负数呢?于是想出了一个办法,对于固定字长n的二进制数,把2个数划分为正负数,把最高位规定为符号位,0代表正,1代表负,剩下的二进制数对应十进制数的绝对值。例如假设字长为3,那么一共表示8个数:
这种规定叫做“原码”,即3的原码是011,-3的原码是111。看起来很完美吧,但是有2个问题:
- 0的表示不唯一
- 无法将减法转换为加法
0的表示不唯一一目了然,为什么不能将减法转换为加法?我们看个例子:
2 - 1 = 2 + (-1) = 010 + 101 = 111 = -3(正确结果为1)
结果错误。那么又为什么要把减法转换为加法呢?我们学习过计算机组成,知道CPU中只有加法寄存器,因为计算机中处理加法比较简单,如果要直接处理减法,需要增加逻辑部件,而且处理减法有借位问题很麻烦。因此在计算机中用原码来进行运算和存储行不通。
3.7.1.3.2反码
还有别的办法吗?人们又发明了“反码”。反码规定:正数的反码和原码一致,负数的反码为该数对应的绝对值的原码按位取反。假设字长为3,原码反码分别如下:
反码解决了减法转换为加法的问题,但是额外需要多一个规定,就是当发生溢出时,需要对最低位加1。我们看2个例子:
1 – 1 = 1 + (-1) = 001 + 110 = 111 =-0 2 - 1 = 2 + (-1) = 010 + 110 = 1000,溢出了,去掉溢出位后需再加1即000 + 001 = 001 = 1
我们看到,结果都正确。但是还是存在2个问题:
- 0的表示不唯一
- 减法转加法,需要判断溢出问题
3.7.1.3.3补码
继续探讨,于是出现“补码”。补码规定正数的补码和原码一致,负数的补码为该数对应的绝对值按位取反后加1(如果溢出丢弃最高位)
我们发现0的表示唯一了。另外用补码计算减法也很简单了,直接转换即可(溢出直接丢弃最高位),我们看2个例子:
1 – 1 = 1 + (-1) = 001 + 111 = 1000 = 000 = 0 2 - 1 = 2 + (-1) = 010 + 111 = 1001 = 001 = 1
喜欢钻牛角尖的同学就会问了,为什么使用补码就可以解决这些问题呢?有什么道理吗?我就知道你会问,还好我也恶补了这段知识,下面我们来研究一下。
3.7.1.3.4补码原理
我们知道,对于一个3位的二进制,对应的十进制为0-7,一共8个。7+1=111+000=1000,去掉溢出位,又变成000即0。我们可以说这8个数字形成了一个闭环。这其实对应数学中的一个概念:模。
模是指一个计量系统的计数范围,例如我们熟悉的时钟,它的计数范围是0-11,模是12。计算机也可以看成一个计量机器,因为计算机的字长是定长的,即存储和处理的位数是有限的,因此它也有一个计量范围,即都存在一个“模”。对于字长3位的机器来说,计数范围是0-7,模是8。“模”实质上是计量器产生“溢出”的量,它的值在计量器上表示不出来,计量器上只能表示出模的余数。任何有模的计量器,均可化减法为加法运算。
我们以时钟为例:当前时间是2点,逆时针拨2格变成0点。顺时针拨10格也是0点。假设逆时针叫减,顺时针叫加,那么对于模12的系统里,减2和加10的效果一样。事实上,减3和加9,减4和加8效果也一样。我们把2和10、3和9、4和8互称为补数,特点就是二者相加等于模。因此在有模的系统里,减去一个数,可以变成加上它的补数,即可以把减法变成加法。
回到3位数的二进制如下图:
我们很容易就知道模为8,1和7、2和6、3和5、4和4他们互为补数。列一个表:
但是问题来了,3位二进制系统里,虽然减n可以变成加n,但是由于没有负数,因此计算减法,需要先计算减数的补数,例如减1,需要计算1的补数8-1。怎么办?聪明的你一定可以想到,补数都是成对的,我们把成对的补数中的一半规定为负数是不是就可以了?例如a-1=a+(-1)=a+7,假如我们规定7的二进制111代表-1,那么在计算的时候就没有减法了。同理我们还可以规定110代表-2,101代表-3。至于100是代表4还是-4,都可以,一般我们选择代表-4。这样一来,对于3位二进制系统,表示数的范围就变成-4~3,而所有的减法就变成加法了。而且这样一来我们还惊奇的发现:
- 所有的正数最高位都是0,负数最高位都是1
- 所有负数的二进制都是它所对应的绝对值的二进制按位取反后+1,就是补码
到此为止,我们就搞清楚了为什么在计算中要用补码来表示负数了。
最后,我们回到开头的例子:
2147483647 + 1 = -2147483648
现在回答这个问题太easy了。在Java中,一个数字如果不加后缀,默认就是int型的。我们知道int型占用4个字节,则int的系统是一个模为2的系统。然后采用补码规则存储,这样最大的正数是2-1=2147483647。这个数再加1就变成2。2的补数是它自己,但是由于2的二进制最高位是1,我们习惯把它规定为负数,即-2,因此就是-2147483648。