使用內置的 null
來表示沒有對象,每次使用引用的時候就必須測試一下引用是否爲 null
,這顯得有點枯燥,而且勢必會產生相當乏味的代碼。
null
沒啥行爲,只會產生 NullPointException
。
java.util.Optional
爲 null
值提供了一個輕量級代理,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
,所以無 getter
和 setter
方法。即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
title
和 person
都是普通字段,修改唯一途徑是調用 setTitle()
、setPerson()
,都藉助 Optional
對字段限制。
想保證 title
字段不會成 null
,在 setTitle()
檢查參數值。但其實還有更好的做法,函數式編程一大優勢就是可以讓我們重用經過驗證的功能,以減少自己手動編寫代碼可能產生的一些小錯誤。
- 所以用
ofNullable()
把newTitle
轉換一個Optional
傳null
,ofNullable()
返回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
類型。這個代理滿足了 Robot
和 Null
接口的需要,並提供了它所代理的類型的確切名字。