本小節是Java基礎篇章的第三小節,主要講述Java中的Exception與Error,JIT編譯器以及值傳遞與引用傳遞的知識點。
一、Java中的Exception和Error有什麼區別
Exception和Error的主要區別可以概括如下:
-
Exception是程序正常運行中預料到可能出現的錯誤,並且應該被捕獲並進行相應處理,是一種異常現象。
-
Error是正常情況下不可能發生的錯誤,Error會導致JVM處於已追蹤不可恢復的狀態,不需要捕獲處理,比如說OutOfMemoryError
解析:
Exception又分爲了運行時異常和編譯異常。
編譯異常(受檢異常)表示當前調用的方法體內部拋出了一個異常,所以編譯器檢測到這段代碼在運行時可能會出現異常,所以我們必須對異常進行相應處理,可以捕獲異常或者拋給上層調用方。
運行時異常(非受檢異常)表示在運行時出現的異常,常見的運行異常包括:空指針異常,數組越界異常,數字轉換異常以及算數異常等。
前面說到了異常Exception應該被捕獲,我們可以使用try-catch-finally來處理異常,並且使得程序恢復正常。
那麼我們捕獲異常應該遵循哪些基本原則呢?
-
儘可能捕獲比較詳細的異常,而不是使用Exception一起捕獲。
-
當本模塊不知道捕獲之後該怎麼處理異常時,可以將其拋給上層模塊。上層模塊擁有更多的業務邏輯,可以進行更好的處理。
-
捕獲異常後至少應該有日誌記錄,方便之後的排查。
-
不要使用一個很大的try-catch包住整段代碼,不利於問題的排查。
NoClassDefFoundError 和 ClassNotFoundException 有什麼區別?
從名字中,我們可以看出前者是一個錯誤,後者是一個異常。我們先來看下JDK中對ClassNotFoundException異常的闡述:
大概意思就是在說,當我們使用例如Class.forName方法來動態的加載該類的時候,傳入了一個類名,但是其並沒有在類路徑中被找到的時候,就會報ClassNotFoundException異常。出現這種情況,一般都是類名字傳入有誤導致的。
我們再來看下JDK中對該錯誤NoClassDefFoundError的闡述:
大概意思是這樣的,如果JVM或者ClassLoader實例嘗試加載(可以通過正常的方法調用,也可能是使用new來創建新的對象)類的時候卻找不到類的定義。但是要查找的類在編譯的時候是存在的,運行的時候卻找不到了。這個時候就會導致NoClassDefFoundError。出現這種情況,一般是由於打包的時候漏掉了部分類或者Jar包被篡改已經損壞。
前面我們談到了Java是一種先編譯,後解釋執行的語言。那麼我們就來說下何爲JIT編譯器吧。
JIT編譯器全名叫Just In Time Compile 也就是即時編譯器,把經常運行的代碼作爲"熱點代碼"編譯成與本地平臺相關的機器碼,並進行各種層次的優化。JIT編譯除了具有緩存的功能外,還會對代碼做各種優化,包括逃逸分析、鎖消除、 鎖膨脹、方法內聯、空值檢查消除、類型檢測消除以及公共子表達式消除等。
解釋:
JIT編譯器屬於Java基礎中的比較有深度的題目了,回答出來算是一個亮點了。既然說到了JIT編譯器,我們來看下JIT對代碼優化使用到的逃逸分析技術吧。
逃逸分析:
逃逸分析的基本行爲就是分析對象動態作用域,當一個對象在方法中被定義後,它可能被外部方法所引用,例如作爲調用參數傳遞到其他地方中,稱爲方法逃逸。JIT編譯器的優化包括如下:
-
同布省略:也就是鎖消除,當JIT編譯器判斷不會產生併發問題,那麼會將同步synchronized去掉
-
標量替換
我們先來解釋下標量和聚合量的基本概念。
-
標量(Scalar)是指一個無法再分解成更小的數據的數據。Java中的原始數據類型就是標量。
-
聚合量(Aggregate)是還可以分解的數據。Java中的對象就是聚合量,因爲他可以分解成其他聚合量和標量。
在JIT階段,如果經過逃逸分析,發現一個對象不會被外界訪問的話,那麼經過JIT優化,就會把這個對象拆解成若干個其中包含的若干個成員變量來代替。這個過程就是標量替換。標量替換的好處就是對象可以不在堆內存進行分配,爲棧上分配提供了良好的基礎。
那麼逃逸分析技術存在哪些缺點呢?
技術不是特別成熟,分析的過程也很耗時,如果沒有一個對象是不逃逸的,那麼就得不償失了。
三、Java中的值傳遞和引用傳遞
值傳遞和引用傳遞的解釋可以概括如下。
-
值傳遞,意味着傳遞了對象的一個副本,即使副本被改變,也不會影響源對象。
-
引用傳遞,意味着傳遞的並不是實際的對象,而是對象的引用。因此,外部對引用對象的改變會反映到所有的對象上。
我們先看一個值傳遞的例子:
public class Test { public static void main(String[] args) { int x=0; change(x); System.out.println(x); } static void change(int i){ i=7; } }
我們再來分析一個引用傳遞的例子:
public class Test { public static void main(String[] args) { StringBuffer x = new StringBuffer("Hello"); change(x); System.out.println(x); } static void change(StringBuffer i) { i.append(" world!"); } }
接着,我們修改下change方法,代碼如下所示:
public class Test { public static void main(String[] args) { StringBuffer x = new StringBuffer("Hello"); change2(x); System.out.println(x); } static void change2(StringBuffer i) { i = new StringBuffer("hi"); i.append(" world!"); } }
最後,我們繼續升級該題目代碼如下:
public class Test { public static void main(String[] args) { StringBuffer sb = new StringBuffer("Hello "); System.out.println("Before change, sb = " + sb); changeData(sb); System.out.println("After change, sb = " + sb); } public static void changeData(StringBuffer strBuf) { StringBuffer sb2 = new StringBuffer("Hi,I am "); strBuf = sb2; sb2.append("World!"); } }
Before change, sb = Hello
After change, sb = Hello