1. 程式人生 > >Java 面試題問與答:編譯時與執行時

Java 面試題問與答:編譯時與執行時

在開發和設計的時候,我們需要考慮編譯時,執行時以及構建時這三個概念。理解這幾個概念可以更好地幫助你去了解一些基本的原理。下面是初學者晉級中級水平需要知道的一些問題。

Q.下面的程式碼片段中,行A和行B所標識的程式碼有什麼區別呢?

public class ConstantFolding {

    static final  int number1 = 5;

    static final  int number2 = 6;

    static int number3 = 5;

    static int number4= 6;

    public static void main(String[ ] args) {

          int product1 = number1 * number2;         //line A

          int product2 = number3 * number4;         //line B

    }

}

A.在行A的程式碼中,product的值是在編譯期計算的,行B則是在執行時計算的。如果你使用Java反編譯器(例如,jd-gui)來反編譯ConstantFolding.class檔案的話,那麼你就會從下面的結果裡得到答案。

public class ConstantFolding

{

  static final int number1 = 5;

  static final int number2 = 6;

  static int number3 = 5;

  static int number4 = 6;

  public static void main(String[ ] args)

  {

      int product1 = 30;

      int product2 = number3 * number4;

  }

}

常量摺疊是一種Java編譯器使用的優化技術。由於final變數的值不會改變,因此就可以對它們優化。Java反編譯器和javap命令都是檢視編譯後的程式碼(例如,位元組碼)的利器。

Q.你能想出除了程式碼優化外,在什麼情況下,檢視編譯過的程式碼是很有幫助的?

A.Java裡的泛型是在編譯時構造的,可以通過檢視編譯後的class檔案來理解泛型,也可以通過檢視它來解決泛型相關的問題。

Q.下面哪些是發生在編譯時,執行時,或者兩者都有?

A.

方法過載:這個是發生在編譯時的。方法過載也被稱為編譯時多型,因為編譯器可以根據引數的型別來選擇使用哪個方法。

public class {

     public static void evaluate(String param1);  // method #1

     public static void evaluate(int param1);   // method #2

}

如果編譯器要編譯下面的語句的話:

evaluate(“My Test Argument passed to param1”);

它會根據傳入的引數是字串常量,生成呼叫#1方法的位元組碼

方法覆蓋:這個是在執行時發生的。方法過載被稱為執行時多型,因為在編譯期編譯器不知道並且沒法知道該去呼叫哪個方法。JVM會在程式碼執行的時候做出決定。

public class A {

   public int compute(int input) {          //method #3

        return 3 * input;

   }        

}

public class B extends A {

   @Override

   public int compute(int input) {          //method #4

        return 4 * input;

   }        

}

子類B中的compute(..)方法重寫了父類的compute(..)方法。如果編譯器遇到下面的程式碼:

public int evaluate(A reference, int arg2)  {

     int result = reference.compute(arg2);

}

編譯器是沒法知道傳入的引數reference的型別是A還是B。因此,只能夠在執行時,根據賦給輸入變數“reference”的物件的型別(例如,A或者B的例項)來決定呼叫方法#3還是方法#4.

泛型(又稱型別檢驗):這個是發生在編譯期的。編譯器負責檢查程式中型別的正確性,然後把使用了泛型的程式碼翻譯或者重寫成可以執行在當前JVM上的非泛型程式碼。這個技術被稱為“型別擦除“。換句話來說,編譯器會擦除所有在尖括號裡的型別資訊,來保證和版本1.4.0或者更早版本的JRE的相容性。

List<String> myList = new ArrayList<String>(10);

編譯後成為了:

List myList = new ArrayList(10);

註解(Annotation):你可以使用執行時或者編譯時的註解。

public class B extends A {

   @Override

    public int compute(int input){      //method #4

        return 4 * input;

    }       

}

@Override是一個簡單的編譯時註解,它可以用來捕獲類似於在子類中把toString()寫成tostring()這樣的錯誤。在Java 5中,使用者自定義的註解可以用註解處理工具(Anotation Process Tool ——APT)在編譯時進行處理。到了Java 6,這個功能已經是編譯器的一部分了。

public class MyTest{

    @Test

     public void testEmptyness( ){

         org.junit.Assert.assertTrue(getList( ).isEmpty( ));

     }

     private List getList( ){

        //implemenation goes here

     }

}

@Test是JUnit框架用來在執行時通過反射來決定呼叫測試類的哪個(些)方法的註解。

@Test (timeout=100)

public void testTimeout( ) {

    while(true);   //infinite loop

}

如果執行時間超過100ms的話,上面的測試用例就會失敗。

@Test (expected=IndexOutOfBoundsException.class)

public void testOutOfBounds( ) {

       new ArrayList<Object>( ).get(1);

}

如果上面的程式碼在執行時沒有丟擲IndexOutOfBoundsException或者丟擲的是其他的異常的話,那麼這個用例就會失敗。使用者自定義的註解可以在執行時通過Java反射API裡新增的AnnotatedElement和”Annotation”元素介面來處理。

異常(Exception):你可以使用執行時異常或者編譯時異常。

執行時異常(RuntimeException)也稱作未檢測的異常(unchecked exception),這表示這種異常不需要編譯器來檢測。RuntimeException是所有可以在執行時丟擲的異常的父類。一個方法除要捕獲異常外,如果它執行的時候可能會丟擲RuntimeException的子類,那麼它就不需要用throw語句來宣告丟擲的異常。

例如:NullPointerException,ArrayIndexOutOfBoundsException,等等

受檢查異常(checked exception)都是編譯器在編譯時進行校驗的,通過throws語句或者try{}cathch{} 語句塊來處理檢測異常。編譯器會分析哪些異常會在執行一個方法或者建構函式的時候丟擲。

面向切面的程式設計(Aspect Oriented Programming-AOP):切面可以在編譯時,執行時或,載入時或者執行時織入。

  • 編譯期:編譯期織入是最簡單的方式。如果你擁有應用的程式碼,你可以使用AOP編譯器(例如,ajc – AspectJ編譯器)對原始碼進行編譯,然後輸出織入完成的class檔案。AOP編譯的過程包含了waver的呼叫。切面的形式可以是原始碼的形式也可以是二進位制的形式。如果切面需要針對受影響的類進行編譯,那麼你就需要在編譯期織入了。

  • 編譯後:這種方式有時候也被稱為二進位制織入,它被用來織入已有的class檔案和jar檔案。和編譯時織入方式相同,用來織入的切面可以是原始碼也可以是二進位制的形式,並且它們自己也可以被織入切面。

  • 裝載期:這種織入是一種二進位制織入,它被延遲到JVM載入class檔案和定義類的時候。為了支援這種織入方式,需要顯式地由執行時環境或者通過一種“織入代理(weaving agent)“來提供一個或者多個“織入類載入器(weaving class loader)”。

  • 執行時:對已經載入到JVM裡的類進行織入

繼承 – 發生在編譯時,因為它是靜態的

代理或者組合 – 發生在執行時,因為它更加具有動態性和靈活性。

Q.你有沒有聽說過“組合優於繼承”這樣的說法呢?如果聽說過的話,那麼你是怎麼理解的呢?

A.繼承是一種多型工具,而不是一種程式碼複用工具。有些開發者喜歡用繼承的方式來實現程式碼複用,即使是在沒有多型關係的情況下。是否使用繼承的規則是繼承只能用在類之間有“父子”關係的情況下。

  • 不要僅僅為了程式碼複用而繼承。當你使用組合來實現程式碼複用的時候,是不會產生繼承關係的。過度使用繼承(通過“extends”關鍵字)的話,如果修改了父類,會損壞所有的子類。這是因為子類和父類的緊耦合關係是在編譯期產生的。

  • 不要僅僅為了多型而繼承。如果你的類之間沒有繼承關係,並且你想要實現多型,那麼你可以通過介面和組合的方式來實現,這樣不僅可以實現程式碼重用,同時也可以實現執行時的靈活性。

這就是為什麼四人幫(Gang of Four)的設計模式裡更傾向於使用組合而不是繼承的原因。面試者會在你的答案裡著重關注這幾個詞語——“耦合”,“靜態還是動態”,以及“發生在編譯期還是執行時”。執行時的靈活性可以通過組合來實現,因為類可以在執行時動態地根據一個結果有條件或者無條件地進行組合。但是繼承卻是靜態的。

Q.你能夠通過例項來區別編譯期繼承和執行時繼承,以及指出Java支援哪種嗎?

A.“繼承”表示動作和屬性從一個物件傳遞到另外一個物件的場景。Java語言本身只支援編譯期繼承,它是通過“extends”關鍵字來產生子類的方式實現的,如下所示:

public class Parent {

    public String saySomething( ) {

          return “Parent is called”;

    }

}

public class Child extends Parent {

     @Override

     public String saySomething( ) {

          return super.saySomething( ) +  “, Child is called”;

    }

}

“Child”類的saySomething()方法的呼叫會返回“Parent is called,Child is Called”,因為,子類的呼叫繼承了父類的“Parenet is called”。關鍵字“super”是用來呼叫“Parent”類的方法。執行時繼承表示在執行時構建父/子類關係。Java語言本身不支援執行時繼承,但是有一種替代的方案叫做“代理”或者“組合”,它表示在執行時元件一個層次物件的子類。這樣可以模擬執行時繼承的實現。在Java裡,代理的典型實現方式如下:

public class Parent {

    public String saySomething( ) {

          return “Parent is called”;

    }

}

public class Child  {

     public String saySomething( ) {

          return new Parent( ).saySomething( ) +  “, Child is called”;

    }

}

子類代理了父類的呼叫。組合可以按照下面的方式來實現:

public class Child  {

     private Parent parent = null;

     public Child( ){

          this.parent = new Parent( );

     }

     public String saySomething( ) {

          return this.parent.saySomething( ) +  “, Child is called”;

    }

}

                             長按二維碼關注我們,檢視更多精彩(微信公眾號)