第5節:Java基礎 - 必知必會(下)

第5節:Java基礎 - 必知必會(下)

 

本小節是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包被篡改已經損壞。

二、JIT編譯器

前面我們談到了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;
    }
}

  

毫無疑問,上邊的代碼會輸出0。因爲如果參數是基本數據類型,那麼是屬於值傳遞的範疇,傳遞的其實是源對象的一個copy副本,不會影響源對象的值。

我們再來分析一個引用傳遞的例子:

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!");
    }
}

  

通過運行程序,輸出爲Hello world!接下來我們通過圖片來分析下程序執行過程種的內存變化吧。

 

 

 

由圖中我們可以看出x和i指向了同樣的內存地址,那麼i.append操作將直接修改了內存地址裏邊的值,所以當方法結束,局部變量i消失,先前變量x所指向的內存值已經發生了變化,所以輸出爲Hello 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!");
    }
}

  先給出答案,上邊Demo的輸出爲Hello,我們依然來畫圖分析內存變化。

 

 

由圖中我們可以看出來,在函數change2中將引用變量i重新指向了堆內存中另一塊區域,下邊都是對另一塊區域進行修改,所以輸出是Hello。

最後,我們繼續升級該題目代碼如下:

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

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章