## 類的繼承
#### 繼承的概念
在Java中,類的繼承是指在一個現有類的基礎上去構建一個新的類,構建出來的新類被稱作子類,現有類被稱作父類,子類會自動擁有父類所有可繼承的屬性和方法。在程序中,如果想聲明一個類繼承另一個類,需要使用extends關鍵字,接下來通過一個案例來學習子類是如何繼承父類的。
```java
// 定義動物類
class Animal {
String name;
void shout() {
System.out.println("動物發出叫聲");
}
}
// 定義dog類繼承動物類
class Dog extends Animal {
public void printName() {
System.out.println("name = " + name);
}
}
public class Example {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "極品二哈";
dog.printName();
dog.shout();
}
}
```
運行結果:
```
name = 極品二哈
動物發出叫聲
```
例子中,Dog類通過extends關鍵字繼承了Animal類,這樣Dog類便是Animal類的子類。從運行結果不難看出,子類雖然沒有定義name屬性和shout()方法,但是卻能訪問這兩個成員。這就說明,子類在繼承父類的時候,會自動擁有父類所有的成員。在類的繼承中,需要注意一些問題,具體如下:
- 在Java中,類只支持單繼承,不允許多重繼承,也就是說一個類只能有一個直接父類,例如下面這種情況是不合法的。
```
class A{}
class B{}
class C extends A,B{} //C 類不可以同時繼承A 類和B 類
```
- 多個類可以繼承一個父類,例如下面這種情況是允許的。
```
class A{}
class B extends A{}
class C extends A{} //類B 和類C 都可以繼承類A
```
- 在Java中,多層繼承是可以的,即一個類的父類可以再去繼承另外的父類,例如C類繼承自B類,而B類又可以去繼承A 類,這時,C類也可稱作A 類的子類。下面這種情況是允許的。
```
class A{}
class B extends A{} //類B 繼承類A,類B 是類A 的子類
class C extends B{} //類C 繼承類B,類C 是類B 的子類,同時也是類A 的子類
```
- 在Java中,子類和父類是一種相對概念,也就是說一個類是某個類父類的同時,也可以是另一個類的子類。例如上面的示例中,B類是A 類的子類,同時又是C 類的父類。
#### 重寫父類方法
在繼承關系中,子類會自動繼承父類中定義的方法,但有時在子類中需要對繼承的方法進行一些修改,即對父類的方法進行重寫。需要注意的是,在子類中重寫的方法需要和父類被重寫的方法具有相同的方法名、參數列表以及返回值類型。
例中,Dog類從Animal類繼承了shout()方法,該方法在被調用時會打印“動物發出叫聲”,這明顯不能描述一種具體動物的叫聲,Dog類對象表示犬類,發出的叫聲應該是“汪汪”。為了解決這個問題,可以在Dog類中重寫父類Animal中的shout()方法。
```java
// 定義動物類
class Animal {
String name;
void shout() {
System.out.println("動物發出叫聲");
}
}
// 定義dog類繼承動物類
class Dog extends Animal {
void shout() {
System.out.println("汪汪汪汪");
}
}
public class Example {
public static void main(String[] args) {
Dog dog = new Dog();
dog.shout();
}
}
```
運行結果:
```
汪汪汪汪
```
案例中定義了Dog類并且繼承自Animal類。在子類Dog中定義了一個shout()方法對父類的方法進行重寫。從運行結果可以看出,在調用Dog類對象的shout()方法時,只會調用子類重寫的該方法,并不會調用父類的shout()方法。
**注意:**子類重寫父類方法時,不能使用比父類中被重寫的方法更嚴格的訪問權限,如父類中的方法是public的,子類的方法就不能是private的,關于訪問權限中更多的知識,我們將在本章結尾進行詳細講解,在這里大家只要有個印象就行了。
#### super關鍵字
從上一個例子中的運行結果可以看出,當子類重寫父類的方法后,子類對象將無法訪問父類被重寫的方法,為了解決這個問題,在Java中專門提供了一個super關鍵字用于訪問父類的成員。例如訪問父類的成員變量、成員方法和構造方法。接下來分兩種情況來學習一下super關鍵字的具體用法。
- 使用super關鍵字調用父類的成員變量和成員方法。具體格式如下:
```
super.成員變量
super.成員方法([參數1,參數2…])
```
```java
// 定義動物類
class Animal {
String name = "動物";
void shout() {
System.out.println("動物發出叫聲");
}
}
// 定義dog類繼承動物類
class Dog extends Animal {
String name = "犬類";
// 重寫父類的shout方法
void shout() {
super.shout();
}
// 定義打印方法
void printName() {
System.out.println("name = " + super.name);
}
}
public class Example {
public static void main(String[] args) {
Dog dog = new Dog();
dog.shout();
dog.printName();
}
}
```
運行結果:
```
動物發出叫聲
name = 動物
```
案例中,定義了一個Dog類繼承Animal類,并重寫了Animal類的shout()方法。在子類Dog 的shout()方法中使用“super.shout()”調用了父類被重寫的方法,在printName()方法中使用“super.name”訪問父類的成員變量。從運行結果可以看出,子類通過super關鍵字可以成功地訪問父類成員變量和成員方法。
- 使用super關鍵字調用父類的構造方法。具體格式如下:
```
super([參數1,參數2…])
```
```java
// 定義動物類
class Animal {
public Animal(String name) {
System.out.println("我是一只 " + name);
}
}
// 定義dog類繼承動物類
class Dog extends Animal {
public Dog() {
super("極品二哈");
}
}
public class Example {
public static void main(String[] args) {
Dog dog = new Dog();
}
}
```
運行結果:
```
我是一只 極品二哈
```
根據前面所學的知識,在實例化Dog對象時一定會調用Dog類的構造方法。從運行結果可以看出,Dog類的構造方法被調用時父類的構造方法也被調用了。需要注意的是,通過super調用父類構造方法的代碼必須位于子類構造方法的第一行,并且只能出現一次。
在定義一個類時,如果沒有特殊需求,盡量在類中定義一個無參的構造方法,避免被繼承時出現錯誤。
## final關鍵字
#### final關鍵字修飾類
Java中的類被final關鍵字修飾后,該類將不可以被繼承,也就是不能夠派生子類。
```java
// 定義動物類
final class Animal {
}
// 定義dog類繼承動物類
class Dog extends Animal {
}
public class Example {
public static void main(String[] args) {
Dog dog = new Dog();
}
}
```
編譯程序報錯
由于Animal類被final關鍵字所修飾,因此,當Dog類繼承Animal類時,編譯出現了“無法從最終Animal進行繼承”的錯誤。由此可見,被final關鍵字修飾的類為最終類,不能被其他類繼承。
#### final關鍵字修飾方法
當一個類的方法被final關鍵字修飾后,這個類的子類將不能重寫該方法。
```java
// 定義動物類
class Animal {
public final void shout() {
}
}
// 定義dog類繼承動物類
class Dog extends Animal {
public final void shout() {
}
}
public class Example {
public static void main(String[] args) {
Dog dog = new Dog();
}
}
```
編譯程序報錯
Dog 類重寫父類Animal中的shout()方法后,編譯報錯。這是因為Animal類的shout()方法被final所修飾。由此可見,被final關鍵字修飾的方法為最終方法,子類不能對該方法進行重寫。正是由于final的這種特性,當在父類中定義某個方法時,如果不希望被子類重寫,就可以使用final關鍵字修飾該方法。
#### final關鍵字修飾變量
Java中被final修飾的變量為常量,它只能被賦值一次,也就是說final修飾的變量一旦被賦值,其值不能改變。如果再次對該變量進行賦值,則程序會在編譯時報錯。
```java
public class Example {
public static void main(String[] args) {
final int num = 100;
num = 4;
}
}
```
編譯程序報錯
當第4行對num 賦值時,編譯報錯。原因在于變量num 被final修飾。由此可見,被final修飾的變量為常量,它只能被賦值一次,其值不可改變。
被final關鍵字修飾的變量為局部變量。
```java
class Student {
final String name;
public void introduce() {
System.out.println("我是一名學生,我的名字叫" + name);
}
}
public class Example {
public static void main(String[] args) {
Student student = new Student();
student.introduce();
}
}
```
出現了編譯錯誤,提示變量name沒有初始化。這是因為使用final關鍵字修飾成員變量時,虛擬機不會對其進行初始化。因此使用final修飾成員變量時,需要在定義變量的同時賦予一個初始值,下面將第2行代碼修改為:
```
final String name="小海綿"; //為final 關鍵字修飾的name 屬性賦值
```
運行結果:
```
我是一名學生,我的名字叫小海綿
```
## 抽象類和接口
#### 抽象類
當定義一個類時,常常需要定義一些方法來描述該類的行為特征,但有時這些方法的實現方式是無法確定的。例如前面在定義Animal類時,shout()方法用于表示動物的叫聲,但是針對不同的動物,叫聲也是不同的,因此在shout()方法中無法準確描述動物的叫聲。
針對上面描述的情況,Java允許在定義方法時不寫方法體,不包含方法體的方法為抽象方法,抽象方法必須使用abstract關鍵字來修飾,具體示例如下:abstract void shout(); //定義抽象方法shout()
當一個類中包含了抽象方法,該類必須使用abstract關鍵字來修飾,使用abstract關鍵字修飾的類為抽象類,具體示例如下:
```
abstract void shout(); //定義抽象方法shout()
```
當一個類中包含了抽象方法,該類必須使用abstract關鍵字來修飾,使用abstract關鍵字修飾的類為抽象類,具體示例如下:
```
//定義抽象類Animal
abstract class Animal {
//定義抽象方法shout()
abstract int shout();
}
```
在定義抽象類時需要注意,包含抽象方法的類必須聲明為抽象類,但抽象類可以不包含任何抽象方法,只需使用abstract關鍵字來修飾即可。另外,抽象類是不可以被實例化的,因為抽象類中有可能包含抽象方法,抽象方法是沒有方法體的,不可以被調用。如果想調用抽象類中定義的方法,則需要創建一個子類,在子類中將抽象類中的抽象方法進行實現。接下來通過一個案例來學習如何實現抽象類中的方法。
```java
// 定義抽象類animal
abstract class Animal {
// 定義抽象方法shout
abstract void shout();
}
// 定義dog類繼承抽象方法Animal
class Dog extends Animal {
// 實現抽象方法shout
void shout() {
System.out.println("汪汪汪汪");
}
}
public class Example {
public static void main(String[] args) {
Dog dog = new Dog();
dog.shout();
}
}
```
運行結果:
```
汪汪汪汪
```
從運行結果可以看出,子類實現了父類的抽象方法后,可以正常進行實例化,并通過實例化對象調用方法。
#### 接口
如果一個抽象類中的所有方法都是抽象的,則可以將這個類用另外一種方式來定義,即接口。在定義接口時,需要使用interface關鍵字來聲明,具體示例如下:
```
interface Animal {
int ID = 1; //定義全局常量
void breathe(); //定義抽象方法
void run();
}
```
上面的代碼中,Animal即為一個接口。從示例中會發現抽象方法breathe()并沒有使用abstract關鍵字來修飾,這是因為接口中定義的方法和變量都包含一些默認修飾符。接口中定義的方法默認使用“publicabstract”來修飾,即抽象方法。接口中的變量默認使用“publicstaticfinal”來修飾,即全局常量。
由于接口中的方法都是抽象方法,因此不能通過實例化對象的方式來調用接口中的方法。此時需要定義一個類,并使用implements關鍵字實現接口中所有的方法。接下來通過一個案例來學習。
```java
// 定義接口animal
interface Animal {
int ID = 1;
void breathe();
void run();
}
// 定義dog實現接口Animal
class Dog implements Animal {
public void breathe() {
System.out.println("狗在呼吸");
}
public void run() {
System.out.println("狗在跑");
}
}
public class Example {
public static void main(String[] args) {
Dog dog = new Dog();
dog.breathe();
dog.run();
}
}
```
運行結果:
```
狗在呼吸
狗在跑
```
從運行結果可以看出,類Dog在實現了Animal接口后是可以被實例化的。
上面的例子是類與接口之間的實現關系,在程序中,還可以定義一個接口使用extends關鍵字去繼承另一個接口,接下來對上一個例子稍加修改,演示接口之間的繼承關系。
```java
//定義了Animal 接口
interface Animal {
int ID = 1; // 定義全局常量
void breathe(); // 定義抽象方法breathe()
void run(); // 定義抽象方法run()
}
// 定義了LandAnimal 接口,并繼承了Animal 接口
interface LandAnimal extends Animal { // 接口繼承接口
void liveOnland(); // 定義抽象方法liveOnLand()
}
// 定義Dog 類實現Animal 接口
class Dog implements LandAnimal {
// 實現breathe()方法
public void breathe() {
System.out.println("狗在呼吸");
}
// 實現run()方法
public void run() {
System.out.println("狗在跑");
}
// 實現liveOnLand()方法
public void liveOnland() {
// TODO Auto-generated method stub
System.out.println("狗生活在陸地上");
}
}
public class Example {
public static void main(String[] args) {
Dog dog = new Dog(); // 創建Dog 類的實例對象
dog.breathe(); // 調用Dog 類的breathe()方法
dog.run(); // 調用Dog 類的run()方法
dog.liveOnland(); // 調用Dog 類的liveOnland()方法
}
}
```
運行結果:
```
狗在呼吸
狗在跑
狗生活在陸地上
```
例中,定義了兩個接口,其中LandAnimal接口繼承了Animal接口,因此LandAnimal接口包含了三個抽象方法。當Dog類實現LandAnimal接口時,需要實現兩個接口中定義的三個方法。從運行結果看出,程序可以針對Dog類實例化對象并調用類中的方法。
為了加深初學者對接口的認識,接下來對接口的特點進行歸納,具體如下:
- 接口中的方法都是抽象的,不能實例化對象。
- 當一個類實現接口時,如果這個類是抽象類,則實現接口中的部分方法即可,否則需要實現接口中的所有方法。
- 一個類通過implements關鍵字實現接口時,可以實現多個接口,被實現的多個接口之間要用逗號隔開。具體示例如下:
```
interface Run {
程序代碼……
}
interface Fly {
程序代碼……
}
class Bird implements Run,Fly {
程序代碼……
}
```
- 一個接口可以通過extends關鍵字繼承多個接口,接口之間用逗號隔開。具體示例如下:
```
interface Running {
程序代碼……
}
interface Flying {
程序代碼……
}
Interface Eating extends Running,Flying {
程序代碼……
}
```
- 一個類在繼承另一個類的同時還可以實現接口,此時,extends關鍵字必須位于implements關鍵字之前。具體示例如下:
```
class Dog extends Canidae implements Animal { //先繼承,再實現
程序代碼……
}
```
## 多態
#### 多態概述
在設計一個方法時,通常希望該方法具備一定的通用性。例如要實現一個動物叫的方法,由于每種動物的叫聲是不同的,因此可以在方法中接收一個動物類型的參數,當傳入貓類對象時就發出貓類的叫聲,傳入犬類對象時就發出犬類的叫聲。在同一個方法中,這種由于參數類型不同而導致執行效果各異的現象就是多態。
在Java中為了實現多態,允許使用一個父類類型的變量來引用一個子類類型的對象,根據被引用子類對象特征的不同,得到不同的運行結果。接下來通過一個案例來演示。
```java
//定義接口Anmal
interface Animal {
void shout(); // 定義抽象shout()方法
}
// 定義Cat 類實現Animal 接口
class Cat implements Animal {
// 實現shout()方法
public void shout() {
System.out.println("喵喵……");
}
}
// 定義Dog 類實現Animal 接口
class Dog implements Animal {
// 實現shout()方法
public void shout() {
System.out.println("汪汪……");
}
}
// 定義測試類
public class Example {
public static void main(String[] args) {
Animal an1 = new Cat(); // 創建Cat 對象,使用Animal 類型的變量an1 引用
Animal an2 = new Dog(); // 創建Dog 對象,使用Animal 類型的變量an2 引用
animalShout(an1); // 調用animalShout()方法,將an1 作為參數傳入
animalShout(an2); // 調用animalShout()方法,將an2 作為參數傳入
}
// 定義靜態的animalShout()方法,接收一個Animal 類型的參數
public static void animalShout(Animal an) {
an.shout(); // 調用實際參數的shout()方法
}
}
```
運行結果:
```
喵喵……
汪汪……
```
第25行、第26行代碼實現了父類類型變量引用不同的子類對象,當第27行、第28行代碼調用animalShout()方法時,將父類引用的兩個不同子類對象分別傳入,結果打印出了“喵喵”和“汪汪”。由此可見,多態不僅解決了方法同名的問題,而且還使程序變得更加靈活,從而有效地提高程序的可擴展性和可維護性。
#### 對象的類型轉換
在多態的學習中,涉及到將子類對象當作父類類型使用的情況,例如下面兩行代碼:
```
Animal an1=new Cat(); //將Cat 對象當作Animal 類型來使用
Animal an2=new Dog(); //將Dog 對象當作Animal 類型來使用
```
將子類對象當作父類使用時不需要任何顯式地聲明,需要注意的是,此時不能通過父類變量去調用子類中的某些方法,接下來通過一個案例來演示。
```java
//定義Animal 接口
interface Animal {
void shout(); // 定義抽象方法shout()
}
// 定義Cat 類實現Animal 接口
class Cat implements Animal {
// 實現抽象方法shout()
public void shout() {
System.out.println("喵喵……");
}
// 定義sleep()方法
void sleep() {
System.out.println("貓睡覺……");
}
}
// 定義測試類
public class Example {
public static void main(String[] args) {
Cat cat = new Cat(); // 創建Cat 類的實例對象
animalShout(cat); // 調用animalShout()方法,將cat 作為參數傳入
}
// 定義靜態方法animalShout(),接收一個Animal 類型的參數
public static void animalShout(Animal animal) {
animal.shout(); // 調用傳入參數animal 的shout()方法
animal.sleep(); // 調用傳入參數animal 的sleep()方法
}
}
```
編譯程序報錯
在main方法中,調用animalShout()方法時傳入了Cat類型的對象,而方法的參數類型為Animal類型,這便將Cat對象當作父類Animal類型使用。當編譯器檢查到第29行代碼時,發現Animal類中沒有定義sleep()方法,從而出現編譯程序報錯的錯誤信息,報告找不到sleep()方法。由于傳入的對象是Cat類型,在Cat類中定義了sleep()方法,通過Cat類型的對象調用sleep()方法是可行的,因此可以在animalShout()方法中將Animal類型的變量強轉為Cat類型。將animalShout()方法進行修改,具體代碼如下:
```java
public static void animalShout(Animal animal) {
Cat cat = (Cat) animal; //將animal 對象強制轉換為Cat 類型
cat.shout(); //調用cat 的shout()方法
cat.sleep(); //調用cat 的sleep()方法
}
```
修改后再次編譯,程序沒有報錯,運行結果:
```
喵喵……
貓睡覺……
```
#### Object類
在JDK中提供了一個Object類,它是所有類的父類,即每個類都直接或間接繼承自該類。先來看一個例子。
```java
//定義Animal 類
class Animal {
// 定義動物叫的方法
void shout() {
System.out.println("動物叫!");
}
}
// 定義測試類
public class Example {
public static void main(String[] args) {
Animal animal = new Animal(); // 創建Animal 類對象
System.out.println(animal.toString()); // 調用toString()方法并打印
}
}
```
運行結果:
```
Animal@15db9742
```
在例中,第13行代碼調用了Animal對象的toString()方法,雖然Animal類并沒有定義這個方法,但程序并沒有報錯。這是因為Animal默認繼承自Object類,在Object類中定義了toString()方法,在該方法中輸出了對象的基本信息。
Object類的toString()方法中的代碼具體如下:
```
getClass().getName()+"@"+Integer.toHexString(hashCode());
```
為了方便初學者理解上面的代碼,接下來分別對其中用到的方法進行解釋,具體如下:
- getClass().getName()代表返回對象所屬類的類名,即Animal。
- hashCode()代表返回該對象的哈希值。
- Integer.toHexString(hashCode())代表將對象的哈希值用16進制表示。
其中,hashCode()是Object類中定義的一個方法,這個方法將對象的內存地址進行哈希運算,返回一個int類型的哈希值。
在實際開發中,通常希望對象的toString()方法返回的不僅僅是基本信息,而是一些特有的信息,這時重寫Object的toString()方法便可以實現。
```java
//定義Animal 類
class Animal {
// 重寫Object 類的toString()方法
public String toString() {
return "I am an animal";
}
}
// 定義測試類
public class Example {
public static void main(String[] args) {
Animal animal = new Animal(); // 創建Animal 對象
System.out.println(animal.toString()); // 打印animal 的toString()方法的返回值
}
}
```
運行結果:
```
I am an animal
```
在例子的Animal類中重寫了Object類的toString()方法,當在main()方法中調用toString()方法時,就打印出了Animal類的描述信息“Iamananimal”。