Java基礎之剖析面向對象的三大特性:封裝、繼承、多態

封裝

封裝也被稱爲數據隱藏,在面向對象程序設計中,是將對象特有的實例域(也就是對象的屬性)隱藏私有化, 外界不能直接訪問到對象的屬性,只能通過對象提供的屬性方法訪問。舉個簡單的例子:

如下代碼:一個Girl類,有三個私有屬性,姓名、年齡、體重,別的人(類)是無法獲取這三個屬性的,但是提供了三個set方法,可以設置Girl對象的姓名年齡、體重和身高。但是對於一個女生來說,體重和年齡是不能隨便說出來的,所以並沒有提供對應的get方法,所以是無法獲取體重和年齡的。

package cn.test;

public class Girl {

    //姓名
    private String nane;
    //年齡
    private String age;
    //體重
    private String weight;

    public void setNane(String nane) {
        this.nane = nane;
    }

    public void setAge(String age) {
        this.age = age;
    }

    public void setWeight(String weight) {
        this.weight = weight;
    }

    public String getNane() {
        return nane;
    }
}

對比:

如果代碼這麼寫的話,那女生的身高和年齡,任何人都可以知道(任何類都可以訪問),而且也不符合javaBean的開發規範。

package cn.test;

public class Girl {

    //姓名
    public String nane;
    //年齡
    public String age;
    //體重
    public String weight;
     
}

 封裝的真正內涵也就是把該隱藏的隱藏起來,該暴露的暴露出來,實現封裝的好處:

  1. 隱藏類的實現細節:只提供暴露方法,避免了代碼之間的亂引用,提高了重用性和可靠性;
  2. 設置默認屬性:可以設置屬性的默認值
  3. 可以數據進行校驗,保證信息的完整性和合理性:加入屬性校驗,不合理的輸入和訪問直接拒絕掉,比如年齡是500歲;
  4. 利用修改,提高代碼的可維護性:修改屬性的字段類型時,只需要同步修改get和set方法。

Java的封裝是通過訪問控制符來控制的,一張圖表就可以理解明白: 

  private defalut protected public
同一個類
同一個包  
子類中    
全局範圍內      

 

在這裏需要注意的是類的屬性一般都要定義爲私有屬性也就是private,當然常量除外;在類中的有些方法起到輔助作用,但是不想被其他類調用的話,也應該使用private修飾;如果在一個父類中,方法需要被子類重寫,但是又不想讓被其他類調用,應該使用protecqted修飾;還有就是其他類可以任意調用的話,就可以用public修飾。

繼承

Java只支持單繼承,所有一個類只能同時擁有一個父類。繼承描述的是一個"is -a"關係,如果A繼承了B,可以描述爲“A是B”。繼承有三個特點:

  1. 子類擁有父類對象的所有屬性和方法,父類的私有屬性雖然無法訪問,但是擁有;
  2. 子類可以擁有自己的屬性和方法,也就是可以擴展父類;
  3. 子類可以用自己的方式實現父類的方法(也就是重載和重寫)。

 舉個例子:

下面代碼中提供了三個類,一個動物類是父類,兩個子類人類和黑猩猩類,還有一個測試類。父類中,還有三個屬性:年齡、體重、身高,兩個動作特性:喫和喝;人類的類中,有自己的屬性:語言,自己的動作特性:駕駛,這就是對父類做了擴展,還重寫了父類的喫的特性。而黑猩猩類中只是重寫了父類喫的特性。在測試類中,發現,聲明子類People時,子類可以調用父類的操作屬性的方法,同時還可以調用父類的動作特性,說明子類擁有父類的所有屬性和方法,及時屬性是私有的;還能看到People調用自己重寫父類的方法,但是方法的具體實現和父類卻不一樣。

/**
 * 動物的公共屬性
 */
public class Animal {
    private String age;//年齡
    private String weight;//體重
    private String height;//身高
    /**
     * 喫的技能
     * @param str
     */
    public void eat(String str){
        System.out.print("喫:" +str);
    }
    /**
     * 喝的技能
     * @param str
     */
    public void drink(String str){
        System.out.print("喝:" +str);
    }

/**
 * 人類
 * 除了保持動物的基本特徵,
 * 還有自己區別動物的地方
 * 比如開車:
 */
public class People extends Animal{
    /**
     * 獨有的屬性
     * 語言
     */
    private String language;
    /**
     * 獨特的特性開車
     * @param car
     */
    public void drive(String car){
        System.out.print("駕駛:" +car);
    }

    /**
     * 重寫父類方法eat
     * @param food
     */
    @Override
    public  void eat(String food){
        System.out.print("在座位上喫 :" +food);
    }

/**
 * 黑猩猩
 * 保持動物的特徵
 */
public class Chimpanzee extends Animal{
    /**
     * 重寫父類方法eat
     * @param food
     */
    @Override
    public  void eat(String food){
        System.out.print("在樹上喫 :" +food);
    }
}

//測試類
public class Test {
    public static void main(String[] args) {
        People people = new People();
        people.setAge("23");
        people.setLanguage("漢語");
        people.eat("肯德基");
        people.drive("小轎車");
    }
}

關於構造器:

構造器是不能繼承的,但是Java關於構造器有明確的規範:如果子類的構造器沒有顯示的調用父類的構造器,則將會自動調用默認無參的構造器(類本身會有默認一個無參構造器),如果父類中沒有不帶參數的構造器,並且子類的構造器沒有顯示的調用父類構造器的話,那麼是無法通過編譯的。這也說明了繼承強耦合的特點(構造器不支持重寫,支持重載)。

通過super顯示調用父類構造器,回憶下super和this的作用:super有兩個用途:1.調用父類的方法,2。調用父類構造器;this:1.引用隱式參數 2.調用其他構造器。super和this的作用域都是在子類中,但是調用對象不同,super調用對象是父類,this調用對象是子類。

來看下例子,給父類Animal和子類Peopleg各增加一個顯示的無參構造方法

public class Animal {

    private String age;//年齡
    private String weight;//體重
    private String height;//身高

    public Animal( ){
        System.out.print("父類初始化。。。"+"\n");
    }
public class People extends Animal{

    public People( ){
        System.out.print("子類初始化。。。"+"\n");
    }

 再執行Test類中的main方法,看打印結果可以看出,父類的加載是在子類加載之前。

父類初始化。。。
子類初始化。。。
在座子上喫 :肯德基
駕駛:小轎車

當然了爲了更加明確基礎關係,你可以在父類的方法上將方法修飾爲 protected,這樣一來,非同包下的非子類就不能任意父類的中的方法,可以使繼承關係更加穩健。

聊聊向上轉型:

向上轉型就是子類轉型成父類,在繼承圖上向上移動,因此被稱爲向上轉型,向上轉型轉型最直觀的遍歷就是提高了代碼的複用性,不用每增加一個子類就新增一套方法,只要堅守住該類是父類的子類就可以執行調用,這也更能體現出兩個類之間的關係。

比如給父類增加一個getInfo方法,入參爲父類本身;修改一下測試類,People和Chimpanzee分別調用這個方法並傳入本身

    /**
     * 信息
     * @param animal
     */
    protected  void getInfo(Animal animal){
        System.out.print("年齡"+animal.age+"身高"+animal.height+"\n");
    }

//測試類
public class Test {
    public static void main(String[] args) {
        People people = new People();
        people.setAge("23");
        people.setHeight("185");
        people.getInfo(people);
        Chimpanzee chimpanzee = new Chimpanzee();
        chimpanzee.setAge("5");
        chimpanzee.setHeight("193");
        chimpanzee.getInfo(chimpanzee);
    }
}

打印結果:在父類只新增了一個方法,但是任意子類都可以調用

年齡:23身高:185

年齡:5身高:193

繼承的缺點 

一直在強調繼承的優點,雖然繼承在編碼和設計上帶來了很大的便利,但是也存在的響應的缺點:

  1. 繼承是“is-a”的一種關係,是一種強耦合的關係,父類發生變化時,子類也需要跟着變化,所有多變或者不穩定的業務場景不建議使用繼承
  2. 繼承打破了封裝的嚴謹性,封裝的意義本身就是爲了保證一個類的安全性和隱藏性,不過繼承恰恰打破了這種平衡。

 那什麼時候需要使用繼承,什麼時候不能用繼承,還需要根據場景而定,這裏只是建議慎用。

多態

多態就是指程序中定義的引用變量所指向的具體類型和通過該引用變量發出的方法調用在編程時並不確定,而是在程序運行期間才確定,即一個引用變量倒底會指向哪個類的實例對象,該引用變量發出的方法調用到底是哪個類中實現的方法,必須在由程序運行期間才能決定。

實現多態有三個必備的條件:

  1. 繼承
  2. 方法重寫
  3. 向上轉型 

多態有兩種實現方式,一種是基於繼承,一種是基於接口實現

我們修改下上面的例子,已經子類People重寫了父類Animal的中的eat方法

public class Test {
    public static void main(String[] args) {
        Animal people = new People();
        people.eat("肯德基");
    }

輸出結果: 

在座子上喫 :肯德基

 上面的例子只是簡單應用,來看一個經典的例子:

父類爲A,A中show(D obj)和show(A obj)是重載關係;B繼承了A,B的show(B obj)重載了A的show(D obj)或者show(A obj)方法,同時也show(A obj)重寫了A的show(A obj),,B中show(B obj)和show(A obj)也是重載關係;C、D都繼承了B,是B的子類。

public class A {
    public String show(D obj) {
        return ("A and D");
    }

    public String show(A obj) {
        return ("A and A");
    }

}

public class B extends A{
    public String show(B obj){
        return ("B and B");
    }

    public String show(A obj){
        return ("B and A");
    }

}

public class C extends B{
    
}

public class D extends B{
    
}

public class Test2 {
    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new B();
        B b = new B();
        C c = new C();
        D d = new D();

        System.out.println("1--" + a1.show(b));
        System.out.println("2--" + a1.show(c));
        System.out.println("3--" + a1.show(d));
        System.out.println("4--" + a2.show(b));
        System.out.println("5--" + a2.show(c));
        System.out.println("6--" + a2.show(d));
        System.out.println("7--" + b.show(b));
        System.out.println("8--" + b.show(c));
        System.out.println("9--" + b.show(d));
        System.out.println("10--" + b.show(a1));

    }
}


輸出結果:

1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D
10--B and A

分析代碼前先來回顧下重載和重寫

重載和重寫是在多態特性的最基本的特徵,那如何確定正確的目標方法?這裏又引出一個新的概念,將一個方法調用同一個方法主體關聯起來被稱爲綁定,綁定又分爲前期綁定和後期綁定(也就是動態綁定、運行時綁定),也就是隻有程序運行期間才能確定一個引用變量到底會指向哪個類的實現方法,這也就體現了多態性。前期綁定和後期綁定也被稱爲靜態分派和動態分派。靜態綁定典型應用就是重載,而動態綁定的典型應用是重寫。

使用哪個重載版本:

首先選定對應參數類型的方法,當重載方法中沒有對應參數類型的方法,那將在繼承的關係中從下往上開始搜索,越接近上層優先級越低

例如:從1、2、3可以看出,當傳輸入參數有對應類型時,3執行的結果就是對應的方法,但是1,2沒有對應的參數時,會向上轉型爲父類,查找對應的方法。

調用哪個重寫方法

聲明子類,調用方法時,先查子類沒有該方法,如果沒有子類沒有該方法,在繼承的關係中從下往上開始搜索,依次查找;當沒有對應參數類型方法時,則參數向上轉型爲父類再依次查找;如果子類重寫父類方法時,優先調用子類重寫父類的方法;

例如:7.8.9.10中,重點說下8,9,10。8的調用邏輯中,B沒有show(C obj),B的父類A也沒有,則C向上轉型爲B,在B類中找到對應方法,9的調用邏輯中,B中沒有show(D obj),而B的父類A中存在;10的調用邏輯中,B類和父類A都存在show(A obj)方法,優先調用B類重寫A類的方法。

父類聲明子類實現

以優先查找父類方法中,如果子類重寫父類方法,則優先調用子類方法

1 A a2 = new B();
2   a2 = new C();

如代碼1,根據代碼的字面意思,把"A"被稱爲靜態類型或者就外觀類型,而後面的“B”則是變量的實際類型,兩者在程序中都可以發生變化,靜態類型類型可以僅發生在使用時,變量本身不會改變,並且時可知的,而實際變量的變化只有在運行期期纔可以確定(這裏只的虛擬機)。

例如:4,5,6.在4,5的調用邏輯中,首先在A中查找,如果A中沒有找到方法,參數向上轉型爲A,而子類重寫了show(A objf)方法,則調用B類中的方法返回找到方法返回;6中在A中找到方法,直接返回。

參考博客:http://blog.csdn.net/thinkGhoster/archive/2008/04/19/2307001.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章