天天看點

java編譯時和運作時

轉載,原文連結、譯文連結和譯者等資訊如下:

譯文連結:http://www.importnew.com/1796.html

英文原文:java-success,編譯:ImportNew - 朱偉傑

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

Q.下面的代碼片段中,行A和行B所辨別的代碼有什麼差別呢?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

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檔案的話,那麼你就會從下面的結果裡得到答案。

1 2 3 4 5 6 7 8 9 10 11 12 13

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.

java編譯時和運作時

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

1 2 3 4

public

class

{

public

static

void

evaluate(String param1); 

// method #1

public

static

void

evaluate(

int

param1);  

// method #2

}

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

1

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

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

方法覆寫:這個是在運作時發生的。方法重載被稱為運作時多态,因為在編譯期編譯器不知道并且沒法知道該去調用哪個方法。JVM會在代碼運作的時候做出決定。

1 2 3 4 5 6 7 8 9 10 11 12

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(..)方法。如果編譯器遇到下面的代碼:

1 2 3

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的相容性。

1

List<String> myList =

new

ArrayList<String>(

10

);

編譯後成為了:

1

List myList =

new

ArrayList(

10

);

注解(Annotation):你可以使用運作時或者編譯時的注解。

1 2 3 4 5 6

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,這個功能已經是編譯器的一部分了。

1 2 3 4 5 6 7 8 9 10

public

class

MyTest{

@Test

public

void

testEmptyness( ){

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

}

private

List getList( ){

//implemenation goes here

}

}

@Test是JUnit架構用來在運作時通過反射來決定調用測試類的哪個(些)方法的注解。

1 2 3 4

@Test

(timeout=

100

)

public

void

testTimeout( ) {

while

(

true

);  

//infinite loop

}

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

1 2 3 4

@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”關鍵字來産生子類的方式實作的,如下所示:

1 2 3 4 5 6 7 8 9 10 11 12

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裡,代理的典型實作方式如下:

1 2 3 4 5 6 7 8 9 10 11

public

class

Parent {

public

String saySomething( ) {

return

“Parent is called”;

}

}

public

class

Child  {

public

String saySomething( ) {

return

new

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

}

}

子類代理了父類的調用。組合可以按照下面的方式來實作:

1 2 3 4 5 6 7 8 9 10 11

public

class

Child  {

private

Parent parent =

null

;

public

Child( ){

this

.parent =

new

Parent( );

}

public

String saySomething( ) {

return

this

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

}

}