Version:1.0 StartHTML:0000000163 EndHTML:0000092209 StartFragment:0000041077 EndFragment:0000092169 SourceURL:file:///Z:/thinking_in_java/java_reveiwv2.docx
3.1 Java运算符
几乎所有运算符都只能操作原始数据类型(Primitives)。唯一的例外是“=”、“==”和“!=”,它们能操作所有对象(也是对象易令人混淆的一个地方)。除此以外,String 类支持“+”和“+=”。
赋值
赋值是用等号运算符(=)进行的。它的意思是“取得右边的值,把它复制到左边”。右边的值可以是任何常 数、变量或者表达式,只要能产生一个值就行。但左边的值必须是一个明确的、已命名的变量。也就是说, 它必须有一个物理性的空间来保存右边的值。举个例子来说,可将一个常数赋给一个变量(A=4;),但不可 将任何东西赋给一个常数(比如不能4=A)。
但在为对象“赋值”的时候,情况却发生了变化。对一个对象进行操作时,我们真正操作的是它的句柄。所 以倘若“从一个对象到另一个对象”赋值,实际就是将句柄从一个地方复制到另一个地方。这意味着假若为 对象使用“C=D”,那么C 和 D 最终都会指向最初只有 D 才指向的那个对象。
Example:
class Number {
int i;
}
public class Assignment {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
n1.i = 9;
n2.i = 47;
System.out.println("1: n1.i: " + n1.i +
", n2.i: " + n2.i);
n1 = n2;
System.out.println("2: n1.i: " + n1.i +
", n2.i: " + n2.i);
n1.i = 27;
System.out.println("3: n1.i: " + n1.i +
", n2.i: " + n2.i);
}
}
Console output:
1: n1.i: 9, n2.i: 47
2: n1.i: 47, n2.i: 47
3: n1.i: 27, n2.i: 27
看来改变n1 的同时也改变了n2!这是由于无论n1 还是n2 都包含了相同的句柄,它指向相同的对象(最初 的句柄位于 n1 内部,指向容纳了值9 的一个对象。在赋值过程中,那个句柄实际已经丢失;它的对象会由 “垃圾收集器”自动清除)。 这种特殊的现象通常也叫作“别名”,是 Java 操作对象的一种基本方式。但假若不愿意在这种情况下出现别 名,又该怎么操作呢?可放弃赋值,并写入下述代码:
n1.i = n2.i;
这样便可保留两个独立的对象,而不是将 n1 和n2 绑定到相同的对象。
自动递增和递减
对每种类型的运算符,都有两个版本可供选用;通常将其称为“前缀版”和“后缀版”。“前递增”表示++ 运算符位于变量或表达式的前面;而“后递增”表示++运算符位于变量或表达式的后面。类似地,“前递 减”意味着--运算符位于变量或表达式的前面;而“后递减”意味着--运算符位于变量或表达式的后面。对 于前递增和前递减(如++A 或--A),会先执行运算,再生成值。而对于后递增和后递减(如A++或A--), 会先生成值,再执行运算。下面是一个例子:
//: AutoInc.java
//Demonstrates the ++ and -- operators
public class Assignment {
public static void main(String[] args) {
int i = 1;
prt("i : " + i);
prt("++i : " + ++i); // Pre-increment
prt("i++ : " + i++); // Post-increment
prt("i : " + i);
prt("--i : " + --i); // Pre-decrement
prt("i-- : " + i--); // Post-decrement
prt("i : " + i);
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
关系运算符
关系运算符生成的是一个“布尔”(Boolean)结果。它们评价的是运算对象值之间的关系。若关系是真实
的,关系表达式会生成 true(真);若关系不真实,则生成false(假)。关系运算符包括小于(<)、大于
(>)、小于或等于(<=)、大于或等于(>=)、等于(==)以及不等于(!=)。等于和不等于适用于所有内
建的数据类型,但其他比较不适用于boolean 类型。
检查对象是否相等
//: Equivalence.java
public class Assignment {
public static void main(String[] args) {
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1 == n2);
System.out.println(n1 != n2);
}
} ///:~
其中,表达式System.out.println(n1 == n2)可打印出内部的布尔比较结果。一般人都会认为输出结果肯定
先是true,再是 false,因为两个 Integer 对象都是相同的。但尽管对象的内容相同,句柄却是不同的,而
==和!=比较的正好就是对象句柄。所以输出结果实际上先是 false,再是 true。这自然会使第一次接触的人
感到惊奇。
若想对比两个对象的实际内容是否相同,又该如何操作呢?此时,必须使用所有对象都适用的特殊方法
equals()。
//: EqualsMethod.java
public class EqualsMethod {
public static void main(String[] args) {
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1.equals(n2));
}
} ///:~
正如我们预计的那样,此时得到的结果是 true。但事情并未到此结束!假设您创建了自己的类,就象下面这样:
//: EqualsMethod2.java
class Value {
int i;
}
public class EqualsMethod2 {
public static void main(String[] args) {
Value v1 = new Value();
Value v2 = new Value();
v1.i = v2.i = 100;
System.out.println(v1.equals(v2));
}
} ///:~
此时的结果又变回了false!这是由于 equals()的默认行为是比较句柄。所以除非在自己的新类中改变了
equals(),否则不可能表现出我们希望的行为。不幸的是,要到第 7 章才会学习如何改变行为。但要注意
equals()的这种行为方式同时或许能够避免一些“灾难”性的事件。
大多数 Java 类库都实现了 equals(),所以它实际比较的是对象的内容,而非它们的句柄。
逻辑运算符
逻辑运算符 AND(&&)、OR(||)以及 NOT(!)能生成一个布尔值(true 或 false)——以自变量的逻辑关系为基础。
短路
操作逻辑运算符时,我们会遇到一种名为“短路”的情况。这意味着只有明确得出整个表达式真或假的结
论,才会对表达式进行逻辑求值。因此,一个逻辑表达式的所有部分都有可能不进行求值:
public class Assignment {
static boolean test1(int val) {
System.out.println("test1(" + val + ")");
System.out.println("result: " + (val < 1));
return val < 1;
}
static boolean test2(int val) {
System.out.println("test2(" + val + ")");
System.out.println("result: " + (val < 2));
return val < 2;
}
static boolean test3(int val) {
System.out.println("test3(" + val + ")");
System.out.println("result: " + (val < 3));
return val < 3;
}
public static void main(String[] args) {
if(test1(0) && test2(2) && test3(2))
System.out.println("expression is true");
else
System.out.println("expression is false");
}
} ///:~
第一个测试生成一个true 结果,所以表达式求值会继续下去。然而,第二个测试产生了一个 false 结果。由于这意味着整个表达式肯定为 false,所以为什么还要继续剩余的表达式呢?这样做只会徒劳无益。事实
上,“短路”一词的由来正种因于此。如果一个逻辑表达式的所有部分都不必执行下去,那么潜在的性能提
升将是相当可观的。
按位运算符
按位运算符允许我们操作一个整数主数据类型中的单个“比特”,即二进制位。按位运算符会对两个自变量
中对应的位执行布尔代数,并最终生成一个结果。
按位运算来源于C 语言的低级操作。我们经常都要直接操纵硬件,需要频繁设置硬件寄存器内的二进制位。
Java 的设计初衷是嵌入电视顶置盒内,所以这种低级操作仍被保留下来了。然而,由于操作系统的进步,现在也许不必过于频繁地进行按位运算。
若两个输入位都是 1,则按位AND 运算符(&)在输出位里生成一个 1;否则生成 0。若两个输入位里至少有一个是 1,则按位 OR 运算符(|)在输出位里生成一个 1;只有在两个输入位都是0 的情况下,它才会生成一个0。若两个输入位的某一个是1,但不全都是1,那么按位 XOR(^,异或)在输出位里生成一个 1。按位NOT(~,也叫作“非”运算符)属于一元运算符;它只对一个自变量进行操作(其他所有运算符都是二元运算符)。按位 NOT 生成与输入位的相反的值——若输入 0,则输出1;输入 1,则输出0。
按位运算符和逻辑运算符都使用了同样的字符,只是数量不同。因此,我们能方便地记忆各自的含义:由于“位”是非常“小”的,所以按位运算符仅使用了一个字符。
按位运算符可与等号(=)联合使用,以便合并运算及赋值:&=,|=和^=都是合法的(由于~是一元运算符,所以不可与=联合使用)。我们将 boolean(布尔)类型当作一种“单位”或“单比特”值对待,所以它多少有些独特的地方。我们可执行按位AND,OR 和 XOR,但不能执行按位 NOT(大概是为了避免与逻辑 NOT 混淆)。对于布尔值,按位运算符具有与逻辑运算符相同的效果,只是它们不会中途“短路”。此外,针对布尔值进行的按位运算为我们新增了一个XOR 逻辑运算符,它并未包括在“逻辑”运算符的列表中。在移位表达式中,我们被禁止使用布尔运算,原因将在下面解释。
移位运算符
移位运算符面向的运算对象也是二进制的“位”。可单独用它们处理整数类型(主类型的一种)。左移位运
算符(<<)能将运算符左边的运算对象向左移动运算符右侧指定的位数(在低位补 0)。“有符号”右移位
运算符(>>)则将运算符左边的运算对象向右移动运算符右侧指定的位数。“有符号”右移位运算符使用了
“符号扩展”:若值为正,则在高位插入 0;若值为负,则在高位插入1。Java 也添加了一种“无符号”右
移位运算符(>>>),它使用了“零扩展”:无论正负,都在高位插入0。这一运算符是C 或C++没有的。
若对char,byte 或者short 进行移位处理,那么在移位进行之前,它们会自动转换成一个 int。只有右侧的5个低位才会用到。这样可防止我们在一个 int 数里移动不切实际的位数。若对一个 long 值进行处理,最后得到的结果也是long。此时只会用到右侧的 6 个低位,防止移动超过 long 值里现成的位数。但在进行“无符号”右移位时,也可能遇到一个问题。若对 byte 或short 值进行右移位运算,得到的可能不是正确的结果(Java 1.0 和Java 1.1 特别突出)。它们会自动转换成int 类型,并进行右移位。但“零扩展”不会发
生,所以在那些情况下会得到-1 的结果。
三元if -else运算符
布尔表达式 ? 值 0:值 1
若“布尔表达式”的结果为true,就计算“值0”,而且它的结果成为最终由运算符产生的值。但若“布尔
表达式”的结果为 false,计算的就是“值 1”,而且它的结果成为最终由运算符产生的值。
当然,也可以换用普通的if-else 语句(在后面介绍),但三元运算符更加简洁。尽管 C 引以为傲的就是它
是一种简练的语言,而且三元运算符的引入多半就是为了体现这种高效率的编程,但假若您打算频繁用它,
还是要先多作一些思量——它很容易就会产生可读性极差的代码。
造型运算符
“造型”(Cast)的作用是“与一个模型匹配”。在适当的时候,Java 会将一种数据类型自动转换成另一
种。例如,假设我们为浮点变量分配一个整数值,计算机会将 int 自动转换成 float。通过造型,我们可明
确设置这种类型的转换,或者在一般没有可能进行的时候强迫它进行。
为进行一次造型,要将括号中希望的数据类型(包括所有修改符)置于其他任何值的左侧。下面是一个例
子:
void casts() {
int i = 200;
long l = (long)i;
long l2 = (long)200;
}
正如您看到的那样,既可对一个数值进行造型处理,亦可对一个变量进行造型处理。但在这儿展示的两种情况下,造型均是多余的,因为编译器在必要的时候会自动进行 int 值到long 值的转换。当然,仍然可以设置一个造型,提醒自己留意,也使程序更清楚。在其他情况下,造型只有在代码编译时才显出重要性。
大家可以看到,除 boolean 以外,任何一种主类型都可通过造型变为其他主类型。同样地,当造型成一种较小的类型时,必须留意“缩小转换”的后果。否则会在造型过程中不知不觉地丢失信息。
3.2 执行控制
Java和C,C++的真和假
所有条件语句都利用条件表达式的真或假来决定执行流程。条件表达式的一个例子是 A==B。它用条件运算符“==”来判断A 值是否等于 B 值。该表达式返回 true 或 false。本章早些时候接触到的所有关系运算符都可拿来构造一个条件语句。注意 Java 不允许我们将一个数字作为布尔值使用,即使它在 C 和 C++里是允许的(真是非零,而假是零)。若想在一次布尔测试中使用一个非布尔值——比如在 if(a)里,那么首先必须用一个条件表达式将其转换成一个布尔值,例如 if(a!=0)。
If-else
if(布尔表达式)
语句
else
语句
While
while(布尔表达式)
语句
在循环刚开始时,会计算一次“布尔表达式”的值。而对于后来每一次额外的循环,都会在开始前重新计算
一次。
do-while
do
语句
while(布尔表达式)
while 和do-while 唯一的区别就是do-while 肯定会至少执行一次;也就是说,至少会将其中的语句“过一
遍”——即便表达式第一次便计算为false。而在 while 循环结构中,若条件第一次就为false,那么其中的
语句根本不会执行。在实际应用中,while 比 do-while 更常用一些。
For
for 循环在第一次反复之前要进行初始化。随后,它会进行条件测试,而且在每一次反复的时候,进行某种
形式的“步进”(Stepping)。for 循环的形式如下:
for(初始表达式; 布尔表达式; 步进)
语句
无论初始表达式,布尔表达式,还是步进,都可以置空。每次反复前,都要测试一下布尔表达式。若获得的结果是 false,就会继续执行紧跟在 for 语句后面的那行代码。在每次循环的末尾,会计算一次步进。
for 循环通常用于执行“计数”任务。
中断和继续
在任何循环语句的主体部分,亦可用break 和continue 控制循环的流程。其中,break 用于强行退出循环,
不执行循环中剩余的语句。而continue 则停止执行当前的反复,然后退回循环起始和,开始新的反复。
开关switch
“开关”(Switch)有时也被划分为一种“选择语句”。根据一个整数表达式的值,switch 语句可从一系列
代码选出一段执行。它的格式如下:
switch(整数选择因子) {
case 整数值1 : 语句; break;
case 整数值2 : 语句; break;
case 整数值3 : 语句; break;
case 整数值4 : 语句; break;
case 整数值5 : 语句; break;
//..
default:语句;
其中,“整数选择因子”是一个特殊的表达式,能产生整数值。switch 能将整数选择因子的结果与每个整数
值比较。若发现相符的,就执行对应的语句(简单或复合语句)。若没有发现相符的,就执行default 语
句。
在上面的定义中,大家会注意到每个case 均以一个break 结尾。这样可使执行流程跳转至switch 主体的末
尾。这是构建switch 语句的一种传统方式,但break 是可选的。若省略break,会继续执行后面的case 语
句的代码,直到遇到一个break 为止。尽管通常不想出现这种情况,但对有经验的程序员来说,也许能够善
加利用。注意最后的default 语句没有break,因为执行流程已到了break 的跳转目的地。当然,如果考虑
到编程风格方面的原因,完全可以在default 语句的末尾放置一个break,尽管它并没有任何实际的用处。
switch 语句是实现多路选择的一种易行方式(比如从一系列执行路径中挑选一个)。但它要求使用一个选择
因子,并且必须是int 或char 那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它
们在switch 语句里是不会工作的。对于非整数类型,则必须使用一系列if 语句。
将一个float 或double 值造型成整数值后,总是将小数部分“砍掉”,不作任何进位处理。
如果没有break语句,那么程序会继续向下执行。直到有下一个break出现跳出switch。