JAVA中的操作符
Java是一門靜態強類型的語言,因此如果操作符接收的值的類型不符合操作符規定的類型,就會在編譯期產生編譯錯誤,通常IDE會對這種情況進行提示,所以Java的操作符不會跟JavaScript中的操作符那樣發生很多的類型轉換,其行為更具有確定性,所以只需要進行簡單的介紹。
運算符說明Java運算符分隔符. [] (){} , ;單目運算符++ -- - !強制類型轉換符(type)乘/除/求余* / %加/減+ -位移運算符<< >> >>>關系運算符< <= >= > instanceof等價運算符== !=按位與&按位異或^按位或|條件與&& (短路)條件或|| (短路)三目運算符?:賦值= += -=*= /= &=|= ^= %=<<= >>= >>>=
特別注意==和!=
==和!=這兩個操作符用來進行相等性的判斷,但是需要注意的是,這類似于一個全等操作,對于基本類型的值來說沒有任何問題,但是對于引用類型的值來說,應該采用對象內部的equals()方法來進行比較。
Java中的表達式和合法的語句
在程序設計語言中,表達式和語句是完全不同的兩個概念,在我的理解中:
- 表達式是有值的,任何一個表達式計算完成之后都會返回一個結果值
- 語句是一段可執行的代碼,它不一定有值。
- 表達式本身是可以作為語句存在的,我們稱之為表達式語句。
通過上面的介紹,我們可以認為表達式是語句的一種特殊情況,表達式是具有值的語句
同時,我們需要注意,在很多時候,我們提到語句的時候一般認為語句是沒有值的,這種情況下的語句被狹義化了。
使用表達式的目的和合法的語句
程序中的許多動作都是通過計算表達式而完成的,計算一個表達式要么是為了他們的副作用,例如對變量賦值;要么是為了得到他們的值,將其用做更大的表達式的引元或操作數,要么用來改變語句的執行順序(如if中接收bool值進行流程控制),或者是同時為了這兩個目的
基于這個原因,很多的編程語言中如果一個表達式不滿足使用表達式的目的,那么這個表達式就不能作為一條單獨的語句存在,也就是說不能運行這個表達式,因為這其實是沒有任何意義的,在Java中就是這樣的,如下:
int a = 0; int b = 1; a+b; //ERROR,是一個正確的表達式,但是由于不能滿足使用表達式的目的,把他作為一個語句執行沒有任何意義,所以Java認為這不是一個合法的語句。 復制代碼
同時我們需要注意,賦值語句在Java中被認為是表達式,所以賦值語句是有值的,如下:
public static void main(String[] args) { int a = 0; System.out.println(a = 3+2); //5 System.out.println(a); //5 } 復制代碼
語句
我們接下來要討論的語句就是被狹義之后的語句,主要的功能是用于控制程序的執行流程。
程序的三種基本結構
無論我們使用哪種編程范式(無論是結構化編程范式,面向對象的編程范式還是函數式編程范式),書寫算法都是必須的,而算法的實現過程是由一系列操作組成的,這些操作之間的執行次序就是程序的控制結構。
在以前,很多編程語言都提供了goto語句,goto語句非常靈活,可以讓程序的控制流程任意流轉。但是goto語句太隨意了,大量使用goto語句會使得程序難以理解并且容易出錯。
1996年,計算機科學家Bohm和Jacopini證明了這樣的事實:**任何簡單或者復雜的算法都可以有順序結構,分支(選擇)結構和循環結構這三種基本結構組合而成。**所以這三種結構就被稱為程序設計的三種基本結構。
不論哪一種編程語言,都會提供兩種基本的流程控制結構:分支結構和循環結構。
其中分支結構用于實現根據條件來選擇性地執行某段代碼,循環結構則用于實現根據循環條件重復執行某段代碼,通過這兩種控制結構,就可以改變程序原來順序執行的順序,實現流程的控制進而實現任意復雜的算法。
Java中也為我們提供了分支和循環語句,同時還提供了其他的一些語句用來更加靈活的控制程序流程。
順序結構
任何編程語言中最常見的程序結構就是順序結構。順序結構就是程序從上到下逐行地執行,中間沒有任何判斷和跳轉。
如果一個方法的多行代碼之間沒有任何流程控制,則程序總是從上向下依次執行,排在前面的代碼先執行,排在后面的代碼后執行。這意味著,沒有流程控制,Java中方法的語句是一個順序執行流,從上向下依次執行每一條語句
分支(選擇)結構
Java中提供了兩種常見的分支控制結構:if語句和switch語句。其中if語句使用布爾表達式或者布爾值作為分支條件來進行分支控制;而switch則用戶對多個值進行匹配,從而實現分支控制。
if語句
if語句使用布爾表達式或者布爾值作為分支條件來進行分支控制。if語句有如下三種形式:
if(logic expression) { statement... } 復制代碼 if (logic expression) { statement... } else { statement... } 復制代碼 if (logic expression) { statement... } else if(logic expression) { statement... } ... 復制代碼
使用if語句的時候需要注意下面幾點:
- 當if和else后面之后一條語句的時候可以省略{},但是通常最好不要省略{}
- 對于if語句,還有一個很容易出現的邏輯錯誤。看如下的程序:
int age = 45; if (age > 20) { System.out.println("青年人"); } else if (age > 40) { System.out.println("中年人"); } else if (age > 60) { System.out.println("老年人"); } 復制代碼
- 表面上看來,上面的程序沒有任何問題,但是age=45程序的運行結果卻是“青年人”,這顯然是有問題的!
- **對于任何if else語句,表面上看起來else后沒有任何條件,或者esle if后面只有一個條件——但是這只是表面現象,因為else的含義是"否則"——else本身就是一個條件!else的隱含條件是對前面的條件取反。**因此,上面的代碼可以修改為:
int age = 45; if (age > 60) { System.out.println("老年人"); } else if (age > 40) { System.out.println("中年人"); } else if (age > 20) { System.out.println("青年人"); } 復制代碼
- 上面的程序運行之后就能得到正確的結果,其實上面的程序就等同于下面的這段代碼:
int age = 45; if (age > 60) { System.out.println("老年人"); } //在原本的if條件中增加了else的隱含條件 if (age > 40 && !(age > 60)) { System.out.println("中年人"); } //在原本的if條件中增加了else的隱含條件 if (age > 20 && !(age > 40 && !(age > 60)) && !(age > 60) { System.out.println("青年人"); } 復制代碼
- 也就是說上面的判斷邏輯轉為如下三種情況:
- age大于60歲,判斷為“老年人”
- age大于40歲,并且age小于等于60歲,判斷為“中年人”
- age大于20歲,并且age小于等于40歲,判斷為“青年人”。
- 上面的邏輯才是實際希望的判斷邏輯。因此,使用if...else語句進行流程控制的時候,一定不要忽略了else所帶的隱含條件。
- 如果每次都去計算if條件和else條件的交集也是一件麻煩的事情,為了避免出現上述的錯誤,在使用if...else語句的時候有一條基本規則: 總是優先把包含范圍小(子集)的條件放在前面處理。
- 如 age>60是age>40的子集,而age>40是age>20的子集,把自己放在前面進行處理就可以避免忽略else的隱含條件而造成程序錯誤。
switch語句
switch語句由一個控制表達式和多個case標簽組成,switch語句根據表達式的值將控制流程轉移到了多個case標簽中的某一個上。其形式如下:
switch (expression) { case value1: { statement...; break; } case value2: { statement...; break; } ... case valuen: { statement...; break; } default: { statement...; } } 復制代碼
switch語句先執行對expression的求職,然后依次匹配value1、value2...valuen,遇到匹配的值則從對應的case塊開始向下執行代碼,如果沒有任何一個case塊能夠匹配,則會執行default塊中的代碼。
switch支持的數據類型
Java7之后,對switch語句進行了一定的增強,switch語句后的控制表達式的數據類型只能是byte、short、char、int四種整型類型,還有枚舉類型和String類型。
- switch中不支持浮點數,因為二進制保存浮點數字不是完全精確的,所以對浮點數進行相等性判斷并不可靠
- switch中不支持boolean類型的數據,因為對于boolean類型,使用if語句是更好的選擇
- switch中不支持long整型,我認為一個原因是根本使用不了這么多的分支
同時,我們需要注意的是,case關鍵字后面只能跟一個和switch中表達式的數據類型相同的常量表達式
switch的貫穿
需要注意的是,switch語句并不是一個多選一的分支控制語句,考慮到物理上的多種情況可能是邏輯上的一種情況,switch塊中語句的執行會貫穿標號”,除非遇到break語句。如下代碼:
public void switchTest(int a) { switch (a) { case 1: System.out.print("1"); case 2: System.out.print("2"); case 3: System.out.print("3"); default: System.out.print("default"); } } switchTest(1); 復制代碼
上面的代碼中執行switchTest(1)方法輸出的結果如下:
123default 復制代碼
但是如果在某個case塊中加入break語句,那么這個case塊就不會被貫穿,如下:
public void switchTest(int a) { switch (a) { case 1: { System.out.print("1"); break; } case 2: System.out.print("2"); case 3: System.out.print("3"); default: System.out.print("default"); } } switchTest(1); switchTest(2); 復制代碼
上面的代碼中執行switchTest(1)輸出結果為:
1 復制代碼
可見加入break之后沒有被貫穿,
而執行switchTest(2)輸出結果為:
23default 復制代碼
關于break語句后面會有詳細的介紹
我對case代碼塊的理解。
其實case代碼塊和其他的代碼塊在本質上是一樣的,它有自己的作用域,如下:
public static void main(String[] args) { int a = 1; switch(a) { case 1: { int b = 1; System.out.println(b); } case 2: { int b = 2; System.out.println(b); } } } 復制代碼
上述main方法執行的結果是
1 2 復制代碼
case代碼塊和如下的代碼塊本質上是一樣的:
//普通的代碼塊,使用的主要目的就是創建一個單獨的詞法作用域 { int a = 3; System.out.println(a); } //帶標簽的代碼塊,除了能夠創建一個單獨的詞法作用域,還能夠使用break進行一定的流程控制 labelName: { int a = 3; System.out.println(a); } 復制代碼
而case代碼塊不同的地方在于,case代碼塊只能出現在switch語句中,并且其后面跟著一個跟switch中的表達式的值的類型相同的常量表達式,其實case的作用就是聲明了一個個的錨點,能夠跟switch后的表達式的值匹配的哪個case錨點就是switch程序開始執行的地方,這也就解釋了switch的貫穿
基本上所有擁有switch語句的語言中,case的作用大都是相同的,同時switch也都具有貫穿的特性。
循環結構
循環語句可以在滿足循環條件的情況下,反復執行某一段代碼,這段被重復執行的代碼被稱為循環體。循環語句可能包含如下四個部分
- 初始化語句(init_statement):一條或者多條語句,這些語句用于完成一些初始化工作,初始化語句在循環體開始執行之前執行。
- 循環條件(test_expression):這個測試表達式的值必須是boolean類型的,這個表達式決定是否繼續執行循環體
- 循環體(loop_statement):這個部分是循環的主體,如果循環條件成立,這個代碼塊將被一直執行。
- 迭代語句(iteration_statement):這個部分在每一次循環體結束之后,在對測試表達式求值之前執行,通常用于改變循環條件相關的變量,使得循環在合適的時候結束。
上面的四個部分只是一般性的分類,并不是每個循環中都非常明確的分出了這4個部分。
直到型循環結構
直到型循環結構的特點就是循環體會無條件的執行一次,在Java中提供了do while語句對直到型循環提供了支持。
do while語句
do while循環的結構如下:
init_statement do { loop_statement iteration_statement [break/continue] //[]代表可選的 } while(test_expression); //不要忘記末尾的分號 復制代碼
需要注意的是,iteration_statement和loop_statement之間并沒有明確的順序關系,但是我們需要注意的是,iteration_statement應該位于continue語句之前,否則會容易造成死循環,如下:
public static void main(String[] args) { int i = 0; do { System.out.println(i); if (i%3 == 1) continue; i++; } while (i < 9); } 復制代碼
運行上面的main方法將會產生死循環。所以迭代語句應該放在continue語句之前
當型循環結構
當型循環結構的特點就是,在執行循環體之前就進行循環條件的檢測,如果循環條件在剛開始就不滿足,那么循環體一次都不會執行。
Java提供了while語句和for語句對當型循環結構提供支持
while語句
while循環的語法格式如下:
[init statement] while(test_expression) { loop_statement [iteration_statement] [break/continue] //跟do類似的是,如果存在迭代語句,要放在continue語句之前,防止造成死循環 } 復制代碼
for語句
for循環是最簡潔的一種循環語句,同時其寫法也是最為靈活的,它把循環體獨立出來,同時固定了迭代語句和測試表達式的執行順序,for語句的基本格式如下:
for ([init_statement]; [test_statement], [iteration_statement]) { loop_statement } 復制代碼
可以看到,for語句匯總的init_statement、test_statement和iteration_statement都是可選的,這就意味著for循環可以完全取代while循環,for循環是使用頻率最高的循環語句,原因如下:
- 在for循環的初始化語句中聲明的變量其作用于可以限制在for循環的語句體之內。
- for循環的迭代語句沒有與循環體放在一起,因此即使是在執行循環體的時候遇到了continue語句提前結束本次循環,迭代語句也會得到執行。
雖然for循環的寫法比較自由,但是我們在使用for循環的時候盡量采用標準的寫法,這樣可以充分利用for循環的特性,同時提高代碼的可讀性,所以這里就不介紹關于for循環的騷操作了。
增強的for語句
Java5中,為Iterable和數組類型新增了增強的for語句,其格式如下:
for ([variableModifier] Type variableDeclaratorId: Expression) { loop_statement } 復制代碼
需要注意的是,上述Express的類型必須是Iterable或者是數組類型,否則會產生編譯錯誤
增強的for循環只是一種語法糖,它在編譯器的解糖階段就會轉換成普通的for循環,增強的for循環會按照如下規則轉譯為基本的for語句:
- 如果Expression 的類型是 Iterable 的子類型,那么就按照如下方式進行轉譯: 以如下代碼為例
List<String> nameList = Arrays.asList("zhangsan","lisi","wangwu"); //增強for循環 for(String name: nameList) { System.out.println(name); } //上述的增強的for循環就等價于如下的普通循環 for (Iterator<String> i = Expression.iterator(); i.hasNext(); ) { String name = i.next(); System.out.println(name); } 復制代碼
- 否則, Expression必須具有數組類型,這時候其轉換方式如下: 以如下代碼為例:
String[] nameArr = new String[]{"zhangsan","lisi","wangwu"}; //增強的for循環 for(String name: nameArr) { System.out,println(name); } //上述的增強的for循環等價于如下的普通for循環 for (int i = 0; i< nameArr.length; i++) { String name = nameArr[i]; System.out.println(name); } 復制代碼
更精細的流程控制
分支和循環結構只是為我們提供了基本的流程控制功能,但是在循環中有時候我們需要提前結束某次或者是整個循環,又或者某段代碼在執行到一定的流程的時候需要提前結束,這樣基本的流程控制是滿足不了的。
雖然Java中放棄了飽受詬病的goto語句,但是也為我們提供了一些對程序流程進行更加精細控制的方式。
前面也對break語句和continue語句做過一些介紹了,下面進行一下詳細的說明。
break語句
break語句會將控制流程轉移到包含它的語句或者塊之外。它分為帶標號和不帶標號兩種情況。不帶標號的 break語句只能用在循環結構或者是switch語句之中,而帶標號的 break可以使用在任何語句塊中
不帶標簽的break語句
不帶標號的break語句視圖將控制流轉移到包圍它的最內層的switch、do、while或者for語句中(一定要注意這一點,因為這四種語句之間可以相互嵌套或者是自嵌套)。這條語句被稱為break目標,然后立即正常結束。
如果該不帶標號的break語句不被包含在任何switch、do、while或者for語句中,那么就會編譯失敗。
帶標簽的break語句
在了解帶標簽的break語句之前,我們先來了解一下什么是標號語句
語句前可以有標號前綴,如下:
label: { System.out.println("a1"); } 復制代碼
java編程語言沒有任何goto語句,標識符語句標號會用于出現在標號語句內的任何地方的break或者continue語句之上。
標號本質上其實也是一個標識符,而這個標識符的作用域就是其直接包含的語句的內部,因為Java中規定,只能在該標號所包含的語句之中才能引用這個標號。,如下代碼說明了標號語句的標號標識符的作用域:
public class a { public static void main(String[] args) { a: { int a = 3; System.out.println("a1"); } //這個地方編譯通過, 說明a的作用域并不是在main方法中,而是在它包含的語句之中 a: { System.out.println("a2"); } } } 復制代碼
同時,通過上面的代碼我們也可以看出,不同類型的標識符之間是互不影響的(Java中的標識符有類型名、變量名、標號等等),即對于將相同的標識符同時用做標號和包名、類名、接口名、方法名、域名城、方法參數名、或者局部變量名這種做法,Java沒有做出任何限制
下面正式進入帶標號的break語句的講解
帶有標號的break語句視圖將控制流轉移到將相同標號作為其標號的標號語句,該語句稱為break目標語句,然后立即結束。
在帶標號的break語句中break目標不必是switch、do、while或者for語句
需要注意的是,前面講到過標號語句表標號的作用范圍是在標號語句的語句體內部,所以被break的標號語句必須包含break語句。
前面的描述中稱“視圖轉移控制流程”而不是"直接轉移控制流程",是因為如果在 break目標 內有任何try語句,其try子句或者catch子句包含break語句,那么在控制流程轉移到 break 目標之前,這些 try語句的所有的 finally 子句會按照從最內部到最外部的數學被執行,而 finally 子句的猝然結束都會打斷break語句觸發的控制流轉移
continue語句
continue語句只能出現在while、do或者for語句匯總,這三種語句被稱為迭代語句。控制流會傳遞到迭代語句的循環持續點上。
continue語句也有代標號和不帶標號兩種使用方式,和break語句不同的是,continue語句的這兩種使用方式都只能出現在迭代語句中,下文進行詳細的介紹
不帶標簽的continue語句
不帶標簽的continue試圖將控制流轉移到包圍它的最內層的switch、do、while或者for語句中(一定要注意這一點,因為這四種語句之間可以相互嵌套或者是自嵌套)。這條語句被稱為continue目標,然后立即結束當前的迭代并進行下一輪的迭代。
如果該不帶標號的continue語句不被包含在任何switch、do、while或者for語句中,那么就會編譯失敗。
帶標簽的continue語句
前面已經對標號進行了介紹,下面我們直接進入帶標簽的continue語句的講解
帶標號的continue語句視圖將控制流轉移到將相同標號作為標號的標號語句,但是同時,這個標號語句必須是一個迭代語句,這條語句被稱為continue目標然后理解結束當前的迭代并進行新一輪的迭代。
如果continue目標不是一個迭代語句,那么就會產生一個編譯時錯誤。
前面的描述中稱“視圖轉移控制流程”而不是"直接轉移控制流程",是因為如果在 continue目標 內有任何try語句,其try子句或者catch子句包含continue語句,那么在控制流程轉移到 continue目標之前,這些 try語句的所有的 finally 子句會按照從最內部到最外部的數學被執行,而 finally 子句的猝然結束都會打斷 continue 語句觸發的控制流轉移
其他語句
return 語句
return 語句會將控制流返回給方法或構造器的調用者。return語句必須被包含在可執行成員中(方法、構造器、lambda表達式),return語句也分為兩種,一種是帶有返回值的,一種是不帶有返回值的。
需要注意的是,return語句觸發的流程轉移也是視圖轉移控制流,而不是直接轉移控制流,原因也是try的finally子句
對于return語句,我們不需要說明太多。
throw 語句
throw語句會導致異常對象被拋出,這將會視圖將控制流程進行轉移,有可能會退出多個語句和多個構造器、實例初始化器、靜態初始化器和域初始化器的計算以及方法調用,知道找到可以捕獲拋出值的try語句。如果沒有這種try語句,那么執行throw的線程的執行會在該線程所屬的線程組上調用uncaughtException 方法后被終止。
上述也提到了視圖轉移控制流程,也是因為try的finally子句,如下:
try { throw new RuntimeException("故意拋出"); } finally { System.out.println("還是會執行"); throw new RuntimeException("finally中拋出"); } 復制代碼
但是上述的用法是基本不會出現的,因為在一個執行體中拋出異常就是為了給調用這個執行體的上層執行體提供一種提示,并期望調用者能夠處理這種異常。
關于異常會在后面進行更加詳細的說明。
synchronized 語句
這涉及到了多線程編程,以后會進行專門的介紹。
try 語句
try語句會執行一個語句塊。**如果拋出了值并且try語句有一個或者多個可以捕獲它的catch子句,那么控制流就會轉移到第一個這種catch子句上。**這一點在后面講異常處理的時候也會講到。
如果try語句有finally子句,那么將會執行另一個代碼塊,無論try塊是正常結束還是猝然結束,并且無論控制流之前是否轉移到某個catch塊上。
try語句的格式如下所示,有三種形式:
// try-catch try { statement } catch(Type1 | Type2... e) { statement } //try-finally try { statement } finally { statement } //try-catch-finally try { statement } catch (Type1 | Type2... e) { statement } finally { statement } 復制代碼
在try語句后緊跟的語句塊我們稱之為try語句的try塊
在finally關鍵字之后緊跟的語句塊我們稱之為finally塊
try語句的catch子句通常被稱為異常處理器。catch子句有且僅有一個參數,被稱為異常參數,異常參數可以將它的類型表示成單一的類類型,或者表示成兩個或者更多類型的聯合體(這些類型稱為可選項),聯合體中的可選項在語法上使用|進行分割。
需要注意的是,當catch子句的異常參數的類型是一個類型聯合體的時候,這個異常參數就一定是final類型的,要么是隱式的,當然也可以顯示聲明為final的,但是這時候應該講類型聯合體看做是一個類型,final修飾符只需要書寫一次就可以了。如下:
try { throw new RuntimeException(); } catch (final UnsupportedOperationException | NullPointerException e) { //final修飾符只需在類型聯合體前面書寫一次就可以了,這時候要把類型聯合體看做一個類型 System.out.println("是可以改變的!"); } 復制代碼
這是因為catch類型處理器完全依賴于try塊中拋出的異常的類型。而使用了類型聯合體之后,異常參數的類型就只能在運行時才能確定,所以必須是final的,在被捕獲的時候初始化一次。
同時我們上面也提到過一句話,**如果拋出了值并且try語句有一個或者多個可以捕獲它的catch子句,那么控制流就會轉移到第一個這種catch子句上。**這就提醒我們在使用catch子句的時候需要注意,要把子類類型放在前面進行捕獲,否則就會被父類類型的異常處理器攔截。同時,在catch子句中使用類型聯合體的時候,類型聯合體中的各個可選項之間不能具有任何的父子關系。
try語句的執行流程沒有什么好說的,我們只需要記住一點,finally子句肯定會被執行。
帶資源的 try 語句
帶資源的try語句是用變量(被稱為資源)來參數化的,這些資源在try塊執行之前被初始化,并且在try塊執行之后,自動地以與初始化相反的順序被關閉。當資源會被自動關閉的時候,catch子句和finally子句經常不是必須的。
關于資源,我們需要注意以下幾點:
- try中的資源變量肯定是final的,如果不顯式聲明為final,則會被隱式聲明為final,這是因為需要對聲明的資源進行自動關閉,所以不允許修改資源變量的指向。
- 在帶資源的try語句中聲明的資源必須是AutoCloseable的子類型,否則會產生編譯錯誤。
- 資源是按照從左到右的順序被初始化的。如果某個資源初始化失敗了(即初始化表達式拋出了異常),那么所有已經完成了初始化的資源都將被關閉。如果所有的資源都初始化成功了,那么try塊就會正常執行,然后帶資源的try語句的所有非null資源都將被關閉。
- 資源將以它們被初始化的順序相反的順序被關閉。資源只有在它們被初始化為非null值的時候才會被關閉。在關閉資源時拋出的異常不會阻止其他資源的關閉,其實最終帶資源的try語句都會被轉化為try-finally語句
不可達語句
如果某條語句因為它是不可達的而不能被執行,那么就是一個編譯時錯誤。我們只需知道這種語句的存在,在不可達表現的非常明顯的時候IDE就會給出提示,但是有時候會有錯誤的邏輯造成的不可達語句,就需要我們自己對代碼的結構進行分析了。
總結
以上就是我對Java中的表達式和操作符的理解和介紹,如有不當之處,歡迎進行指正。