3.5 操作符
运算符用于组合值。正如您将在下面的章节中看到的,Java有丰富的算术和逻辑运算符和数学函数集。
3.5.1 算术运算符
常用的算术运算符+
,-
,*
,/
用于Java中的加法、减法、乘法和除法。如果两个参数都是整数,/
运算符表示整数除法,否则表示浮点除法。整数余数(有时称为模)用%表示。例如,15/2是7,15%2是1,15.0/2是7.5。
请注意,整数被0除会引发异常,而浮点被0除会产生无穷大或NaN结果。
注意
Java编程语言的一个明确目标是可移植性。无论哪个虚拟机执行计算,计算都应该产生相同的结果。对于浮点数的算术计算,实现这种可移植性是非常困难的。double类型使用64位存储数值,但有些处理器使用80位浮点寄存器。这些寄存器在计算的中间步骤中产生额外的精度。例如,考虑以下计算:
double w = x * y / z;
许多英特尔处理器计算x*y,将结果保留在80位寄存器中,然后除以z,最后将结果截断为64位。这样可以得到更精确的结果,并且可以避免指数溢出。但是结果可能与使用64位的计算不同。出于这个原因,Java虚拟机的初始规范要求所有中间计算必须被截断。数字社会对此深恶痛绝。被截断的计算不仅会导致溢出,而且实际上它们比更精确的计算慢,因为截断操作需要时间。出于这个原因,Java编程语言被更新以识别对于最佳性能和完美再现性的冲突需求。默认情况下,现在允许虚拟机设计者使用扩展精度进行中间计算。但是,使用
strictfp
关键字标记的方法必须使用严格的浮点操作,以产生可重复的结果。例如,你能标记
main
为public static strictfp void main(String[] args)
然后,main方法中的所有指令都将使用严格的浮点计算。如果将类标记为
strictfp
,则其所有方法都必须使用严格的浮点计算。这些血淋淋的细节与英特尔处理器的行为密切相关。在默认模式下,中间结果可以使用扩展的指数,但不能使用扩展的尾数。(Intel芯片支持尾数的截断而不会损失性能。)因此,默认模式和严格模式之间的唯一区别是,当默认计算不存在时,严格计算可能溢出。
如果你看这张纸条时目光呆滞,别担心。对于大多数常见的程序来说,浮点溢出不是一个问题。我们在这本书中没有使用strictfp关键字。
3.5.2 数学函数和常量
Math
类包含您偶尔需要的各种数学函数,这取决于您所做的编程类型。
要获取数字的平方根,请使用sqrt
方法:
double x = 4;
double y = Math.sqrt(x);
System.out.println(y); // prints 2.0
注意
println方法和sqrt方法之间有细微的区别。println方法对System.out对象进行操作。但是数学类中的sqrt方法不在任何对象上操作。这种方法称为静态方法。您可以在第4章中进一步了解静态方法。
Java编程语言没有操作符将一个量提升到一个幂:必须在Math
类中使用pow
方法。语句
double y = Math.pow(x, a);
将y设置为x的幂(x^a)。pow方法的参数都是double类型,它也返回double。
floorMod
方法旨在解决一个长期存在的整数余数问题。考虑表达式n%2。每个人都知道,如果n是偶数,这是0;如果n是奇数,这是1。当然,当n为负时除外。然后是-1。为什么?当第一台计算机建立时,有人必须为整数除法和余数对负操作数的作用制定规则。数学家已经知道了几百年的最优(或“欧几里得”)规则:总是让余数大于等于0。但是,这些先驱们并没有打开数学教科书,而是提出了看似合理但实际上不方便的规则。
考虑这个问题。你计算时钟时针的位置。应用了一个调整,您希望规范化为介于0和11之间的数字。这很容易:(position + adjustment) % 12
。但是如果调整是负的呢?你可能会得到一个负数。因此,您必须引入分支,或使用((position + adjustment) % 12 + 12) % 12
。不管怎样,这都很麻烦。
floorMod方法更简单:floorMod(position + adjustment, 12) 总是生成介于0和11之间的值。(不幸的是,floorMod对负除数给出了否定的结果,但这种情况在实践中并不常见。)
Math类提供了常见的三角函数:
Math.sin
Math.cos
Math.tan
Math.atan
Math.atan2
指数函数及其逆函数、自然对数和十进制对数:
Math.exp
Math.log
Math.log10
最后,两个常数表示最接近数学常数π和E的近似值:
Math.PI
Math.E
提示
通过在源文件的顶部添加以下行,可以避免数学方法和常量的
Math
前缀:import static java.lang.Math.*;
例如:
System.out.println("The square root of \u03C0 is " + sqrt(PI));
我们在第4章讨论静态引入。
注意
Math
类提供了几种使整数算术更安全的方法。当计算溢出时,数学运算符会悄悄地返回错误的结果。例如,10亿乘以3(100000000*3)的计算结果为-1294967296,因为最大的int值刚刚超过20亿。如果您改为调用Math.multiplyExact(100000000, 3),则会生成一个异常。您可以捕获该异常,或者让程序终止,而不是静静地继续执行错误的结果。还有addExact、subtractExact、incrementExact、decrementExact、negateExact等方法,都带有int和long参数。
3.5.3 数字类型之间的转换
通常需要从一种数值类型转换为另一种数值类型。图3.1显示了合法转换。
图3.1 数字类型之间的合法转换
图3.1中的六个实心箭头表示没有信息丢失的转换。三个虚线箭头表示可能会丢失精度的转换。例如,一个大整数(如123456789)的位数超过了浮点类型所能表示的位数。当整数转换为浮点数时,得到的值具有正确的大小,但会丢失一些精度。
int n = 123456789;
float f = n; // f is 1.23456792E8
当两个值与一个二元运算符(如n+f,其中n是一个整数,f是一个浮点值)组合时,在执行操作之前,两个操作数都将转换为一个公共类型。
- 如果其中一个操作数的类型为double,则另一个操作数将转换为double。
- 否则,如果其中一个操作数的类型为float,则另一个操作数将转换为float。
- 否则,如果其中一个操作数的类型为long,则另一个操作数将转换为long。
- 否则,两个操作数都将转换为int。
3.5.4 转化
在前面的部分中,您看到了必要时,int值会自动转换为双精度值。另一方面,显然有一些时候你想把一个双精度数看作一个整数。数字转换在Java中是可能的,但是当然可能丢失信息。信息可能丢失的转换是通过强制转换完成的。强制转换的语法是在括号中给出目标类型,后跟变量名。例如:
double x = 9.997;
int nx = (int) x;
现在,变量nx的值为9,因为将浮点值强制转换为整数会丢弃小数部分。
如果要将浮点数舍入到最接近的整数(在大多数情况下,这是一个更有用的操作),请使用Math.round
方法:
double x = 9.997;
int nx = (int) Math.round(x);
现在变量nx的值为10。调用round时仍然需要使用cast(int)。原因是round方法的返回值很长,并且long只能通过显式强制转换分配给int,因为存在信息丢失的可能性。
小心
如果试图将一个类型的数字强制转换为另一个超出目标类型范围的数字,则结果将是具有不同值的截断数字。例如,(byte)300实际上是44。
C++注意
不能在布尔值和任何数值类型之间强制转换。这种惯例防止了常见的错误。在极少数情况下,如果要将布尔值转换为数字,可以使用条件表达式,如
b ? 1 : 0
。
3.5.5 结合赋值和运算符
在赋值中使用二元运算符有一个方便的快捷方式。例如,
x += 4
等价于
x = x + 4
(通常,将运算符放在=符号的左侧,例如*=
或%=
)。
注意
如果运算符生成的值的类型与左侧的不同,则强制该值匹配。例如,如果x是int,那么语句
x += 3.5;
有效,将x设置为(int) (x+3.5)。
3.5.6 递增和递减运算符
当然,程序员知道使用数字变量最常见的操作之一是加或减1。遵循C和C++的脚步,Java具有增量和减量运算符:n++给变量n的当前值加1,而n–减去1。例如,代码
int n = 12;
n++;
将n改为13。由于这些运算符更改了变量的值,因此它们不能应用于数字本身。例如,4++不是合法声明。
这些运算符有两种形式;您刚刚看到了放在操作数后面的运算符的后缀形式。还有一个前缀形式,++n。两者都将变量的值更改为1。只有在表达式内部使用时,这两者之间的差异才会出现。前缀形式首先执行加法;后缀形式的计算结果是变量的旧值。
int m = 7;
int n = 7;
int a = 2 * ++m; // now a is 16, m is 8
int b = 2 * n++; // now b is 14, n is 8
我们建议不要在表达式内部使用++,因为这通常会导致代码混乱和令人讨厌的错误。
3.5.7 关系运算符和布尔运算符
Java有完整的关系操作符的补充。要测试相等性,请使用双等号,==。例如,值
3 == 7
是false。
使用a!=
表示不等式。例如,值
3 != 7
是true。
最后,您有通常的<(小于)、>(大于)、<=(小于或等于)和>=(大于或等于)运算符。
Java遵循C++,使用&&
用于逻辑与操作,使用||
用于逻辑或操作。就像你很容易记住的那样!=
运算符,感叹号!
是逻辑否定运算符。&&
和||
运算符以“短路”方式计算:如果第一个参数已经确定了值,则不会计算第二个参数。如果将两个表达式用&&运算符组合在一起,
expression1 && expression2
第一个表达式的真值被确定为假,那么结果就不可能是真的。因此,不计算第二个表达式的值。可以利用这种行为来避免错误。例如,在表达式中
x != 0 && 1 / x > x + y // no division by 0
如果x等于零,则从不计算第二部分。因此,如果x为零,则不计算1/x,并且不会出现被零除错误。
同样,如果第一个表达式为真,则expression1 || expression2
的值自动为真,而不计算第二个表达式。
最后,Java支持三元表达式?:
,这个偶尔很有用。表达式
condition ? expression1 : expression2
如果条件为真,则计算为第一个表达式,否则计算为第二个表达式。例如,
x < y ? x : y
给出x和y的较小值
3.5.8 位运算符
对于任何整数类型,都有可以直接处理组成整数的位的运算符。这意味着您可以使用掩码技术获取数字中的单个位。位运算符是
& (“and”) | (“or”) ^ (“xor”) ~ (“not”)
这些运算符处理位模式。例如,如果n是一个整数变量,那么
int fourthBitFromRight = (n & 0b1000) / 0b1000;
如果n的二进制表示中右边的第四位是1,则给出1,否则为0。使用&和适当的2次方能让你屏蔽所有,除了一个位。
注意
当应用于布尔值时,
&
和|
运算符生成布尔值。这些运算符类似于&&
和||
运算符,只是&
和|
运算符的计算方式不是“短路”,也就是说,两个参数都是在计算结果之前进行计算的。
还有>>和<<运算符,它们左右移动位模式。当您需要建立位模式来进行位屏蔽时,这些运算符非常方便:
int fourthBitFromRight = (n & (1 << 3)) >> 3;
最后,一个>>>运算符用零填充顶部的位,而>>运算符将符号位扩展到顶部的位。没有<<<运算符。
小心
移位运算符的右侧参数是约简模32(除非左侧参数是long,在这种情况下,右侧参数是约简模64)。例如,1<<35的值与1<<3或8的值相同。
C++注意
在C/C++中,不能保证>>是否执行算术移位(扩展符号位)或逻辑移位(用零填充)。实施者可以自由选择效率更高的方案。这意味着C/C++ >>算子可以产生负相关的实现结果。Java消除了不确定性。
3.5.9 括号和运算符层次结构
表3.4显示了运算符的优先级。如果不使用括号,则按指示的层次顺序执行操作。同一级别的运算符从左到右进行处理,但右相关的运算符除外,如表中所示。例如,&&的优先级高于||,因此表达式
表3.4 操作符优先级
操作符 | 关联性 |
---|---|
[] . ()(方法调用) | 左到右 |
! ~ ++ – +(一元) -(一元) ()(强制转化) new | 右到左 |
* / % | 左到右 |
+ - | 左到右 |
<< >> >>> | 左到右 |
< <= > >= instanceof | 左到右 |
== != | 左到右 |
& | 左到右 |
^ | 左到右 |
| | 左到右 |
&& | 左到右 |
|| | 左到右 |
?: | 右到左 |
= += -= *= /= %= &= = ^= <<= >>= >>>= | 右到左 |
a && b || c
意味着
(a && b) || c
因为+=从右向左关联,所以表达式
a += b += c
意味着
a += (b += c)
也就是说,b+=c的值(加上后b的值)加在a上
C++注意
与C或C++不同,Java没有逗号运算符。但是,可以在for语句的第一个和第三个槽中使用逗号分隔的表达式列表。