本文是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
關鍵字顯示的調用基類擁有的某個構造方法。 this
和super
都是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
(私有),它們的具體作用如下:
public
:被public
修飾的成員變量和成員方法可以在所有類中訪問。(注意:所謂在某類中訪問某成員變量是指在該類的方法中給該成員變量賦值和取值。所謂在某類中訪問某成員方法是指在該類的方法中調用該成員方法。)protected
:被protected
修飾的成員變量和成員方法可以在聲明它的類中訪問,在該類的子類中訪問,也可以在與該類位於同一個包中的類訪問,但不能在位於其它包的非子類中訪問。- 缺省:缺省指不使用權限修飾符。不使用權限修飾符修飾的成員變量和成員方法可以在聲明它的類中訪問,也可以在與該類位於同一個包中的類訪問,但不能在位於其它包的類中訪問。
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
修飾類的成員變量,然後提供一對public
的getter
方法和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
類的對象person1
和person2
屬性值完全對應相同,故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
的類,比如String
、Scanner
、Byte
、Short
、Integer
、Double
等類都是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
類中的成員變量eyeColor
和skinColor
是靜態的,無論使用Chinese
類名還是Chinese
類的對象chinese1
及chinese2
,訪問這兩個類成員變量時,都訪問的是方法區中的同一塊內存地址。圖示如下:
- 類成員也體現了面向對象的思想,它可以描述某一類中具有共性的,可以不依賴於對象而存在的成員。比如本例中,就算沒有任何一箇中國人的對象存在,中國人的眼睛顏色也應該是黑色的,皮膚顏色也應該是黃色的。
下面是另一個示例:
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
類中的成員PI
、E
都是靜態的,絕大多數成員方法也都是靜態的。將這些成員修飾爲靜態,使的Math
類在語義上更加自然,在開發過程中的使用上更加方便。- 注意觀察成員
PI
、E
,它們都是自然存在的常數,故由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
方法中可以直接使用。