Java基礎系列4:抽象類與介面的前世今生
- 2019 年 11 月 12 日
- 筆記
該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著了解每個Java知識點背後的實現原理,更完整地了解整個Java技術體系,形成自己的知識框架。
1、抽象類:
當編寫一個類時,常常會為該類定義一些方法,這些方法用以描述該類的行為方式,那麼這些方法都有具體的方法體。但在某些情況下,某個父類只是知道其子類應該包含怎樣的方法,但無法準確地知道這些子類如何實現這些方法。例如定義了一個Shape類,這個類應該提供一個計算周長的方法calPerimeter(),但不同Shape子類對周長的計算方法是不一樣的,即Shape類無法準確地知道其子類計算周長的方法。
可能有讀者會提出,既然Shape類不知道如何實現calPerimeter()方法,那就乾脆不要管它了!這不是一個好思路:假設有一個Shape引用變數,該變數實際上引用到Shape子類的實例,那麼這個Shape變數就無法調用calPerimeter()方法,必須將其強制類型轉換為其子類類型,才可調用calPerimeter0方法,這就降低了程式的靈活性。
如何既能讓Shape類里包含calPerimeter()方法,又無須提供其方法實現呢?使用抽象方法即可滿足該要求:抽象方法是只有方法簽名,沒有方法實現的方法。
定義:
抽象方法和抽象類必須使用abstract修飾符來定義,有抽象方法的類只能被定義成抽象類,抽象類裡面可以沒有抽象方法。
抽象方法和抽象類的規則如下:
- 抽象類必須使用abstract修飾符來修飾,抽象方法也必須使用abstract修飾符來修飾,抽象方法不能有方法體。
- 抽象類不能被實例化,無法使用new關鍵字來調用抽象類的構造器創建抽象類的實例。即使抽象類里不包含抽象方法,這個抽象類也不能創建實例。
- 抽象類可以包含成員變數、方法(普通方法和抽象方法都可以)、構造器、初始化塊、內部類(介面、枚舉)5種成分。抽象類的構造器不能用於創建實例,主要是用於被其子類調用。
- 含有抽象方法的類(包括直接定義了一個抽象方法;或繼承了一個抽象父類,但沒有完全實現父類包含的抽象方法;或實現了一個介面,但沒有完全實現介面包含的抽象方法三種情況)只能被定義成抽象類。
說了一大堆概念,聽得有點糊塗了,下面我們來看一段程式碼:
下面定義一個Shape抽象類:
public abstract class Shape { { System.out.println("執行Shape的初始化塊"); } private String color; //定義一個計算周長的抽象方法 public abstract double calPerimeter(); //定義一個返回形狀的抽象方法 public abstract String getType(); //定義Shape的構造器,該構造器並不是用於創建對象,而是被子類調用 public Shape() {} public Shape(String color) { System.out.println("執行Shape的構造器"); this.color=color; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } }
上面的Shape類里包含了兩個抽象方法:calPerimeter()和 getType(),所以這個Shape類只能被定義成抽象類。Shape類里既包含了初始化塊,也包含了構造器,這些都不是在創建 Shape對象時被調用的,而是在創建其子類的實例時被調用。
抽象類不能用作創建實例,只能當做父類被其他子類繼承。
下面定義一個三角形類,三角形類被定義成普通類,繼承Shape抽象類,因此必須實現Shape類中的抽象方法
public class Triangle extends Shape { //定義三角形的三邊 private double a; private double b; private double c; public Triangle(String color,double a,double b,double c) { super(color); setSize(a, b, c); } public void setSize(double a,double b,double c) { if(a+b<=c||a+c<=b||b+c<=a) { System.out.println("三角形兩邊之和必須大於第三邊"); return; } this.a=a; this.b=b; this.c=c; } //重寫Shape類計算周長的方法 @Override public double calPerimeter() { return a+b+c; } //重寫Shape類返回形狀的方法 @Override public String getType() { // TODO Auto-generated method stub return "三角形"; } }
上面的Triangle類繼承了Shape抽象類,並實現了Shape類中兩個抽象方法,是一個普通類,因此可以創建 Triangle類的實例,可以讓一個Shape類型的引用變數指向Triangle對象。
下面編寫測試程式碼:
public class TestShape { public static void main(String[] args) { Shape s1=new Triangle("黑色", 3, 4, 5); System.out.println(s1.getColor()); System.out.println(s1.getType()); } }
輸出結果:
執行Shape的初始化塊 執行Shape的構造器 黑色 三角形
利用抽象類和抽象方法的優勢,可以更好的發揮多態的優勢,使得程式更加靈活。
使用抽象類有以下幾點需要注意:
1、當使用abstract修飾類時,表明這個類時抽象類,不能實例化,只能被繼承;當使用abstract修飾方法時,表明這個方法必須由子類去實現。
2、final修飾的類不能被繼承,final修飾的方法不能被重寫,因此final和abstract不能同時出現。
3、abstract不能用於修飾成員變數,不能用於修飾局部變數,即沒有抽象變數、沒有抽象成員變數等說法;abstract也不能用於修飾構造器,沒有抽象構造器,抽象類里定義的構造器只能是普通構造器。
4、當使用static修飾一個方法時,表明這個方法屬於該類本身,即通過類就可調用該方法,但如果該方法被定義成抽象方法,則將導致通過該類來調用該方法時出現錯誤(調用了一個沒有方法體的方法肯定會引起錯誤)。因此static和abstract不能同時修飾某個方法,即沒有所謂的類抽象方法。
5、abstract修飾的方法沒有方法體,必須被之類重寫才有意義,所以抽象方法不能用private修飾,也就是private和abstract不能同時使用。
抽象類的作用:
- 從前面的示常式序可以看出,抽象類不能創建實例,只能當成父類來被繼承。從語義的角度來看,抽象類是從多個具體類中抽象出來的父類,它具有更高層次的抽象。從多個具有相同特徵的類中抽象出一個抽象類,以這個抽象類作為其子類的模板,從而避免了子類設計的隨意性。
- 抽象類體現的就是一種模板模式的設計,抽象類作為多個子類的通用模板,子類在抽象類的基礎上進行擴展、改造,但子類總體上會大致保留抽象類的行為方式。
- 如果編寫一個抽象父類,父類提供了多個子類的通用方法,並把一個或多個方法留給其子類實現,這就是一種模板模式,模板模式也是十分常見且簡單的設計模式之一。例如前面介紹的 Shape、Triangle類,已經使用了模板模式。
2、介面
抽象類是從多個類中抽象出來的模板,如果將這種抽象進行得更徹底,則可以提煉出一種更加特殊的“抽象類”——介面(interface)。Java9對介面進行了改進,允許在介面中定義默認方法和類方法,默認方法和類方法都可以提供方法實現,Java9為介面增加了一種私有方法,私有方法也可提供方法實
定義:
和類定義的不同,定義介面不再使用class關鍵字,而是使用interface關鍵字。介面的基本語法如下:
[修飾符] interface 介面名稱 extends 父介面1 父介面2 .... { 零到多個常量定義... 零到多個抽象方法定義... 零到多個內部類,介面,枚舉定義... 零到多個私有方法,默認方法或者類方法定義... }
- 修飾符可以是public或者省略,如果省略了public訪問控制符,則默認採用包許可權訪問控制符,即只有在相同包結構下才可以訪問該介面。
- 介面名應與類名採用相同的命名規則,即如果僅從語法角度來看,介面名只要是合法的標識符即可;如果要遵守Java可讀性規範,則介面名應由多個有意義的單詞連綴而成,每個單詞首字母大寫,單詞與單詞之間無須任何分隔符。介面名通常能夠使用形容詞。
- 一個介面可以有多個直接父介面,但介面只能繼承介面,不能繼承類。
- 介面中可以包含成員變數(只能是靜態常量),方法(只能是抽象實例方法、類方法、默認方法或私有方法),內部類(包括內部介面,枚舉)定義
- 介面中定義的常量系統會自動為常量加上static和final兩個修飾符
- 介面中定義的方法只能是抽象實例方法、類方法、默認方法或私有方法,如果定義的不是類方法,默認方法和私有方法,系統將自動為普通方法增加abstract修飾符,介面中的普通方法總是用public abstract來修飾,但類方法,默認方法,私有方法都必須有方法體實現。
下面來看一個具體的介面:
public interface Output { //介面中定義的成員變數只能是常量 int MAX_CACHE_LINE=50; //介面中定義的普通方法只能是public abstract抽象方法 void out(); //在介面中定義默認方法,需要用default修飾 default void print(String...msg) { for (String str : msg) { System.out.println(str); } } //在介面中定義類方法,需要使用static修飾 static String staticTest() { return "介面中的類方法"; } //定義私有方法 private void foo() { System.out.println("介面中的私有方法"); } //定義私有靜態方法 private static void bar() { System.out.println("bar私有靜態方法"); } }
介面的繼承:
介面的繼承和類繼承不一樣,介面完全支援多繼承,即一個介面可以有多個直接父介面。和類繼承相似,子介面擴展某個父介面,將會獲得父介面里定義的所有抽象方法、常量。
一個介面繼承多個父介面時,多個父介面排在extends關鍵字之後,多個父介面之間以英文逗號(,)隔開。下面程式定義了三個介面,第三個介面繼承了前面兩個介面。
interface InterfaceA{ int PROP_A=5; void testA(); } interface InterfaceB{ int PROP_B=6; void testB(); } interface InterfaceC extends InterfaceA,InterfaceB{ int PROP_C=7; void testC(); } public class InterfaceExtendsTest { public static void main(String[] args) { System.out.println(InterfaceC.PROP_A); System.out.println(InterfaceC.PROP_B); System.out.println(InterfaceC.PROP_C); } }
使用介面:
介面不能用於創建實例,但介面可以用於聲明引用類型變數。當使用介面來聲明引用類型變數時,這個引用類型變數必須引用到其實現類的對象。除此之外,介面的主要用途就是被實現類實現。歸納起來,介面主要有如下用途。
- 定義變數,也可用於進行強制類型轉化
- 調用介面中定義的常量
- 被其他類實現
一個類可是實現多個介面,用關鍵字implements實現,類實現介面的語法格式如下:
[修飾符] class 類名 extends 父類 implements 介面1,介面2…{
類體部分
}
注意:類實現介面時必須要實現介面中所有的抽象方法
interface interfaceA{ void printA(); } interface interfaceB{ void printB(); } public class ImplmentsTest implements interfaceA,interfaceB { @Override public void printB() { System.out.println("printB"); } @Override public void printA() { System.out.println("printA"); } }
上述程式碼的ImplmentsTest實現了兩個介面,並重寫了其中的抽象方法
介面和抽象類的區別:
相同點:
- 介面和抽象類都不能被實例化,它們都位於繼承樹的頂端,用於被其他類實現和繼承。
- 介面和抽象類都可以包含抽象方法,實現介面或繼承抽象類的普通子類都必須實現這些抽象方法。
不同點:
- 介面里只能包含抽象方法、靜態方法、默認方法和私有方法,不能為普通方法提供方法實現;抽象類則完全可以包含普通方法。
- 介面里只能定義靜態常量,不能定義普通成員變數;抽象類里則既可以定義普通成員變數,也可以定義靜態常量。
- 介面里不包含構造器;抽象類里可以包含構造器,抽象類里的構造器並不是用於創建對象,而是讓其子類調用這些構造器來完成屬於抽象類的初始化操作。
- 介面里不能包含初始化塊;但抽象類則完全可以包含初始化塊。
- 一個類最多只能有一個直接父類,包括抽象類;但一個類可以直接實現多個介面,通過實現多個介面可以彌補Java單繼承的不足。