JavaRush /Java Blog /Random-TW /設計類別和介面(文章的翻譯)
fatesha
等級 22

設計類別和介面(文章的翻譯)

在 Random-TW 群組發布
設計類別和介面(文章翻譯)- 1

內容

  1. 介紹
  2. 介面
  3. 介面標記
  4. 函數式介面、靜態方法和預設方法
  5. 抽象類別
  6. 不可變(永久)類
  7. 匿名類
  8. 能見度
  9. 遺產
  10. 多重繼承
  11. 繼承與組合
  12. 封裝
  13. 最終類別和方法
  14. 下一步是什麼
  15. 下載原始碼

1. 簡介

無論您使用哪種程式語言(Java 也不例外),遵循良好的設計原則是編寫乾淨、可理解和可驗證程式碼的關鍵;並使其具有長久的生命力並輕鬆支持問題的解決。在本教程的這一部分中,我們將討論 Java 語言提供的基本構建塊,並介紹一些設計原則,以幫助您做出更好的設計決策。更具體地說,我們將討論介面和使用預設方法的介面(Java 8 中的一個新功能)、抽象類別和最終類別、不可變類別、繼承、組合,並重新審視我們在第 1 部分課程「如何創建和銷毀物件”

2. 介面

在物件導向程式設計中,介面的概念構成了契約開發的基礎。簡而言之,介面定義了一組方法(契約),每個需要支援該特定介面的類別必須提供這些方法的實作:一個相當簡單但功能強大的想法。許多程式語言都具有一種或另一種形式的接口,但 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 void performAction();
void performAction();
值得一提的是,介面中的每個方法都隱式宣告為抽象,甚至這些方法宣告也是等效的:
public abstract void performAction();
public void performAction();
void performAction();
至於宣告的常數字段,除了public之外,它們也是隱式靜態的並標記為Final。因此以下聲明也是等效的:
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
最後,嵌套類別、介面或計數除了是public之外,還隱式宣告為static。例如,這些聲明也相當於:
class InnerClass {
}

static class InnerClass {
}
您選擇的樣式是個人喜好,但了解介面的這些簡單屬性可以幫助您避免不必要的打字。

3. 介面標記

標記接口是一種特殊的接口,沒有方法或其他嵌套結構。Java 函式庫如何定義它:
public interface Cloneable {
}
介面標記本身不是契約,而是一種將某些特定特徵與類別「附加」或「關聯」的有用技術。例如,對於Cloneable,該類別被標記為可克隆,但它可以或應該實現的方式不是介面的一部分。介面標記的另一個非常著名且廣泛使用的範例是Serializable
public interface Serializable {
}
此介面將類別標記為適合序列化和反序列化,並且它沒有指定如何或應該如何實現。介面標記在物件導向程式設計中佔有一席之地,儘管它們不符合介面作為契約的主要目的。 

4. 函數介面、預設方法和靜態方法

自從 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 語言的原因有很多,無論它們多麼有用或令人困惑,它們都供您和您使用。函數式介面是一個完全不同的故事,並且已被證明是該語言的非常有用的補充。基本上,函數式介面是一種僅宣告一個抽象方法的介面。Runnable標準庫介面是這個概念的一個很好的例子。
@FunctionalInterface
public interface Runnable {
    void run();
}
Java 編譯器以不同的方式對待函數式接口,並且可以將 lambda 函數轉換為有意義的函數式介面實作。讓我們考慮以下函數描述: 
public void runMe( final Runnable r ) {
    r.run();
}
要在 Java 7 及更低版本中呼叫此函數,必須提供介面的實作Runnable(例如,使用匿名類別),但在 Java 8 中,使用 lambda 語法提供 run() 方法的實作就足夠了:
runMe( () -> System.out.println( "Run!" ) );
此外,@FunctionalInterface註解(註解將在教程的第 5 部分中詳細介紹)暗示編譯器可以檢查介面是否只包含一個抽象方法,因此未來對該介面所做的任何更改都不會違反此假設。

5. 抽象類別

Java 語言支援的另一個有趣的概念是抽象類別的概念。抽象類別有點類似於Java 7中的接口,並且非常接近Java 8中的預設方法接口。與常規類別不同,抽象類別不能實例化,但可以子類別化(更多詳細資訊請參閱繼承部分)。更重要的是,抽象類別可以包含抽象方法:一種沒有實作的特殊方法,就像介面一樣。例如:
package com.javacodegeeks.advanced.design;

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

    public abstract void performAnotherAction();
}
在此範例中,該類別SimpleAbstractClass被聲明為抽象類別並包含一個聲明的抽象方法。抽象類別非常有用;大部分甚至某些部分的實作細節可以在許多子類別之間共享。儘管如此,它們仍然敞開大門,允許您使用抽象方法自訂每個子類別固有的行為。值得一提的是,與只能包含公共聲明的介面不同,抽象類別可以使用可訪問性規則的全部功能來控制抽象方法的可見性。

6. 立即上課

如今,不變性在軟體開發中變得越來越重要。多核心系統的興起引發了許多與資料共享和並行性相關的問題。但肯定出現了一個問題:幾乎沒有(甚至沒有)可變狀態會帶來更好的可擴展性(可伸縮性),並且更容易對系統進行推理。不幸的是,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 );
}
最後,確保正確的訪問(getter)。對於集合,必須以包裝器的形式提供不變性 Collections.unmodifiableXxx:對於數組,提供真正不變性的唯一方法是提供副本而不是返回對數組的引用。從實際角度來看,這可能是不可接受的,因為它非常依賴陣列的大小,並且會給垃圾收集器帶來巨大的壓力。
public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
即使這個小例子也很好地說明了不變性還不是 Java 中的一等公民。如果不可變類別有一個引用另一個類別的物件的字段,事情就會變得複雜。這些類別也應該是不可變的,但沒有辦法確保這一點。有幾種不錯的 Java 原始碼分析器,例如 FindBugs 和 PMD,它們可以透過檢查程式碼並指出常見的 Java 程式設計缺陷來提供很大幫助。這些工具是任何 Java 開發人員的好朋友。

7. 匿名類

在 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();
    }
}

8. 能見度

我們已經在本教程的第 1 部分中討論了 Java 中的可見性和可訪問性規則。在這一部分中,我們將再次回顧這個主題,但是是在子類化的背景下。 設計類別和介面(文章翻譯)- 2不同層級的可見性允許或阻止類別查看其他類別或介面(例如,如果它們位於不同的套件中或相互嵌套)或子類別查看和存取其父類別的方法、建構函式和欄位。在下一節「繼承」中,我們將看到它的實際應用。

9. 繼承

繼承是物件導向程式設計的關鍵概念之一,也是建構一類關係的基礎。結合可見性和可訪問性規則,繼承允許將類別設計成可擴展和維護的層次結構。在概念層面上,Java 中的繼承是使用子類化和extends關鍵字以及父類別來實現的。子類別繼承父類別的所有公共和受保護元素。此外,如果子類別和類別都位於同一個套件中,則子類別將繼承其父類別的套件私有元素。話雖這麼說,無論您嘗試設計什麼,堅持類別公開公開的最小方法集或其子類別都是非常重要的。例如,讓我們看一下該類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。但是,沒有特殊的語法或關鍵字將方法標記為已重寫,這可能會導致混亂。這就是引入@Override註解的原因:每當您的目標是重寫繼承的方法時,請使用@Override註解來簡潔地指示它。Java 開發人員在設計中經常面臨的另一個困境是類別層次結構(具有具體或抽象類別)的建構與介面的實作。我們強烈建議盡可能使用介面而不是類別或抽象類別。介面更輕,更容易測試和維護,並且還可以最大限度地減少實現變更的副作用。許多高階程式技術,例如在 Java 標準庫中建立代理類,都嚴重依賴介面。

10. 多重繼承

與 C++ 和其他一些語言不同,Java 不支援多重繼承:在 Java 中,每個類別只能有一個直接父類別(該類別Object位於層次結構的頂部)。然而,一個類別可以實現多個接口,因此接口堆疊是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
    }
}
實作多個介面其實非常強大,但通常需要一遍又一遍地使用實作會導致深層的類別層次結構,以此來克服 Java 缺乏對多重繼承的支援。 
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 8 在某種程度上解決了預設方法注入的問題。由於預設方法,介面實際上不僅提供了契約,還提供了實作。因此,實作這些介面的類別也會自動繼承這些實作的方法。例如:
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
    }
}
請記住,多重繼承是一個非常強大的工具,但同時也是危險的工具。眾所周知的死亡鑽石問題經常被認為是多重繼承實作中的一個主要缺陷,迫使開發人員非常仔細地設計類別層次結構。不幸的是,具有預設方法的 Java 8 介面也成為這些缺陷的受害者。
interface A {
    default void performAction() {
    }
}

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

interface C extends A {
    @Override
    default void performAction() {
    }
}
例如,以下程式碼片段將無法編譯:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
在這一點上,可以公平地說,Java 作為一種語言一直試圖避免物件導向程式設計的極端情況,但隨著語言的發展,其中一些情況突然開始出現。 

11. 繼承和組合

幸運的是,繼承並不是設計類別的唯一方法。許多開發人員認為比繼承更好的另一種選擇是組合。這個想法非常簡單:不需要建立類別的層次結構,而是需要由其他類別組成。讓我們來看這個例子:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
該類別Vehicle由引擎和車輪組成(以及為簡單起見而保留的許多其他部件)。然而,可以說類別Vehicle也是一個引擎,因此可以使用繼承來設計。 
public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
}
哪一種設計方案是正確的?一般核心準則稱為 IS-A(是)和 HAS-A(包含)原則。IS-A是一種繼承關係:子類別也滿足父類別的類別規範和父類別的變體(子類別)擴展其父類別。如果你想知道一個實體是否擴展了另一個實體,可以做一個匹配測試- IS -A(是)。」)因此,HAS-A 是一種組合關係:一個類別擁有(或包含)一個對象,該對像在大多數情況下,HAS-A 原則比IS-A 更有效,原因有很多: 
  • 設計更加靈活;
  • 該模型更加穩定,因為變更不會透過類別層次結構傳播;
  • 與將父類別與其子類別緊密耦合的組合相比,類別及其組合是鬆散耦合的。
  • 類別中的邏輯思路更簡單,因為它的所有依賴項都包含在其中的一個位置。 
不管怎樣,繼承有它的地位,並以多種方式解決許多現有的設計問題,所以它不應該被忽視。在設計物件導向模型時,請記住這兩種選擇。

12. 封裝。

物件導向程式設計中的封裝概念是向外界隱藏所有實作細節(如操作模式、內部方法等)。封裝的好處是可維護性和易於更改。類別的內部實作是隱藏的,對類別資料的處理僅透過類別的公共方法進行(如果您正在開發一個由許多人使用的程式庫或框架,那麼這是一個真正的問題)。Java 中的封裝是透過可見性和可訪問性規則來實現的。在 Java 中,最佳實踐是永遠不要直接公開字段,而只能透過 getter 和 setter 公開字段(除非字段被標記為 Final)。例如:
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 語言中所謂的JavaBean:標準 Java 類別是根據一組約定編寫的,其中一個約定允許只使用 getter 和 setter 方法來存取欄位。正如我們在繼承部分中已經強調的那樣,請始終遵守類別中的最小公開契約,並使用封裝原則。所有不應該公開的內容都應該變成私有(或受保護/包私有,取決於您要解決的問題)。從長遠來看,這將帶來回報,讓您可以自由地進行設計,而無需進行重大更改(或至少最大限度地減少更改)。 

13. 最終課程與方法

在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類別和方法限制了可擴展性,並且很難提前考慮一個類別是否應該被繼承,或者一個方法是否應該在將來被重寫。這對於庫開發人員來說尤其重要,因為這樣的設計決策可能會嚴重限制庫的適用性。Java 標準函式庫有幾個 Final 類別的範例,最著名的是 String 類別。在早期階段,做出這項決定是為了防止開發人員嘗試提出自己的「更好」的字串實作解決方案。 

14. 下一步是什麼

在課程的這一部分中,我們介紹了 Java 中物件導向程式設計的概念。我們還快速瀏覽了合約編程,觸及了一些函數概念,並了解了該語言如何隨著時間的推移而演變。在本課程的下一部分中,我們將了解泛型以及它們如何改變我們在程式設計中處理類型安全的方式。 

15.下載原始碼

您可以在此處下載原始程式碼 - Advanced-java-part-3 原始碼:如何設計類
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION