8.繼承、覆蓋、重載與多態
1. 繼承
1.1 繼承的概念
1.1.1 概念
繼承是java面向對象編程技術的一塊基石,因為它允許創建分等級層次的類。 繼承就是子類繼承父類的特征和行為,使得子類對象(實例)具有父類的實例域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。 例如:
兔子和羊屬於食草動物類,獅子和豹屬於食肉動物類。 食草動物和食肉動物又是屬於動物類。 所以繼承需要符合的關系是:is-a,父類更通用,子類更具體。 雖然食草動物和食肉動物都是屬於動物,但是兩者的屬性和行為上有差別,所以子類會具有父類的一般特性也會具有自身的特性
1.1.2 繼承的格式
在 Java 中通過 extends 關鍵字可以申明一個類是從另外一個類繼承而來的,格式:
class 父類 {
}
class 子類 extends 父類 {
}
1.1.3 為什麽需要繼承
接下來我們通過實例來說明這個需求。 開發動物類,其中動物分別為企鵝以及老鼠,要求如下:
企鵝:屬性(姓名,id),方法(吃,睡,自我介紹)
public class Penguin { private String name; private int id; public Penguin(String myName, int myid) { name = myName; id = myid; } public void eat(){ System.out.println(name+"正在吃"); } public void sleep(){ System.out.println(name+"正在睡"); } public void introduction() { System.out.println("大家好!我是" + id + "號" + name + "."); } }
老鼠:屬性(姓名,id),方法(吃,睡,自我介紹)
public class Mouse { private String name; private int id; public Mouse(String myName, int myid) { name = myName; id = myid; } public void eat(){ System.out.println(name+"正在吃"); } public void sleep(){ System.out.println(name+"正在睡"); } public void introduction() { System.out.println("大家好!我是" + id + "號" + name + "."); } }
從這兩段代碼可以看出來,代碼存在重復了,導致後果就是代碼量大且臃腫,而且維護性不高(維護性主要是後期需要修改的時候,就需要修改很多的代碼,容易出錯),所以要從根本上解決這兩段代碼的問題,就需要繼承,將兩段代碼中相同的部分提取出來組成 一個父類:
public class Animal {
private String name;
private int id;
public Animal(String myName, int myid) {
name = myName;
id = myid;
}
public void eat(){
System.out.println(name+"正在吃");
}
public void sleep(){
System.out.println(name+"正在睡");
}
public void introduction() {
System.out.println("大家好!我是" + id + "號" + name + ".");
}
}
這個Animal類就可以作為一個父類,然後企鵝類和老鼠類繼承這個類之後,就具有父類當中的屬性和方法,子類就不會存在重復的代碼,維護性也提高,代碼也更加簡潔,提高代碼的復用性(復用性主要是可以多次使用,不用再多次寫同樣的代碼) 繼承之後的代碼:
企鵝類:
public class Penguin extends Animal {
public Penguin(String myName, int myid) {
super(myName, myid);
}
}
老鼠類:
public class Mouse extends Animal {
public Mouse(String myName, int myid) {
super(myName, myid);
}
}
1.2 繼承的特性
- 子類擁有父類非private的屬性,方法。
- 子類可以擁有自己的屬性和方法,即子類可以對父類進行擴展。
- 子類可以用自己的方式實現父類的方法。
- Java的繼承是單繼承,但是可以多重繼承,單繼承就是一個子類只能繼承一個父類,多重繼承就是,例如A類繼承B類,B類繼承C類,所以按照關系就是C類是B類的父類,B類是A類的父類,這是java繼承區別於C++繼承的一個特性。
- 提高了類之間的耦合性(繼承的缺點,耦合度高就會造成代碼之間的聯系)。
繼承的註意點: 1. 子類不是父類的子集,子類一般比父類包含更多的數據域和方法。 2. 父類中的 private 數據域在子類中是不可見的,因此在子類中不能直接使用它們。 3. 繼承是為"是一個"的關系建模的,父類和其子類間必須存在"是一個"的關系,否則不能用繼承。 4. 但也並不是所有"是一個"的關系都應該用繼承。例如,正方形是一個矩形,但不能讓 Square 類來繼承 Rectangle 類,因為正方形不能從矩形擴展得到任何東西。正確的繼承關系是 Square 類繼承 Shape 類 5. Java 只允許單一繼承(即一個子類只能有一個直接父類),C++ 可以多重繼承(即一個子類有多個直接父類)。
1.3 繼承關鍵字
繼承可以使用 extends 和 implements 這兩個關鍵字來實現繼承,而且所有的類都是繼承於 java.lang.Object,當一個類沒有繼承的兩個關鍵字,則默認繼承object(這個類在 java.lang 包中,所以不需要 import)祖先類。
1.3.1 extends關鍵字
在 Java 中,類的繼承是單一繼承,也就是說,一個子類只能擁有一個父類,所以 extends 只能繼承一個類。
public class Animal {
private String name;
private int id;
public Animal(String myName, String myid) {
//初始化屬性值
}
public void eat() { //吃東西方法的具體實現 }
public void sleep() { //睡覺方法的具體實現 }
}
public class Penguin extends Animal{
}
1.3.2 implements關鍵字
使用 implements 關鍵字可以變相的使java具有多繼承的特性,使用範圍為類繼承接口的情況,可以同時繼承多個接口(接口跟接口之間采用逗號分隔)。
public interface A {
public void eat();
public void sleep();
}
public interface B {
public void show();
}
public class C implements A,B {
}
1.3.3 super與this關鍵字
super關鍵字:我們可以通過super關鍵字來實現對父類成員的訪問,用來引用當前對象的父類。
this關鍵字:指向自己的引用。
public class SuperDemo {
public static void main(String []args) {
new SubClass().showMessage();
}
}
class SuperClass {
int i = 50;
}
class SubClass extends SuperClass {
int i =100;
public void showMessage() {
System.out.printf("super.i = %d, this.i = %d\n", super.i, this.i);
}
}
註意:
- super 語句必須是子類構造方法的第一條語句。不能在子類中使用父類構造方法名來調用父類構造方法。 父類的構造方法不被子類繼承。調用父類的構造方法的唯一途徑是使用 super 關鍵字,如果子類中沒顯式調用,則編譯器自動將 super(); 作為子類構造方法的第一條語句。這會形成一個構造方法鏈。
- 靜態方法中不能使用 super 關鍵字。
- 如果是繼承的方法,是沒有必要使用 super 來調用,直接即可調用。但如果子類覆蓋或重寫了父類的方法,則只有使用 super 才能在子類中調用父類中的被重寫的方法。
調用父類的方法語法:
super.方法名(參數列表);
1.4 final關鍵字
final 關鍵字聲明類可以把類定義為不能繼承的,即最終類;或者用於修飾方法,該方法不能被子類重寫:
聲明類:
final class 類名 {//類體}
聲明方法:
修飾符(public/private/default/protected) final 返回值類型 方法名(){//方法體}
1.4 繼承構造器
子類不能繼承父類的構造器(構造方法或者構造函數),但是父類的構造器帶有參數的,則必須在子類的構造器中顯式地通過super關鍵字調用父類的構造器並配以適當的參數列表。
如果父類有無參構造器,則在子類的構造器中用super調用父類構造器不是必須的,如果沒有使用super關鍵字,系統會自動調用父類的無參構造器。
class SuperClass {
private int n;
SuperClass(){
System.out.println("SuperClass()");
}
SuperClass(int n) {
System.out.println("SuperClass(int n)");
this.n = n;
}
}
class SubClass extends SuperClass{
private int n;
SubClass(){
super(300);
System.out.println("SubClass");
}
public SubClass(int n){
System.out.println("SubClass(int n):"+n);
this.n = n;
}
}
public class TestSuperSub{
public static void main (String args[]){
SubClass sc = new SubClass();
SubClass sc2 = new SubClass(200);
}
}
輸出結果:
SuperClass(int n)
SubClass
SuperClass()
SubClass(int n):200
2. 覆蓋/重寫Override與重載Overload
2.1 覆蓋/重寫Override
2.1.1 覆蓋/重寫的實現
重寫是子類對父類的允許訪問的方法的實現過程進行重新編寫, 返回值和形參都不能改變。即外殼不變,核心重寫!
重寫的好處在於子類可以根據需要,定義特定於自己的行為。 也就是說子類能夠根據需要實現父類的方法。
重寫方法不能拋出新的檢查異常或者比被重寫方法申明更加寬泛的異常。例如: 父類的一個方法申明了一個檢查異常 IOException,但是在重寫這個方法的時候不能拋出 Exception 異常,因為 Exception 是 IOException 的父類,只能拋出 IOException 的子類異常。
在面向對象原則裏,重寫意味著可以重寫任何現有方法。實例如下:
class Animal{
public void move(){
System.out.println("動物可以移動");
}
}
class Dog extends Animal{
public void move(){
System.out.println("狗可以跑和走");
}
}
public class TestDog{
public static void main(String args[]){
Animal a = new Animal(); // Animal 對象
Animal b = new Dog(); // Dog 對象
a.move();// 執行 Animal 類的方法
b.move();//執行 Dog 類的方法
}
}
以上實例編譯運行結果如下:
動物可以移動
狗可以跑和走
思考以下例子:
class Animal{
public void move(){
System.out.println("動物可以移動");
}
}
class Dog extends Animal{
public void move(){
System.out.println("狗可以跑和走");
}
public void bark(){
System.out.println("狗可以吠叫");
}
}
public class TestDog{
public static void main(String args[]){
Animal a = new Animal(); // Animal 對象
Animal b = new Dog(); // Dog 對象
a.move();// 執行 Animal 類的方法
b.move();//執行 Dog 類的方法
b.bark();
}
}
該程序將拋出一個編譯錯誤,因為b的引用類型Animal沒有bark方法。
2.1.2 方法的重寫規則
- 參數列表必須完全與被重寫方法的相同;
- 返回類型必須完全與被重寫方法的返回類型相同;
- 訪問權限不能比父類中被重寫的方法的訪問權限更低。例如:如果父類的一個方法被聲明為public,那麽在子類中重寫該方法就不能聲明為protected。
- 父類的成員方法只能被它的子類重寫。
- 聲明為final的方法不能被重寫。
- 聲明為static的方法不能被重寫,但是能夠被再次聲明。如果父類中的方法為靜態的,而子類中的方法不是靜態的,但是兩個方法除了這一點外其他都滿足覆蓋條件,那麽會發生編譯錯誤。反之亦然。即使父類和子類中的方法都是靜態的,並且滿足覆蓋條件,但是仍然不會發生覆蓋,因為靜態方法是在編譯的時候把靜態方法和類的引用類型進行匹配。
- 子類和父類在同一個包中,那麽子類可以重寫父類所有方法,除了聲明為private和final的方法。
- 子類和父類不在同一個包中,那麽子類只能夠重寫父類的聲明為public和protected的非final方法。
- 重寫的方法能夠拋出任何非強制異常,無論被重寫的方法是否拋出異常。但是,重寫的方法不能拋出新的強制性異常,或者比被重寫方法聲明的更廣泛的強制性異常,反之則可以。
- 構造方法不能被重寫。
- 如果不能繼承一個方法,則不能重寫這個方法。
2.1.3 Super關鍵字的使用
當需要在子類中調用父類的被重寫方法時,要使用super關鍵字。
class Animal{
public void move(){
System.out.println("動物可以移動");
}
}
class Dog extends Animal{
public void move(){
super.move(); // 應用super類的方法
System.out.println("狗可以跑和走");
}
}
public class TestDog{
public static void main(String args[]){
Animal b = new Dog(); // Dog 對象
b.move(); //執行 Dog類的方法
}
}
2.2 重載(Overload)
重載(overloading) 是在一個類裏面,方法名字相同,而參數不同。返回類型可以相同也可以不同。
每個重載的方法(或者構造函數)都必須有一個獨一無二的參數類型列表。
最常用的地方就是構造器的重載。
重載規則:
- 被重載的方法必須改變參數列表(參數個數或類型或順序不一樣);
- 被重載的方法可以改變返回類型;
- 被重載的方法可以改變訪問修飾符;
- 被重載的方法可以聲明新的或更廣的檢查異常;
- 方法能夠在同一個類中或者在一個子類中被重載。
- 無法以返回值類型作為重載函數的區分標準。
兩個問題需要註意:
- 什麽叫參數列表?參數列表又叫參數簽名,指三樣東西:參數的類型,參數的個數,參數的順序。這三者只要有一個不同就叫做參數列表不同。
- 重載關系只能發生在同一個類中嗎?非也。這時候你要深刻理解繼承,要知道一個子類所擁有的成員除了自己顯式寫出來的以外,還有父類遺傳下來的。所以子類中的某個方法和父類中繼承下來的方法也可以發生重載的關系。 大家在使用的時候要緊扣定義,看方法之間是否是重載關系,不用管方法的修飾符和返回類型以及拋出的異常,只看方法名和參數列表。而且要記住,構造器也可以重載。
實例:
public class Overloading {
public int test(){
System.out.println("test1");
return 1;
}
public void test(int a){
System.out.println("test2");
}
//以下兩個參數類型順序不同
public String test(int a,String s){
System.out.println("test3");
return "returntest3";
}
public String test(String s,int a){
System.out.println("test4");
return "returntest4";
}
public static void main(String[] args){
Overloading o = new Overloading();
System.out.println(o.test());
o.test(1);
System.out.println(o.test(1,"test3"));
System.out.println(o.test("test4",1));
}
}
2.3 覆蓋與重載的區別
| 區別點 | 重載方法 | 重寫方法 |
| ----- | --------- | ------ |
| 參數列表 | 必須修改 | 一定不能修改 |
| 返回類型 | 可以修改 | 一定不能修改 |
| 異常 | 可以修改 | 可以減少或刪除,一定不能拋出新的或者更廣的異常 |
| 訪問 | 可以修改 | 一定不能做更嚴格的限制(可以降低限制) |
總結:
方法的重寫(Overriding)和重載(Overloading)是java多態性的不同表現,重寫是父類與子類之間多態性的一種表現,重載可以理解成多態的具體表現形式。
3. 多態
3.1 多態的概念
多態是同一個行為具有多個不同表現形式或形態的能力。
多態就是同一個接口,使用不同的實例而執行不同操作
多態性是對象多種表現形式的體現。
多態存在的三個必要條件
- 繼承
- 重寫
- 父類引用指向子類對象
Parent p = new Child();
3.2 多態的示例
當使用多態方式調用方法時,首先檢查父類中是否有該方法,如果沒有,則編譯錯誤;如果有,再去調用子類的同名方法。
多態的好處:可以使程序有良好的擴展,並可以對所有類的對象進行通用處理。
public class Test {
public static void main(String[] args) {
show(new Cat()); // 以 Cat 對象調用 show 方法
show(new Dog()); // 以 Dog 對象調用 show 方法
Animal a = new Cat(); // 向上轉型
a.eat(); // 調用的是 Cat 的 eat
Cat c = (Cat)a; // 向下轉型
c.work(); // 調用的是 Cat 的 catchMouse
}
public static void show(Animal a) {
a.eat();
// 類型判斷
if (a instanceof Cat) { // 貓做的事情
Cat c = (Cat)a;
c.work();
} else if (a instanceof Dog) { // 狗做的事情
Dog c = (Dog)a;
c.work();
}
}
}
abstract class Animal {
abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃魚");
}
public void work() {
System.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨頭");
}
public void work() {
System.out.println("看家");
}
}
輸出結果:
吃魚
抓老鼠
吃骨頭
看家
吃魚
抓老鼠
3.3 虛方法
當子類對象調用重寫的方法時,調用的是子類的方法,而不是父類中被重寫的方法。
要想調用父類中被重寫的方法,則必須使用關鍵字super。
/* 文件名 : Employee.java */
public class Employee {
private String name;
private String address;
private int number;
public Employee(String name, String address, int number) {
System.out.println("Employee 構造函數");
this.name = name;
this.address = address;
this.number = number;
}
public void mailCheck() {
System.out.println("郵寄支票給: " + this.name
+ " " + this.address);
}
public String toString() {
return name + " " + address + " " + number;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
public void setAddress(String newAddress) {
address = newAddress;
}
public int getNumber() {
return number;
}
}
假設下面的類繼承Employee類:
/* 文件名 : Salary.java */
public class Salary extends Employee
{
private double salary; // 全年工資
public Salary(String name, String address, int number, double salary) {
super(name, address, number);
setSalary(salary);
}
public void mailCheck() {
System.out.println("Salary 類的 mailCheck 方法 ");
System.out.println("郵寄支票給:" + getName()
+ " ,工資為:" + salary);
}
public double getSalary() {
return salary;
}
public void setSalary(double newSalary) {
if(newSalary >= 0.0) {
salary = newSalary;
}
}
public double computePay() {
System.out.println("計算工資,付給:" + getName());
return salary/52;
}
}
輸出結果:
/* 文件名 : VirtualDemo.java */
public class VirtualDemo {
public static void main(String [] args) {
Salary s = new Salary("員工 A", "北京", 3, 3600.00);
Employee e = new Salary("員工 B", "上海", 2, 2400.00);
System.out.println("使用 Salary 的引用調用 mailCheck -- ");
s.mailCheck();
System.out.println("\n使用 Employee 的引用調用 mailCheck--");
e.mailCheck();
}
}
運行結果如下:
Employee 構造函數
Employee 構造函數
使用 Salary 的引用調用 mailCheck --
Salary 類的 mailCheck 方法
郵寄支票給:員工 A ,工資為:3600.0
使用 Employee 的引用調用 mailCheck--
Salary 類的 mailCheck 方法
郵寄支票給:員工 B ,工資為:2400.0
例子解析
- 實例中,實例化了兩個 Salary 對象:一個使用 Salary 引用 s,另一個使用 Employee 引用 e。
- 當調用 s.mailCheck() 時,編譯器在編譯時會在 Salary 類中找到 mailCheck(),執行過程 JVM 就調用 Salary 類的 mailCheck()。
- 因為 e 是 Employee 的引用,所以調用 e 的 mailCheck() 方法時,編譯器會去 Employee 類查找 mailCheck() 方法 。
- 在編譯的時候,編譯器使用 Employee 類中的 mailCheck() 方法驗證該語句, 但是在運行的時候,Java虛擬機(JVM)調用的是 Salary 類中的 mailCheck() 方法。
以上整個過程被稱為虛擬方法調用,該方法被稱為虛擬方法。
Java中所有的方法都能以這種方式表現,因此,重寫的方法能在運行時調用,不管編譯的時候源代碼中引用變量是什麽數據類型。
3.4 多態的實現方式
- 重寫
- 接口
- 生活中的接口最具代表性的就是插座,例如一個三接頭的插頭都能接在三孔插座中,因為這個是每個國家都有各自規定的接口規則,有可能到國外就不行,那是因為國外自己定義的接口類型。
- java中的接口類似於生活中的接口,就是一些方法特征的集合,但沒有方法的實現。具體可以看 java接口 這一章節的內容。
- 抽象類和抽象方法
3.5 多態的總結
對於多態,可以總結以下幾點:
- 使用父類類型的引用指向子類的對象;
- 該引用只能調用父類中定義的方法和變量;
- 如果子類中重寫了父類中的一個方法,那麽在調用這個方法的時候,將會調用子類中的這個方法;(動態連接、動態調用);
- 變量不能被重寫(覆蓋),"重寫"的概念只針對方法,如果在子類中"重寫"了父類中的變量,那麽在編譯時會報錯。
8.繼承、覆蓋、重載與多態