讓對象明白什麼是面向對象

前言

之所以寫下這篇文章,是因爲女朋友這學期要修Java,但在這之前只接觸過C語言,對於面向對象的概念一時難以理解,於是這裏寫一篇文章來講一講。我之前並沒有接觸過Java,原本只是打算講講OOP的一些概念的,不過後來還是打算開始學習一下Java,並且整理一個筆記系列,這篇文章就做個開頭吧。有關Java的內容基本上是基於對《On Java 8》《Java核心技術 卷一》這兩本書。
把這些內容發佈在公衆號上,希望也能對有相同需求的人提供幫助,但是需要注意,在我後續的文章裏,都假定讀者有一定C語言基礎。

抽象

抽象是計算機科學中一個非常重要的概念,它和具體正好相反。抽象是爲了降低複雜度。在文學作品中常常以具體事物來比喻抽象的東西:

一往情深深幾許?深山夕照深秋雨。

但是具體的事物往往是繁瑣的複雜的,在計算機科學中,通過抽象,能夠更好的解決問題。
首先我們在課堂上應該都聽過老師講解二進制時講過二進制的10與計算機內部的電路開關狀態對應,CPU中有非常非常多的電路。讓我們來看一下我們的計算機程序,不管是一個簡單的計算1+1還是一個類似Windows這樣的操作系統,它們在運行的時候,其實都是這些電路中電子在跑來跑去。CPU內部有數以億計的晶體管。
在一堆晶體管的基礎上,我們抽象出門電路,與門、或門、非門等等,不用知道在電路中電子怎麼運動的,也不需要知道輸入多少電壓對應二進制的哪一位,只要知道有什麼樣的輸入能得到什麼樣的輸出就行了。
在這之上,又可以抽象出更復雜的電路,觸發器、寄存器,我們只要知道怎麼操作會得到什麼結果,而不必知道怎麼得到的結果。
最初的程序,我們使用1和0組成的機器語言編寫,它能直接被計算機CPU識別運行。但是對於人類來說還是難以理解。
在機器語言的基礎上,產生了彙編語言。彙編語言是對機器語言的一種抽象,如下是一個實現兩數相加的彙編程序:

datas segment
      x dw 1234h
      y dw 5678h
      z dw ?
datas ends
codes segment
    assume cs:codes,ds:datas
start:
    mov ax,datas
    mov ds,ax
    mov ax,x
    add ax,y
    mov z,ax
    mov ah,4ch
    int 21h
codes ends
    end start

在彙編中,使用例如movadd這樣的指令,我們不用操心具體寫什麼樣的機器碼,彙編器會最終幫我們將代碼翻譯成機器語言。但在彙編中,我們還要操心例如將數據移動到某個CPU的寄存器中這樣的問題。
再對彙編進行抽象,我們發展出了高級語言,例如在Python中:

print(2+3)

這樣不僅能計算加法的結果還能顯示在屏幕上,我們編寫的代碼更解決人類的自然語言,不用去關心數據是不是要從內存放進CPU,怎麼實現了數據的相加,怎麼將數據顯示在屏幕中。
好了,現在我們有了一個高級語言,現在我們要寫一些程序大致是這樣:

函數
函數1
函數2
函數3
函數4
...

數據
數據1
數據2
數據3
數據4
...

這時候是一種面向過程的編程模式,隨着時代的發展,當我們的需求越來越多,一個程序可能會寫出非常非常多的函數,有一些函數要實現的功能類似,但又不得不多寫很多行代碼。
以一個工廠舉例,一家工廠,需要工人,生產線,原材料採購,市場銷售,技術研發,如果用面向過程的思想去解決,那麼這個程序員要考慮一家工廠從設計產品到購買材料到投入生產到銷售這一系列環節中所有的功能,所有的數據。
這個時候就需要面向對象的思想了。

面向對象

面向對象編程(Object-Oriented Programming OOP)是一種編程思維方式和編碼架構,面向對象是一種對現實世界理解和抽象的方法。

在工廠的例子中,使用面向對象的思路來解決,那麼我們會將問題分而治之,讓工人來負責生產,技術員來負責研發,銷售負責販賣產品,採購負責買材料。

以下摘錄自《On Java 8》:

  1. 萬物皆對象。你可以將對象想象成一種特殊的變量。它存儲數據,但可以在你對其“發出請求”時執行本身的操作。理論上講,你總是可以從要解決的問題身上抽象出概念性的組件,然後在程序中將其表示爲一個對象。
  2. 程序是一組對象,通過消息傳遞來告知彼此該做什麼。要請求調用一個對象的方法,你需要向該對象發送消息。
  3. 每個對象都有自己的存儲空間,可容納其他對象。或者說,通過封裝現有對象,可製作出新型對象。所以,儘管對象的概念非常簡單,但在程序中卻可達到任意高的複雜程度。
  4. 每個對象都有一種類型。根據語法,每個對象都是某個“類”的一個“實例”。其中,“類”(Class)是“類型”(Type)的同義詞。一個類最重要的特徵就是“能將什麼消息發給它?”。
  5. 同一類所有對象都能接收相同的消息。這實際是別有含義的一種說法,大家不久便能理解。由於類型爲“圓”(Circle)的一個對象也屬於類型爲“形狀”(Shape)的一個對象,所以一個圓完全能接收發送給"形狀”的消息。這意味着可讓程序代碼統一指揮“形狀”,令其自動控制所有符合“形狀”描述的對象,其中自然包括“圓”。這一特性稱爲對象的“可替換性”,是OOP最重要的概念之一。

Grady Booch 提供了對對象更簡潔的描述:一個對象具有自己的狀態,行爲和標識。這意味着對象有自己的內部數據(提供狀態)、方法 (產生行爲),並彼此區分(每個對象在內存中都有唯一的地址)。

在Java中使用class關鍵字定義類,使用例如Worker w1 = new Worker()來實例化類爲一個對象。

/*
這是一個類,它表示一種類型,例如工人,就是一類人,
它有name這個屬性(field),work這個方法(method)
*/
public class Worker {
    String name;
    public void word() {
    ...
    }
}
/*
w1是一個對象,對象是類的實例,比如工廠裏有100個工人,w1就是工人中的一個實例
w1這個對象的name屬性是張三
w1可以調用work方法
定義了類之後,每一個這個類的實例對象,都會有類定義的屬性與方法
*/
Worker w1 = new Worker();
w1.name = "張三";
w1.work()

封裝

面向對象編程將數據以及對數據的操作打包起來放到對象裏,外界無需知道程序內部實現的細節,只要通過類似xx.xx()的形式去調用,封裝可以隱藏起對象的屬性和實現細節。

我們可以把編程的側重領域劃分爲研發和應用。應用程序員調用研發程序員構建的基礎工具類來做快速開發。研發程序員開發一個工具類,該工具類僅嚮應用程序員公開必要的內容,並隱藏內部實現的細節。這樣可以有效地避免該工具類被錯誤的使用和更改,從而減少程序出錯的可能。彼此職責劃分清晰,相互協作。當應用程序員調用研發程序員開發的工具類時,雙方建立了關係。應用程序員通過使用現成的工具類組裝應用程序或者構建更大的工具庫。如果工具類的創建者將類的內部所有信息都公開給調用者,那麼有些使用規則就不容易被遵守。因爲前者無法保證後者是否會按照正確的規則來使用,甚至是改變該工具類。只有設定訪問控制,才能從根本上阻止這種情況的發生。

Java 有三個顯式關鍵字來設置類中的訪問權限:public(公開),private(私有)和protected(受保護)。這些訪問修飾符決定了誰能使用它們修飾的方法、變量或類。

  1. public(公開)表示任何人都可以訪問和使用該元素;
  2. private(私有)除了類本身和類內部的方法,外界無法直接訪問該元素。private 是類和調用者之間的屏障。任何試圖訪問私有成員的行爲都會報編譯時錯誤;
  3. protected(受保護)類似於 private,區別是子類(下一節就會引入繼承的概念)可以訪問 protected 的成員,但不能訪問 private 成員;
  4. default(默認)如果你不使用前面的三者,默認就是 default 訪問權限。default 被稱爲包訪問,因爲該權限下的資源可以被同一包(庫組件)中其他類的成員訪問。

使用訪問控制,我們讓使用我們編寫的類的程序員不要接觸我們不想讓他們訪問的部分,同時我們可以修改程序,優化我們的代碼而不影響應用程序員的使用。

public class HelloWorld {
    public static void main(String[] args)
    {
        System.out.println("Hello World");
    }
}

在上面這個簡單的helloworld程序中,我們使用了System.out.println()來打印文字到屏幕上,但是我們並不知道這個方法的實現細節,哪怕在版本更新中重寫了這個方法的實現,只要接口和功能沒變,我們的代碼就沒有問題。

繼承

隨着工廠不斷髮展,業務越做越多,出現了各種不同的職位分工,這些員工在很多方面有相似之處,但具體做的事情略有不同,那我們是不是要寫很多個員工類呢?豈不是會有很多重複的代碼嗎?

我們可以這樣做:先定義一個員工類,它擁有名字、年齡、薪水、工作時間等等公有的屬性與方法,然後有各種類型的員工繼承這個工人類,員工類可以稱爲父類,細分的工種稱子類。
子類會擁有父類所有的屬性與方法。父類做出改變,也會反映在子類上。

繼承

子類也可以添加自己獨有的屬性與方法,例如工人可能會有領取防護用品的方法

多態

在我們的例子中,我們通過員工類派生出各種類型的子類,有工人、銷售、研發等等,他們都繼承了父類的work()方法,可是,不同的職位做的工作會相同嗎?
答案顯然是否定的。
那麼我們工廠調用員工去工作,怎麼實現不同類型的員工做不同的事情呢?要寫上很多的類型判斷嗎?根據不同類型完成不同行爲?
看下面一個例子:

void toWork(Employee employee) {
    employee.work();
    // ...
}

使用toWork()方法,表面看上去只能使用Employee類,讓我們接着往下看:

Work work = new Work();
Technician tech = new Technician();
Salesman sale = new Salesman();
toWork(work);
toWork(tech);
toWork(sale);

可以看到傳入任何類型的員工都能得到執行。
可以看到我們沒有做任何的類型判斷,由於工人、技術員、銷售都是員工的子類,在toWork()中它們都能被當作員工類型,這種特性在Java中被稱爲向上轉型(upcasting)

發送消息給對象時,如果程序不知道接收的具體類型是什麼,但最終執行是正確的,這就是對象的“多態性”(Polymorphism)。面向對象的程序設計語言是通過“動態綁定”的方式來實現對象的多態性的。編譯器和運行時系統會負責對所有細節的控制;我們只需知道要做什麼,以及如何利用多態性來更好地設計程序。

結語

面向對象歸根結底是一種程序設計的思想,並不是有class關鍵字就是面向對象。通過這種設計思想,我們在應對複雜龐大的問題時,能寫出可讀性更高、複用性更強的程序。如果你去閱讀Linux內核的源代碼,會發現雖然其使用C語言編寫,但仍然有着面向對象的思想在其中。

掃碼關注公衆號:
公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章