Last updated on September 4th, 2023
Java 8 introduced the Optional feature, providing a safer way to handle potential null values and reduce the risk of NullPointerExceptions.
In this post, we’ll explore Java 8 Optional methods, practical examples and how they can improve code readability.
Java 8 Optional’s methods overview
Optional
is a container that holds at most one value, promoting better programming practices. Indeed, the Optional
class proves valuable in wrapping our data, eliminating the need for conventional null checks and reducing try-catch blocks that deal with NullPointerExceptions.
Below is a brief introduction to some methods that can be used with the Optional
class:
Optional.ofNullable(value)
: return a non-empty Optional if a value is present in the given object; otherwise returns an empty Optional.
Optional.of(value)
: return Optional containing the non-null value.
Optional.orElse(defaultValue)
: return the wrapped value if it’s present, or the specified defaultValue if it’s empty; defaultValue is always evaluated, even if the Optional is not empty.
Optional.orElseGet(supplier)
: Return the wrapped value if it’s present, or the value produced by the supplier function if it’s empty. The supplier function is only called if the Optional is empty.
Optional.orElseThrow(ExceptionSupplier)
: Return the wrapped value if it’s present, or throws an exception produced by the exceptionSupplier function if it’s empty.
Optional Practical Use Cases
In this part, we will explore the practical applications of Java 8’s Optional, leveraging real-life examples based on my personal experiences.
Use Optional for null references handling
Assume that I have a Student
and Address
class as below:
public class Student {
public Student(Integer id) {
this.id = id;
}
private Integer id;
private Address address;
public Address getAddress() {
return address;
}
}
public class Address {
private String province;
public String getProvince() {
return province;
}
}
Let’s say I have a method called getProvince()
that returns a province if it exists; otherwise, it returns a string.
public class OptionalPractices {
private String getProvince() {
Student student = getStudent();
if (student != null) {
Address address = student.getAddress();
if (address != null) {
String province = address.getProvince();
if (province != null) {
return province;
}
}
}
return "not specified";
}
private Student getStudent() {
// logic to getStudent(), it might be retrieved from DB
return new Student(1);
}
}
This issue occurs in much of our code, where nested if
statements and manual checking for null
values are prevalent. In practice, we often encounter nested JSON objects, which easily lead to this problem. While the code itself functions, there are better ways to achieve the same functionality by utilizing Optional
in conjunction with Map
.
private String getProvinceRefactor() {
Student student = getStudent();
return Optional.ofNullable(student)
.map(Student::getAddress)
.map(Address::getProvince)
.orElse("not specified");
}
The nested if
is replaced by chained map()
calls that access nested properties only if the Optional
is not empty (no explicit null checks). Basically, a map()
method applies a transformation function to the wrapped value if it’s present and returns a new Optional
contain result. If the wrapped value is not present (empty), a new empty Optional
is returned.
This version retains the same functionality as the original code but is easier to read, maintain and scales better.
Using Optional Repository layer
One valuable use case for Optional is in the repository layer when working with Spring Data JPA. It allows us to wrap data before returning it, especially when the data might potentially be null.
public interface LegalInfoRepository extends JpaRepository<LegalInfoEntity, Long> {
Optional<LegalInfoEntity> findByUserEntityUserId(String userId);
}
By using Optional in this context, we indicate that the result may or may not exist, providing a clear and safe way to handle null values.
Use Optional properties in requests for partial updates
Consider an application that manages user legal information, which includes an UpdateLegalInfoRequest
class. The problem of partially updating a resource is quite common, as illustrated below:
public class UpdateLegalInfoRequest {
private String identificationNum;
private String taxCodeNo;
}
The logic at Controller and Service:
@PostMapping(value = "/{userId}/legal-info")
void updateLegalInformation(
@PathVariable UUID userId, @Valid @RequestBody UpdateLegalInfoRequest legalInfo);
The traditional way to handle this is by checking for null
values in properties that need to be updated.
public void updateUserLegalInfo(User user, UpdateLegalInfoRequest updateRequest) {
if (updateRequest.getIdentificationNum() != null) {
user.setIdentificationNum(updateRequest.getIdentificationNum());
}
if (updateRequest.getTaxCodeNo() != null) {
user.setTaxCodeNo(updateRequest.getTaxCodeNo());
}
}
All good, but there are several issues with this approach:
- It’s not evident whether a
null
value indicates that the property should not be updated or if the property should be updated with anull
value. - Adding more fields or conditions complicates the code, making it harder to maintain.
- Code duplication arises due to the repetitive
null
checks for each property.
Now, let’s see how Optional
can help with partial updates. We’ll modify the UpdateLegalInfoRequest
class to use Optional
fields:
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UpdateLegalInfoRequest {
private static final String LENGTH_255_VALIDATION = "Length should be from 1 to 255 characters.";
private Optional<@Length(max = 255, message = LENGTH_255_VALIDATION) String> identificationNum;
private Optional<@Length(max = 255, message = LENGTH_255_VALIDATION) String> taxCodeNo;
}
At its service
@Override
public void updateLegalInformation(UUID userId, UpdateLegalInfoRequest legalInfoRequest) {
final ProfileEntity profileEntity = getUserById(userId.toString());
LegalInfoEntity existentEntity =
legalInfoRepository
.findByUserEntityUserId(userId.toString())
.orElseGet(() -> LegalInfoEntity.builder().build());
existentEntity.setProfileEntity(profileEntity);
LegalInfoEntity updatedEntity =
legalInfoMapper.convertToLegalEntity(existentEntity, legalInfoRequest);
legalInfoRepository.save(updatedEntity);
}
Mapper using Mapstruct:
@Mapper(
componentModel = "spring",
uses = {JsonNullableMapper.class, DateMapper.class})
public interface LegalInfoMapper {
@BeanMapping(
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT)
@Mapping(
target = "identificationNum",
source = "legalInfoRequest.identificationNum",
qualifiedByName = "unwrap")
@Mapping(target = "taxCodeNo", source = "legalInfoRequest.taxCodeNo", qualifiedByName = "unwrap")
LegalInfoEntity convertToLegalEntity(
@MappingTarget LegalInfoEntity existentLegalInfoEntity,
UpdateLegalInfoRequest legalInfoRequest);
}
@Mapper(componentModel = "spring")
public interface JsonNullableMapper {
@Named("unwrap")
default <T> T unwrap(Optional<T> optional) {
return optional.orElse(null);
}
@Named("wrap")
default <T> Optional<T> wrap(T object) {
return Optional.of(object);
}
}
Then, let’s do the integration test for 2 cases:
TC2: No value is provided, the property should not be updated
@Test
@Sql("TEST_SAMPLE_DATA")
void givenValidRequestWithExistentLegalInfo_WhenUpdateLegalInformation_ThenUpdatedSuccessful()
throws Exception {
String request =
"{\n"
+ " \"identificationNum\": \"IDENTIFICATION\""
+ "}";
mockMvc
.perform(
post("/{userId}/legal-info", )
.contentType(APPLICATION_JSON_MEDIA_TYPE)
.content(request))
.andExpect(status().isOk());
mockMvc
.perform(get("/{userId}/legal-info", "494f9ae8-d16a-4f21-a5ed-f25ff45d8b98"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.identificationNum").value("IDENTIFICATION"))
// the taxCodeNo should not be updated
.andExpect(jsonPath("$.taxCodeNo").value("123TAX"));
}
TC2: Update null value explicitly
@Test
@Sql("TEST_SAMPLE_DATA")
void givenValidRequestWithExistentLegalInfo_WhenUpdateLegalInformation_ThenUpdatedSuccessful()
throws Exception {
String request =
"{\n"
+ " \"identificationNum\": \"IDENTIFICATION\",\n"
+ " \"taxCodeNo\": null\n"
+ "}";
mockMvc
.perform(
post("/{userId}/legal-info", )
.contentType(APPLICATION_JSON_MEDIA_TYPE)
.content(request))
.andExpect(status().isOk());
mockMvc
.perform(get("/{userId}/legal-info", "494f9ae8-d16a-4f21-a5ed-f25ff45d8b98"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.identificationNum").value("IDENTIFICATION"))
// the taxCodeNo should be updated to null
.andExpect(jsonPath("$.taxCodeNo"), CoreMatchers.is(nullValue())));
}
Conclusion
In this post, I covered most of the important features of the Java 8 Optional class, including various methods and my practical usages in the repository, null references handling and partial updates. This knowledge will continue to evolve, and I hope it proves helpful in your coding endeavours.
Happy coding!