使用内置的null来表示没有对象,每次使用引用的时候就必须测试一下引用是否为null,这显得有点枯燥,而且势必会产生相当乏味的代码。
null没啥行为,只会产生NullPointException。 java.util.Optional为null值提供了一个轻量级代理,Optional对象可以防止你的代码抛NullPointException。
虽然Optional是 Java 8 为了支持流式编程才引入的,但其实它是一个通用的工具。实际上,在所有地方都使用Optional是没有意义的,有时候检查一下是不是null也挺好的,或者有时我们可以合理地假设不会出现null,甚至有时候检查NullPointException异常也是可以接受的。
Optional最有用武之地的是在那些“更接近数据”的地方,在问题空间中代表实体的对象上。
举个简单的例子,很多系统中都有Person类型,代码中有些情况下你可能没有一个实际的Person对象(或者可能有,但是你还没用关于那个人的所有信息)。这时,在传统方法下,你会用到一个null引用,并且在使用的时候测试它是不是null。而现在,我们可以使用Optional:
输出结果:
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: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:, Position: Marketing Manager, Employee:, Position: Product Manager, Employee:, Position: Project Lead, Employee: Janet Planner The Burbs, Position: Software Engineer, Employee: Bob Coder Bright Light City, Position: Software Engineer, Employee:, Position: Software Engineer, Employee:, Position: Software Engineer, Employee:, Position: Test Engineer, Employee:, Position: Technical Writer, Employee:]
有些地方你可能还是要测试引用是不是Optional,这跟检查是否为null没什么不同。但是在其它地方(例如本例中的toString()转换),你就不必执行额外的测试了,而可以直接假设所有对象都是有效的。
标记接口
有时使用标记接口表示空值更方便,把它的名字当做标签来用即可
用接口取代具体类,即可使用DynamicProxy自动创建Null对象。
假设有一个Robot接口
Operation包含一个描述和一个命令(这用到了命令模式)。
定义成函数式接口的引用,所以可以把 lambda 表达式或者方法的引用传给Operation的构造器:
现在我们可以创建一个扫雪Robot:
假设许多不同类型的Robot,想让每种Robot都创建一个Null对象来执行一些特殊的操作
本例中,提供Null对象所代表Robot的确切类型信息。这些信息是通过动态代理捕获的:
如果你需要一个空Robot对象,只需调用newNullRobot(),并传递需要代理的Robot类型。这个代理满足了Robot和Null接口的需要,并提供了它所代理的类型的确切名字。