Values

Values

Option

Option的主要目标是利用Java类型系统来消除我们代码中的空检查。OptionVavr中的一个对象容器,其最终目标与Java 8中的Option类似。VavrOption实现了Serializable、Iterable,并且拥有更丰富的API。由于Java中的任何对象引用都可能有一个空值,所以我们在使用它之前通常要用if语句来检查空值。这些检查使代码变得健壮而稳定。

@Test
public void givenValue_whenNullCheckNeeded_thenCorrect() {
    Object possibleNullObj = null;
    if (possibleNullObj == null) {
        possibleNullObj = "someDefaultValue";
    }
    assertNotNull(possibleNullObj);
}

如果不进行检查,应用程序可能会因为一个简单的NPE而崩溃。

@Test(expected = NullPointerException.class)
public void givenValue_whenNullCheckNeeded_thenCorrect2() {
    Object possibleNullObj = null;
    assertEquals("somevalue", possibleNullObj.toString());
}

然而,这些检查使代码变得啰嗦,不那么可读,特别是当if语句最终被嵌套多次时。Option解决了这个问题,它完全消除了空值,并在每个可能的情况下用一个有效的对象引用来代替它们。有了Option,一个空值将评估为一个None的实例,而一个非空值将评估为一个Some的实例。

@Test
public void givenValue_whenCreatesOption_thenCorrect() {
    Option<Object> noneOption = Option.of(null);
    Option<Object> someOption = Option.of("val");

    assertEquals("None", noneOption.toString());
    assertEquals("Some(val)", someOption.toString());
}

请注意,在调用toString之前,我们不需要做检查,也不需要像以前那样处理NullPointerExceptionOptiontoString在每次调用中都会给我们返回有意义的值。在本节的第二个片段中,我们需要一个null检查,在尝试使用变量之前,我们会给它分配一个默认值。Option可以在一行中处理这个问题,即使有一个null

@Test
public void givenNull_whenCreatesOption_thenCorrect() {
    String name = null;
    Option<String> nameOption = Option.of(name);

    assertEquals("baeldung", nameOption.getOrElse("baeldung"));
}

@Test
public void givenNonNull_whenCreatesOption_thenCorrect() {
    String name = "baeldung";
    Option<String> nameOption = Option.of(name);

    assertEquals("baeldung", nameOption.getOrElse("notbaeldung"));
}

Try

Vavr中,Try是一个可能导致异常的计算的容器。就像Option包装一个可空对象,这样我们就不必显式地用if检查来处理空值一样,Try包装一个计算,这样我们就不必显式地用try-catch块来处理异常。

@Test(expected = ArithmeticException.class)
public void givenBadCode_whenThrowsException_thenCorrect() {
    int i = 1 / 0;
}

如果没有try-catch块,应用程序会崩溃。为了避免这种情况,你需要用try-catch块来包装语句。通过Vavr,我们可以将同样的代码包裹在一个Try实例中,并得到一个结果。

@Test
public void givenBadCode_whenTryHandles_thenCorrect() {
    Try<Integer> result = Try.of(() -> 1 / 0);

    assertTrue(result.isFailure());
}

计算是否成功,可以在代码中的任何一点选择检查。在上面的代码段中,我们选择简单地检查成功或失败。我们也可以选择返回一个默认值。

@Test
public void givenBadCode_whenTryHandles_thenCorrect2() {
    Try<Integer> computation = Try.of(() -> 1 / 0);
    int errorSentinel = result.getOrElse(-1);

    assertEquals(-1, errorSentinel);
}

@Test(expected = ArithmeticException.class)
public void givenBadCode_whenTryHandles_thenCorrect3() {
    Try<Integer> result = Try.of(() -> 1 / 0);
    result.getOrElseThrow(ArithmeticException::new);
}

Try异常处理

Vavr库给我们提供了一个特殊的容器,它表示一个可能导致异常或成功完成的计算。将操作封装在Try对象中,我们得到的结果要么是Success,要么是Failure。然后我们可以根据这个类型执行进一步的操作。

public class VavrTry {
    private HttpClient httpClient;

    public Try<Response> getResponse() {
        return Try.of(httpClient::call);
    }

    // standard constructors
}

需要注意的是一个返回类型为 Try<Response> 的方法。当一个方法返回这样的结果类型时,我们需要正确处理,并且要记住,这个结果类型可能是SuccessFailure,所以我们需要在编译时明确处理。

处理成功的结果

让我们写一个测试用例,在httpClient返回成功结果的情况下使用我们的Vavr类。方法getResponse()返回的是 Try<Resposne> 对象。因此我们可以调用map()方法,只有当TrySuccess类型时,才会对Response执行操作。

@Test
public void givenHttpClient_whenMakeACall_shouldReturnSuccess() {
    // given
    Integer defaultChainedResult = 1;
    String id = "a";
    HttpClient httpClient = () -> new Response(id);

    // when
    Try<Response> response = new VavrTry(httpClient).getResponse();
    Integer chainedResult = response
      .map(this::actionThatTakesResponse)
      .getOrElse(defaultChainedResult);
    Stream<String> stream = response.toStream().map(it -> it.id);

    // then
    assertTrue(!stream.isEmpty());
    assertTrue(response.isSuccess());
    response.onSuccess(r -> assertEquals(id, r.id));
    response.andThen(r -> assertEquals(id, r.id));

    assertNotEquals(defaultChainedResult, chainedResult);
}

函数actionThatTakesResponse()只是简单地将Response作为参数,并返回一个id字段的hashCode

public int actionThatTakesResponse(Response response) {
    return response.id.hashCode();
}

如果Try里面有Success,它就返回Try的值,否则就返回defaultChainedResult。我们的httpClient执行成功,因此isSuccess方法返回true。然后我们可以执行onSuccess()方法,对Response对象进行操作。Try也有一个方法andThen,当Try的值是Success时,它就会接受一个Consumer来消费这个值。我们可以把我们的Try响应当作一个流。要做到这一点,我们需要使用toStream()方法将其转换为一个Stream,然后所有在Stream类中可用的操作都可以用来对该结果进行操作。如果我们想在Try类型上执行一个操作,我们可以使用transform()方法,将Try作为一个参数,并对其进行操作,而不需要拆开封闭的值。

public int actionThatTakesTryResponse(Try<Response> response, int defaultTransformation){
    return response.transform(responses -> response.map(it -> it.id.hashCode())
      .getOrElse(defaultTransformation));
}

Lazy

Lazy是一个容器,它代表了一个懒惰计算的值,即计算被推迟到需要结果的时候。此外,被评估的值会被缓存或记忆,并在每次需要时再次返回,而不需要重复计算。

@Test
public void givenFunction_whenEvaluatesWithLazy_thenCorrect() {
    Lazy<Double> lazy = Lazy.of(Math::random);
    assertFalse(lazy.isEvaluated());

    double val1 = lazy.get();
    assertTrue(lazy.isEvaluated());

    double val2 = lazy.get();
    assertEquals(val1, val2, 0.1);
}

在上面的例子中,我们正在评估的函数是Math.random。请注意,在第二行中,我们检查了值,发现函数还没有被执行。这是因为我们仍然没有对返回值表现出兴趣。在第三行代码中,我们通过调用Lazy.get来显示对计算值的兴趣。此时,函数执行,Lazy.evaluated返回true

我们还可以继续通过再次尝试获取值来确认Lazy的记忆位。如果再次执行我们提供的函数,我们肯定会得到一个不同的随机数。然而,Lazy再次懒惰地返回最初计算的值,正如最后的断言所确认的那样。

处理异常场景

让我们写一个例子,当我们的HttpClient执行时,会抛出ClientException。与前面的例子相比,我们的getOrElse方法将返回defaultChainedResult,因为Try将是Failure类型。

@Test
public void givenHttpClientFailure_whenMakeACall_shouldReturnFailure() {
    // given
    Integer defaultChainedResult = 1;
    HttpClient httpClient = () -> {
        throw new ClientException("problem");
    };

    // when
    Try<Response> response = new VavrTry(httpClient).getResponse();
    Integer chainedResult = response
        .map(this::actionThatTakesResponse)
        .getOrElse(defaultChainedResult);
     Option<Response> optionalResponse = response.toOption();

    // then
    assertTrue(optionalResponse.isEmpty());
    assertTrue(response.isFailure());
    response.onFailure(ex -> assertTrue(ex instanceof ClientException));
    assertEquals(defaultChainedResult, chainedResult);
}

方法getReposnse()返回Failure,因此方法isFailure返回true,我们可以对返回的响应执行onFailure()回调,看到异常是ClientException类型。我们可以在返回的响应上执行onFailure()回调,看到异常是ClientException类型。Try类型的对象可以使用toOption()方法映射到Option类型。

当我们不想在所有代码库中携带我们的Try结果,但我们有一些方法使用Option类型来处理一个显式的缺失值时,它是有用的。当我们将Failure映射到Option时,那么方法isEmpty()将返回true。当Try对象是Success类型时,对它调用toOption将使Option被定义,因此方法isDefined()将返回true

Pattern Matching

当我们的httpClient返回一个Exception时,我们可以对该Exception的类型进行模式匹配。然后根据该Exception的类型,在recover()方法中,我们可以决定是否要从该Exception中恢复,将Failure转化为Success,或者将计算结果保留为Failure

@Test
public void givenHttpClientThatFailure_whenMakeACall_shouldReturnFailureAndNotRecover() {
    // given
    Response defaultResponse = new Response("b");
    HttpClient httpClient = () -> {
        throw new RuntimeException("critical problem");
    };

    // when
    Try<Response> recovered = new VavrTry(httpClient).getResponse()
      .recover(r -> Match(r).of(
          Case(instanceOf(ClientException.class), defaultResponse)
      ));

    // then
    assertTrue(recovered.isFailure());

只有当Exception的类型是ClientException时,recover()方法内部的模式匹配才会将Failure变成Success。否则,它将把它作为一个Failure()。我们看到我们的httpClient抛出了RuntimeException,因此我们的恢复方法不会处理这种情况,因此isFailure()返回true。如果我们想从恢复的对象中获取结果,但在关键故障的情况下,我们可以使用getOrElseThrow()方法来实现。

recovered.getOrElseThrow(throwable -> {
    throw new RuntimeException(throwable);
});

有些错误是至关重要的,当它们发生时,我们希望通过在调用堆栈中较高的位置抛出异常来明确地发出信号,让调用者决定进一步的异常处理。在这种情况下,像上面的例子一样重新抛出异常是非常有用的。当我们的客户端抛出一个非关键异常时,我们在recover()方法中的模式匹配将把我们的Failure变成Success。我们正在从两种类型的异常ClientExceptionIllegalArgumentException中恢复。

@Test
public void givenHttpClientThatFailure_whenMakeACall_shouldReturnFailureAndRecover() {
    // given
    Response defaultResponse = new Response("b");
    HttpClient httpClient = () -> {
        throw new ClientException("non critical problem");
    };

    // when
    Try<Response> recovered = new VavrTry(httpClient).getResponse()
      .recover(r -> Match(r).of(
        Case(instanceOf(ClientException.class), defaultResponse),
        Case(instanceOf(IllegalArgumentException.class), defaultResponse)
       ));

    // then
    assertTrue(recovered.isSuccess());
}

Either

在函数式编程的世界里,函数值或对象不能被修改(即以正常形式;在Java术语中,它被称为不可变的变量。Either代表两种可能的数据类型的值。一个Either要么是左,要么是右。按照惯例,左表示失败的情况结果,右表示成功。

让我们考虑一个用例,在这个用例中,我们需要创建一个方法,该方法接受一个输入,并根据输入返回一个字符串或一个整数。我们可以用两种方式实现这个方法。要么我们的方法可以返回一个带有代表成功/失败结果的键的映射,要么它可以返回一个固定大小的List/Array,其中位置表示一个结果类型。

public static Map<String, Object> computeWithoutEitherUsingMap(int marks) {
    Map<String, Object> results = new HashMap<>();
    if (marks < 85) {
        results.put("FAILURE", "Marks not acceptable");
    } else {
        results.put("SUCCESS", marks);
    }
    return results;
}

public static void main(String[] args) {
    Map<String, Object> results = computeWithoutEitherUsingMap(8);

    String error = (String) results.get("FAILURE");
    int marks = (int) results.get("SUCCESS");
}

public static Object[] computeWithoutEitherUsingArray(int marks) {
    Object[] results = new Object[2];
    if (marks < 85) {
        results[0] = "Marks not acceptable";
    } else {
        results[1] = marks;
    }
    return results;
}

我们可以看到,这两种方式都需要相当大的工作量,而且最后的效果不是很美观,使用起来也不安全。现在让我们看看如何利用VavrEither工具来实现同样的结果。

private static Either<String, Integer> computeWithEither(int marks) {
    if (marks < 85) {
        return Either.left("Marks not acceptable");
    } else {
        return Either.right(marks);
    }
}

此外,Either还提供了一个非常方便的类似monadicAPI来处理这两种情况。

computeWithEither(80)
  .right()
  .filter(...)
  .map(...)
  // ...

按照惯例,Either的左属性代表失败的情况,右属性代表成功。但是,根据我们的需要,我们可以使用投影来改变这种情况:Vavr中的Either并不偏向左或右。如果我们向右投射,如果Either为左,则filter()map()等操作将没有效果。

computeWithEither(90).right()
  .filter(...)
  .map(...)
  .getOrElse(Collections::emptyList);
上一页