Our site uses cookies. Learn more about their purpose and change of settings in a browser If you are using the site, you give you consent to use cookies, according to current browser settings. Got it

Dealing with Java quirks while switching to functional programming style

Learn some useful techniques for functional programming in Java.
Java a programowanie funkcyjne

Functional programming promises easy testing and unproblematic reasoning about the execution flow – without surprising exceptions, hard-to-track variables and collections mutations, nor coupling the program definition with interpretation.

Thus Java constantly gains more features supporting writing code in functional style, starting with 2014’s addition of lambdas and streams of collection elements.

Yet it looks like the language architects are not convinced whether fully embrace that style by not providing basic combinator methods (zipzipWithfold) with existing Optional<T> and Stream<T> classes, nor classes that can carry information about a failure like Try<T> or Either<E, T>.

This limits the possibility of writing more complicated logic without a nasty mixture of good ol' try {} s and mutable elements.

In this article, we’ll explore a couple of libraries that make it easier to stay with the FP side.

All code examples and AsciiDoc version of this article are provided in this GitHub repository.

The basics — immutable data classes

It’s not an easy task to operate on data classes without much boilerplate. Getters, builders, equals and hashcode methods can take more space than the data of interests itself. A notable library to cut out the boilerplate is Immutables. A well known, yet controversial library is Lombok.

The former generates the code with concrete implementations from abstract classes, while with the latter you’ll end up with a different bytecode in your *.class files than in your *.java ones. I don’t mind it as long as IDEs and Maven provide good support of the library and as there are more chances you already have it in your project, I’ll pick Lombok for the subsequent examples.

How to live without setters

Here’s an example of an immutable class that isn’t bloated with unnecessary information (all-args constructor, builder, equals+hashCode, getters).

import lombok.Builder;
import lombok.Value;

import java.time.ZonedDateTime;
import java.util.Optional;

@Value
@Builder(toBuilder = true)
class Customer {

  @Value
  @Builder(toBuilder = true)
  static class Addrees {
    Optional<String> line1;
    Optional<String> line2;
    Optional<String> zipCode;
    Optional<String> city;
    Optional<String> country;
  }

  String name;
  Addrees address;
  ZonedDateTime bornOn;
  Boolean active;
}


Here, the @Value annotation creates all the necessary methods (assuming setters are not necessary) to be used in the immutable class. To use it in IntelliJ you must turn on annotation processors in settings.

So far so good. The problem appears when one wants to change a single field in an object. Typically it’d require rewriting all the parameters from the old object to the constructor of the class. It’s quite a tedious task and a temptation to add setters by switching @Value to @Data.

To ease the pain of "mutating" immutable objects one can add a builder with toBuilder argument set to true. Unfortunately, it’s not a default value. The annotation allows obtaining the builder with pre-filled fields from an existing object.

As a simple example let’s use the builder to solve a task of deactivating all users in a list.

static List<Customer> deactivateCustomers(List<Customer> customers) {
    return customers
        .stream()
        .map(customer -> customer
            .toBuilder()
            .active(FALSE)
            .build())
        .collect(Collectors.toList());
  }

(test data at the end of the section)

@Test
  void deactivateConsumersTest() {
    var originalList = getTestConsumers();

    var result = deactivateCustomers(originalList);

    result.forEach(customer ->
        assertThat(customer.getActive())
            .describedAs("Customer [" + customer + "] should not be active")
            .isEqualTo(FALSE)
    );

    assertThat(originalList)
        .describedAs("Customer objects in the original list are not modified")
        .containsExactly(getTestConsumers().toArray(new Customer[0]));
  }


Now a more complicated example. Given that we must normalize all the country values from local names to English ones in a collection of Customer s.

  // the best we can get in a language without type aliases
  @FunctionalInterface
  private interface CustomerMapper extends Function<Customer, Customer> {
  }

  private static CustomerMapper countryRenamer(String oldValue, String newValue) {
    return customer -> {
      var oldAddress = customer.getAddress();
      return customer
          .toBuilder()
          .address(oldAddress
              .toBuilder()
              .country(oldAddress
                  .getCountry()
                  .map(countryName -> countryName.replace(oldValue, newValue)))
              .build())
          .build();
    };
  }


The country field is defined as Optional<String>, so don’t revert to using .isPresent() + .get() which makes the code look not better than if (x != null) {…​}. Remember that Optional<A> has convenient .map(A → B) and .flatMap(A → Optional<B>) methods.

No need of rewriting each field, but still some nested code, is probably the most we can get if we stick to Java. To see how replacing a single element in a nested structure can be cleaned up in other languages check the Lenses concept.

For completeness here’s the usage (and one country name for simplicity).

 @Test
  void countryNamesAfterNormalizationContainOnlyAllowedValues() {
    normalizeCountry(getTestConsumers()) .forEach(customer ->
        customer.getAddress().getCountry().ifPresent(countryName ->
            assertThat(countryName)
            .describedAs("The country name of [" + customer + "] after the normalization, if present should be within allowed value set ["+ ALlOWED_COUNTRY_NAMES + "]")
            .isIn(ALlOWED_COUNTRY_NAMES)
            )
        );
  }


We can compare the original and new values in the log statement, as the objects from the original list couldn’t be modified in the .map(…​) stream pipeline element.

Unfortunately, the java.util.List itself is mutable. The caller of the normalizeCountry method doesn’t know if it won’t mess with the parameter structure by adding or removing elements. We’ll address it later.

Side note: Optional<T> as a field

When you try to use Optional<T> as a field in IntelliJ, you’ll be greeted with a warning.

Inspection info: Reports any uses of java.util.Optional<T>, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLong or com.google.common.base.Optional as the type for a field or a parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result". Using a field with type java.util.Optional is also problematic if the class needs to be Serializable, which java.util.Optional is not.


Don’t panic. Here’s a possible source of the inspection rule written by Brian Goetz. The Usage of Optional here is fine for our purpose. Libraries like Jackson can deal with (de)serialization. With Lombok, you need to use jackson-modules-java8.

Side note: honing intuition about type bounds of generics in methods taking Function<? super T, ? extends R> as a parameter

You’ll encounter many method signatures like.

public <U> Optional<U> map(Function<? super T, ? extends U> mapper)
public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper


while reading documentation of functional libraries. Such signatures aren’t so scary when you know the reason for putting the type bounds. Having the following types…

  interface RawMaterial {}
  interface Steel extends RawMaterial {}
  interface Vehicle {}
  interface Car extends Vehicle {}

and the functions…

  RawMaterial rawMaterialObject = new RawMaterial() { };
  Steel steelObject = new Steel() { };
  Vehicle vehicleObject = new Vehicle() { };
  Car carObject = new Car() { };

  Function<Steel, Vehicle> steelToVehicle = steel -> vehicleObject;
  Function<Steel, Car> steelToCar = steel -> carObject;
  Function<RawMaterial, Car> rawMaterialToCar = rawMaterial -> carObject;
  Function<RawMaterial, Vehicle> rawMaterialToVehicle = rawMaterial -> vehicleObject;


and an assignment…

    Optional<Vehicle> vehicle = Optional.of(steelObject).map((Function<? super Steel, ? extends Vehicle>) mapper);


Think about what we can put instead of the mapper? In other words, what are the subtypes of Function<? super Steel, ? extends Vehicle> and why there’s super next to the Steel and extends next to the Vehicle?

It turns out the code compiles with all our mappers.

    Optional<Vehicle> vehicle1 = Optional.of(steelObject).map(steelToVehicle);
    Optional<Vehicle> vehicle2 = Optional.of(steelObject).map(steelToCar);
    Optional<Vehicle> vehicle3 = Optional.of(steelObject).map(rawMaterialToCar);
    Optional<Vehicle> vehicle4 = Optional.of(steelObject).map(rawMaterialToVehicle);


It’s because we can use a function that can produce a Vehicle or something more concrete like Car from Steel. And we can’t complain if just any RawMaterial, not necessarily Steel is enough for it.

In other words, a function B is a subtype of a function A if the function B returns a subtype of the function A and takes a supertype of the function A.

And in another words functions are covariant in their return types and contravariant in their input types.

In practice, if you see ? super next to a type you can assume it’s some input and if you see ? extends you can assume it’s some output.

Test data used in this section:

  Set<String> ALlOWED_COUNTRY_NAMES = Set.of("USA", "France", "India", "Poland");

  List<Customer> getTestConsumers() {
    return List.of(
        Customer
            .builder()
            .name("John Kovalsky")
            .address(Customer.Addrees
                .builder()
                .line1(of("Warszawska 1"))
                .line2(empty())
                .zipCode(of("00-000"))
                .city(of("Warsaw"))
                .country(of("Poland"))
                .build())
            .active(TRUE)
            .bornOn(ZonedDateTime.of(2014, 3, 18, 12, 0, 0, 0, UTC))
            .build(),
        Customer
            .builder()
            .name("Jan Kowalski")
            .address(Customer.Addrees
                .builder()
                .line1(of("Warszawska 2"))
                .line2(empty())
                .zipCode(of("00-001"))
                .city(of("Warszawa"))
                .country(of("Polska"))
                .build())
            .active(FALSE)
            .bornOn(ZonedDateTime.of(2019, 3, 18, 12, 0, 0, 0, UTC))
            .build()
    );
  }


What about the processed Stream elements?

A temptation to use mutable state in a Stream<T> comes when we need to access a previous element that has already been processed. Say we need to summarize changes in Customer objects for auditing purposes.

First, let’s come up with machinery for producing a string describing differences between 2 objects. Here defined is a map of attribute names to their projectors on a customer object.

  @Value
  private static class ComparableAttribute {
    String name;
    Function<Customer, String> getter;
  }

  private static final List<ComparableAttribute> COMPARABLE_ATTRIBUTES = List.of(
      new ComparableAttribute("name", Customer::getName),
      new ComparableAttribute("address", c -> c.getAddress().toString()),
      new ComparableAttribute("born on", c -> c.getBornOn().toString()),
      new ComparableAttribute("is active", c -> c.getActive().toString())
  );


Now let’s try to define the actual builder of a String with changes summary.

  private static Optional<String> valueDiff(String valueName, String v1, String v2) {
    if (v1.equals(v2)) return Optional.empty();
    else return Optional.of(valueName + ": " + v1 + " -> " + v2);
  }

  static String customerDiff(Customer c1, Customer c2) {
    return COMPARABLE_ATTRIBUTES
        .stream()
        .map(attr -> valueDiff(attr.name, attr.getGetter().apply(c1), attr.getter.apply(c2)))
        .filter(Optional::isPresent)
        .map(Optional::get)
        .collect(Collectors.joining(" | "));
  }

(test data at the end of the section):

  @Test
  void customerDiffTest() {
    assertThat(Zipping.customerDiff(c1, c3)).isEqualTo("name: Johny Kovalsky -> Jan Kowalski | born on: 2014-03-18T12:00Z -> 2019-03-18T12:00Z | is active: true -> false");
  }

And then use it:

  static List<String> compareSubsequentChangesWithAtomicRefence(List<Customer> customerStateSnapshots) {
    if (customerStateSnapshots.size() < 2) return Collections.emptyList();
    final var lastValue = new AtomicReference<>(customerStateSnapshots.get(0));
    return customerStateSnapshots
        .stream()
        .skip(1)
        .map(customer -> customerDiff(lastValue.getAndSet(customer), customer))
        .collect(Collectors.toList());
  }
  private List<String> expectedChangesDescriptions = List.of(
      "name: Johny Kovalsky -> John Kovalsky",
      "name: John Kovalsky -> Jan Kowalski | born on: 2014-03-18T12:00Z -> 2019-03-18T12:00Z | is active: true -> false"
  );

  @Test
  void customerListDiffWithAtomicReference() {
    assertThat(Zipping
        .compareSubsequentChangesWithAtomicReference(customers))
        .isEqualTo(expectedChangesDescriptions);
  }


The lastValue constant indicates a strong desire to use Streams and problem of enforcing that used variables must be declared as final. AtomicReference<Customer> is a quick hack for changing a for-each loop to the New Fancy Functional Streams™.

It’s a nasty hack, of course. All the promises of simplicity about reasoning about code are thrown away when one needs to keep track of all the places where a variable can be mutated. One can argue that it’s not a big deal when the mutable state isn’t leaked outside such a method, and it’s a valid claim. However, in this case, it’d be easier to just use a variable and old loops.

Zipping it

An easy way to compare two subsequent elements is to combine two streams — the original one and one with the first element skipped. Unfortunately java.util.Stream lacks such a method. Insufficiencies of Java’s standard libraries make it a high time to start using the vavr library.

I’ll be addressing vavr classes with fully qualified package names (io.vavr. …​) to avoid confusion which class in the code example belongs to the standard Java, and which does not. Normally, you can import vavr counterparts of Java classes to make the code more succinct.

Here’s how one can achieve the goal with vavr’s List.

  static List<String> compareSubsequentChanges(List<Customer> customerStateSnapshots) {
    if (customerStateSnapshots.size() < 2) return Collections.emptyList();
    final var vavrList = io.vavr.collection.List.ofAll(customerStateSnapshots);
    return vavrList
        .zipWith(vavrList.drop(1), Zipping::customerDiff)
        .asJava();
  }


First, Java’s List is changed to vavr’s one. Then in the zipWith, the original collection is combined with the one without the first element using Zipping::customerDiff as a method taking two elements, one from each collection, and returning the result. It’s worth to check other methods provided by vavr collections which can be missing in Java’s Stream.

Note that creating a lazy Stream and then .collect ing the result is not obligatory in vavr’s collections. This allows us to clean up the code logic. If one switches to vavr completely, .asJava() becomes unnecessary as well.

Generalizing with foldX

Let’s now use a universal mechanism when we want to access any state that in an imperative style would be a variable(s) updated in a loop.

  @Value
  private static final class ComparisonState {
    final Customer lastVale;
    final io.vavr.collection.List<String> stateAcc;
  }

  static List<String> compareSubsequentChangesWithFoldLeft(List<Customer> customerStateSnapshots) {
    if (customerStateSnapshots.size() < 2) return Collections.emptyList();
    final var zero = new ComparisonState(customerStateSnapshots.get(0), io.vavr.collection.List.empty());
    final var vavrList = io.vavr.collection.List.ofAll(customerStateSnapshots);
    return vavrList
        .drop(1)
        .foldLeft(zero, (ComparisonState foldAcc, Customer c) ->
            new ComparisonState(c, foldAcc.stateAcc.append(customerDiff(foldAcc.lastVale, c))))
        .stateAcc
        .asJava();
  }


The foldLeft method (absent in Java’s Stream) is generally used like:

.foldLeft(initialState, (currentState, element) -> newState))

The initial element is sometimes called zero, sometimes unit. To see why think about most basic examples of associative binary operations with neutral elements (such a combination is called a monoid).

  • Sum: the neutral element is 0 and the binary operation is + (hence zero)

        assertThat(io.vavr.collection.List
            .of(1, 2, 3, 4)
            .foldLeft(0, (a, b) -> a + b))
            .isEqualTo(10);
  • Product: the neutral element is 1 and the binary operation is * (hence unit)

        assertThat(io.vavr.collection.List
            .of(1, 2, 3, 4)
            .foldLeft(1, (a, b) -> a * b))
            .isEqualTo(24);


I’ll stick with zero name, as this is what the argument is called in vavr. We know that there always be a zero element due to the guard code if (customerStateSnapshots.size() < 2) return Collections.emptyList();.

It could be simplified if there was a type like NonEmptyList with foldLeft not requiring the zero element. Such addition was proposed on the vavr’s issue tracker, but apparently wasn’t sufficiently motivated and was rejected.

Because the list used in stateAcc is immutable (like all vavr collections), the append method executed on it returns a new list leaving the original intact, so we don’t need to worry about it.

After reducing the list to just the state object, we access its .stateAcc field, and because it is a vavr List, we convert it to the Java counterpart with .asJava() to match the expected return type.

Notice that .stateAcc is not accessed via a getter, but directly. It’s intentional because ComparisonState is final (here added explicitly, but Lombok’s @Value adds final and private modifiers anyway); thus a getter cannot be overridden and return something different in a subclass. .stateAcc itself is final as well; it cannot be changed without using reflection.

Overall, unless we want to fit in the java bean convention, there’s no point of using a getters layer.

Test data used in this section:

  private Addrees address = Addrees
                .builder()
                .line1(of("Warszawska 1"))
                .line2(empty())
                .zipCode(of("00-000"))
                .city(of("Warsaw"))
                .country(of("Poland"))
                .build();

  private Customer c1 = Customer
            .builder()
            .name("Johny Kovalsky")
            .address(address)
            .active(TRUE)
            .bornOn(ZonedDateTime.of(2014, 3, 18, 12, 0, 0, 0, UTC))
            .build();

  private Customer c2 = Customer
      .builder()
      .name("John Kovalsky")
      .address(address)
      .active(TRUE)
      .bornOn(ZonedDateTime.of(2014, 3, 18, 12, 0, 0, 0, UTC))
      .build();

  private Customer c3 = Customer
            .builder()
            .name("Jan Kowalski")
            .address(address)
            .active(FALSE)
            .bornOn(ZonedDateTime.of(2019, 3, 18, 12, 0, 0, 0, UTC))
            .build();


Dealing with failures

Checked exceptions don’t blend with Streams. Runtime exceptions don’t blend with predictable methods invocations.

Let’s start with such a service:

  @AllArgsConstructor
  static class CustomerService {
    private io.vavr.collection.List<Customer> customersSource;

    static class ServiceException extends Exception {
      ServiceException(String msg) {
        super(msg);
      }
    }

    Optional<Customer> getByNameOptionalThrowing(String name) throws ServiceException {
      if ("Error-prone Customer".equals(name)) throw new ServiceException("Life is life... Nananana");
      return customersSource.find(c -> c.getName().equals(name)).toJavaOptional();
    }
  }


And a task of getting the average age of a list of customers.

The first attempt…

    try {
      res = names
          .stream()
          .map(name -> cs.getByNameOptionalThrowing(name))
          // further processing
          .collect(Collectors.toList());
    } catch (CustomerService.ServiceException e) {
      log.error("An error obtaining customers", e);
    }

…and a disappointment

Error:(77, 52) java: unreported exception com.kpalka.fpplayground.FailableBehaviour.CustomerService.ServiceException; must be caught or declared to be thrown
Error:(80, 7) java: exception com.kpalka.fpplayground.FailableBehaviour.CustomerService.ServiceException is never thrown in body of corresponding try statement


.map won’t accept something that throws a checked exception. So the second attempt:

  static Integer getAvgAge(CustomerService cs, List<String> names, ZonedDateTime now) {

    // Don't do this at home
    Function<String, Optional<Customer>> aHackYouCanSometimesSpot = name -> {
      try {
        return cs.getByNameOptionalThrowing(name);
      } catch (CustomerService.ServiceException e) {

        // A service tries to inform me in the method signature that something can go wrong.
        // But I cannot use a method that throws an exception inside `.map()`.
        // But I REALLY want to use that fancy Stream feature... Hmm...

        throw new RuntimeException(e);
      }
    };

    // Similar examples are often used to show the possibilities of Stream<T> and method references...

    return names
        .stream()
        .map(aHackYouCanSometimesSpot)

        // ... and when you see such a call to service as a fragment of stream pipeline, you should smell something bad. Things can fail. In a nasty way. And I think such situations make some people, softly said, not very willing to incorporate the newer features of the language to their daily usage


        // the rest of processing
  }


Now, by calling getAvgAge we aren’t even informed that something can go wrong, so it’s easy to forget to handle an error. The argument of easier reasoning about a program written in the functional style apparently doesn’t apply here.

For a moment let’s try to finish the broken implementation and then fix the error handling part.

We’re going to need a class representing a state that will be used in reducing a stream of customers' age to the average value.

  @AllArgsConstructor
  static class AvgPeriodCounter {
    static final AvgPeriodCounter ZERO = new AvgPeriodCounter(Period.ZERO, 0);
    final Period sum;
    final Integer elementsNumber;

    AvgPeriodCounter plus(Period period) {
      return new AvgPeriodCounter(sum.plus(period), elementsNumber + 1);
    }

    AvgPeriodCounter plus(AvgPeriodCounter avgPeriodCounter) {
      return new AvgPeriodCounter(sum.plus(avgPeriodCounter.sum), elementsNumber + avgPeriodCounter.elementsNumber);
    }

    int getAvgYear(ZonedDateTime relativeTo) {
      return Period.between(relativeTo.minus(sum).toLocalDate(), relativeTo.toLocalDate()).getYears() / elementsNumber;
    }
  }


And again, this time the complete implementation:

  static Integer getAvgAge(CustomerService cs, List<String> names, ZonedDateTime now) {
    Function<String, Optional<Customer>> aHackYouCanSometimesSpot = name -> {
      try {
        return cs.getByNameOptionalThrowing(name);
      } catch (CustomerService.ServiceException e) {
        throw new RuntimeException(e);
      }
    };

    var toAge = periodTo(now);

    return names
        .stream()
        .map(aHackYouCanSometimesSpot)
        .filter(Optional::isPresent)
        .map(Optional::get)
        .map(Customer::getBornOn)
        .map(toAge)
        .reduce(AvgPeriodCounter.ZERO,
            (AvgPeriodCounter acc, Period p) -> acc.plus(p),
            (AvgPeriodCounter acc1, AvgPeriodCounter acc2) -> acc1.plus(acc2)
        )
        .getAvgYear(now);
  }

(test data at the end of the section):

  @Test
  void countAvgForExistingCustomers() {
    assertThat(FailableBehaviour.getAvgAge(cs, existingNames, now))
        .describedAs("Counts the avg of existing customers' age")
        .isEqualTo(avgAge);
  }

  @Test
  void countAvgThrowing() {
    assertThatThrownBy(() -> FailableBehaviour.getAvgAge(cs, List.of(errorProneCustomer), now))
        .describedAs("Method can throw an expected error, but doesn't inform that it can fail in any way. Additionally, meaningful ServiceException is wrapped in a very generic RuntimeException")
        .isExactlyInstanceOf(RuntimeException.class);
  }


It’s worth noticing the (over)complicated reduce available in Java’s Stream. Compared to the foldLeft available in vavr, in reduce, we have two stages. The first is the same as in foldLeft; the second combines the states produced by the first stage.

In the first, the new accumulated state is dependent on the previous value of the stream. In the second, states can be combined independently, which means they can be parallelized. It’s great if you need it. If you don’t you have to deal with the burden of defining additional binary operation, here AvgPeriodCounter plus(AvgPeriodCounter avgPeriodCounter) next to AvgPeriodCounter plus(Period period). So if we already have the whole list in memory, using foldX seams to be a sexier solution than reduce.

Now it’s time to tame the method calls that can fail. vavr offers Try<T> and Either<E, T> classes with which you can inform about a possible failure in the return type. Either is more powerful as you can put in Left (failure) part anything signaling the error. Try can be seen as:

    interface Try<T> extends Either<Throwable, T> {}


So the errors can be only a (sub)instance of Throwable. Say it’s good enough for now. Let’s add the following method to CustomerService:

    Try<Optional<Customer>> getByNameWithTry(String name) {
      return Try.of(() -> getByNameOptionalThrowing(name));


Notice that there’s no throws ServiceException in the method signature, which means it can be used within Stream. Notice that the getByNameOptionalThrowing(name) call is now wrapped in a lambda expression. Without it, the exception would be thrown immediately, before vavr had a chance to wrap call execution into Try.Success or Try.Failure.

Now we’re ready to use the new method of the service.

  static Try<Integer> getAvgAgeWithTry(CustomerService cs, List<String> names, ZonedDateTime now) {

    var toAge = periodTo(now);

    return Try.traverse(names, cs::getByNameWithTry) // Try<Seq<Optional<Customer>>>
        .map(customers -> customers // Seq<Optional<Customer>>
            .filter(Optional::isPresent)
            .map(Optional::get)
            .map(Customer::getBornOn)
            .map(toAge)
            .foldLeft(AvgPeriodCounter.ZERO, AvgPeriodCounter::plus)
            .getAvgYear(now)
        );
  }


in the client code of this method, you can add on the result:

    result
        .onFailure(e -> log.warn("Cannot obtain customers {}", names, e))
        .onSuccess(result -> {
          if (log.isDebugEnabled()) {
            log.debug("For customers {} received the average age {}", names, result);
          }
        });
  @Test
  void countAvgForExistingCustomersWithTry() {
    assertThat(FailableBehaviour.getAvgAgeWithTry(cs, existingNames, now))
        .describedAs("Counts the avg of existing customers' age")
        .isEqualTo(Try.success(41));
  }

  @Test
  void countAvgForExistingCustomersWithTryWhenThereIsAServiceException() {
    var withErrorProneCustomer = io.vavr.collection.List.ofAll(existingNames).append(errorProneCustomer).asJava();
    assertThat(FailableBehaviour.getAvgAgeWithTry(cs, withErrorProneCustomer, now).getCause())
        .describedAs("Try.Failure has the cause of ServiceException")
        .isExactlyInstanceOf(FailableBehaviour.CustomerService.ServiceException.class);
  }


The first thing here is to use Try.traverse instead of mapping each name in the list like name → cs.getByNameWithTry(name). With the latter, we’d end up with Stream<Try<Optional<Customer>>> and what we’re interested in is a kind of Try<Stream<Optional<Customer>>>, so that we have a Stream<Optional<Customer>> to process. The traverse method does the job of "flipping" a Stream, or actually Seq, with Try.

With a simplification, it can be thought of as:

cs.getByNameWithTry(names.get(0)).flatMap(customer0 ->
  cs.getByNameWithTry(names.get(1)).flatMap(customer1 -> /* and so on */
    cs.getByNameWithTry(names.get(N)).map(customerN -> List.of(
        customer0, customer1, /* and so on */ customerN
    ))));


If any of the flatMap s end with Try.Failure (here if the Try.of(() → …​) caught an exception) the call chain is short-circuited and the end result is the first Try.Failure. Otherwise, it’s Try.Success with the list of processed elements.

Back to the logic. Later, the Try<T> behaves similarly to Option<T> in the way we can call map and flatMap, plus some specific method like onFailureonSuccessrecover, and recoverWith. So we’re mapping a Seq of customers to their ages and reducing those to an average. The result can be processed further with map s and flatMap s, or we can finally call .get() first ensuring ourselves with .isSuccess() if the call succeeded.

If we’re into asynchronous processing, we can switch Try to Future (the vavr’s one, not the Java one which has method naming inconsistent with Stream/Option part). However, then we must be careful not to call .get or we’d block the execution. What we can do is use .onSuccess and .onFailure to complete the Promise of whatever framework or library expects.

Test data used in this section:

  io.vavr.collection.List<Customer> testCustomers = io.vavr.collection.List.of(
      ZonedDateTime.of(1970, 1, 1, 1, 0, 0, 0, UTC),
      ZonedDateTime.of(1990, 1, 1, 1, 0, 0, 0, UTC))
      .zipWithIndex()
      .map(bornOnIdx -> new Customer("Test John " + bornOnIdx._2,
          new Customer.Addrees(empty(), empty(), empty(), empty(), empty()),
          bornOnIdx._1,
          TRUE)
      );

  ZonedDateTime now = ZonedDateTime.of(2020, 12, 31, 1, 0, 0, 0, UTC);
  Integer avgAge = 41;
  List<String> existingNames = List.of("Test John 0", "Test John 1");
  List<String> existingAndNonexistingNames = List.of("Test John 0", "Non-existing John");

  String errorProneCustomer = "Error-prone Customer";

  FailableBehaviour.CustomerService cs = new FailableBehaviour.CustomerService(testCustomers);


Summary

There are some external libraries which make writing the functional style in Java less painful. In examples above we didn’t separate the program definition from its execution, which is difficult in a language without higher-kinded types or a library like Arrow for Kotlin.

Nonetheless, without mixing pre-java8 and post-lambda style (not using mutable collections, data structures, nor variables), we obtain a code that is easy to reason about. A topic worth exploring for now that’s already available in vavr and may be in future added to Java is pattern matching. It can simplify code like:

    .filter(Optional::isPresent)
    .map(Optional::get)

with:

    .collect(
        Case(Some($()), t -> t)
    )


Generally, the more the filtering conditions and transforming logic are complicated, the more pattern matching can clean up the code.

Big thank you goes to Paweł Włodarski, who encouraged me to write this article; Piotr Łukomiak and Anna Skawińska, who thoroughly reviewed and corrected it; and users of JVM-Poland Slack group for ideas!

Let me know your thoughts on this article and the topic of FP in Java in general because I’m curious if it’s a goal worth striving for :)

They are looking for