Java使用任何基于C的语言中常见的运算符,如表1-3所示,我们将在下面的小节学习如何使用它们。
表1-3 Java运算符
注意: 在本表中,运算符是按照优先级递减的顺序排列的。例如,由于 + 的优先级高于 <<,因此3+ 4 <<5 等价于(3+ 4) << 5。当一个运算符是从左向右进行分组时,我们称它是左结合的。例如,3– 4 − 5表示(3 − 4) − 5。但 −= 是右结合的,例如I −= j −= k表示 i −= (j −= k)。
表1-3中的最后一行表示赋值运算符,例如:
x = expression;
以上语句会将x的值设置为右侧表达式的值,同时替换掉x之前的数值。
赋值是一个带有值的运算,具体来讲就是所赋的那个值。因此,在另一个表达式中使用赋值运算是合法的。例如:
(next = in.read()) != -1
以上语句中,next被in.read()的返回值赋值,如果该值不是−1,则赋值运算的值就不是−1,最后整个表达式的值为true。
当 = 前面有另一个运算符时,该运算符将左侧和右侧组合起来,并计算得到结果。例如:
amount -= fee
等同于
amount = amount - fee;
加法、减法、乘法和除法分别用 +、−、* 和 / 表示。例如,2* n + 1表示将2和n相乘再加1。
使用 / 运算符时,一定要小心。如果两个操作数都是整数类型,它表示整数除法,将得到整数结果并丢弃余数。例如,17/ 5是 3,而17.0/ 5是3.4。
整数除以零会产生一个异常,如果未捕捉到该异常,则程序会终止运行。(有关异常处理的更多信息,参见第5章。)一个浮点数除以零会生成一个无穷或NaN(参见1.2.2小节),并且不会导致异常。
使用 % 运算符将会得到余数。例如,17 % 5的结果是2,即17减去15(5的最接近17的整数倍)后的余数。如果a % b的结果为零,则a是b的整数倍。
% 运算符的一个基本用途就是测试整数是否为偶数。如果n为偶数,则表达式n % 2的结果为0。如果n是奇数呢?这时,如果n为正,则n % 2为1;如果n为负,则n % 2为 −1。在实践中,处理负数是比较复杂的。当 % 运算符与那些可能为负的操作数一起使用时,一定要注意这些问题。
考虑一下这个问题。你需要计算一个时钟的时针位置。你需要调整时针,并将其标准化为一个介于0~11的数字。那么处理起来很简单:使用(position + adjustment) % 12即可。但是如果adjustment使时针的位置为负呢?那么你可能会得到一个负数。所以这时你必须引入一个分支,或者使用((position + adjustment) % 12 + 12) % 12。不管怎样,这样处理都很麻烦。
提示: 在这种情况下,使用Math.floorMod方法会更容易。
Math.floorMod(position + adjustment, 12)将总是产生一个介于0和11之间的值。但遗憾的是,floorMod针对负除数的运算也会给出负数的结果,但这种情况在实际应用中并不常见。
Java有递增和递减运算符:
n++; // Adds one to n n--; // Subtracts one from n
和其他基于C的语言类似,这些运算符也有前缀形式。n++和++n都会使变量n的值递增,但在表达式中使用时,它们可能会有不同的值。第一种形式在n递增之前生成表达式的值,第二种形式则在n递增之后生成表达式的值。例如:
String arg = args[n++];
以上语句将arg设置为args[n],然后再使n递增。大概30年前,当编译器不能很好地优化代码时,这样做是有意义的。但是如今,使用两条单独的语句并不会产生性能上的差异,并且许多程序员认为显式的形式更容易阅读。
在Java中,没有运算符能够实现幂运算。因此需要调用Math.pow()方法来实现:Math.pow(x, y) 将得到x y 。如果要计算x的平方根,则需要调用Math.sqrt(x)。这些方法都是静态方法,因此与static常量一样,只需要在方法前添加类名,并不需要通过对象来调用这些方法。
此外,比较常用的还有Math.min()和Math.max()方法,可用于计算两个值的最小值和最大值。
此外,Math类还提供了三角函数、对数函数,还有常量Math.PI和常量Math.E。
注意: Math类提供了几种方法以确保整数的算术运算更安全。当计算溢出时,算术运算符会悄悄返回一个错误的结果。例如,10亿乘以3(1000000000 * 3)计算得到的结果为−1294967296,这是因为最大的int值恰好刚刚超过20亿。如果你调用Math.multiplyExact(1000000000, 3),那么将会产生一个异常。你可以捕获该异常,或者让程序终止,而不是使用错误的计算结果,并让程序继续运行。此外,还有addExact、subtractExact、incrementExact、decrementExact、negateExact等方法,它们都使用int和long作为参数。
在其他类中也有一些数学方法。例如,Integer类和Long类中有compareUnsigned、divideUnsigned和 remainderUnsigned方法来处理无符号数。
当运算符的操作数是不同的数值类型时,在运算之前,这些数值会自动转换为一个通用的类型。转换是按照以下顺序进行的。
● 如果两个操作数中有一个为double类型,则另一个将转换为double类型。
● 如果其中一个操作数是float类型,则另一个将转换为float类型。
● 如果其中一个操作数是long类型,则另一个将转换为long类型。
● 否则,两个操作数都转换为int类型。
例如,如果计算3.14 + 42,那么第二个操作数将会从int类型转换为double类型的值42.0,然后进行加法计算,得到double类型的值45.14。
如果计算 'J' + 1,则char类型的值'J'将被转换成为int类型的值74,最后结果为int类型的值75。至于如何将该值转换回char类型,还需要继续阅读后面的内容。
当你将数值类型的值赋值给一个变量,或将其作为参数传递给一个方法时,如果类型不匹配,则必须转换该值的类型。例如,在以下赋值中,值42会自动从int类型转换为double类型。
double x = 42;
在Java中,如果没有精度损失,那么将发生以下形式的转换:
● 从byte类型到short类型、int类型、long类型,或者double类型;
● 从short类型和char类型到int类型、long类型,或者double类型;
● 从int类型到long类型或者double类型。
所有的整数类型都会被转换成为浮点类型。
警告: 以下转换可能会丢失精度信息。
● 从int类型到float类型。
● 从long类型到float类型或double类型。
例如,考虑以下赋值:
float f = 123456789;
因为float类型只有7位有效数字,所以f实际上是1.23456792E8。
为了实现这些自动转换之外的类型转换,需要使用强制类型转换运算符,强制类型转换的语法格式是在圆括号中指定目标类型的名称。例如:
double x = 3.75; int n = (int) x;
在这种情况下,小数部分将会被舍弃,n会被设置为3。
如果想四舍五入到最接近的整数,可以使用Math.round方法,该方法返回一个long类型的值。如果明确知道结果更加适合int类型,可以调用:
int n = (int) Math.round(x);
在这个示例中,x为3.75,n被设置为4。
如果需要将整数类型转换为另一个字节更少的类型,也需要使用强制转换:
int n = 1; char next = (char)('J' + n); // Converts 75 to 'K'
在这种强制转换中,只保留最后的字节,例如:
int n = (int) 3000000000L; // Sets n to -1294967296
注意: 如果担心强制类型转换会无警告地丢弃数值的重要部分,那么可以使用Math.toIntExact方法。当该方法无法将long类型转换为int类型时,就会产生异常。
== 和 != 运算符的功能是测试相等性。例如,当n不为0时,表达式n!= 0的值为true。此外,<(小于)、>(大于)、<=(小于或等于)和 >=(大于或等于)运算符都是常用的运算符。
你也可以将boolean类型的表达式与 &&(与)、||(或)和 !(非)运算符组合。例如:
0 <= n && n < length
当n介于0(包含0)和length(不包含length)之间时,表达式为真。
以上表达式中,如果第一个条件为false,则第二个条件不会被计算。如果第二个条件可能会产生错误,那么这种“短路”测试的方式非常有用。考虑以下条件:
n != 0 && s + (100 - s) / n < 50
如果n为0,那么第二个条件,即其中包含除n运算的条件永远不会被求值,因此也不会出现除数为0的计算错误。
短路测试也可以用于“或”运算,只要一个操作数为true时,其余的求值就会停止,即不计算第二个条件。例如:
n == 0 || s + (100 - s) / n >= 50
如果n为0,则以上表达式将得到true,并且第二个条件不会被计算。
最后, 条件( conditional)运算符接受3个操作数:一个条件和两个值。如果条件为true,整个表达式的结果是第一个值,否则是第二个值。例如:
time < 12 ? "am" : "pm"
表示如果time < 12 为真,则得到字符串"am",否则得到字符串"pm"。
注意: Java还有位运算符 &(位与)、|(位或)、^(位异或)。它们是针对整数,按照位模式进行运算的。例如,由于0xF的二进制数字是0...01111,因此n & 0xF就会得到n的二进制数字中的最低4位;n = n | 0xF 将会将n的二进制值的最低4位设置为1;n = n ^ 0xF则将翻转n的最低4位。与 ! 运算符类似的位运算符是~,它的功能是翻转操作数的所有位,即~0xF的结果是1...10000。
此外,还有在位模式下向左或向右移位的运算符。例如,0xF << 2将得到二进制数字为0...0111100。Java中共有两个右移运算符,其中 >> 是将符号位扩展到顶部,而 >>> 则会用0来填充顶部的符号位。如果你在程序中进行移位运算,那么你必须要知道这意味着什么;如果你并不了解移位运算,那么也就表明你可能不需要使用这些运算符了。
警告: 如果移位运算符号的左操作数是int类型,那么右操作数的模是32;如果左操作数是long类型,那么右操作数的模是64。例如,1<< 35的值与1<< 3或8 相同。
提示: 当 &(位与)和 |(位或)运算符应用于boolean值时,在计算结果之前将会对两个操作数进行强制求值。当然,这种用法非常罕见。加入右操作数没有副作用,它们就像 && 和 || 一样,只是效率较低。除非确实需要强制对第二个操作数求值,并将其赋值给一个boolean变量,以使得执行流程清晰可见。
如果基本类型的整数和浮点数的精度无法满足实际需求,那么可以使用java.math包中的BigInteger类和BigDecimal类。这些类的对象可以表示具有任意长度数字序列的数值。BigInteger类可以实现任意精度的整数运算,BigDecimal也可以对浮点数实现同样的功能。当然,使用大数的计算效率远远低于使用基本数据类型的计算效率。
静态方法valueOf可以将long类型转换为BigInteger:
BigInteger n = BigInteger.valueOf(876543210123456789L);
你还可以从数字形式的字符串中构造一个BigInteger的对象:
var k = new BigInteger("9876543210123456789");
此外还有一些预定义的常量,例如,BigInteger.ZERO、BigInteger.ONE、BigInteger.TWO和BigInteger.TEN。
Java不允许对象使用运算符,因此必须调用对应的方法来处理大数的运算。
BigInteger r = BigInteger.valueOf(5).multiply(n.add(k)); // r = 5 * (n + k)
1.2.2小节中,你看到了浮点数减法 2.0 - 1.7的结果为0.30000000000000004。使用BigDecimal类可以计算出精确结果。
调用BigDecimal.valueOf(n, e)将返回一个值为n×10 −e 的BigDecimal实例。例如,以下方法调用将准确地得到结果0.3:
BigDecimal.valueOf(2, 0).subtract(BigDecimal.valueOf(17, 1))