第3章 JAVE学习控制程序流程.docx
《第3章 JAVE学习控制程序流程.docx》由会员分享,可在线阅读,更多相关《第3章 JAVE学习控制程序流程.docx(68页珍藏版)》请在冰豆网上搜索。
第3章JAVE学习控制程序流程
-------------------------------------------------
本教程由yyc,spirit整理
-------------------------------------------------
第3章控制程序流程
“就象任何有感知的生物一样,程序必须能操纵自己的世界,在执行过程中作出判断与选择。
”
在Java里,我们利用运算符操纵对象和数据,并用执行控制语句作出选择。
Java是建立在C++基础上的,所以对C和C++程序员来说,对Java这方面的大多数语句和运算符都应是非常熟悉的。
当然,Java也进行了自己的一些改进与简化工作。
3.1使用Java运算符
运算符以一个或多个自变量为基础,可生成一个新值。
自变量采用与原始方法调用不同的一种形式,但效果是相同的。
根据以前写程序的经验,运算符的常规概念应该不难理解。
加号(+)、减号和负号(-)、乘号(*)、除号(/)以及等号(=)的用法与其他所有编程语言都是类似的。
所有运算符都能根据自己的运算对象生成一个值。
除此以外,一个运算符可改变运算对象的值,这叫作“副作用”(SideEffect)。
运算符最常见的用途就是修改自己的运算对象,从而产生副作用。
但要注意生成的值亦可由没有副作用的运算符生成。
几乎所有运算符都只能操作“主类型”(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才指向的那个对象。
下面这个例子将向大家阐示这一点。
这里有一些题外话。
在后面,大家在代码示例里看到的第一个语句将是“package03”使用的“package”语句,它代表本书第3章。
本书每一章的第一个代码清单都会包含象这样的一个“package”(封装、打包、包裹)语句,它的作用是为那一章剩余的代码建立章节编号。
在第17章,大家会看到第3章的所有代码清单(除那些有不同封装名称的以外)都会自动置入一个名为c03的子目录里;第4章的代码置入c04;以此类推。
所有这些都是通过第17章展示的CodePackage.java程序实现的;“封装”的基本概念会在第5章进行详尽的解释。
就目前来说,大家只需记住象“package03”这样的形式只是用于为某一章的代码清单建立相应的子目录。
为运行程序,必须保证在classpath里包含了我们安装本书源码文件的根目录(那个目录里包含了c02,c03c,c04等等子目录)。
对于Java后续的版本(1.1.4和更高版本),如果您的main()用package语句封装到一个文件里,那么必须在程序名前面指定完整的包裹名称,否则不能运行程序。
在这种情况下,命令行是:
javac03.Assignment
运行位于一个“包裹”里的程序时,随时都要注意这方面的问题。
下面是例子:
//:
Assignment.java
//Assignmentwithobjectsisabittricky
packagec03;
classNumber{
inti;
}
publicclassAssignment{
publicstaticvoidmain(String[]args){
Numbern1=newNumber();
Numbern2=newNumber();
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
//Passingobjectstomethodscanbeabittricky
classLetter{
charc;
}
publicclassPassObject{
staticvoidf(Lettery){
y.c='z';
}
publicstaticvoidmain(String[]args){
Letterx=newLetter();
x.c='a';
System.out.println("1:
x.c:
"+x.c);
f(x);
System.out.println("2:
x.c:
"+x.c);
}
}///:
~
在许多程序设计语言中,f()方法表面上似乎要在方法的作用域内制作自己的自变量Lettery的一个副本。
但同样地,实际传递的是一个句柄。
所以下面这个程序行:
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
//Demonstratesthemathematicaloperators
importjava.util.*;
publicclassMathOps{
//Createashorthandtosavetyping:
staticvoidprt(Strings){
System.out.println(s);
}
//shorthandtoprintastringandanint:
staticvoidpInt(Strings,inti){
prt(s+"="+i);
}
//shorthandtoprintastringandafloat:
staticvoidpFlt(Strings,floatf){
prt(s+"="+f);
}
publicstaticvoidmain(String[]args){
//Createarandomnumbergenerator,
//seedswithcurrenttimebydefault:
Randomrand=newRandom();
inti,j,k;
//'%'limitsmaximumvalueto99:
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-pointnumbertests:
floatu,v,w;//appliestodoubles,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);
//thefollowingalsoworksfor
//char,byte,short,int,long,
//anddouble:
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
//Demonstratesthe++and--operators
publicclassAutoInc{
publicstaticvoidmain(String[]args){
inti=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);
}
staticvoidprt(Strings){
System.out.println(s);
}
}///:
~
该程序的输出如下:
i:
1
++i:
2
i++:
2
i:
3
--i:
2
i--:
2
i:
1
从中可以看到,对于前缀形式,我们在执行完运算后才得到值。
但对于后缀形式,则是在运算执行之前就得到值。
它们是唯一具有“副作用”的运算符(除那些涉及赋值的以外)。
也就是说,它们会改变运算对象,而不仅仅是使用自己的值。
递增运算符正是对“C++”这个名字的一种解释,暗示着“超载C的一步”。
在早期的一次Java演讲中,BillJoy(始创人之一)声称“Java=C++--”(C加加减减),意味着Java已去除了C++一些没来由折磨人的地方,形成一种更精简的语言。
正如大家会在这本书中学到的那样,Java的许多地方都得到了简化,所以Java的学习比C++更容易。
3.1.5关系运算符
关系运算符生成的是一个“布尔”(Boolean)结果。
它们评价的是运算对象值之间的关系。
若关系是真实的,关系表达式会生成true(真);若关系不真实,则生成false(假)。
关系运算符包括小于(<)、大于(>)、小于或等于(<=)、大于或等于(>=)、等于(==)以及不等于(!
=)。
等于和不等于适用于所有内建的数据类型,但其他比较不适用于boolean类型。
1.检查对象是否相等
关系运算符==和!
=也适用于所有对象,但它们的含义通常会使初涉Java领域的人找不到北。
下面是一个例子:
//:
Equivalence.java
publicclassEquivalence{
publicstaticvoidmain(String[]args){
Integern1=newInteger(47);
Integern2=newInteger(47);
System.out.println(n1==n2);
System.out.println(n1!
=n2);
}
}///:
~
其中,表达式System.out.println(n1==n2)可打印出内部的布尔比较结果。
一般人都会认为输出结果肯定先是true,再是false,因为两个Integer对象都是相同的。
但尽管对象的内容相同,句柄却是不同的,而==和!
=比较的正好就是对象句柄。
所以输出结果实际上先是false,再是true。
这自然会使第一次接触的人感到惊奇。
若想对比两个对象的实际内容是否相同,又该如何操作呢?
此时,必须使用所有对象都适用的特殊方法equals()。
但这个方法不适用于“主类型”,那些类型直接使用==和!
=即可。
下面举例说明如何使用:
//:
EqualsMethod.java
publicclassEqualsMethod{
publicstaticvoidmain(String[]args){
Integern1=newInteger(47);
Integern2=newInteger(47);
System.out.println(n1.equals(n2));
}
}///:
~
正如我们预计的那样,此时得到的结果是true。
但事情并未到此结束!
假设您创建了自己的类,就象下面这样:
//:
EqualsMethod2.java
classValue{
inti;
}
publicclassEqualsMethod2{
publicstaticvoidmain(String[]args){
Valuev1=newValue();
Valuev2=newValue();
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
//Relationalandlogicaloperators
importjava.util.*;
publicclassBool{
publicstaticvoidmain(String[]args){
Randomrand=newRandom();
inti=rand.nextInt()%100;
intj=rand.nextInt()%100;
prt("i="+i);
prt("j="+j);
prt("i>jis"+(i>j));
prt("iprt("i>=jis"+(i>=j));
prt("i<=jis"+(i<=j));
prt("i==jis"+(i==j));
prt("i!
=jis"+(i!
=j));
//Treatinganintasabooleanis
//notlegalJava
//!
prt("i&&jis"+(i&&j));
//!
prt("i||jis"+(i||j));
//!
prt("!
iis"+!
i);
prt("(i<10)&&(j<10)is"
+((i<10)&&(j<10)));
prt("(i<10)||(j<10)is"
+((i<10)||(j<10)));
}
staticvoidprt(Strings){
System.out.println(s);
}
}///:
~
只可将AND,OR或NOT应用于布尔值。
与在C及C++中不同,不可将一个非布尔值当作布尔值在逻辑表达式中使用。
若这样做,就会发现尝试失败,并用一个“//!
”标出。
然而,后续的表达式利用关系比较生成布尔值,然后对结果进行逻辑运算。
输出列表看起来象下面这个样子:
i=85
j=4
i>jistrue
ii>=jistrue
i<=jisfalse
i==jisfalse
i!
=jistrue
(i<10)&&(j<10)isfalse
(i<10)||(j<10)istrue
注意若在预计为String值的地方使用,布尔值会自动转换成适当的文本形式。
在上述程序中,可将对int的定义替换成除boolean以外的其他任何主数据类型。
但要注意,对浮点数字的比较是非常严格的。
即使一个数字仅在小数部分与另一个数字存在极微小的差异,仍然认为它们是“不相等”的。
即使一个数字只比零大一点点(例如2不停地开平方根),它仍然属于“非零”值。
1.短路
操作逻辑运算符时,我们会遇到一种名为“短路”的情况。
这意味着只有明确得出整个表达式真或假的结论,才会对表达式进行逻辑求值。
因此,一个逻辑表达式的所有部分都有可能不进行求值:
//:
ShortCircuit.java
//Demonstratesshort-circuitingbehavior
//withlogic