如何使用Java 8 Optionals,如果三者都存在则执行操作?

hug*_*vey 54 java optional java-8

我有一些使用Java Optionals的(简化)代码:

Optional<User> maybeTarget = userRepository.findById(id1);
Optional<String> maybeSourceName = userRepository.findById(id2).map(User::getName);
Optional<String> maybeEventName = eventRepository.findById(id3).map(Event::getName);

maybeTarget.ifPresent(target -> {
    maybeSourceName.ifPresent(sourceName -> {
        maybeEventName.ifPresent(eventName -> {
            sendInvite(target.getEmail(), String.format("Hi %s, $s has invited you to $s", target.getName(), sourceName, meetingName));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

不用说,这看起来很糟糕.但我想不出另一种以较少嵌套和更易读的方式做到这一点的方法.我考虑过流式传输3个选项,但放弃了这个想法,因为做了.filter(Optional::isPresent)一个.map(Optional::get)感觉甚至更糟.

那么是否有更好的,更"Java 8"或"可选识字"的方式来处理这种情况(基本上需要多个Optionals来计算最终操作)?

Sha*_*her 53

我认为流三者Optional是一种矫枉过正,为什么不简单

if (maybeTarget.isPresent() && maybeSourceName.isPresent() && maybeEventName.isPresent()) {
  ...
}
Run Code Online (Sandbox Code Playgroud)

在我看来,与使用流API相比,这更清楚地说明了条件逻辑.

  • @hughjdavey并不是'Optional.isPresent`和`Optional.get`都不好,*本身*,只是在`Optional`上使用`map`或`ifPresent`或其他方法组合可以更好地表达.这种情况很复杂,替代方案不一定是显而易见的,因此可以在这里使用`isPresent`和`get`. (13认同)
  • @jeremysprofile使用Optional包装的null可以让其他使用您代码的人知道您打算将null作为代码处理的可接受值."总是更好"是一个难以匹敌的声明,因为有些人可能会争辩说,在某些优化案例中最好不要这样做,但一般来说,如果不更清楚并且不鼓励人们不考虑自己在脚下拍摄就不会受到伤害当它被期望作为有效输入时的null情况. (3认同)
  • 有些人说使用'isPresent` +`get`是不好的,因为通过允许然后你必须*证明你的程序是安全的:你总是可以忘记`isPresent`检查或输入错字(例如丢失括号) `if`等).如果你总是使用`ifPresent`来打开这个值,或者使用`orElse` /`或者ElGet` /`orElseThrow`你肯定知道NPE不会发生. (3认同)
  • @SharonBenAsher这个确实很懒,或者你只能使用java-8功能我想/sf/answers/3391598661/ (2认同)
  • 这种替代方案在结构上看起来与进行一系列空检查完全相同.假设OP可以及时返回并重写方法以返回非可选值,这可以吗?或者选项总是比返回一个未包装的null更好? (2认同)
  • 尽管我喜欢这段代码的简单性,但我认为这是错误的,因为无论第一查询还是第二查询返回空结果,它总是查询数据库3次。我认为,在不需要时查询数据库毫无意义且昂贵。 (2认同)

Jor*_*nee 29

使用辅助函数,事情至少会变得非嵌套:

@FunctionalInterface
interface TriConsumer<T, U, S> {
    void accept(T t, U u, S s);
}

public static <T, U, S> void allOf(Optional<T> o1, Optional<U> o2, Optional<S> o3,
       TriConsumer<T, U, S> consumer) {
    o1.ifPresent(t -> o2.ifPresent(u -> o3.ifPresent(s -> consumer.accept(t, u, s))));
}
Run Code Online (Sandbox Code Playgroud)

allOf(maybeTarget, maybeSourceName, maybeEventName,
    (target, sourceName, eventName) -> {
        /// ...
});
Run Code Online (Sandbox Code Playgroud)

显而易见的缺点是,对于每个不同数量的Optionals ,您需要单独的辅助函数重载

  • 我也非常喜欢这种方法,但是对DB的调用都非常渴望,即使第一个可选项是空的.如果`o2`和`o3`分别是`Supplier <Optional <U>`和`Supplier <Optional <S>`,我认为你可以很容易地添加所需的懒惰.然后你将在每个`ifPresent`的lambda中调用每个供应商的`get()`. (4认同)

Stu*_*rks 28

由于原始代码正在执行其副作用(发送电子邮件),而不是提取或生成值,嵌套ifPresent调用似乎是合适的.原始代码似乎并不太糟糕,事实上它似乎比已经提出的一些答案更好.但是,语句lambdas和类型的局部变量Optional似乎增加了相当多的混乱.

首先,我将冒昧地修改原始代码,方法是将它包装在一个方法中,为参数提供好的名称,并组成一些类型名称.我不知道实际代码是否是这样的,但这对任何人来说都不应该令人惊讶.

// original version, slightly modified
void inviteById(UserId targetId, UserId sourceId, EventId eventId) {
    Optional<User> maybeTarget = userRepository.findById(targetId);
    Optional<String> maybeSourceName = userRepository.findById(sourceId).map(User::getName);
    Optional<String> maybeEventName = eventRepository.findById(eventId).map(Event::getName);

    maybeTarget.ifPresent(target -> {
        maybeSourceName.ifPresent(sourceName -> {
            maybeEventName.ifPresent(eventName -> {
                sendInvite(target.getEmail(), String.format("Hi %s, %s has invited you to %s",
                                                  target.getName(), sourceName, eventName));
            });
        });
    });
}
Run Code Online (Sandbox Code Playgroud)

我玩了不同的重构,我发现将内部语句lambda提取到自己的方法对我来说是最有意义的.给定源和目标用户以及一个事件 - 没有可选的东西 - 它会发送有关它的邮件.这是在处理完所有可选内容后需要执行的计算.我还在这里移动了数据提取(电子邮件,名称),而不是将其与外层中的Optional处理混合.同样,这对我来说很有意义:从目标发送关于事件的邮件.

void setupInvite(User target, User source, Event event) {
    sendInvite(target.getEmail(), String.format("Hi %s, %s has invited you to %s",
               target.getName(), source.getName(), event.getName()));
}
Run Code Online (Sandbox Code Playgroud)

现在,让我们来处理可选的东西.正如我上面所说,ifPresent是这样的方式,因为我们想要做副作用的事情.它还提供了一种从Optional中"提取"值并将其绑定到名称的方法,但仅限于lambda表达式的上下文中.由于我们希望为三个不同的Optional执行此操作,因此需要嵌套.嵌套允许外部lambda的名称被内部lambda捕获.这使我们可以将名称绑定到从Optionals中提取的值 - 但前提是它们存在.这不能用线性链完成,因为像元组这样的中间数据结构对于构建部分结果是必要的.

最后,在最里面的lambda中,我们调用上面定义的helper方法.

void inviteById(UserId targetId, UserId sourceID, EventId eventId) {
    userRepository.findById(targetId).ifPresent(
        target -> userRepository.findById(sourceID).ifPresent(
            source -> eventRepository.findById(eventId).ifPresent(
                event -> setupInvite(target, source, event))));
}
Run Code Online (Sandbox Code Playgroud)

请注意,我已经内联了Optionals而不是将它们保存在局部变量中.这揭示了嵌套结构更好一点.如果其中一个查找没有找到任何内容,它还提供操作的"短路",因为ifPresent在空的Optional上什么都不做.

不过,我的眼睛仍然有点密集.我认为原因是这段代码仍然依赖于一些外部存储库来进行查找.将它与Optional处理混合在一起有点不舒服.一种可能性是简单的查找提取到自己的方法findUserfindEvent.这些很明显,所以我不会把它们写出来.但如果这样做,结果将是:

void inviteById(UserId targetId, UserId sourceID, EventId eventId) {
    findUser(targetId).ifPresent(
        target -> findUser(sourceID).ifPresent(
            source -> findEvent(eventId).ifPresent(
                event -> setupInvite(target, source, event))));
}
Run Code Online (Sandbox Code Playgroud)

从根本上说,这与原始代码没有什么不同.这是主观的,但我认为我更喜欢原始代码.它具有相同的,相当简单的结构,虽然嵌套而不是典型的可选处理线性链.不同的是,查找是在可选处理中有条件地完成的,而不是预先完成,存储在局部变量中,然后仅对可选值进行条件提取.此外,我已将数据操作(提取电子邮件和名称,发送消息)分离为单独的方法.这避免了将数据操作与可选处理混合在一起,如果我们处理多个Optional实例,我认为这会使事情变得混乱.

  • 很好的答案,你已经完成了比我更好的重构.提取查找和`sendInvite`方法是可行的方法,同时让嵌套的`ifPresent'.我今天学到了很好的重构课,非常感谢你. (7认同)
  • @FedericoPeraltaSchaffner谢谢!我认为这里的一个要点是,这不仅仅是关于可选的,而且还涉及几个小的东西,如名称,类型,lambdas,以及其他调整和重构. (3认同)

pvp*_*ran 25

这样的事情怎么样?

 if(Stream.of(maybeTarget, maybeSourceName,  
                        maybeEventName).allMatch(Optional::isPresent))
  {
   sendinvite(....)// do get on all optionals.
  }
Run Code Online (Sandbox Code Playgroud)

话说回来.如果你在数据库中查找的逻辑只是发送邮件,那么如果maybeTarget.ifPresent()是false,那么没有必要获取其他两个值,不是吗?我担心,这种逻辑只能通过传统的if else语句来实现.

  • 在没有支票(`isPresent`)的情况下进行选择是一个坏主意.但是既然我们在`allMatch`检查中已经这样做了,那就安全了.此外,我不认为使用收集和检查大小的选项是一个好主意,因为您正在创建一个不必要的最多3个对象的列表,而不是仅使用布尔检查 (3认同)
  • @pvpkiran的事情是,这将执行所有3个查询,即使第一个查询足以回答问题.阅读此/sf/answers/3391598661/ (2认同)

Fed*_*ner 10

我认为你应该考虑采取另一种方法.

我首先不要在开始时向DB发出三个调用.相反,我会发出第一个查询,只有当结果存在时,我才会发出第二个查询.然后,我将对第3个查询应用相同的基本原理,最后,如果最后的结果也存在,我将发送邀请.当前两个结果中的任何一个不存在时,这将避免对DB的不必要的调用.

为了使代码更具可读性,可测试性和可维护性,我还将每个数据库调用提取到自己的私有方法,并将它们链接起来Optional.ifPresent:

public void sendInvite(Long targetId, Long sourceId, Long meetingId) {
    userRepository.findById(targetId)
        .ifPresent(target -> sendInvite(target, sourceId, meetingId));
}

private void sendInvite(User target, Long sourceId, Long meetingId) {
    userRepository.findById(sourceId)
        .map(User::getName)
        .ifPresent(sourceName -> sendInvite(target, sourceName, meetingId));
}

private void sendInvite(User target, String sourceName, Long meetingId) {
    eventRepository.findById(meetingId)
        .map(Event::getName)
        .ifPresent(meetingName -> sendInvite(target, sourceName, meetingName));
}

private void sendInvite(User target, String sourceName, String meetingName) {
    String contents = String.format(
        "Hi %s, $s has invited you to $s", 
        target.getName(), 
        sourceName, 
        meetingName);
    sendInvite(target.getEmail(), contents);
}
Run Code Online (Sandbox Code Playgroud)


Ole*_*hov 8

第一种方法并不完美(它不支持懒惰 - 无论如何都将触发所有3个数据库调用):

Optional<User> target = userRepository.findById(id1);
Optional<String> sourceName = userRepository.findById(id2).map(User::getName);
Optional<String> eventName = eventRepository.findById(id3).map(Event::getName);

if (Stream.of(target, sourceName, eventName).anyMatch(obj -> !obj.isPresent())) {
    return;
}
sendInvite(target.get(), sourceName.get(), eventName.get());
Run Code Online (Sandbox Code Playgroud)

以下示例有点冗长,但它支持懒惰和可读性:

private void sendIfValid() {
    Optional<User> target = userRepository.findById(id1);
    if (!target.isPresent()) {
        return;
    }
    Optional<String> sourceName = userRepository.findById(id2).map(User::getName);
    if (!sourceName.isPresent()) {
        return;
    }
    Optional<String> eventName = eventRepository.findById(id3).map(Event::getName);
    if (!eventName.isPresent()) {
        return;
    }
    sendInvite(target.get(), sourceName.get(), eventName.get());
}

private void sendInvite(User target, String sourceName, String eventName) {
    // ...
}
Run Code Online (Sandbox Code Playgroud)


Wor*_*der 8

如果您想坚持Optional并且不承诺立即使用该值,则可以使用以下内容.它使用Triple<L, M, R>Apache Commons:

/**
 * Returns an optional contained a triple if all arguments are present,
 * otherwise an absent optional
 */
public static <L, M, R> Optional<Triple<L, M, R>> product(Optional<L> left,
        Optional<M> middle, Optional<R> right) {
    return left.flatMap(l -> middle.flatMap(m -> right.map(r -> Triple.of(l, m, r))));
}

// Used as
product(maybeTarget, maybeSourceName, maybeEventName).ifPresent(this::sendInvite);
Run Code Online (Sandbox Code Playgroud)

可以想象两个或多个Optionals 的类似方法,尽管java遗憾的是没有一般的元组类型(还).