如何在Java 8中实现Elvis运算符?

24

我有一个经典的“Elvis操作符”案例,其中我调用的每个方法都可能返回null并将它们链接在一起:

thing?:nullableMethod1(a)?:nullableMethod2(b)?:nullableMethod3()

在Java 8中,我发现最忠实的实现方式类似于这样:
return Optional.ofNullable(thing)
    .flatMap(x -> Optional.ofNullable(x.nullableMethod1(a)))
    .flatMap(y -> Optional.ofNullable(y.nullableMethod2(b)))
    .flatMap(z -> Optional.ofNullable(z.nullableMethod3()))

我希望Java的Optional有类似Elvis操作符的东西:

public<U> Optional<U> elvisOperator(Function<? super T, ? extends U> mapper) {
    return flatMap(t -> Optional.ofNullable(mapper.apply(t));
}

这样我就不必每次都包装返回值:

return Optional.ofNullable(thing)
    .elvisOperator(x -> x.nullableMethod1(a))
    .elvisOperator(y -> y.nullableMethod2(b))
    .elvisOperator(Z::nullableMethod3); // also nice

在Java 8中实现Elvis操作符模式的更高效且习惯用法的方法是否存在?

6个回答

24
也许我漏看了什么,但是你不能使用Optional#map吗?
以下示例不会打印任何内容,因为Optional是一种“短路”的方式,即如果Optional中的值不存在(它是null或者Optional为空),则将其视为空。
Optional.ofNullable("test")
        .map(s -> null)
        .ifPresent(System.out::println);

出于这个原因,我认为您只需要执行以下操作:

return Optional.ofNullable(thing)
               .map(x -> x.nullableMethod1(a))
               .map(y -> y.nullableMethod2(b))
               .map(Z::nullableMethod3);

如果存在thing,则进行映射;否则返回一个空的Optional


4
值得指出的是,Optional.map的文档表明这种行为是有意的:“*如果映射函数返回null结果,则该方法返回一个空的Optional*”。 - Holger
3
@Holger,而且this解释了为什么从长远来看,那是一个不好的想法。或者至少,从FP的角度来看是次优的。 - melston
1
他们实现了一个非常重要的功能,但不知何故在此过程中决定严重损害可靠的旧地图。如果他们实际上用这种行为命名必要的第二个方法为“Elvis”,那么原谅会更容易。 - yeoman

13
在Java 8中,可以通过在Optional.ofNullable(...)上链接.map(...)调用,并使用.orElse(...)对其进行封顶,来模拟Elvis运算符。
Optional.ofNullable(dataObject)
.map(DataObject::getNestedDataObject)
.map(NestedDataObject::getEvenMoreNestedDataObject)
...
.orElse(null);

一个完整的示例:

import java.util.Optional;

class Main {
  // Data classes:
  static class Animal {
    Leg leg;

    Animal(Leg leg) {
      this.leg = leg;
    }

    Leg getLeg(){return this.leg;}

    public String toString(){
      String out = "This is an animal";
      out += leg != null ? " with a leg" : "";
      return out;
    }
  }

  static class Leg {
    Toes toes;

    Leg(Toes toes) {
      this.toes = toes;
    }

    Toes getToes(){return this.toes;}

    public String toString(){
      String out = "This is a leg";
      out += toes != null ? " with a collection of toes" : "";
      return out;
    }
  }

  static class Toes {
    Integer numToes;

    Toes(Integer numToes) {
      this.numToes = numToes;
    }

    Integer getNumToes(){return this.numToes;}

    public String toString(){
      String out = "This is a collection of ";
      out += numToes != null && numToes > 0 ? numToes : "no";
      out += " toes";
      return out;
    }
  }

  // A few example Elvis operators:
  static Integer getNumToesOrNull(Animal a) {
    return Optional.ofNullable(a)
      .map(Animal::getLeg)
      .map(Leg::getToes)
      .map(Toes::getNumToes)
      .orElse(null);
  }

  static Toes getToesOrNull(Animal a) {
    return Optional.ofNullable(a)
      .map(Animal::getLeg)
      .map(Leg::getToes)
      .orElse(null);
  }

  static Leg getLegOrNull(Animal a) {
    return Optional.ofNullable(a)
      .map(Animal::getLeg)
      .orElse(null);
  }

  // Main function:
  public static void main(String[] args) {
    // Trying to access 'numToes':
    System.out.println(getNumToesOrNull(new Animal(new Leg(new Toes(4))))); // 4
    System.out.println(getNumToesOrNull(new Animal(new Leg(new Toes(null))))); // null
    System.out.println(getNumToesOrNull(new Animal(new Leg(null)))); // null
    System.out.println(getNumToesOrNull(new Animal(null))); // null
    System.out.println(getNumToesOrNull(null)); // null

    // Trying to access 'toes':
    System.out.println(getToesOrNull(new Animal(new Leg(new Toes(4))))); // This is a collection of 4 toes
    System.out.println(getToesOrNull(new Animal(new Leg(new Toes(null))))); // This is a collection of no toes
    System.out.println(getToesOrNull(new Animal(new Leg(null)))); // null
    System.out.println(getToesOrNull(new Animal(null))); // null
    System.out.println(getToesOrNull(null)); // null

    // Trying to access 'leg':
    System.out.println(getLegOrNull(new Animal(new Leg(new Toes(4))))); // This is a leg with a collection of toes
    System.out.println(getLegOrNull(new Animal(new Leg(new Toes(null))))); // This is a leg with a collection of toes
    System.out.println(getLegOrNull(new Animal(new Leg(null)))); // This is a leg
    System.out.println(getLegOrNull(new Animal(null))); // null
    System.out.println(getLegOrNull(null)); // null
  }
}

我认为您在接受的答案中所添加的是,某些语言以一种模糊的方式混淆了可选性和空值的概念,以至于可以使用Elvis运算符而无需在最后解包结果。在Java中,方法实现有时会混淆这些概念,但类型系统具有不同的概念,因此,如果想要@Nullable Foo而不是Optional<Foo>,则很可能需要在最后使用orElse。 - Mickalot
1
@Mickalot 是的,那就是我想表达的意思。我看到了一些其他的答案,不知道如何在没有“Optional”的情况下实现它,直到我尝试了这个例子。我认为这可能有益于其他人。 - Abhishek Divekar

1
我写了一个编译器插件,它在编译时操作AST生成Optional.ofNullable链。
它有一些限制,但确实减少了重复代码。
PS:我是这个插件的作者。

0
为了完善一下Jacob的想法,我创建了这个类:
public class OptionalUtils {
  public static <T, U> Optional<U> elvisOperator(T t, Function<? super T, ? extends U> mapper) {
    return elvisOperator(Optional.ofNullable(t), mapper);
  }

  public static <T, U> Optional<U> elvisOperator(Optional<T> optional, Function<? super T, ? extends U> mapper) {
    return optional.flatMap(t -> Optional.ofNullable(mapper.apply(t)));
  }
}

那么你可以这样做

elvisOperator(elvisOperator(dataObject, DataObject::getNestedDataObject)
    NestedDataObject::getEvenMoreNestedDataObject))...

另一种流畅的方法:
import java.util.function.Function;
import java.util.Optional;

public class OptionalChaining<T> {
  private final Optional<T> t;

  public OptionalChaining(T t) {
    this.t=Optional.ofNullable(t);
  }
  public OptionalChaining(Optional<T> t) {
    this.t=t;
  }
  public <U> OptionalChaining<U> get(Function<T, U> function) {
    return new OptionalChaining(this.t
        .flatMap(e -> Optional.ofNullable(function.apply(e))));
  }
  public Optional<T> get() {
    return t;
  }
}

我们可以这样写:
new OptionalChaining<>(dataObject)
    .get(DataObject::getNestedDataObject)
    .get(NestedDataObject::getEvenMoreNestedDataObject)
    .get()

我非常沮丧,因为在常见的Java库(如Guava、Langs、Vavr等)中找不到这个功能。唯一的方法似乎是在Stream上使用“or”运算符,但我觉得这样写太啰嗦了 :-(


-1

模拟 Elvis 操作符的另一个选项是使用以下辅助函数:

public static <T> T get(final Supplier<T> it) {
    try {
        return it.get();
    } catch (final NullPointerException e) {
        return null;
    }
}

你可以像这样使用它:

var x = get(() -> thing.nullableMethod1(a).nullableMethod2(b).nullableMethod3());

1
其实并不是那么愚蠢!但是无法区分是由getter引发的NPE,还是由结果引发的NPE :-( - Christophe Moine

-1

也许还有一种更直接的方法:

Object o;
String s = (o = nullableResult()) == null ? null : o.toString();

使用三元运算符,看起来像是旧版的Java,但在使用Optional时需要更少的方法调用堆栈帧。 在这种情况下,我只想获取可空结果的字符串表示形式。 您可以调用其他名称而不是toString()

或者您可以在Groovy中完成它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接