Java的繼承(深刻版)

前言

前文咱們瞭解了面向對象的三大特徵:封裝、繼承、多態。html

那麼在Java中是如何展示繼承的特性呢?對於子類繼承於父類時,又有什麼限制呢?數據結構

在此解答這些問題以後,咱們再瞭解下類的加載過程,加深對繼承的瞭解。ide

(若文章有不正之處,或難以理解的地方,請多多諒解,歡迎指正)this

引入繼承

假如咱們有兩個類:生物類、貓類。spa

生物類:.net

class Animal{  
    private String name;  
    public void setName(String name){  
        this.name = name;  
    }  
    public String getName(){  
        return this.name;  
    }   
}

貓類:code

class Cat{  
    private String name;  
    private String sound;  
    public void setName(String name){  
        this.name = name;  
    }  
    public void setSound(String sound){  
        this.sound = sound;  
    }  
    public String getName(){  
        return this.name;  
    }   
    public String getSound(){  
        return this.sound;  
    }  
}

咱們知道,貓也是屬於生物中的一種,生物有的屬性和行爲,貓按理來講也是有的。但此時沒有繼承的概念,那麼代碼就得不到複用,長期發展,代碼冗餘、維護困難且開發者的工做量也很是大。htm

繼承的概念

繼承就是子類繼承父類的特徵和行爲,使得子類對象(實例)具備父類的實例域和方法,或子類從父類繼承方法,使得子類具備父類相同的行爲。

簡單來講,子類能吸取父類已有的屬性和行爲。除此以外,子類還能夠擴展自身功能。子類又被稱爲派生類,父類被稱爲超類。對象

在Java中,若是要實現繼承的關係,可使用以下語法:blog

class 子類 extends 父類{}

繼承的基本實現

繼承的基本實現以下:

class Animal{  
    private String name;  
    public void setName(String name){  
        this.name = name;  
    }  
    public String getName(){  
        return this.name;  
    }   
}  
class Cat extends Animal{}  
​  
public class Test{  
    public static void main(String[] args){  
        Cat cat = new Cat();  
        cat.setName("貓");  
        System.out.println(cat.getName());  
    }  
}

運行結果爲:

咱們能夠看出,子類能夠在不擴展操做的狀況下,使用父類的屬性和功能

子類擴充父類

繼承的基本實現以下:

class Animal{  
    private String name;  
    public void setName(String name){  
        this.name = name;  
    }  
    public String getName(){  
        return this.name;  
    }   
}  
class Cat extends Animal{  
    private String sound;  
    public void setSound(String sound){  
        this.sound = sound;  
    }  
    public String getSound(){  
        return this.sound;  
    }   
}  
​  
public class Test{  
    public static void main(String\[\] args){  
        Cat cat = new Cat();  
        cat.setName("NYforSF")  
        cat.setSound("我不是你最愛的小甜甜了嗎?");  
        System.out.println(cat.getName()+":"+cat.getSound()); 
    }  
}

運行結果爲:

NYforSF:我不是你最愛的小甜甜了嗎?

咱們能夠看出,子類在父類的基礎上進行了擴展,並且對於父類來講,子類定義的範圍更爲具體。也就是說,子類是將父類具體化的一種手段

總結一下,Java中的繼承利用子類和父類的關係,能夠實現代碼複用,子類還能夠根據需求擴展功能。

繼承的限制

1. 子類只能繼承一個父類

爲何子類不能多繼承?舉個栗子。

class ACat{  
 public void mewo(){...}  
}  
class BCat{  
 public void mewo(){...}  
}  
class CCat extends ACat, BCat{  
 @Override  
 public void mewo(){...?}  //提問:這裏的mewo()是繼承自哪一個類?  
}

雖然說Java只支持單繼承,可是不反對多層繼承呀!

class ACat{}  
class BCat extends ACat{}  
class CCat extends BCat{}

這樣,BCat就繼承了ACat全部的方法,而CCat繼承了ACat、BCat全部的方法,實際上CCat是ACat的子(孫)類,是BCat的子類。

總結一下,子類雖然不支持多重繼承,只能單繼承,可是能夠多層繼承

2. private修飾不可直接訪問,final修飾不可修改

private修飾

對於子類來講,父類中用private修飾的屬性對其隱藏的,但若是提供了這個變量的setter/getter接口,仍是可以訪問和修改這個變量的。

class ACat {  
    private String sound = "meow";  
    public String getSound() {  
        return sound;  
    }  
    public void setSound(String sound) {  
        this.sound = sound;  
    }  
}  
​
class BCat extends ACat {  
}  
​  
public class Test {  
    public static void main(String[] args) {  
        BCat b = new BCat();  
        b.setSound("我不是你最愛的小甜甜了嗎?");  
        System.out.println(b.getSound());  
    }  
}

final修飾

父類已經定義好的final修飾變量(方法也同樣),子類能夠訪問這個屬性(或方法),可是不能對其進行更改

class ACat {  
    final String sound = "你是我最愛的小甜甜";  
    public String getSound() {  
        return sound;  
    }  
    public void setSound(String sound){  
        this.sound = sound;  //這句執行不了,會報錯的  
    }  
}  
​  
class BCat extends ACat {  
}

總結一下,用private修飾的變量能夠經過getter/setter接口來操做,final修飾的變量就只能訪問,不能更改

3. 實例化子類時默認先調用父類的構造方法

在實例化子類對象時,會調用父類的構造方法對屬性進行初始化,以後再調用子類的構造方法。

class A {  
    public A(){  
        System.out.println("我不是你最愛的小甜甜了嗎?");  
    }  
    public A(String q){  
        System.out.println(q);  
    }  
}  
​  
class B extends A {  
    public B(){  
        System.out.println("你是個好姑娘");  
    }  
}  
​  
public class Test {  
    public static void main(String[] args) {  
        B b = new B();  
    }  
}

運行結果爲:

我不是你最愛的小甜甜了嗎?  
你是個好姑娘

從結果咱們能夠知道,在實例化子類時,會默認先調用父類中無參構造方法,而後再調動子類的構造方法。

那麼怎麼調用父類帶參的構造方法呢?只要在子類構造方法的第一行調用super()方法就好。

class A {  
    public A(String q){  
        System.out.println(q);  
    }  
}  
​  
class B extends A {  
    public B(){  
        super("我是你的小甜甜?");  
        System.out.println("你是個好姑娘");  
    }  
}  
​  
public class Test {  
    public static void main(String\[\] args) {  
        B b = new B();  
    }  
}

運行結果爲:

我是你的小甜甜?
你是個好姑娘

在子類實例化時,默認調用的是父類的無參構造方法,而若是沒有父類無參構造方法,則子類必須經過super()來調用父類的有參構造方法,且super()方法必須在子類構造方法的首行

總結一下,Java繼承中有三種繼承限制,分別是子類只能單繼承、父類中private修飾的變量不能顯式訪問和final修飾的變量不能改變,以及實例化子類一定會先調用父類的構造方法,以後才調用子類的構造方法。

類是怎麼加載的?

(此處只是粗略介紹類加載的過程,想了解更多可參考《深刻理解Java虛擬機》)

類加載過程包括三個大步驟:加載、鏈接、初始化

這三個步驟的開始時間仍然保持着固定的前後順序,可是進行和完成的進度就不必定是這樣的順序了。
簡:類加載過程.jpg

  1. 加載:虛擬機經過這個類的全限定名來獲取這個類的二進制字節流,而後在字節流中提取出這個類的結構數據,並轉換成這個類在方法區(存儲類結構)的運行時數據結構
  2. 驗證:先驗證這字節流是否符合Class文件格式的規範,而後檢查這個類的其父類中數據是否存在衝突(如這個類的父類是否繼承被final修飾的類),接着對這個類內的方法體進行檢查,若是都沒問題了,那就把以前的符號引用換成直接引用
  3. 準備:爲類變量(static修飾的變量)分配內存(方法區)並設置類變量初始值,而這裏的初始值是指這個數據類型的零值,如int的初始值是0;
  4. 解析:在Class文件加載過程當中,會將Class文件中的標識方法、接口的常量放進常量池中,而這些常量對於虛擬機來講,就是符號引用。此階段就是針對類、接口、字段等7類符號引用,轉換成直接指向目標的句柄——直接引用
  5. 初始化:這階段是執行static代碼塊和類構造器的過程,有小夥伴可能會疑惑類構造器不是默認static的嗎?詳情請看這個博客:再議Java中的static關鍵字

總結一下,類加載的過程當中,首先會對Class文件中的類提取並轉換成運行時數據結構,而後對類的父類和這個類的數據信息進行檢驗以後,爲類中的類變量分配內存而且設置初始值,接着將Class文件中與這個類有關的符號引用轉換成直接引用,最後再執行類構造器。

並且咱們能夠從第二步看出,在加載類的時候,會先去檢查這個類的父類的信息,而後再檢查這個類的方法體,也就是說,在加載類的時候,會先去加載它的父類

結語

初學Java的時候知道這些概念,但只是淺嘗而止。如今跟着Hollis大佬的《Java 工程師成神之路!》,從新回顧這些知識的時候,發現若是本身只是像之前同樣片面瞭解,那豈不是沒有成長?因此在寫文章的過程當中,嘗試寫得更加深刻且儘可能易懂。固然,本人水平有限,若有不正之處,歡迎指正。

若是本文對你有幫助,請點一個贊吧,這對我來講是最大的動力~

參考資料:

Java 繼承(extends)詳解

相關文章
相關標籤/搜索