天天看點

Java程式設計拾遺『接口與抽象類』

上篇文章講了Java中類和對象的一些基礎概念,本篇文章講述一下,Java中兩種特殊的”類”,接口和抽象類。熟悉Java程式設計的都知道,Java中是不允許多繼承的,有人講Java中可以通過接口實作多繼承,但其實這種說法是不對的,Java之父James Gosling在1995年2月發表了一篇名為”Java: an Overview”的Java白皮書,文章解釋了Java不支援多重繼承的原因。

JAVA omits many rarely used, poorly understood, confusing features of C++ that in our experience bring more grief than benefit. This primarily consists of operator overloading (although it does have method overloading), multiple inheritance, and extensive automatic coercions.

也就是講,Java為了使用的高效快捷,去除了C++中多繼承這一不是很常用,但卻容易出錯的特性。其實也有些人講,Java不支援多繼承是Java語言源生的缺陷。關于是不是的問題,這裡不讨論了,Java之父都這麼講了,我們暫時也不以”批判”的觀點來看這個問題了。轉而,來看一下Java中這兩個特殊的”類”—接口和抽象類。

1. 為什麼Java不支援多繼承

有兩個類B和C繼承自A。假設B和C都繼承了A的方法并且進行了覆寫,編寫了自己的實作。假設D通過多重繼承繼承了B和C,那麼D應該繼承B和C的重載方法,那麼它應該繼承哪個的呢?是B的還是C的呢?如果不兩外進行特殊規定(比如C++的虛基類),肯定是無法區分繼承哪一個方法,産生二義性,這就是多重繼承的菱形繼承問題。

Java程式設計拾遺『接口與抽象類』

James Gosling對Java的定義如下:

Java: 一種簡單的,面向對象的,分布式的,解釋型的,健壯的,安全的,架構中立的,可移植的,高性能的,支援多線程的,動态語言。

也就是講,Java為了其簡單的特性,放棄了面向對象多重繼承這一特點。但其實多重繼承在開發中使用的并不是很多(作為一個Java程式員的了解),即使有多重繼承的需求,也可以通過替代方案來解決(比如之前講的内部類),放棄多重繼承這一特性,對開發者而言,是一個減負的操作。

2. 接口

相信很多人都有一種概念,Java中接口就是用來實作多繼承的。其實這是一種比較常見的誤區。Java中的接口其實是表示一種能力或者講約定一種契約,接口并沒有實作這個能力,它隻是一個約定,它涉及互動兩方對象,一方需要實作這個接口,另一方使用這個接口,但雙方對象并不直接互相依賴,它們隻是通過接口間接互動。舉個簡單的例子,USB協定規定了USB裝置所需要具有的能力,具體的USB裝置都需要實作這些能力,電腦使用USB協定與USB裝置互動,電腦和USB裝置互不依賴,但可以通過USB接口跟所有的USB裝置互動。

Java程式設計拾遺『接口與抽象類』

Java中接口可以了解為一種高度抽象的類,跟類不同的是,類是對實體對象的抽象,并定義了該類對象相關的一系列屬性和行為。而接口更傾向于定義一類對象具備的一些通用能力,并且本身并不去實作這種能力,而将實作交給具體要擁有這種能力的類實作。通過接口進行互動可以更加靈活且耦合性比較小。接口定義的格式如下:

public interface interfaceName {
	[成員變量]
	[成員方法]
}

//比如
public interface MyInterface {
	Integer max_num = 1000;

	Integer compareTo(Object other);
}
           
  • interface關鍵字用來聲明接口,修飾符一般都是public。
  • 接口中的成員變量都是public static final,聲明時必須給賦初值,可以不添加public static final修飾,效果一樣。
  • 接口中的成員方法都是public abstract的,是以沒有方法實作,在聲明接口時,可以不必添加public abstract修飾,效果一樣。
  • 接口也可以繼承,一個接口可以繼承别的接口,接口的繼承使用extends關鍵字,多個父接口之間以逗号分隔。
  • Java8之後,接口中允許實作default方法。

2.1 接口使用示例

2.1.1 接口定義

首先我們模仿java.long.Comparable聲明一個自定義接口MyComparable,表示對象具有可比較的能力。

public interface MyComparable {
    int compareTo(Object other);
}
           

compareTo方法有”兩個參數”,其中一個是顯式參數other,表示用于比較的另一個對象。另一個是”隐式參數”,表示對象本身。compareTo方法傳回值未一個int型數字,1表示本對象比另一個對象大,0表示兩個對象相等,-1表示本對象小于另一個對象。任何實作了MyCompareable的類都需要覆寫compareTo方法,用于對象的比較。

2.1.2 接口實作

自定義一個Point類,并實作MyComparable接口,表示對象是可比較的。比較規則未point距離原點的距離。

@Getter
@Setter
@AllArgsConstructor
public class Point implements MyComparable {

    private int x;
    private int y;

    public double distance() {
        return Math.sqrt(x * x + y * y);
    }

    public int compareTo(Object other) {
        if (!(other instanceof Point)) {
            throw new IllegalArgumentException();
        }
        Point otherPoint = (Point) other;
        double delta = distance() - otherPoint.distance();
        if (delta < 0) {
            return -1;
        } else if (delta > 0) {
            return 1;
        } else {
            return 0;
        }
    }

    @Override
    public String toString() {
        return "(" + x + "," + y + ")";
    }

    public static void main(String[] args) {
        MyComparable p1 = new Point(2,3);
        MyComparable p2 = new Point(1,2);
        System.out.println(p1.compareTo(p2));
    }
}
           

main方法中定義兩個MyComparable變量,指向Point對象,之是以能指派是因為Point實作了MyComparable接口。如果一個類型實作了多個接口,那這種類型的對象就可以被指派給任一接口類型的變量。p1和p2可以調用MyComparable接口的方法,運作時,執行的是具體實作類的代碼(其實就是多态的概念),比較兩個Point對象距離遠點的大小。運作結果如下:

Connected to the target VM, address: '127.0.0.1:4729', transport: 'socket'
1
Disconnected from the target VM, address: '127.0.0.1:4729', transport: 'socket'

Process finished with exit code 0
           

說明p1距離原點更遠。但是上述代碼存在一個問題,我在代碼中本來就知道比較的兩個對象是Point對象,但是卻賦給了MyComparable類型的變量,其實是沒有什麼意義的,完全可以直接賦給Point類型變量。但假如有些情況下,我們無法預知對象的具體類型時,這時候使用接口多态的這種特性的優勢就展現出來了。

public class CompareUtils {

    /**
     * 擷取最大值
     */
    public static Object getMax(MyComparable[] comparables) {
        if (comparables == null || comparables.length == 0) {
            return null;
        }
        MyComparable max = comparables[0];
        for (int i = 1; i < comparables.length; i++) {
            if (max.compareTo(comparables[i]) < 0) {
                max = comparables[i];
            }
        }
        return max;
    }

    /**
     * 升序排序
     */
    public static void sort(MyComparable[] comparables) {
        for (int i = 0; i < comparables.length; i++) {
            int min = i;
            for (int j = i + 1; j < comparables.length; j++) {
                if (comparables[j].compareTo(comparables[min]) < 0) {
                    min = j;
                }
            }
            if (min != i) {
                MyComparable temp = comparables[i];
                comparables[i] = comparables[min];
                comparables[min] = temp;
            }
        }
    }

    public static void main(String[] args) {
        Point[] points = new Point[]{
                new Point(2, 3),
                new Point(3, 4),
                new Point(1, 2)
        };
        System.out.println("max: " + CompareUtils.getMax(points));
        CompareUtils.sort(points);
        System.out.println("sort: " + Arrays.toString(points));
    }
}
           

如上CompareUtils類,我們定義了兩個靜态方法,分别用來擷取一個MyComparable數組的最大值及對數組進行排序。這裡注意一下參數類型為MyComparable[],根據多态的特點,方法可以接收任何實作了MyComparable接口類的數組,是以所有實作了MyComparable接口的類都可以使用該類擷取最大值、排序。

這就是使用接口的好處,針對接口而非具體類型進行程式設計,是一種重要程式設計思維。面向接口程式設計的優點有很多,首先是代碼複用,同一套代碼可以處理多種不同類型的對象,隻要這些對象都有相同的能力。如CompareUtils可以對所有具有比較能力的對象數組進行排序、擷取最大值。其次就是降低了耦合,提高了靈活性,使用接口的代碼依賴的是接口本身,而非實作接口的具體類型,程式可以根據情況替換接口的實作,而不影響接口使用者。比如CompareUtils可以接收任意實作了MyComparable接口的類對象數組,實作相同的效果,但并不用修改代碼。

2.2 Java8中接口的改動

Java8之前,接口規定隻能聲明方法,而不能有方法實作。Java8引入了一個重要的新特性—函數式程式設計,同時允許在接口中實作default method。從Java 8的設計主題來看,default method是為了配合JDK标準庫的函數式風格而設計的。通過default method,很多JDK裡原有的接口都添加了新的可以接收FunctionalInterface參數的方法,使它們更便于以函數式風格使用。

以java.util.List 接口為例,它在Java 7的時候還沒有sort()方法,而到Java 8的時候添加了這個方法。那麼如果我以前在Java 7的時候寫了個自定義類MyList實作了List<T>接口,當時是不需要實作這個sort()方法的。當更新到JDK8的時候,突然發現接口上多了個方法,于是MyList類就也得實作這個方法并且重新編譯才可以繼續使用了,是以就有了default method。上述List.sort()方法在Java 8裡就是一個default method,它在接口上提供了預設實作,于是MyList即便不提供sort()的實作,也會自動從接口上繼承到預設的實作,于是MyList不必重新編譯也可以繼續在Java 8使用。

接口中可以進行方法實作,看起來好像跟抽象類很像了。那是不是可以放棄抽象類了?答案是否定的。因為接口中定義的成員變量預設都是public static final類型的,是以在聲明時就要賦初值。所有子類持有相同的成員變量且無法改變,也就是講Java8的接口是無狀态的,而抽象類中可以定義執行個體變量也可以定義類變量,是有狀态的。另外接口中的default method必須是public的,而抽象類中的方法可以是public、protected、private、default類型的。

由于Java中允許實作多個接口,Java8又允許接口中提供default method實作,那麼肯定會帶來一個問題—菱形繼承問題。如下:

interface InterfaceA {
    default void f() {}
}

interface InterfaceB {
    default void f() {}
}

class InterfaceC implements InterfaceA, InterfaceB {
    
}
           

為了解決以上的沖突,需要手動重寫(override)預設方法,例如:

class InterfaceC implements InterfaceA, InterfaceB {
    public void f() {
        System.out.println("my local f");
    }
}
           

如果想使用特定接口的預設方法,可以使用如下方式:

class InterfaceC implements InterfaceA, InterfaceB {
    public void f() {
        InterfaceA.super.f();
    }
}
           

現階段接口還是無法代替抽象類的,Java 9的接口已經可以有非公有的靜态方法了。未來的Java版本的接口可能會有更強的功能,或許能更大程度地替代原本需要使用抽象類的場景。

3. 抽象類

抽象類顧名思義就是抽象的類,相比于具體的類,抽象類并不是對一個實體實體的抽象,它是一個比類更抽象的概念。相比于接口,它又不是完全抽象的,可以在抽象類中對成員方法進行實作,可以講抽象程度處于接口和類之間。一般而言,具體類有直接對應的對象,而抽象類沒有,它表達的是抽象概念,一般是具體類的比較上層的父類。比如Java中List是一個接口,定義了連結清單的一系列的操作。AbstractList是個抽象類,ArrayList中實作了List接口,并覆寫了List中的多數方法(未覆寫的是abstract抽象方法)。ArrayList繼承抽象類AbstractList,根據需求覆寫非抽象方法,實作AbstractList抽象方法。

3.1 抽象類定義

[類定義修飾符] abstract class <類名> {
	[類變量聲明]
	[成員變量聲明]
	[構造函數]
	[類方法]
	[成員方法]
	[抽象方法]
	[成員變量的get/set方法]
}
           

上述抽象類除了class的abstract修飾符,其他的都是可選的。對比之前具體類的定義格式可以發現,差別在于多了個abstract修飾符,類中有abstract抽象方法(可以沒有),其它的跟普通類的定義一緻。對于abstract抽象方法,隻有聲明,沒有實作,方法由實作類來實作。抽象類和具體類最大的差別是,具體類可以執行個體化對象,抽象類無法執行個體化對象。

3.2 為什麼要使用抽象類

對于抽象方法,可以看作是目前類不知道如何來實作該方法,要結合具體的類才能确定并實作其功能,是以定義為抽象方法,保留給實作類實作。換一種思維,暫時不知道如何實作,先定義一個空方法體,好像也能解決這種需求。單獨定義一個抽象類,而抽象類又不能建立對象,看上去好像增加了一個不必要的限制。其實,引入抽象方法和抽象類,是Java提供的一種文法工具,對于一些類和方法,引導使用者正确使用它們,減少被誤用。使用抽象方法,而非空方法體,子類就知道他必須要實作該方法,而不可能忽略。使用抽象類,類的使用者建立對象的時候,就知道他必須要使用某個具體子類,而不可能誤用不完整的父類。

3.3 抽象類的使用

抽象類和接口有很多相似之處,比如都不能用于建立對象,另外接口中的方法其實都是抽象方法。如果抽象類中隻定義了抽象方法,那抽象類和接口就更像了。但抽象類和接口根本上是不同的,一個類可以實作多個接口,但隻能繼承一個類(包括抽象類)。抽象類和接口是配合而非替代關系,它們經常一起使用,接口聲明能力,抽象類提供預設實作,實作全部或部分方法,一個接口經常有一個對應的抽象類。比如:

  • Collection接口和AbstractCollection抽象類
  • List接口和AbstractList抽象類
  • Map接口和AbstractMap抽象類

對于具體類而言,有兩個選擇,一個是實作接口,自己實作全部方法,另一個則是繼承抽象類,然後根據需要重寫方法。繼承的好處是複用代碼,隻重寫需要的即可,容易實作。不過,如果這個具體類已經有父類了,那就隻能選擇實作接口了。下面展示以下這接口、抽象類和具體類的配合使用。

首先定義一個接口,可以向一個數組添加元素或批量添加或者擷取對應索引的元素值。

public interface ArrayDemoInterface {
    
    void add(int number);

    void addAll(int[] numbers);
    
    int get(int index);
}
           

聲明一個抽象類,實作ArrayDemoInterface接口,接口中實作addAll方法

public abstract class AbstractAdder implements ArrayDemoInterface {

    public void addAll(int[] numbers) {
        for(int num : numbers){
            add(num);
        }
    }
}
           

定義一個具體類,繼承,在具體類中實作add和get方法

public class Base extends AbstractAdder {
    private static final int MAX_NUM = 1000;
    private int[] arr = new int[MAX_NUM];
    private int count;

    public void add(int number) {
        if (count < MAX_NUM) {
            arr[count++] = number;
        }
    }

    public int get(int index) {
        return arr[index];
    }

    public static void main(String[] args) {
        ArrayDemoInterface arrayDemoInterface = new Base();
        arrayDemoInterface.add(1);

        arrayDemoInterface.addAll(new int[]{4, 5, 6});

        System.out.println(arrayDemoInterface.get(1));
    }
}
           

對于具體類Base來說,它可以選擇直接實作ArrayDemoInterface接口,或者從AbstractAdder類繼承。如果選擇實作ArrayDemoInterface接口,那麼在具體類中要覆寫addAll、add、get方法。如果如果繼承,隻需要實作add和get方法就可以了。

參考連結:
  1. 《Java程式設計的邏輯》
  2. Why Multiple Inheritance is Not Supported in Java
  3. Java 8接口有default method後是不是可以放棄抽象類了?