这是发表于台湾java网站上的一篇文章。
Interface 和 Abstract class 的差異,我想各位都有一定的瞭解,雖然在別的 forum 看過有人問起這個問題,我不打算在此討論兩者的細節。我打算只提及一些我個人對二者的感覺。
Interface 和 Class 兩者皆可引進 type 的觀念,前者純粹是定義了由一組介面所構成的規格,本身無法承載實做細節;後者本身也能帶來 type 的定義,同時還能附上實做的細節,abstract class 是一種 class(abstract class 甚至可以沒有任何 abstract method),同樣具備 class 所有的條件。這裡我強調的是後者(class)本身帶來 type(規格)這一件事。
為什麼要強調這一點呢?當你寫出如下的程式碼:
12345678910111213141516
interface IX {
public Object narrow();
}
public class XY implements IX {
public static void main(String[] args)
{
XY obj = new XY();
System.out.println(obj.narrow());
}
public Object narrow()
{
return this;
}
}
XY implements IX 也定義了和 IX 所明列的操作(這是必然的,編譯器要求設計者做到這一點)。
基本上你操作 obj 所參考的物件是以 XY(type) 的觀點(規格)來操作,main method 裡沒有 IX interface 的參與,即使你沒有讓 XY implements IX,main method 所產生的 instructions 和上例所產生的是一模一樣,並不會因為 XY implements IX 而使得在 invoke 符合 IX 所明列的操作介面的那一點上編譯器"主動"把物件(XY instance)視為一種 IX 規格的東西。我對這一點的看法是,implements IX 只是通知編譯器在必要的時刻允許 VM 將 XY instance 當作是 IX 規格的東西,"XY implements IX" 表示出 XY 相容於 IX 規格(所以編譯器要求一定要明白實做出 IX 所有的介面,否則只能成為 abstract class)。儘管如此,narrow method 的確成為了 XY 的一個(操作)介面,我的意思是這並不是因為 XY implements IX 不得不實做出這樣的一個(操作)介面所帶來的結果,而是因為 XY 本身也定義(宣告)了 narrow 這樣一個(操作)介面而使得 XY 相容於 IX,回顧此段一開始所提及,即使 IX interface 根本不存在而 CX 沒有 implemets IX,main method 一樣能操作的好好(因為 instructions 完全沒變),不會有 runtime error,CX 能執行 narrow 操作不是拜 IX 之賜,純粹是 XY "自己"定義並實做了這個操作。可能乍聽之下有點意外,我的意思是:因為 XY 定義了 narrow (操作)介面,所以可以明列出 XY 是相容於 IX 這一點(implements IX)並在適當時刻把 XY object 當作 IX 規格之物,而不是反過來的因果關係,因為 implements IX,所以 XY 到定義並實做 narrow (操作)介面,雖然這樣說很合理,但我覺得這樣可能會帶來一些誤導。
現在出現了一個 XYZ 類別 extends XY 並 implements 其他的 interface but IX,XYZ 透過 extends 的繼承機制同時從 XY 竊取了規格(type)與實做(implementation),即便 XYZ 不作什麼就可以把 narrow 的實做碼變成自己的一部份(其實是可以向 XY 借用),因而 XYZ (一定)可以相容於 IX 規格,從這一點來看,可能會比較清楚我在上一段所強調的東西。現在我產生了 XYZ object 並執行其 narrow 操作:
123
XYZ obj = new XYZ();
// or XY obj = new XYZ();
obj.narrow();
上一段例子一樣,這幾行完全沒出現 IX 的影子,也沒受到 IX 帶來的影響,拿掉 IX 並不指明 XY 相容於 IX ,還是跑得好好的。(到這裡還是不清楚我的意思也無大礙,繼續下去就會慢慢清楚了)
照我所講的 "implements" 帶來 class 相容某個 interface 的資料與保證,可以在必要的時刻讓 VM 把 XY instance 當作 IX 規格之物來用,什麼是必要時刻呢?
123456
XY obj = new XY();
System.out.println(((IX)obj).narrow());
/* or
* IX obj = new XY();
* System.out.println(obj.narrow());
*/
產生出來的 instructions 和 mark 掉的部分相同,(((IX)ob).narrow(); 此一 statement 並不會真的作 checkcast 的動作,類似 class 之間的 casting 的道理)只是語意上不大一樣,前者編譯器會以 XY 的規格來對待 new XY() 所建構的物件,"偶爾"臨時以 IX 規格來對待;後者則是直接以 IX 規格來對待新建構的物件。
正式進入主題了,這一次我想要比較什麼呢?基本上 class 和 inteface 本質就差異蠻大的,即使 abstract class 和 interface 之間還是差很多,所以我真正的焦點在於退化的 abstract class,一個只擁有(自己只定義) abstract method 的 pure abstract class(講 pure 也不太恰當,畢竟還是至少得繼承 java.lang.Object,就會有 concrete method)。一個 interface 可以改寫成 pure abstract class,用起來在多數情況下以 programmer 角度來看是相同的,除了 interface 可以隨時混搭到其他的 type hierarchy 裡,pure abstract class 不行,但是後者的資料承載比較強。如果現在有一份規格已訂出來了,你會以 pure abstract class 來訂製還是做成一個 interface,我會選擇做成 interface,基於彈性的考量,可能你也是吧!不過在看完下面的對抗之後,可能或多或少會影響你的抉擇。
對抗的是執行效率,因為以不同的觀點來看帶同一個物件並執行同一操作所使用的 instruction 不同,很明顯在效率上一定有差別。
1234
XY obj = new XY();
IX x = obj;
obj.narrow(); // used instruction: invokevirtual
x.narrow(); // used instruction: invokeinterface
在實際測量前,我大概知道輸家是 invokeinterface,因為其需要從 stack 取出的 actual argument 總是比 invokevirtual 多一個(多一個用來指示要從 stack 取出的參數數量),花的時間很難不多一點。
我把測試用的相關類別 post 一份在這裡方便直接瀏覽,也可以按此下載。
IX.java
12345
package com.jsptw.example.interfac;
public interface IX {
public void action();
}
CX.java
12345
package com.jsptw.example.jabstrac;
public abstract class CX {
public abstract void action();
}
XObject.java
12345678910
package com.jsptw.example;
import com.jsptw.example.interfac.IX;
import com.jsptw.example.jabstrac.CX;
public class XObject extends CX implements IX{
public void action()
{
}
}
Measurement.java
12345
package com.jsptw.example;
public abstract class Measurement {
public abstract long evaluate(XObject x, int freq);
}
ForInterface.java
123456789101112131415161718
package com.jsptw.example.interfac;
import com.jsptw.example.Measurement;
import com.jsptw.example.XObject;
public class ForInterface extends Measurement {
public long evaluate(XObject x, int freq)
{
IX obj = x;
long start = System.currentTimeMillis();
for (int i = 0; i < freq; ++i)
obj.action();
long end = System.currentTimeMillis();
return end - start;
}
}
ForAbstractC.java
123456789101112131415161718
package com.jsptw.example.jabstrac;
import com.jsptw.example.Measurement;
import com.jsptw.example.XObject;
public class ForAbstractC extends Measurement {
public long evaluate(XObject x, int freq)
{
CX obj = x;
long start = System.currentTimeMillis();
for (int i = 0; i < freq; ++i)
obj.action();
long end = System.currentTimeMillis();
return end - start;
}
}
MeasureProc.java
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
package com.jsptw.example;
public class MeasureProc {
public static final int UNIT = 1000000;
public MeasureProc(XObject sample, Measurement m, int iterations)
{
this.sample = sample;
this.iterations = iterations;
measurement = m;
}
public MeasureProc(XObject sample, Measurement m)
{
this(sample, m, 1);
}
public MeasureProc(XObject sample)
{
this(sample, null);
}
public int getIterations()
{
return iterations;
}
public void setIterations(int i)
{
iterations = i;
}
public Measurement getMeasurement()
{
return measurement;
}
public void setMeasurement(Measurement measurement)
{
this.measurement = measurement;
}
public long getResult()
{
if (measurement == null)
return 0;
return measurement.evaluate(sample, iterations);
}
private Measurement measurement;
private XObject sample;
private int iterations;
}
附帶一提,我已經儘量做到公平,讓兩者的 package+class name 一樣長(這和 VM 搜尋 callee 有關)。另外我在計算時間時把 loop counter 的操作所好時間一起加進去(因為沒辦法把每次執行 action method 的時間分次計算然後求和),當然也包括了執行 System.currentTimeMillis() 本身所需要的時間(取得截止時間時)。
以下是結果:(powered by JFreeChart 0.9.8)
我本來也想順便測試在其他的 Java Platform(J2ME) 的各個 Configuration(使用不同的 VM, KVM/CVM 之類的)下作測試,後來想想還是把這樣的機會讓給有興趣也對其他相關技術熟悉的人。