【问题标题】:Is there an elegant way to get the first non null value of multiple method returns in Java?有没有一种优雅的方法来获取 Java 中多个方法返回的第一个非 null 值?
【发布时间】:2014-11-24 22:04:27
【问题描述】:

你自己已经看过很多次了,我敢肯定:

public SomeObject findSomeObject(Arguments args) {
    SomeObject so = queryFirstSource(args); // the most likely source first, hopefully
    if (so != null) return so;

    so = querySecondSource(args); // a source less likely than the first, hopefully
    if (so != null) return so;

    so = queryThirdSource(args); // a source less likely than the previous, hopefully
    if (so != null) return so;

    // and so on
}

我们有不同的来源,我们搜索的对象可能在哪里。作为一个更生动的例子,我们可以想象我们首先检查用户 ID 是否在特权用户列表中。如果不是,我们检查用户 ID 是否在允许的用户列表中。否则我们返回 null。 (这不是最好的例子,但我希望它是一个足够生动的例子。)

Guava 为我们提供了一些可以美化上面代码的助手:

public SomeObject findSomeObject(Arguments args) {
    // if there are only two objects
    return com.google.common.base.Objects.firstNonNull(queryFirstSource(args), querySecondSource(args));

    // or else
    return com.google.common.collect.Iterables.find(
        Arrays.asList(
            queryFirstSource(args)
            , querySecondSource(args)
            , queryThirdSource(args)
            // , ...
        )
        , com.google.common.base.Predicates.notNull()
    );
 }

但是,正如我们当中更有经验的人已经看到的那样,如果查找(即queryXXXXSource(args))需要一定的时间,这可能会表现不佳。这是因为我们现在首先查询所有源,然后将结果传递给在那些结果中找到第一个不是 null 的方法。

与第一个示例相比,下一个源仅在前一个不返回某些内容时才评估,第二个解决方案一开始可能看起来更好,但性能可能更差。

这是我们提出实际问题的地方,也是我提出一些建议的地方,我希望有人已经实现了它的基础,或者有人可能会提出一个更聪明的解决方案。

用简单的英语:有人已经实现了这样的defferedFirstNonNull(见下文)或类似的东西吗?使用新的Stream 框架是否有一个简单的纯Java 解决方案来实现这一点?你能提出另一个达到相同结果的优雅解决方案吗?

规则: 允许使用 Java 8 以及积极维护和知名的第三方库,例如具有 Apache 许可证或类似许可证(无 GPL!)的 Google 的 Guava 或 Apache 的 Commons Lang。

建议的解决方案:

public SomeObject findSomeObject(Arguments args) {
    return Helper.deferredFirstNonNull(
        Arrays.asList(
            args -> queryFirstSource(args)
            , args -> querySourceSource(args)
            , args -> queryThirdSource(args)
        )
        , x -> x != null
     )
 }

因此,defferedFirstNonNull 方法将逐个评估每个 lambda 表达式,并且一旦谓词 (x -> x != null) 为真(即我们找到匹配项),该方法将立即返回结果并且不会查询任何进一步的源.

PS:我知道表达式args -> queryXXXXSource(args) 可以缩短为queryXXXXSource。但这会使提议的解决方案更难阅读,因为乍一看并不明显会发生什么。

【问题讨论】:

    标签: java lambda guava java-8


    【解决方案1】:

    是的,有:

    Arrays.asList(source1, source2, ...)
       .stream()
       .filter(s -> s != null)
       .findFirst();
    

    这更加灵活,因为它返回 Optional 而不是 null,以防找到非空 source

    编辑:如果你想要惰性评估,你应该使用Supplier

    Arrays.<Supplier<Source>>asList(sourceFactory::getSource1, sourceFactory::getSource2, ...)
       .stream()
       .filter(s -> s.get() != null)
       .findFirst();
    

    【讨论】:

    • 如果你想从可变参数创建一个Stream,你也可以直接调用Stream.of(source1, source2, ...)而不是Arrays.asList(source1, source2, ...).stream()
    【解决方案2】:

    我会这样写(你可能不需要泛型,但为什么不这样做):

    public static <A, T> Optional<T> findFirst(Predicate<T> predicate, A argument,
                                             Function<A, T>... functions) {
      return Arrays.stream(functions)
              .map(f -> f.apply(argument))
              .filter(predicate::test)
              .findFirst();
    }
    

    你可以这样称呼它:

    return findFirst(Objects::nonNull, args, this::queryFirstSource,
                                           this::querySecondSource,
                                           this::queryThirdSource);
    

    (假设您的 queryXXX 方法是实例方法)

    这些方法将按顺序应用,直到返回一个与谓词匹配的值(在上面的示例中:返回一个非空值)。

    【讨论】:

      【解决方案3】:

      这取决于您未定义的一些因素。如您的问题所示,您是否有一组固定的、相当小的 query…Source 操作,或者您更愿意拥有一个更灵活、可扩展的操作列表?

      在第一种情况下,您可以考虑更改 query…Source 方法以返回 Optional&lt;SomeObject&gt; 而不是 SomeObjectnull。如果你改变你的方法,就像

      Optional<SomeObject> queryFirstSource(Arguments args) {
          …
      }
      

      你可以这样链接它们:

      public SomeObject findSomeObject(Arguments args) {
          return queryFirstSource(args).orElseGet(
            ()->querySecondSource(args).orElseGet(
            ()->queryThirdSource(args).orElse(null)));
      }
      

      如果您无法更改它们或希望它们返回 null,您仍然可以使用 Optional 类:

      public SomeObject findSomeObject(Arguments args) {
          return Optional.ofNullable(queryFirstSource(args)).orElseGet(
             ()->Optional.ofNullable(querySecondSource(args)).orElseGet(
             ()->queryThirdSource(args)));
      }
      

      如果您正在为更多可能的查询寻找更灵活的方式,则不可避免地将它们转换为某种列表或Functions 流。一种可能的解决方案是:

      public SomeObject findSomeObject(Arguments args) {
          return Stream.<Function<Arguments,SomeObject>>of(
            this::queryFirstSource, this::querySecondSource, this::queryThirdSource
          ).map(f->f.apply(args)).filter(Objects::nonNull).findFirst().orElse(null);
      }
      

      这会执行所需的操作,但是,它会在您每次调用该方法时组成必要的操作。如果您想更频繁地调用此方法,可以考虑编写一个可以重复使用的操作:

      Function<Arguments, SomeObject> find = Stream.<Function<Arguments,SomeObject>>of(
          this::queryFirstSource, this::querySecondSource, this::queryThirdSource
      ).reduce(a->null,(f,g)->a->Optional.ofNullable(f.apply(a)).orElseGet(()->g.apply(a)));
      
      public SomeObject findSomeObject(Arguments args) {
          return find.apply(args);
      }
      

      所以你看,方法不止一种。这取决于实际的任务要走什么方向。有时,即使是简单的if 序列也可能是合适的。

      【讨论】:

      • 我选择了这个答案作为我的指定答案,因为它结合了所有其他答案,因此看起来“完整”。我不知道 Optional&lt;&gt; 类,我认为它为我的问题提供了最优雅的方法。
      猜你喜欢
      • 1970-01-01
      • 2013-04-16
      • 2011-09-18
      • 1970-01-01
      • 2010-12-10
      • 2010-10-05
      • 1970-01-01
      • 2011-03-09
      • 2011-04-25
      相关资源
      最近更新 更多