1. 程式人生 > >CH04 面向物件(上)

CH04 面向物件(上)

4.1 面向物件

Java語言是純粹的面向物件的語言。

4.1.1 結構化程式簡介

  • 結構化程式設計主張按功能分析系統需求。主要原則可概括為自頂向下、逐步求精、模組化等。
  • 結構化設計、結構化分析、結構化程式設計方法來實現系統。
  • 也被稱為面向功能的程式設計方法
  • 結構化程式設計方式的侷限性:
    • 設計不夠直觀,與人類思維方式不一致。
    • 適應性差,可擴充套件性不強。

4.1.2 面向物件程式設計簡介

  • 面向物件程式設計方法的基本思想是使用類、物件、繼承、封裝、訊息等基本概念進行程式設計。
  • 採用面向物件方式開發的軟甲系統,其最小的程式單元是類,這些類可以生成系統中的多個物件,而這些物件則直接映像成客觀世界的各種事物。
    • 成員變數(狀態資料)+ 方法(行為)= 類定義

4.1.3 面向物件的基本特徵

  • 面向物件方法具有三個基本特徵:封裝Encapsulation、繼承Inheritance、多型polymorphism
    • 封裝:將物件的實現細節隱藏起來,然後通過一些公用方法來暴露該物件的功能;
    • 繼承:是面向物件實現軟體複用的重要手段,當子類繼承父類後,子類作為一種特殊的父類,將直接獲得父類的屬性和方法
    • 多型:是子類物件可以直接賦給父類變數,但執行時依然表現出子類的行為特徵,意味著同一個型別的物件在執行同一個方法時,可以表現出多種行為特徵。
  • 面向物件程式設計的幾個概念
    • 物件是面向物件方法中最基本的概念,基本特點有:標識唯一性、分類性、多型性、封裝性、模組獨立性好。
    • 類是具有共同屬性、共同方法的一類事物。類是物件的抽象;物件則是類的例項。而類是整個軟體系統中最小的程式單元。
    • 物件間的相互合作需要一個機制協助進行,這樣的機制稱為“訊息”。訊息是一個例項與另一個例項之間相互通訊的機制。

4.2 類和物件

4.2.1 定義類

  • A class is the template or blueprint from which objects are made.

Java語言裡定義類的語法如下

[修飾符] class ClassName
{
    field1
    field2
    .
.. constructor1 constructor2 ... method1 method2 ... }
  • 修飾符可以是public、final、abstract或省略
  • 一個類可以包含三種常見成員:成員變數、構造器、方法,三種成員都可以定義0個或多個。
    • 成員變數用於定義該類或該類的例項所包含的狀態資料
    • 構造器用於構造該類的例項,如果程式設計師沒有為一個類編寫構造器,則系統會為該類提供一個預設的構造器。
    • 方法用於定義該類或該類的例項的行為特徵或者功能實現。
  • 類的各成員之間的定義順序沒有影響,各成員之間可以相互呼叫,static修飾的成員不能訪問沒有static修飾的成員。

定義成員變數的語法如下

[修飾符] 型別 成員變數名 [=預設值];

  • 修飾符:可以省略,或者是public,protected,private,static,final,其中public、protected、private三個最多隻能出現其中之一,可以和static、final組合起來修飾成員變數。
  • 型別:可以是Java語言的任何資料型別

定義方法的語法如下

[修飾符] 方法返回值型別 方法名(形參列表)
{
    // 由零條到多條可執行性語句組成的方法體
}
  • 修飾符:可以省略,也可以是public、protect、private、static、final、abstract,其中public、protected、private三個最多隻能出現其中之一;abstract和final最多隻能出現其中之一,他們可以和static組合起來修飾方法。
  • 方法體內多條可執行語句之間有嚴格的執行先後順序。
  • static修飾的成員表明它屬於類本身,而不屬於該類的單個例項,因此static修飾的成員稱為類變數和類方法,不使用static修飾的成員稱為例項變數、例項方法。
    • static的真正作用是用於區分成員變數、方法、內部類、初始化塊這四種成員到底屬於類本身還是屬於例項。

定義構造器的語法如下:

  • 構造器是一個特殊的方法
[修飾符] 構造器名(形參列表)
{
    // 由零條到多條可執行性語句組成的構造器執行體
}
  • 修飾符:可以省略,也可以是public、protected、private其中之一。
  • 構造器名:構造器名必須和類名相同
  • 構造器既不能定義返回值型別,也不能使用void宣告構造器沒有返回值。
    例:Employee.java
import java.time.LocalDate;
public class Employee {
	private String name;
	private double salary;
	private LocalDate hireDay;
	public Employee(String name, double salary, int year, int month, int day) {
		this.name = name;
		this.salary = salary;
		hireDay = LocalDate.of(year, month, day);
	}
	public String getName()
	{
		return name;
	}
	
	public double getSalary()
	{
		return salary;
	}
	
	public LocalDate getHireDay()
	{
		return hireDay;
	}
	public void raiseSalary(double byPercent)
	{
		double raise  = salary * byPercent / 100;
		salary += raise;
	}
}

4.2.2 物件的建立和使用

  • 建立一個物件的步驟
    • 1.分配物件空間並將物件成員變數初始化
    • 2.執行屬性值的顯式初始化
    • 3.執行構造方法
    • 4.返回物件的地址給相關的變數
Employee stuff = new Employee();
  • 如果訪問許可權允許,類裡定義的方法和成員變數都可以通過類或例項來呼叫。類或例項訪問成員變數的語法是:類.類變數|方法,或者例項.例項變數|方法。
  • static修飾的方法和成員變數,既可以通過類來呼叫,也可以通過例項來呼叫;沒有使用static修飾的普通方法和成員變數,只可以通過例項來呼叫。

4.2.3 物件、引用和堆疊

  • 在前面建立Employee類的一個例項stuff時,實際產生了兩個東西:一個是stuff變數,一個是Employee物件。
  • Employee型別的變數實際上是一個引用,存放在棧記憶體裡,指向實際的Employee物件;而真正的Employee物件則存放在堆(heap)記憶體中。
  • 當一個物件被建立成功以後,這個物件將儲存在堆記憶體中,Java程式不允許直接訪問堆記憶體中的物件,只能通過該物件的引用操作該物件。也就是說,不管是陣列還是物件,都只能通過引用來訪問它們。
  • Java虛擬機器的記憶體分為三個區域:棧、堆和方法區,方法區本質也是堆。
  • 棧的特點:
    • 棧描述的是方法執行的記憶體模型,每個方法被呼叫都會建立一個棧幀儲存區域性變數、運算元、方法出口等。
    • JVM為每個執行緒建立一個棧,用於存放該執行緒執行方法的資訊(實參、區域性變數等)。
    • 棧屬於執行緒私有,不能實現執行緒間的共享。
    • 棧的儲存特點是:先進後出,後進先出
    • 棧是由系統自動分配、速度快,棧是一個連續的記憶體空間。
  • 堆的特點:
    • 堆用於儲存建立好的物件和陣列(陣列也是物件)
    • JVM只要一個堆,被所有執行緒共享。
    • 堆是一個不連續的記憶體空間,分配靈活,速度慢
  • 方法區(又叫靜態區)的特點:
    • JVM只有一個方法區,被所有執行緒共享
    • 方法區實際也是堆,只用於儲存類、常量相關的資訊
    • 用來存放程式中永遠是不變或唯一的內容(類資訊、靜態變數、字串常量等)

4.2.4 物件的this引用

  • Java中的this關鍵字總是指向呼叫該方法的物件。根據this出現的位置不同,this作為物件的預設引用有兩種情形:
    • 構造器中引用該構造器正在初始化的物件
    • 在方法中引用呼叫該方法的物件,讓類中的一個方法,訪問該類裡的另一個方法或例項變數。
      例:Dog.java
public class Dog
{
	// 定義一個jump()方法
	public void jump()
	{
		System.out.println("正在執行jump方法");
	}
	// 定義一個run()方法,run()方法需要藉助jump()方法
	public void run()
	{
		this.jump();
		System.out.println("正在執行run方法");
	}
}
  • 注意static修飾的方法中不能使用this引用
  • 在程式設計時儘量不要使用物件去呼叫static修飾的成員變數、方法(並不是不允許),而是應用使用類去呼叫static修飾的成員變數、方法。

4.3 方法詳解

  • 方法是類或物件的行為特徵的抽象。方法在邏輯上要麼屬於類,要麼屬於物件。

4.3.1 方法的所屬性

  • Java語言裡方法的所屬性主要體現在如下幾個方面:
    • 方法不能獨立定義,方法只能在類體裡定義
    • 從邏輯意義上看,方法要麼屬於該類本身,要麼屬於該類的一個物件。
    • 永遠不能獨立執行方法,執行方法必須使用類或物件作為呼叫者
  • 類方法static methods的用途:
    • When a method doesn’t need to access the object state because all needed parameters are supplied as explicit parameters.
    • When a method only needs to access static fields of the class.
  • Main方法是一個static method. The main method doesn’t operate on any objects.

4.3.2 方法的引數傳遞機制

  • 如果宣告方式時呼叫了形參,則呼叫方法時必須給這些形參指定引數值。呼叫方法時實際傳給形參的引數值也稱為實參。
  • Java裡方法的引數傳遞機制是值傳遞。所謂值傳遞,即將實際引數值的副本(複製品)傳入方法內,而引數本身不會受到影響。
  • 基本型別的引數傳遞:例:PrimitiveTransferTest.java
public class PrimitiveTransferTest {
	public static void swap(int a, int b)
	{
		// 下面三行程式碼實現a、b變數的值交換
		// 定義一個臨時變數來儲存a變數的值
		int tmp = a;
		// 把b的值賦給a
		a = b;
		// 把臨時變數tmp的值賦給a
		b = tmp;
		System.out.println("swap方法裡,a的值是"
				+ a + ": b的值是" + b);
	}
	public static void main(String[] args) {
		int a = 6;
		int b = 9;
		swap(a,b);
		System.out.println("交換結束後,變數a的值是"
				+ a + ": 變數b的值是" + b);
	}
}

執行結果:

swap方法裡,a的值是9: b的值是6
交換結束後,變數a的值是6: 變數b的值是9
  • 引用型別的引數傳遞,例:ReferenceTranferTest.java
class DataWrap
{
	int a;
	int b;
}
public class ReferenceTransferTest {
	public static void swap(DataWrap dw)
	{
		// 下面三行程式碼實現dw的a、b兩個成員變數的值交換
		// 定義一個臨時變數來儲存dw物件的a成員變數的值
		int tmp = dw.a;
		// 把dw物件的b成員變數的值賦給a成員變數
		dw.a = dw.b;
		// 把臨時變數tmp的值賦給dw物件的b成員變數
		dw.b = tmp;
		System.out.println("swap方法裡, a成員變數的值是"
				+ dw.a + "; b成員變數的值是" + dw.b);
	}
	public static void main(String[] args) {
		DataWrap dw = new DataWrap();
		dw.a = 6;
		dw.b = 9;
		swap(dw);
		System.out.println("交換結束後,a成員變數的值是"
				+ dw.a + "; b成員變數的值是" + dw.b);
	}
}

執行結果:

swap方法裡, a成員變數的值是9; b成員變數的值是6
交換結束後,a成員變數的值是9; b成員變數的值是6
  • 程式從main()入口進入,定義一個dw引用變數指向DataWrap物件(此時,dw是在main棧區,而DataWrap物件在堆記憶體中)。main()方法呼叫swap()方法,將dw的副本作為實參傳入swap(),此時系統為swap()方法建立swap棧區,swap()方法的dw形參也儲存了DataWrap物件的地址。實際操作的是堆記憶體中的DataWrap物件,swap()方法中交換dw引數所引用DataWrap物件的a、b兩個成員變數的值,dw變數引用的也是同樣的DataWrap物件,自然其a、b成員變數是發生了變化的。
  • 注意main()方法中的dw和swap()方法中的dw是兩個變數(只是值傳遞)。
  • Here is a summary of what you can and cannot do with method parameters in Java:
    • A method cannot modify a parameter of a primitive type(that is, numbers or boolean values).
    • A method can change the state of an object parameter.
    • A method cannot make an object parameter refer to a new object.

4.3.3 形參個數可變的方法

  • 在定義方法時,在最後一個形參的型別後增加三點(…),則表明該形參可以接受多個引數值,多個引數值被當成陣列傳入。
public class Varargs {
	// 定義了形參個數可變的方法
	public static void test(int a, String...books)
	{
		// books被當做陣列處理
		for (String tmp:books)
		{
			System.out.println(tmp);
		}
		// 輸出整數變數a的值
		System.out.println(a);
	}
	public static void main(String[] args) {
		// 呼叫test方法
		test(5,"Core Java", "Effective Java");
	}
}

執行結果

Core Java
Effective Java
5
  • 個數可變的形參只能處於形參列表的最後,一個方法中最多隻能包含一個個數可變的形參。
  • 個數可變的形參本質就是一個數組型別的形參。

4.3.4 遞迴方法

  • 一個方法體內呼叫它自身,稱為方法遞迴。
  • 例:已知f(0) = 1, f(1) = 4, f(n+2) = 2*f(n+1) + f(n),其中n是大於零的整數,求f(10)的值。
public class Recursive {
	public static int fn(int n)
	{
		if (n == 0)
		{
			return 1;
		}
		else if (n == 1)
		{
			return 4;
		}
		else 
		{
			return 2* fn(n-1) + fn(n-2);// 方法中呼叫自身
		}
	}
	public static void main(String[] args) {
		// 輸出fn(10)的結果
		System.out.println(fn(10));
	}
}

4.3.5 方法過載

  • 如果同一個類中包含了兩個或兩個以上方法的方法名相同,但形參列表不同,則被稱為方法過載。
  • 在Java程式中確定一個方法需要三個要素:
    • 呼叫者,也就是方法的所屬者,可以是類或者物件。
    • 方法名,方法的標識。
    • 形參列表,當呼叫方法時,系統會根據傳入的實參列表匹配。
  • 例:Overload.java
public class Overload {
	// 下面定義了兩個test()方法,但方法的形參列表不同
	// 系統可以區分這兩種方法,這被稱為方法過載
	public void test()
	{
		System.out.println("無引數");
	}
	public void test(String msg)
	{
		System.out.println("過載的test方法 " + msg);
	}
	public static void main(String[] args) {
		Overload o1 = new Overload();
		// 呼叫test()時沒有傳入引數,系統會呼叫上面沒有引數的test()方法
		o1.test();
		// 呼叫test()時傳入一個字串引數
		o1.test("hello");
	}
}

4.4 成員變數和區域性變數

  • Java中,根據定義變數的位置不同,可以將變數分為兩大類:成員變數和區域性變數。

4.4.1 成員變數和區域性變數

  • 成員變數指的是在類裡定義的變數,包括例項變數(沒有static修飾符)和類變數(有static修飾符)
  • 區域性變數指的是在方法裡定義的變數,包括形參(方法簽名中定義的變數),方法區域性變數(方法內定義),程式碼塊區域性變數(在程式碼塊中定義)

成員變數

  • 類變數:類變數從該類的準備階段開始存在,直到系統完全銷燬這個類,類變數的作用域與這個類的生存範圍相同。只要類存在,程式就可以訪問該類的類變數。
  • 例項變數:從該類的例項被建立起開始存在,直到系統完全銷燬這個例項。例項變數的作用域與對應的生存範圍完全相同。只要例項存在,程式就可以訪問該類的類變數。
  • 同一個類裡,不能定義兩個同名的成員變數。
    - 例:PersonTest.java
class Person
{
	// 定義一個例項變數
	public String name;
	// 定義一個類變數
	public static int eyeNum; 
}
public class PersonTest {
	public static void main(String[] args) {
		// 第一次主動使用Person類,該類自動初始化,則eyeNum變數開始起作用,輸出0
		System.out.println("Person的eyeNum類變數值:"
				+ Person.eyeNum);
		// 建立Person物件
		Person p = new Person();
		// 通過Person物件的引用p來訪問Person物件name例項變數並通過例項訪問eyeNum類變數
		System.out.println("p變數的name變數值是:" + p.name
				+ " p物件的eyeNum變數值是:" + p.eyeNum);
		// 直接為name例項變數賦值
		p.name = "孫悟空";
		// 通過p訪問eyeNum類變數,依然是訪問Person的eyeNum類變數
		p.eyeNum = 2;
		// 再次通過Person物件來訪問name例項變數和eyeNum類變數
		System.out.println("p變數的name變數值是:" + p.name
				+ " p物件的eyeNum變數值是: " + p.eyeNum);
		// 前面通過p修改了Person的eyeNum,此處的Person.eyeNum將輸出2
		System.out.println("Person的eyeNum類變數值:" + Person.eyeNum);
		Person p2 = new Person();
		// p2訪問的eyeNum類變數依然引用的Person類的,因此依然輸出2
		System.out.println("p2物件的eyeNum類變數值:" + p2.eyeNum);
		}
}

執行結果:

Person的eyeNum類變數值:0
p變數的name變數值是:null p物件的eyeNum變數值是:0
p變數的name變數值是:孫悟空 p物件的eyeNum變數值是: 2
Person的eyeNum類變數值:2
p2物件的eyeNum類變數值:2
  • 注意不建議使用例項.類變數這種方式來訪問類變數(The static field should be accessed in a static way)

區域性變數

  • 形參:形參的作用域在整個方法內有效。
  • 方法區域性變數:在方法體內定義的區域性變數,它的作用域是從定義該變數的地方生效,到該方法介紹時失效。
  • 程式碼塊區域性變數:在程式碼塊中定義的區域性變數,這個區域性變數的作用域從定義該變數的地方生效,到該程式碼塊結束時失效。
  • 例:BlockTest.java
public class BlockTest {
	public static void main(String[] args) {
		{
			// 定義一個程式碼塊區域性變數a
			int a;
			// 下面程式碼將出現錯誤,因為a變數還未初始化
//			System.out.println("程式碼塊區域性變數a的值:" + a);
			// 為a賦初值
			a = 5;
			System.out.println("程式碼塊區域性變數a的值: " + a);
		}
		// 下面試圖訪問的a變數並不存在
//		System.out.print(a);
	}
}
  • 例:MethodLoclaVariable.java
public class MethodLocalVariable {
	public static void main(String[] args) {
		// 定義一個方法區域性變數
		int a;
		// 下面程式碼將出現錯誤,因為a變數還未初始化
//		System.out.println("方法區域性變數a的值: " + a);
		// 為a變數賦初值
		a = 5;
		System.out.println("方法區域性變數a的值:" + a);
	}
}
  • 形參的作用域是在整個方法體內有效,而且形參也無須顯式初始化,形參的初始化在呼叫該方法時由系統完成,形參的值由方法的呼叫者負責制定。
  • Java允許區域性變數和成員變數同名,如果方法裡的區域性變數和成員變數同名,區域性變數會覆蓋成員變數,如果需要在這個方法裡引用被覆蓋的成員變數,則可使用this(對於例項變數)或類名(對於類變數)作為呼叫者來限定訪問成員變數。
public class VariableOverrideTest {
	// 定義一個name例項變數
	private String name = "Abby";
	// 定義一個price類變數
	private static double price = 78.0;
	public static void main(String[] args)
	{
		// 方法裡的區域性變數,區域性變數覆蓋成員變數
		int price = 65;
		// 直接訪問price變數,將輸出price區域性變數的值:65
		System.out.println(price);
		// 使用類名作為price變數的限定,將輸出price類變數的值:78.0
		System.out.println(VariableOverrideTest.price);
		// 執行info方法
		new VariableOverrideTest().info();
	}
	private void info() {
		// 方法裡的區域性變數,區域性變數覆蓋成員變數
		String name = "八戒";
		// 直接訪問name變數,將輸出name區域性變數的值:"孫悟空"
		System.out.println(name);
		// 使用this來作為name變數的限定,將輸出name例項變數的值:“Abby"
		System.out.println(this.name);
	}
}

執行結果:

65
78.0
八戒
Abby

4.4.2 變數的使用規則

  • 如下幾種情形考慮使用成員變數:
    • 需要定義的變數是用於描述某個類或某個物件的固有資訊的,如人的身高、體重等
    • 在某個類中需要以一個變數來儲存該類或者例項執行時的狀態資訊
    • 某個資訊需要在某個類的多個方法之間進行共享

4.5 封裝

4.5.1 理解封裝

  • 封裝指的是將物件的狀態資訊隱藏在物件內部,不允許外部程式直接訪問物件內部資訊,而是通過該類所提供的方法來實現對內部資訊的操作和訪問。
  • 對一個類或物件實現良好的封裝,可以實現以下目的:
    • 隱藏類的實現細節
    • 讓使用者只能通過事先預定的方法來訪問資料,從而可以在該方法里加入控制邏輯,限制對成員變數的不合理訪問
    • 可進行資料檢查,從而有利於保證物件資訊的完整性。
    • 便於修改,提高程式碼的可維護性
  • 為了實現良好的封裝,you need to supply three items:
    • A private data field;
    • A public field accessor method;
    • A public field mutator method
  • 因此,封裝有兩個方面的含義:把該隱藏的隱藏起來,把該暴露的暴露出來,這需要Java的訪問控制符來實現。

4.5.2 使用訪問控制符

  • Java的訪問控制級別:

    private -> default -> protected -> public
  • private(當前類訪問許可權)
  • default(包訪問許可權)
  • protected(子類訪問許可權)
  • public(公共訪問許可權)
    • 訪問控制級別表
訪問控制符 private default protected public
同一個類中
同一個包中
子類中
全域性範圍內
  • 對於外部類而言,只能有兩種訪問控制級別:public和預設,不能用private和protected修飾(沒意義)
  • 例:定義一個Person類,Person.java
public class Person
{
    // 使用private修飾成員變數,將這些成員變數隱藏起來
    private String name;
    private int age;
    // 提供方法來操作name成員變數
    public void setName(String name)
    {
        // 執行合理性校驗,要求使用者名稱必須在2-6位之間
        if (name.length() > 6 || name.length() < 2)
        {
            System.out.println("您設定的人名不符合要求");
            return;
        }
        else
        {
            this.name = name;
        }
    }
    public String getName()
    {
        return this.name;
    }
    // 提供方法來操作age成員變數
    public void setAge(int age)
    {
        // 執行合理性校驗,要求使用者年齡必須在0-100之間
        if (age > 100 || age < 0)
        {
            System.out.println("您設定的年齡不合法");
            return;
        }
        else
        {
            this.age = age;
        }
        public int getAge()
        {
            return this.age;
        }
    }
}

例:建立Person物件,並嘗試操作和訪問該物件的age和name兩個例項變數,PersonTest.java

public class PersonTest
{
    public static void main(String[] args)
    {
        Person p = new Person();
        // 因為age成員變數被隱藏,所以下面語句將出現編譯錯誤
        // p.age = 1000;
        // 下面語句不會出現編譯錯誤,但執行時會提示”您設定的年齡不合法“
        p.setAge(1000);
        // 訪問p的age成員變數也必須通過對於的getter方法
        // 因為上面未成功設定p的age成員變數,故此處輸出0
        System.out.println("未能設定age成員變數時:"
            + p.getAge());
        // 成功修改p的age成員變數
        p.setAge(30);
        System.out.println("成功設定age成員變數後:"
            + p.getAge());
        // 不能直接操作p的name成員變數,只能通過其對應的setter方法
        p.setName("Abby");
        System.out.println("成功設定name成員變數後:"
            p.getName());
    }
}
  • 一個類就是一個小的模組,進行程式設計時應該儘量避免一個模組直接操作和訪問另一個模組的資料,模組設計追求高內聚、低耦合。

4.6 深入構造器

  • 構造器是一個特殊的方法