介面 (Interface) – 描述不同類別的共通行為
撰寫物件導向程式的第一步, 就是分析出程式中需要哪些類別, 以及類別之間的繼承關係。不過就像我們在現實世界中所看到的, 許多『不同類』的事物, 其間又通常會具有一些相似的行為。舉例來說, 飛機和小鳥很顯然不會是同性質的類別, 而其飛的方式也不同, 但不可否認它們都具有會飛行的行為。
類似這樣的情況, 在設計程式時也會遇到:一些在繼承架構中明顯不同的類別, 它們卻有具有一些相似的行為 (特性), 造成設計類別時的困擾, 例如將明顯不同性質的類別 (例如飛機和鳥), 湊成在同一繼承架構下, 使得類別的繼承關係不合常理。因此為了不打亂原有的繼承架構, 我們可能要選擇分別在各自的類別中定義各自有的行為。
為了讓不同性質的類別, 在實作它們應有的共通行為時, 不會造成遺漏或命名不一致, 在 Java 中特別提供了介面 (Interface) 來描述這個共通的行為。
Java 不支援多重繼承
有些物件導向程式語言會支援多重繼承 (Multiple Inheritance), 也就是讓單一類別同時繼承自多個父類別, 如此一來可解決上述不同性質類別有共通行為的問題。
不過多重繼承也會使語言複雜化, 而在第一章我們就提過, 『簡單』是 Java 語言的主要特色之一, 因此當初開發 Java 語言的小組就決定讓 Java 語言不支援多重繼承, 以保持其簡單學的特色。
雖然如此, Java 的實用性並不因此而減少, 需要使用到多重繼承的場合, 幾乎也都可透過 Java 的介面功能達成。
定義介面
由於介面代表的是一群共通的行為, 感覺上和類別好像有些類似之處, 但其實兩者具有相當的差異, 只是外觀上定義介面也是用大括號來描述此介面的方法, 而開頭要改用 interface 來表示要定義的是介面:
interface 介面名稱 { // 介面中的方法 } |
由於類別是用以描述實際存在的物件, 而介面則僅是用以描述某種行為方式 (例如『會飛』這件事), 所以兩者本質上有許多差異, 以下是定義介面時要注意的重點:
介面的命名也和類別一樣, 通常都是以首字母大寫的方式, 使得其在程式中容易被識別。有些人習慣在介面的名稱前加上一個大寫字母'I", 以特別標示這個名稱是個介面。
在介面中只能定義方法的型別 (傳回值) 及參數型別, 不可定義方法本體 (和抽象方法相同), 這些方法預設都會自動成為public 的抽象方法, 請記得在右括號之後加上分號。
由於介面通常代表某種特性, 因此介面的名稱一般都是一個形容詞, 表示可以如何的意思, 例如可用 Flying 表示『會飛』的意思。
舉例來說, 前一章最後我們舉了計算地價的範例, 要計算地價時, 當然要算出土地的面積, 然而計算面積這件事, 可能是很多類別需要的功能, 所以我們可以定義一個計算面積的介面:
interface Surfacing { double area(); // 計算面積的方法 } |
如前所述, 不可定義方法本體, 因為每種不同形體, 其面積的計算方式也都不同, 我們也無法預知有哪些類別需要計算面積, 因此此處 surfacing 介面只規定了計算面積的方式名稱為 area、沒有參數、但傳回值為 double 型別。
如前所述, 介面中的方法預設都是公開的抽象方法, 所以通常 public、abstract 也都省略不寫。
介面的實作
定義好介面之後, 需要使用該介面的類別, 就必須實作該介面, 實作介面時, 必須在類別名稱之後, 使用implements保留字, 再加上要實作的介面名稱。此外, 前面提過, 介面中所定義的方法會自動成為抽象方法, 因此實作介面時就必須完全實作介面中的所有方法。
interface Surfacing { double area(); // 計算面積的方法 } class Circle implements Surfacing { ... public double area() { // 計算圓面積並回傳 } } |
我們先用上一章 Shape 類別及 Circle 類別的繼承架構, 並讓 Circle 實作 Surfacing 介面:
ShapeArea.java 實作 Surfacing 介面
interface Surfacing { double area(); // 計算面積的方法 } class Shape { // 代表圖形原點的類別 protected double x,y; // 座標 public Shape(double x,double y) { this.x = x; this.y = y; } public String toString() { return "圖形原點:(" + x + ", " + y + ")"; } } class Circle extends Shape implements Surfacing { private double r; // 圓形半徑 final static double PI = 3.14159; // 圓周率常數 public Circle(double x,double y,double r) { super(x,y); // 呼叫父類別建構方法 this.r = r; } public double area() {// 計算圓面積 return PI*r*r; } public String toString() { return "圓心:(" + x + ", " + y + ")、半徑:" + r + "、面積:" + area(); } } public class ShapeArea { public static void main(String[] argv) { Circle c = new Circle(5,8,7); System.out.println(c.toString()); } }
執行結果 圓心:(5.0, 8.0)、半徑:7.0、面積:153.93791 |
第 18 行的 Circle 類別定義, 先繼承了 Shape 類別再以 implements 保留字表示此類別要實作 Surfacing。因此 Circle 類別必須定義於 Surfacing 介面中宣告的 area() 方法, 所以在第 27 行定義了計算圓面積的 area() 方法。如果宣告了要實作某個介面, 但類別中未定義該介面所宣告的方法, 編譯時將會出現錯誤。
另外, 要記住介面所提供的方法都是 public, 因此實作介面時也要將之宣告為 public, 不可將之設為 protected 或 private, 如此也會造成編譯錯誤。
介面中的成員變數
介面也可以擁有成員變數, 不過在介面中宣告的成員會自動擁有static public final的存取控制。換句話說, 在介面中僅能定義由所有實作該介面的類別所共享的常數。舉例來說, 在剛才的例子中, 將圓周率常數定義在介面中, 而實作該介面的類別, 亦可存取該常數:
InterfaceMember.java 在介面中使用成員變數
interface Surfacing { double area(); // 計算面積的方法 double PI = 3.14159; // 定義常數 } class Shape { // 代表圖形原點的類別 protected double x,y; // 座標 public Shape(double x,double y) { this.x = x; this.y = y; } public String toString() { return "圖形原點:(" + x + ", " + y + ")"; } } class Circle extends Shape implements Surfacing { private double r; // 圓形半徑 public Circle(double x,double y,double r) { super(x,y); // 呼叫父類別建構方法 this.r = r; } public double area() {// 計算圓面積 return PI*r*r; } public String toString() { return "圓心:(" + x + ", " + y + ")、半徑:" + r + "、面積:" + area(); } } public class InterfaceMember { public static void main(String[] argv) { Circle c = new Circle(3,6,2); System.out.println(c.toString()); System.out.println("圓周率:" + Surfacing.PI); System.out.println("圓周率:" + c.PI); } }
執行結果 圓心:(3.0, 6.0)、半徑:2.0、面積:12.56636 圓周率:3.14159 圓周率:3.14159 |
在第 3 行於介面中定義了成員變數 PI, 並在第 28 行 Circle 類別的 area() 方法用以計算圓面積。雖然定義 PI 時未加上任何存取控制字, 但如前述, 介面中的成員變數會自動成為 public static final, 所以在程式第 41 行也能如同存取類別 static 成員變數一般, 用介面名稱存取其值。
參考文件:
https://goo.gl/cZjY3z