13、接口和抽象類有什麼區別

目錄

談談接口和抽象類有什麼區別?

典型回答

考點分析

知識擴展

我會從接口、抽象類的一些實踐,以及語言變化方面去闡述一些擴展知識點。

面向對象設計

OOP 原則實踐中的取捨

OOP 原則在面試題目中的分析


Java 是非常典型的面嚮對象語言,曾經有一段時間,程序員整天把面向對象、設計模式掛在嘴邊。雖然如今大家對這方面已經不再那麼狂熱,但是不可否認,掌握面向對象設計原則和技巧,是保證高質量代碼的基礎之一。

面向對象提供的基本機制,對於提高開發、溝通等各方面效率至關重要。考察面向對象也是面試中的常見一環,下面我來聊聊面向對象設計基礎。

談談接口和抽象類有什麼區別?

典型回答

接口和抽象類是 Java 面向對象設計的兩個基礎機制。

接口是對行爲的抽象,它是抽象方法的集合,利用接口可以達到 API 定義和實現分離的目的。接口,不能實例化;不能包含任何非常量成員,任何 field 都是隱含着 public static final 的意義;同時,沒有非靜態方法實現,也就是說要麼是抽象方法,要麼是靜態方法。Java 標準類庫中,定義了非常多的接口,比如java.util.List。

抽象類是不能實例化的類,用 abstract 關鍵字修飾 class,其目的主要是代碼重用。除了不能實例化,形式上和一般的 Java 類並沒有太大區別,可以有一個或者多個抽象方法,也可以沒有抽象方法。抽象類大多用於抽取相關 Java 類的共用方法實現或者是共同成員變量,然後通過繼承的方式達到代碼複用的目的。Java 標準庫中,比如 collection 框架,很多通用部分就被抽取成爲抽象類,例如java.util.AbstractList。

Java 類實現 interface 使用 implements 關鍵詞,繼承 abstract class 則是使用 extends 關鍵詞,我們可以參考 Java 標準庫中的 ArrayList。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//...
}

 

考點分析

這是個非常高頻的 Java 面向對象基礎問題,看起來非常簡單的問題,如果面試官稍微深入一些,你會發現很多有意思的地方,可以從不同角度全面地考察你對基本機制的理解和掌握。比如:

  •     對於 Java 的基本元素的語法是否理解準確。能否定義出語法基本正確的接口、抽象類或者相關繼承實現,涉及重載(Overload)、重寫(Override)更是有各種不同的題目。
  •     在軟件設計開發中妥善地使用接口和抽象類。你至少知道典型應用場景,掌握基礎類庫重要接口的使用;掌握設計方法,能夠在 review 代碼的時候看出明顯的不利於未來維護的設計。
  •     掌握 Java 語言特性演進。現在非常多的框架已經是基於 Java 8,並逐漸支持更新版本,掌握相關語法,理解設計目的是很有必要的。


知識擴展

我會從接口、抽象類的一些實踐,以及語言變化方面去闡述一些擴展知識點。

Java 相比於其他面嚮對象語言,如 C++,設計上有一些基本區別,比如Java 不支持多繼承。這種限制,在規範了代碼實現的同時,也產生了一些侷限性,影響着程序設計結構。Java 類可以實現多個接口,因爲接口是抽象方法的集合,所以這是聲明性的,但不能通過擴展多個抽象類來重用邏輯。

在一些情況下存在特定場景,需要抽象出與具體實現、實例化無關的通用邏輯,或者純調用關係的邏輯,但是使用傳統的抽象類會陷入到單繼承的窘境。以往常見的做法是,實現由靜態方法組成的工具類(Utils),比如 java.util.Collections。

設想,爲接口添加任何抽象方法,相應的所有實現了這個接口的類,也必須實現新增方法,否則會出現編譯錯誤。對於抽象類,如果我們添加非抽象方法,其子類只會享受到能力擴展,而不用擔心編譯出問題。

接口的職責也不僅僅限於抽象方法的集合,其實有各種不同的實踐。有一類沒有任何方法的接口,通常叫作 Marker Interface,顧名思義,它的目的就是爲了聲明某些東西,比如我們熟知的 Cloneable、Serializable 等。這種用法,也存在於
業界其他的 Java 產品代碼中。

從表面看,這似乎和 Annotation 異曲同工,也確實如此,它的好處是簡單直接。對於 Annotation,因爲可以指定參數和值,在表達能力上要更強大一些,所以更多人選擇使用 Annotation。

Java 8 增加了函數式編程的支持,所以又增加了一類定義,即所謂 functional interface,簡單說就是隻有一個抽象方法的接口,通常建議使用@FunctionalInterface Annotation 來標記。Lambda 表達式本身可以看作是一類functional interface,某種程度上這和麪向對象可以算是兩碼事。我們熟知的Runnable、Callable 之類,都是 functional interface 。

還有一點可能讓人感到意外,嚴格說,Java 8 以後,接口也是可以有方法實現的!

從 Java 8 開始,interface 增加了對 default method 的支持。Java 9 以後,甚至可以定義 private default method。Default method 提供了一種二進制兼容的擴展已有接口的辦法。比如,我們熟知的 java.util.Collection,它是collection 體系的 root interface,在 Java 8 中添加了一系列 default  method,主要是增加 Lambda、Stream 相關的功能。我在專欄前面提到的類似Collections 之類的工具類,很多方法都適合作爲 default method 實現在基礎接口裏面。

你可以參考下面代碼片段:

public interface Collection<E> extends Iterable<E> {
     /**
     * Returns a sequential Stream with this collection as its source 
     * ...
     **/
     default Stream<E> stream() {
         return StreamSupport.stream(spliterator(), false);
     }
  }

 

面向對象設計

談到面向對象,很多人就會想起設計模式,那些是非常經典的問題和設計方法的總結。我今天來夯實一下基礎,先來聊聊面向對象設計的基本方面。

我們一定要清楚面向對象的基本要素:封裝繼承多態

  • 封裝  的目的是隱藏事務內部的實現細節,以便提高安全性和簡化編程。封裝提供了合理的邊界,避免外部調用者接觸到內部的細節。我們在日常開發中,因爲無意間暴露了細節導致的難纏 bug 太多了,比如在多線程環境暴露內部狀態,導致的併發修改問題。從另外一個角度看,封裝這種隱藏,也提供了簡化的界面,避免太多無意義的細節浪費調用者的精力。
  • 繼承  是代碼複用的基礎機制,類似於我們對於馬、白馬、黑馬的歸納總結。但要注意,繼承可以看作是非常緊耦合的一種關係,父類代碼修改,子類行爲也會變動。在實踐中,過度濫用繼承,可能會起到反效果。
  • 多態,你可能立即會想到重寫(override)和重載(overload)、向上轉型。簡單說,重寫是父子類中相同名字和參數的方法,不同的實現;重載則是相同名字的方法,但是不同的參數,本質上這些方法簽名是不一樣的,爲了更好說明,請參考下面的樣例代碼:
public int doSomething() {
    return 0;
}
// 輸入參數不同,意味着方法簽名不同,重載的體現
public int doSomething(List<String> strs) {
    return 0;
}
// return 類型不一樣,編譯不能通過
public short doSomething() {
    return 0;
}

這裏你可以思考一個小問題,方法名稱和參數一致,但是返回值不同,這種情況在Java 代碼中算是有效的重載嗎? 答案是不是的,編譯都會出錯的。

 

面向對象開發的5個基本原則

進行面向對象編程,掌握基本的設計原則是必須的,我今天介紹最通用的面向對象開發的5個基本原則,也就是所謂的 S.O.L.I.D 原則。

  •     單一職責(Single Responsibility),類或者對象最好是隻有單一職責,在程序設計中如果發現某個類承擔着多種義務,可以考慮進行拆分。
  •     開閉原則(Open-Close, Open for extension, close for modification),設計要對擴展開放,對修改關閉。換句話說,程序設計應保證平滑的擴展性,儘量避免因爲新增同類功能而修改已有實現,這樣可以少產出些迴歸(regression)問題。
  •     里氏替換(Liskov Substitution),這是面向對象的基本要素之一,進行繼承關係抽象時,凡是可以用父類或者基類的地方,都可以用子類替換
  •     接口分離(Interface Segregation),我們在進行類和接口設計時,如果在一個接口裏定義了太多方法,其子類很可能面臨兩難,就是隻有部分方法對它是有意義的,這就破壞了程序的內聚性。對於這種情況,可以通過拆分成功能單一的多個接口,將行爲進行解耦。在未來維護中,如果某個接口設計有變,不會對使用其他接口的子類構成影響。
  •     依賴反轉(Dependency Inversion),實體應該依賴於抽象而不是實現。也就是說高層次模塊,不應該依賴於低層次模塊,而是應該基於抽象。實踐這一原則是保證產品代碼之間適當耦合度的法寶。

 

OOP 原則實踐中的取捨

值得注意的是,現代語言的發展,很多時候並不是完全遵守前面的原則的,比如,Java 10 中引入了本地方法類型推斷和 var 類型。按照,里氏替換原則,我們通常這樣定義變量:

List<String> list = new ArrayList<>();

如果使用 var 類型,可以簡化爲

var list = new ArrayList<String>();

但是,list 實際會被推斷爲“ArrayList < String >”

ArrayList<String> list = new ArrayList<String>();

理論上,這種語法上的便利,其實是增強了程序對實現的依賴,但是微小的類型泄漏卻帶來了書寫的便利和代碼可讀性的提高,所以,實踐中我們還是要按照得失利弊進行選擇,而不是一味得遵循原則。

 

OOP 原則在面試題目中的分析

我在以往面試中發現,即使是有多年編程經驗的工程師,也還沒有真正掌握面向對象設計的基本的原則,如開關原則(Open-Close)。看看下面這段代碼,改編自朋友圈盛傳的某偉大公司產品代碼,你覺得可以利用面向對象設計原則如何改進?

public class VIPCenter {
  void serviceVIP(T extend User user>) {
     if (user instanceof SlumDogVIP) {
        // 窮 X VIP,活動搶的那種
        // do somthing
      } else if(user instanceof RealVIP) {
        // do somthing
      }
      // ...
  }

這段代碼的一個問題是,業務邏輯集中在一起,當出現新的用戶類型時,比如,大數據發現了我們是肥羊,需要去收穫一下, 這就需要直接去修改服務方法代碼實現,這可能會意外影響不相關的某個用戶類型邏輯。

利用開關原則,我們可以嘗試改造爲下面的代碼:

public class VIPCenter {
   private Map<User.TYPE, ServiceProvider> providers;
   void serviceVIP(T extend User user) {
      providers.get(user.getType()).service(user);
   }
 }
 interface ServiceProvider{
   void service(T extend User user) ;
 }
 class SlumDogVIPServiceProvider implements ServiceProvider{
   void service(T extend User user){
     // do somthing
   }
 }
 class RealVIPServiceProvider implements ServiceProvider{
   void service(T extend User user) {
     // do something
   }
 } 

上面的示例,將不同對象分類的服務方法進行抽象,把業務邏輯的緊耦合關係拆開,實現代碼的隔離保證了方便的擴展。

今天我對 Java 面向對象技術進行了梳理,對比了抽象類和接口,分析了 Java 語言在接口層面的演進和相應程序設計實現,最後回顧並實踐了面向對象設計的基本原則,希望對你有所幫助。
 

發佈了96 篇原創文章 · 獲贊 19 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章