數據類型的局限
之前我們一直在說,程序主要就是數據以及對數據的操作,而為了方便操作數據,高級語言引入了數據類型的概念,Java定義了八種基本數據類型,而類相當於是自定義數據類型,通過類的組合和繼承可以表示和操作各種事物或者說對象。
但,這種只是將對象看做屬於某種數據類型,並按該類型進行操作,在一些情況下,並不能反映對象以及對對象操作的本質。
為什麼這麼說呢?很多時候,我們實際上關心的,並不是對象的類型,而是對象的能力,只要能提供這個能力,類型並不重要。我們來看一些生活中的例子。
要拍個照片,很多時候,只要能拍出符合需求的照片就行,至於是用手機拍,還是用Pad拍,或者是用單反相機拍,並不重要,關心的是對象是否有拍出照片的能力,而並不關心對象到底是什麼類型,手機、Pad或單反相機都可以。
要計算一組數字,只要能計算出正確結果即可,至於是由人心算,用算盤算,用計算器算,用電腦軟件算,並不重要,關心的是對象是否有計算的能力,而並不關心對象到底是算盤還是計算器。
要將冷水加熱,只要能得到熱水即可,至於是用電磁爐加熱,用燃氣灶加熱,還是用電熱水壺,並不重要,重要的是對象是否有加熱水的能力,而並不關心對象到底是什麼類型。
在這些情況中,類型並不重要,重要的是能力。那如何表示能力呢?
接口的概念
Java使用接口這個概念來表示能力。
接口這個概念在生活中並不陌生,電子世界中一個常見的接口就是USB接口。電腦往往有多個USB接口,可以插各種USB設備,可以是鍵盤、鼠標、U盤、攝像頭、手機等等。
接口聲明了一組能力,但它自己並沒有實現這個能力,它只是一個約定,它涉及交互兩方對象,一方需要實現這個接口,另一方使用這個接口,但雙方對象並不直接互相依賴,它們只是通過接口間接交互。圖示如下:
拿上面的USB接口來說,USB協議約定了USB設備需要實現的能力,每個USB設備都需要實現這些能力,電腦使用USB協議與USB設備交互,電腦和USB設備互不依賴,但可以通過USB接口相互交互。
下面我們來看Java中的接口。
定義接口
我們通過一個例子來說明Java中接口的概念。
這個例子是"比較",很多對象都可以比較,對於求最大值、求最小值、排序的程序而言,它們其實並不關心對象的類型是什麼,只要對象可以比較就可以了,或者說,它們關心的是對象有沒有可比較的能力。Java API中提供了Comparable接口,以表示可比較的能力,但它使用了泛型,而我們還沒有介紹泛型,所以本節,我們自己定義一個Comparable接口,叫MyComparable。
現在,首先,我們來定義這個接口,代碼如下:
public interface MyComparable { int compareTo(Object other); }
解釋一下:
再來解釋一下compareTo方法:
接口與類不同,它的方法沒有實現代碼。定義一個接口本身並沒有做什麼,也沒有太大的用處,它還需要至少兩個參與者,一個需要實現接口,另一個使用接口,我們先來實現接口。
實現接口
類可以實現接口,表示類的對象具有接口所表示的能力。我們來看一個例子,以前面介紹過的Point類來說明,我們讓Point具備可以比較的能力,Point之間怎麼比較呢?我們假設按照與原點的距離進行比較,下面是Point類的代碼:
public class Point implements MyComparable { private int x; private int y; public Point(int x, int y) { this.x = x; this.y = y; } public double distance(){ return Math.sqrt(x*x+y*y); } @Override public int compareTo(Object other) { if(!(other instanceof Point)){ throw new IllegalArgumentException(); } Point otherPoint = (Point)other; double delta = distance() - otherPoint.distance(); if(delta<0){ return -1; }else if(delta>0){ return 1; }else{ return 0; } } @Override public String toString() { return "("+x+","+y+")"; } }
我們解釋一下:
我們再來解釋一下Point的compareTo實現:
一個類可以實現多個接口,表明類的對象具備多種能力,各個接口之間以逗號分隔,語法如下所示:
public class Test implements Interface1, Interface2 { .... }
定義和實現了接口,接下來我們來看怎麼使用接口。
使用接口
與類不同,接口不能new,不能直接創建一個接口對象,對象只能通過類來創建。但可以聲明接口類型的變量,引用實現了接口的類對象。比如說,可以這樣:
MyComparable p1 = new Point(2,3); MyComparable p2 = new Point(1,2); System.out.println(p1.compareTo(p2));
p1和p2是MyComparable類型的變量,但引用了Point類型的對象,之所以能賦值是因為Point實現了MyComparable接口。如果一個類型實現了多個接口,那這種類型的對象就可以被賦值給任一接口類型的變量。
p1和p2可以調用MyComparable接口的方法,也只能調用MyComparable接口的方法,實際執行時,執行的是具體實現類的代碼。
為什麼Point類型的對象非要賦值給MyComparable類型的變量呢?在以上代碼中,確實沒必要。但在一些程序中,代碼並不知道具體的類型,這才是接口發揮威力的地方,我們來看下面使用MyComparable接口的例子。
public class CompUtil { public static Object max(MyComparable[] objs){ if(objs==null||objs.length==0){ return null; } MyComparable max = objs[0]; for(int i=1;i<objs.length;i++){ if(max.compareTo(objs[i])<0){ max = objs[i]; } } return max; } public static void sort(MyComparable[] objs){ for(int i=0;i<objs.length;i++){ for(int j=i+1;j<objs.length;j++){ if(objs[i].compareTo(objs[j])>0){ MyComparable temp = objs[i]; objs[i] = objs[j]; objs[j] = temp; } } } } }
類CompUtil提供了兩個方法,max獲取傳入數組中的最大值,sort對數組升序排序,參數都是MyComparable類型的數組。max的代碼是比較容易理解的,不再解釋,sort使用的是冒泡排序,其細節我們留待後續文章解釋。
可以看出,這個類是針對MyComparable接口編程,它並不知道具體的類型是什麼,也並不關心,但卻可以對任意實現了MyComparable接口的類型進行操作。我們來看下對Point類型進行操作,代碼如下:
Point[] points = new Point[]{ new Point(2,3), new Point(3,4), new Point(1,2) }; System.out.println("max: " + CompUtil.max(points)); CompUtil.sort(points); System.out.println("sort: "+ Arrays.toString(points));
創建了一個Point類型的數組points,然後使用CompUtil的max方法獲取最大值,使用sort排序,並輸出結果,輸出如下:
max: (3,4) sort: [(1,2), (2,3), (3,4)]
這裡演示的是對Point數組操作,實際上可以針對任何實現了MyComparable接口的類型數組進行操作。
這就是接口的威力,可以說,針對接口而非具體類型進行編程,是計算機程序的一種重要思維方式。針對接口,很多時候反映了對象以及對對象操作的本質。它的優點有很多,首先是代碼復用,同一套代碼可以處理多種不同類型的對象,只要這些對象都有相同的能力,如CompUtil。
更重要的是降低了耦合,提高了靈活性,使用接口的代碼依賴的是接口本身,而非實現接口的具體類型,程序可以根據情況替換接口的實現,而不影響接口使用者。解決復雜問題的關鍵是分而治之,分解為小問題,但小問題之間不可能一點關系沒有,分解的核心就是要降低耦合,提高靈活性,接口為恰當分解,提供了有力的工具。
接口的細節
上面我們介紹了接口的基本內容,接口還有一些細節,包括:
我們逐個來介紹下。
接口中的變量
接口中可以定義變量,語法如下所示:
public interface Interface1 { public static final int a = 0; }
這裡定義了一個變量int a,修飾符是public static final,但這個修飾符是可選的,即使不寫,也是public static final。這個變量可以通過"接口名.變量名"的方式使用,如Interface1.a。
接口的繼承
接口也可以繼承,一個接口可以繼承別的接口,繼承的基本概念與類一樣,但與類不同,接口可以有多個父接口,代碼如下所示:
public interface IBase1 { void method1(); } public interface IBase2 { void method2(); } public interface IChild extends IBase1, IBase2 { }
接口的繼承同樣使用extends關鍵字,多個父接口之間以逗號分隔。
類的繼承與接口
類的繼承與接口可以共存,換句話說,類可以在繼承基類的情況下,同時實現一個或多個接口,語法如下所示:
public class Child extends Base implements IChild { //... }
extends要放在implements之前。
instanceof
與類一樣,接口也可以使用instanceof關鍵字,用來判斷一個對象是否實現了某接口,例如:
Point p = new Point(2,3); if(p instanceof MyComparable){ System.out.println("comparable"); }
使用接口替代繼承
上節我們提到,可以使用接口替代繼承。怎麼替代呢?
我們說繼承至少有兩個好處,一個是復用代碼,另一個是利用多態和動態綁定統一處理多種不同子類的對象。
使用組合替代繼承,可以復用代碼,但不能統一處理。使用接口,針對接口編程,可以實現統一處理不同類型的對象,但接口沒有代碼實現,無法復用代碼。將組合和接口結合起來,就既可以統一處理,也可以復用代碼了。我們還是以上節的例子來說明。
我們先增加一個接口IAdd,代碼如下:
public interface IAdd { void add(int number); void addAll(int[] numbers); }
修改Base代碼,讓他實現IAdd接口,代碼基本不變:
public class Base implements IAdd { //... }
修改Child代碼,也是實現IAdd接口,代碼基本不變:
public class Child implements IAdd { //... }
這樣,就既可以復用代碼,也可以統一處理,而且不用擔心破壞封裝了。
小結
本節我們談了數據類型思維的局限,提到了很多時候關心的是能力,而非類型,所以引入了接口,介紹了Java中接口的概念和細節,針對接口編程是一種重要的程序思維方式,這種方式不僅可以復用代碼,還可以降低耦合,提高靈活性,是分解復雜問題的一種重要工具。
接口沒有任何實現代碼,而之前介紹的類都有完整的實現,都可以創建對象,Java中還有一個介於接口和類之間的概念,抽象類,它有什麼用呢?
----------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。