Consider the following nested object structure for a person with a camera phone:
class Person {
private Phone phone;
public Phone getPhone() {
return phone;
}
}
class Phone {
private Camera camera;
public Camera getCamera() {
return camera;
}
}
class Camera {
private Resolution resolution;
public Resolution getResolution() {
return resolution;
}
}
class Resolution {
private int width;
private int height;
}
Now, let's say that you want to get the resolution of a person's camera phone.
You might be tempted to do this:
public Resolution getPhoneCameraResolution(Person person) {
return person.getPhone().getCamera().getResolution();
}
However, not everyone owns a phone, so person.getPhone()
might return null
and consequently person.getPhone().getCamera()
will result in a NullPointerException
at run-time! Similarly, a person might own a phone, but the phone might not have a camera.
One way to avoid NullPointerException
s is to check for nulls, as shown below:
public Resolution getPhoneCameraResolution(Person person) {
if (person != null) {
Phone phone = person.getPhone();
if (phone != null) {
Camera camera = phone.getCamera();
if (camera != null) {
return camera.getResolution();
}
}
}
return Resolution.UNKNOWN;
}
This code doesn't scale very well and makes your code harder to read and maintain. Every time a variable could be null
, you have to add another nested if
statement.
Java 8: Optional class
Java 8 introduced a new class called java.util.Optional<T>
to model potentially absent values. It forces you to actively unwrap an optional and deal with the absence of a value. It also leads to better APIs because, just by reading the signature of a method, you can tell whether to expect an optional value.
We can re-write the original object model using the Optional
class as shown below:
import java.util.Optional;
class Person {
private Optional<Phone> phone;
public Optional<Phone> getPhone() {
return phone;
}
}
class Phone {
private Optional<Camera> camera;
public Optional<Camera> getCamera() {
return camera;
}
}
class Camera {
// resolution is not optional - all cameras must have it
private Resolution resolution;
public Resolution getResolution() {
return resolution;
}
}
Creating Optional objects:
- Empty Optional: You can create an empty optional object use
Optional.empty
:
Optional<Phone> optPhone = Optional.empty();
- Optional from a non-null value: To create an optional from a non-null value use
Optional.of
. Note that if the value is null
a NullPointerException
is thrown immediately:
Optional<Phone> optPhone = Optional.of(phone);
- Optional from null: Use
Optional.ofNullable
to create an optional that may hold a null
value. If the value is null
, an empty optional is created.
Optional<Phone> optPhone = Optional.ofNullable(phone);
Extracting values from Optional objects:
Optional
supports a map
method, which can be used to extract information from an object. For example, to get the Resolution
of a Camera
:
Optional<Camera> optCamera = Optional.ofNullable(camera);
Optional<Resolution> resolution = optCamera.map(Camera::getResolution);
What this means is that if the camera optional contains a value, getResolutions
is called, otherwise nothing happens and an empty optional is returned.
Optional
also contains a flatMap
method in order to "flatten" nested optionals. For example, if you were to call map
with a function that returns an Optional
, you will get an Optional containing an Optional, as illustrated below:
Optional<Phone> optPhone = Optional.ofNullable(phone);
// calling map returns a two-level optional
Optional<Optional<Camera>> optOptCamera = optPhone.map(Phone::getCamera);
// but flatMap, returns a single-level optional
Optional<Camera> optCamera = optPhone.flatMap(Phone::getCamera);
Chaining Optional objects:
Now let's get back to our original problem of getting the resolution of a person's camera phone and re-write those ugly, nested if-statements using optionals instead:
public Resolution getPhoneCameraResolution(final Optional<Person> person) {
return
person.flatMap(Person::getPhone) // returns Optional<Phone>
.flatMap(Phone::getCamera) // returns Optional<Camera>
.map(Camera::getResolution) // returns Optional<Resolution>
.orElse(Resolution.UNKNOWN); // returns Resolution or UNKNOWN if not found
}
This code returns the resolution of the person's camera phone. If the person is null, doesn't have a phone, or the phone doesn't have a camera, an UNKNOWN
resolution is returned.