Java面試筆試之面向物件技術(二)
1.8 抽象類與繼承
1.8.1 繼承、抽象、介面的概念
①繼承指的是從已有的類中派生出新的類,新的類能吸收已有類的資料屬性和行為,並能擴充套件新的能力。在Java語言中,繼承是使用已存在的類的定義作為基礎建立新類的技術,新類的定義可以增加新的資料或新的功能,也可以使用父類的功能,但不能選擇性地繼承父類。類只允許單繼承,但是為了實現類似於C++語言中多繼承的特性,Java語言引入了介面的概念,雖然Java語言只允許繼承一個類,但是卻可以同時實現多個介面,因此也間接實現了多繼承。
②抽象方法是指在類中存在沒有方法體的方法,在Java語言中,當用abstract來修飾一個方法時,該方法就是抽象方法,因此抽象方法不能使用大括號{}括住(一旦有了大括號就代表有了方法體)。
③在Java語言中,介面是一系列方法的宣告,是一些方法特徵的集合,一個介面只有方法的特徵,沒有方法的實現,因此這些方法可以在不同的地方被不同的類實現,而這些實現可以具有不同的行為(功能)。外部介面只能被public和abstract這兩個關鍵字來修飾,而內部介面卻可以被private、protected和static修飾。
1.8.2 介面和抽象類的異同
.介面(interface)和抽象類(abstract class)是支援抽象類定義的兩種機制(注意,該句中前後兩個前後兩個抽象類的意義不一樣,前者表示的是一個實體,後者表示的是一個概念)。二者具有很大的相似性,甚至有時候是可以互換的。但同時,兩者也存在很大的區別。
具體而言,介面是公開的,裡面不能有私有的方法或變數,是用於讓別人使用的,而抽象類是可以有私有方法或私有變數的,如果一個類中包含抽象方法,那麼這個類就是抽象類。在Java語言中,可以通過把類或者類中的某些方法宣告為abstract(abstract只能修飾類或者方法,不能用來修飾屬性)來表示一個類是抽象類。介面就是指一系列方法的集合,介面中的所有方法都沒有方法體,在Java語言中,介面是通過interface關鍵字來實現的。
包含一個或多個抽象方法的類就必須被宣告為抽象類,抽象類可以宣告方法的存在而不去實現它,被宣告為抽象的方法不能含方法體。在抽象類的子類中,實現方法必須含有相同的或者更高的訪問級別(public>protected>default>private)。抽象類在使用的過程中不能被例項化,但是可以建立一個物件使其指向具體子類的一個例項。抽象類的子類為父類中所有的抽象方法提供具體的實現,否則它們也是抽象類。介面可以看做是抽象類的變體,介面中的所有方法都是抽象的,可以通過介面來間接地實現多重繼承。介面中的成員變數是static、final型別,由於抽象類可以包含部分方法的實現,所有在一些場合下抽象類比介面存在更多的優勢。
總而言之,介面和抽象類的異同總結如下:
相同點:
①都不能被例項化;
②介面的實現類或抽象類的子類都只有實現了介面或抽象類中的方法後才能被例項化;
不同點:
①介面只有定義,不能有方法實現,而抽象類可以有定義與實現,即其方法可以在抽象類中被實現;
②實現介面的關鍵字為interface,繼承抽象類的關鍵字為extends。一個類可以實現多個介面,但一個類只能繼承一個抽象類,因此使用介面可以間接地達到多重繼承的目的;
③介面強調特定功能的實現,其設計理念是“like-a”關係,而抽象類強調所屬關係,其設計理念是“is-a”關係;
④介面中定義的成員變數預設為public、static和final,只能夠有靜態的不能被修改的資料成員,而且,必須給其賦初值,其所有的成員方法都是public、abstract的,而且只能別這兩個關鍵字修飾。而抽象類可以有自己的資料成員變數,也可以有非抽象的成員方法,而且,抽象類中的成員變數預設為default,當然也可以被定義為private、protected和public,這些成員變數可以在子類中被重新定義,也可以被重新賦值,抽象類中的抽象方法(其前有abstract修飾)不能用private、static、synchronized和native等訪問修飾符修飾,同時方法必須以分號結尾,並且不能帶花括號{}。所以,當功能需要累積時,使用抽象類,不需要累積時使用介面。
⑤介面被運用於實現比較常用的功能,便於日後維護或者新增刪除方法,而抽象類更傾向於充當公共類的角色,不適用於日後重新對立面的程式碼進行修改。
1.8.3 抽象基類和介面的使用場景
介面是一種特殊形式的抽象類,使用介面完全有可能實現與抽象類相同的操作,但一般而言,抽象類多用於在同類事物中有無法具體描述的方法的場景,所以當子類和父類之間存在邏輯上的層次結構時,推薦使用抽象類,而介面多用於不同類之間,定義不同類之間的通訊規則。因此,當希望支援差別較大的兩個或者更多物件的特定互動行為時,應該使用介面。使用介面能大大降低軟體系統的耦合度。
需要注意的是,介面可以繼承介面,抽象類可以實現介面,抽象類可以繼承實體類。
1.8.4 為什麼Java語言不允許有多重繼承?
Java語言不支援多重繼承,但是可以通過實現多個介面的方式來間接地實現多重繼承。那麼為什麼Java語言沒有被設計為支援多重繼承的語言呢?一般認為主要有以下兩個原因:
(1)C++語言實現了多重繼承,而Java語言沒有,這顯然不是因為技術無法實現的原因,而是為了程式的結構能夠更加清晰從而便於維護。假設Java語言支援多重繼承,類C繼承類A和B,如果類A和類B中都有自定義的方法f(),那麼當在程式碼中呼叫類C的f()方法時,無法確定是呼叫類A還是類B的方法,將會產生二義性。這種實現將非常不利於系統的維護。但是Java語言卻可以通過實現多個介面的方式間接地支援多重繼承,由於介面只有方法體,沒有方法實現,假設類C實現了介面A和介面B,即使它們都有方法f(),但由於介面A和介面B中方法的定義沒有實現,只有類C中才會有一個方法的實現,因此也就不存在二義性了。
(2)多重繼承會使型別轉換、、構造方法的呼叫順序變得非常複雜,當然也會影響到效能。
由於在實際情況下沒有必須使用多重繼承的場景,因此為了設計簡單,同時擁有好的效能,Java語言最終沒有支援多重繼承。
1.8.5 集合框架中實現排序的介面是什麼?
Comparator介面。
在Java語言中,如果要對集合物件或陣列物件進行排序,就需要實現Comparator介面的compare方法,從而實現自定義類的比較。下面給出一個對自定義的類進行排序的例子(通過年齡大小進行排序),示例程式碼如下:
import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; class Student{ private String name; private int age; public Student(String name,int age){ this.name = name; this.age = age; } public String getName(){return name;} public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } class StudentComparator implements Comparator<Student>{ @Override public int compare(Student s1,Student s2){ if (s1.getAge()>s2.getAge()) return 1; else return -1; } } public class Example{ public static void main(String[] args) { List<Student> stus = new ArrayList<Student>(); stus.add(new Student("name1",5)); stus.add(new Student("name2",6)); stus.add(new Student("name3",4)); Collections.sort(stus,new StudentComparator()); for (Student stu : stus){ System.out.println(stu.getName()); } } }
執行結果為:
name3
name1
name3
1.9 多型
1.9.1 兩種多型機制
多型是面向物件程式設計中程式碼重用的一個重要機制,它表示當同一個操作作用在不同物件時,會有不同的語義,從而會產生不同的結果。在Java語言中,多型主要有以下兩個表現形式:
(1)過載(Overload):過載是指同一個類中有多個同名的方法,但這些方法有著不同的引數,因此在編譯時就可以確定到底呼叫的是哪個方法,它是一種編譯時多型。過載可以被看做一個類中的方法多型性。
(2)覆蓋(Override):子類可以覆蓋父類的方法,因此同樣的方法會在父類與子類中有著不同的表現形式。在Java語言中,基類的引用變數不僅可以指向基類的例項物件,也可以指向其子類的例項物件。同樣,介面的引用變數也可以指向其實現類的例項物件。而程式呼叫的方法在執行期才動態繫結(繫結指的是將一個方法呼叫和一個方法主體連線到一起),即引用變數所指向的具體例項物件的方法,也就是記憶體里正在執行的那個物件的方法,而不是引用變數的型別中定義的方法。通過這種動態繫結的方法實現了多型。由於只有在執行時才能確定呼叫的是哪個方法,因此通過方法覆蓋實現的多型也可以被稱為執行時多型。如下例所示:
class Base{ public Base(){ g(); } public void f(){ System.out.println("Base f()"); } public void g(){ System.out.println("Base g()"); } } class Derived extends Base{ @Override public void f() { System.out.println("Derived f()"); } @Override public void g() { System.out.println("Derived g()"); } } public class Test{ public static void main(String[] args) { Base b = new Derived(); b.f(); b.g(); } }
這段程式碼中,由於子類Derived的f()方法和g()方法與父類Base的方法同名,因此Derived的方法會覆蓋父類Base的方法。在執行Base b = new Derived()語句時,會呼叫Base類的構造方法,而在Base的構造方法中,執行了g()方法,由於Java語言的多型特性,此時會呼叫子類Derived的g()方法,而非父類Base的g()方法,因此會輸出Derived g()。由於實際建立的是Derived類的物件,後面的方法呼叫都會呼叫子類Derived的方法。
1.9.2 Overload和Override的區別是什麼?Overload能否改變返回值的型別?
Overload(過載)和Override(覆蓋)是Java多型性的不同表現。其中,過載是在一個類中多型性的一種表現,是指在一個類中定義了多個同名的方法,它們或有不同的引數個數或有不同的引數型別。在使用過載時,需要注意一下幾點:
①過載時通過不同的方法引數來區分的,例如不同的引數個數、不同的引數型別或不同的引數順序;
②不能通過方法的訪問許可權、返回值型別和丟擲的異常型別來進行過載;
③對於繼承來說,如果基類方法的訪問許可權是private,那麼就不能在派生類中對其進行過載,如果派生類也定義了一個同名的函式,這只是一個新的方法,不會達到過載的效果。
Override是指派生類函式覆蓋基類函式,覆蓋一個方法並對其重寫,已達到不同的作用。在使用覆蓋時,需要注意以下幾點:
①派生類中的覆蓋方法必要要和基類中被覆蓋的方法有相同的函式名和引數;
②派生類中的覆蓋方法的返回值必須和積累中被覆蓋的方法的返回值相同;
③派生類中的覆蓋方法所丟擲的異常必須和基類中被覆蓋的方法所丟擲的異常一致或是其子類;
④基類中被覆蓋的方法不能是private,否則其子類只是定義了一個方法,並沒有對其覆蓋。
過載和覆蓋的區別主要有以下幾點:
①覆蓋是子類和父類的關係,是垂直關係,過載是同一個類中方法之間的關係,是水平關係;
②覆蓋只能有一個方法或一對方法產生關係;方法的過載時多個方法之間的關係;
③覆蓋要求引數列表相同,過載要求引數列表不同;
④覆蓋關係中,呼叫方法體是根據物件的型別(物件對應儲存空間型別)來決定,而過載關係式根據呼叫時的實參表和形參表來選擇方法體的。
如果在一個類中定義了多個同名的方法,他們或有不同的引數個數或有不同的引數型別,則稱為方法的過載。Overload的方法可以便便返回值的型別,但是Override方法不能改變返回值型別。
1.9.3 為什麼不同通過返回值來對方法進行過載?
如果使用返回值對犯法進行過載,那麼在呼叫得到時候會產生二義性,呼叫者無法確定到底該呼叫哪個方法。如下例所示:
class Test{ public float add(int a,int b){ return a+b; } public int add(int a,int b){ return a+b+1; } public static void main(String[] args) { Test t = new Test(); t.add(1,2); //呼叫哪個方法? } }
因此無法通過返回值對方法進行過載。