簡體   English   中英

Java 中的“密封接口”有什么意義?

[英]What is the point of a “sealed interface” in Java?

密封類密封接口Java 15中的一個預覽功能,在 Java 16 中第二個預覽,現在建議在 Java 17 中交付

他們提供了經典的例子,如Shape -> CircleRectangle等。

我理解密封:提供的switch語句示例對我來說很有意義。 但是,密封接口對我來說是個謎。 任何實現接口的類都被迫為它們提供定義。 接口不會損害實現的完整性,因為接口本身是無狀態的。 我是否想將實現限制為幾個選定的類並不重要。

您能告訴我 Java 15+ 中密封接口的正確用例嗎?

盡管接口本身沒有狀態,但它們可以訪問狀態,例如通過 getter,並且可能具有通過default方法對該狀態執行某些操作的代碼。

因此,支持類sealed的推理也可以應用於接口。

你能告訴我 Java 15+ 中密封接口的正確用例嗎?

我編寫了一些實驗代碼和一個支持博客來說明如何使用密封接口為 Java 實現ImmutableCollection接口層次結構,該層次結構提供契約性結構性可驗證的不變性。 我認為這可能是密封接口的實際用例。

該示例包括四個sealed接口: ImmutableCollectionImmutableSetImmutableListImmutableBag ImmutableCollectionImmutableList/Set/Bag擴展。 每個葉接口permits兩個最終的具體實現。 此博客描述了限制接口的設計目標,因此開發人員無法實現“不可變”接口並提供可變實現。

注意:我是Eclipse Collections的提交者。

基本上是在沒有在不同成員之間共享的具體狀態時提供密封的層次結構。 這是實現接口和擴展類之間的主要區別 - 接口沒有自己的字段或構造函數。

但在某種程度上,這不是重要的問題。 真正的問題是為什么你想要一個密封的層次結構。 一旦確定,應該更清楚密封接口適合的位置。

(為示例的人為性和冗長的內容提前道歉)

1. 在沒有“為子類化而設計”的情況下使用子類化。

假設你有一個這樣的類,它在你已經發布的庫中。

public final class Airport {
    private List<String> peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList<>();
    }

    public void bookPerson(String name) {
        this.peopleBooked.add(name);
    }

    public void bookPeople(String... names) {
        for (String name : names) {
            this.bookPerson(name);
        }
    }

    public int peopleBooked() {
        return this.peopleBooked.size();
    }
}

現在,您想向您的圖書館添加一個新版本,該版本將在預訂時打印出預訂人員的姓名。 有幾種可能的途徑可以做到這一點。

如果您是從頭開始設計,您可以合理地將Airport類替換為Airport接口,並將PrintingAirport設計為與這樣的BasicAirport組合。

public interface Airport {
    void bookPerson(String name);

    void bookPeople(String... names);

    int peopleBooked();
}
public final class BasicAirport implements Airport {
    private final List<String> peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList<>();
    }

    @Override
    public void bookPerson(String name) {
        this.peopleBooked.add(name);
    }

    @Override
    public void bookPeople(String... names) {
        for (String name : names) {
            this.bookPerson(name);
        }
    }

    @Override
    public int peopleBooked() {
        return this.peopleBooked.size();
    }
}
public final class PrintingAirport implements Airport {
    private final Airport delegateTo;

    public PrintingAirport(Airport delegateTo) {
        this.delegateTo = delegateTo;
    }

    @Override
    public void bookPerson(String name) {
        System.out.println(name);
        this.delegateTo.bookPerson(name);
    }

    @Override
    public void bookPeople(String... names) {
        for (String name : names) {
            System.out.println(name);
        }

        this.delegateTo.bookPeople(names);
    }

    @Override
    public int peopleBooked() {
        return this.peopleBooked.size();
    }
}

這在我們的假設中是不可行的,因為Airport類已經存在。 將會有對new Airport()和方法的調用, new Airport()方法期望特定類型的Airport類型的東西不能以向后兼容的方式保持,除非我們使用繼承。

因此,要在 Java 15 之前的版本中執行此操作,您需要從類中刪除final並編寫子類。

public class Airport {
    private List<String> peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList<>();
    }

    public void bookPerson(String name) {
        this.peopleBooked.add(name);
    }

    public void bookPeople(String... names) {
        for (String name : names) {
            this.bookPerson(name);
        }
    }

    public int peopleBooked() {
        return this.peopleBooked.size();
    }
}
public final class PrintingAirport extends Airport {
    @Override
    public void bookPerson(String name) {
        System.out.println(name);
        super.bookPerson(name);
    }
}

在這一點上,我們遇到了最基本的繼承問題之一——有很多方法可以“打破封裝”。 因為AirportbookPeople方法恰好在內部調用this.bookPerson ,所以我們的PrintingAirport類按設計工作,因為它的新bookPerson方法最終將為每個人調用一次。

但是如果Airport類改成這樣,

public class Airport {
    private List<String> peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList<>();
    }

    public void bookPerson(String name) {
        this.peopleBooked.add(name);
    }

    public void bookPeople(String... names) {
        for (String name : names) {
            this.peopleBooked.add(name);
        }
    }

    public int peopleBooked() {
        return this.peopleBooked.size();
    }
}

那么PrintingAirport子類將不會正確運行,除非它也覆蓋了bookPeople 進行反向更改,除非它沒有覆蓋bookPeople否則它不會正確運行。

這不是世界末日或任何事情,它只是需要考慮和記錄的事情 - “你如何擴展這個類以及你允許覆蓋什么”,但是當你有一個公共類可以擴展任何人時可以延長它。

如果您跳過記錄如何子類化或沒有記錄足夠多的內容,則很容易出現以下情況:您無法控制使用庫或模塊的代碼可能依賴於您現在遇到的超類的一個小細節。

密封類讓您可以通過將超類打開到僅對您想要的類進行擴展來避免這一點。

public sealed class Airport permits PrintingAirport {
    // ...
}

現在您無需向外部消費者記錄任何內容,只需您自己。

那么接口如何適應這種情況呢? 好吧,假設您確實提前考慮了,並且您擁有通過組合添加功能的系統。

public interface Airport {
    // ...
}
public final class BasicAirport implements Airport {
   // ...
}
public final class PrintingAirport implements Airport {
    // ...
}

你可能不相信你不想使用繼承后保存類之間有一些重復,而是因為你的機場界面公共你需要做一些中間abstract class或類似的東西。

您可以防御並說“您知道嗎,直到我更好地了解我希望此 API 的去向之前,我將成為唯一能夠實現接口的人”。

public sealed interface Airport permits BasicAirport, PrintingAirport {
    // ...
}
public final class BasicAirport implements Airport {
   // ...
}
public final class PrintingAirport implements Airport {
    // ...
}

2. 表示具有不同形狀的數據“案例”。

假設您向 Web 服務發送請求,它將以 JSON 形式返回以下兩種內容之一。

{
    "color": "red",
    "scaryness": 10,
    "boldness": 5
}
{
    "color": "blue",
    "favorite_god": "Poseidon"
}

當然,有些人為設計,但您可以輕松想象一個“類型”字段或類似字段,用於區分將出現的其他字段。

因為這是 Java,所以我們想要將原始的無類型 JSON 表示映射到類中。 讓我們發揮一下這種情況。

一種方法是讓一個類包含所有可能的字段,並且只有一些為null

public enum SillyColor {
    RED, BLUE
}
public final class SillyResponse {
    private final SillyColor color;
    private final Integer scaryness;
    private final Integer boldness;
    private final String favoriteGod;

    private SillyResponse(
        SillyColor color,
        Integer scaryness,
        Integer boldness,
        String favoriteGod
    ) {
        this.color = color;
        this.scaryness = scaryness;
        this.boldness = boldness;
        this.favoriteGod = favoriteGod;
    }

    public static SillyResponse red(int scaryness, int boldness) {
        return new SillyResponse(SillyColor.RED, scaryness, boldness, null);
    }

    public static SillyResponse blue(String favoriteGod) {
        return new SillyResponse(SillyColor.BLUE, null, null, favoriteGod);
    }

    // accessors, toString, equals, hashCode
}

雖然這在技術上有效,因為它確實包含所有數據,但在類型級安全性方面並沒有獲得太多收益。 任何獲得SillyResponse代碼都需要知道在訪問對象的任何其他屬性之前檢查color本身,並且需要知道哪些是安全的。

我們至少可以將color設為枚舉而不是字符串,這樣代碼就不需要處理任何其他顏色,但它仍然遠不理想。 不同的情況變得越復雜或越多,情況就變得更糟。

理想情況下,我們想要做的是為您可以打開的所有案例提供一些通用的超類型。

因為不再需要打開它,所以color屬性不是絕對必要的,但根據個人喜好,您可以將其保留為可在界面上訪問的內容。

public interface SillyResponse {
    SillyColor color();
}

現在這兩個子類將擁有不同的方法集,獲得其中任何一個的代碼可以使用instanceof來找出它們擁有的方法。

public final class Red implements SillyResponse {
    private final int scaryness;
    private final int boldness;

    @Override
    public SillyColor color() {
        return SillyColor.RED;
    }

    // constructor, accessors, toString, equals, hashCode
}
public final class Blue implements SillyResponse {
    private final String favoriteGod;

    @Override
    public SillyColor color() {
        return SillyColor.BLUE;
    }

    // constructor, accessors, toString, equals, hashCode
}

問題是,因為SillyResponse是一個公共接口,任何人都可以實現它,而RedBlue不一定是唯一可以存在的子類。

if (resp instanceof Red) {
    // ... access things only on red ...
}
else if (resp instanceof Blue) {
    // ... access things only on blue ...
}
else {
    throw new RuntimeException("oh no");
}

這意味着這種“哦不”的情況總是會發生。

旁白:在java 15之前,人們使用“類型安全訪問者”模式來解決這個問題。 我建議不要為了你的理智而學習,但如果你很好奇,你可以看看ANTLR生成的代碼 - 它都是一個不同“形狀”數據結構的大層次結構。

密封類讓您說“嘿,這些是唯一重要的情況。”

public sealed interface SillyResponse permits Red, Blue {
    SillyColor color();
}

即使這些案例共享零個方法,該接口也可以像“標記類型”一樣發揮作用,並且在您期望其中一個案例時仍然為您提供要編寫的類型。

public sealed interface SillyResponse permits Red, Blue {
}

在這一點上,您可能會開始看到與枚舉的相似之處。

public enum Color { Red, Blue }

枚舉說“這兩個實例是唯一的兩種可能性。” 他們可以有一些方法和字段。

public enum Color { 
    Red("red"), 
    Blue("blue");

    private final String name;

    private Color(String name) {
        this.name = name;
    }

    public String name() {
        return this.name;
    }
}

但是所有實例都需要具有相同的方法和相同的字段,並且這些值需要是常量。 在密封的層次結構中,您可以獲得相同的“這些是僅有的兩種情況”的保證,但不同的情況可能具有非常量數據和彼此不同的數據 - 如果這是有道理的。

“密封接口 + 2 個或更多記錄類”的整個模式與 rust 的枚舉等構造的意圖非常接近。

這也同樣適用於具有不同行為“形狀”的一般對象,但它們沒有自己的要點。

3. 強制不變量

如果允許子類,則無法保證某些不變性,例如不變性。

// All apples should be immutable!
public interface Apple {
    String color();
}
public class GrannySmith implements Apple {
    public String color; // granny, no!

    public String color() {
        return this.color;
    }
}

並且這些不變量可能會在稍后的代碼中被依賴,比如將對象提供給另一個線程或類似的。 使層次結構密封意味着您可以記錄和保證比允許任意子類化更強的不變量。

關閉

密封接口或多或少與密封類具有相同的目的,當您想在類之間共享超出默認方法所能提供的實現的實現時,您只需使用具體繼承。

假設您編寫了一個身份驗證庫,其中包含一個用於密碼編碼的接口,即char[] encryptPassword(char[] pw) 你的庫提供了幾個用戶可以選擇的實現。

您不希望他能夠傳遞他自己的可能不安全的實現。

接口並不總是完全由它們的 API 單獨定義。 ProtocolFamily為例。 考慮到它的方法,這個接口很容易實現,但結果對於預期的語義沒有用,因為在最好的情況下, 所有接受ProtocolFamily作為輸入的方法只會拋出UnsupportedOperationException

這是一個接口的典型示例,如果該功能存在於早期版本中,則該接口將被密封; 該接口旨在抽象庫導出的實現,但不具有該庫之外的實現。

較新的類型ConstantDesc甚至明確提到了這個意圖:

非平台類不應直接實現ConstantDesc 相反,他們應該擴展DynamicConstantDesc ...

API注意事項:

將來,如果 Java 語言允許, ConstantDesc可能會成為一個密封的接口,除非明確允許的類型,否則它將禁止子類化。

關於可能的用例,密封抽象類和密封接口沒有區別,但是密封接口仍然允許實現者擴展不同的類(在作者設置的限制內)。 或者由enum類型實現。

簡而言之,有時,接口被用來使庫與其客戶端之間的耦合最小,而無意在客戶端實現它。

由於 Java 在版本 14 中引入了記錄,密封接口的一個用例肯定是創建密封記錄。 這對於密封類是不可能的,因為記錄不能擴展類(很像枚舉)。

在此處輸入圖片說明

在 Java 15 之前,開發人員過去常常認為代碼可重用性是目標。 但這並非在所有程度上都是正確的,在某些情況下,我們想要廣泛的可訪問性,但不是為了更好的安全性和代碼庫管理的可擴展性。

這個特性是關於在 Java 中啟用更細粒度的繼承控制。 密封允許類和接口定義它們允許的子類型。

密封的接口使我們能夠清楚地推斷出所有可以實現它的類。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM