Java——物件導向程式設計的一些總結

琚ICIW發表於2020-10-31

1. 程式導向與物件導向

1.1 程式導向程式設計

在傳統的程式導向程式設計中,演算法 + 資料結構 = 程式,首先要決定如何運算元據,而後在決定以何種形式組織資料,以便於資料的操作。

對於一些規模較小的問題,將其分解為過程的設計方式比較理想。

但如果所需解決的問題規模較大,程式導向程式設計就顯得比較複雜,可能需要將其分解為很多個過程,每個過程可能都需要對一組資料進行操作

1.2 物件導向程式設計

物件導向程式設計(簡稱OPP,Object Oriented Programming)是當今主流的程式設計規範,十分適合一些大專案的開發。在程式導向程式設計中需要2000個程式才能完成的功能,採用物件導向的設計風格,可能只需要100個類,平均每個類包含20個方法,管理100個類顯然比管理2000個過程簡單的多。

Java是一種完全物件導向的語言,在程式設計的過程中,我們不必關心物件的具體實現,而是聚焦於使用者的需求。

物件導向程式設計有三大特徵——封裝、繼承、多型。

2. 物件導向的三大特徵

2.1 封裝

2.1.1 什麼是封裝

很多人喜歡把封裝比喻成一個多功能的黑盒子,我們可以使用黑盒子提供給我們的功能,但卻不知道這些功能在黑盒子裡是如何具體實現的。封裝給資料的安全性提供了很好的保障,只要你將資料設定成私有的,別人就不能直接訪問它。

封裝的關鍵在於不能讓類中的方法直接地訪問其他類的例項域(物件中的資料稱為例項域)。

2.1.2 Java是如何實現封裝的

Java通過來實現這一特性,利用private關鍵字可以將變數或方法設定成私有的(事實上我們應該儘可能的將類內部的資料定義成私有的),一旦某變數或方法被宣告為private,就意味著在其他的類中將無法直接訪問此變數或方法。

2.1.3 如何獲取私有物件的資訊

當我們在一個類中將某一變數設定成私有的之後,如果在其他方法中需要用到這一變數該怎麼辦?常用的做法是在該類中新增get()set()方法,並讓他們是公有(public)的,這樣就可以對私有的物件進行間接的操作了。

2.1.4 為什麼要用公有方法間接訪問私有變數

可能有人會覺得這麼做不是多此一舉嗎?為什麼要將變數設定成私有的呢?直接讓它是公有的不就行了,實則不然。

  1. 首先,這麼做可以便於改變內部實現

    例如原本有一個類它有一個name變數和一個toString()方法:

   private String name = "";
   public String toString() {
   	return name;
   }

然而之後我們想要把姓名分別用姓和名錶示,即需要兩個變數firstNamelastName,只需要在類內部稍作修改即可。

	private String firstName = "";
	private String lastName = "";
    public String toString() {
    	return firstName + lastName;
    }

顯然如果是將name設定為公有的,並在其他類中呼叫了此物件,就無法實現如此便捷的修改。

  1. 其次,更改器方法可以執行錯誤檢查

    當涉及到需要修改私有變數的值時,應使用方法更改器(set()方法),這樣做的好處是我們可以在方法中就對此操作進行檢查,如修改年齡時首先檢查年齡是否小於0。

  2. 最後,這樣可以保證安全性

    在編寫get()方法時,應該注意不要直接返回可變物件。如果直接返回可變物件,在外部類中就可以對其直接做出任何修改,這將是不可控的,我們不能對其安全性做出任何保證,同時排查錯誤也將變得更加的困難,將物件變數設定成公有的亦然。

    因此我們最好只返回可變物件的某些資訊,而不要直接將物件作為返回值(對8大基本型別則無此限制)。

2.2 繼承

繼承使得人們可以基於已經存在的類構造一個新類,給程式設計帶來了極大的可擴充套件性。“子承父類”,子類可以通過繼承獲得父類的方法和域,在此基礎上子類可以新增一些新的方法和域,或者重新定義(覆蓋,override)從父類中繼承的方法。

2.2.1Java中的繼承

Java中用關鍵字extends表示繼承,如:

public class Student extends Person {
	...
}

表示Student類繼承了Person類,它將擁有Person類的所有方法和域。

2.2.2 覆蓋(override)

如果想在子類中重新定義父類中的某個方法,可以利用覆蓋來實現,如重新定義Person類中的toString()方法:

public class Person {
	private String name = "";

	public String toString() {
		return name;
	}
}

public class Student extends Person {
	private int studentNumber = 0;

	public String toString() {
		return super.toString() + studentNumber;
	}
}

當我們在子類中覆蓋了父類的toString()方法之後,再用子類的物件呼叫此方法,呼叫的就將是子類中的toString()方法。

2.2.3 什麼時候該使用繼承

“is-a”是判斷是否應該設計為繼承關係的簡單原則,它表明子類的每個物件也是父類的物件,即被包含的關係。

例如,動物和獅子,動物是個更大的範圍,獅子屬於動物的一種。在程式設計中我們就可以將動物定義成父類,讓獅子是繼承它,成為它的子類。

2.2.4 介面(interface)

Java不支援多繼承,而是通過介面來實現多繼承的功能。Java的類只能有一個父類,即只能繼承一個類,但它可以實現(implements)一個或多個介面。

2.2.5 為什麼要用介面代替多繼承

C++具有多重繼承特性,但隨之也帶來了一些注入虛基類、控制規則等複雜特性。Java的設計者選擇了不支援多繼承,其主要原因就是多繼承會讓語言變得十分複雜。

實際上,介面可以提供多重繼承的大多數好處,同時還能避免多繼承的複雜性。

2.3 多型

多型可以理解為同一行為具有不同的表現形式。即同一介面,因具體的例項不同,介面的行為也有所不同。

2.3.1 物件變數是多型的

在Java中,物件變數是多型的,父類變數除了可以引用它自身的物件之外,還可以引用繼承了它的任何一個子類的物件。例如

	Student student = new Student();
	Person person = student; // OK

在這裡一個Person型別的變數引用了一個Student的物件,這是合法的,因為Student類繼承了Person類,但反之則不行:

	Student student1 = new Person(); // error

這就像是前面繼承中所說的動物和獅子,我們可以說獅子是一種動物,但顯然說動物是一種獅子是不合理的。

2.3.2 方法呼叫是多型的

再來看下面的程式碼:

	Person person = new Person();
	person.toString();

這裡就會有疑問了,我用Person型別的變數引用了一個Student的物件,又呼叫了它的toString()方法,那麼此時呼叫的是Person類的toString()方法還是Student類的toString()方法呢?

答案是Student類的toString()方法,即Java的方法呼叫是多型的,同一個Person變數,如果它引用了Person類的物件,那麼它呼叫的將是Person類的toString()方法,而如果它引用了Student類的物件,那麼呼叫的就將會是Student類中的toString()方法。

Java方法呼叫的多型性的實現,依賴於動態繫結

2.3.3 動態繫結

當程式執行,並且採用動態繫結呼叫方法時,虛擬機器會呼叫與變數所引用物件的實際型別最匹配的那個類的方法。

就如上節的例子所示,變數person所引用的物件的實際型別是Student類,當執行person.toString()時,會首先在Student類中搜尋,如果Student類中定義了此方法,則直接呼叫;如果沒有,就到它的父類中搜尋,以此類推。

每次呼叫方法都要進行搜尋的話,時間開銷會非常大。因此虛擬機器會預先為每個類建立一個方法表,其中列出了所有方法的簽名和實際呼叫方法。這樣一來,在真正呼叫方法的時候,虛擬機器直接查表即可。

2.2.4 靜態繫結

如果是privatestaticfinal方法或者構造器,編譯器可以準確的知道應該呼叫哪個方法,我們將這種呼叫方式稱為靜態繫結

2.2.5 多型的實現方法

方法一:重寫(上述例子就是利用了重寫)

方法二:抽象類和抽象方法

方法三:介面

相關文章