The Beauty of Java Optional and Either
Many Java developers often overlook its functional programming capabilities. Learn how to chain Optional and Either to write concise and beautiful code.
Join the DZone community and get the full member experience.
Join For FreeMany of us Java developers — particularly beginners — often overlook its functional programming capabilities. In this article, we'd look at how to chain Optional
and Either
to write concise and beautiful code.
To illustrate, let's assume we have a bank where a user could have zero or more accounts. The entities look as below:
record User(
int id,
String name
) {}
record Account(
int id,
User user // a user can have zero or more account
) {}
For fetching the entities, the repositories look as below:
interface UserRepository {
Optional<User> findById(int userId);
}
interface AccountRepository {
Optional<Account> findAnAccountOfUser(User user); // given a user, find its any account if it has one
}
Now, get ready for a couple of assignments!
First Assignment
Let's code a method Optional<Account> findAnAccountByUserId(Integer userId)
that would:
- Given a
userId
, return any one account of the user, if there's one - If either there's no user with the given id, or there's no account of the user, return an empty
Optional
A novice solution could be as follows:
public Optional<Account> findAccountByUserId(int userId) {
Optional<User> possibleUser = userRepository.findById(userId);
if (possibleUser.isEmpty())
return Optional.empty();
var user = possibleUser.orElseThrow();
return accountRepository.findAnAccountOfUser(user);
}
But, then the map
method of Optional
strikes our mind! Instead of checking for possibleUser.isEmpty()
, we could just map
the user, if present, to an account:
public Optional<Account> findAccountByUserId(int userId) {
return userRepository
.findById(userId)
.map(accountRepository::findAnAccountOfUser);
}
We land up with a compilation error because accountRepository.findAnAccountOfUser(user)
returns an Optional<Account>
, whereas the map
method above needs an Account
. For this exact use case, Optional
provides a flatMap
method, which flattens nested Optional
s. So, changing map
to flatMap
would work.
public Optional<Account> findAccountByUserId(int userId) {
return userRepository
.findById(userId)
.flatMap(accountRepository::findAnAccountOfUser);
}
Cool! Get ready for a more complex assignment.
Second Assignment
When a user
/account
is not found, instead of returning an empty optional
, how about indicating exactly what was not found: user or account?
We could approach this problem in a few ways:
Throw Exceptions
We could define some custom exceptions, viz. UserNotFoundException
and AccountNotFoundException
, and throw those:
public Account findAccountByUserIdX(int userId) {
var user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new);
return accountRepository.findAnAccountOfUser(user).orElseThrow(AccountNotFoundException::new);
}
However, using exceptions for expected cases is considered an anti-pattern: Googling will get you numerous articles about the subject. So let's avoid that.
Use a Result Interface
Another approach would be returning a custom Result
object instead of returning Optional
; i.e., Result findAnAccountByUserId(Integer userId)
. The result would be an interface that would be implemented by custom error classes, as well as Account
and User
.
Use Either
A third approach, which I find simpler, is to return an Either
instead of Optional
. Whereas an Optional
holds zero or one value, an Either
holds either of two values. It's typically used to hold either an error or a success value.
Unlike Optional
, you don't get a Java Either
implementation out of the box, but there are quite a few libraries. I prefer using jbock-java/either because it's lightweight and simple.
So, let's first define the error
interface and classes:
interface Error {}
record UserNotFound() implements Error {}
record AccountNotFound() implements Error {}
Let's now attempt coding:
public Either<Error, Account> findAccountByUserId(int userId) {
...
}
Did you notice above that we used Error
as the left generic parameter, whereas Account
as the right one? That wasn't accidental: the convention when using Either
is that the left is used for errors whereas the right is used for success values.
Either
has a similar functional API like Optional
. For example, we have map
and flatMap
for mapping the success values, whereas we have mapLeft
and flatMapLeft
for mapping the errors. We also have utility methods like Either.left(value)
and Either.right(value)
to create Either
objects. Do have a look at its API. It has many interesting features for functional programming.
So, continuing our journey, we could first create an either
having the user
or error
as below:
public Either<Error, Account> findAccountByUserId(int userId) {
var eitherErrorOrUser = userRepository
.findById(userId)
.map(Either::<Error, User>right)
.orElseGet(() -> Either.left(new UserNotFound()))
...
}
Lines 4 and 5 above convert an Optional<User>
to Either<UserNotFound, User>
. Because converting an Optional
to an Either
would be a common use case, let's code a utility method for it:
public class EitherUtils {
public static <L, R> Either<L, R> of(Optional<R> possibleValue, Supplier<L> errorSupplier) {
return possibleValue.map(Either::<L, R>right).orElseGet(() -> Either.<L, R>left(errorSupplier.get()));
}
}
It takes the optional
and an errorSupplier
. The errorSupplier
is used for composing the error if the optional
is empty.
Using it, our code now looks like this:
public Either<Error, Account> findAccountByUserId(int userId) {
var eitherErrorOrUser = EitherUtils
.<Error, User>of(userRepository.findById(userId), UserNotFound::new)
...
}
Next, as above, eitherErrorOrUser
could be mapped for an account in a similar way. The complete solution would then look like this:
public Either<Error, Account> findAccountByUserId(int userId) {
return EitherUtils
.<Error, User>of(userRepository.findById(userId), UserNotFound::new)
.flatMap(user -> of(accountRepository.findAnAccountOfUser(user), AccountNotFound::new));
}
Looks cute, doesn't it?
Many elegant uses of Optional
and Either
could be found in this GitHub repo, where we avoid the anti-pattern of using exceptions for validation/business rules.
Video
Summary
Consider using functional programming capabilities of Java wherever possible, and make your code cute and concise!
Opinions expressed by DZone contributors are their own.
Comments