Java 8中是否有类似于Scala的Either的等价物?

92

就像Java 8中的java.util.Optional<T>类型有点类似于Scala的Option[T]类型一样,是否有一个相当于Scala的Either[L, R]类型的等效物呢?


1
这么长时间过去了,仍然没有标准的实现 :( - Cherry
总有一个 java.lang.Object ... 令人难过。 - Alex R
我从2011年1月开始使用Scala,然后又回到了Java(我是1997年开始的),希望能找到一个java.utils.Either。但是没有。所以,我就尽量创建了一个类似于我在Scala中喜欢的东西:https://stackoverflow.com/a/76435293/501113 - undefined
8个回答

80

Java 8 中没有 Either 类型,因此您需要自己创建一个或使用一些第三方库。

您可以使用新的 Optional 类型构建这样的功能(但请阅读本答案的末尾):

final class Either<L,R>
{
    public static <L,R> Either<L,R> left(L value) {
        return new Either<>(Optional.of(value), Optional.empty());
    }
    public static <L,R> Either<L,R> right(R value) {
        return new Either<>(Optional.empty(), Optional.of(value));
    }
    private final Optional<L> left;
    private final Optional<R> right;
    private Either(Optional<L> l, Optional<R> r) {
      left=l;
      right=r;
    }
    public <T> T map(
        Function<? super L, ? extends T> lFunc,
        Function<? super R, ? extends T> rFunc)
    {
        return left.<T>map(lFunc).orElseGet(()->right.map(rFunc).get());
    }
    public <T> Either<T,R> mapLeft(Function<? super L, ? extends T> lFunc)
    {
        return new Either<>(left.map(lFunc),right);
    }
    public <T> Either<L,T> mapRight(Function<? super R, ? extends T> rFunc)
    {
        return new Either<>(left, right.map(rFunc));
    }
    public void apply(Consumer<? super L> lFunc, Consumer<? super R> rFunc)
    {
        left.ifPresent(lFunc);
        right.ifPresent(rFunc);
    }
}

示例用例:

new Random().ints(20, 0, 2).mapToObj(i -> (Either<String,Integer>)(i==0?
  Either.left("left value (String)"):
  Either.right(42)))
.forEach(either->either.apply(
  left ->{ System.out.println("received left value: "+left.substring(11));},
  right->{ System.out.println("received right value: 0x"+Integer.toHexString(right));}
));

回顾来看,基于Optional的解决方案更像是一个学术示例,而不是推荐的方法。其中一个问题是将null视为“空”,这与“either”的含义相矛盾。
下面的代码展示了一个Either,它将null视为可能的值,因此严格地说是“either”,左或右,即使该值为null
abstract class Either<L,R>
{
    public static <L,R> Either<L,R> left(L value) {
        return new Either<L,R>() {
            @Override public <T> T map(Function<? super L, ? extends T> lFunc,
                                       Function<? super R, ? extends T> rFunc) {
                return lFunc.apply(value);
            }
        };
    }
    public static <L,R> Either<L,R> right(R value) {
        return new Either<L,R>() {
            @Override public <T> T map(Function<? super L, ? extends T> lFunc,
                                       Function<? super R, ? extends T> rFunc) {
                return rFunc.apply(value);
            }

        };
    }
    private Either() {}
    public abstract <T> T map(
      Function<? super L, ? extends T> lFunc, Function<? super R, ? extends T> rFunc);

    public <T> Either<T,R> mapLeft(Function<? super L, ? extends T> lFunc) {
        return this.<Either<T,R>>map(t -> left(lFunc.apply(t)), t -> (Either<T,R>)this);
    }
    public <T> Either<L,T> mapRight(Function<? super R, ? extends T> lFunc) {
        return this.<Either<L,T>>map(t -> (Either<L,T>)this, t -> right(lFunc.apply(t)));
    }
    public void apply(Consumer<? super L> lFunc, Consumer<? super R> rFunc) {
        map(consume(lFunc), consume(rFunc));
    }
    private <T> Function<T,Void> consume(Consumer<T> c) {
        return t -> { c.accept(t); return null; };
    }
}

如果在工厂方法的开头插入Objects.requireNonNull(value),就可以轻松将其更改为严格拒绝null。同样地,支持空either也是可以想象的。


12
请记住,虽然它的行为类似于“Either”,但在某种意义上,这个类型“太大了”,因为你的“left”和“right”字段原则上都可以为空或者都可以定义。你已经隐藏了能实现这一点的构造函数,但这种方法仍然存在你的实现可能出现漏洞的潜力。从简单的类型算术角度来看,你试图从“(1 + a) * (1 + b)”中得到“a + b”。当然,在这个表达式的结果中,会出现“a + b”,但也包括“1”和“a * b”。 - Mysterious Dan
9
@神秘的丹: 在Java中,禁止在对象构造期间使用某种特定状态是首选的方式。否则,你将不得不为几乎每个使用int作为变量的用例发明一个新的“有效范围”类型,因为在使用int变量时使用整个值范围是例外,只是举个例子。毕竟,Optional也是这样做的,强制在对象构造期间执行不变式。 - Holger
2
@Holger:Either.left(42).map(left -> null, right -> right)this.right.get() 上抛出了 NoSuchElementException(正确)。此外,可以通过 Either.left(42).mapLeft(left -> null) 来绕过不变量的执行,并产生 Either<empty, empty>。或者当它们组合在一起时,在 Either.left(42).mapLeft(left -> null).map(left -> left, right -> right) 上再次失败。 - charlie
2
@charlie:该解决方案并未考虑Optional.map允许函数返回null,从而将其转换为空的Optional。然而,除了及时检测并立即抛出异常的机会外,我没有看到任何更“正确”的替代方案。据我所知,与Scala不同,Java中没有参考行为,您不能映射到null... - Holger
2
@Blaisorblade:这里没有提到任何错误,只有一个不受支持的情况,由于问题中只指定了Scala的Either且Scala没有null,因此没有明确定义的行为。一旦为该情况指定了行为,将该代码适应到该行为上是很容易的。 - Holger
显示剩余16条评论

29

撰写本文时,vavr(以前称为javaslang)可能是最受欢迎的Java 8函数库。它与我的另一个答案中所述的lambda-companion的Either非常相似。

Either<String,Integer> value = compute().right().map(i -> i * 2).toEither();

19

Java标准库中没有Either。但是FunctionalJava中有Either的实现,以及许多其他好用的类。


Either 的链接似乎已被删除。您确定该项目仍在得到支持吗? - Flame_Phoenix
我已经更新了链接。据我所知,该项目仍在积极维护中。 - Ravi Kiran
是的,但是文档非常糟糕。 :( - Misha Tavkhelidze

10

cyclops-react有一种“向右”偏置的实现,称为Xor

 Xor.primary("hello")
    .map(s->s+" world")

 //Primary["hello world"]

 Xor.secondary("hello")
    .map(s->s+" world")

 //Secondary["hello"]

 Xor.secondary("hello")
    .swap()
    .map(s->s+" world")

 //Primary["hello world"]

Xor.accumulateSecondary(ListX.of(Xor.secondary("failed1"),
                                 Xor.secondary("failed2"),
                                 Xor.primary("success")),
                                 Semigroups.stringConcat)

//failed1failed2

还有一种相关类型Ior,可以作为either或tuple2。

  • 声明:我是cyclops-react的作者。

3
Xor在Cyclops X中已被更名Either: https://static.javadoc.io/com.oath.cyclops/cyclops/10.0.0-FINAL/cyclops/control/Either.html - seanf

8
没有,确实如此。
Java语言的开发者明确表示像Option<T>这样的类型只应被用作临时值(例如在流操作结果中),因此虽然它们与其他语言中的相同,但它们不应该像其他语言中那样使用。因此,并没有类似于Either的东西,因为它不会像Optional那样自然地出现(例如从流操作中)。

9
你有这方面的来源吗? - Cannoliopsida
3
@akroy,这似乎是正确的,Brian Goetz在这个答案中写了同样的内容:链接 - RonyHe
4
对我来说,“Either”并不是自然而然的。也许我做错了。当一个方法有可能返回两种不同的东西时,比如 Either<List<String>, SomeOtherClass>,你会怎么做?请问。 - tamas.kenez
6
我也反对Either自然产生,因为在我的流中,一个映射操作可能会抛出异常,所以我将其映射到Either<Exception, Result>的流。 - Olivier Gérardin
对我来说,这也是自然而然的。事实上,我甚至对@Holder的第一个实现进行了改进,使用了两个Optional,使其更加适合我的需求:https://dev59.com/XV8e5IYBdhLWcg3wCWx2#76435293 - chaotic3quilibrium

6

有一个独立实现的Either库叫做"ambivalence": http://github.com/poetix/ambivalence

你可以从Maven中央仓库获取它:

<dependency>
    <groupId>com.codepoetics</groupId>
    <artifactId>ambivalence</artifactId>
    <version>0.2</version>
</dependency>

5

lambda-companion包含一个 Either 类型(以及其他一些函数类型,例如Try)。

<dependency>
    <groupId>no.finn.lambda</groupId>
    <artifactId>lambda-companion</artifactId>
    <version>0.25</version>
</dependency>

使用它很容易:

final String myValue = Either.right("example").fold(failure -> handleFailure(failure), Function.identity())

3
该项目已不再维护。 - Flame_Phoenix
该项目现在推荐使用vavr - undefined

2

注意:有关下文所示的Either类的增强和完整文档版本(包括equalshashCodeflatMap和其他杂项辅助函数),请访问此Gist


我采用了@Holger在他的(目前最高票)答案中提供的实现,并稍作修改以消除我能找到的所有null问题。

我对其进行了重构,使其更符合OpenJDK的命名约定。

然后我阅读了评论,并进行了微调和调整,以进一步提高实现的质量。

我还在构造函数中添加了状态验证,并添加了一些辅助方法:isLeft()isRight()getLeft()getRight()

/**
 * Implementation of Either<L, R> via a pair of Optionals which explicitly
 * reject null values.
 * <p>
 * Inspired by the (first) solution presented in this StackOverflow Answer:
 * <a href="https://dev59.com/XV8e5IYBdhLWcg3wCWx2#26164155">...</a>
 **/
public static final class Either<L, R> {

  public static <L, R> Either<L, R> left(L value) {
    return new Either<>(Optional.of(value), Optional.empty());
  }

  public static <L, R> Either<L, R> right(R value) {
    return new Either<>(Optional.empty(), Optional.of(value));
  }

  private final Optional<L> left;
  private final Optional<R> right;

  private Either(Optional<L> left, Optional<R> right) {
    if (left.isEmpty() == right.isEmpty()) {
      throw new IllegalArgumentException(
          "left.isEmpty() and right.isEmpty() cannot be equal");
    }
    this.left = left;
    this.right = right;
  }

  public boolean isLeft() {
    return this.left.isPresent();
  }

  public boolean isRight() {
    return this.right.isPresent();
  }

  public L getLeft() {
    return this.left.get();
  }

  public R getRight() {
    return this.right.get();
  }

  public <T> T map(
      Function<? super L, ? extends T> leftFunction,
      Function<? super R, ? extends T> rightFunction
  ) {
    return this.left
        .<T>map(l -> Objects.requireNonNull(leftFunction.apply(l)))
        .orElseGet(() ->
            this.right
                .map(r -> Objects.requireNonNull(rightFunction.apply(r)))
                .orElseThrow(() ->
                    new IllegalStateException(
                        "should never get here")));
  }

  public <T> Either<T, R> mapLeft(
    Function<? super L, ? extends T> leftFunction
  ) {
    return new Either<>(
        this.left.map(l ->
            Objects.requireNonNull(leftFunction.apply(l))),
        this.right);
  }

  public <T> Either<L, T> mapRight(
    Function<? super R, ? extends T> rightFunction
  ) {
    return new Either<>(
        this.left,
        this.right.map(r -> 
            Objects.requireNonNull(rightFunction.apply(r))));
  }

  public void forEach(
    Consumer<? super L> leftAction,
    Consumer<? super R> rightAction
  ) {
    this.left.ifPresent(leftAction);
    this.right.ifPresent(rightAction);
  }
}

注意:如果你想获取增强版并完整文档化的Either类(包括equalshashCodeflatMap以及其他杂项辅助函数),请访问此处的Gist


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