深度解析 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 接口的需要,并提供了它所代理的类型的确切名字。

猜你喜欢

转载自blog.csdn.net/qq_33589510/article/details/106894533