1. 程式人生 > >【面試題】Java三大特性封裝繼承多型總結

【面試題】Java三大特性封裝繼承多型總結

本文內容總結於多篇部落格,參考到的地方在文中都會一一列出

http://www.cnblogs.com/ibalintha/p/3897821.html

 

 

1.封裝

 

封裝的定義:

  • 首先是抽象,把事物抽象成一個類,其次才是封裝,將事物擁有的屬性和動作隱藏起來,只保留特定的方法與外界聯絡

為什麼需要封裝:

  • 封裝符合面向物件設計原則的第一條:單一性原則,一個類把自己該做的事情封裝起來,而不是暴露給其他類去處理,當內部的邏輯發生變化時,外部呼叫不用因此而修改,他們只調用開放的介面,而不用去關心內部的實現

舉例:

public class Human
{
    private int age;
    private String name;
 
    public int getAge()
    {
        return age;
    }
 
    public void setAge( int age ) throws Exception
    {
        //封裝age的檢驗邏輯,而不是暴露給每個呼叫者去處理
        if( age > 120 )
        {
            throw new Exception( "Invalid value of age" );
        }
        this.age = age;
    }
 
    public String getName()
    {
        return name;
    }
 
    public void setName( String name )
    {
        this.name = name;
    }
}

2. 繼承

Java的類可以分為三類:

  • 類:使用class定義,沒有抽象方法
  • 抽象類:使用abstract class定義,可以有也可以沒有抽象方法
  • 介面:使用inerface定義,只能有抽象方法

在這三個型別之間存在如下關係:

  • 類可以extends:類、抽象類(必須實現所有抽象方法),但只能extends一個,可以implements多個介面(必須實現所有介面方法)
  • 抽象類可以extends:類,抽象類(可全部、部分、或者完全不實現父類抽象方法),可以implements多個介面(可全部、部分、或者完全不實現介面方法)
  • 介面能extends多個介面(//這裡原博主中有錯誤,已經修正)

繼承以後子類可以得到什麼:

  • 子類擁有父類非private的屬性和方法(//這裡其實我是持懷疑態度,之前聽到過一種說法,子類繼承了父類所有的成員,也包括private修飾的,只是 不能夠呼叫罷了,如果有人知道,歡迎留言討論)
  • 子類可以新增自己的方法和屬性,即對父類進行擴充套件
  • 子類可以重新定義父類的方法,即多型裡面的覆蓋,後面會詳述

關於建構函式:

  • 建構函式不能被繼承,子類可以通過super()顯示呼叫父類的建構函式
  • 建立子類時,編譯器會自動呼叫父類的 無參建構函式
  • 如果父類沒有定義無參建構函式,子類必須在建構函式的第一行程式碼使用super()顯示呼叫

類預設擁有無參建構函式,如果定義了其他有參建構函式,則無參函式失效,所以父類沒有定義無參建構函式,不是指父類沒有寫無參建構函式。看下面的例子,父類為Human,子類為Programmer。

public class Human
{
    //定義了有參建構函式,預設無參建構函式失效
    public Human(String name)
    {
    }
}
public class Programmer
    extends Human
{
    public Programmer()
    {
        //如不顯示呼叫,編譯器會出現如下錯誤
        //Implicit super constructor Human() is undefined. Must explicitly invoke another constructor
        super( "x" );
    }
}

 

為什麼需要繼承:

  • 程式碼重用是一點,最重要的還是所謂想上轉型,即父類的引用變數可以指向子類物件,這是Java面向物件最重要特性多型的基礎

 

 

3. 多型

 

在瞭解多型之前,首先需要知道方法的唯一性標識即什麼是相同/不同的方法:

  • 一個方法可以由:修飾符如public、static+返回值+方法名+引數+throw的異常 5部分構成
  • 其中只有方法名和引數是唯一性標識,意即只要方法名和引數相同那他們就是相同的方法
  • 所謂引數相同,是指引數的個數,型別,順序一致,其中任何一項不同都是不同的方法

何謂過載:

  • 過載是指一個類裡面(包括父類的方法)存在方法名相同,但是引數不一樣的方法,引數不一樣可以是不同的引數個數、型別或順序
  • 如果僅僅是修飾符、返回值、throw的異常 不同,那這是2個相同的方法,編譯都通不過,更不要說過載了

 

//過載的例子
public class Programmer
    extends Human
{
    public void coding() throws Exception
    {
 
    }
 
    public void coding( String langType )
    {
 
    }
 
    public String coding( String langType, String project )
    {
        return "";
    }
}
//這不是過載,而是三個相同的方法,編譯報錯
public class Programmer
    extends Human
{
    public void coding() throws Exception
    {
 
    }
 
    public void coding()
    {
 
    }
 
    public String coding()
    {
        return "";
    }
}

 

何謂覆蓋/重寫:

  • 覆蓋描述存在繼承關係時子類的一種行為
  • 子類中存在和父類相同的方法即為覆蓋,何謂相同方法請牢記前面的描述,方法名和引數相同,包括引數個數、型別、順序

 

public class Human
{
    public void coding( String langType )
    {
 
    }
}
public class Programmer
    extends Human
{
    //此方法為覆蓋/重寫
    public void coding( String langType )
    {
 
    }
 
    //此方法為上面方法的過載
    public void coding( String langType, String project )
    {
 
    }
}

 

覆蓋/重寫的規則:(兩同兩小一大原則)

  • 子類不能覆蓋父類private的方法,private對子類不可見,如果子類定義了一個和父類private方法相同的方法,實為新增方法
  • 重寫方法的修飾符一定要大於被重寫方法的修飾符(public > protected > default > private)
  • 重寫丟擲的異常需與父類相同或是父類異常的子類,或者重寫方法乾脆不寫throws
  • 重寫方法的返回值必須與被重寫方法一致,否則編譯報錯
  • 靜態方法不能被重寫為非靜態方法,否則編譯出錯

理解了上述知識點,是時候定義多型了:

  • 多型可以說是“一個介面,多種實現”或者說是父類的引用變數可以指向子類的例項,被引用物件的型別決定呼叫誰的方法,但這個方法必須在父類中定義
  • 多型可以分為兩種型別:編譯時多型(方法的過載)和執行時多型(繼承時方法的重寫),編譯時多型很好理解,後述內容針對執行時多型
  • 執行時多型依賴於繼承、重寫和向上轉型

上例子:

class Human
{
    public void showName()
    {
        System.out.println( "I am Human" );
    }
}
 
//繼承關係
class Doctor
    extends Human
{
    //方法重寫
    public void showName()
    {
        System.out.println( "I am Doctor" );
    }
}
 
class Programmer
    extends Human
{
    public void showName()
    {
        System.out.println( "I am Programmer" );
    }
}
 
public class Test
{
    //向上轉型
    public Human humanFactory( String humanType )
    {
        if( "doctor".equals( humanType ) )
        {
            return new Doctor();
        }
        if( "programmer".equals( humanType ) )
        {
            return new Programmer();
        }
        return new Human();
    }
 
    public static void main( String args[] )
    {
        Test test = new Test();
        Human human = test.humanFactory( "doctor" );
        human.showName();//Output:I am Doctor
        human = test.humanFactory( "programmer" );
        human.showName();//Output:I am Programmer
        //一個介面的方法,表現出不同的形態,意即為多型也
    }
}

向上轉型的缺憾:

  • 只能呼叫父類中定義的屬性和方法,對於子類中的方法和屬性它就望塵莫及了,必須強轉成子類型別

總結概括:

  • 當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法,但是它仍然要根據繼承鏈中方法呼叫的優先順序來確認方法,該優先順序為:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)

稍微複雜的例子:

class Human {
 
    public void fun1() {
        System.out.println("Human fun1");
        fun2();
    }
 
    public void fun2() {
        System.out.println("Human fun2");
    }
}
 
class Programmer extends Human {
 
    public void fun1(String name) {
        System.out.println("Programmer's fun1");
    }
 
    public void fun2() {
        System.out.println("Programmer's fun2");
    }
}
 
public class Test {
    public static void main(String[] args) {
        Human human = new Programmer();
        human.fun1();
    }
    /*
     * Output:
     *  Human fun1
     *  Programmer's fun2
     */
}
  • Programmer中的fun1(String name) 和Human中的fun1(),只是方法名相同但引數不一樣,所以是過載關係
  • Programmer中的fun2()和Human中的fun2()是相同的方法,即Programmer重寫了Human的fun2()方法
  • 把Programmer的物件向上轉型賦值個Human後,human.fun1()會呼叫父類中的fun1()方法,子類的fun1(String name)是不同的方法
  • 在human的fun1()中又呼叫了fun2()方法,該方法在Programmer中被重寫,實際呼叫的是被引用變數Programmer中的fun2()方法
package test;
 
class A {
    public void func() {
        System.out.println("func in A");
    }
}
 
class B extends A {
    public void func() {
        System.out.println("func in B");
    }
}
 
class C extends B {
    public void func() {
        System.out.println("func in B");
    }
}
 
public class Bar {
    public void test(A a) {
        a.func();
        System.out.println("test A in Bar");
    }
 
    public void test(C c) {
        c.func();
        System.out.println("test C in Bar");
    }
 
    public static void main(String[] args) {
        Bar bar = new Bar();
        A a = new A();
        B b = new B();
        C c = new C();
        bar.test(a);
        bar.test(b);
        bar.test(c);
        /*
            func in A
            test A in Bar
            func in B
            test A in Bar
            func in B
            test C in Bar
         */
    }
}

 最近看騰訊的一道多型面試題:(轉載自:http://blog.csdn.net/qq_27258799)

public class A {    
    public int a = 0;
    public void fun(){
        System.out.println("-----A-----");
    }
}
 
public class B extends A{
    public int a = 1;
    public void fun(){
        System.out.println("-----B-----");
    }
 
public static void main(String[] args){
        A classA = new B();     
        System.out.println(classA.a);
        classA.fun();
    }   
}

答案是: 

0
-----B-----

 

解析: 
java中變數不能重寫,可以按如下口訣記憶:

變數多型看左邊, 
方法多型看右邊, 
靜態多型看左邊。

 

 

這裡補充一下介面和抽象類的相關知識:(部分把內容轉載自:http://www.importnew.com/18780.html)

 

 

一.抽象類

 

在瞭解抽象類之前,先來了解一下抽象方法。抽象方法是一種特殊的方法:它只有宣告,而沒有具體的實現。抽象方法的宣告格式為:

abstract void  fun();


  下面要注意一個問題:在《Java程式設計思想》一書中,將抽象類定義為“包含抽象方法的類”,但是後面發現如果一個類不包含抽象方法,只是用abstract修飾的話也是抽象類。也就是說抽象類不一定必須含有抽象方法。個人覺得這個屬於鑽牛角尖的問題吧,因為如果一個抽象類不包含任何抽象方法,為何還要設計為抽象類?所以暫且記住這個概念吧,不必去深究為什麼。抽象方法必須用abstract關鍵字進行修飾。如果一個類含有抽象方法,則稱這個類為抽象類,抽象類必須在類前用abstract關鍵字修飾。因為抽象類中含有無具體實現的方法,所以不能用抽象類建立物件。

[public] abstract class  ClassName {

    abstract void  fun();

}


包含抽象方法的類稱為抽象類,但並不意味著抽象類中只能有抽象方法,它和普通類一樣,同樣可以擁有成員變數和普通的成員方法。注意,抽象類和普通類的主要有三點區別:從這裡可以看出,抽象類就是為了繼承而存在的,如果你定義了一個抽象類,卻不去繼承它,那麼等於白白建立了這個抽象類,因為你不能用它來做任何事情。對於一個父類,如果它的某個方法在父類中實現出來沒有任何意義,必須根據子類的實際需求來進行不同的實現,那麼就可以將這個方法宣告為abstract方法,此時這個類也就成為abstract類了。

1)抽象方法必須為public或者protected(因為如果為private,則不能被子類繼承,子類便無法實現該方法),預設情況下預設為public。

2)抽象類不能用來建立物件;

3)如果一個類繼承於一個抽象類,則子類必須實現父類的抽象方法。如果子類沒有實現父類的抽象方法,則必須將子類也定義為為abstract類。

在其他方面,抽象類和普通的類並沒有區別。

 

 

 

二.介面

 

介面,英文稱作interface,在軟體工程中,介面泛指供別人呼叫的方法或者函式。從這裡,我們可以體會到Java語言設計者的初衷,它是對行為的抽象。在Java中,定一個介面的形式如下:

 
[修飾符] interface 介面名 [extends 父介面名列表]{
 
	[public] [static] [final] 常量;
	[public] [abstract] 方法;
}
注意:“[ ]”中的內容是可選的。但是在編譯之後,會自動為常量加上public static final,同理會為方法自動加上public abstract

 

 

 

介面中可以含有 變數和方法。但是要注意,介面中的變數會被隱式地指定為public static final變數(並且只能是public static final變數,用private修飾會報編譯錯誤),而方法會被隱式地指定為public abstract方法且只能是public abstract方法(用其他關鍵字,比如private、protected、static、 final等修飾會報編譯錯誤),並且介面中所有的方法不能有具體的實現,也就是說,介面中的方法必須都是抽象方法。從這裡可以隱約看出介面和抽象類的區別,介面是一種極度抽象的型別,它比抽象類更加“抽象”,並且一般情況下不在介面中定義變數。

要讓一個類遵循某組特地的介面需要使用implements關鍵字,具體格式如下:

class ClassName implements Interface1,Interface2,[....]{

}


三.抽象類和介面的區別可以看出,允許一個類遵循多個特定的介面。如果一個非抽象類遵循了某個介面,就必須實現該介面中的所有方法。對於遵循某個介面的抽象類,可以不實現該介面中的抽象方法。

1.語法層面上的區別

1)抽象類可以提供成員方法的實現細節,而介面中只能存在public abstract 方法;

2)抽象類中的成員變數可以是各種型別的,而介面中的成員變數只能是public static final型別的;

3)介面中不能含有靜態程式碼塊以及靜態方法,而抽象類可以有靜態程式碼塊和靜態方法;

4)一個類只能繼承一個抽象類,而一個類卻可以實現多個介面。

2.設計層面上的區別

1)抽象類是對一種事物的抽象,即對類抽象,而介面是對行為的抽象。抽象類是對整個類整體進行抽象,包括屬性、行為,但是介面卻是對類區域性(行為)進行抽象。舉個簡單的例子,飛機和鳥是不同類的事物,但是它們都有一個共性,就是都會飛。那麼在設計的時候,可以將飛機設計為一個類Airplane,將鳥設計為一個類Bird,但是不能將 飛行 這個特性也設計為類,因此它只是一個行為特性,並不是對一類事物的抽象描述。此時可以將 飛行 設計為一個介面Fly,包含方法fly( ),然後Airplane和Bird分別根據自己的需要實現Fly這個介面。然後至於有不同種類的飛機,比如戰鬥機、民用飛機等直接繼承Airplane即可,對於鳥也是類似的,不同種類的鳥直接繼承Bird類即可。從這裡可以看出,繼承是一個 “是不是”的關係,而 介面 實現則是 “有沒有”的關係。如果一個類繼承了某個抽象類,則子類必定是抽象類的種類,而介面實現則是有沒有、具備不具備的關係,比如鳥是否能飛(或者是否具備飛行這個特點),能飛行則可以實現這個介面,不能飛行就不實現這個介面。

2)設計層面不同,抽象類作為很多子類的父類,它是一種模板式設計。而介面是一種行為規範,它是一種輻射式設計。什麼是模板式設計?最簡單例子,大家都用過ppt裡面的模板,如果用模板A設計了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它們的公共部分需要改動,則只需要改動模板A就可以了,不需要重新對ppt B和ppt C進行改動。而輻射式設計,比如某個電梯都裝了某種報警器,一旦要更新報警器,就必須全部更新。也就是說對於抽象類,如果需要新增新的方法,可以直接在抽象類中新增具體的實現,子類可以不進行變更;而對於介面則不行,如果介面進行了變更,則所有實現這個介面的類都必須進行相應的改動。

下面看一個網上流傳最廣泛的例子:門和警報的例子:門都有open( )和close( )兩個動作,此時我們可以定義通過抽象類和介面來定義這個抽象概念:

abstract class  Door {

    public abstract  void  open();

    public abstract  void  close();

}

或者:

 

interface Door {

    public abstract  void  open();

    public abstract  void  close();

}

 

 

但是現在如果我們需要門具有報警alarm( )的功能,那麼該如何實現?下面提供兩種思路:

1)將這三個功能都放在抽象類裡面,但是這樣一來所有繼承於這個抽象類的子類都具備了報警功能,但是有的門並不一定具備報警功能;

2)將這三個功能都放在接口裡面,需要用到報警功能的類就需要實現這個介面中的open( )和close( ),也許這個類根本就不具備open( )和close( )這兩個功能,比如火災報警器。

從這裡可以看出, Door的open() 、close()和alarm()根本就屬於兩個不同範疇內的行為,open()和close()屬於門本身固有的行為特性,而alarm()屬於延伸的附加行為。因此最好的解決辦法是單獨將報警設計為一個介面,包含alarm()行為,Door設計為單獨的一個抽象類,包含open和close兩種行為。再設計一個報警門繼承Door類和實現Alarm介面。

 

interface Alram {

    void alarm();

}



abstract class  Door {

    void open();

    void close();

}



class AlarmDoor extends Door implements Alarm {

    void oepn() {

      //....

    }

    void close() {

      //....

    }

    void alarm() {

      //....

    }

}