請用物件導向的思想,談一談面試的過程

沉默王二發表於2019-04-08

01、

很久沒有思考過什麼是物件導向這個問題了,就好像很久沒有吃過烤紅薯一樣,那股香味究竟是什麼,已經很難準確地形容出來了。腦海中只浮現出這樣一幅動圖:

在這裡插入圖片描述

前兩天,讀者秋秋問我:

二哥,究竟什麼是物件導向呢?還有,什麼是程式導向。今天去面試的時候,面試官讓我用物件導向的思想談一談這次面試的過程。

看到這個問題後,我思考了好一會兒,總覺得面試官的問法有點問題:為什麼要用物件導向的思想談一談面試的“過程”?

有點矛盾,有沒有?先不管這麼多了,且來看看什麼是物件導向吧。

一開始的時候,並沒有物件導向,只有程式導向的概念。我們回到秋秋面試的話題上,把面試前(可以降低需求的複雜性)的過程簡單地拆解一下。

  • 秋秋投遞簡歷
  • 面試官收到秋秋的簡歷
  • 面試官通知秋秋面試

為了實現這 3 個步驟,我們定義 3 個方法,並依次呼叫:

  • qiuqiuDeliverResume();
  • interviewerReceiveResume();
  • interviewerNotifyQiuqiu();

但是,假如參加面試的不是秋秋,這 3 個方法就要重新定義了(莫抬槓),儘管步驟並沒有變。物件導向從另一個角度來解決這個問題,它把物件(對事物的一種抽象描述)作為程式的基本單元。

回到秋秋面試的例子,用物件導向的思想來實現,就需要先定義 2 個類(類是構建物件的藍圖,裡面包含若干的資料和操作這些資料的方法),分別是應聘者和麵試官。

應聘者可以投遞簡歷;面試官可以接收應聘者的簡歷和通知應聘者前來面試。然後再通過類建立兩個物件,分別是秋秋和他的面試官;物件建立成功後,就可以依次呼叫對應的方法完成上述的 3 個步驟。

物件導向(英語:Object Oriented,縮寫:OO)思想是一種試圖降低程式碼間的依賴,應對複雜性,從而解決程式碼重用的軟體設計思想——恰好解決了程式導向帶來的問題。

物件導向有很多重要的特性,比如說封裝、繼承和多型。這些概念又該怎麼理解呢?所謂一圖勝千言,我給你來一張有趣的、形象的。

請用物件導向的思想,談一談面試的過程

瞭解了物件導向的思想後,我們來通過具體的程式碼完成秋秋面試前的 3 個步驟。並對類和物件的相關知識點進行歸納和總結。

02、

先來細緻地看一下應聘者類——Candidate.java。

package com.cmower

class Candidate {
	private String name;
	
	public Candidate(String name) {
		this.name = name;
	}
	
	public void deliverResume() {
		System.out.println(getName() + "發簡歷");
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

}
複製程式碼

Candidate 包含了類的 4 個重要概念:

  • 成員變數(有時叫做域,有時叫做欄位,有時叫做屬性)name
  • 成員變數訪問器(有時叫做 getter/settergetName()setName()
  • 構造方法(有時叫做構造器)Candidate()
  • 普通方法 deliverResume()

Candidate 類雖然簡單,但卻大有學問。

1)為了保證包名的絕對唯一,Sun 公司建議將域名(絕對是獨一無二的)以逆序的形式作為包名——這也是為什麼包名經常以 orgcom 開頭的原因(是不是有一種豁然開朗的感覺)。我曾申請過一個域名,叫 cmower.com,所以我個人編寫的絕大多數程式碼都是在 com.cmower包下。

2)類的方法定義順序依次是:構造方法 > 公有(public)方法或保護(protected)方法 > 私有(private)方法 > getter/setter 方法。

構造方法是建立物件的必經之路,放在首位是必須的。如果只有系統預設的無參構造方法,可忽略。

公有方法是類的呼叫者和維護者最關心的方法,應該在比較靠前的位置展示;保護方法雖然只有子類關心,也可能是“模板設計模式”下的核心方法,所以也要靠前;私有方法只對本類可見,一般不需要特別關心,所以往後放;getter/setter 方法承載的資訊價值較低,所以放在類的最後面。

3)setter 方法中,引數名稱與成員變數名稱保持一致,採用 this.成員名 = 引數名 的形式。

4)成員變數不要用 public 修飾,儘量用 private 修飾;如果需要被子類繼承,可以用 protected 修飾。

在初學 Java 程式設計的時候,我經常產生一個疑惑:為什麼不使用 public 修飾成員變數呢?這樣做不是比 getter/setter 更方便嗎?

我最先想到的答案是這樣的:

請用物件導向的思想,談一談面試的過程

解釋:如果只有 private String name 而沒有 getter/setter 的話,Eclipse 會提示 The value of the field Candidate.name is not used 的警告。

當然了,這樣的答案過於牽強。那能不能來個靠譜點的答案呢?

能,為了體現封裝的思想:將資料與行為進行分離。封裝有什麼好處呢?

  • 隱藏類的實現細節;
  • 讓使用者只能通過事先定製好的方法(getter/setter)來訪問資料,可以方便地加入控制方法,限制對成員變數的不合理操作;
  • 便於修改,增強程式碼的維護性和健壯性;
  • 提高程式碼的安全性和規範性;
  • 使程式更加具備穩定性和可擴充性。

不過,我對這些嚴肅的詞彙和科學用語實在是提不起半點興致。那就再換一個答案吧。

套用《Java 開發實戰經典》中舉過的一個例子,我們增加一個應聘者年齡的共有成員變數 age。

class Candidate {
    public int age;
}
複製程式碼

然後在建立應聘者物件的時候,直接通過類成員變數賦值:new Candidate().age = -99; 這樣賦值是沒有任何問題的,但沒有實際的意義,年齡是不可能為負數的。為了防止出現這樣的錯誤,可以對它進行封裝,也就是私有化,然後在 setter 方法中對年齡進行判斷,程式碼如下:

class Candidate {
    private int age;

    public void setAge(int age) {
      if (age >= 0) {
        this.age = age;
      }
    }
}
複製程式碼

這個答案你覺得滿意嗎?我最開始看到這個答案的時候覺得很滿意。但看了《阿里巴巴 Java 開發手冊》後(詳情截圖如下),就覺得不滿意了。

請用物件導向的思想,談一談面試的過程

第一,類成員變數使用基本型別很容易造成NullPointException的錯誤;第二,在 getter/setter 增加業務邏輯的確很容易把實際的問題隱藏起來。

那,好的答案究竟是什麼呢?

如果設定成員變數為 public,那麼每個呼叫者都可以讀寫它,但如果以 private 配合 getter/setter 的形式訪問時,就可以達到“不準訪問”、“只讀訪問”、“讀寫訪問”以及“只寫訪問”的目的。因為不是每個成員變數都需要 getter/setter

5)每個類都至少會有一個構造方法。初學者可能會非常疑惑:我的那個類真的沒有構造方法啊!

如果在編寫一個類的時候沒有編寫構造方法,那麼系統就會提供一個無參的構造方法,就好像是這樣:

class Candidate {
	private String name;
	
	public Candidate() {
	}

}
複製程式碼

當執行 new Candidate() 的時候,成員變數 name 就會被初始化為 null。一般情況下,我們會為類設定它必須的構造方法,然後在建立物件的時候對成員變數進行賦值。

03、

再來粗略地看一下面試官類——Interviewer.java。

class Interviewer {
	private Candidate candidate;
	
	public Interviewer (Candidate candidate) {
		this.candidate = candidate;
	}
	
	public void receviveResume() {
		System.out.println("收到" + getCandidate().getName() + "簡歷");
	}

	public void notifyInterview() {
		System.out.println("通知" + getCandidate().getName() + "面試");
	}

	public Candidate getCandidate() {
		return candidate;
	}

	public void setCandidate(Candidate candidate) {
		this.candidate = candidate;
	}

}
複製程式碼

Interviewer 有一個成員變數 Candidate,一個構造方法,兩個共有方法,以及成員變數對應的 getter/setter

(這段程式碼存在一個嚴重的問題,你注意到了嗎?)

04、

然後,我們讓應聘者傳送簡歷,讓面試官接收簡歷併傳送通知。

Candidate qiuqiu = new Candidate("秋秋");
// 傳送簡歷
qiuqiu.deliverResume();

Interviewer interviewer = new Interviewer(qiuqiu);
// 面試官接收到簡歷
interviewer.receviveResume();
// 面試官通知應聘者來面試
interviewer.notifyInterview();
複製程式碼

在初學 Java 的很長一段時間裡,我總是搞不清楚什麼是“物件”,什麼是“引用”,差點因此放棄我的程式生涯。後來,在網上認識了一個大佬,人稱老王,是他挽救了我的程式生涯。

他解釋說。

Candidate qiuqiu = new Candidate("秋秋");可以拆分為兩行程式碼:

Candidate qiuqiu;
qiuqiu = new Candidate("秋秋");
複製程式碼

第一行程式碼 Candidate qiuqiu; 中的 qiuqiu 這時候可以稱作是物件變數,它暫時還沒有引用任何物件,嚴格意義上,它也不能稱為 null

第二行程式碼 qiuqiu = new Candidate("秋秋"); 可以拆分為兩個部分,= 號左側和 = 號右側。

右側的表示式 new Candidate("秋秋") 先執行,執行完後,會在堆上建立了一個 name 為“秋秋”的物件,型別為 Candidate,表示式 new Candidate("秋秋") 的值是新建立物件的引用。

然後再把這個引用通過 = 操作符賦值給左側的物件變數 qiuqiu,賦值後,qiuqiu就不再是物件變數了,應該稱為物件引用。

請用物件導向的思想,談一談面試的過程

看完老王的解釋,你會不會情不自禁地“哦,原來如此啊!”反正我當時頓悟的時候是這樣的。

前面提到,Interviewer 類的設計存在一個嚴重的問題,是什麼呢?

Candidate qiuqiu = new Candidate("秋秋");
Interviewer interviewer = new Interviewer(qiuqiu);

interviewer.getCandidate().setName("夏夏");
System.out.println(qiuqiu.getName());
複製程式碼

這段程式碼執行完後,你會發現秋秋變成了夏夏,應聘者的私有成員變數 name 竟然被改變了!問題的原因也很簡單,qiuqiu 和 interviewer.getCandidate() 引用了同一個物件。

請用物件導向的思想,談一談面試的過程

那怎麼解決呢?當 getter 需要返回一個可變物件的引用時,應該先進行克隆(clone)。以下展示了一個非常簡單的克隆方案。

class Interviewer {
	private Candidate candidate;
	
	public Interviewer (Candidate candidate) {
		this.candidate = candidate;
	}

	public Candidate getCandidate() {
		Candidate candidate = new Candidate(this.candidate.getName());
		return candidate;
	}

}
複製程式碼

05、

這篇文章花了 5 個多小時才寫完,此刻我的感覺只有一個字——餓,我要出去吃飯了。吃飯之前,我決定先買個烤紅薯吃,重溫一下那種久違的香。


在這裡插入圖片描述

相關文章