第3章 控制程序流程 “就象任何有感知的生物一样,程序必须能操纵自己的世界,在执行过程中作出判断与选择。” 在Java里,我们利用运算符操纵对象和数据,并用执行控制语句作出选择。Java是建立在C++基础上的,所以对C和C++程序员来说,对Java这方面的大多数语句和运算符都应是非常熟悉的。当然,Java也进行了自己的一些改进与简化工作。 3.1 使用Java运算符 运算符以一个或多个自变量为基础,可生成一个新值。自变量采用与原始方法调用不同的一种形式,但效果是相同的。根据以前写程序的经验,运算符的常规概念应该不难理解。 加号(+)、减号和负号(-)、乘号(*)、除号(/)以及等号(=)的用法与其他所有编程语言都是类似的。 所有运算符都能根据自己的运算对象生成一个值。除此以外,一个运算符可改变运算对象的值,这叫作“副作用”(Side Effect)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。 几乎所有运算符都只能操作“主类型”(Primitives)。唯一的例外是“=”、“==”和“!=”,它们能操作所有对象(也是对象易令人混淆的一个地方)。除此以外,String类支持“+”和“+=”。 3.1.1 优先级 运算符的优先级决定了存在多个运算符时一个表达式各部分的计算顺序。Java对计算顺序作出了特别的规定。其中,最简单的规则就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先级规则,所以应该用括号明确规定计算顺序。例如: A = X + Y - 2/2 + Z; 为上述表达式加上括号后,就有了一个不同的含义。 A = X + (Y - 2)/(2 + Z); 3.1.2 赋值 赋值是用等号运算符(=)进行的。它的意思是“取得右边的值,把它复制到左边”。右边的值可以是任何常数、变量或者表达式,只要能产生一个值就行。但左边的值必须是一个明确的、已命名的变量。也就是说,它必须有一个物理性的空间来保存右边的值。举个例子来说,可将一个常数赋给一个变量(A=4;),但不可将任何东西赋给一个常数(比如不能4=A)。 对主数据类型的赋值是非常直接的。由于主类型容纳了实际的值,而且并非指向一个对象的句柄,所以在为其赋值的时候,可将来自一个地方的内容复制到另一个地方。例如,假设为主类型使用“A=B”,那么B处的内容就复制到A。若接着又修改了A,那么B根本不会受这种修改的影响。作为一名程序员,这应成为自己的常识。 但在为对象“赋值”的时候,情况却发生了变化。对一个对象进行操作时,我们真正操作的是它的句柄。所以倘若“从一个对象到另一个对象”赋值,实际就是将句柄从一个地方复制到另一个地方。这意味着假若为对象使用“C=D”,那么C和D最终都会指向最初只有D才指向的那个对象。下面这个例子将向大家阐示这一点。 这里有一些题外话。在后面,大家在代码示例里看到的第一个语句将是“package 03”使用的“package”语句,它代表本书第3章。本书每一章的第一个代码清单都会包含象这样的一个“package”(封装、打包、包裹)语句,它的作用是为那一章剩余的代码建立章节编号。在第17章,大家会看到第3章的所有代码清单(除那些有不同封装名称的以外)都会自动置入一个名为c03的子目录里;第4章的代码置入c04;以此类推。所有这些都是通过第17章展示的CodePackage.java程序实现的;“封装”的基本概念会在第5章进行详尽的解释。就目前来说,大家只需记住象“package 03”这样的形式只是用于为某一章的代码清单建立相应的子目录。 为运行程序,必须保证在classpath里包含了我们安装本书源码文件的根目录(那个目录里包含了c02,c03c,c04等等子目录)。 对于Java后续的版本(1.1.4和更高版本),如果您的main()用package语句封装到一个文件里,那么必须在程序名前面指定完整的包裹名称,否则不能运行程序。在这种情况下,命令行是: java c03.Assignment 运行位于一个“包裹”里的程序时,随时都要注意这方面的问题。 下面是例子: //: Assignment.java // Assignment with objects is a bit tricky package c03; 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); } } ///:~ Number类非常简单,它的两个实例(n1和n2)是在main()里创建的。每个Number中的i值都赋予了一个不同的值。随后,将n2赋给n1,而且n1发生改变。在许多程序设计语言中,我们都希望n1和n2任何时候都相互独立。但由于我们已赋予了一个句柄,所以下面才是真实的输出: 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绑定到相同的对象。但您很快就会意识到,这样做会使对象内部的字段处理发生混乱,并与标准的面向对象设计准则相悖。由于这并非一个简单的话题,所以留待第12章详细论述,那一章是专门讨论别名的。其时,大家也会注意到对象的赋值会产生一些令人震惊的效果。 1. 方法调用中的别名处理 将一个对象传递到方法内部时,也会产生别名现象。 //: PassObject.java // Passing objects to methods can be a bit tricky class Letter { char c; } public class PassObject { static void f(Letter y) { y.c = 'z'; } public static void main(String[] args) { Letter x = new Letter(); x.c = 'a'; System.out.println("1: x.c: " + x.c); f(x); System.out.println("2: x.c: " + x.c); } } ///:~ 在许多程序设计语言中,f()方法表面上似乎要在方法的作用域内制作自己的自变量Letter y的一个副本。但同样地,实际传递的是一个句柄。所以下面这个程序行: y.c = 'z'; 实际改变的是f()之外的对象。输出结果如下: 1: x.c: a 2: x.c: z 别名和它的对策是非常复杂的一个问题。尽管必须等至第12章才可获得所有答案,但从现在开始就应加以重视,以便提早发现它的缺点。 3.1.3 算术运算符 Java的基本算术运算符与其他大多数程序设计语言是相同的。其中包括加号(+)、减号(-)、除号(/)、乘号(*)以及模数(%,从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。 Java也用一种简写形式进行运算,并同时进行赋值操作。这是由等号前的一个运算符标记的,而且对于语言中的所有运算符都是固定的。例如,为了将4加到变量x,并将结果赋给x,可用:x+=4。 下面这个例子展示了算术运算符的各种用法: //: MathOps.java // Demonstrates the mathematical operators import java.util.*; public class MathOps { // Create a shorthand to save typing: static void prt(String s) { System.out.println(s); } // shorthand to print a string and an int: static void pInt(String s, int i) { prt(s + " = " + i); } // shorthand to print a string and a float: static void pFlt(String s, float f) { prt(s + " = " + f); } public static void main(String[] args) { // Create a random number generator, // seeds with current time by default: Random rand = new Random(); int i, j, k; // '%' limits maximum value to 99: j = rand.nextInt() % 100; k = rand.nextInt() % 100; pInt("j",j); pInt("k",k); i = j + k; pInt("j + k", i); i = j - k; pInt("j - k", i); i = k / j; pInt("k / j", i); i = k * j; pInt("k * j", i); i = k % j; pInt("k % j", i); j %= k; pInt("j %= k", j); // Floating-point number tests: float u,v,w; // applies to doubles, too v = rand.nextFloat(); w = rand.nextFloat(); pFlt("v", v); pFlt("w", w); u = v + w; pFlt("v + w", u); u = v - w; pFlt("v - w", u); u = v * w; pFlt("v * w", u); u = v / w; pFlt("v / w", u); // the following also works for // char, byte, short, int, long, // and double: u += v; pFlt("u += v", u); u -= v; pFlt("u -= v", u); u *= v; pFlt("u *= v", u); u /= v; pFlt("u /= v", u); } } ///:~ 我们注意到的第一件事情就是用于打印(显示)的一些快捷方法:prt()方法打印一个String;pInt()先打印一个String,再打印一个int;而pFlt()先打印一个String,再打印一个float。当然,它们最终都要用System.out.println()结尾。 为生成数字,程序首先会创建一个Random(随机)对象。由于自变量是在创建过程中传递的,所以Java将当前时间作为一个“种子值”,由随机数生成器利用。通过Random对象,程序可生成许多不同类型的随机数字。做法很简单,只需调用不同的方法即可:nextInt(),nextLong(),nextFloat()或者nextDouble()。 若随同随机数生成器的结果使用,模数运算符(%)可将结果限制到运算对象减1的上限(本例是99)之下。 1. 一元加、减运算符 一元减号(-)和一元加号(+)与二元加号和减号都是相同的运算符。根据表达式的书写形式,编译器会自动判断使用哪一种。例如下述语句: x = -a; 它的含义是显然的。编译器能正确识别下述语句: x = a * -b; 但读者会被搞糊涂,所以最好更明确地写成: x = a * (-b); 一元减号得到的运算对象的负值。一元加号的含义与一元减号相反,虽然它实际并不做任何事情。 3.1.4 自动递增和递减 和C类似,Java提供了丰富的快捷运算方式。这些快捷运算可使代码更清爽,更易录入,也更易读者辨读。 两种很不错的快捷运算方式是递增和递减运算符(常称作“自动递增”和“自动递减”运算符)。其中,递减运算符是“--”,意为“减少一个单位”;递增运算符是“++”,意为“增加一个单位”。举个例子来说,假设A是一个int(整数)值,则表达式++A就等价于(A = A + 1)。递增和递减运算符结果生成的是变量的值。 对每种类型的运算符,都有两个版本可供选用;通常将其称为“前缀版”和“后缀版”。“前递增”表示++运算符位于变量或表达式的前面;而“后递增”表示++运算符位于变量或表达式的后面。类似地,“前递减”意味着--运算符位于变量或表达式的前面;而“后递减”意味着--运算符位于变量或表达式的后面。对于前递增和前递减(如++A或--A),会先执行运算,再生成值。而对于后递增和后递减(如A++或A--),会先生成值,再执行运算。下面是一个例子: //: AutoInc.java // Demonstrates the ++ and -- operators public class AutoInc { 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); } } ///:~ 该程序的输出如下: i : 1 ++i : 2 i++ : 2 i : 3 --i : 2 i-- : 2 i : 1 从中可以看到,对于前缀形式,我们在执行完运算后才得到值。但对于后缀形式,则是在运算执行之前就得到值。它们是唯一具有“副作用”的运算符(除那些涉及赋值的以外)。也就是说,它们会改变运算对象,而不仅仅是使用自己的值。 递增运算符正是对“C++”这个名字的一种解释,暗示着“超载C的一步”。在早期的一次Java演讲中,Bill Joy(始创人之一)声称“Java=C++--”(C加加减减),意味着Java已去除了C++一些没来由折磨人的地方,形成一种更精简的语言。正如大家会在这本书中学到的那样,Java的许多地方都得到了简化,所以Java的学习比C++更容易。 3.1.5 关系运算符 关系运算符生成的是一个“布尔”(Boolean)结果。它们评价的是运算对象值之间的关系。若关系是真实的,关系表达式会生成true(真);若关系不真实,则生成false(假)。关系运算符包括小于(<)、大于(>)、小于或等于(<=)、大于或等于(>=)、等于(==)以及不等于(!=)。等于和不等于适用于所有内建的数据类型,但其他比较不适用于boolean类型。 1. 检查对象是否相等 关系运算符==和!=也适用于所有对象,但它们的含义通常会使初涉Java领域的人找不到北。下面是一个例子: //: Equivalence.java public class Equivalence { 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(),所以它实际比较的是对象的内容,而非它们的句柄。 3.1.6 逻辑运算符 逻辑运算符AND(&&)、OR(||)以及NOT(!)能生成一个布尔值(true或false)——以自变量的逻辑关系为基础。下面这个例子向大家展示了如何使用关系和逻辑运算符。 //: Bool.java // Relational and logical operators import java.util.*; public class Bool { public static void main(String[] args) { Random rand = new Random(); int i = rand.nextInt() % 100; int j = rand.nextInt() % 100; prt("i = " + i); prt("j = " + j); prt("i > j is " + (i > j)); prt("i < j is " + (i < j)); prt("i >= j is " + (i >= j)); prt("i <= j is " + (i <= j)); prt("i == j is " + (i == j)); prt("i != j is " + (i != j)); // Treating an int as a boolean is // not legal Java //! prt("i && j is " + (i && j)); //! prt("i || j is " + (i || j)); //! prt("!i is " + !i); prt("(i < 10) && (j < 10) is " + ((i < 10) && (j < 10)) ); prt("(i < 10) || (j < 10) is " + ((i < 10) || (j < 10)) ); } static void prt(String s) { System.out.println(s); } } ///:~ 只可将AND,OR或NOT应用于布尔值。与在C及C++中不同,不可将一个非布尔值当作布尔值在逻辑表达式中使用。若这样做,就会发现尝试失败,并用一个“//!”标出。然而,后续的表达式利用关系比较生成布尔值,然后对结果进行逻辑运算。 输出列表看起来象下面这个样子: i = 85 j = 4 i > j is true i < j is false i >= j is true i <= j is false i == j is false i != j is true (i < 10) && (j < 10) is false (i < 10) || (j < 10) is true 注意若在预计为String值的地方使用,布尔值会自动转换成适当的文本形式。 在上述程序中,可将对int的定义替换成除boolean以外的其他任何主数据类型。但要注意,对浮点数字的比较是非常严格的。即使一个数字仅在小数部分与另一个数字存在极微小的差异,仍然认为它们是“不相等”的。即使一个数字只比零大一点点(例如2不停地开平方根),它仍然属于“非零”值。 1. 短路 操作逻辑运算符时,我们会遇到一种名为“短路”的情况。这意味着只有明确得出整个表达式真或假的结论,才会对表达式进行逻辑求值。因此,一个逻辑表达式的所有部分都有可能不进行求值: //: ShortCircuit.java // Demonstrates short-circuiting behavior // with logical operators. public class ShortCircuit { 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"); } } ///:~ 每次测试都会比较自变量,并返回真或假。它不会显示与准备调用什么有关的资料。测试在下面这个表达式中进行: if(test1(0)) && test2(2) && test3(2)) 很自然地,你也许认为所有这三个测试都会得以执行。但希望输出结果不至于使你大吃一惊: if(test1(0) && test2(2) && test3(2)) 第一个测试生成一个true结果,所以表达式求值会继续下去。然而,第二个测试产生了一个false结果。由于这意味着整个表达式肯定为false,所以为什么还要继续剩余的表达式呢?这样做只会徒劳无益。事实上,“短路”一词的由来正种因于此。如果一个逻辑表达式的所有部分都不必执行下去,那么潜在的性能提升将是相当可观的。 3.1.7 按位运算符 按位运算符允许我们操作一个整数主数据类型中的单个“比特”,即二进制位。按位运算符会对两个自变量中对应的位执行布尔代数,并最终生成一个结果。 按位运算来源于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逻辑运算符,它并未包括在“逻辑”运算符的列表中。在移位表达式中,我们被禁止使用布尔运算,原因将在下面解释。 3.1.8 移位运算符 移位运算符面向的运算对象也是二进制的“位”。可单独用它们处理整数类型(主类型的一种)。左移位运算符(<<)能将运算符左边的运算对象向左移动运算符右侧指定的位数(在低位补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的结果。可用下面这个例子检测自己的实现方案: //: URShift.java // Test of unsigned right shift public class URShift { public static void main(String[] args) { int i = -1; i >>>= 10; System.out.println(i); long l = -1; l >>>= 10; System.out.println(l); short s = -1; s >>>= 10; System.out.println(s); byte b = -1; b >>>= 10; System.out.println(b); } } ///:~ 移位可与等号(<<=或>>=或>>>=)组合使用。此时,运算符左边的值会移动由右边的值指定的位数,再将得到的结果赋回左边的值。 下面这个例子向大家阐示了如何应用涉及“按位”操作的所有运算符,以及它们的效果: //: BitManipulation.java // Using the bitwise operators import java.util.*; public class BitManipulation { public static void main(String[] args) { Random rand = new Random(); int i = rand.nextInt(); int j = rand.nextInt(); pBinInt("-1", -1); pBinInt("+1", +1); int maxpos = 2147483647; pBinInt("maxpos", maxpos); int maxneg = -2147483648; pBinInt("maxneg", maxneg); pBinInt("i", i); pBinInt("~i", ~i); pBinInt("-i", -i); pBinInt("j", j); pBinInt("i & j", i & j); pBinInt("i | j", i | j); pBinInt("i ^ j", i ^ j); pBinInt("i << 5", i << 5); pBinInt("i >> 5", i >> 5); pBinInt("(~i) >> 5", (~i) >> 5); pBinInt("i >>> 5", i >>> 5); pBinInt("(~i) >>> 5", (~i) >>> 5); long l = rand.nextLong(); long m = rand.nextLong(); pBinLong("-1L", -1L); pBinLong("+1L", +1L); long ll = 9223372036854775807L; pBinLong("maxpos", ll); long lln = -9223372036854775808L; pBinLong("maxneg", lln); pBinLong("l", l); pBinLong("~l", ~l); pBinLong("-l", -l); pBinLong("m", m); pBinLong("l & m", l & m); pBinLong("l | m", l | m); pBinLong("l ^ m", l ^ m); pBinLong("l << 5", l << 5); pBinLong("l >> 5", l >> 5); pBinLong("(~l) >> 5", (~l) >> 5); pBinLong("l >>> 5", l >>> 5); pBinLong("(~l) >>> 5", (~l) >>> 5); } static void pBinInt(String s, int i) { System.out.println( s + ", int: " + i + ", binary: "); System.out.print(" "); for(int j = 31; j >=0; j--) if(((1 << j) & i) != 0) System.out.print("1"); else System.out.print("0"); System.out.println(); } static void pBinLong(String s, long l) { System.out.println( s + ", long: " + l + ", binary: "); System.out.print(" "); for(int i = 63; i >=0; i--) if(((1L << i) & l) != 0) System.out.print("1"); else System.out.print("0"); System.out.println(); } } ///:~ 程序末尾调用了两个方法:pBinInt()和pBinLong()。它们分别操作一个int和long值,并用一种二进制格式输出,同时附有简要的说明文字。目前,可暂时忽略它们具体的实现方案。 大家要注意的是System.out.print()的使用,而不是System.out.println()。print()方法不会产生一个新行,以便在同一行里罗列多种信息。 除展示所有按位运算符针对int和long的效果之外,本例也展示了int和long的最小值、最大值、+1和-1值,使大家能体会它们的情况。注意高位代表正负号:0为正,1为负。下面列出int部分的输出: -1, int: -1, binary: 11111111111111111111111111111111 +1, int: 1, binary: 00000000000000000000000000000001 maxpos, int: 2147483647, binary: 01111111111111111111111111111111 maxneg, int: -2147483648, binary: 10000000000000000000000000000000 i, int: 59081716, binary: 00000011100001011000001111110100 ~i, int: -59081717, binary: 11111100011110100111110000001011 -i, int: -59081716, binary: 11111100011110100111110000001100 j, int: 198850956, binary: 00001011110110100011100110001100 i & j, int: 58720644, binary: 00000011100000000000000110000100 i | j, int: 199212028, binary: 00001011110111111011101111111100 i ^ j, int: 140491384, binary: 00001000010111111011101001111000 i << 5, int: 1890614912, binary: 01110000101100000111111010000000 i >> 5, int: 1846303, binary: 00000000000111000010110000011111 (~i) >> 5, int: -1846304, binary: 11111111111000111101001111100000 i >>> 5, int: 1846303, binary: 00000000000111000010110000011111 (~i) >>> 5, int: 132371424, binary: 00000111111000111101001111100000 数字的二进制形式表现为“有符号2的补值”。 3.1.9 三元if-else运算符 这种运算符比较罕见,因为它有三个运算对象。但它确实属于运算符的一种,因为它最终也会生成一个值。这与本章后一节要讲述的普通if-else语句是不同的。表达式采取下述形式: 布尔表达式 ? 值0:值1 若“布尔表达式”的结果为true,就计算“值0”,而且它的结果成为最终由运算符产生的值。但若“布尔表达式”的结果为false,计算的就是“值1”,而且它的结果成为最终由运算符产生的值。 当然,也可以换用普通的if-else语句(在后面介绍),但三元运算符更加简洁。尽管C引以为傲的就是它是一种简练的语言,而且三元运算符的引入多半就是为了体现这种高效率的编程,但假若您打算频繁用它,还是要先多作一些思量——它很容易就会产生可读性极差的代码。 可将条件运算符用于自己的“副作用”,或用于它生成的值。但通常都应将其用于值,因为那样做可将运算符与if-else明确区别开。下面便是一个例子: static int ternary(int i) { return i < 10 ? i * 100 : i * 10; } 可以看出,假设用普通的if-else结构写上述代码,代码量会比上面多出许多。如下所示: static int alternative(int i) { if (i < 10) return i * 100; return i * 10; } 但第二种形式更易理解,而且不要求更多的录入。所以在挑选三元运算符时,请务必权衡一下利弊。 3.1.10 逗号运算符 在C和C++里,逗号不仅作为函数自变量列表的分隔符使用,也作为进行后续计算的一个运算符使用。在Java里需要用到逗号的唯一场所就是for循环,本章稍后会对此详加解释。 3.1.11 字串运算符+ 这个运算符在Java里有一项特殊用途:连接不同的字串。这一点已在前面的例子中展示过了。尽管与+的传统意义不符,但用+来做这件事情仍然是非常自然的。在C++里,这一功能看起来非常不错,所以引入了一项“运算符过载”机制,以便C++程序员为几乎所有运算符增加特殊的含义。但非常不幸,与C++的另外一些限制结合,运算符过载成为一种非常复杂的特性,程序员在设计自己的类时必须对此有周到的考虑。与C++相比,尽管运算符过载在Java里更易实现,但迄今为止仍然认为这一特性过于复杂。所以Java程序员不能象C++程序员那样设计自己的过载运算符。 我们注意到运用“String +”时一些有趣的现象。若表达式以一个String起头,那么后续所有运算对象都必须是字串。如下所示: int x = 0, y = 1, z = 2; String sString = "x, y, z "; System.out.println(sString + x + y + z); 在这里,Java编译程序会将x,y和z转换成它们的字串形式,而不是先把它们加到一起。然而,如果使用下述语句: System.out.println(x + sString); 那么早期版本的Java就会提示出错(以后的版本能将x转换成一个字串)。因此,如果想通过“加号”连接字串(使用Java的早期版本),请务必保证第一个元素是字串(或加上引号的一系列字符,编译能将其识别成一个字串)。 3.1.12 运算符常规操作规则 使用运算符的一个缺点是括号的运用经常容易搞错。即使对一个表达式如何计算有丝毫不确定的因素,都容易混淆括号的用法。这个问题在Java里仍然存在。 在C和C++中,一个特别常见的错误如下: while(x = y) { //... } 程序的意图是测试是否“相等”(==),而不是进行赋值操作。在C和C++中,若y是一个非零值,那么这种赋值的结果肯定是true。这样使可能得到一个无限循环。在Java里,这个表达式的结果并不是布尔值,而编译器期望的是一个布尔值,而且不会从一个int数值中转换得来。所以在编译时,系统就会提示出现错误,有效地阻止我们进一步运行程序。所以这个缺点在Java里永远不会造成更严重的后果。唯一不会得到编译错误的时候是x和y都为布尔值。在这种情况下,x = y属于合法表达式。而在上述情况下,则可能是一个错误。 在C和C++里,类似的一个问题是使用按位AND和OR,而不是逻辑AND和OR。按位AND和OR使用两个字符之一(&或|),而逻辑AND和OR使用两个相同的字符(&&或||)。就象“=”和“==”一样,键入一个字符当然要比键入两个简单。在Java里,编译器同样可防止这一点,因为它不允许我们强行使用一种并不属于的类型。 3.1.13 造型运算符 “造型”(Cast)的作用是“与一个模型匹配”。在适当的时候,Java会将一种数据类型自动转换成另一种。例如,假设我们为浮点变量分配一个整数值,计算机会将int自动转换成float。通过造型,我们可明确设置这种类型的转换,或者在一般没有可能进行的时候强迫它进行。 为进行一次造型,要将括号中希望的数据类型(包括所有修改符)置于其他任何值的左侧。下面是一个例子: void casts() { int i = 200; long l = (long)i; long l2 = (long)200; } 正如您看到的那样,既可对一个数值进行造型处理,亦可对一个变量进行造型处理。但在这儿展示的两种情况下,造型均是多余的,因为编译器在必要的时候会自动进行int值到long值的转换。当然,仍然可以设置一个造型,提醒自己留意,也使程序更清楚。在其他情况下,造型只有在代码编译时才显出重要性。 在C和C++中,造型有时会让人头痛。在Java里,造型则是一种比较安全的操作。但是,若进行一种名为“缩小转换”(Narrowing Conversion)的操作(也就是说,脚本是能容纳更多信息的数据类型,将其转换成容量较小的类型),此时就可能面临信息丢失的危险。此时,编译器会强迫我们进行造型,就好象说:“这可能是一件危险的事情——如果您想让我不顾一切地做,那么对不起,请明确造型。”而对于“放大转换”(Widening conversion),则不必进行明确造型,因为新类型肯定能容纳原来类型的信息,不会造成任何信息的丢失。 Java允许我们将任何主类型“造型”为其他任何一种主类型,但布尔值(bollean)要除外,后者根本不允许进行任何造型处理。“类”不允许进行造型。为了将一种类转换成另一种,必须采用特殊的方法(字串是一种特殊的情况,本书后面会讲到将对象造型到一个类型“家族”里;例如,“橡树”可造型为“树”;反之亦然。但对于其他外来类型,如“岩石”,则不能造型为“树”)。 1. 字面值 最开始的时候,若在一个程序里插入“字面值”(Literal),编译器通常能准确知道要生成什么样的类型。但在有些时候,对于类型却是暧昧不清的。若发生这种情况,必须对编译器加以适当的“指导”。方法是用与字面值关联的字符形式加入一些额外的信息。下面这段代码向大家展示了这些字符。 //: Literals.java class Literals { char c = 0xffff; // max char hex value byte b = 0x7f; // max byte hex value short s = 0x7fff; // max short hex value int i1 = 0x2f; // Hexadecimal (lowercase) int i2 = 0X2F; // Hexadecimal (uppercase) int i3 = 0177; // Octal (leading zero) // Hex and Oct also work with long. long n1 = 200L; // long suffix long n2 = 200l; // long suffix long n3 = 200; //! long l6(200); // not allowed float f1 = 1; float f2 = 1F; // float suffix float f3 = 1f; // float suffix float f4 = 1e-45f; // 10 to the power float f5 = 1e+9f; // float suffix double d1 = 1d; // double suffix double d2 = 1D; // double suffix double d3 = 47e47d; // 10 to the power } ///:~ 十六进制(Base 16)——它适用于所有整数数据类型——用一个前置的0x或0X指示。并在后面跟随采用大写或小写形式的0-9以及a-f。若试图将一个变量初始化成超出自身能力的一个值(无论这个值的数值形式如何),编译器就会向我们报告一条出错消息。注意在上述代码中,最大的十六进制值只会在char,byte以及short身上出现。若超出这一限制,编译器会将值自动变成一个int,并告诉我们需要对这一次赋值进行“缩小造型”。这样一来,我们就可清楚获知自己已超载了边界。 八进制(Base 8)是用数字中的一个前置0以及0-7的数位指示的。在C,C++或者Java中,对二进制数字没有相应的“字面”表示方法。 字面值后的尾随字符标志着它的类型。若为大写或小写的L,代表long;大写或小写的F,代表float;大写或小写的D,则代表double。 指数总是采用一种我们认为很不直观的记号方法:1.39e-47f。在科学与工程学领域,“e”代表自然对数的基数,约等于2.718(Java一种更精确的double值采用Math.E的形式)。它在象“1.39×e的-47次方”这样的指数表达式中使用,意味着“1.39×2.718的-47次方”。然而,自FORTRAN语言发明后,人们自然而然地觉得e代表“10多少次幂”。这种做法显得颇为古怪,因为FORTRAN最初面向的是科学与工程设计领域。理所当然,它的设计者应对这样的混淆概念持谨慎态度(注释①)。但不管怎样,这种特别的表达方法在C,C++以及现在的Java中顽固地保留下来了。所以倘若您习惯将e作为自然对数的基数使用,那么在Java中看到象“1.39e-47f”这样的表达式时,请转换您的思维,从程序设计的角度思考它;它真正的含义是“1.39×10的-47次方”。 ①:John Kirkham这样写道:“我最早于1962年在一部IBM 1620机器上使用FORTRAN II。那时——包括60年代以及70年代的早期,FORTRAN一直都是使用大写字母。之所以会出现这一情况,可能是由于早期的输入设备大多是老式电传打字机,使用5位Baudot码,那种码并不具备小写能力。乘幂表达式中的‘E’也肯定是大写的,所以不会与自然对数的基数‘e’发生冲突,后者必然是小写的。‘E’这个字母的含义其实很简单,就是‘Exponential’的意思,即‘指数’或‘幂数’,代表计算系统的基数——一般都是10。当时,八进制也在程序员中广泛使用。尽管我自己未看到它的使用,但假若我在乘幂表达式中看到一个八进制数字,就会把它认作Base 8。我记得第一次看到用小写‘e’表示指数是在70年代末期。我当时也觉得它极易产生混淆。所以说,这个问题完全是自己‘潜入 |