1. 运算符
几乎所有运算符都只能操作基本类型(Primitives)。=、== 和 !=,它们能操作所有对象。除此以外,String 类支持 + 和 +=。
1.1 运算符的优先级和结合性
所有的数学运算符都是从左向右运算的,在 Java 中大部分运算符也是从左向右运算的。只有单目运算符、赋值运算符和三目运算符除外。
运算符优先级从高到底排列为:
| 运算符类型 | 枚举值 | ||
|---|---|---|---|
| 分隔符 | . , ; [] () {} | ||
| 单目运算符 | ++ -- ~ ! + -(单目的 + 和 - 运算符) | ||
| 强制类型转换符 | (type) | ||
| 乘、除、取余 | * / % | ||
| 加、减 | + - | ||
| 移位运算符 | << >> >>> | ||
| 关系运算符 | < > <= >= instanceof | ||
| 等价运算符 | == != | ||
| 按位与 | & | ||
| 按位异或 | ^ | ||
| 按位或 | ` | ` | |
| 条件与 | && | ||
| 条件或 | ` | ` | |
| 三目运算符 | ? : | ||
| 赋值运算符 | = += -= *= /= %= &= |
一般来说:单目运算符 > 双目运算符 > 三目运算符 > 赋值运算符
所有的关系运算符的优先级比算术运算的低,比赋值运算的高;
比较运算符中的 == 和 != 优先级比其他的低。
a *= b + 6; // 等价于 a = a * (b + 6)
1.2 赋值运算符
对于基本类型,会将值复制一份;
对于引用类型,会将引用赋值一份,此时,两者指向的堆中的对象还是同一个。这种现象通常称为别名(aliasing),是 Java 处理对象的一种基本方式。
可以与二元运算符连用,形成扩展后的赋值运算符,如下:
| 运算符 | 枚举值 |
|---|---|
| 算数运算符 | += -= /= *= %= |
| 位运算符 | &= = ^= (一元运算符 ~ 不可与 = 连用) |
| 移位运算符 | <<= >>= >>>= |
对于扩展后的赋值运算符中,会发生隐式的强制类型转换:
byte b = 2, c = 2;
b = b + c; // 编译失败,“b + c”的值自动提升到 int 类型
b += c; // 等效于 “b = (byte) (b + c)”
int a = 3;
b += a; // 等效于 “b = (byte) (b + a)”
推荐使用扩展后的赋值运算符。
1.3 算数运算符
Java 的基本算术运算符与其他大多编程语言是相同的。
-
表达式类型的自动提升
当一个算术表达式中包含多个基本类型的值时,整个算术表达式的数据类型将发生自动提升。规则如下:
- 所有的 char、byte、short 类型将被提升到 int 类型
- 整个表达式的数据类型自动提升到与表达式中最高等级操作数相等的类型
-
对于计算发生溢出时,要程序员判断结果的范围是否溢出,否则就很可能在不知不觉中丢失精度。
class TestDemo { public static void main(String[] args) { int i = Integer.MAX_VALUE; System.out.println("i = " + i); int i2 = i * 2; System.out.println("i2 = " + i2); } } /** Output: i = 2147483647 i2 = -2 */编译器没有报错或警告,运行时一切看起来都无异常。诚然,Java 是优秀的,但是还不足够优秀。
-
对于除法、取余运算中:除以 0 的操作要小心处理。
// 以下语句会抛出异常:ArithmeticException // System.out.println(12 / 0); // System.out.println(12 % 0); // System.out.println(0 % 0); // 以下语句会得到“非数值“ System.out.println(12.3 / 0.0); // Infinity System.out.println(12.0 / 0); // Infinity System.out.println(10 / 0.0); // Infinity System.out.println(0 / 0.0); // NaN System.out.println(0.0 / 0.0); // NaN System.out.println(12.3 % 0.0); // NaN System.out.println(12.0 % 0); // NaN System.out.println(10 % 0.0); // NaN System.out.println(0 % 0.0); // NaN System.out.println(0.0 % 0.0); // NaN可见,当除以 0 时,不是抛出异常就是出现”非数值“,归根结底,该算术运算本身是无意义的。
1.4 一元加减运算符
一元减号可以得到数据的负值。一元加号的作用相反,不过它唯一能影响的就是把较小的数值类型自动转换为 int 类型(发生自动类型提升)。
1.5 递增和递减
和 C 语言类似,Java 提供了许多快捷运算方式。其中包括递增 ++ 和递减 --,意为“增加或减少一个单位”。每种类型的运算符,都有两个版本可供选用;通常将其称为“前缀”和“后缀”。对于前缀形式,我们将在执行递增/减操作后获取值;使用后缀形式,我们将在执行递增/减操作之前获取值。它们是唯一具有“副作用”的运算符(除那些涉及赋值的以外)。
C++ 名称来自于递增运算符,暗示着“比 C 更进一步”。在早期的 Java 演讲中,Bill Joy(Java 作者之一)说“Java = C++ --”(C++ 减减),意味着 Java 在 C++ 的基础上减少了许多不必要的东西,因此语言更简单。随着进一步地学习,我们会发现 Java 的确有许多地方相对 C++ 来说更简便,但是在其他方面,难度并不会比 C++ 小多少。
1.6 关系运算符
关系运算符会通过产生一个布尔(boolean)结果来表示操作数之间的关系。== 和 != 可用于所有基本类型,但其他运算符不能用于基本类型 boolean,因为布尔值只能表示 true 或 false,所以比较它们之间的“大于”或“小于”没有意义。
1.6.1 == 和 equals
== 和 != 比较对象时比较的是对象的引用。当要比对象的内容是否相等时,必须使用所有对象(不包括基本类型)中都存在的 equals() 方法。
有几点需要注意的地方,下面结合代码进行说明:
-
浮点数计算会有误差,如果两数相差在误差允许范围内,则认为两数相等
double d = 0.0; for (int i = 0; i < 1000; i++) { d = d + 0.001; } System.out.println(1.0 == d); // false /** 以下代码认为误差在 1e-6 之内,两浮点数就是相等的 */ System.out.println(Math.abs(1.0 - d) < 1e-6); // true -
如果比较的两个数值都是数值型,即使数据类型不相同,只要它们的值相等,也都返回 true
char c = 'a'; int i = 97; double d = 97.0; System.out.println(c == i && i == d); // true -
Java 对整型对应的包装类使用缓存了机制,影响
==的使用。比如,Integer 缓存了 [-128, 127] 的数Integer i1 = 47; Integer i2 = 47; System.out.println(i1 == i2); // true Integer i3 = 129; Integer i4 = 129; System.out.println(i3 == i4); // false -
基本数值类型与对应的包装类在使用 == 进行比较时,会自动拆箱
Integer integer = new Integer(1000); int i = 1000; System.out.println(integer == i); // true Double d = new Double(1000.0); System.out.println(d == i); // true -
boolean 类型的变量只能与 boolean 类型的变量(或者 Boolean 对象)使用 == 进行比较
-
基本类型的值、变量不能和引用类型的变量使用
==进行比较char c = 'a'; String str = "a"; System.out.println(c == str); // 编译报错:不可比较的类型: char和java.lang.String -
使用
==进行引用类型变量比较时,只有当其具有继承关系时才可比较(即使可以比较,比较的也是二者的引用,换句话说,只有必须直响同一个对象时,才会返回 true)class A {} class B {} class Test { public static void main(String[] args) { A a = new A(); B b = new B(); System.out.println(a == b); // 编译报错:不可比较的类型: com.example.demo.A和com.example.demo.B } }
1.6 逻辑运算符
每个逻辑运算符 && (与)、||(或)和 !(非)根据参数的逻辑关系生成布尔(boolean)值。
逻辑运算符支持一种称为“短路”(short-circuiting)的现象。整个表达式会在运算到可以明确结果时就停止并返回结果,这意味着该逻辑表达式的后半部分不会被执行到。运用“短路”可以节省部分不必要的运算,从而提高程序潜在的性能。
1.7 位运算符
位运算符 & (按位与)、| (按位或)、~ (按位非)、^ 按位异或允许我们操作一个整型数字中的单个二进制位。位运算符会对两个整数(byte、short、int、long 和 char)对应的位执行布尔代数,从而产生结果。
可以对 boolean 类型变量执行与、或、异或运算,但不能执行非运算(大概是为了避免与逻辑“非”混淆)。对于布尔值,位运算符具有与逻辑运算符相同的效果,只是它们不会中途“短路”。此外,针对布尔值进行的位运算为我们新增了一个“异或”逻辑运算符,它并未包括在逻辑运算符的列表中。
// 逻辑运算符与位运算符的区别,以 || 和 | 为例
int a = 5, b = 10;
if (a > 4 || ++b > 10) {
System.out.println("a = " + a + ", b = " + b); // a = 5, b = 10
}
if (a > 4 | ++b > 10) {
System.out.println("a = " + a + ", b = " + b); // a = 5, b = 11
}
1.8 移位运算符
移位运算符面向的运算对象也是二进制的“位”,只能用于处理整数类型(byte、short、int、long 和 char)。
| 运算符 | 说明 |
|---|---|
左移运算符 << | 向左移指定位数:低位补 0。其结果可能为正也可能为负 |
右移运算符 >> | 向右移指定位数:值为正,高位补 0;值为负,高位补 1。其结果正负与原来的相同 |
无符号右移 >>> | 向右移指定位数:无论正负,高位补 0。结果永远是正数 |
几点说明:
-
如果移动 char、byte 或 short,则会在移动发生之前将其提升为 int,结果为 int。仅使用右值的 5 个低阶位。这可以防止我们移动超过 int 范围的位数。若对一个 long 值进行处理,最后得到的结果也是 long。
-
移位可以与等号
<<=或>>=或>>>=组合使用。左值被替换为其移位运算后的值。但是,问题来了,当无符号右移与赋值相结合时,若将其与 byte 或 short 一起使用的话,则结果错误。取而代之的是,它们被提升为 int 型并右移,但在重新赋值时被截断。 -
当进行移位运算时,只要被移位的二进制没有发生有效的数据丢失,左移
<<n 位相等于乘以 2^n,右移>>n 位则相当于除以 2^n(对于负数,右移要万分小心)。// 左移,无数据溢出时,正负数计算结果符合上述运算规则 System.out.println(5 << 2); // 20 (算数运算:5 * 4 = 20) System.out.println(5 << 1); // 10 (算数运算:5 * 2 = 10) System.out.println(-5 << 2); // -20 (算数运算:5 * (-4) = (-20)) System.out.println(5 << -1); // -2147483648 (不符合上述计算规则) // 右移 System.out.println(5 >> 2); // 1 (算数运算:5/4=1.25,截取整数位,结果为 1) System.out.println(5 >> 1); // 2 (算数运算:5/2=2.5,截取整数位,结果为 2) System.out.println(-5 >> 2); // -2 (不符合上述计算规则) System.out.println(-5 >> 1); // -3 (不符合上述计算规则) System.out.println(-5 >> -1); // -1 (不符合上述计算规则) -
对于 int、long 类型的数进行移位,如
a << b,当 b > 32 或 64 时,系统先用 b 对 32 或 64 求余,得到的结果才是真正移位的位数int a = 16; System.out.println(a >> 32); // 16 long b = 52; System.out.println(b << 64); // 52
1.9 三元运算符
与 if-else 相比,三元运算符更加简洁。但如果频繁使用它,会产生可读性差的代码。
与 if-else 不同的是,三元运算符是有返回结果的。
注意点:
-
三目运算符 condition? 表达式 1 : 表达式 2 中,高度注意表达式 1 和 2 在类型对齐
时,可能抛出因自动拆箱导致的 NPE 异常
Integer a = 1; Integer b = 2; Integer c = null; Boolean flag = false; // a*b 的结果是 int 类型,那么 c 会强制拆箱成 int 类型,抛出 NPE 异常 Integer result = flag ? a * b : c;
2. 控制流
大多数面向过程编程语言都有共通的某种控制语句,在 Java 使用了 C 的所有执行控制语句。
2.1 条件语句
所有的条件语句都利用条件表达式的“真”或“假”来决定执行路径。
注意:在 C/C++ 中,会将数值与 boolean 进行替换,“零为假,非零为真”,但在 Java 中,使用数值作为布尔值是非法的,Java 不会对数值进行 boolean 类型的转换。这样避免了一些因为粗心导致不容易发现的 BUG。如下:
while(x = y) {
// do sth.
}
上述代码原意是测试等价性 ==,而非赋值 =。若变量 y 非 0 的话,在 C/C++ 中,这样的赋值操作总会返回 true。于是,上面的代码示例将会无限循环。而在 Java 中,这样的表达式结果并不会转化为一个布尔值。 而编译器会试图把这个 int 型数据转换为预期应接收的布尔类型。最后,我们将会在试图运行前收到编译期错误。因此,Java 天生避免了这种陷阱发生的可能。
唯一有种情况例外:当变量 x 和 y 都是布尔值,例如 x=y 是一个逻辑表达式。除此之外,之前的那个例子,很大可能是错误。(这是因为 C/C++ 中,有“0为假,非0为真”的约定。而Java 中没有!)
2.2 选择语句
包含 if 和 switch。
2.2.1 switch
switch 支持 byte、short、int、char 和对应的包装类。
另外,在 Java 中添加了 switch 对 enum(Java 5)、String(Java 7)支持的语法糖(其本质还是对整数的支持)。(暂不支持 StringBuffer、StringBuilder)
-
switch 和 String
public class Test { public static void main(String[] args) { String str = "abc"; switch (str) { case "123": System.out.println("Hello"); break; case "abc": System.out.println("Hi"); break; default: } } }使用 CFR 反编译后得到:
/* * Decompiled with CFR 0.151. */ public class Test { public static void main(String[] args) { String str; String string = str = "abc"; int n = -1; switch (string.hashCode()) { case 48690: { if (!string.equals("123")) break; n = 0; break; } case 96354: { if (!string.equals("abc")) break; n = 1; } } switch (n) { case 0: { System.out.println("Hello"); break; } case 1: { System.out.println("Hi"); break; } } } }可以看出,对于 switch 支持 String 其实是 Java 7 给的一个语法糖。具体实现是根据 hashCode() 和 equals() 来比较的。
-
switch 和 Enum
public class TestSwitch { public static void main(String[] args) { switch (Note2.B_FLAT) { case MIDDLE_C: System.out.println("MIDDLE_C"); break; case C_SHARP: System.out.println("C_SHARP"); break; case B_FLAT: System.out.println("B_FLAT"); break; default: } } } enum Note { // 音符 MIDDLE_C, C_SHARP, B_FLAT; }使用 CFR 反编译后得到:
/* * Decompiled with CFR 0.151. */ public class TestSwitch { public static void main(String[] args) { switch (1.$SwitchMap$Note[Note.B_FLAT.ordinal()]) { case 1: { System.out.println("MIDDLE_C"); break; } case 2: { System.out.println("C_SHARP"); break; } case 3: { System.out.println("B_FLAT"); break; } } } }可以得出,switch 对于 Enum 的支持也是将其转换为整数后,进行选择。
2.3 循环语句
while,do-while 和 for 用来控制循环语句(有时也称迭代语句)。只有控制循环的布尔表达式计算结果为 false,循环语句才会停止。
for 中的每一个表达式都可以省略,但 while、do while 中的循环条件不可以省略。
for (;;); // 是可以的,但会死循环
while (); // 编译报错:非法的表达式开始
2.3.1 增强的 for-in 语法
Java 5 引入了更为简洁的“增强版 for 循环”语法来操纵数组和集合。任何一个返回数组的方法都可以使用 for-in 循环语法来遍历元素。for-in 循环也适用于任何可迭代(iterable)的 对象。
2.3.2 循环的选取
tips:可以先写循环体,再确定使用哪种循环方式。
1)如果循环次数固定,用 for;
2)如果至少执行一次,用 do while;
3)其他情况,用 while。
2.3.3 循环的关注点
关注两点:循环执行次数、循环变量。
Q1:循环会被执行多少次,循环变量的范围是什么?
Q2:循环变量的值是如何变化的(步长是多少)?循环结束后,循环变量的值是多少?
2.4 控制语句
控制语句包含:return、break 和 continue。
2.4.1 臭名昭著的 goto
goto 关键字很早就在程序设计语言中出现。事实上,goto 起源于汇编(assembly language)语言中的程序控制:“若条件 A 成立,则跳到这里;否则跳到那里”。如果你读过由编译器编译后的代码,你会发现在其程序控制中充斥了大量的跳转。较之汇编产生的代码直接运行在硬件 CPU 中,Java 也会产生自己的“汇编代码”(字节码),只不过它是运行在 Java 虚拟机里的(Java Virtual Machine)。
一个源码级别跳转的 goto,为何招致名誉扫地呢?若程序总是从一处跳转到另一处,还有什么办法能识别代码的控制流程呢?随着 Edsger Dijkstra发表著名的 “Goto 有害” 论(Goto considered harmful)以后,goto 便从此失宠。甚至有人建议将它从关键字中剔除。
正如上述提及的经典情况,我们不应走向两个极端。问题不在 goto,而在于过度使用 goto。在极少数情况下,goto 实际上是控制流程的最佳方式。
尽管 goto 仍是 Java 的一个保留字,但其并未被正式启用。可以说, Java 中并不支持 goto。然而,在 break 和 continue 这两个关键字的身上,我们仍能看出一些 goto 的影子。它们并不属于一次跳转,而是中断循环语句的一种方法。之所以把它们纳入 goto 问题中一起讨论,是由于它们使用了相同的机制:标签。
对 Java 来说,唯一用到标签的地方是在循环语句之前。在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中 break 或 continue。
参考
- 《Java 编程思想》
- 《疯狂 Java 讲义》
- 《阿里巴巴开发手册》