表达式
相对于基本类型、字面量和变量,表达式是Java程序更高一级的结构。Java解释器会求出表达式的值,最简单的表达式叫基本表达式,由字面量和变量组成。例如,下面几个例子都是表达式:
1.7 // 一个浮点数字面量
true // 一个布尔字面量
sum // 一个变量
Java解释器计算字面量表达式得到的结果是字面量本身,计算变量表达式得到的结果是存储在变量中的值。
基本表达式没什么特殊意思。使用运算符把基本表达式连接在一起可以组成复杂的表达式。例如,下面的表达式使用赋值运算符(=)把两个基本表达式(一个变量,一个浮点数字面量)连在一起,组成赋值表达式:
sum = 1.7;
运算符不仅能连接基本表达式,也能在任意复杂度的表达式中使用。如下都是合法的表达式:
sum = 1 + 2 + 3 * 1.2 + (4 + 8)/3.0
sum/Math.sqrt(3.0 * 1.234)
(int)(sum + 33)
运算符
一门编程语言能编写什么样的表达式,完全取决于可以用的运算符。Java提供了丰富的运算符,但是在使用之前,要弄清楚两个重要的概念:优先级和结合性。
下表总结了Java提供的运算符。P列和A列分支表示每类相关运算符的优先级和结合性。
1.优先级
在上图中,P列是运算符的优先级。优先级指定运算符执行的顺序,优先级高的运算符在优先级低的运算符之前运算。例如,有如下表达式:
a + b * c
乘号的优先级比加号的优先级高,所以结果是a和b乘以c的结果相加,这与小学数学上学到的一样。运算符的优先级可以理解为运算符和操作数之间绑定的紧密程度,优先级越高,绑定的越紧密。
运算符默认的优先级可以使用小括号改变,小括号能明确指定运算的顺序。前面那个表达式可以像下面这样重写,先相加再相乘:
(a + b)* c
Java采用的默认运算符优先级和C语言兼容,C语言的设计者选定的优先级无需使用括号就能流畅地写出大多数表达式,只有少量的Java惯用句法需要使用括号,例如:
((Integer) o).intValue(); // 类校正和成员访问结合在一起
while((line = in.readLine()) != null) { ... } // 赋值和比较结合在一起
if ((flags & (PUBLIC | PROTECTED)) != 0) { ... } // 位运算符和比较结合在一起
2.结合性
结合性是运算符的一个属性,定义如何计算有歧义的表达式。如果表达式中有多个优先级相同的运算符,结合性尤其重要。
大多数运算符由左至右结合,即从左向右计算。不过,赋值和一元运算符由右至左结合,在上图表中,A列是运算符或运算符组的结合性,L表示由左至右,R表示由右至左。
加号和减号的结合性都是由左至右。所以表达式a+b-c从左向右计算,即(a+b)-c。一元运算符和赋值运算符由右向左计算,例如下面这个复杂的表达式:
a = b += c = -~d
计算的顺序是:
a = (b += (c = -(~d)))
和运算符的优先级一样,运算符的结合性也建立了计算表达式的默认顺序。这个默认的顺序可以使用括号改变。然而,Java选定的默认运算符结合性是为了使用流畅的句法编写表达式,几乎不需要改变。
操作数的数量和类型
在上面运算符的图表中,第4列是每种运算符能处理的操作数数量和类型。有些运算符只有一个操作数,这种运算符叫一元运算符,例如一元减号的作用是改变单个数字的符号:
-n //一元减号运算符
不过,大多数运算符都是二元运算符,有两个操作数。-运算符其实还有一种用法:
a - b //减法运算符是二元运算符
Java还定义了一个三元运算符,经常称作条件运算符,就像是表达式中的if语句,它的三个操作数由冒号和问号分开,第二个和第三个操作数必须能转换成同一种类型:
x > y ? x : y // 三元表达式;计算x和y哪个大
除了需要特定数量的操作数之外,每个运算符还需要特定类型的操作数。上面的图表中第4列是操作数的类型,其中使用的文本需要进一步说明:
数字:整数、浮点数或字符(即除了布尔类型之外的任何一种基本类型)。因为这些类型对应的包装类(例如 Character、Integer 和 Double)能自动拆包,所以在这些地方也能使用相应的包装类。
整数:byte、short、int、long 或 char 类型的值(获取数组元素的运算符 [ ] 不能使用 long类型的值)。因为能自动拆包,所以也能使用 Byte、Short、Integer、Long 和 Character类型的值。
引用类型:对象或数组。
变量:变量或其他符号名称(例如数组中的元素),只要能赋值就行。
返回类型
就像运算符只能处理特定类型的操作数一样,运算得到的结果也是特定类型的值。对算术运算符、递增和递减、位运算符和位移运算符来说,如果至少有一个操作数是 double 类型,返回值就是 double 类型;如果至少有一个操作数是 float 类型,返回值是 float 类型;如果至少有一个操作数是 long 类型,返回值是 long 类型;除此之外都返回 int 类型的值,就算两个操作数都是 byte、short 或 char 类型,也会放大转换成 int 类型。比较、相等性和逻辑运算符始终返回布尔值。各种赋值运算符都返回赋予的值,类型和表达式左边的变量兼容。条件运算符返回第二个或第三个操作数(二者的类型必须相同)。
副作用
每个运算符都会计算一个或多个操作数,得到一个结果。但是,有些运算符除了基本的计算之外还有副作用。如果表达式有副作用,计算时会改变 Java 程序的状态,即再次执行时会得到不同的结果。
例如,++ 递增运算符的副作用是递增变量中保存的值。表达式 ++a 会递增变量 a 中的值,返回递增后得到的值。如果再次计算这个表达式,会得到不同的值。各种赋值运算符也有副作用。例如,表达式 a\*=2 也可以写成 a=a\*2,这个表达式的结果是乘于 2 后得到的值,但是有副作用——把计算结果重新赋值给 a。
如果调用的方法有副作用,方法调用运算符 () 也有副作用。有些方法,例如 Math.sqrt(),只是计算后返回一个值,没有任何副作用。可是,一般情况下,方法都有副作用。最后,new 运算符有重大的副作用,它会创建一个新对象。
计算的顺序
Java 解释器计算表达式时,会按照表达式中的括号、运算符的优先级和结合性指定的顺序运算。不过,在任何运算之前,解释器会先计算运算符的操作数(&&、|| 和 ?: 例外,不会总是计算这些运算符的全部操作数)。解释器始终使用从左至右的顺序计算操作数。如果操作数是有副作用的表达式,这种顺序就很重要了。例如下面的代码:
int a = 2;
int v = ++a + ++a * ++a;
虽然乘法的优先级比加法高,但是会先计算 ++ 运算符的两个操作数。因为这两个操作数都是 ++a,所以得到的计算的结果分别是 3 和 4,因此这个表达式计算的是 3 + 4 * 5,结果为 23。