@学习thinking in java
二、控制程序流程
- 负数使用 Java 运算符:
运算符以一个或多个自变量为基础,可生成一个新值。自变量采用与原始方法调用不同的一种形式,但效果
是相同的。根据以前写程序的经验,运算符的常规概念应该不难理解。
加号(+)、减号和负号(-)、乘号(*)、除号(/)以及等号(=)的用法与其他所有编程语言都是类似
的。
所有运算符都能根据自己的运算对象生成一个值。除此以外,一个运算符可改变运算对象的值,这叫作“副
作用”(Side Effect)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的
值亦可由没有副作用的运算符生成。
几乎所有运算符都只能操作“主类型”(Primitives)。唯一的例外是“=”、“==”和“!=”,它们能操作
所有对象(也是对象易令人混淆的一个地方)。除此以外,String 类支持“+”和“+=”。 - 优先级:个人觉得不必花费力气去记什么优先级,因为能常情况下我们直接用“()”小括号去定义我们的优先级就可以了
- 赋值:
赋值是用等号运算符(=)进行的。它的意思是“取得右边的值,把它复制到左边”。右边的值可以是任何常
数、变量或者表达式,只要能产生一个值就行。但左边的值必须是一个明确的、已命名的变量。也就是说,
它必须有一个物理性的空间来保存右边的值。举个例子来说,可将一个常数赋给一个变量(A=4;),但不可
将任何东西赋给一个常数(比如不能4=A)。- 基本类型:首先我们要知道java内存中对于基本类型的存储是位于栈空间中的,下面看例子:
@Test public void test() { int a=12; int b; b=a; a=13; System.out.println(b);//12 /* * 这里我们在栈中创建一个a=12,一个b * 然后把b=a也就是说b的值也等于a,即12 * 这个时候把a变成了13,操作的是a * b没有影响,故b还是12 * */ }
- 引用类型也就是对象,这也是初学者最容易出错的地方,下面看例子:
@Test public void test01() { // 本来想用Integer来做例子的,发现Integer除了new新对象改值,没有什么方法去修改,所以就用Demo1类吧 // 1.创建Demo对象d1并赋值为12,这里我们要注意的是程序首先在栈中创建一个d1的句柄,然后在堆中new 一个对象,然后这个d1的句柄 // 指向了这个对象 Demo1 d1 = new Demo1(); d1.i = 12; // 2.创建Demo对象d2,并让其也具体d1的功能,d1指向对象A,所以d2也指向对象A, Demo1 d2 = d1; // d1指向的对象A的值改变了 d1.i = 13; // d2指向的对象,即同样的A,所以当然也改变了 System.out.println(d2.i);// 13 } class Demo1 { int i; }
- String对象的赋值:@学习http://www.cnblogs.com/ITtangtang/p/3976820.html
- Java内存模型
对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的, 对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。在程序执行的时候,常量池会储存在Method Area,而不是堆中。常量池中保存着很多String对象; 并且可以被共享使用,因此它提高了效率 - 例:这个例子是我在网上看到的比较全的,讲的比较透彻的例子
@Test public void test04() { /** * 情景一:字符串池 JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象; 并且可以被共享使用,因此它提高了效率。 * 由于String类是final的,它的值一经创建就不可改变。 * 字符串池由String类维护,我们可以调用intern()方法来访问字符串池。 */ String s1 = "abc"; // ↑ 在字符串池创建了一个对象 String s2 = "abc"; // ↑ 字符串pool已经存在对象“abc”(共享),所以创建0个对象,累计创建一个对象 System.out.println("s1 == s2 : " + (s1 == s2)); // ↑ true 指向同一个对象, System.out.println("s1.equals(s2) : " + (s1.equals(s2))); // ↑ true 值相等 // ↑------------------------------------------------------over /** * 情景二:关于new String("") * */ String s3 = new String("abc"); // ↑ 创建了两个对象,一个存放在字符串池中,一个存在与堆区中; // ↑ 还有一个对象引用s3存放在栈中 String s4 = new String("abc"); // ↑ 字符串池中已经存在“abc”对象,所以只在堆中创建了一个对象 System.out.println("s3 == s4 : " + (s3 == s4)); // ↑false s3和s4栈区的地址不同,指向堆区的不同地址; System.out.println("s3.equals(s4) : " + (s3.equals(s4))); // ↑true s3和s4的值相同 System.out.println("s1 == s3 : " + (s1 == s3)); // ↑false 存放的地区多不同,一个栈区,一个堆区 System.out.println("s1.equals(s3) : " + (s1.equals(s3))); // ↑true 值相同 // ↑------------------------------------------------------over /** * 情景三: 由于常量的值在编译的时候就被确定(优化)了。 在这里,"ab"和"cd"都是常量, * 这行代码编译后的效果等同于: String str3 = "abcd"; */ String str1 = "ab" + "cd"; // 1个对象 String str11 = "abcd"; System.out.println("str1 = str11 : " + (str1 == str11)); // ↑------------------------------------------------------over /** * 情景四: 局部变量str2,str3存储的是存储两个拘留字符串对象(intern字符串对象)的地址。 * * 第三行代码原理(str2+str3): 运行期JVM首先会在堆中创建一个StringBuilder类, * 同时用str2指向的拘留字符串对象完成初始化, 然后调用append方法完成对str3所指向的拘留字符串的合并, * 接着调用StringBuilder的toString()方法在堆中创建一个String对象, * 最后将刚生成的String对象的堆地址存放在局部变量str3中。 * * 而str5存储的是字符串池中"abcd"所对应的拘留字符串对象的地址。 str4与str5地址当然不一样了。 * * 内存中实际上有五个字符串对象: 三个拘留字符串对象、一个String对象和一个StringBuilder对象。 */ String str2 = "ab"; // 1个对象 String str3 = "cd"; // 1个对象 String str4 = str2 + str3; String str5 = "abcd"; System.out.println("str4 = str5 : " + (str4 == str5)); // false // ↑------------------------------------------------------over /** * 情景五: JAVA编译器对string + 基本类型/常量 是当成常量表达式直接求值来优化的。 * 运行期的两个string相加,会产生新的对象的,存储在堆(heap)中 */ String str6 = "b"; String str7 = "a" + str6; String str67 = "ab"; System.out.println("str7 = str67 : " + (str7 == str67)); // ↑str6为变量,在运行期才会被解析。 final String str8 = "b"; String str9 = "a" + str8; String str89 = "ab"; System.out.println("str9 = str89 : " + (str9 == str89)); // ↑str8为常量变量,编译期会被优化 // ↑------------------------------------------------------over }
- 补充:
- 代码中的字符串常量在编译的过程中收集并放在class文件的常量区中,如"123"、"123"+"456"等,含有变量的表达式不会收录,如"123"+a。
- JVM在加载类的时候,根据常量区中的字符串生成常量池,每个字符序列如"123"会生成一个实例放在常量池里,这个实例是不在堆里的,也不会被GC
- 在执行到双引号包含字符串的语句时,如String a = "123",JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里。如果是 String a = "123" + b (假设b是"456"),前半部分"123"还是走常量池的路线,但是这个+操作符其实是转换成[SringBuffer].Appad()来实现的,所以最终a得到是一个新的实例引用,而且a的value存放的是一个新申请的字符数组内存空间的地址(存放着"123456"),而此时"123456"在常量池中是未必存在的。
要注意: 我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象
- 在执行String a = new String("123")的时候,首先走常量池的路线取到一个实例的引用,然后在堆上创建一个新的String实例,走以下构造函数给value属性赋值,然后把实例引用赋值给a
- String对象的实例调用intern方法后,可以让JVM检查常量池,如果没有实例的value属性对应的字符串序列比如"123"(注意是检查字符串序列而不是检查实例本身),就将本实例放入常量池,如果有当前实例的value属性对应的字符串序列"123"在常量池中存在,则返回常量池中"123"对应的实例的引用而不是当前实例的引用,即使当前实例的value也是"123"。
- 存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的 intern()方法就是扩充常量池的 一个方法;当一个String实例str调用intern()方法时,Java 查找常量池中 是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常 量池中增加一个Unicode等于str的字符串并返回它的引用;看示例就清楚了:
@Test public void test06() { String s0 = "kvill"; String s1 = new String("kvill"); String s2 = new String("kvill"); System.out.println( s0 == s1 ); //false System.out.println( "**********" ); s1.intern(); //虽然执行了s1.intern(),但它的返回值没有赋给s1 s2 = s2.intern(); //把常量池中"kvill"的引用赋给s2 System.out.println( s0 == s1); //flase System.out.println( s0 == s1.intern() ); //true//说明s1.intern()返回的是常量池中"kvill"的引用 System.out.println( s0 == s2 ); //true }
- Java内存模型
- 基本类型:首先我们要知道java内存中对于基本类型的存储是位于栈空间中的,下面看例子:
- 算术运算
- 注意优先级(用“()"就能解决)
- 关于进阶的问题
- 整数与整数运算不存在进阶,直接砍掉小数:例:System.out.println(3/2)//1
- 自动递增和递减(注意++i和i++,前者先运算后用值,后者先用值,用运算
@Test public void test08() { int i=0; int j=0; System.out.println(i++);//先用i所以输出为0 System.out.println(++j);//先运算后等于1,再打印为1 }
- 关系运算符(注意:检查对象是否相等)
- 逻辑运算符
- 逻辑运算符 AND(&&)、OR(||)以及 NOT(!)能生成一个布尔值(true 或 false)——以自变量的逻辑关
系为基础。要注意其优先级的问题,不过本人还是一般用”()“来搞定这个问题 - 短路的问题(&)、(|)其实很简单比如说:A||B||C和A|B|C,如果A正确显示不管B和C是否正确,整个表达式肯定是正确的,前者将不再验证BC的条件,后者却依然进行验证,这称为短路,举个例子:
@Test public void test09() { Demo2_1 d = new Demo2_1(); System.out.println(d.A() || d.B() || d.C()); System.out.println("**************"); System.out.println(d.A() | d.B() | d.C()); } class Demo2_1 { public boolean A() { System.out.println("A"); return true; } public boolean B() { System.out.println("B"); return true; } public boolean C() { System.out.println("C"); return true; } } /* 运行结果: A true ************** A B C true */
- 逻辑运算符 AND(&&)、OR(||)以及 NOT(!)能生成一个布尔值(true 或 false)——以自变量的逻辑关
- 按位运算符(就是将任意一个数转化成二进制,然后在二进制的基础上按下面的原则进行计算,注意负数的二进制就是对应正数的反码+1)
若两个输入位都是1,则按位AND运算符(&)在输出位里生成一个1;否则生成0。若两个输入位里至少有一个是1,则按位OR运算符(|)在输出位里生成一个1;只有在两个输入位都是0的情况下,它才会生成一个0。若两个输入位的某一个是1,但不全都是1,那么按位XOR(^,异或)在输出位里生成一个1。按位NOT(~,也叫作“非”运算符)属于一元运算符;它只对一个自变量进行操作(其他所有运算符都是二元运算符)。按位NOT生成与输入位的相反的值——若输入0,则输出1;输入1,则输出0,例: - 移位运算符
- 左移:将二进制码向左移动N格,空白位用0补足,例:
@Test public void test11() { int i=3; System.out.println(i<<2);//12 }
- 右移:将二进制码向右移动N格,空白位用符号位数字补足,即正数用0补,负数用1补,例:
@Test public void test12() { int i=3; int j=-3; System.out.println(i>>2); System.out.println(j>>2); }
- 无符号右移(>>>),针对右移(>>)java中的无符号右移就是改变了右移(>>)中的用符号位填充空白处用0去填充空白处,例:
@Test public void test13() { int i=3; int j=-3; System.out.println(i>>>2); System.out.println(j>>>2); }
- 左移:将二进制码向左移动N格,空白位用0补足,例:
- 三元 if-else 运算符(个人感觉就是if—else的减化版)
布尔表达式 ? 值 0:值 1 例:@Test public void test14() { int a = 12; int b = 10; if (a > b) { System.out.println(a); } else { System.out.println(b); } System.out.println("*************"); System.out.println(a > b ? a : b); /* * 12 ************* * 12 */ }
- 字符串运算符
运用“String +”时一些有趣的现象。若表达式以一个String起头,那么后续所有运算对象都必
须是字串 - 转型(低位与高位加运算,自动向高位转)
二、执行控制
- if-else
if-else 语句或许是控制程序流程最基本的形式。其中的 else 是可选的,所以可按下述两种形式来使用if:
if(布尔表达式)
语句
或者
if(布尔表达式)
语句
else
语句条件必须产生一个布尔结果。“语句”要么是用分号结尾的一个简单语句,要么是一个复合语句——封闭在
括号内的一组简单语句。在本书任何地方,只要提及“语句”这个词,就有可能包括简单或复合语句。
作为if-else 的一个例子,下面这个 test()方法可告诉我们猜测的一个数字位于目标数字之上、之下还是相
等:
static int test(int testval) {
int result = 0;
if(testval > target)
result = -1;
else if(testval < target)
result = +1;
else
result = 0; // match
return result;
}
最好将流程控制语句缩进排列,使读者能方便地看出起点与终点。- 1. return
return关键字有两方面的用途:指定一个方法返回什么值(假设它没有 void 返回值),并立即返回那个
值。可据此改写上面的 test()方法,使其利用这些特点:
85
static int test2(int testval) {
if(testval > target)
return -1;
if(testval < target)
return +1;
return 0; // match
}
不必加上else,因为方法在遇到 return后便不再继续。
- 1. return
- 循环
- while,do-while和 for控制着循环,有时将其划分为“反复语句”。除非用于控制反复的布尔表达式得到
“假”的结果,否则语句会重复执行下去。while 循环的格式如下:
while(布尔表达式)
语句
在循环刚开始时,会计算一次“布尔表达式”的值。而对于后来每一次额外的循环,都会在开始前重新计算
一次。
下面这个简单的例子可产生随机数,直到符合特定的条件为止:public class WhileTest { public static void main(String[] args) { double r = 0; while(r < 0.99d) { r = Math.random(); System.out.println(r); } } } ///:~
- do-while
do-while 的格式如下:
do
语句
while(布尔表达式)while 和do-while 唯一的区别就是do-while肯定会至少执行一次;也就是说,至少会将其中的语句“过一
遍”——即便表达式第一次便计算为false。而在 while 循环结构中,若条件第一次就为false,那么其中的
语句根本不会执行。在实际应用中,while 比 do-while 更常用一些。 - for
for(初始表达式; 布尔表达式; 步进)语句
- 要学习for中的执行顺序:
@Test public void test() { for (int i = getI(); i < getJ(); i = IJaJa(i)) { System.out.println("****************"); } /* * 分析:首先执行语句之前初始表达式(这个过程只执行一次)故先打印一个"getI()" * 然后,判断条件,满足打印一个"getJ()",满足就执行语句块打印************** 再然后,步进,打印一个“i++" * 再然后,判断条件,满足打印一个"getJ()",满足就执行语句块打印************** 再然后,步进,打印一个“i++" * 再然后,判断条件,不满足,则结束程序 */ } public int getI() { System.out.println("getI()"); return 0; } public int getJ() { System.out.println("getJ()"); return 2; } public int IJaJa(int i) { System.out.println("i++"); return ++i; }
其实等效下列的语句:
@Test public void test01() { int i=0; while(i<2){ System.out.println("************"); ++i; } }
这是for例子的结果:
getI() getJ() **************** i++ getJ() **************** i++ getJ()
- 无论初始表达式,布尔表达式,还是步进,都可以置空。
其实知道了for的执行顺序之后,相信应该明白,当for循环全部置空就是无限循环了 - for循环中的无论初始表达式,布尔表达式,还是步进可以为多个条件,例:
@Test public void test02() { for(int i=0,j=0;i<2&&j<1;i++,j++){ int count=1; System.out.println("******count: "+count);//******count: 1 count++; } }
- while,do-while和 for控制着循环,有时将其划分为“反复语句”。除非用于控制反复的布尔表达式得到
- 循环的中断(对while和for通用,这里仅用for举例)
- 用break 和continue 控制循环的流程
- break:中断当前循环,例:
@Test public void test03() { for(int i=0;;){//这是一个无限循环 System.out.println(i++); if(i>1)//当i>1的时候终止当前循环,注意if可不是循环 break;//所以输出0,1 } }
- continue:忽略循环体后面的内容,继续进行循环体:
@Test public void test04() { for(int i=0;;){//这是一个无限循环 i++; if(i<2)//当i<2的时候,注意if可不是循环 continue;//不再后面的的内容,再次循环,所以执行下面的语句 System.out.println(i);//2 if(i>=2) break; } }
- break:中断当前循环,例:
- 使用“标签”配合continue和break:
“标签”是后面跟一个冒号的标识符,就象下面这样:
label1:
对Java 来说,唯一用到标签的地方是在循环语句之前。进一步说,它实际需要紧靠在循环语句的前方——在
标签和循环之间置入任何语句都是不明智的。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另
一个循环或者一个开关。这是由于 break和 continue 关键字通常只中断当前循环,但若随同标签使用,它们
就会中断到存在标签的地方。如下所示:label1:
外部循环{
内部循环{
//...
break; //1
//...
continue; //2
//...
continue label1; //3
//...
break label1; //4
}
}在条件1 中,break 中断内部循环,继续外部循环。在条件2 中,continue 移回内部循环的起始处。但
在条件3 中,continue label1 却同时中断内部循环以及外部循环,并移至label1 处。随后,它实际是继续
循环,但却从外部循环开始。在条件4 中,break label1也会中断所有循环,并回到label1 处,但并不重
新进入循环。也就是说,它实际是完全中止了两个循环。
下面是for 循环的一个例子:@Test public void test06() { int i = 0; outer: // 标签要紧邻着循环 for (; true;) { // 这是个无限循环 inner: // 标签要紧邻着循环 for (; i < 10; i++) { prt("i = " + i);// static void prt(String s) // {System.out.println(s);} if (i == 2) { prt("continue"); continue; } if (i == 3) { prt("break"); i++; // i增加的语句不能放break后面,不然永远不会得到增加 break; } if (i == 7) { prt("continue outer"); i++; // i增加的语句不能放break后面,不然永远不会得到增加 continue outer; } if (i == 8) { prt("break outer"); break outer; } for (int k = 0; k < 5; k++) { if (k == 3) { prt("continue inner"); continue inner; } } } } /* * i = 0 continue inner i = 1 continue inner i = 2 continue i = 3 break * i = 4 continue inner i = 5 continue inner i = 6 continue inner i = 7 * continue outer i = 8 break outer */ } static void prt(String s) { System.out.println(s); }
如果没有break outer 语句,就没有办法在一个内部循环里找到出外部循环的路径。这是由于break 本身只
能中断最内层的循环(对于continue 同样如此)。
当然,若想在中断循环的同时退出方法,简单地用一个return 即可。
备注:其实吧,这种也不推荐使用,我们完成可以做个flag来控制循环,达到同样的效果
- 用break 和continue 控制循环的流程
- switch
switch(整数选择因子) {
case 整数值1 : 语句; break;
case 整数值2 : 语句; break;
case 整数值3 : 语句; break;
case 整数值4 : 语句; break;
case 整数值5 : 语句; break;
//..
default:语句;
}
要注意的是这里的break不能少,少了break不会报错,会一直往下执行- 在Java7之前,switch只能支持 byte、short、char、int或者其对应的封装类以及Enum类型。在Java7中,呼吁很久的String支持也终于被加上了。例:
@Test public void test07() { String a="a"; switch(a){ case "b" : case "a" : System.out.println("a"); break;//a default :System.out.println("**"); } }
注意这里的String a不能为空,代码的原理其实是根据String的hashCode来匹配的,String 为null ,String.hashCode必然会报空指针异常
- 在Java7之前,switch只能支持 byte、short、char、int或者其对应的封装类以及Enum类型。在Java7中,呼吁很久的String支持也终于被加上了。例: