在本篇博客中將介紹JAVA里抽象類和介面的基礎知識以及兩者的異同點,在有繼承和多型的基礎知識上學習會更好~
目錄
抽象類基礎知識
抽象類的定義、創建等基礎
抽象類的幾點說明(一)
為何使用抽象方法
抽象類的幾點說明(二)
介面基礎知識
介面的定義、創建等基礎
介面具體例題詳析(重要)
關于介面的幾點說明
抽象類和介面的異同點
抽象類與介面的相同點
抽象類與介面的相異點(重要)
抽象類與介面的區別運用
抽象類基礎知識
在學習完繼承和多型的知識后,我們對“類”的運用又有了更深的了解,那么在Java中還存在一個重要且特殊的類--抽象類,為什么說特殊,因為Java中的抽象類是不可以用于創建物件的,但是它可以包含抽象方法,這些方法將在具體的子類中實作,注意具體和子類兩個關鍵字,下面是抽象類的具體定義:
在繼承的層次結構中,每個新的子類都使類變得更加明確和具體,如果從一個子類向父類追溯,類就會變得更通用,也就更加不明確,類的設計應該確保父類包含它的子類的共同特征,有時候,一個父類設計得非常抽象,以至于它都沒有任何具體的實體,這樣的類稱為抽象類(abstract class) 這里要記住它的特殊性!沒有任何實體!
抽象類的定義、創建等基礎
我們來舉個栗子,創建下面這樣一個GeometricObject (幾何)類,這個在上一篇博客中也有,用來引入講繼承(大概意思是定義一個幾何類,那么像Circle類和Rectangle類都可以從它衍生出來成為子類(并不是子集!))
public class GeometricObject //創建這樣一個試用于所有幾何的大類,簡稱父類
{
private String color = "white"; //一般設定為私有變數
private boolean filled; //布爾型別,是否填充
private java.util.Date dateCreated;
public GeometricObject() //創建一個初始的或者是錯誤的幾何物件,初始建構式
{
dateCreated = new java.util.Date();//記錄創建時間
}
public GeometricObject(String color,boolean filled) //創建一個初始的物件包含顏色和是否填充
{
dateCreated = new java.util.Date(); //更新創建時間
this.color = color;
this.filled = filled;
}
public String getColor() //用戶想得知顏色可以回傳
{
return color;
}
public void setColor(String color) //用戶在外部更改顏色
{
this.color = color;
}
public boolean isFilled()
{
return filled;
}
public void setFilled(Boolean filled)
{
this.filled = filled;
}
public java.util.Date getDateCreated()
{
return dateCreated;
}
//回傳這個物件的基礎資訊,其實由前面學過的多載可知,我們這里是多載了toString這個函式,它的原始父類應該是在Object中
@Override
public String toString()
{
return "created on " + dateCreated + "\ncolor: " + color + " and filled: " + filled;
}
}
可以看到,GeometricObject類對幾何物件的共同特征進行了建模,那么假設我們寫了一個Circle類和Rectangle類(它的子類),然后它們都包含了分別用于計算圓和矩形的面積和周長的getArea()方法和getPerimeter()方法,
因為可以計算所有幾何物件的面積和周長,所以最好在GeometricObject類中定義getArea()和getPerimeter()方法,但是!這些方法不能在GeometricObject類中實作,因為它們的實作取決于幾何物件的具體型別!這樣的方法就稱之為抽象方法(abstract method),在方法頭中使用abstract修飾符表示,一旦在GeometricObject類中定義了這些方法后,GeometricObject就成為了一個抽象類,在類的頭部使用abstract修飾符表示該類為抽象類,我們現在給出新的GeometricObject類的定義:(很重要!認真看!)
public abstract class GeometricObject //抽象類前也要用abstract關鍵詞定義
{
private String color = "white";
private boolean filled;
private java.util.Date dateCreated;
//創建一個初始的未設定的幾何物件/構造方法
protected GeometricObject() //抽象類的構造方法定義為protected,因為它只被子類使用
{
dateCreated = new java.util.Date();
}
//創建一個初始的帶設定的幾何物件/這個也是構造方法,看方法頭!
protected GeometricObject(String color, boolean filled)
{
dateCreated = new java.util.Date();
this.color = color;
this.filled = filled;
}
//接下來顏色和是否填充的獲取器和修改器方法和之前的都一樣
//下面這些就是抽象方法!也是使得這個類成為抽象類的原因!
public abstract double getArea();
public abstract double getPerimeter();
//注意!這里都不需要寫怎么計算或者回傳什么,因為并不確定具體的幾何物件,也就是具體的子類!
}
抽象類的幾點說明(一)
其實上面的注釋中也提到了關于抽象類的重要知識點,我們在這里再總結一下:
1.抽象類和常規類很像,但是不能使用new運算子創建它的實體!抽象方法只有定義而沒有實作!它的實作由子類提供!一個包含抽象方法的類必須宣告為抽象類
2.抽象類的構造方法定義為protected,因為它只被子類使用!創建一個具體子類的實體時,其父類的構造方法被呼叫以初始化父類中定義的資料域!
那么Circle類在上一篇博客的繼承中也有,這時候繼承這個新定義的GeometricObject類就可以除錯試試,寫法上是沒有任何差別的,呼叫定義抽象方法即可,同學們可以回上一篇看看
為何使用抽象方法
其實既然提到子類既然和之前的創建書寫都沒有什么不一樣,那么為什么要在GeometricObject類中定義方法getArea()和getPerimeter()為抽象的,而不是在每個子類中定義他們,這樣有什么好處?
我們來看一下下面這個代碼,程式創建了兩個幾何物件:一個圓和一個矩形,呼叫equalArea方法來檢查它們的面積是否相同,然后呼叫displayGeometricObject方法來顯示它們:
public class TestGeometricObject
{
public static void main(String[] args)
{
GeometricObject geoObject1 = new Circle(5); //向上轉型!
GeometricObject geoObject2 = new Rectangle(5, 3); //向上轉型!
System.out.println("The two objects hava the same area? " + equalArea(geoObject1, geoObject2));
displayGeometricObject(geoObject1);
displayGeometricObject(geoObject2);
}
public static boolean equalArea(GeometricObject object1, GeometricObject object2)
{
return object1.getArea() == object2.getArea();
}
public static void displayGeometricObject(GeometricObject object)
{
System.out.println();
System.out.println("The area is " + object.getArea());
System.out.println("The perimeter is " + object.getPerimeter());
}
}
//輸出結果是:
//The two objects have the same area? false
//The area is 78.53981633974483
//The perimeter is 31.41592653589793
//The area is 13.0
//The perimeter is 16.0
要注意的是:Circle類和Rectangle類中重寫了定義在GeometricObject類中的getArea()和fetPerimeter()方法,從陳述句第五到六行可以看出,創建了一個新的圓和一個新的矩形,并把它們賦值給變數geoObject1和geoObject2,這兩個變數都是GeometricObject型別的,這是向上轉型!!!
我們這里分析一下,當呼叫equalArea方法時,由于geoObject1是一個圓,所以object1.getArea()使用的是Circle類定義的getArea()方法,而另一個是一個矩形,所以它呼叫時使用的是Rectangle類中定義的getArea方法,類似地在呼叫displayGeometricObject方法時,呼叫的那些物件方法都是子類中定義的同名的重寫方法,這也表明,JVM在運行時根據呼叫該方法的實際物件的型別來動態地決定呼叫哪一個方法 (可以回顧多型的相關知識)
注意!如果GeometricObject類中沒有定義getArea()等這些抽象方法,就不能再該程式中定義equalArea方法來計算這兩個幾何物件的面積是否相同,所以這可以看做是定義抽象方法的好處
抽象類的幾點說明(二)
下面是關于抽象類值得注意的幾點:
1.抽象方法不能包含在非抽象類中,如果抽象父類的子類不能實作所有的,注意是所有的抽象方法,那么子類也必須定義為抽象的,換句話說,在繼承自抽象類的非抽象子類中,必須實作所有的抽象方法,還要注意到,抽象方法是非靜態的!
2.抽象類不能使用new運算子來初始化,也就是說它沒有實體,但是!仍然可以定義它的構造方法!這個構造方法在它的子類的構造方法中呼叫,比如我們上面的GeometricObject類的構造方法在Circle類和Rectangle類中呼叫,也就是回顧多型的向上轉型知識!
3.包含抽象方法的類必須是抽象的,然而,可以定義一個不包含抽象方法的抽象類,這個抽象類用于作為定義新子類的基類,
4.子類可以重寫父類的方法并將它定義為抽象的,這很少見,但是它在當父類的方法實作在子類中變得無效時是很有用的,那么也可以知道在這種情況下,這個子類也必須被定義為抽象的,
5.即是子類的父類是具體的,但是這個子類也可以是抽象的!例如,大類基類Object是具體的,但是它的子類比如我們定義的GeometricObject可以是抽象的,
6.不能使用new運算子從一個抽象類創建一個實體,但是抽象類可以用作一種資料型別,比如下面的陳述句創建一個元素是GeometricObjecy型別的陣列是正確的:
GeometricObject[] objects = new GeometricObject[10];
然后可以創建一個GeomettricObject的實體,并將它的參考賦值給陣列,如下:
object[0] = new Circle();
建議這里可以回顧多型的知識~
介面基礎知識
我們現在學習下介面的基礎知識,其實介面在許多方面都與抽象類很相似,它也是一種與類相似的結構,用于為物件定義共同的操作,但是!它的目的是指明相關或者不相關類的物件的共同行為,例如,使用適當的介面,可以指明這些物件是課比較的、可食用的或者可克隆的,我們下面會深入理解這些話的意思,
介面的定義、創建等基礎
為了區分介面和類(我們上面也說了介面只是一種與類相似的結構),Java用下面的語法來定義介面:
modifier /*介面類別,公用的還是私有的等等*/ interface InterfaceName /*介面名稱*/
{
//Constant declarations 常數宣告
//Abstract method signatures 抽象方法簽名
}
//interface為介面關鍵字,同抽象類的abstract一樣
//下面是一個介面的例子:
public interface Edlib
{
// Describle hou to eat
public abstract String howToEat(); //必須包含抽象方法
}
從上述定義代碼也可以看出,介面包括常量和抽象方法(只能包括抽象方法,下面有總結),但是這里提一點,Cloneable介面是一個特殊情況,這個介面是空的,實作Cloneable介面的類標記為可克隆的,而且它的物件可以使用在Object類中定義的clone()方法克隆,感興趣的同學可以去了解一下,
在Java中,介面被看做是一種特殊的類,就像常規類一樣,每個介面都被編譯為獨立的位元組碼檔案,使用介面或多或少有點像使用抽象類,例如,可以使用介面作為參考變數的資料型別或型別轉換的結果等,與抽象類相同的是,不能使用new運算子創建介面的實體!!我們也可以理解為因為介面中是包含抽象方法的!它需要依靠具體的類實作!!
介面具體例題詳析(重要)
現在通過下面的這個包含介面實作(介面用上面的那個Edlib)、抽象類、常規類以及測驗等等的代碼例題來理解介面的運用與意義:
abstract class Animal //由上節學習過的知識可知,我們創建了一個抽象類(必包含抽象方法)
{
private double weight; //和常規類一樣,私有成員變數、修改器訪問器等方法
public double getWeight()
{
return weight;
}
public void setWeight(double weight)
{
this.weight = weight;
}
public abstract String sound(); //包含的抽象方法,回傳動物的聲音的方法,其依賴于具體的子類(哪種動物)實作
}
//使用Edlib介面來指定一個物件是否是可食用的,使用implements關鍵字讓物件所屬的類實作這個介面
class Chicken extends Animal implements Edible
{
@Override //方法重寫前的關鍵字,可得知我們這里重寫了介面中的抽象方法,這是否是必須的?
public String howToEat() //若不重寫它或者說不需要它,你對這個子類使用介面的意義又何在?
{
return "Chicken: Fry it";
}
@Override
public String sound()
{
return "Chicken: cock-a-doodle-doo";
}
}
//這個Tiger類就僅僅是繼承自Animal類了,并沒有使用Edlib介面
class Tiger extends Animal
{
@Override
public String sound() //從上節抽象類知識可知,繼承自抽象類的子類必須要重寫所有的抽象方法!
{
return "Tigher: RROOAARR";
}
}
//注意!我們上面是繼承自抽象類Animal的子類Chicken去和介面連接
//而在這里Fruit類為什么要被定義為abstract的?還是說它就是一個抽象類連接介面?
abstract class Fruit implements Edlib
{
//創建一些資料域,訪問器和修改器方法
}
class Apple extends Fruit
{
@Override
public String howToEat()
{
return "Apple: Make apple cider";
}
}
class Orange extends Fruit
{
@Override
public String howToEat()
{
return "Orange: Make orange juice";
}
}
//很顯然,繼承自Fruit的子類Apple和Orange有了“重寫howToEat的資格”
//現在測驗一下這些類和介面,觀察輸出結果
public class TestEdible
{
public static void main(String[] args)
{
//利用ArrayList來創建這些物件,更加方便,這是向上轉型!最后的執行方法取決于物件的實際型別!可以回顧多型知識!
Object[] objects = {new Tiger(), new Chicken(), new Apple()};
//回圈測驗,注意判別條件
for (int i = 0;i < objects.length;i++)
{
if (objects[i] instanceof Edible) //這里使用instanceof關鍵字來判斷從屬關系
System.out.println(((Edible)objects[i]).howToEat()); //這種格式是怎樣的?
if (objects[i] instanceof Animal)
System.out.println(((Animal)objects[i]).sound());
}
}
}
//輸出的結果是:
//Tiger: RROOAARR 它沒有Edlib介面,不輸出howToEat,它屬于Animal的子類,輸出聲音
//Chiken: Fry it 它有Edlib介面,輸出howToEat
//Chicken: cock-a-doodle-doo 它屬于Animal的子類,輸出聲音
//Apple: Make apple cider 它有Edlib介面,輸出howToEat,它不屬于Animal的子類,不輸出聲音
這個例子使用了多個類和介面,通過下面的UML圖來看看

關于介面的幾點說明
仔細看一遍代碼和注解后,其實我們很容易理解捋清這些類的繼承、介面關系等,捋清了這些后,我們總結一下在該例題中出現的知識點和需要注意的地方:
1.可以使用Edlib介面來指定一個物件是否是可食用的,這需要使用implements關鍵字讓物件所屬的類實作這個介面,比如我們這里的Chicken類和Fruit類實作了Edible介面
2.類和介面之間的關系稱為介面繼承,因為介面繼承和類繼承本質上是相同的,所以我們將它們都簡稱為繼承,像實作多型的條件之一就是必須有繼承關系,這里的繼承包括介面繼承
3.注意!當一個類實作介面時,該類實作定義在介面中的所有方法(不管是什么方法),在這里,Fruit類實作了Edible,但是!因為它沒有實作howToEat方法,所以Fruit類必須定義為abstract,這就是原因!正因如此,Fruit的具體子類必須實作howToEat方法,理解!不然他們也要被定義為抽象類!
強調:類與類之間也好,類與介面之間也罷,這些繼承、多型的關系一定要搞清楚,比如一個類是可以隨意定義的還是必須有某種關鍵字的,這樣我們自己才不會混亂,也不至于出錯
4.介面中的所有的資料域都是public static final 而且所有的方法都是public abstract(這也是介面區別于抽象類的關鍵之一!抽象類只需包含抽象方法即可,其他型別的方法是允許有的),所以Java允許忽略這些修飾符,比如下列在介面中的定義都是等價的:
public static final int k = 1; public abstract void p(); 等價于 int k = 1; void p();
抽象類和介面的異同點
抽象類與介面的相同點
其實在剛開始學習介面的程序中,就有一直提到介面其實和抽象類在使用上是比較相似的,這里總結幾點:
1.它們都是用來指定多個物件的共同特征的,即都是Java提供的對現實世界中的物體進行抽象的兩種機制
2.抽象類與介面都不能被實體化,即不能使用new運算子來創建它們的實體
3.在抽象類與介面中所列的所有抽象方法(當然介面中只含有抽象方法)都必須在別的地方中被重寫,如果達不到所有或者說沒有被重寫,那么它必須也定義為抽象的(例題中有說)
4.介面與類都可以定義一種型別,一個介面型別的變數可以參考任何實作該介面類的實體,如果一個類實作了一個介面,那么這個介面就類似于該類的一個父類,這也就是為什么叫做介面繼承的原因,也就是說可以將介面當做一種資料型別使用,將介面型別的變數轉換為它的子類,反過來也可以
其實學習了多型的相關知識(上一篇博客中),這句話很容易理解,我們也可以結合下面這個圖掌握,假設c是圖中Class2的一個實體,那么c也是Object、Class1、Interface1、Interface1_1、Interface1_2、Interface2_1和Interface2_2的實體
以上這些及上面的抽象類與介面的知識點中都是兩者極大的相似性,通過上面Edible介面的例題中也可以看到(我有在例題中做注釋),過多的相同點這里就不再贅述
抽象類與介面的相異點(重要)
雖說兩者相同處眾多,但其實在上面的學習程序里提及到的很多要點中我們也能感受到兩者在語法和使用意義上的多處不同,
從根本上來說,抽象類是類,而介面不是(當然我們時常也將它當成一種特殊的類),它的目的是為了給其他類提供行為說明,包括抽象類,比如上面例題中的 abstract class Fruit implements Edible 就是很好地說明這一點(這里是因為沒有重寫Edible中的抽象方法而被定義為抽象類),很多地方也可以看出介面與類的不同,比如類的關鍵字是class,而介面的是interface,介面中的方法只有方法頭加分號結尾(因為都是抽象方法),沒有方法主體,而類中的不是......
從語法層面來說兩者也有以下不同:
更詳細一點可以總結為:
上面兩張表格就很詳盡地說明了抽象類與介面的從多個方面的不同,介面的語法從變數方法的類別等很多方面都比抽象類嚴格得多,這些也強調過很多遍了,現在我們著重看一下兩者在繼承方面的區別
一個類可以實作多個介面,但是只能繼承一個父類(比如抽象類),也就是說我們在學繼承關系的時候也提到,Java只允許為類的繼承做單一繼承,但是允許使用介面做多重繼承!這樣就解決了只能單一繼承的弊端,卻又沒有破壞單一繼承,這就是介面的好處!
當然了,我們會利用extends關鍵字使某類繼承某類,我們同樣也可以使用extends關鍵字使介面繼承其他介面,介面與介面之間正同于類與類之間存在繼承關系的,這樣的介面稱為子介面 我們仔捋一捋類與類,介面與介面,類與介面之間的繼承關系:
1.類與類之間可以存在繼承,但是只能做單一繼承,即一個類只能繼承自一個父類,而一個父類可以有多個繼承它的子類
2.介面與介面之間可以存在繼承,且可以做多重繼承,即一個介面可以繼承自其他幾個介面,比如:
public interface NewInterFace extends Interface1,...,InterfaceN {
//constants and abstract methods }
在這里,NewInterFace是Interface1,...,InterfaceN的子介面,一個實作NewInterFace的類必須實作在NewInterFace,Interface1,...,InterfaceN中定義的抽象方法(注意細節!包括NewInterFace)
3.一個類可以實作(繼承)一個或多個介面,使用implements關鍵字(說得更準確一點是使用該關鍵字讓這個類實作這個介面),稱為介面繼承,但是!介面不能繼承類,只能繼承介面!
4.我們都知道所有的類都共享一個根類Object,但是!介面沒有共同的根!
抽象類與介面的區別運用
最后,其實在大多數情況下使用介面會比使用類更加地靈活,也更加推薦使用介面!
因為介面可以為不相關類定義共同的父型別,這也是我們介紹介面時就說了它的目的:指明相關或者不相關類的物件的共同行為,而抽象類定義的抽象行為是為具有相關類而設定的
我們可以繼續上面那個Edible介面的例子來更加清楚使用抽象類和使用介面的區別,現在考慮Animal類,假設Animal類中定義了howToEat方法:
abstract class Animal
{
public abstract String howToEat();
}
//Animal的兩個子類定義如下:
class Chicken extends Animal
{
@Override
public String howToEat()
{
return "Fry it";
}
}
class Duck extends Animal
{
@Override
public String howToEat()
{
return "Roast it";
}
}
假設給定這個繼承體系結構,多型使你可以在一個型別為Animal的變數中保存Chicken物件或Duck物件的參考,(多型--向上轉型)如:
public static void main(String[] args)
{
Animal animal = new Chicken();
eat(animal);
animal = new Duck(); //重新賦值新的物件,這個知識點講過很多了!
eat(animal);
}
public static void eat(Animal animal)
{
Ststem.out.println(animal.howToEat());
}
同樣的又是一句說過很多很多遍的話:
JVM會基于呼叫方法時所用的確切物件來動態地決定呼叫哪個howToEat方法(回顧多型的知識)
可以定義Animal的一個子類,但這里有個限制條件,那就是該子類必須是另一種動物,但是新問題產生了:如果一種動物不可食用,那么它繼承Animal類就不合適了,因為我們在這里定義的抽象類就是Animal類,它的子類必須重寫它的所有方法,這個很容易理解,
而介面完全沒有這種問題,介面比類擁有更多的靈活性,因為不用使所有東西都屬于同一個型別的類,可以在介面中定義howToEat()方法,然后把它當做其他類的共同父型別,
/* 這個代碼就像我們寫在介面的那個一樣(這里就不寫了)(∩_∩) */
要表示一個可食用物件的類,只需讓該類實作Edible介面即可,順理成章地,這個類就成了Edible型別的子型別,任何Edible物件都可以被傳遞以呼叫howToEat方法,我的意思是通過向上轉型定義一個型別為Edible的變數去參考物件
到此本篇的內容就全部結束了,逐一學懂抽象類和介面(當然如果繼承和多型的知識掌握得很好那學起來就相當容易),每個總結都寫在文章里了,這樣區別兩者的異同也會輕車熟路,很深刻不會搞混,同的到底是在哪些地方同,不同的地方還是非常明顯,一用起來就知道了,所以還是要多實踐多寫代碼,加油!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/379474.html
標籤:java
