1. 程式人生 > >精進Java——面向物件上篇

精進Java——面向物件上篇

這篇文章碼了好幾天,也是我準備開的一個新系列,是十月的目標,既然名字是《精進Java》,那麼我也會也對各個點都進行深入瞭解, 當然有些比較基礎的我就直接跳過了。到時我會搭配自己總結的面試題一起閱讀(畢竟一篇文章不能太長了哈哈,面試題是否開放到時再看吧),如果這裡有沒有總結到的知識點,希望各位讀者能給我提提意見,我會加上我自己的理解和總結,後期還會根據變化不斷補充內容。碼字不易,都是一個字一個字敲出來的,希望各位大佬們覺得不錯的話可以多關注我的文章,你們的每一個評論和收藏都是我的動力!!

面向物件和麵向過程的區別

面向過程適合簡單、不需協作的事務,按步驟實現,比如如何開車?

面向物件更趨向於的是如何設計,而不是按步驟進行,比如如何造車?

這兩種都是解決問題的思維方式,都是程式碼組織的方式。

解決複雜的問題,巨集觀上使用面向物件把握,微觀處理上仍然是面向過程。

面向物件三大特性

封裝、繼承、多型。後面會拆開講解。

記憶體分析

Java虛擬機器的記憶體可以分為三個區域:棧stack、堆heap、方法區method area

棧的特點

  1. 棧描述的是方法執行的記憶體模型,每個方法被呼叫都會建立一個棧幀(儲存區域性變數、運算元、方法入口),每個方法執行完就幹掉這個棧幀
  2. JVM為每個執行緒建立一個棧,用於存放該執行緒執行方法的資訊(實際引數、區域性變數等),當這個執行緒的方法執行完,那麼就把這整個棧幹掉
  3. 棧屬於執行緒私有,不能實現執行緒間的共享
  4. 棧的儲存特點是“先進後出,後進先出”
  5. 棧是由系統自動分配的,速度快,棧是一個連續的記憶體空間

堆的特點

  1. 堆用於儲存建立好的物件和陣列(陣列也是物件)
  2. JVM只有一個堆,被所有執行緒共享
  3. 堆是一個不連續的記憶體空間,分配靈活,速度慢

方法區(靜態區)特點

  1. JVM只有一個方法區,被所有執行緒共享
  2. 方法區實際也是堆,只是用於儲存類、常量相關的資訊
  3. 用來儲存程式中永遠不變或唯一的內容(類資訊、class物件、靜態變數、字串常量等等)
public class Student{
    int id;         //學號
    String name;    //姓名
    int age;        //年齡
    School school;  //所在學校
    
    void study(){
        System.out.println("好好學習天天向上");
    }
    
    void sleep(){
        System.out.println("好好休息");
    }
}

public static void main(String[] args){
    Student student = new Student();
    student.id = 1111;
    student.name = "tihom";
    student.age = 18;
    School school = new School;
    school.name = "GDUT";
    student.school = school;
}

class School{
    String name;
}

這裡反映了記憶體間的關係

垃圾回收機制

拿C++與Java對比,C++好比沒有服務員的飯店,每桌吃完之後沒人收拾垃圾,那麼隨著店內乾淨的桌子不斷減少,最後沒有乾淨的桌子可以使用了;而Java則自帶一個GC服務員,會在每桌吃完之後主動收拾垃圾。

垃圾回收演算法一般要做的兩件事

  • 發現無用的物件
  • 回收無用物件佔用的記憶體空間

所使用的演算法

  • 引用計數演算法

    堆中每個物件都有一個引用計數。被引用一次計數+1,被引用變數變為null,則計數-1,直到計數為0,則表示變成無用物件,演算法簡單,但是”迴圈引用的無用物件“無法被識別

  • 引用可達演算法(根搜尋演算法)

    程式把所有的引用關係看作一張圖,從一個節點GC ROOT開始,尋找對應的引用節點,找到這個節點之後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之後,剩餘的節點則認為是沒有被引用到的節點,即無用的節點

分代垃圾回收機制

不同物件有不同的生命週期。因此,不同生命週期的物件可以採取不同的回收演算法,以便提高回收效率。我們將物件分為三種狀態:年輕代、年老代、持久代。JVM將堆記憶體劃分為 Eden、Survivor 和 Tenured/Old 空間。

  1. 年輕代

所有新生成的物件首先都是放在Eden區。 年輕代的目標就是儘可能快速的收集掉那些生命週期短的物件,對應的是Minor GC,每次 Minor GC 會清理年輕代的記憶體,演算法採用效率較高的複製演算法,頻繁的操作,但是會浪費記憶體空間。當“年輕代”區域存放滿物件後,就將物件存放到年老代區域。

  1. 年老代

在年輕代中經歷了N(預設15)次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。年老代物件越來越多,我們就需要啟動Major GC和Full GC(全量回收),來一次大掃除,全面清理年輕代區域和年老代區域。

  1. 持久代

用於存放靜態檔案,如Java類、方法等。持久代對垃圾回收沒有顯著影響。

1.png

  • Minor GC

    用於清理年輕代區域。Eden區滿了就會觸發一次Minor GC。清理無用物件,將有用物件複製 到“Survivor1”、“Survivor2”區中(這兩個區,大小空間也相同,同一時刻Survivor1和Survivor2只有一個在用,一個為空)

  • Major GC

用於清理年老代區域。

  • Full GC

用於清理年輕代、年老代區域。 成本較高,會對系統性能產生影響。

垃圾回收過程

​ 1、新建立的物件,絕大多數都會儲存在Eden中,

​ 2、當Eden滿了(達到一定比例)不能建立新物件,則觸發垃圾回收(GC),將無用物件清理掉,

​ 然後剩餘物件複製到某個Survivor中,如S1,同時清空Eden區

​ 3、當Eden區再次滿了,會將S1中的不能清空的物件存到另外一個Survivor中,如S2,

​ 同時將Eden區中的不能清空的物件,也複製到S1中,保證Eden和S1,均被清空。

​ 4、重複多次(預設15次)Survivor中沒有被清理的物件,則會複製到老年代Old(Tenured)區中,

​ 5、當Old區滿了,則會觸發一個一次完整地垃圾回收(FullGC),之前新生代的垃圾回收稱為(minorGC)

JVM調優和Full GC

在對JVM調優的過程中,很大一部分都是對Full GC的調優

有如下原因可能導致Full GC

  1. 年老代(Tenured)被寫滿

  2. 持久代(Perm)被寫滿

  3. System.gc()被顯式呼叫(程式建議GC啟動,不是呼叫GC)

  4. 上一次GC之後Heap的各域分配策略動態變化

記憶體洩漏

為什麼會發生記憶體洩漏?

A引用了B,A的生命週期為t1-t4,B的生命週期為t2-t3,當B不使用時,A仍然保持著對B的引用,垃圾回收機制無法對B進行清理,導致B一直在記憶體中存在,如果很多個這種存在的話,那麼記憶體會消耗很大的空間。

也有情況是B引用著很多個其他的物件,那些物件無法被銷燬,導致A引用B,B引用了其他物件都無法被回收。

  • 建立大量的無用物件

    比如,使用字串拼接時,使用了String而不是StringBuilder

  • 靜態集合類的使用

    像HashMap、Vector、List等的使用最容易出現記憶體洩露,這些靜態變數的生命週期和應用程式一致,所有的物件Object也不能被釋放

  • 各種連線物件未關閉(IO流物件、資料庫連線物件、網路連線物件)

    這些連線物件都是物理物件,和硬碟或者網路連線,不使用時一定要關閉

  • 監聽器的使用

    釋放物件時,未刪除對應的監聽器

注意

  • 可以呼叫System.gc(),但是該方法只是通知JVM,並不是執行垃圾回收器。儘量少用,會申請啟動Full GC,成本高,影響系統性能
  • finalize方法,是Java提供給程式設計師用來釋放物件或資源的方法,但是儘量少用

this和this()

this指的就是當前物件,this()指的就是當前物件的構造方法

public class Main{
	int a,b;
    Main(int a,int b){
        this.a = a;
        this.b = b;
    }
    
    Main(int a,int b,int c){
        this(a,b);
        c = a+b;
    }
}

static

static修飾的方法是屬於類的,而普通的方法是屬於例項(物件)的,所以static修飾的方法不能直接呼叫普通的方法,因為普通的方法需要物件去呼叫

核心就是

static修飾的成員變數和方法,從屬於類。

普通變數和方法從屬於物件。

普通的方法可以呼叫static修飾的方法和變數,但是static修飾的並不能直接呼叫普通方法和變數。

靜態初始化塊

構造方法用於物件的初始化;靜態初始化塊,用於類的初始化操作;所以先執行的是初始化塊,沒有類也就沒有了物件。在靜態初始化塊中不能直接訪問非static成員。

注意

靜態初始化塊執行順序

  1. 上溯到Object類,先執行Object的靜態初始化塊,再向下執行子類的靜態初始化塊,直到我們的類的靜態初始化塊為止。

  2. 構造方法執行順序和上面順序一樣

引數傳值機制

Java內方法的引數都是使用值傳遞,而值傳遞本身又傳遞的是值的副本,所以我們得到的都是影印件而非原件,所以影印件的改變並不影響原件。

基本資料型別的傳值

傳遞的是值的副本,副本改變不會影響原件。

引用型別引數的傳值

傳遞的是值的副本,但是引用型別指的是“物件的地址”。因此,副本和原引數都指向了同一個“地址”,改變“副本指向地址物件的值,也意味著原引數指向物件的值也發生了改變”。

比如

 public class User {
    int id;        //id
    String name;   //賬戶名
    String pwd;   //密碼
       
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
      
    public void testParameterTransfer1(User u){
        u.name="tihom1";
    }
     
    public void testParameterTransfer2(User u){
        u = new User(200,"tihom2");
    }
      
    public static void main(String[] args) {
    	//建立一個User物件,在堆記憶體中產生這個物件的記憶體空間,裡面存的值是100、tihom3,然後u1在棧中引用這個物件,指向的地址假設是123
        User u1 = new User(100, "tihom3");
        //這裡的引數u1是副本,進入方法中,u指向的地址也是123,所以將name的值更改了 
        u1.testParameterTransfer1(u1);
        System.out.println(u1.name); //tihom1
 
 		//這裡傳入的u1是副本,但是在方法內重新建立了新的記憶體空間(地址為124),所以u指向的地址為124並非123
        u1.testParameterTransfer2(u1);
        //但是這裡仍然用的是u1的引用,而u1的值在testParameterTransfer2方法中並未被改變,所以結果依然是tihom1
        System.out.println(u1.name);
    }
}

import

靜態匯入:import static是用來直接引入類中的靜態屬性的

繼承

繼承的概念應該都很瞭解了,注意的就是Java中類只有單繼承,介面有多繼承,且類如果沒extends如何類的話,那麼預設繼承的是Object類,Object類是所有類的根基類

重寫

需要注意的幾個點

  • 方法名、形參列表相同

  • 返回值型別和宣告異常型別,子類小於等於父類

  • 訪問許可權,子類大於等於父類

instanceof

instanceof是二元運算子,左邊是物件,右邊是類,當物件是右邊類或子類所建立的物件時,返回true,反之,false

使用a instanceof b

toString方法

Object類中的toString方法原始碼如下

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

表示會輸出類名[email protected]+16進位制的hashCode,在print或者字串連線時會呼叫該方法。

所以,一般在構造物件時,我們會重寫toString方法,在列印時能更清晰的顯示物件資訊。

”==“、equals方法、hashCode()

對於字串常量來說,使用"=="和equals方法比較字串時

  • “==”比較兩個變數本身的值,即兩個物件在記憶體中的地址

  • equals方法比較的是字串中所包含的內容是否相同

對於非字串變數來說,使用“==”和equals方法的作用是相同的

  • 都是用來比較其物件在堆記憶體的首地址,即用來比較兩個引用變數是否指向同一個物件

Object中有equals方法,比較的是物件內容是否相等,比如通過身份證號碼、學號等等確定是否是同一個人。

Object中的equals方法預設比較的是兩個物件的hashCode,是同一個物件的引用時返回true,否則返回false,同時,我們可以根據自己的不同需求重寫equals方法

複寫equals時需要注意的準則

  • 自反性(reflexive)。對於任意不為null的引用值x,x.equals(x)一定是true
  • 對稱性(symmetric)。對於任意不為null的引用值xy,當且僅當x.equals(y)true時,y.equals(x)也是true
  • 傳遞性(transitive)。對於任意不為null的引用值xyz,如果x.equals(y)true,同時y.equals(z)true,那麼x.equals(z)一定是true
  • 一致性(consistent)。對於任意不為null的引用值xy,如果用於equals比較的物件資訊沒有被修改的話,多次呼叫時x.equals(y)要麼一致地返回true要麼一致地返回false
  • 對於任意不為null的引用值xx.equals(null)返回false

hashCode()方法

在Object類中,hashCode方法的原始碼如下

public native int hashCode()

說明這是一個本地方法,它的實現是根據本地機器相關的。當然我們可以在自己的類中複寫hashCode方法,比如String、Integer、Double等這些類都是複寫了hashCode方法。下面是String中的實現

public int hashCode() {  
    int h = hash;  
    if (h == 0) {  
        int off = offset;  
        char val[] = value;  
        int len = count;  
  
        for (int i = 0; i < len; i++) {  
            h = 31 * h + val[off++];  
        }  
         hash = h;  
    }  
    return h;  
}

hashCode的作用

hashCode與Java集合的聯絡比較明顯,Java集合有兩類,List和Set,List集合中的元素是有序的,而Set中的元素是無序的,那麼是怎麼做到元素不重複的呢,判斷的方法是什麼?

使用的是Object中的equals方法,每增加一個元素就進行一次比較,但是如果資料量大起來之後,沒增加一個都進行比較的話,效率會很慢,比如現在有2000個元素,那麼增加一個元素就要equals比較2000次,任何人都不會設計出這種不科學的方法,所以Java採用了雜湊表的原理。

使用雜湊演算法(雜湊演算法),主要通過求模、異或、移位來實現,有興趣的可以去更深入的瞭解一下。根據演算法找到特定的地址,如果地址位置沒有元素,那麼直接儲存在這個地址上,如果這個地址位置已經有元素,那麼就需要呼叫equals方法,若相同就不存了(因為這裡做到的是不重複),不相同的話就雜湊到別的地址上。所以這裡可能會出現雜湊衝突問題

雜湊衝突

由於雜湊演算法被計算的資料是無限的,而計算後的結果範圍有限,因此總會存在不同的資料經過計算後得到的值相同,這就是雜湊衝突

解決雜湊方法的方法

  • 開放定址法(線性探測再雜湊、平方探測再雜湊)

    線性探測再雜湊很好理解,就是算出來的地址上如果已經存在元素了,那麼就在表格上往後走,假設0位置上衝突了,並且1、2有元素而3沒有,那麼就將0處衝突的元素存在3處,以此類推,後面有衝突的也是這樣解決。

    平方探測再雜湊也不難,假設在1的位置上衝突了,那麼就計算1+12=21+1^2=2,如果2的位置上是空的,那麼就存在2上。如果在2上衝突了,那麼就計算$2+1^2=$3,3位置有元素,那麼計算212=12-1^2=1,如果還是有元素,那麼計算2+22=62+2^2=6,有人就繼續計算222=22-2^2=-2,-2就相當於這個有限表的倒數第二個位置,以此類推下去…

  • 鏈地址法

    將所有雜湊地址相同的記錄都連結在同一連結串列中,具體可以看hashMap的原始碼

  • 再雜湊法

    算出來重複了,那麼就用另外一個演算法去算,直到不重複為止。(猜測)

  • 建立公共溢位區法

    就是不將資料存在那個表中了,放在另外的地方。(猜測)

hashCode()和equals()的聯絡

根據資料查詢和上面講的這麼多東西可以得出來的歸納

  1. 若重寫了equals(Object obj)方法,則有必要重寫hashCode()方法。

  2. 若兩個物件equals(Object obj)返回true,則hashCode()有必要也返回相同的int數。

  3. 若兩個物件equals(Object obj)返回false,則hashCode()不一定返回不同的int數,但為不相等的物件生成不同hashCode值可以提高 雜湊表的效能

  4. 若兩個物件hashCode()返回相同int數,則equals(Object obj)不一定返回true。

  5. 若兩個物件hashCode()返回不同int數,則equals(Object obj)一定返回false。

  6. 同一物件在執行期間若已經儲存在集合中,則不能修改影響hashCode值的相關資訊,否則會導致記憶體洩露問題。

  7. hashCode是為了提高在雜湊結構儲存中查詢的效率,線上性表中沒有作用。

這裡與Java集合的密切關係我準備到時在精進Java集合的時候進行講解,敬請關注嘿嘿嘿

繼承樹的追溯

構造方法第一句總是:super(…)來呼叫父類對應的構造方法,先追溯到Object類,依次向下執行類的初始化和構造方法,直到當前子類為止。

靜態初始化塊的呼叫與構造方法的呼叫流程一致。

封裝

訪問控制符

物件屬性一般使用的是private訪問許可權,一些只在本類使用的輔助方法也使用private,set/get類的需要外面呼叫來賦值與讀取操作的還是使用public

多型

多型指的是一個方法的呼叫,不同物件有不同的處理方案。比如我同樣呼叫“吃飯”這個方法,中國人會用筷子吃飯,美國人會用叉子吃飯,摩洛哥人會用手抓飯。

多型的要點

  1. 多型是方法的多型,不是屬性的多型

  2. 多型的存在有三個必要的條件

    • 繼承
    • 方法重寫
    • 父類引用指向子類物件
  3. 父類引用指向子類物件後,用該父類引用呼叫子類重寫的方法

多型提高了程式碼的可擴充套件性,符合開閉原則。但是多型也有弊端,父類無法呼叫子類特有的方法。不過,如果要使用子類的特有方法,可以使用物件的轉型

物件的轉型

理解何為向上轉型和向下轉型只需要通過程式碼即可清晰瞭解

public class TestCasting {
    public static void main(String[] args) {
        Object obj = new String("tihom"); // 向上可以自動轉型
        // obj.charAt(0) 無法呼叫。編譯器認為obj是Object型別而不是String型別
        // 編寫程式時,如果想呼叫執行時型別的方法,只能進行強制型別轉換,不然通不過編譯器的檢查
        String str = (String) obj; // 向下轉型
        System.out.println(str.charAt(0)); // 位於0索引位置的字元
        System.out.println(obj == str); // true.他們倆執行時是同一個物件
    }
}

在進行強制型別轉換之前,先用instanceof運算子判斷是否可以成功轉換,從而避免出現ClassCastException異常

抽象方法和抽象類

何為抽象類?

就是你無法具體的描述出這個類代表的是什麼,你需要其他類來說明,比如我們定義一個Animal動物類,我們只知道這是動物,但是具體是什麼動物呢,我們需要其他類來說明,假如我在Animal類中定義了talk方法

public abstract class Animal{
    public abstract void talk();
}

public class Dog extends Animal{
	@Override
	public void talk(){
        System.out.println("汪汪汪~");
	}
}

public class Cat extends Animal{
    @Override
    public void talk(){
        System.out.println("喵喵喵~");
    }
}

public static void main(String[] args){
    Animal a1 = new Cat();
    Animal a2 = new Dog();
    //Animal a3 = new Animal();   這樣是錯誤的,無法例項化,只能交給子類
    a1.talk();
    a2.talk();
}

這就是很經典的抽象方法和抽象類,抽象類中定義的抽象方法必須要子類去重寫

注意的點

  1. 抽象類不能被例項化,例項化的工作交給子類去完成,抽象類只需要有一個引用即可。

  2. 抽象方法必須由子類來進行重寫。

  3. 只要包含一個抽象方法的抽象類,該方法必須要定義成抽象類,不管是否還包含有其他方法。

  4. 抽象類中可以包含具體的方法,也可以不包含抽象方法。

  5. 子類中的抽象方法不能與父類的抽象方法同名。

  6. abstract不能與final並列修飾同一個類和方法。

  7. abstract 不能與private、static、final或native並列修飾同一個方法。

介面

何為介面?

可以說是比抽象類還抽象的抽象類,哈哈哈,是不是突然覺得很繞,這裡只是做個描述而已。

準確來說,介面有了更多的約束,JDK7以前介面中的方法必須都是抽象的,並且沒有沒有具體的實現,而JDK8後接口可以使用default、static方法

default方法

簡單的說,就是可以在介面中定義一個已經實現的方法,並且該介面的實現類不需要實現該方法

為什麼要有預設方法?

因為在之前的開發中,我們介面一旦新增或刪除了一個方法,那麼所有實現該介面的類都需要進行修改,如果這個介面被很多類實現了,那麼修改起來還是很麻煩的。所以Java8為了更好的擴充套件性,假如我們要在介面中新增一個方法,那麼只需要使用default實現一個預設方法,不用對實現類進行修改,並且實現類都會繼承這個default方法。

參考了網上的資料,在Java8的Iterable介面中,我們新增了一個預設方法forEach,因為這是default修飾的預設方法,所以不用修改所有實現了Iterable介面的類

default void forEach(Consumer<? super T> action) {   //入參是函式式介面,支援Lambda表示式
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}

因為Collection介面繼承了Iterable介面,所以Collection具有了forEach方法

List<String> list = new ArrayList<String>();
list.add("001");
list.add("002");
list.forEach(System.out::println);

可見,我們在未破壞Iterable介面實現類的前提下,給Iterable介面的所有實現類添加了一個新方法forEach,這在Java 8之前是不可能的。

重寫Override方法

如果介面實現的類沒有重寫介面的預設方法,那麼預設繼承了介面中的預設實現。

如果介面實現類重寫了介面中的預設方法,那麼與普通的重寫沒有區別。

如果子類(介面或抽象類)重寫父介面的預設方法是抽象方法,那麼子類的子類都需要實現這個方法。

預設方法呼叫衝突問題

因為一個類是可以實現多個介面的,那麼如果多個介面都定義了一樣的預設方法,我們實現的時候該如何呼叫父類的預設方法呢?

  1. 首先,如果子類覆蓋了父類的預設方法,那麼直接使用子類覆蓋後的方法e
  2. 其次,優先選擇呼叫更加具體的預設方法,就是說比如介面1繼承了介面2,那麼介面實現類呼叫預設方法時優先呼叫的是介面2的方法,因為介面2的比介面1的更具體
  3. 如果實現類同時實現了介面1和介面2,且介面1和介面2有同名的預設方法,那麼實現類呼叫時會編譯報錯,提示定義了重名的介面,快速修復的方法是覆蓋其中的一個即可
interface InterfaceA{
    default void test(){
        System.out.println("testA~~");
    }
}

interface InterfaceB extends InterfaceA{
    @Override
    default void test(){
        System.out.println("testB~~");
    }
}

interface InterfaceC extends InterfaceA{
    
}

class TestClass implements InterfaceB,InterfaceC{
    @Override
    public void test(){
        InterfaceB.super.test();
        //InterfaceC.super.test();  這句會報錯,報錯說明中的意思是介面B中有比C更具體的實現,所以不使用介面C,預設使用介面B
    }
}

還要注意一個問題,如果該類實現介面時,還繼承了某個抽象類,該抽象類擁有一個和default簽名一樣的抽象方法,則在該類中必須重寫抽象方法(也是介面中的該default方法)

抽象類、介面存在同樣的簽名方法,抽象類有實現體但是不是public修飾的

—-> 如果子類沒有去實現,那麼編譯錯誤:抽象介面中的實現不能隱藏介面中的方法;如果子類實現了方法,編譯通過

—->解決辦法:將抽象類中的方法訪問控制符使用public修飾

沒想到一個小小的介面可以引申出這麼多問題,Java還真是深似海啊。。。

static靜態方法

static修飾的方法,只能使用介面名呼叫,介面.xxx來呼叫,所以它不存在呼叫衝突問題,因為編譯器可以區分不同介面的呼叫

public interface JDK8Interface {
 
    // static修飾符定義靜態方法
    static void staticMethod() {
        System.out.println("介面中的靜態方法");
    }
 
    // default修飾符定義預設方法
    default void defaultMethod() {
        System.out.println("介面中的預設方法");
    }
}

public class JDK8InterfaceImpl implements JDK8Interface {
    //實現介面後,因為預設方法不是抽象方法,所以可以不重寫,但是如果開發需要,也可以重寫
}

public class Main {
    public static void main(String[] args) {
        // static方法必須通過介面類呼叫
        JDK8Interface.staticMethod();
 
        //default方法必須通過實現類的物件呼叫
        new JDK8InterfaceImpl().defaultMethod();
    }
}

上面程式碼已經直觀的表示了

抽象類和介面的區別

儘管抽象類和介面之間存在較大的相同點,甚至有時候還可以互換,但這樣並不能彌補他們之間的差異之處。下面將從語法層次和設計層次兩個方面對抽象類和介面進行闡述。

語法層次

在語法層次,java語言對於抽象類和介面分別給出了不同的定義。下面以Demo類來說明他們之間的不同之處。

使用抽象類來實現:

public abstract class Demo {
    abstract void method1();
    void method2(){
        //實現
    }
}

使用介面來實現

interface Demo {
    void method1();
    void method2();
}

抽象類方式中,抽象類可以擁有任意範圍的成員資料,同時也可以擁有自己的非抽象方法,但是介面方式中,它僅能夠有靜態、不能修改的成員資料(但是我們一般是不會在介面中使用成員資料),同時它所有的方法都必須是抽象的。在某種程度上來說,介面是抽象類的特殊化。(java8之後介面中可以有預設實現的方法)

對子類而言,它只能繼承一個抽象類(這是java為了資料安全而考慮的),但是卻可以實現多個介面。

設計層次

上面只是從語法層次和程式設計角度來區分它們之間的關係,這些都是低層次的,要真正使用好抽象類和介面,我們就必須要從較高層次來區分了。只有從設計理念的角度才能看出它們的本質所在。一般來說他們存在如下三個不同點:

  1. 抽象層次不同。抽象類是對抽象,而介面是對行為的抽象。抽象類是對整個類整體進行抽象,包括屬性、行為,但是介面卻是對類區域性(行為)進行抽象。

  2. 跨域不同。抽象類所跨域的是具有相似特點的類,而介面卻可以跨域不同的類。我們知道抽象類是從子類中發現公共部分,然後泛化成抽象類,子類繼承該父類即可,但是介面不同。實現它的子類可以不存在任何關係,共同之處。例如貓、狗可以抽象成一個動物類抽象類,具備叫的方法。鳥、飛機可以實現飛Fly介面,具備飛的行為,這裡我們總不能將鳥、飛機共用一個父類吧!所以說抽象類所體現的是一種繼承關係,要想使得繼承關係合理,父類和派生類之間必須存在**“is-a”**關係,即父類和派生類在概念本質上應該是相同的。對於介面則不然,並不要求介面的實現者和介面定義在概念本質上是一致的, 僅僅是實現了介面定義的契約而已。

  3. 設計層次不同。對於抽象類而言,它是自下而上來設計的,我們要先知道子類才能抽象出父類,而介面則不同,它根本就不需要知道子類的存在,只需要定義一個規則即可,至於什麼子類、什麼時候怎麼實現它一概不知。比如我們只有一個貓類在這裡,如果你這是就抽象成一個動物類,是不是設計有點兒過度?我們起碼要有兩個動物類,貓、狗在這裡,我們在抽象他們的共同點形成動物抽象類吧!所以說抽象類往往都是通過重構而來的!但是介面就不同,比如說飛,我們根本就不知道會有什麼東西來實現這個飛介面,怎麼實現也不得而知,我們要做的就是事前定義好飛的行為介面。所以說抽象類是自底向上抽象而來的,介面是自頂向下設計出來的。

​ (上面純屬個人見解,如有出入、錯誤之處,望各位指點!!!!)

我們有一個Door的抽象概念,它具備兩個行為open()和close(),此時我們可以定義通過抽象類和介面來定義這個抽象概念:

抽象類

abstract class Door{
    abstract void open();
    abstract void close();
}

介面

interface Door{
    void open();
    void close();
}       

至於其他的具體類可以通過使用extends使用抽象類方式定義Door或者Implements使用介面方式定義Door,這裡發現兩者並沒有什麼很大的差異。

但是現在如果我們需要門具有報警的功能,那麼該如何實現呢?

解決方案一:給Door增加一個報警方法:clarm();

abstract class Door{
    abstract void open();
    abstract void close();
    abstract void alarm();
}

或者

interface Door{
    void open();
    void close();
    void alarm();
}

這種方法違反了面向物件設計中的一個核心原則ISP (Interface Segregation Principle - 介面隔離原理)—見批註,在Door的定義中把Door概念本身固有的行為方法和另外一個概念"報警器"的行為方法混在了一起。這樣引起的一個問題是那些僅僅依賴於Door這個概念的模組會因為"報警器"這個概念的改變而改變,反之依然。

解決方案二

既然open()、close()和alarm()屬於兩個不同的概念,那麼我們依據ISP原則將它們分開定義在兩個代表兩個不同概念的抽象類裡面,定義的方式有三種:

  1. 兩個都使用抽象類來定義。

  2. 兩個都使用介面來定義。

  3. 一個使用抽象類定義,一個是用介面定義。

由於java不支援多繼承所以第一種是不可行的。後面兩種都是可行的,但是選擇何種就反映了你對問題域本質的理解。

如果選擇第二種都是介面來定義,那麼就反映了兩個問題:1、我們可能沒有理解清楚問題域,AlarmDoor在概念本質上到底是門還報警器。2、如果我們對問題域的理解沒有問題,比如我們在分析時確定了AlarmDoor在本質上概念是一致的,那麼我們在設計時就沒有正確的反映出我們的設計意圖。因為你使用了兩個介面來進行定義,他們概念的定義並不能夠反映上述含義。

第三種,如果我們對問題域的理解是這樣的:AlarmDoor本質上Door,但同時它也擁有報警的行為功能,這個時候我們使用第三種方案恰好可以闡述我們的設計意圖。AlarmDoor本質是門,所以對於這個概念我們使用抽象類來定義,同時AlarmDoor具備報警功能,說明它能夠完成報警概念中定義的行為功能,所以alarm可以使用介面來進行定義。如下:

abstract class Door{
    abstract void open();
    abstract void close();
}
 
interface Alarm{
    void alarm();
}
 
class AlarmDoor extends Door implements Alarm{
    void open(){}
    void close(){}
    void alarm(){}
}

這種實現方式基本上能夠明確的反映出我們對於問題領域的理解,正確的揭示我們的設計意圖。其實抽象類表示的是"is-a"關係,介面表示的是"like-a"關係,大家在選擇時可以作為一個依據,當然這是建立在對問題領域的理解上的,比如:如果我們認為AlarmDoor在概念本質上是報警器,同時又具有Door的功能,那麼上述的定義方式就要反過來了。

批註: ISP(Interface Segregation Principle):面向物件的一個核心原則。它表明使用多個專門的介面比使用單一的總介面要好。 一個類對另外一個類的依賴性應當是建立在最小的介面上的。 一個介面代表一個角色,不應當將不同的角色都交給一個介面。沒有關係的介面合併在一起,形成一個臃腫的大介面,這是對角色和介面的汙染。

總結

  1. 抽象類在java語言中所表示的是一種繼承關係,一個子類只能存在一個父類,但是可以存在多個介面。

  2. 在抽象類中可以擁有自己的成員變數和非抽象類方法,但是介面中只能存在靜態的不可變的成員資料(不過一般都不在介面中定義成員資料),而且它的所有方法都是抽象的。

  3. 抽象類和介面所反映的設計理念是不同的,抽象類所代表的是“is-a”的關係,而介面所代表的是“like-a”的關係。

抽象類和介面是Java語言中兩種不同的抽象概念,他們的存在對多型提供了非常好的支援,雖然他們之間存在很大的相似性。但是對於他們的選擇往往反應了您對問題域的理解。只有對問題域的本質有良好的理解,才能做出正確、合理的設計。

面向介面程式設計

面向介面程式設計實現了**“高內聚、低耦合”**

內聚性又稱塊內聯絡。指單個模組的功能強度的度量,即一個模組內部各個元素彼此結合的緊密程度的度量。若一個模組內各元素(語名之間、程式段之間)聯絡的越緊密,則它的內聚性就越高。 高內聚就是在一個模組內,讓每個元素之間都儘可能的緊密相連。也就是充分利用每一個元素的功能,各施所能,以最終實現某個功能。如果某個元素與該模組的關係比較疏鬆的話,可能該模組的結構還不夠完善,或者是該元素是多餘的。

最充分的利用模組中每一個元素的功能,達到功能實現最大化,內聚性越強越好,用最小的資源幹最大的事情

耦合性也稱塊間聯絡。指軟體系統結構中各模組間相互聯絡緊密程度的一種度量。模組之間聯絡越緊密,其耦合性就越強,模組的獨立性則越差。模組間耦合高低取決於模組間介面的複雜性、呼叫的方式及傳遞的資訊。 專案中的各個模組之間的關聯要儘可能的小,耦合性(相互間的聯絡)越低越好,減小“牽一髮而動全身”的可能性

內聚和耦合,需要儘量實現功能的內聚和資料的耦合,在縱向和橫向都要實現優化,縱向主要是對各個層的內聚和耦合,橫向主要是對一個層上的各個模組和類之間的耦合。

並且面向介面程式設計也符合多項面向物件的設計原則,單一職責原則告訴我們實現類要職責單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向介面程式設計;介面隔離原則告訴我們在設計介面的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,他告訴我們要對擴充套件開放,對修改關閉。

寫到這裡先停一下,一是字數也的確有點多了,二是希望先把這段鞏固起來再繼續往下深入。各位大佬們如果覺得有收穫希望能關注關注或者點點收藏哈哈。