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,這里不再多介紹了,有興趣你可以參考:[https://www.oreilly.com/learning/java-8-functional-interfaces](https://www.oreilly.com/learning/java-8-functional-interfaces)。
還有一點可能讓人感到意外,嚴格說,**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 代碼中算是有效的重載嗎? 答案是不是的,編譯都會出錯的。
進行面向對象編程,掌握基本的設計原則是必須的,我今天介紹最通用的部分,也就是所謂的 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 ”
~~~
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 語言在接口層面的演進和相應程序設計實現,最后回顧并實踐了面向對象設計的基本原則,希望對你有所幫助。
## 一課一練
關于接口和抽象類的區別,你做到心中有數了嗎?給你布置一個思考題,思考一下自己的產品代碼,有沒有什么地方違反了基本設計原則?那些一改就崩的代碼,是否遵循了開關原則?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?