Java進階3 —— 類和接口設計原則

原文連接:http://www.javacodegeeks.com/2015/09/how-to-design-classes-and-interfaces.htmlhtml

本文是Java進階課程的第三篇。java

本課程的目標是幫你更有效的使用Java。其中討論了一些高級主題,包括對象的建立、併發、序列化、反射以及其餘高級特性。本課程將爲你的精通Java的旅程提供幫助。程序員

內容綱要

  1. 引言編程

  2. 接口segmentfault

  3. 標記性接口數組

  4. 函數式接口,默認方法及靜態方法安全

  5. 抽象類併發

  6. 不可變類框架

  7. 匿名類less

  8. 可見性

  9. 繼承

  10. 多重繼承

  11. 繼承與組合

  12. 封裝

  13. Final類和方法

  14. 源碼下載

  15. 下章概要

引言

無論使用哪一種編程語言(Java也不例外),遵循好的設計原則是你編寫乾淨、易讀、易測試代碼的關鍵,而且在程序的整個生命週期中,可提升後期的可維護性。在本章中,咱們將從Java語言提供的基礎構造模塊開始,並引入一組有助於你設計出優秀結構的設計原則。

具體包括:接口接口的默認方法(Java 8新特性),抽象類final類不可變類繼承組合以及在對象的建立與銷燬中介紹過的可見性(訪問控制)規則。

接口

在面向對象編程中,接口構成了基於契約的開發過程的基礎組件。簡而言之,接口定義了一組方法(契約),每一個支持該接口的具體類都必須提供這些方法的實現。這是開發過程當中一種簡單卻強有力的理念。

不少編程語言有一種或多種接口實現形式,而Java語言則提供了語言級的支持。下面簡單看一下Java中的接口定義形式:

package com.javacodegeeks.advanced.design;

public interface SimpleInterface {
    void performAction();
}

在上面的代碼片斷中,命名爲SimpleInterface的接口只定義了一個方法performAction。接口與類的主要區別就在於接口定義了約定(聲明方法),但不爲他們提供具體實現。

在Java中,接口的用法很是豐富:能夠嵌套包含其餘接口、類、枚舉和註解(枚舉和註解將在枚舉和註解的使用中介紹)以及常量,以下:

package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefinitions {
    String CONSTANT = "CONSTANT";

    enum InnerEnum {
        E1, E2;
    }

    class InnerClass {
    }

    interface InnerInterface {
        void performInnerAction();
    }

    void performAction();
}

針對上面的複雜場景,Java編譯器強制爲嵌套的類對象構造和方法聲明提供了一組隱式的要求。首當其衝的即是接口中的每一個聲明必須是public(即使不指定也是public,而且不能設置爲非public,詳細規則可參考可見性部分介紹)。因此下面代碼中的用法與上面看到的聲明是等價的:

public void performAction();
void performAction();

另外,接口中定義的每一個方法都被默認聲明爲abstract的,因此下面的聲明都是等價的:

public abstract void performAction();
public void performAction();
void performAction();

對於常量字段,除了隱式的public外,也被加上了staticfinal修飾,因此下面的聲明也是等價的:

String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";

對於嵌套的類、接口或枚舉的定義,也隱式的聲明爲static的,因此下面的聲明也是等價的:

class InnerClass {
}

static class InnerClass {
}

根據我的偏好可使用任意的聲明風格,不過了解上面的約定卻是能夠減小一些沒必要要的代碼編寫。

標記性接口

標記性接口是接口的一種特殊形式:即沒有任何方法或其餘嵌套定義。在使用Object的通用方法章節中咱們已經見過這種接口:Cloneable,下面是它的定義:

public interface Cloneable {
}

標記性接口並不像普通接口聲明一些契約,但卻爲類「附加」或"綁定"特定的特性提供了支持。例如對於Cloneable,實現了此接口的類就會被認爲具備克隆的能力,儘管如何克隆並未在Cloneable中定義。另一個普遍使用的標記性接口是Serializable

public interface Serializable {
}

這個接口聲明類能夠被序列化或反序列化,一樣它並未指定序列化過程當中使用的方法。

儘管標記性接口並不知足接口做爲契約的主要用途,不過在面向對象設計過程種仍然有必定的用武之地。

函數式接口,默認方法及靜態方法

伴隨着Java 8的發佈,接口被賦予了新的能力:靜態方法、默認方法以及從lambda表達式的自動轉換(函數式接口)。

在上面的接口部分,咱們強調過在Java中接口只能做爲聲明但不能提供任何實現。但默認方法打破了這一原則:在接口中能夠爲default標記的方法提供實現,以下:

package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    void performAction();

    default void performDefaulAction() {
        // Implementation here
    }
}

從對象實例層次看,默認方法可被任何的接口實現者重載;除此以外,接口還提供了另外的靜態方法,以下:

package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    static void createAction() {
        // Implementation here
    }
}

也許你會認爲在接口中提供實現違背了基於契約的開發過程,不也你也能夠列出不少Java把這些特性引入其中的理由。無論是帶來了幫助仍是困擾,它們已然存在,你也可使用它們。

函數式接口有着不一樣的場景,並被認爲是對編程語言的一種強大的擴展。本質上,函數式接口也是接口,不過包含一個抽象的方法聲明。Java 標準庫中的Runnable接口就是這種理念的絕佳範例:

@FunctionalInterface
public interface Runnable {
    void run();
}

Java 編譯器在處理函數式接口時有所不一樣,並能把lamdba表達式轉化爲函數式接口的實現。咱們先看一下下面方法的定義:

public void runMe( final Runnable r ) {
    r.run();
}

在Java 7及之前的版本中,必需要提供Runnable接口的具體實現(例如使用匿名類),但在Java 8中卻能夠經過傳遞lambda表達式來運行run()方法:

runMe( () -> System.out.println( "Run!" ) );

最後,可使用@FunctionalInterfact註解(註解會在枚舉和註解的使用章節進行詳細介紹)告知編譯器以在編譯階段驗證函數式接口中僅包含了一個抽象方法聲明,從而保證將來任何變動的引入不會破壞該接口的函數式特性。

抽象類

抽象類是Java 語言支持的另一個有趣的主題。抽象類與Java 7中的接口有些相似,與Java 8中支持默認方法的接口更爲相像。不一樣於普通類,抽象類不能實例化,但能夠被繼承。更重要的是,抽象類能包含抽象方法:一種沒有定義實現的特殊方法,相似於接口中的方法聲明,以下:

package com.javacodegeeks.advanced.design;

public abstract class SimpleAbstractClass {
    public void performAction() {
        // Implementation here
    }

    public abstract void performAnotherAction();
}

在上述例子中,SimpleAbstractClass類被聲明爲abstract,而且包含了一個abstract方法。當類有部分實現可被子類共享時,抽象類就變得特別有用,由於它還爲子類對抽象方法的定製實現提供了支持入口。

另外,抽象類與接口還有一點不一樣在於接口只能提供public的聲明,而抽象類可以使用全部的訪問控制規則來支持方法的可見性

不可變類

不可變性在現代軟件開發中的地位日益顯著。隨着多核系統的發展,隨之而來引入了大量併發與數據共享的問題(併發最佳實踐中會詳細介紹併發相關主題)。但有一個理念是明確的:系統的可變狀態越少(甚至不可變),擴展性和可維護性就越高。

遺憾的是,Java並未從語言特性上提供強大的不可變性支持。儘管如此,使用一些開發技巧依然能設計出不可變的類和系統。首先要保證類的全部字段均設置爲final,固然這只是一個好的開始,你並不能單純的經過final就徹底保證不可變性:

package com.javacodegeeks.advanced.design;

import java.util.Collection;

public class ImmutableClass {
    private final long id;
    private final String[] arrayOfStrings;
    private final Collection< String > collectionOfString;
}

其次,遵循良好的初始化規則:若是字段聲明的是集合或數組,不要直接經過構造方法的參數進行賦值,而是使用數據複製,從而保證集合或數組的狀態不受外界的變化而改變:

public ImmutableClass( final long id, final String[] arrayOfStrings,
        final Collection< String > collectionOfString) {
    this.id = id;
    this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
    this.collectionOfString = new ArrayList<>( collectionOfString );
}

最後,提供合適的數據獲取手段(getters)。對於集合數據, 應該使用Collections.unmodifiableXxx獲取集合的不可變視圖:

public Collection<String> getCollectionOfString() {
    return Collections.unmodifiableCollection( collectionOfString );
}

對於數組,惟一能保證不可變性的方式只有逐一複製數組中的元素到新數組而不是直接返回原數組的引用。不過有些時候這種作法可能不切實際,由於過大的數組複製將會爲增長垃圾回收的開銷。

public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}

儘管上面的例子提供了一些示範,然而不可變性依然不是Java中的一等公民。當不可變類的字段引用了其餘類的實例時,狀況可能會變得更加複雜。其餘類也應該保證不可變,然而並無簡單有效的途徑進行保證。

有一些優秀的Java源碼分析工具,像FindBugsPMD能幫助你分析代碼並找出常見的Java代碼編寫缺陷。對於任何一個程序員,這些工具都應當成爲你的好幫手。

匿名類

在Java 8以前,匿名類是實如今類定義的地方一併完成實例化的惟一方式。匿名類的目的是減小沒必要要的格式代碼,並以簡捷的方式把類表示爲表達式。下面看下Java中典型的建立線程的方式:

package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread(
            // Example of creating anonymous class which implements
            // Runnable interface
            new Runnable() {
                @Override
                public void run() {
                    // Implementation here
                }
            }
        ).start();
    }
}

在上例中,須要Runnable接口的地方使用了匿名類的實例。儘管使用匿名類時有一些限制,然而其最大的缺點在於Java 語法強加給的煩雜語法。即使實現一個最簡單的匿名類,每次也都須要至少5行代碼來完成:

new Runnable() {
   @Override
   public void run() {
   }
}

好在 Java 8中的lambda表達式和函數式接口消除了這些語法上的固有代碼,使得Java代碼能夠變的更酷:

package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread( () -> { /* Implementation here */ } ).start();
    }
}

可見性

咱們在對象的建立與銷燬章節中已經學習過Java中的可見性與可訪問性的概念,本部分咱們回過頭看看父類中定義的訪問修飾符在子類裏的可見性:

修飾符 包可見性 子類可見性 公開可見性
public 可見 可見 可見
protected 可見 可見 不可見
<無修飾符> 可見 不可見 不可見
private 不可見 不可見 不可見

表 1

不一樣的可見性級別限制了類或接口對其餘類(例如不一樣的包或嵌套的包中的類)的可見性,也控制着子類對父類中定義的方法、函數方法及字段的可見與可訪問性。

在接下面的繼承,咱們會看到父類中的定義對子類的可見性。

繼承

繼承是面向對象編程的核心概念之一,也是構造類的關係的基礎。憑藉着類的可見性與可訪問性規則,經過繼承可實現易擴展和維護的類層次關係。

語法上,Java 中實現繼承的方式是經過extends關鍵字後跟着父類名實現的。子類從父類中繼承全部public和protected的成員,若是子類與父類處於同一個包中,子類也將會繼承只有包訪問權限的成員。不過話說回來,在設計類時,應保持具備最少的公開方法或能被子類繼承的方法。下面經過Parent類和Child類來講明不一樣的可見性及達到的效果:

package com.javacodegeeks.advanced.design;

public class Parent {
    // Everyone can see it
    public static final String CONSTANT = "Constant";

    // No one can access it
    private String privateField;
    // Only subclasses can access it
    protected String protectedField;

    // No one can see it
    private class PrivateClass {
    }

    // Only visible to subclasses
    protected interface ProtectedInterface {
    }

    // Everyone can call it
    public void publicAction() {
    }

    // Only subclass can call it
    protected void protectedAction() {
    }

    // No one can call it
    private void privateAction() {
    }

    // Only subclasses in the same package can call it
    void packageAction() {
    }
}
package com.javacodegeeks.advanced.design;

// Resides in the same package as parent class
public class Child extends Parent implements Parent.ProtectedInterface {
    @Override
    protected void protectedAction() {
        // Calls parent's method implementation
        super.protectedAction();
    }

    @Override
    void packageAction() {
        // Do nothing, no call to parent's method implementation
    }

    public void childAction() {
        this.protectedField = "value";
    }
}

繼承自己就是一個龐大的主題, 在Java語言中也制定了一系列精細的規範。儘管如此,仍是有一些易於遵循的原則幫助你實現精練的類層次結構。在Java中,子類能夠重載從父類中繼承過來的任意非final方法(final的概念參見Final類和方法)。

然而,起初在Java中並無特定的語法或關鍵字標識方法是不是重載了的,這經常會給代碼的編寫引入混淆。所以後來引入了@Override註解用於解決這個問題:當你確實是在重載繼承來的方法時,請使用@Override註解進行標記。

另一個Java開發者常常須要權衡的問題在設計系統時使用類繼承(具體類或抽象類)仍是接口實現。這個建議就是優先選擇接口實現而非繼承。由於接口更爲輕量,易於測試(經過接口mock)和維護,並能下降修改實現所帶來的反作用。不少優秀的編程技術都偏向於依賴接口爲標準Java庫建立代理。

多重繼承

不一樣於C++或其餘編程語言,Java並不支持多重繼承:Java中的每一個類最多隻能有一個直接的父類(在使用Object的通用方法中咱們知道Object類處於繼承層次的頂端)。然而Java中的類能夠實現多個接口,因此實現多個接口是Java中達到多重繼承效果的惟一途徑。

package com.javacodegeeks.advanced.design;

public class MultipleInterfaces implements Runnable, AutoCloseable {
    @Override
    public void run() {
        // Some implementation here
    }

    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}

儘管實現多個接口的方式很是強大,但有時爲了更好的重用某個接口的實現,你不得不經過更深的類繼承層次以達到多重繼承的效果:

public class A implements Runnable {
    @Override
    public void run() {
        // Some implementation here
    }
}
// Class B wants to inherit the implementation of run() method from class A.
public class B extends A implements AutoCloseable {
    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
// Class C wants to inherit the implementation of run() method from class A
// and the implementation of close() method from class B.
public class C extends B implements Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}

Java中引入的默認方法在必定程序上解決了類繼承層次過深的問題。隨着默認方法的引入,接口便不僅是提供方法聲明約束,同時還能夠提供默認的方法實現。相應的,實現了此接口的類也順帶着繼承了接口中實現的方法。示例以下:

package com.javacodegeeks.advanced.design;

public interface DefaultMethods extends Runnable, AutoCloseable {
    @Override
    default void run() {
        // Some implementation here
    }

    @Override
    default void close() throws Exception {
       // Some implementation here
    }
}

// Class C inherits the implementation of run() and close() methods from the
// DefaultMethods interface.
public class C implements DefaultMethods, Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}

多重繼承雖然很強大,卻也是個危險的工具。衆所周知的"死亡鑽石(Diamond of Death)"就常做爲多重繼承的主要缺陷被說起,因此開發者在設計類繼承關係時務必多加當心。湊巧Java 8接口規範裏的默認方法也一樣成了死亡鑽石的犧牲品。

interface A {
    default void performAction() {
    }
}

interface B extends A {
    @Override
    default void performAction() {
    }
}

interface C extends A {
    @Override
    default void performAction() {
    }
}

根據上面的定義,下面的接口E將會編譯失敗:

// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}

坦白的說,做爲面向對象編程語言,Java一貫都在盡力避免一些極端場景。然而避免語言自己的發展,這些極端場景也逐漸暴露。

繼承與組合

好在繼承並不是設計類的關係的惟一方式。組合是另一種被大多開發者認爲優於繼承的設計方法。其主旨也至關簡單:取代層次結構,類應該由其餘類組合而來。

先看一個簡單的例子:

public class Vehicle {
    private Engine engine;
    private Wheels[] wheels;
    // ...
}

Vehicle類由enginewheels組成(簡單起見,忽略了其餘的組成部分)。
不過也有人會說Vehicle也是一個engine,所以應該使用繼承的方式:

public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
}

到底哪一種設計纔是正確的呢?業界通用的原則分別稱之爲IS-AHAS-A規則。IS-A表明的是繼承關係:子類知足父類的規則,從而是父類的一個(IS-A)變量。與之相反,HAS-A表明的是組合關係:類擁有(HAS-A)屬於它的對象。一般,HAS-A優於IS-A,緣由以下:

  • 設計更靈活,便於之後的變動

  • 模型更穩定,變化不會隨着繼承關係擴散

  • 依賴更鬆散,而繼承把父類與子類牢牢的綁在了一塊兒

  • 代碼更易讀,類全部的依賴都被包含在類的成員聲明裏

儘管如此,繼承也有本身的用武之地,在解決問題時不該被忽略。在設計面向對象模型時,要時刻記着組合和繼承這兩種設計方法,儘量多些嘗試以作出最優選擇。

封裝

在面向對象編程中,封裝的含義就是把細節(像狀態、內部方法等)隱藏於內部而不暴露於實現以外。封裝帶來的好處就是提升了可維護性,並便於未來的變動。越少的細節暴露,就會帶來越多的將來變動實現的控制權,而不用擔憂破壞其餘代碼(若是你是一位代碼庫或框架的開發者,必定會遇到這種情景)。

在Java語言中,封裝是經過可見性和可訪問性規則實現的。公認的優秀實踐就是從不直接暴露類的字段,而是經過setter(若是字段沒有聲明爲final)和getter的方式來訪問它。請看下面的例子:

package com.javacodegeeks.advanced.design;

public class Encapsulation {
    private final String email;
    private String address;

    public Encapsulation( final String email ) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getEmail() {
        return email;
    }
}

相似例子中定義的類,在Java中稱做JavaBeans:遵循"只能以setter和getter方法的方式暴露容許外部方法的字段"規則的普通Java類。

如咱們在繼承部分強調過的,遵照封裝原則,把公開的信息最小化。不須要public的時候, 要使用private替代(或者protected/package/private,取決於具體的問題場景)。在未來的維護過程,你會獲得回報:帶給你足夠的自由來優化設計而不會引入破壞性的變動(或者說對外部的變動達到最小化)。

Final 類和方法

在Java中,有一種方式能阻止類被其餘類繼承:把類聲明爲final

package com.javacodegeeks.advanced.design;

public final class FinalClass {
}

final修飾在方法上時,也能達到阻止方法被重載的效果:

package com.javacodegeeks.advanced.design;

public class FinalMethod {
    public final void performAction() {
    }
}

是否應該使用final修飾類或方法並沒有定論。Final的類和方法必定程度上會限制擴展性,而且在設計之初很難判斷類是否會被繼承、方法是否能被重載。對於類庫開發者,尤爲值得注意,使用final可能會嚴重影響類庫的適用性。

Java標準庫中有一些final類的例子,例如衆所周知的String類。在很早時候,就把String設計成了final,從而避免了開發者們自行設計的好壞不一的字符串實現。

源碼下載

能夠從這裏下載本文中的源碼:advanced-java-part-3

下章概要

在本章節中,咱們學習了Java中的面向對象設計的概念。同時簡單介紹了基於契約的開發方式,涉及了一些函數式編程概念,也看到了編程語言隨着時間的演進。在下一章中,咱們將會學習到泛型編程,以及如何實現類型安全的編程。

相關文章
相關標籤/搜索