JAVA學習筆記 08 - 繼承、封裝、多態

本文是Java基礎課程的第八課。是Java面向對象編程的核心部分,主要介紹Java中的繼承、裝、多態等特性,最後介紹Java中final關鍵字和static關鍵字的作用

一、繼承

1、繼承的概念

1.1、生活中的繼承

面向對象的方法論告訴我們,在認識世界的過程中,萬事萬物皆爲對象對象按狀態行爲可以歸類。而在現實世界中進行對象的歸類時,會發現類與類之間也經常存在包含與從屬關係

比如將筆記本電腦與臺式電腦分別歸類,而筆記本電腦和臺式電腦又都屬於電腦,圖示如下:
在這裏插入圖片描述
筆記本電腦和臺式電腦都具有電腦的一般特徵,但同時又具有各自不同的特徵;筆記本電腦、臺式電腦相對於電腦要更加具體,而電腦相對於筆記本電腦或臺式電腦要更加通用。此時,可以認爲筆記本電腦、臺式電腦繼承了電腦,電腦派生了筆記本電腦、臺式電腦。

再比如,兔子和羊屬於食草動物,獅子和豹屬於食肉動物,食草動物和食肉動物又同時屬於動物,圖示如下:
在這裏插入圖片描述
此時,可以認爲食草動物、食肉動物繼承了動物,動物派生了食草動物、食肉動物;兔子、羊繼承了食草動物,食草動物派生了兔子、羊;獅子、豹繼承了食肉動物,食肉動物派生了獅子、豹。

嚴格的繼承需要符合的關係是 is-a ,父類更通用,子類更具體。即筆記本電腦 is a 電腦,羊 is a 食草動物,食草動物 is a 動物。

1.2、Java中的繼承

Java是一種面向對象編程語言,其非常注重的一點便是讓開發人員在設計軟件系統時能夠運用面向對象的思想自然地描述現實生活中的問題域,事實上,使用Java編程時,也能夠描述現實生活中繼承關係,甚至可以說,繼承Java面向對象編程技術的一塊基石

Java中的繼承允許開發人員創建分等級層次的類。利用繼承機制,可以先創建一個具有共性一般類,根據該一般類再創建具有特殊性新類新類繼承一般類屬性行爲,並根據需要定製自己屬性行爲。通過繼承創建的新類稱爲 派生類(或子類),被繼承的具有共性的一般類稱爲 基類(或超類父類)。

繼承使派生類獲得了能夠直接使用基類屬性和行爲能力,也使得基類能夠在無需重新編寫代碼的情況下通過派生類進行功能擴展。繼承的過程,就是從一般到特殊的過程。類的繼承機制Java面向對象程序設計中的核心特徵,是實現軟件可重用性重要手段,是實現多態基礎

2、Java中繼承的實現

2.1、Java中繼承的語法

Java中聲明派生類繼承某基類的語法格式如下:

[修飾符] class 派生類名 extends 基類名 {
	// 派生類成員變量
	// 派生類成員方法
}

說明:

  • 如上,Java中使用extends關鍵字實現類的繼承。

下面是一個示例:
基類Animal類的源碼:

package com.codeke.java.test;

/**
 * 動物類
 */
public class Animal {

	// 屬性
	String type;    // 類型
	String breed;    // 品種
	String name;    // 名稱

	/**
	 * 構造函數
	 * @param type  類型
	 * @param breed 品種
	 * @param name  名稱
	 */
	public Animal(String type, String breed, String name) {
		this.type = type;
		this.breed = breed;
		this.name = name;
	}

	/**
	 * 自我介紹方法
	 */
	public void introduce() {
		System.out.printf("主人好,我是%s,我的品種是%s,我的名字叫%s。",
				this.type, this.breed, this.name);
	}
}

派生類Cat類的源碼:

package com.codeke.java.test;

/**
 * 貓類
 */
public class Cat extends Animal {
	/**
	 * 派生類構造函數
	 * @param breed 品種
	 * @param name  名稱
	 */
	public Cat(String breed, String name) {
		super("貓", breed, name);
	}
}

派生類Dog類的源碼:

package com.codeke.java.test;

/**
 * 狗類
 */
public class Dog extends Animal {
	/**
	 * 派生類構造函數
	 * @param breed 品種
	 * @param name  名稱
	 */
	public Dog(String breed, String name) {
		super("狗", breed, name);
	}
}

派生類Duck類的源碼:

package com.codeke.java.test;

/**
 * 鴨子類
 */
public class Duck extends Animal {
	/**
	 * 派生類構造函數
	 * @param breed 品種
	 * @param name  名稱
	 */
	public Duck(String breed, String name) {
		super("鴨子", breed, name);
	}
}

測試類PetShop類的源碼:

package com.codeke.java.test;

/**
 * 寵物店(測試類)
 */
public class PetShop {
	/**
	 * 用來測試的main方法
	 */
	public static void main(String[] args) {
		Cat cat = new Cat("波斯貓", "大花");		// 定義貓對象cat
		Dog dog = new Dog("牧羊犬","大黑");		// 定義狗對象dog
		Duck duck = new Duck("野鴨","大鴨");		// 定義鴨子對象duck
		cat.introduce();	// 貓調用自我介紹方法
		dog.introduce();	// 狗調用自我介紹方法
		duck.introduce();	// 鴨子調用自我介紹方法
	}
}

說明:

  • main方法中的貓類對象cat、狗類對象dog、鴨子類對象duck都可以調用introduce()方法,但Cat類、Dog類、Duck類中並未定義introduce()方法,這是因爲Cat類、Dog類、Duck類從父類Animal類中繼承introduce()方法。
  • 派生類不能繼承基類的構造方法,因爲基類的構造方法用來初始化基類對象,派生類需要自己的構造方法來初始化派生類自己的對象。
  • 派生類初始化時,會先初始化基類對象。如果基類沒有無參構造方法,需要在派生類構造方法中使用super關鍵字顯示調用基類擁有的某個構造方法
  • thissuper都是Java中的關鍵字,this可以引用當前類對象super可以引用基類對象。關於這兩個關鍵字將在後面的內容中展開介紹。

2.2、Java支持的繼承類型

Java對各種形式的繼承類型支持情況如下所示:
在這裏插入圖片描述
說明:

  • 需要注意的是:Java中的類不支持多繼承

3、this和super

3.1、this

this是Java中的關鍵字,this可以理解爲指向當前對象(正在執行方法的對象)本身的一個引用

Java中,this關鍵字只能沒有被static關鍵字修飾方法中使用。其主要的應用場景有下面幾種。

第一,作爲當前對象本身的引用直接使用。
第二,訪問當前對象本身的成員變量。當方法中有局部變量成員變量重名時,訪問成員變量需要使用this.成員變量名

下面是一個示例:
Person類的源碼:

package com.codeke.java.test;

/**
 * 人類
 */
public class Person {

	String name;    // 名稱
	int age;        // 年齡
	int sex;        // 性別( 1:男 0:女 )
	Person partner;    // 伴侶

	/**
	 * 構造方法
	 * @param name 姓名
	 * @param age  年齡
	 * @param sex  性別
	 */
	public Person(String name, int age, int sex) {
		this.name = name;
		this.age = age;
		this.sex = sex;
	}

	/**
	 * 墜入愛河的方法
	 * @param person 那個要一同墜入愛河的人
	 */
	public void fallInLove(Person person) {
		// 性別一樣不能fall in love
		if (this.sex == person.sex) {
			System.out.printf("%s和%s性別相同,無法fall in love.\n",
					this.name, person.name);
			return;
		}
		// 自己未滿18,不能fall in love
		if (this.age < 18) {
			System.out.printf("%s太小,無法fall in love.\n",
					this.name);
			return;
		}
		// 對方未滿18,不能fall in love
		if (person.age < 18) {
			System.out.printf("%s太小,無法fall in love.\n",
					person.name);
			return;
		}
		// 自己有對象,不能再fall in love
		if (this.partner != null) {
			System.out.printf("%s已經fall in love with %s,無法再fall in love with %s.\n",
					this.name, this.partner.name, person.name);
			return;
		}
		// 對方有對象,不能再fall in love
		if (person.partner != null) {
			System.out.printf("%s已經fall in love with %s,無法再fall in love with %s.\n",
					person.name, person.partner.name, this.name);
			return;
		}
		// 兩人fall in love
		this.partner = person;
		person.partner = this;
		// 打印
		System.out.printf("%s fall in love with %s.\n",
				this.name, person.name);
	}
}

Test類的源碼:

package com.codeke.java.test;

/**
 * 測試類
 */
public class Test {
	public static void main(String[] args) {
		// 實例化若干person
		Person person1 = new Person("宋江",18, 1);
		Person person2 = new Person("武松",19, 1);
		Person person3 = new Person("燕青",17, 1);
		Person person4 = new Person("扈三娘",16, 0);
		Person person5 = new Person("孫二孃",18, 0);
		// 開發 fall in love 吧
		person1.fallInLove(person3);
		person2.fallInLove(person4);
		person3.fallInLove(person5);
		person1.fallInLove(person5);
		person2.fallInLove(person5);
	}
}

說明:

  • 本例中多次使用this.成員變量名訪問當前對象成員變量
  • 本例中的代碼person.partner = this;,作用是將方法形參代表的person對象的伴侶賦值爲當前正在執行方法的對象,即將this作爲當前對象本身的引用來使用。

第三,調用當前對象本身的成員方法。作用與訪問成員變量類似。

下面是一個示例:
Person類修改後的源碼:

package com.codeke.java.test;

/**
 * 人類
 */
public class Person {

	// 成員變量部分和上例中一樣

	// 構造方法和上例中一樣

	/**
	 * 自我介紹的方法
	 */
	public void introduce() {
		System.out.printf("大家好,我是%s,我想談戀愛。\n", this.name);
	}

	/**
	 * 墜入愛河的方法
	 * @param person 那個要一同墜入愛河的人
	 */
	public void fallInLove(Person person) {
		// 先自我介紹下
		this.introduce();
		// 後面的代碼和上例中一樣
		...
	}
}

說明:

  • 本例中,爲Person類增加了自我介紹的方法introduce(),並在fallInLove(Person person)方法中使用this.introduce();introduce()方法進行了調用,即仍然是當前執行方法person對象調用introduce()方法

第四,調用本類中的其他構造方法,語法格式爲:

this([參數1, ..., 參數n]);

下面是一個示例:
Person類修改後的源碼:

package com.codeke.java.test;

/**
 * 人類
 */
public class Person {

	// 成員變量部分和上例中一樣

	/**
	 * 構造方法,調用該構造方法,age屬性會初始化爲18
	 * @param name 名稱
	 * @param sex 性別
	 */
	public Person(String name, int sex) {
		this.name = name;
		this.sex = sex;
		this.age = 18;
	}
	
	/**
	 * 構造方法
	 * @param name 姓名
	 * @param age  年齡
	 * @param sex  性別
	 */
	public Person(String name, int age, int sex) {
		this(name, sex);
		this.age = age;
	}
	
	// introduce() 方法和 fallInLove(Person person) 方法和上例中一樣 

}

說明:

  • 本例中,Person類中新增了構造方法Person(String name, int sex),而在另一個構造方法中使用this(name, sex);調用了新增的構造方法。
  • this([參數1, ..., 參數n]);語句必須位於其他構造方法中的第一行

3.2、super

super也是Java中的關鍵字,super可以理解爲是指向當前對象的基類對象的一個引用,而這個基類指的是離自己最近的一個基類

Java中,super關鍵字也只能沒有被static關鍵字修飾方法中使用。其主要的應用場景有下面幾種。

第一,訪問當前對象的基類對象成員變量。當方法中有基類成員變量其他變量重名時,訪問基類成員變量需要使用super.基類成員變量名
第二,訪問當前對象的基類對象成員方法。作用與訪問成員變量類似。
第三,調用基類構造方法,語法格式爲:

super([參數1, ..., 參數n]);

下面是一個示例:
Person類修改後的源碼:

package com.codeke.java.test;

/**
 * 人類
 */
public class Person {

	String name;    // 名稱
	int age;        // 年齡
	int sex;        // 性別( 1:男 0:女 )

	/**
	 * 構造方法
	 * @param name 姓名
	 * @param age  年齡
	 * @param sex  性別
	 */
	public Person(String name, int age, int sex) {
		this.name = name;
		this.sex = sex;
		this.age = age;
	}

	/**
	 * 自我介紹的方法
	 */
	public void introduce() {
		System.out.printf("大家好,我是%s,我是%s生,我今年%d歲。\n",
				this.name, this.sex == 0 ? "女" : "男", this.age);
	}
}

Boy類的源碼:

package com.codeke.java.test;

/**
 * 男生類
 */
public class Boy extends Person {
	/**
	 * 構造方法
	 * @param name 姓名
	 * @param age  年齡
	 */
	public Boy(String name, int age) {
		super(name, age, 1);
		System.out.printf("創建了一個%s生對象。\n",
				super.sex == 0 ? "女" : "男");
	}

	/**
	 * 講話的方法
	 */
	public void say() {
		super.introduce();
	}
}

說明:

  • 在本例的Boy類中,使用super(name, age, 1);調用了基類Person類的構造方法Person(String name, int age, int sex);使用super.sex訪問當前對象的基類對象成員變量;使用super.introduce();調用了當前對象的基類對象成員方法
  • super([參數1, ..., 參數n]);語句必須位於派生類構造方法中的第一行super([參數1, ..., 參數n]);語句和this([參數1, ..., 參數n]);語句無法同時出現在同一個構造方法中。
  • 事實上,每個派生類的構造方法中,如果第一行沒有寫super([參數1, ..., 參數n]);語句,都會隱含調用 super(),如果基類沒有無參的構造方法,那麼在編譯的時候就會報錯。

4、Object類

在Java中,java.lang.Object類是所有類基類,當一個類沒有使用extends關鍵字顯式繼承其他類的時候,該類默認繼承Object類,因此所有類都是Object類的派生類都繼承了Object類的屬性和方法

4.1、常用API

Object類的API如下:

方法 返回值類型 方法說明
getClass() Class<?> 返回此Object所對應的Class類實例
clone() Object 創建並返回此對象的副本
hashCode() int 返回對象的哈希碼值
equals(Object obj) boolean 判斷其他對象是否等於此對象
toString() String 返回對象的字符串表示形式
finalize() void 當垃圾收集確定不再有對該對象的引用時,垃圾收集器在對象上調用該對象
notify() void 喚醒正在等待對象監視器的單個線程
notifyAll() void 喚醒正在等待對象監視器的所有線程
wait() void 導致當前線程等待,直到另一個線程調用該對象的 notify()方法或 notifyAll()方法
wait(long timeout) void 導致當前線程等待,直到另一個線程調用 notify()方法或該對象的 notifyAll()方法,或者指定的時間已過
wait(long timeout, int nanos) void 導致當前線程等待,直到另一個線程調用該對象的 notify()方法或 notifyAll()方法,或者某些其他線程中斷當前線程,或一定量的實時時間

4.2、案例

下面是一個示例:
Animal類的源碼:

package com.codeke.java.test;

/**
 * 動物類
 */
public class Animal {

	// 屬性
	String type;    // 類型
	String breed;    // 品種
	String name;    // 名稱

	/**
	 * 構造函數
	 * @param type  類型
	 * @param breed 品種
	 * @param name  名稱
	 */
	public Animal(String type, String breed, String name) {
		this.type = type;
		this.breed = breed;
		this.name = name;
	}

	/**
	 * 自我介紹方法
	 */
	public void introduce() {
		System.out.printf("主人好,我是%s,我的品種是%s,我的名字叫%s。",
				this.type, this.breed, this.name);
	}
}

Test類的源碼:

package com.codeke.java.test;

/**
 * 測試類
 */
public class Test {
	public static void main(String[] args) {
		// 實例化若干Animal
		Animal animal1 = new Animal("貓","波斯貓", "大花");
		Animal animal2 = new Animal("狗","牧羊犬", "大黑");

		// 調用繼承自Object類的一些方法
		System.out.println("animal1.hashCode() = " + animal1.hashCode());
		System.out.println("animal1.toString() = " + animal1.toString());
		System.out.println("animal1.equals(animal2) = " + animal1.equals(animal2));
	}
}

說明:

  • 上例中,通過Animal類的對象調用了Animal類繼承自基類Object類的方法。
  • 查看Object類中toString()方法的實現,如下:
    public String toString() {
         return getClass().getName() + "@" + Integer.toHexString(hashCode());
     }
    
    可以看到,Object類中toString()方法打印了類的完全限定名+@+hashCode()方法的返回值
  • 查看Object類中equals(Object obj)方法的實現,如下:
    public boolean equals(Object obj) {
         return (this == obj);
     }
    
    可以看到,Object類中equals(Object obj)方法就是對兩個對象進行了==操作符比較運算,並返回比較結果。

二、封裝

1、什麼是封裝

所謂封裝,即是對信息屬性方法的實現細節等)進行隱藏。封裝亦是面向對象編程中基本的、核心特徵之一。

在Java中,一個一個封裝數據(屬性)以及操作這些數據的代碼(方法)的邏輯實體。具體的說,在將客觀事物按照面向對象思想抽象成類過程中,開發人員可以給類中不同成員提供不同級別保護。比如可以公開某些成員使其能夠被外界訪問;可以將某些成員私有,使其不能被外界訪問;或者給予某些成員一定的訪問權限,使其只能被特定、有限的外界訪問。

通過封裝,Java中的類既提供了能夠與外部聯繫的必要API也儘可能隱藏了類的實現細節避免了這些細節外部程序意外的改變被錯誤的使用

封裝爲軟件提供了一種安全的健壯的模塊化設計機制。類的設計者提供標準化的,而使用者根據實際需求選擇組裝各種功能的通過API使它們協同工作,從而實現軟件系統。在具體開發的過程中,類的設計者需要考慮如何定義類中的成員變量和方法,如何設置其訪問權限等問題。類的使用者只需要知道有哪些類可以選擇,每個類有哪些功能,每個類中有哪些可以訪問的成員變量和成員方法等,而不需要了解其實現的細節。

2、類中成員的訪問權限

按照封裝原則,類的設計者既要提供與外部聯繫方式,又要儘可能隱藏類實現細節具體辦法就是爲類的成員變量成員方法設置合理的訪問權限

Java爲類中的成員提供了四種訪問權限修飾符,它們分別是public公開)、protected保護)、缺省private私有),它們的具體作用如下:

  1. public:被public修飾的成員變量和成員方法可以在所有類中訪問。(注意:所謂在某類中訪問某成員變量是指在該類的方法中給該成員變量賦值和取值。所謂在某類中訪問某成員方法是指在該類的方法中調用該成員方法。)
  2. protected:被protected修飾的成員變量和成員方法可以聲明它的類中訪問,在該類的子類中訪問,也可以在與該類位於同一個包中的類訪問,但不能在位於其它包非子類中訪問
  3. 缺省缺省不使用權限修飾符。不使用權限修飾符修飾的成員變量和成員方法可以聲明它的類中訪問,也可以在與該類位於同一個包中的類訪問,但不能在位於其它包的類中訪問
  4. private:被private修飾的成員變量和成員方法只能聲明它們的類中訪問,而不能其它類包括子類中訪問

總結如下:

public protected 缺省 private
當前類中能否訪問
同包子類中能否訪問
同包非子類中能否訪問
不同包子類中能否訪問
不同包非子類中能否訪問

下面是一個示例:
com.codeke.java.test1包下Person類的源碼:

package com.codeke.java.test1;

/**
 * 人類
 */
public class Person {

	public String name;         // 名稱
	protected int age;          // 年齡
	int sex;                    // 性別( 1:男 0:女 )
	private String favourite;   // 愛好

	/**
	 * 構造方法
	 *
	 * @param name      姓名
	 * @param age       年齡
	 * @param sex       性別
	 * @param favourite 愛好
	 */
	public Person(String name, int age, int sex, String favourite) {
		this.name = name;
		this.sex = sex;
		this.age = age;
		this.favourite = favourite;
	}

	/**
	 * 自我介紹的方法
	 */
	public void introduce() {
		System.out.printf("大家好,我是%s,我是%s生,我今年%d歲,我的愛好是%s。\n",
				this.name, this.sex == 0 ? "女" : "男", this.age, this.favourite);
	}

	/**
	 * 閱讀的方法
	 */
	protected void read() {
		System.out.printf("我是%s,我正在閱讀。\n", this.name);
	}

	/**
	 * 寫作的方法
	 */
	void write() {
		System.out.printf("我是%s,我正在寫作。\n", this.name);
	}

	/**
	 * 休息的方法
	 */
	private void rest() {
		System.out.printf("我是%s,我正在休息。\n", this.name);
	}
}

com.codeke.java.test1包下Boy類的源碼:

package com.codeke.java.test1;

/**
 * 男生類
 */
public class Boy extends Person {

	/**
	 * 構造方法
	 * @param name      姓名
	 * @param age       年齡
	 * @param favourite 愛好
	 */
	public Boy(String name, int age, String favourite) {
		super(name, age, 1, favourite);
	}

	/**
	 * 做某些事的方法
	 */
	public void doSomething () {
		this.introduce();
		this.read();
		this.write();
	}
}

com.codeke.java.test2包下Girl類的源碼:

package com.codeke.java.test2;

import com.codeke.java.test1.Person;

/**
 * 女生類
 */
public class Girl extends Person {
	
	/**
	 * 構造方法
	 * @param name      姓名
	 * @param age       年齡
	 * @param favourite 愛好
	 */
	public Girl(String name, int age, String favourite) {
		super(name, age, 0, favourite);
	}

	/**
	 * 做某些事的方法
	 */
	protected void doSomething () {
		this.introduce();
		this.read();
	}
}

說明:

  • 嘗試在com.codeke.java.test2包及com.codeke.java.test2包下新建測試類,在main方法中創建Person類、Boy類、GIrl類的對象,訪問這些對象的屬性及方法,觀察它們被不同的訪問權限修飾符修飾時的效果。

3、getter/setter訪問器

在之前的例子中,類中的成員變量都是缺省權限修飾符的,這在一定程度上破壞了封裝性。事實上,在Java中極力提倡使用private修飾類的成員變量,然後提供一對publicgetter方法和setter方法對私有屬性進行訪問。這樣的getter方法和setter方法也被稱爲屬性訪問器
下面是一個示例:

package com.codeke.java.test;

/**
 * 人類
 */
public class Person {

	private String name;         // 名稱
	private int age;          // 年齡
	private int sex;                    // 性別( 1:男 0:女 )
	private String favourite;   // 愛好

	/**
	 * 構造方法
	 *
	 * @param name      姓名
	 * @param age       年齡
	 * @param sex       性別
	 * @param favourite 愛好
	 */
	public Person(String name, int age, int sex, String favourite) {
		this.name = name;
		this.sex = sex;
		this.age = age;
		this.favourite = favourite;
	}

	/**
	 * 獲取名稱的方法
	 * @return 名稱
	 */
	public String getName() {
		return name;
	}

	/**
	 * 設置名稱的方法
	 * @param name 要設置的名稱
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * 獲取年齡的方法
	 * @return 年齡
	 */
	public int getAge() {
		return age;
	}

	/**
	 * 設置年齡的方法
	 * @param age 要設置的年齡
	 */
	public void setAge(int age) {
		this.age = age;
	}

	/**
	 * 獲取性別的方法
	 * @return 性別
	 */
	public int getSex() {
		return sex;
	}

	/**
	 * 設置性別的方法
	 * @param sex 要設置的性別
	 */
	public void setSex(int sex) {
		this.sex = sex;
	}

	/**
	 * 獲取愛好的方法
	 * @return 愛好
	 */
	public String getFavourite() {
		return favourite;
	}

	/**
	 * 設置愛好的方法
	 * @param favourite 要設置的愛好
	 */
	public void setFavourite(String favourite) {
		this.favourite = favourite;
	}
}

說明:

  • 本例中,Person類的成員變量都被private修飾,只有在Person類的內部才能直接訪問,在Person類的外部,需要使用Person類提供的屬性訪問器纔可以訪問。

4、類的訪問權限

通常情況下(不考慮內部類的情況,內部類將在後面的章節中詳細介紹),聲明類時只能使用public訪問權限修飾符缺省。雖然一個Java源文件可以定義多個類,但只能一個類使用public修飾符,該類的類名與類文件的文件名必須相同,而其他類需要缺省權限修飾符

使用public修飾的類,在其他類都可以被使用,而缺省權限修飾符的類,只有在同包的情況下才能被使用

三、多態

1、多態的概念

1.1、生活中的多態

多態簡單的理解就是多種形態多種形式。具體來說,多態是指同一個行爲具有多個不同表現形式形態

比如遙控器都有打開按鈕,電視遙控器按打開按鈕,執行打開的行爲,可以打開電視機播放節目,而電燈的遙控器按打開按鈕,執行打開的行爲,可以打開電燈照明。圖示如下:
在這裏插入圖片描述

1.2、Java中的多態

在Java中,多態是指同一名稱方法可以有多種實現(方法實現是指方法體)。系統根據調用方法參數調用方法對象自動選擇某一個具體的方法實現執行多態亦是面向對象核心特徵之一

多態機制使具有不同內部結構對象可以共享相同外部接口。這意味着,雖然針對不同對象的具體操作不同,但通過一個公共的類,它們可以通過相同的方式予以調用。

2、Java中多態的實現

在Java中,多態可以通過方法重載(overload)和方法重寫(override)來實現

2.1、方法的重載(overload)

在之前的章節中已經介紹過方法重載(overload)。在一個類中,多個方法具有相同方法名稱,但卻具有不同參數列表,與返回值無關,稱作方法重載(overload)。

重載的方法在程序設計階段根據調用方法時的參數便已經可以確定調用的是具體哪一個方法實現,故方法重載體現了設計時多態

下面是一個示例:
Animal類的源碼:

package com.codeke.java.test;

/**
 * 動物類
 */
public class Animal {
	// 屬性
	private String type;    // 類型
	private String breed;   // 品種
	private String name;    // 名稱

	/**
	 * 構造函數(明確知道類型、品種、名稱)
	 * @param type  類型
	 * @param breed 品種
	 * @param name  名稱
	 */
	public Animal(String type, String breed, String name) {
		this.type = type;
		this.breed = breed;
		this.name = name;
	}

	/**
	 * 構造函數(只知道類型和名稱,但是品種未知)
	 * @param type  類型
	 * @param name  名稱
	 */
	public Animal(String type, String name) {
		this.type = type;
		this.name = name;
		this.breed = "未知";
	}

	/**
	 * 構造函數(只知道名稱,但是類型和品種都未知)
	 * @param name  名稱
	 */
	public Animal(String name) {
		this.name = name;
		this.type = "未知";
		this.breed = "未知";
	}
}

測試類PetShop類的源碼:

package com.codeke.java.test;

/**
 * 寵物店(測試類)
 */
public class PetShop {
	/**
	 * 用來測試的main方法
	 */
	public static void main(String[] args) {
		// 定義若干動物對象,使用了各種不同的Animal類的構造方法
		Animal animal1 = new Animal("狗", "牧羊犬", "大黑");
		Animal animal2 = new Animal("貓", "大花");
		Animal animal3 = new Animal("大鴨");
	}
}

說明:

  • PetShop類的main方法中使用了各種不同的Animal類的構造方法,在設計階段,根據這些構造方法的入參,開發者就已經可以確定調用哪一個具體的構造方法。

2.2、方法的重寫(override)

方法重寫(override)指在繼承關係中派生類重寫基類方法,以達到同一方法不同派生類中有不同實現

如果基類中方法實現不適合派生類派生類便可以重新定義。派生類中定義的方法與基類中的方法具有相同返回值方法名稱參數列表,但具有不同方法體稱之爲派生類重寫基類方法

僅僅在派生類中重寫了基類的方法,仍然不足以體現多態性還需要使用面向對象程序設計中的一條基本原則,即 里氏替換原則里氏替換原則 表述爲,任何基類可以出現的地方派生類一定可以出現。直白的說就是基類類型變量可以引用派生類對象(即基類類型變量代表的內存中存儲的是一個派生類對象內存中的地址編號)。此時,通過基類類型變量調用基類中的方法,真正的方法執行者派生類對象,被執行的方法如果在派生類中被重寫過,實際執行的便是派生類中方法體

通過上述方式,相同基類類型變量調用相同方法根據調用方法的具體派生類對象不同,便可以執行不同方法實現。由於在程序運行階段變量引用內存地址才能最終確定,故這種形式的多態體現了運行時多態

下面是一個示例:
基類Animal類的源碼:

package com.codeke.java.test;

/**
 * 動物類
 */
public class Animal {
	// 屬性
	private String type;    // 類型
	private String breed;   // 品種
	private String name;    // 名稱

	/**
	 * 構造函數
	 * @param type  類型
	 * @param breed 品種
	 * @param name  名稱
	 */
	public Animal(String type, String breed, String name) {
		this.type = type;
		this.breed = breed;
		this.name = name;
	}

	/**
	 * 獲取名稱的方法
	 * @return 名稱
	 */
	public String getName() {
		return this.name;
	}
	
	/**
	 * 發出聲音
	 */
	public void makeSound() { }
}

派生類Cat類的源碼:

package com.codeke.java.test;

/**
 * 貓類
 */
public class Cat extends Animal {
	/**
	 * 派生類構造函數
	 * @param breed 品種
	 * @param name  名稱
	 */
	public Cat(String breed, String name) {
		super("貓", breed, name);
	}

	/**
	 * 重寫基類的makeSound()方法
	 */
	@Override
	public void makeSound() {
		System.out.printf("%s發出叫聲,喵喵喵。\n", super.getName());
	}
}

派生類Dog類的源碼:

package com.codeke.java.test;

/**
 * 狗類
 */
public class Dog extends Animal {
	/**
	 * 派生類構造函數
	 * @param breed 品種
	 * @param name  名稱
	 */
	public Dog(String breed, String name) {
		super("狗", breed, name);
	}

	/**
	 * 重寫基類的makeSound()方法
	 */
	@Override
	public void makeSound() {
		System.out.printf("%s發出叫聲,汪汪汪。\n", super.getName());
	}
}

測試類PetShop類的源碼:

package com.codeke.java.test;

/**
 * 寵物店(測試類)
 */
public class PetShop {
	/**
	 * 用來測試的main方法
	 */
	public static void main(String[] args) {
		// 實例化Cat類和Dog類的對象,並將它們賦值給基類Animal類的變量
		Cat cat = new Cat("波斯貓", "大花");
		Animal animal1 = cat;
		Animal animal2 = new Dog("牧羊犬", "大黑");
		
		// 使用Animal類的變量調用Animal類中被派生類重寫過的方法
		animal1.makeSound();
		animal2.makeSound();
	}
}

執行輸出結果:

大花發出叫聲,喵喵喵。
大黑髮出叫聲,汪汪汪。

說明:

  • 本例中,對象animal1的數據類型是Animal,但該變量實際引用的是一個Cat類型的實例,由animal1調用makeSound()方法時,實際執行的是Cat類中makeSound()方法的方法體;對象animal2的數據類型也是Animal,但該變量實際引用的是一個Dog類型的實例,由animal2調用makeSound()方法時,實際執行的是Dog類中makeSound()方法的方法體。
  • 注意,在派生類中重寫的makeSound()方法上標註了一個@Override,這種由一個@+單詞組成的標註在Java中稱爲註解(也叫元數據),是一種代碼級別說明註解可以出現字段方法局部變量方法參數等的前面,用來對這些元素進行說明。本例中的註解@Override用來說明其後的方法是一個重寫方法
  • 注意,重寫方法不能縮小基類中被重寫方法訪問權限
  • 實現運行時多態的三個必要條件:繼承方法重寫基類變量引用派生類對象

3、重寫toString()和equals(Object obj)方法

前文中提到,java.lang.Object類是所有類的基類,故該類中常用方法被所有類繼承。在很多情況下,開發人員需要重寫繼承自java.lang.Object類的一些常用方法toString()方法和equals(Object obj)方法便是比較有代表性的方法。

3.1、重寫toString()方法

toString()方法可以返回對象的字符串表示形式,該方法在java.lang.Object類中的實現爲返回類的完全限定名+@+hashCode()方法的返回值,在調用System.out.println(Object x)方法打印對象時,便會調用被打印對象toString()方法。在實際開發中,toString()方法經常被重寫

下面是一個示例:
Person類的源碼:

package com.codeke.java.test;

/**
 * 人類
 */
public class Person {

    private String name;         // 名稱
    private int age;          // 年齡
    private int sex;                    // 性別( 1:男 0:女 )
    private String favourite;   // 愛好

    /**
     * 構造方法
     * @param name      姓名
     * @param age       年齡
     * @param sex       性別
     * @param favourite 愛好
     */
    public Person(String name, int age, int sex, String favourite) {
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.favourite = favourite;
    }

    /**
     * 重寫的toString()方法
     * @return 描述Person對象的字符串
     */
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                ", favourite='" + favourite + '\'' +
                '}';
    }
}

Test類的源碼:

package com.codeke.java.test;

/**
 * 測試類
 */
public class Test {
    public static void main(String[] args) {
        Person person1 = new Person("宋江",18, 1, "結交朋友");
        Person person2 = new Person("武松",19, 1, "打架");
        System.out.println("person1 = " + person1);
        System.out.println("person2 = " + person2);
    }
}

執行輸出結果:

person1 = Person{name='宋江', age=18, sex=1, favourite='結交朋友'}
person2 = Person{name='武松', age=19, sex=1, favourite='打架'}

說明:

  • 觀察本例的輸出結果,System.out.println(Object x)打印Person類的對象時,使用的是Person中重寫過的toString()的實現。

3.2、重寫equals(Object obj)方法

equals(Object obj)方法用來判斷其他對象是否等於當前對象,該方法在java.lang.Object類中的實現爲返回兩個對象使用==操作符進行比較運算的結果。在實際開發中,有時需要兩個對象屬性值完全對應相同時即認爲兩個對象相同,此時,equals(Object obj)方法需要被重寫

下面是一個示例:
Person類的源碼:

package com.codeke.java.test;

/**
 * 人類
 */
public class Person {

    private String name;         // 名稱
    private int age;          // 年齡
    private int sex;                    // 性別( 1:男 0:女 )
    private String favourite;   // 愛好

    /**
     * 構造方法
     * @param name      姓名
     * @param age       年齡
     * @param sex       性別
     * @param favourite 愛好
     */
    public Person(String name, int age, int sex, String favourite) {
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.favourite = favourite;
    }

    /**
     * 重寫的equals(Object o)方法
     * @param o 要比較的對象
     * @return 比較結果
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        if (age != person.age) return false;
        if (sex != person.sex) return false;
        if (name != null ? !name.equals(person.name) : person.name != null) return false;
        return favourite != null ? favourite.equals(person.favourite) : person.favourite == null;
    }
}

Test類的源碼:

package com.codeke.java.test;

/**
 * 測試類
 */
public class Test {
    public static void main(String[] args) {
        Person person1 = new Person("宋江",18, 1, "結交朋友");
        Person person2 = new Person("宋江",18, 1, "結交朋友");
        System.out.println("person1.equals(person2) = " + person1.equals(person2));
    }
}

執行輸出結果:

person1.equals(person2) = true

說明:

  • 觀察本例中重寫的equals(Object obj)方法,依次比較兩個對象內存地址是否相同,數據類型是否相同,屬性值是否全部對應相同。Person類的對象person1person2屬性值完全對應相同,故person1.equals(person2)的結果爲true
  • 之前的章節中提到,字符串比較字面值是否相同時,需要使用字符串equals(Object anObject)方法,其本質便是String重寫equals(Object obj)方法String類重寫過的equals(Object anObject)方法如下:
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
    

四、final關鍵字

final是Java中一個非常重要的關鍵字final的字面意思是最終的最後的決定性的不可改變的。在Java中,final關鍵字表達的亦是這層含義。final關鍵字可以用來修飾類成員方法成員變量局部變量

1、final關鍵字修飾類

有時候,出於安全考慮,有些類不允許繼承。有些類定義的已經很完美,不需要再生成派生類。凡是不允許繼承需要聲明爲final
```final``關鍵字修飾類的語法格式爲:

[修飾符] final class 類名 {} 

在JDK中,有許多聲明爲final的類,比如StringScannerByteShortIntegerDouble等類都是final類。

2、final關鍵字修飾成員方法

出於封裝考慮有些成員方法不允許被派生類重寫,不允許被派生類重寫方法需要聲明爲final方法
final關鍵字修飾方法的語法格式爲:

[修飾符] final 返回值類型 方法名稱([參數列表]) {
	// 方法體
}

在JDK中,也有許多被final關鍵字修飾的方法,比如Object類的notify()方法、notifyAll()方法、wait()方法等。

3、final關鍵字修飾成員變量

final關鍵字也可以用來修飾成員變量final修飾的成員變量只能顯示初始化或者構造函數初始化的時候賦值一次以後不允許更改final修飾的成員變量也被稱爲常量常量名稱一般所有字母大寫,單詞中間使用下劃線_)分割。

final關鍵字修飾成員變量的語法格式爲:

[修飾符] final 數據類型 常量名稱;

下面是一個示例:
MathUtils類的源碼:

package com.codeke.java.test;

/**
 * 數學工具類
 */
public class MathUtils {

	public final double PI = 3.14159265358979323846;
	public final double E;

	/**
	 * 構造方法
	 */
	public MathUtils(double E) {
		this.E = E;
	}
}

Test類的源碼:

package com.codeke.java.test;

/**
 * 測試類
 */
public class Test {
	public static void main(String[] args) {
		MathUtils mathUtils = new MathUtils(2.7182818284590452354);
	}
}

說明:

  • JDK中提供了java.lang.Math類,該類中提供了常量PI和常量E
  • 常量無法改變,故不需要提供setter訪問器。

4、final關鍵字修飾局部變量

final關鍵字也可以用來修飾局部變量,和修飾成員變量一樣,表示該變量不能被第二次賦值final關鍵字修飾局部變量的語法格式和修飾成員變量的語法相同。

下面是一個示例:

package com.codeke.java.test;

/**
 * 測試類
 */
public class Test {
	public static void main(String[] args) {
		// final修飾的局部變量,只能初始化一次,不能被第二次賦值
		final String str1 = "hello";
		// 這樣也是隻初始化了一次
		final String str2;
		str2 = "world";

		// 對於引用類型的局部變量,被final修飾的變量中的內存地址編號只能初始化一次,
		// 但引用的地址中的數據是可以被多次賦值的
		final int[] nums = new int[]{1, 2, 3};
		nums[0] = 4;
		nums[0] = 5;
	}
}

說明:

  • 注意,對於引用類型局部變量,被final修飾的變量所代表的內存中存儲的內存地址編號只能初始化一次,但引用的對象中的數據可以被多次賦值的。
  • 本質上,final修飾變量包括成員變量局部變量),變量所代表的內存中存儲數據只能初始化一次不能被二次賦值

五、static關鍵字

static也是Java中一個非常重要的關鍵字final的字面意思是靜止的靜態的。在Java中,static關鍵字可以用來聲明類的靜態成員聲明靜態導入等。

1、靜態成員

之前的章節和內容中不斷提到類的成員(成員變量和成員方法),在Java中,類的成員也可以分爲兩種,分別是實例成員類成員

實例成員屬於對象的,實例成員包括實例成員變量實例成員方法。只有創建了對象之後,才能通過對象訪問實例成員變量調用實例成員方法

類成員屬於類的,類成員在聲明時需要使用static修飾符修飾。類成員包括類成員變量類成員方法通過類名可以直接訪問類成員變量調用類成員方法也可以通過對象名訪問類成員變量調用類成員方法

沒有被static修飾符修飾的成員變量爲實例成員變量(實例變量),沒有被static修飾符修飾的成員方法爲實例成員方法(實例方法);static修飾符修飾的成員變量爲類成員變量(靜態成員變量、類變量、靜態變量),static修飾符修飾的成員方法爲類成員方法(靜態成員方法、類方法、靜態方法)。如下圖:
在這裏插入圖片描述
實例成員和類成員核心區別在於內存分配機制不同實例成員變量隨着對象創建堆中分配內存,每個對象都有獨立內存空間存儲各自的實例成員變量類成員變量在程序運行期間,首次使用類名時方法區中分配內存,並且只分配一次,無論使用類名還是對象訪問類成員變量時,訪問的都是方法區同一塊內存實例成員方法必須由堆中的對象調用類成員方法可以直接使用類名調用

需要注意的是,由於實例成員和類成員內存分配機制的不同,顯而易見的現象是,在類體中,可以在一個實例成員方法中調用類成員方法,反之則不行;可以將一個類成員變量賦值給一個實例成員變量,反之則不行。

另外,還需提到的是,類成員方法不能(事實上也無需)被重寫無法表現多態性

下面是一個示例:
Chinese類的源碼:

package com.codeke.java.Test;

/**
 * 中國人類
 */
public class Chinese {
	private String name;                     // 名稱
	private int age;                         // 年齡
	private int sex;                         // 性別( 1:男 0:女 )
	public static String eyeColor = "黑色";  // 眼睛顏色
	public static String skinColor = "黃色"; // 皮膚顏色
	
	/**
	 * 構造方法
	 * @param name      姓名
	 * @param age       年齡
	 * @param sex       性別
	 */
	public Chinese(String name, int age, int sex) {
		this.name = name;
		this.sex = sex;
		this.age = age;
	}
}

Test類的源碼:

package com.codeke.java.Test;

/**
 * 測試類
 */
public class Test {
	public static void main(String[] args) {
		Chinese chinese1 = new Chinese("宋江",18, 1);
		Chinese chinese2 = new Chinese("武松",19, 1);
		System.out.println(Chinese.eyeColor);
		System.out.println(Chinese.skinColor);
		System.out.println(chinese1.eyeColor);
		System.out.println(chinese1.skinColor);
		System.out.println(chinese2.eyeColor);
		System.out.println(chinese2.skinColor);
	}
}

執行輸出結果:

黑色
黃色
黑色
黃色
黑色
黃色

說明:

  • 本例中,Chinese類中的成員變量eyeColorskinColor是靜態的,無論使用Chinese類名還是Chinese類的對象chinese1chinese2,訪問這兩個類成員變量時,都訪問的是方法區中的同一塊內存地址。圖示如下:
    在這裏插入圖片描述
  • 類成員也體現面向對象思想,它可以描述某一類中具有共性的,可以不依賴於對象而存在的成員。比如本例中,就算沒有任何一箇中國人的對象存在,中國人的眼睛顏色也應該是黑色的,皮膚顏色也應該是黃色的。

下面是另一個示例:
java.lang.Math類的部分源碼:

package java.lang;
public final class Math {
	public static final double E = 2.7182818284590452354;
	public static final double PI = 3.14159265358979323846;
	
	public static int addExact(int x, int y) {
        int r = x + y;
        // HD 2-12 Overflow iff both arguments have the opposite sign of the result
        if (((x ^ r) & (y ^ r)) < 0) {
            throw new ArithmeticException("integer overflow");
        }
        return r;
    }
	
	public static int subtractExact(int x, int y) {
        int r = x - y;
        // HD 2-12 Overflow iff the arguments have different signs and
        // the sign of the result is different than the sign of x
        if (((x ^ y) & (x ^ r)) < 0) {
            throw new ArithmeticException("integer overflow");
        }
        return r;
    }
}

說明:

  • java.lang.Math類中的成員PIE都是靜態的,絕大多數成員方法也都是靜態的。將這些成員修飾爲靜態,使的Math類在語義上更加自然,在開發過程中的使用上更加方便。
  • 注意觀察成員PIE,它們都是自然存在的常數,故由final關鍵字修飾而成爲常量;同時,由於它們可以不依賴於Math類的對象而存在,故又由static關鍵子修飾而成爲靜態成員。此時,它們的唯一性、確定性已經得到了保證,爲了方便使用起見,可以將它們的權限修飾符聲明爲public。事實上,在Java中,常量通常都是由public static final共同修飾的

2、靜態代碼塊

類中除了成員變量和成員方法外,還有其他一些成員靜態代碼塊便是其中之一

實例成員是在new時分配內存, 並且有構造函數初始化。靜態成員是在類名首次出現時分配內存的,靜態成員需要靜態代碼塊初始化首次使用類名時,首先爲靜態成員分配內存,然後就調用靜態代碼塊,爲靜態成員初始化。注意,靜態代碼塊只調用一次。另外,顯而易見的,靜態代碼塊中無法訪問類的實例成員變量,也無法調用類的實例成員方法

聲明靜態代碼塊的語法格式如下:

static {
	// 代碼
}

下面是一個示例:
Chinese類的源碼:

package com.codeke.java.Test;

/**
 * 中國人類
 */
public class Chinese {
	private String name;                     // 名稱
	private int age;                         // 年齡
	private int sex;                         // 性別( 1:男 0:女 )
	public static String eyeColor;           // 眼睛顏色
	public static String skinColor;          // 皮膚顏色
	
	static {
		eyeColor = "黑色";
		skinColor = "黃色";
	}

	/**
	 * 構造方法
	 * @param name      姓名
	 * @param age       年齡
	 * @param sex       性別
	 */
	public Chinese(String name, int age, int sex) {
		this.name = name;
		this.sex = sex;
		this.age = age;
	}
}

3、靜態導入

在一個類中使用其他類靜態方法靜態變量時,可以使用static關鍵字靜態導入其他類靜態成員,該類中就可以直接使用其他類靜態成員

靜態導入的語法格式如下:

import static 類完全限定名.靜態成員名

下面是一個示例:

package com.codeke.java.test;

import static java.lang.Math.E;
import static java.lang.Math.PI;
import static java.lang.Math.addExact;

/**
 * 測試類
 */
public class Test {
	public static void main(String[] args) {
		System.out.println("PI = " + PI);
		System.out.println("E = " + E);
		System.out.println("addExact(1,2) = " + addExact(1, 2));
	}
}

說明:

  • 本例的測試類中使用import static導入一些了java.lang.Math類的靜態成員,於是這些靜態成員在main方法中可以直接使用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章