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 ....    {      零到多個常量定義...      零到多個抽象方法定義...      零到多個內部類,介面,枚舉定義...      零到多個私有方法,默認方法或者類方法定義...    }

 

  1. 修飾符可以是public或者省略,如果省略了public訪問控制符,則默認採用包許可權訪問控制符,即只有在相同包結構下才可以訪問該介面。
  2. 介面名應與類名採用相同的命名規則,即如果僅從語法角度來看,介面名只要是合法的標識符即可;如果要遵守Java可讀性規範,則介面名應由多個有意義的單詞連綴而成,每個單詞首字母大寫,單詞與單詞之間無須任何分隔符。介面名通常能夠使用形容詞。
  3. 一個介面可以有多個直接父介面,但介面只能繼承介面,不能繼承類。
  4. 介面中可以包含成員變數(只能是靜態常量),方法(只能是抽象實例方法、類方法、默認方法或私有方法),內部類(包括內部介面,枚舉)定義
  5. 介面中定義的常量系統會自動為常量加上static和final兩個修飾符
  6. 介面中定義的方法只能是抽象實例方法、類方法、默認方法或私有方法,如果定義的不是類方法,默認方法和私有方法,系統將自動為普通方法增加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);  	}    }  

  

 

使用介面:

介面不能用於創建實例,但介面可以用於聲明引用類型變數。當使用介面來聲明引用類型變數時,這個引用類型變數必須引用到其實現類的對象。除此之外,介面的主要用途就是被實現類實現。歸納起來,介面主要有如下用途。

  1. 定義變數,也可用於進行強制類型轉化
  2. 調用介面中定義的常量
  3. 被其他類實現

一個類可是實現多個介面,用關鍵字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單繼承的不足。