💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## [Optional类](https://lingcoder.gitee.io/onjava8/#/book/19-Type-Information?id=optional%e7%b1%bb) 如果你使用内置的`null`来表示没有对象,每次使用引用的时候就必须测试一下引用是否为`null`,这显得有点枯燥,而且势必会产生相当乏味的代码。问题在于`null`没什么自己的行为,只会在你想用它执行任何操作的时候产生`NullPointException`。`java.util.Optional`(首次出现是在[函数式编程](https://lingcoder.gitee.io/onjava8/#/docs/book/13-Functional-Programming)这章)为`null`值提供了一个轻量级代理,`Optional`对象可以防止你的代码直接抛出`NullPointException`。 虽然`Optional`是 Java 8 为了支持流式编程才引入的,但其实它是一个通用的工具。为了证明这点,在本节中,我们会把它用在普通的类中。因为涉及一些运行时检测,所以把这一小节放在了本章。 实际上,在所有地方都使用`Optional`是没有意义的,有时候检查一下是不是`null`也挺好的,或者有时我们可以合理地假设不会出现`null`,甚至有时候检查`NullPointException`异常也是可以接受的。`Optional`最有用武之地的是在那些“更接近数据”的地方,在问题空间中代表实体的对象上。举个简单的例子,很多系统中都有`Person`类型,代码中有些情况下你可能没有一个实际的`Person`对象(或者可能有,但是你还没用关于那个人的所有信息)。这时,在传统方法下,你会用到一个`null`引用,并且在使用的时候测试它是不是`null`。而现在,我们可以使用`Optional`: ~~~ // typeinfo/Person.java // Using Optional with regular classes import onjava.*; import java.util.*; class Person { public final Optional<String> first; public final Optional<String> last; public final Optional<String> address; // etc. public final Boolean empty; Person(String first, String last, String address) { this.first = Optional.ofNullable(first); this.last = Optional.ofNullable(last); this.address = Optional.ofNullable(address); empty = !this.first.isPresent() && !this.last.isPresent() && !this.address.isPresent(); } Person(String first, String last) { this(first, last, null); } Person(String last) { this(null, last, null); } Person() { this(null, null, null); } @Override public String toString() { if (empty) return "<Empty>"; return (first.orElse("") + " " + last.orElse("") + " " + address.orElse("")).trim(); } public static void main(String[] args) { System.out.println(new Person()); System.out.println(new Person("Smith")); System.out.println(new Person("Bob", "Smith")); System.out.println(new Person("Bob", "Smith", "11 Degree Lane, Frostbite Falls, MN")); } } ~~~ 输出结果: ~~~ <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`字段,这样就不会意外触发`NullPointException`了。 现在假设你已经因你惊人的理念而获得了一大笔风险投资,现在你要招兵买马了,但是在虚位以待时,你可以将`Person Optional`对象放在每个`Position`上: ~~~ // typeinfo/Position.java import java.util.*; 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 ~~~ 这里使用`Optional`的方式不太一样。请注意,`title`和`person`都是普通字段,不受`Optional`的保护。但是,修改这些字段的唯一途径是调用`setTitle()`和`setPerson()`方法,这两个都借助`Optional`对字段进行了严格的限制。 同时,我们想保证`title`字段永远不会变成`null`值。为此,我们可以自己在`setTitle()`方法里边检查参数`newTitle`的值。但其实还有更好的做法,函数式编程一大优势就是可以让我们重用经过验证的功能(即便是个很小的功能),以减少自己手动编写代码可能产生的一些小错误。所以在这里,我们用`ofNullable()`把`newTitle`转换一个`Optional`(如果传入的值为`null`,`ofNullable()`返回的将是`Optional.empty()`)。紧接着我们调用了`orElseThrow()`方法,所以如果`newTitle`的值是`null`,你将会得到一个异常。这里我们并没有把`title`保存成`Optional`,但通过应用`Optional`的功能,我们仍然如愿以偿地对这个字段施加了约束。 `EmptyTitleException`是一个`RuntimeException`,因为它意味着程序存在错误。在这个方案里边,你仍然可能会得到一个异常。但不同的是,在错误产生的那一刻(向`setTitle()`传`null`值时)就会抛出异常,而不是发生在其它时刻,需要你通过调试才能发现问题所在。另外,使用`EmptyTitleException`还有助于定位 BUG。 `Person`字段的限制又不太一样:如果你把它的值设为`null`,程序会自动把将它赋值成一个空的`Person`对象。先前我们也用过类似的方法把字段转换成`Optional`,但这里我们是在返回结果的时候使用`orElse(new Person())`插入一个空的`Person`对象替代了`null`。 在`Position`里边,我们没有创建一个表示“空”的标志位或者方法,因为`person`字段的`Person`对象为空,就表示这个`Position`是个空缺位置。之后,你可能会发现你必须添加一个显式的表示“空位”的方法,但是正如 YAGNI\[^2\] (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()`转换),你就不必执行额外的测试了,而可以直接假设所有对象都是有效的。