2.4 表达式和运算符

到目前为止,我们学习了 Java 程序能处理的基本类型,以及如何在 Java 程序中使用基本类型的字面量。还使用了变量作为值的符号名称。字面量和变量都是组成 Java 程序的标记。

表达式是 Java 程序更高一级的结构。Java 解释器会求出表达式的值。最简单的表达式叫基本表达式,由字面量和变量组成。例如,下面几个例子都是表达式:

  1. 1.7 // 一个浮点数字面量
  2. true // 一个布尔字面量
  3. sum // 一个变量

Java 解释器计算字面量表达式得到的结果是字面量本身;计算变量表达式得到的结果是存储在变量中的值。

基本表达式没什么意思。使用运算符把基本表达式连在一起可以组成复杂的表达式。例如,下面的表达式使用赋值运算符把两个基本表达式(一个变量,一个浮点数字面量)连在一起,组成赋值表达式:

  1. sum = 1.7

不过,运算符不仅能连接基本表达式,也能在任意复杂度的表达式中使用。如下都是合法的表达式:

  1. sum = 1 + 2 + 3 * 1.2 + (4 + 8)/3.0
  2. sum/Math.sqrt(3.0 * 1.234)
  3. (int)(sum + 33)

2.4.1 运算符概述

一门编程语言能编写什么样的表达式,完全取决于可用的运算符。Java 提供了丰富的运算符,但在有效使用它们之前,要弄清两个重要的概念:优先级结合性。下面几节详细说明这两个概念和运算符。

1. 优先级

在表 2-4 中,P 列是运算符的优先级。优先级指定运算符执行的顺序。优先级高的运算符在优先级低的运算符之前运算。例如,有如下的表达式:

  1. a + b * c

乘号的优先级比加号的优先级高,所以 ab 乘以 c 的结果相加,这与小学数学课上学到的一样。运算符的优先级可以理解为运算符和操作数之间绑定的紧密程度,优先级越高,绑定得越紧密。

运算符默认的优先级可以使用括号改变,括号能明确指定运算的顺序。前面那个表达式可以像下面这样重写,先相加再相乘:

  1. (a + b) * c

Java 采用的默认运算符优先级和 C 语言兼容,C 语言的设计者选定的优先级无需使用括号就能流畅地写出大多数表达式。只有少量的 Java 惯用句法需要使用括号,例如:

  1. // 类校正和成员访问结合在一起
  2. ((Integer) o).intValue();
  3. // 赋值和比较结合在一起
  4. while((line = in.readLine()) != null) { ... }
  5. // 位运算符和比较结合在一起
  6. if ((flags & (PUBLIC | PROTECTED)) != 0) { ... }

2. 结合性

结合性是运算符的一个属性,定义如何计算有歧义的表达式。如果表达式中有多个优先级相同的运算符,结合性尤其重要。

大多数运算符由左至右结合,即从左向右计算。不过,赋值和一元运算符由右至左结合。在表 2-4 中,A 列是运算符或运算符组的结合性,L 表示由左至右,R 表示由右至左。

加号和减号的结合性都是由左至右,所以表达式 a+b-c 从左向右计算,即 (a+b)-c。一元运算符和赋值运算符从右向左计算。例如下面这个复杂的表达式:

  1. a = b += c = -~d

计算的顺序是:

  1. a = (b += (c = -(~d)))

和运算符的优先级一样,运算符的结合性也建立了计算表达式的默认顺序。这个默认的顺序可以使用括号改变。然而,Java 选定的默认运算符结合性是为了使用流畅的句法编写表达式,几乎不需要改变。

3. 运算符总结表

表 2-4 总结了 Java 提供的运算符。P 列和 A 列分别表示每类相关运算符的优先级和结合性。这张表可以作为运算符(特别是优先级)的快速参考指南。

表2-4:Java运算符

PA运算符操作数类型执行的运算
16L.对象,成员访问对象成员
[ ]数组,int获取数组中的元素
( args )方法,参数列表调用方法
++--变量后递增,后递减
15R++--变量前递增,前递减
+-数字正号,负号
~整数按位补码
!布尔值逻辑求反
14Rnew类,参数列表创建对象
( type )类型,任何类型校正(类型转换)
13L*/%数字,数字乘法,除法,求余数
12L+-数字,数字加法,减法
+字符串,任何类型字符串连接
11L<<整数,整数左移
>>整数,整数右移,高位补符号
>>>整数,整数右移,高位补零
10L<<=数字,数字小于,小于或等于
>>=数字,数字大于,大于或等于
instanceof引用类型,类型类型比较
9L==基本类型,基本类型等于(值相同)
!=基本类型,基本类型不等于(值不同)
==引用类型,引用类型等于(指向同一个对象)
!=引用类型,引用类型不等于(指向不同的对象)
8L&整数,整数位与
&布尔值,布尔值逻辑与
7L^整数,整数位异或
^布尔值,布尔值逻辑异或
6L|整数,整数位或
|布尔值,布尔值逻辑或
5L&&布尔值,布尔值条件与
4L||布尔值,布尔值条件或
3R? :布尔值,任何类型条件(三元)运算符
2R=变量,任何类型赋值
*=/=%=+=-=<<=>>=>>>=&=^=|=变量,任何类型计算后赋值
1R->参数列表,方法体lambda 表达式

4. 操作数的数量和类型

在表 2-4 中,第 4 列是每种运算符能处理的操作数数量和类型。有些运算符只有一个操作数,这种运算符叫一元运算符。例如,一元减号的作用是改变单个数字的符号:

  1. -n // 一元减号

不过,大多数运算符都是二元运算符,有两个操作数。- 运算符其实还有一种用法:

  1. a b // 减法运算符是二元运算符

Java 还定义了一个三元运算符,经常称作条件运算符,就像是表达式中的 if 语句。它的三个操作数由问号和冒号分开,第二个和第三个操作数必须能转换成同一种类型:

  1. x > y ? x : y // 三元表达式;计算x和y哪个大

除了需要特定数量的操作数之外,每个运算符还需要特定类型的操作数。表 2-4 中的第 4 列是操作数的类型,其中使用的文本需要进一步说明。

  • 数字

整数、浮点数或字符(即除了布尔类型之外的任何一种基本类型)。因为这些类型对应的包装类(例如 CharacterIntegerDouble)能自动拆包(参见 2.9.4 节),所以在这些地方也能使用相应的包装类。

  • 整数

byteshortintlongchar 类型的值(获取数组元素的运算符 [ ] 不能使用 long 类型的值)。因为能自动拆包,所以也能使用 ByteShortIntegerLongCharacter 类型的值。

  • 引用类型

对象或数组。

  • 变量

变量或其他符号名称(例如数组中的元素),只要能赋值就行。

5. 返回类型

就像运算符只能处理特定类型的操作数一样,运算得到的结果也是特定类型的值。对算术运算符、递增和递减、位运算符和位移运算符来说,如果至少有一个操作数是 double 类型,返回值就是 double 类型;如果至少有一个操作数是 float 类型,返回值是 float 类型;如果至少有一个操作数是 long 类型,返回值是 long 类型;除此之外都返回 int 类型的值,就算两个操作数都是 byteshortchar 类型,也会放大转换成 int 类型。

比较、相等性和逻辑运算符始终返回布尔值。各种赋值运算符都返回赋予的值,类型和表达式左边的变量兼容。条件运算符返回第二个或第三个操作数(二者的类型必须相同)。

6. 副作用

每个运算符都会计算一个或多个操作数,得到一个结果。但是,有些运算符除了基本的计算之外还有副作用。如果表达式有副作用,计算时会改变 Java 程序的状态,即再次执行时会得到不同的结果。

例如,++ 递增运算符的副作用是递增变量中保存的值。表达式 ++a 会递增变量 a 中的值,返回递增后得到的值。如果再次计算这个表达式,会得到不同的值。各种赋值运算符也有副作用。例如,表达式 a*=2 也可以写成 a=a*2,这个表达式的结果是乘于 2 后得到的值,但是有副作用——把计算结果重新赋值给 a

如果调用的方法有副作用,方法调用运算符 () 也有副作用。有些方法,例如 Math.sqrt(),只是计算后返回一个值,没有任何副作用。可是,一般情况下,方法都有副作用。最后,new 运算符有重大的副作用,它会创建一个新对象。

7. 计算的顺序

Java 解释器计算表达式时,会按照表达式中的括号、运算符的优先级和结合性指定的顺序运算。不过,在任何运算之前,解释器会先计算运算符的操作数(&&||?: 例外,不会总是计算这些运算符的全部操作数)。解释器始终使用从左至右的顺序计算操作数。如果操作数是有副作用的表达式,这种顺序就很重要了。例如下面的代码:

  1. int a = 2;
  2. int v = ++a + ++a * ++a;

虽然乘法的优先级比加法高,但是会先计算 + 运算符的两个操作数。因为这两个操作数都是 ++a,所以得到的计算的结果分别是 34,因此这个表达式计算的是 3 + 4 * 5,结果为 23

2.4.2 算术运算符

算术运算符可用于整数、浮点数和字符(即除了布尔类型之外的所有基本类型)。如果其中有个操作数是浮点数,就按浮点算术运算;否则,按整数算术运算。这一点很重要,因为整数算术和浮点算术是有区别的,例如除法的运算方式,以及上溢和下溢的处理方式。算术运算符如下。

  • 加法(+

+ 号计算两个数之和。稍后会看到,+ 号还能连接字符串。如果 + 号的操作数中有一个是字符串,另一个也会转换成字符串。如果想把加法和连接放在一起使用,一定要使用括号。例如:

  1. System.out.println("Total: " + 3 + 4); // 打印“Total: 34”,不是7!
  • 减法(-

- 号当成二元运算符使用时,计算第一个操作数减去第二个操作数得到的结果。例如,7-3 的结果是 4。- 号也可执行一元取负操作。

  • 乘法(*

* 号计算两个操作数的乘积。例如,7*3 的结果是 21。

  • 除法(/

/ 号用第一个操作数除以第二个操作数。如果两个操作数都是整数,结果也是整数,丢掉余数。如果有一个操作数是浮点数,结果就是浮点数。两个整数相除时,如果除数是零,抛出 ArithmeticException 异常。不过,对浮点数计算来说,如果除以零,得到的是无穷大或 NaN:

  1. 7/3 // 计算结果为2
  2. 7/3.0f // 计算结果为2.333333f
  3. 7/0 // 抛出ArithmeticException异常
  4. 7/0.0 // 计算结果为正无穷大
  5. 0.0/0.0 // 计算结果为NaN
  • 求模(%

% 运算符计算第一个操作数和第二个操作数的模数,即返回第一个操作数除去第二个操作数的整倍数之后剩下的余数。例如,7%3 的结果是 1。结果的符号和第一个操作数的符号一样。虽然求模运算符的操作数一般是整数,但也可以使用浮点数。例如,4.3%2.1 的结果是 0.1。如果操作数是整数,计算零的模数会抛出 ArithmeticException 异常。如果操作数是浮点数,计算 0.0 的模数得到的结果是 NaN;计算无穷大和任何数的模数得到的结果也是 NaN。

  • 负号(-

如果把 - 号当成一元运算符使用,即放在单个操作数之前,执行的是一元取负运算。也就是说,会把正数转换成对应的负数,或把负数转换成对应的正数。

2.4.3 字符串连接运算符

+ 号(以及相关的 += 运算符)除了能计算数字之和以外,还能连接字符串。如果 + 号的两个操作数中有一个是字符串,另一个操作数也会转换成字符串。例如:

  1. // 打印“Quotient: 2.3333333”
  2. System.out.println("Quotient: " + 7/3.0f);

因此,如果加法和字符串连接结合在一起使用,要把加法表达式放在括号中。如果不这么做,加号会被理解成连接运算符。

Java 解释器原生支持把所有基本类型转换成字符串。对象转换成字符串时,调用的是对象的 toString() 方法。有些类自定义了 toString() 方法,所以这些类的对象可以使用这种方式轻易地转换成字符串。数组转换成字符串时会调用原生的 toString() 方法,不过可惜,这个方法没有为数组的内容提供有用的字符串形式。

2.4.4 递增和递减运算符

++ 运算符把它的单个操作数增加 1,这个操作数必须是变量、数组中的元素或对象的字段。这个运算符的行为取决于它相对于操作数的位置。放在操作数之前,是前递增运算符,递增操作数的值,并返回递增后的值。放在操作数之后,是后递增运算符,递增操作数的值,但返回递增前的值。

例如,下面的代码把 ij 的值都设为 2:

  1. i = 1;
  2. j = ++i;

但是,下面的代码把 i 的值设为 2,j 的值设为 1:

  1. i = 1;
  2. j = i++;

类似地,-- 运算符把它的单个数字操作数减小 1,这个操作数必须是变量、数组中的元素或对象的字段。和 ++ 运算符一样,-- 的行为也取决于它相对于操作数的位置。放在操作数之前,递减操作数的值,并返回递减后的值。放在操作数之后,递减操作数的值,但返回递减前的值。

表达式 x++x-- 分别等效于 x=x+1x=x-1,不过使用递增和递减运算符时,只会计算一次 x 的值。如果 x 是有副作用的表达式,情况就大不相同了。例如,下面两个表达式不等效:

  1. a[i++]++; // 递增数组中的一个元素
  2. // 把数组中的一个元素增加1,然后把新值存储在另一个元素中
  3. a[i++] = a[i++] + 1;

这些运算符,不管放在前面还是后面,最常用来递增或递减控制循环的计数器。

2.4.5 比较运算符

比较运算符包括测试两个值是否相等的相等运算符和测试有序类型(数字和字符)数据之间大小关系的关系运算符。这两种运算符计算的结果都是布尔值,因此一般用于 if 语句、whilefor 循环,作为分支和循环的判定条件。例如:

  1. if (o != null) ...; // 不等运算符
  2. while(i < a.length) ...; // 小于运算符

Java 提供了下述相等运算符。

  • 等于(==

如果 == 运算符的两个操作数相等,计算结果为 true;否则计算结果为 false。如果操作数是基本类型,这个运算符测试两个操作数的值是否一样。如果操作数是引用类型,这个运算符测试两个操作数是否指向同一个对象或数组。尤其要注意,这个运算符不能测试两个字符串是否相等。

如果使用 == 比较两个数字或字符,而且两个操作数的类型不同,在比较之前会把取值范围窄的操作数转换成取值范围宽的操作数类型。例如,比较 short 类型的值和 float 类型的值时,在比较之前会先把 short 类型的值转换成 float 类型。对浮点数来说,特殊的负零和普通的正零相等;特殊的 NaN 和任何数,包括 NaN 自己,都不相等。如果想测试浮点数是否为 NaN,要使用 Float.isNan()Double.isNan() 方法。

  • 不等于(!=

!= 运算符完全是 == 运算符的反运算。如果两个基本类型操作数的值不同,或者两个引用类型操作数指向不同的对象或数组,!= 运算符的计算结果为 true;否则,计算结果为 false

关系运算符可用于数字和字符,但不能用于布尔值、对象和数组,因为这些类型无序。Java 提供了下述关系运算符。

  • 小于(<

如果第一个操作数小于第二个操作数,计算结果为 true

  • 小于或等于(<=

如果第一个操作数小于或等于第二个操作数,计算结果为 true

  • 大于(>

如果第一个操作数大于第二个操作数,计算结果为 true

  • 大于或等于(>=

如果第一个操作数大于或等于第二个操作数,计算结果为 true

2.4.6 逻辑运算符

如前所示,比较运算符比较两个操作数,计算结果为布尔值,经常用在分支和循环语句中。为了让分支和循环的条件判断更有趣,可以使用逻辑运算符把多个比较表达式合并成一个更复杂的表达式。逻辑运算符的操作数必须是布尔值,而且计算结果也是布尔值。逻辑运算符如下。

  • 条件与(&&

这个运算符对操作数执行逻辑与运算。仅当两个操作数都是 true 时才返回 true;如果有一个或两个操作数都是 false,计算结果为 false。例如:

  1. if (x < 10 && y > 3) ... // 如果两个比较表达式的结果都是true

这个运算符(以及除了一元运算符 ! 之外的所有逻辑运算符)的优先级没有比较运算符高,因此完全可以编写类似上面的代码。不过,有些程序员倾向于使用括号,明确表明计算的顺序:

  1. if ((x < 10) && (y > 3))...

你觉得哪种写法更易读就用哪种。

这个运算符之所以叫条件与,是因为它会视情况决定是否计算第二个操作数。如果第一个操作数的结算结果为 false,不管第二个操作数的计算结果是什么,这个表达式的计算结果都是 false。因此,为了提高效率,Java 解释器会走捷径,跳过第二个操作数。因为不一定会计算第二个操作数,所以使用这个运算符时,如果表达式有副作用,一定要注意。不过,因为有这种特性,可以使用这个运算符编写如下的 Java 表达式:

  1. if (data != null && i < data.length && data[i] != -1)
  2. ...

如果第一个和第二个比较表达式的计算结果为 false,第二个和第三个比较表达式会导致错误。幸好,我们无需为此担心,因为 && 运算符会视情况决定是否执行后面的表达式。

  • 条件或(||

这个运算符在两个布尔值操作数上执行逻辑或运算。如果其中一个或两个都是 true,计算结果为 true;如果两个操作数都是 false,计算结果为 false。和 && 运算符一样,|| 并不总会计算第二个操作数。如果第一个操作数的计算结果为 true,不管第二个操作数的计算结果是什么,表达式的计算结果都是 true。因此,遇到这种情况时,|| 运算符会跳过第二个操作数。

  • 逻辑非(!

这个运算符改变操作数的布尔值。如果应用于 true,计算结果为 false;如果应用于 false,计算结果为 true。在下面这种表达式中很有用:

  1. if (!found) ... // found是其他地方定义的布尔值
  2. while (!c.isEmpty()) ... // isEmpty()方法返回布尔值

! 是一元运算符,优先级高,经常必须使用括号:

  1. if (!(x > y && y > z))
  • 逻辑与(&

如果操作数是布尔值,& 运算符的行为和 && 运算符类似,但是不管第一个操作数的计算结果如何,总会计算第二个操作数。不过,这个运算符几乎都用作位运算符,处理整数操作数。很多 Java 程序员都认为使用这个运算符处理布尔值操作数是不合法的 Java 代码。

  • 逻辑或(|

这个运算符在两个布尔值操作数上执行逻辑或运算,和 || 运算符类似,但是就算第一个操作数的计算结果为 true,也会计算第二个操作数。| 运算符几乎都用作位运算符,处理整数操作数,很少用来处理布尔值操作数。

  • 逻辑异或(^

如果操作数是布尔值,这个运算符的计算结果是两个操作数的异或。如果两个操作数中只有一个是 true,计算结果才是 true。也就是说,如果两个操作数都是 truefalse,计算结果为 false。这个运算符与 &&|| 不同,始终会计算两个操作数。^ 运算符更常用作位运算符,处理整数操作数。如果操作数是布尔值,这个运算符等效于 != 运算符。

2.4.7 位运算符和位移运算符

位运算符和位移运算符是低层运算符,处理组成整数的单个位。现代 Java 程序很少使用位运算符,除非处理低层操作(例如网络编程)。这两种运算符用于测试和设定整数中的单个标志位。若想理解这些运算符的行为,必须先理解二进制数以及用于表示负整数的二进制补码方式。

这些运算符的操作数不能是浮点数、布尔值、数组或对象。如果操作数是布尔值,&|^ 运算符执行的是其他运算,前一节已经讲过。

如果位运算符的操作数中有一个是 long 类型,结果就是 long 类型。除此之外,结果都是 int 类型。如果位移运算符左边的操作数是 long 类型,结果为 long 类型;否则,结果是 int 类型。位运算符和位移运算符如下。

  • 按位补码(~

一元运算符 ~ 是按位补码运算符,或叫位非运算符。它把单个操作数的每一位反相,1 变成 0,0 变成 1。例如:

  1. byte b = ~12; // ~00001100 ==> 11110011或十进制数-13
  2. flags = flags & ~f; // 把标志集合flags中的f标志清除
  • 位与(&

这个运算符在两个整数操作数的每一位上执行逻辑与运算,合并这两个操作数。只有两个操作数的同一位都为 1 时,结果中对应的位才是 1。例如:

  1. 10 & 7 // 00001010 & 00000111 ==> 00000010或2
  2. if ((flags & f) != 0) // 测试是否设定了f标志

前面已经说过,如果操作数是布尔值,& 是不常使用的逻辑与运算符。

  • 位或(|

这个运算符在两个整数操作数的每一位上执行逻辑或运算,合并这两个操作数。如果两个操作数的同一位中有一个或两个都是 1,结果中对应的位是 1;如果两个操作数的同一位都是 0,结果中对应的位是 0。例如:

  1. 10 | 7 // 00001010 | 00000111 ==> 00001111或15
  2. flags = flags | f; // 设定f标志

前面已经说过,如果操作数是布尔值,| 是不常使用的逻辑或运算符。

  • 位异或(^

这个运算符在两个整数操作数的每一位上执行逻辑异或运算,合并这两个操作数。如果两个操作数的同一位值不同,结果中对应的位是 1;如果两个操作数的同一位都是 1 或 都是 0,结果中对应的位是 0。例如:

  1. 10 ^ 7 // 00001010 ^ 00000111 ==> 00001101或13

如果操作数是布尔值,^ 是很少使用的逻辑异或运算符。

  • 左移(<<

<< 运算符把左侧操作数的每一位向左移动右侧操作数指定的位数。左侧操作数的高位被丢掉,右边缺少的位补零。整数向左移 n 位,相当于乘于 2n。例如:如果左侧操作数是 long 类型,右侧操作数应该介于 0 和 63 之间。

  1. 10 << 1 // 00001010 << 1 = 00010100 = 20 = 10*2
  2. 7 << 3 // 00000111 << 3 = 00111000 = 56 = 7*8
  3. -1 << 2 // 0xFFFFFFFF << 2 = 0xFFFFFFFC = -4 = -1*4

如果左侧操作数是 int 类型,右侧操作数应该介于 0 和 31 之间。

  • 带符号右移(>>

>> 运算符把左侧操作数的每一位向右移动右侧操作数指定的位数。左侧操作符的低位被移除,移入的高位和原来的最高位一样。也就是说,如果左侧操作数是正数,移入的高位是 0;如果左侧操作数是负数,移入的高位是 1。这种技术叫高位补符号,作用是保留左侧操作数的符号。例如:

  1. 10 >> 1 // 00001010 >> 1 = 00000101 = 5 = 10/2
  2. 27 >> 3 // 00011011 >> 3 = 00000011 = 3 = 27/8
  3. -50 >> 2 // 11001110 >> 2 = 11110011 = -13 != -50/4

如果左侧操作数是正数,右侧操作数是 n,>> 运算符的计算结果相当于整数除以 2n。

  • 不带符号右移(>>>

这个运算符和 >> 类似,但是不管左侧操作数的符号是什么,高位总是移入 0。这种技术叫高位补零。左侧操作数是无符号的数字时才适用这个运算符(可是 Java 的整数类型都带符号)。下面是一些例子:

  1. 0xff >>> 4 // 11111111 >>> 4 = 00001111 = 15 = 255/16
  2. -50 >>> 2 // 0xFFFFFFCE >>> 2 = 0x3FFFFFF3 = 1073741811

2.4.8 赋值运算符

赋值运算符把值存储在某种变量中或赋予某种变量。左侧操作数必须是适当的局部变量、数组元素或对象字段。右侧操作数可以是与变量兼容的任何类型。赋值表达式的计算结果是赋予变量的值。不过,更重要的是,赋值表达式的副作用是执行赋值操作。和其他二元运算符不同的是,赋值运算符是右结合的,也就是说,赋值表达式 a=b=c 从右向左执行,即 a=(b=c)

基本的赋值运算符是 =。别把它和相等运算符 == 搞混了。为了区别这两个运算符,我们建议你把 = 读作“被赋值为”。

除了这个简单的赋值运算符之外,Java 还定义了另外 11 个运算符,其中 5 个与算术运算符一起使用,6 个与位运算符和位移运算符一起使用。例如,+= 运算符先读取左侧变量的值,再和右侧操作数相加。这种表达式的副作用是把两数之和赋值给左侧变量,返回值也是两数之和。因此,表达式 x+=2 几乎和 x=x+2 一样。这两种表达式之间的区别是,+= 运算符只会计算一次左侧操作数。如果左侧操作数有副作用,这个区别就体现出来了。如下两个表达式并不等效:

  1. a[i++] += 2;
  2. a[i++] = a[i++] + 2;

组合赋值运算符的一般格式为:

  1. var op= value

(如果 var 没有副作用)等效于:

  1. var = var op value

可用的组合赋值运算符有:

  1. += -= *= /= %= // 算术运算符加赋值运算符
  2. &= |= ^= // 位运算符加赋值运算符
  3. <<= >>= >>>= // 位移运算符加赋值运算符

其中,最常用的运算符是 +=-=,不过处理布尔值标志时,&=|= 也有用。例如:

  1. i += 2; // 循环计数器增加2
  2. c -= 5; // 计数器减小5
  3. flags |= f; // 在一组整数标志flags中设定f标志
  4. flags &= ~f; // 在一组整数标志flags中清除f标志

2.4.9 条件运算符

条件运算符 ?: 是有点晦涩的三元运算符(有三个操作数),从 C 语言继承而来,可以在一个表达式中嵌入条件判断。这个运算符可以看成是 if/else 语句的运算符版。条件运算符的第一个和第二个操作数使用问号(?)分开,第二个和第三个操作数使用冒号(:)分开。第一个操作数的计算结果必须是布尔值。第二个和第三个操作数可以是任意类型,但要能转换成同一类型。

条件运算符先计算第一个操作数,如果结果为 true,就计算第二个操作数,并把结果当成表达式的返回值;如果第一个操作数的计算结果为 false,条件运算符会计算并返回第三个操作数。条件运算符绝不会同时计算第二个和第三个操作数,所以使用有副作用的表达式时要小心。这个运算符的使用示例如下:

  1. int max = (x > y) ? x : y;
  2. String name = (name != null) ? name : "unknown";

注意,?: 运算符的优先级只比赋值运算符高,比其他运算符都低,所以一般不用把操作数放在括号里。不过,很多程序员觉得,把第一个操作数放在括号里,条件表达式更易读。的确,毕竟 if 语句的条件表达式都放在括号里。

2.4.10 instanceof操作符

instanceof 操作符与对象和 Java 的类型系统联系紧密。如果你是初次接触 Java,建议你跳过这一节,等你对 Java 的对象有充足了解后再看。

instanceof 操作符的左侧操作数是对象或数组,右侧操作数是引用类型的名称。如果对象或数组是指定类型的实例,计算结果为 true;否则,计算结果为 false。如果左侧操作数是 nullinstanceof 操作符的计算结果始终为 false。如果 instanceof 表达式的计算结果为 true,意味着可以放心校正并把左侧操作数赋值给类型为右侧操作数的变量。

instanceof 操作符只能用于引用类型和对象,不能用于基本类型和值。instanceof 操作符的使用示例如下:

  1. // true:所有字符串都是String类的实例
  2. "string" instanceof String
  3. // true:字符串也是Object类的实例
  4. "" instanceof Object
  5. // false:null不是任何类的实例
  6. null instanceof String
  7. Object o = new int[] {1,2,3};
  8. o instanceof int[] // true:这个数组是int数组
  9. o instanceof byte[] // false:这个数组不是byte数组
  10. o instanceof Object // true:所有数组都是Object类的实例
  11. // 使用instanceof确保能放心校正对象
  12. if (object instanceof Point) {
  13. Point p = (Point) object;
  14. }

2.4.11 特殊运算符

Java 有六种语言结构,有时当成运算符,有时只当成基本句法的一部分。表 2-4 也列出了这些“运算符”,以便说明相对于其他真正运算符的优先级。本书其他地方会详细介绍这些语言结构的用法,不过这里要简要说明一下,以便在代码示例中能识别它们。

  • 访问对象成员(.)

对象由一些数据和处理这些数据的方法组成。对象的数据字段和方法称为这个对象的成员。点号运算符(.)用来访问这些成员。如果 o 是一个表达式,而且计算结果为对象引用,f 是这个对象的字段名称,那么,o.f 的计算结果是字段 f 中的值。如果 m 是一个方法的名称,那么,o.m 指向这个方法,而且能使用后面介绍的 () 运算符调用。

  • 访问数组中的元素([]

数组是由编号的值组成的列表。数组中的每个元素都能使用各自的编号(或叫索引)引用。[ ] 运算符能指向数组中的单个元素。如果 a 是一个数组,i 是能计算为 int 类型的表达式,那么,a[i] 指向 a 中的一个元素。这个运算符不像其他处理整数的运算符,它强制要求数组的索引必须是 int 类型或者取值范围更窄的类型。

  • 调用方法(()

方法是一些有名称的 Java 代码,在这个名称的后面加上括号,并在括号中放零个或多个以逗号分隔的表达式,可以运行(或叫调用)方法。括号中的表达式计算得到的值是方法的参数。方法会处理这些参数,有时还会返回一个值,这个值是方法调用表达式的返回值。如果 o.m 是一个没有参数的方法,那么这个方法可以使用 o.m() 调用。假设这个方法有三个参数,那么可以使用表达式 o.m(x,y,z) 调用。Java 解释器调用方法之前,会先计算传入的参数。这些表达式始终从左至右计算(如果参数有副作用,就能体现顺序的重要性)。

  • lambda表达式(->

lambda 表达式是一些匿名的 Java 可执行代码,其实就是方法的主体,由方法的参数列表(零个或多个以逗号分隔的表达式,放在括号中)、lambda 箭头运算符和一段 Java 代码组成。如果代码段只有一个语句,可以省略标识块边界常用的花括号。

  • 创建对象(new

在 Java 中,对象和数组使用 new 运算符创建。运算符后面跟着想创建的对象类型,括号中还可以指定一些传给对象构造方法的参数。构造方法是一种特殊的代码块,用于实例化新建的对象。创建对象的句法和调用方法的句法类似。例如:

  1. new ArrayList();
  2. new Point(1,2)
  • 类型转换或强制转换(()

前面已经介绍过,括号还可以当成执行缩小类型转换(或叫强制转换)的运算符。这个运算符的第一个操作数是想转换的类型,放在括号里;第二个操作数是要转换的值,跟在括号后面。例如:

  1. (byte) 28 // 把整数字面量校正成byte类型
  2. (int) (x + 3.14f) // 把浮点数之和强制转换成整数
  3. (String)h.get(k) // 把泛型对象强制转换成字符串