深度解析 Java 的 Optional 類

使用內置的 null 來表示沒有對象,每次使用引用的時候就必須測試一下引用是否爲 null,這顯得有點枯燥,而且勢必會產生相當乏味的代碼。

null 沒啥行爲,只會產生 NullPointException
java.util.Optionalnull 值提供了一個輕量級代理,Optional 對象可以防止你的代碼拋 NullPointException

雖然 Optional 是 Java 8 爲了支持流式編程才引入的,但其實它是一個通用的工具。實際上,在所有地方都使用 Optional 是沒有意義的,有時候檢查一下是不是 null 也挺好的,或者有時我們可以合理地假設不會出現 null,甚至有時候檢查 NullPointException 異常也是可以接受的。

Optional 最有用武之地的是在那些“更接近數據”的地方,在問題空間中代表實體的對象上。
舉個簡單的例子,很多系統中都有 Person 類型,代碼中有些情況下你可能沒有一個實際的 Person 對象(或者可能有,但是你還沒用關於那個人的所有信息)。這時,在傳統方法下,你會用到一個 null 引用,並且在使用的時候測試它是不是 null。而現在,我們可以使用 Optional

輸出結果:

<Empty>
Smith
Bob Smith
Bob Smith 11 Degree Lane, Frostbite Falls, MN

Person 的設計有時候叫“數據傳輸對象(DTO,data-transfer object)”。
所有字段都是 public final ,所以無 gettersetter 方法。即Person 不可變,只能通過構造器賦值,只能讀而不能修改值。
想修改一個 Person,只能用一個新的 Person 對象來替換它。
empty 字段在對象創建的時候被賦值,用於快速判斷這個 Person 對象是不是空對象。

想使用 Person,就必須使用 Optional 接口才能訪問它的 String 字段,就不會意外觸發 NPE

可將 Person Optional 對象放在每個 Position 上:

class EmptyTitleException extends RuntimeException {
}

class Position {
    private String title;
    private Person person;

    Position(String jobTitle, Person employee) {
        setTitle(jobTitle);
        setPerson(employee);
    }

    Position(String jobTitle) {
        this(jobTitle, null);
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String newTitle) {
        // Throws EmptyTitleException if newTitle is null:
        title = Optional.ofNullable(newTitle)
                .orElseThrow(EmptyTitleException::new);
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person newPerson) {
        // Uses empty Person if newPerson is null:
        person = Optional.ofNullable(newPerson)
                .orElse(new Person());
    }

    @Override
    public String toString() {
        return "Position: " + title +
                ", Employee: " + person;
    }

    public static void main(String[] args) {
        System.out.println(new Position("CEO"));
        System.out.println(new Position("Programmer",
                new Person("Arthur", "Fonzarelli")));
        try {
            new Position(null);
        } catch (Exception e) {
            System.out.println("caught " + e);
        }
    }
}

輸出結果:

Position: CEO, Employee: <Empty>
Position: Programmer, Employee: Arthur Fonzarelli
caught EmptyTitleException

titleperson 都是普通字段,修改唯一途徑是調用 setTitle()setPerson() ,都藉助 Optional 對字段限制。
想保證 title 字段不會成 null,在 setTitle()檢查參數值。但其實還有更好的做法,函數式編程一大優勢就是可以讓我們重用經過驗證的功能,以減少自己手動編寫代碼可能產生的一些小錯誤。

  • 所以用 ofNullable()newTitle 轉換一個 Optional



    nullofNullable()返回Optional.empty()
  • 調用 orElseThrow()

    如果 newTitle 的值是 null,會得到異常。
    這裏我們並沒有把 title 保存成 Optional,但通過應用 Optional 的功能,我們仍對字段加了約束。
    在這個方案裏邊,你仍然可能會得到一個異常。不同的是,錯誤產生那刻(向 setTitle()null 值時)就拋異常,而不發生在其它時刻。使用 EmptyTitleException 有助於定位 BUG。

Person 字段的限制:如果把值設 null,程序會自動把將它賦值成一個空的 Person 對象。先前我們也用過類似的方法把字段轉換成 Option,但這裏我們是在返回結果的時候使用 orElse(new Person()) 插入一個空的 Person 對象替代了 null

Position 裏,沒有創建一個表示“空”的標誌位或者方法,因爲 person 字段的 Person 對象爲空,就表示這個 Position 是個空位置。之後,你可能會發現你必須添加一個顯式的表示“空位”的方法,但是正如 YAGNI (You Aren’t Going to Need It,你永遠不需要它)所言,在初稿時“實現盡最大可能的簡單”,直到程序在某些方面要求你爲其添加一些額外的特性,而不是假設這是必要的。

雖然使用了 Optional,可以免受 NullPointerExceptions,但 Staff 類對此毫不知情。

// typeinfo/Staff.java

import java.util.*;

public class Staff extends ArrayList<Position> {
    public void add(String title, Person person) {
        add(new Position(title, person));
    }

    public void add(String... titles) {
        for (String title : titles)
            add(new Position(title));
    }

    public Staff(String... titles) {
        add(titles);
    }

    public Boolean positionAvailable(String title) {
        for (Position position : this)
            if (position.getTitle().equals(title) &&
                    position.getPerson().empty)
                return true;
        return false;
    }

    public void fillPosition(String title, Person hire) {
        for (Position position : this)
            if (position.getTitle().equals(title) &&
                    position.getPerson().empty) {
                position.setPerson(hire);
                return;
            }
        throw new RuntimeException(
                "Position " + title + " not available");
    }

    public static void main(String[] args) {
        Staff staff = new Staff("President", "CTO",
                "Marketing Manager", "Product Manager",
                "Project Lead", "Software Engineer",
                "Software Engineer", "Software Engineer",
                "Software Engineer", "Test Engineer",
                "Technical Writer");
        staff.fillPosition("President",
                new Person("Me", "Last", "The Top, Lonely At"));
        staff.fillPosition("Project Lead",
                new Person("Janet", "Planner", "The Burbs"));
        if (staff.positionAvailable("Software Engineer"))
            staff.fillPosition("Software Engineer",
                    new Person(
                            "Bob", "Coder", "Bright Light City"));
        System.out.println(staff);
    }
}

輸出結果:

[Position: President, Employee: Me Last The Top, Lonely
At, Position: CTO, Employee: <Empty>, Position:
Marketing Manager, Employee: <Empty>, Position: Product
Manager, Employee: <Empty>, Position: Project Lead,
Employee: Janet Planner The Burbs, Position: Software
Engineer, Employee: Bob Coder Bright Light City,
Position: Software Engineer, Employee: <Empty>,
Position: Software Engineer, Employee: <Empty>,
Position: Software Engineer, Employee: <Empty>,
Position: Test Engineer, Employee: <Empty>, Position:
Technical Writer, Employee: <Empty>]

有些地方你可能還是要測試引用是不是 Optional,這跟檢查是否爲 null 沒什麼不同。但是在其它地方(例如本例中的 toString() 轉換),你就不必執行額外的測試了,而可以直接假設所有對象都是有效的。

標記接口

有時使用標記接口表示空值更方便,把它的名字當做標籤來用即可

用接口取代具體類,即可使用 DynamicProxy 自動創建 Null 對象。
假設有一個 Robot 接口

Operation 包含一個描述和一個命令(這用到了命令模式)。
定義成函數式接口的引用,所以可以把 lambda 表達式或者方法的引用傳給 Operation 的構造器:

現在我們可以創建一個掃雪 Robot
假設許多不同類型的 Robot,想讓每種 Robot 都創建一個 Null 對象來執行一些特殊的操作
本例中,提供 Null 對象所代表 Robot 的確切類型信息。這些信息是通過動態代理捕獲的:

如果你需要一個空 Robot 對象,只需調用 newNullRobot(),並傳遞需要代理的 Robot 類型。這個代理滿足了 RobotNull 接口的需要,並提供了它所代理的類型的確切名字。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章