亲宝软件园·资讯

展开

Java使用Optional

吃人陈 人气:0

前言

尽管存在争议,但Optiont极大地改进了Java应用程序的设计。在本文中,我们将了解如何、何时以及在哪里最好地应用Optional。

Optional类的引入是Java语言设计的重大改进,但这一改进一直存在争议。在Optional类之前,许多开发人员使用null或异常来表示何时不存在所需的值;然而,使用Optional类可以让我们明确说明何时可能存在或不存在值。尽管有这种改进,但Optional类可能会不恰当地应用,弊大于利。

在本文中,我们将研究Optional类的基本原理,包括:

Java,像大多数面向对象(OO)语言一样,对它的类型系统有一个秘密的后门。例如,假设我们有以下方法签名:

public Foo doSomething();

显然,此方法返回类型为Foo的对象,但它也可以返回另一个值:null。由于任何非原始类型都可以设置为null,因此没有什么可以阻止开发人员编写以下方法实现:

public Foo doSomething() {
    return null; 
}

Nullity

这为调用此方法的客户端造成了一个繁琐的情况。在使用从doSomething方法返回的Foo对象之前,客户端必须首先检查该对象是否为null

Foo foo = doSomething();
if (foo == null) {
    // handle null case...
}
else {
    // do something with the foo object...
}

这种方法确保了Foo对象的使用不会导致NullPointerException (NPE)。然而,这又产生了另一个问题:任何非原语对象都可以隐式地设置为null。因此,在使用方法之前,我们必须彻底检查方法返回的每个对象是否为空。不用说,这不仅在大型应用程序中是不可行的,而且还会混淆代码的清晰度。例如,为null检查添加额外的行会在整个应用程序中引入样板代码,使Foo对象的使用变得不那么清晰(隐藏在if-else语句中)。

根本问题是,我们不知道方法何时打算返回null——例如当找不到所需的值时——或者可以根据要求保证永远不会返回null。由于我们不确定,我们被迫假设任何返回的对象都可以为null

一个常用的解决方案是记录使用JavaDocs返回值可以为null。虽然这是对原始问题的改进,但这并不能确保客户端在使用对象之前检查空性(即Java编译器将毫无怨无故地编译代码,而无需进行这些null检查)。同样,像@NotNull注释也存在,但这些注释与文档方法存在相同的缺点。也就是说,可以规避执法。

Optional Class

相反,我们需要一种机制,允许方法开发人员显式表示方法的返回值可能存在,也可能不存在。该机制以[Optional]类的形式在Java开发工具包(JDK)7中引入。该类充当可能不存在的对象的包装器。因此,如果我们知道我们的doSomething方法可能不会返回所需的Foo对象,我们可以将签名更改为:

public Optional<Foo> doSomething();

正如我们将在以下各节中看到的,Optional提供了一套方法——许多功能性——允许客户端在不存在所需值时决定该怎么做。例如,当找不到所需的值时,我们可以使用orElse方法返回默认值(在Optional词典中称为空的Optional

Foo foo = doSomething().orElse(new Foo());

同样,当Optional使用orElseThrow方法为空时,我们也可以抛出异常:

Foo foo = doSomething().orElseThrow(SomeException::new);

重要的是要注意两件事:

虽然文档和注释确实将我们推向了正确、更明确的方向,但它们不允许我们将检查缺失值的责任强加给客户。另一方面,Optional对象要求客户端决定如何处理缺失的值。

例如,以下内容不会编译:

Foo foo = doSomething();

相反,我们将看到一个编译错误,提醒我们,类型为Optional<Foo>的对象不能转换为Foo,因为doSomething的返回类型是Optional<Foo>,而foo的类型是Foo。因此,我们必须调用orElseorElseThrow等方法——或get,但我们稍后会看到为什么这不应该是首选——以便将Optional<Foo>对象转换为Foo对象。由于这两种方法都需要一个参数,因此,它们要求我们明确决定使用什么默认值,或者如果Optional为空,则抛出什么异常。

客户责任

这让我们注意到(2):处理空的Optional的责任在于客户。从本质上讲,doSomething方法——本质上返回Optional<Foo>而不是Foo——告诉客户端,可能无法找到结果。因此,当找不到结果时,客户端现在负责处理案件(即必须调用其中一种Optional方法,如orElse,才能从Optional<Foo>Foo)。

这种对客户端负责的方法意味着方法开发人员没有足够的信息来确定在缺少值的情况下应该做什么。当找不到值时,开发人员可以抛出异常,但缺失的值可能不是需要例外的情况(即它可能不是例外情况)。

例如,如果我们想检查一个对象是否已经存在,或者如果没有,创建一个,那么不存在的对象不会是一个错误,抛出异常是没有道理的。此外,抛出异常需要我们在调用代码中捕获异常,以创建默认值。

例如,假设我们创建以下方法:

public Foo findIfExists() throws FooNotFoundException;

要使用现有值,或者如果不存在,则创建默认值,我们必须执行以下操作:

Foo foo = null;
try {
    foo = findIfExists();
}
catch (FooNotFoundException e) {
    foo = // create default value...
}

相反,我们可以从findIfExists返回Optional值:

public Optional<Foo> findIfExists();

然后,我们可以简单地使用orElse方法来提供默认值:

Foo foo = findIfExists().orElse(/* create default value... */);

另外,后一种方法可读性更强。通过简单地阅读代码,我们知道这两行意味着查找是否存在或使用该值。在前一种情况下,当findIfExists无法找到现有值时,我们必须有意识地将catch子句的含义派生为默认值。因此,Optional词汇比异常方法更接近预期的含义。

了解这一点,当客户端负责处理丢失的对象而缺少对象不是错误时,Optional是一种有用的技术。有时,缺失的值可能是错误——例如,当我们假设一个值存在,而其缺失可能会给应用程序带来致命结果时——方法应该抛出一个选中或未检查的异常。然而,在某些情况下(例如findIfExists方法),缺席对象不是错误,使用Optional是确保客户端显式处理缺失对象的有效方法。

null Optional Objects

必须解决一个警告:Optional对象可能是null的。由于Optional对象和Foo一样是非原始对象,因此它们也可以设置为null。例如,doSomething的以下实现将编译无错误:

public Optional<Foo> doSomething() {
    return null;

这将导致客户端必须同时处理null返回值和处理空Optional情况的奇怪情况:

Optional<Foo> possibleFoo = doSomething();
if (possibleFoo == null) {
    // handle missing object case...
}
else {
    Foo foo = possibleFoo.orElse(/* handle missing object case... */); 
}

这不仅为丢失的对象情况带来了重复(即在两个位置处理丢失的对象的情况),而且还重新引入了清晰度降低的问题。相反,当从方法返回Optional值时,我们不应该检查null值。根据Optional类文档

类型为Optional的变量本身永远不应该null;它应该始终指向Optional实例。

如果返回null值代替Optional对象,则方法开发人员违反了方法规定。通过声明方法将返回Optional对象,方法开发人员还声明返回null无效。由于Optional对象表示对象丢失的可能性,因此null值没有有效的用例(即方法在所有情况下都应该返回空的Optional而不是null)。

因此,每当我们处理Optional对象时,我们正确地假设Optional对象永远不会为null。虽然Optional对象在实践中可能是null的,但这个问题应该由方法开发人员而不是客户端解决。

重要方法

通过了解Optional类背后的概念,我们现在可以看看如何在实践中使用Optional对象。Optional类包含大量方法,可以分为两类:创建方法和实例方法。

创建方法

Optional创建方法是静态方法,允许我们创建各种Optional对象来满足我们的需求。目前有三种这样的方法:一种用于创建填充的Optional(即包装值不是null的Optional),一种用于创建填充或空的Optional,一种用于创建空的Optional

of

方法of静态允许我们用Optional对象包装现有对象。如果现有对象不是null,则返回填充的Optional

Optional<Foo> foo = Optional.of(new Foo());

如果现有对象为null,则抛出NPE:

Optional<Foo> foo = Optional.of(null); // 抛出 NullPointerException

ofNullable

当传递非null值时, ofNullable静态方法与方法相同(即生成填充的Optional),但在传递null时将生成空Optional(即不会抛出NPE):

Optional<Foo> foo1 = Optional.ofNullable(new Foo()); // populated Optional
Optional<Foo> foo2 = Optional.ofNullable(null); // null Optional

当对象的空性未知时,通常使用这种方法。

empty

empty静态方法只需创建一个空的Optional

Optional<Foo> foo = Optional.empty(); // empty

根据定义,这种方法与以下方法相同:

Optional<Foo> foo = Optional.ofNullable(null);

正如我们将在下面几节中看到的,empty 通常在已知没有值存在的情况下使用。

实例方法

实例方法允许我们与现有的Optional对象交互,并主要专注于查询Optional对象的状态,从Optional对象获取包装对象,并操作Optional对象。

isPresent&isEmpty

Optional类中包含两种查询方法,允许我们检查给定的Optional是填充的还是空的:

因此,给定填充的Optional,查询方法将返回以下内容:

Optional<Foo> populated = // ...populated Optional...
populated.isPresent(); // true
populated.isEmpty(); // false

给定一个空的Optional,查询方法将返回以下内容:

Optional<Foo> empty = // ...empty Optional...
populated.isPresent();    // false
populated.isEmpty();      // true

get

如果Optional被填充,get方法将获得由Optional包装的值,如果Optional为空,则抛出NoSuchElementException。当我们可以保证填充Optional时,此方法可用于将现有Optional转换为其值(即从Optional<Foo>转换为Foo),但我们应该谨慎使用此方法。

在实践中,保证填充Optional需要我们首先使用isPresentisEmpty方法查询Optional,然后调用get

Optional<Foo> possibleFoo = doSomething();
if (possibleFoo.isPresent()) {
    Foo foo = possibleFoo.get();
    // ...use the foo object...
}
else {
// ...handle case of missing Foo...
}

这种模式的问题在于,这与我们在引入Optional之前执行的null检查非常相似。因此,这种方法消除了Optional类的固有好处。在大多数情况下,我们应该避免使用get方法,并使用其他方法之一(如orElseorElseThrow)来获取与填充的Optional值相关联。

orElse系列

orElse系列方法允许我们获得由Optional包装的值(如果填充了Optional),或者如果Optional为空,则获取默认方法。这个系列中最简单的方法是orElse,它接受包装类型的对象,如果Optional是空的,则返回它。例如,给定anOptionalOptional<Foo>对象,orElse方法接受一个Foo对象。如果填充了Optional,它会返回填充值;如果Optional为空,则返回我们传递给orElse方法的Foo对象:

Optional<Foo> possibleFoo = doSomething();
Foo foo = possibleFoo.orElse(new Foo());

然而,有时创建默认值可能是一项昂贵的操作,并且不太可能使用。例如,默认值可能需要建立到远程服务器的连接,或者可能需要从数据库进行扩展或大型查找。如果可能填充Optional,我们不太可能需要默认值。使用orElse方法,即使未使用,我们也被迫创建默认值,这可能会导致严重的性能影响。

Optional类还包括orElseGet方法,该方法采用可以懒散创建默认对象的Supplier。这允许Optional类仅在需要时创建默认对象(即仅在Optional为空时创建默认对象)。

例如:

Optional<Foo> possibleFoo = doSomething();
Foo foo = possibleFoo
    .orElseGet(() -> { /* ...lazily create a Foo object... */ });

orElseThrow系列

orElse方法类似,Optional类提供了一个orElseThrow方法,如果Optional为空,则允许我们在获取包装值时抛出异常。然而,与orElse方法不同,orElseThrow方法有两种形式:

例如,我们可以从Optional<Foo>对象中获取Foo对象,

如下所示:

Optional<Foo> possibleFoo = doSomething();
Foo foo = possibleFoo
    .orElseThrow();

如果Optional是空的,将抛出NoSuchElementException。如果填充了Optional,则将返回包装值。因此,orElseThrow方法的功能与get方法相同,但它的名称更好地描述了其目的。因此,当Optional未填充时,orElseThrow方法应使用任何值的任何地方来抛出NoSuchElementException,而无需首先检查它是否已填充(即不使用isPresentisEmpty查询方法)。

get方法仅当在Optional查询方法之一中使用时才应保留用于(即首先选中Optional的填充或空状态)。请注意,此orElseThrow方法是在JDK 9中引入的,以减少围绕get方法使用的混乱,应该比get方法更喜欢。

我们[在Java 8]中犯的少数错误之一是命名Optional.get(),因为这个名字只是邀请人们在不调用isPresent()用它,首先破坏了使用Optional的全部意义......

在Java 9时间范围内,我们建议弃用Optional.get(),但公众对此的反应是......比说冷。作为较小的一步,我们在[Java] 10中引入了orElseThrow()...作为当前get()有害行为的更透明的同义词。

Optional类还包括一个重载的orElseThrow方法,当Optional为空时,该方法会抛出自定义异常。此方法接受创建任何Throwable对象或Throwable子类的对象的Suppler并抛出它。例如:

Optional<Foo> possibleFoo = doSomething();
Foo foo = possibleFoo.orElseThrow(() -> { /* ...lazily create a Foo object... */ });

当客户端认为丢失的对象是一个错误,并希望在访问空的Optional时抛出异常时,这非常有用。使用构造函数的功能形式抛出简单的异常也是一种常见的做法:

Optional<Foo> possibleFoo = doSomething();
Foo foo = possibleFoo.orElseThrow(SomeException::new);

ifPresent系列

如果Optional被填充,ifPresent方法接受一个Consumer,该Consumer使用包装的值执行操作。这是使用orElse或orElseThrow方法获得包装对象的函数替代,主要是当我们不希望在值不存在的情况下执行任何操作时。

例如:

Optional<Foo> possibleFoo = doSomething();
possibleFoo.ifPresent(foo -> { /* ...do something with foo... */ });

Optional类还包括类似的方法,ifPresentOrElse,允许我们在Optional也是空时处理案例。ifPresentOrElse方法接受的第一个参数是Consumer,如果填充了Optional,则使用包装值执行操作,而第二个参数是Runnable,如果Optional为空,则执行操作。因此,只有当Optional被填充时才会调用Consumer,而只有当Optional为空时,才会调用Runnable。例如:

Optional<Foo> possibleFoo = doSomething();
possibleFoo.ifPresentOrElse(
    foo -> { /* ...do something with foo... */ },
    () -> { /* ...do something when no foo found... */ }

这两种方法的好处是,如果Optional为空,则永远不会调用Consumer。同样,在ifPresentOrElse的情况下,如果填充了Optional,则永远不会调用Runnable。这使我们能够提供复杂或昂贵的操作,这些操作将根据Optional状态被懒惰地调用。

请注意,这种方法不应该仅仅用于昂贵的操作。每当对填充值执行操作时,都应使用此方法。例如,如果我们只想在对象存在的情况下更新它,我们可以做一些类似于以下内容的事情:

public class Bar {
    private boolean isUpdated = false;
    public void update() {
        isUpdated = true;
    }
}
public Optional<Bar> findBar() {
    // ...return a populated Bar if it could be found...
}
findBar().ifPresent(bar -> bar.update());

在这种情况下,如果找不到Bar对象,我们不关心执行任何操作。如果我们是,我们可以改用ifPresentOrElse方法。

map

如果填充了Optionalmap方法允许我们将包装值从一个对象转换为另一个对象。这种方法可以被认为是一种管道方法,其中包装的值沿着管道传递并转换为新值。此方法的工作原理是接受应用于包装值的Function对象,以生成映射值。如果Optional是空的,则永远不会调用Function对象,并且从map方法返回空的Optional

当我们不知道是否存在一个值时,这种方法非常有用,但如果存在,它应该转换为另一个对象。这是从数据库读取时常见的用例,数据库通常存储数据传输对象(DTO)。在大多数应用程序中,DTO用于有效地将域对象存储在数据库中,但在应用程序的更高级别上,需要域对象本身。因此,我们必须从DTO转换为域对象。

如果我们对数据库对象进行查找,我们可能会找到也可能找不到该对象。因此,这是返回Optional包装DTO的好用例。为了转换为域对象,我们可以使用map方法。例如,假设我们有一个DTO(PersonDto),将Person对象的名称存储在一行中,而Person对象的名称分为名字和姓氏(即,该名称在PersonDto对象中存储为"John Doe"但它在Person对象中以"John"的名字和"Joe"的姓氏存储)。我们可以使用映射器对象从PersonDto转换为Person对象,并使用映射器将从数据库返回的PersonDto对象映射到Person对象:

public class Person {
    private String firstName;
    private String lastName;
    // ...getters & setters...
}
public class PersonDto {
    private String name;
    // ...getters & setters...
}
public class PersonMapper {
    public Person fromDto(PersonDto dto) {
        String[] names = dto.getName().split("\s+");
        Person person = new Person();
        person.setFirstName(names[0]);
        person.setLastName(names[1]);
        return person;
    }
}
public class Database {
    public Optional<PersonDto> findPerson() {
        // ...return populated DTO if DTO is found...
    }
}
Database db = new Database();
PersonMapper mapper = new PersonMapper();
Optional<Person> person = db.findPerson()
    .map(mapper::fromDto);

注意,可能有一个转换会导致一个空的Optional。例如,如果从给定对象到另一个对象的转换是不可能的,那么map方法应该返回一个空的Optional。执行这种技术的反模式是让Function对象返回null,然后用map方法(使用ofNullable,它允许我们的null对象在不抛出异常的情况下被包装)包装到空的可选对象中:

Optional<Person> person = db.findPerson()
    .map(dto -> {
        if (dtoCanBeConverted()) {
            return mapper.fromDto(dto);
       }
       else {
            return null;
        }
    });

如果方法dtoCanBeConverted计算为false,则Function对象返回null,从而导致person为空的Optional。这种方法存在缺陷,因为它重新引入了null值的隐式使用,其替换是Optional类的原始目的。相反,我们应该使用flatMap方法,并显式返回一个空的Optional

flatMap

flatMap方法类似于map方法,但flatMap接受一个Function对象,该函数将包装值转换为新的Optional。与map方法不同,flatMap允许我们返回我们选择的Optional。因此,如果映射Function无法转换包装值,我们可以显式返回空的Optional值:

Optional<Person> person = db.findPerson()
    .flatMap(dto -> {
        if (dtoCanBeConverted()) {
            Person person = return dao.fromDto(dto);
            return Optional.ofNullable(person);
        }
        else {
            return Optional.empty();
        }
    });

需要注意的是,我们不再能够像使用map方法那样简单地返回Person对象。相反,我们现在负责将转换后的对象包装成Optional。注意,如果Function对象返回null Optional,则抛出一个NPE。例如,以下代码在执行时会抛出一个NPE:

Optional<Person> person = db.findPerson()
    .flatMap(dto -> null);

filter

如果填充的Optional满足提供的Predicate,则filter方法允许我们返回填充的Optional。因此,如果filter方法应用于空的Optional,则不会调用Predicate。同样,如果filter方法应用于填充的Optional,但包装值不满足提供的Predicate(即Predicate对象的test方法计算false),则返回一个空的Optional。例如:

public class Bar {
    private int number;
    public Bar(int number) {
        this.number = number;
    }
    // ...getters & setters...
}
Predicate<Bar> greaterThanZero = bar -> bar.getNumber() > 0;
Optional.of(new Bar(1))
    .filter(greaterThanZero)
    .isPresent();              // true
Optional.of(new Bar(-1))
    .filter(greaterThanZero)
    .isPresent();              // false

何时使用

Optional类最具争议的方面之一是何时应该和不应该使用它。在本节中,我们将研究一些常见的用例,例如方法返回值、字段和参数,其中Optional可能非常适合也可能不合适。

返回值

正如我们在本文中看到的那样,Optional值非常适合方法返回值,因为这是其预期目的。根据Optional类文档

Optional主要用于方法返回类型,其中明确需要表示“无结果”,并且使用null可能会导致错误。

一般来说,在以下情况下,应使用Optional作为返回值:

Optional返回值通常用于可能找到或找不到所需对象的查询。

例如,存储库通常将以以下方式定义:

public interface BookRepository {
    public Optional<Book> findById(long id);
    public Optional<Book> findByTitle(String title);
    // ...
}

这允许客户端以适合调用方法的上下文的方式处理丢失的Book对象,例如忽略丢失的对象、创建默认对象或抛出异常。

字段

虽然Optional对象非常适合返回类型,但它们不太适合例如字段。可以创建一个类似于以下内容的字段,但这是非常不可取的:

public class Bar {
    private Optional<Foo> foo;
    // ...getters & setters...
}

Optional应避免字段,因为Optional类不可序列化(即没有实现Serializable接口)。

当然,人们会做他们想做的事。但我们添加此功能时确实有明确的意图,这不是一个通用的目的,也许类型,就像许多人希望我们这样做一样。我们的意图是为库方法返回类型提供有限的机制,其中需要一种明确的方法来表示“无结果”,因此使用null极有可能导致错误。

因此,Optional类型仅适用于方法返回类型。由于字段构成类的内部状态,外部客户端不应可见,如果字段被认为是可选的,则可以创建一个getter,返回Optional对象:

public class Bar {
    private Foo foo;
    public Optional<Foo> getFoo() {
        return Optional.ofNullable(foo);
    }
}

使用这种技术,客户会明确被告知foo值可能存在,也可能不存在,同时保持Bar的可序列化性。

参数

在有效的情况下,方法或构造函数的参数可能是可选的,但Optional的不应用于此目的。

例如,应避免以下情况:

public class Bar {
    public void doSomething(Optional<Foo> foo) {
        // ...
    }
}

不应将参数类型设置为Optional,而应使用方法重载:

public class Bar {
    public void doSomething() {
        // ...
    }
    public void doSomething(Bar bar) {
        // ...
    }
}

此外,具有不同方法名称的非过载方法也可以使用:

public class Bar {
    public void doSomething() {
        // ...
    }
    public void doSomethingWithBar(Bar bar) {
        // ...
    }
}

替代方案

虽然Optional类在正确的上下文中有效,但当可能找到或找不到所需的值时,它并不是可以使用的唯一方法。在本节中,我们涵盖了Optional类的三种替代方案,以及如何在适当的上下文中应用它们。

null

最简单的替代方案是使用null,正如我们在本文开头看到的那样。虽然这种方法确实实现了我们的目标,但在引入Optional类后,仅当Optional对象需要太多的开销时,才应使用null。此开销可以是Optional包装类的额外内存需求,也可以是执行Optional方法所需的额外周期。

Optional更有效的情况下,人们很容易以性能为借口使用null,但是在大多数应用程序中,Optional类只增加了少量的开销。除非我们处理的是低级代码,就像来自网络或驱动程序的字节一样,或者我们处理的是极其大量的数据,否则对于方法返回类型来说,可选项应该总是优先于null

空对象

null值更有效的替代方案是引入空对象。null对象是扩展所需类型的对象,但包含本应为null大小写执行的逻辑。例如,假设我们有以下代码:

public class Article {
    private long id;
    public void submit() {
        // ...
    }
    // ...getters & setters...
}
public class ArticleRepository {
    public Article findById(long id) {
        // ...return the article if it can be found...
    }
}
ArticleRepository repository = new ArticleRepository();
Article article = repository.findById(1);
if (article == null) {
    throw new ArticleNotFoundException();
}
else {
    article.submit();
}

我们可以使用空对象重构此代码到以下内容:

public class Article {
    // ...same as before...
}
public class NullArticle extends Article {
    @Override
    public void submit() {
        throw new ArticleNotFoundException();
    }
}
public class ArticleRepository {
    public Article findById(long id) {
        if (articleIsFound()) {
            // return article...
        }
        else {
            return new NullArticle();
        }
    }
}
ArticleRepository repository = new ArticleRepository();
Article article = repository.findById(1);
article.submit();

需要注意的是,引入空对象假设方法本身知道如何处理缺失值的情况。

例外情况

我们在本文中看到的另一个替代方案是在找不到所需对象时抛出异常。如果方法知道未能找到所需的对象是一个错误,则此方法有效。

例如:

public class ArticleRepository {
    public Article findById(long id) {
        if (articleIsFound()) {
            // return article...
        }
        else {
            throw new ArticleNotFoundException();
        }
    }
}

结论

在许多情况下,所需的值可能存在于也可能不存在于应用程序中,以可读和有效的方式处理这些情况是精心设计的软件的重要组成部分。从JDK 7开始,Java包括Optional类,该类允许开发人员返回可能存在也可能不存在的值,并允许客户端根据发生这些情况的上下文处理这些情况。虽然Optional类只能用于方法返回值,但了解其有用性以及如何使用简单技术应用它是掌握现代Java的重要组成部分。

加载全部内容

相关教程
猜你喜欢
用户评论