Java basics: Optionals

Table of Contents

What are Optionals?

For the the number one use case for them has to do with null values. So whenever we're dealing with objects in Java, we store references to objects in variables. And those variables may or may not actually be pointing back to an object. For example, we could create a string called message and put a word in it. So in this case, the variable message is pointing to the object, which exist somewhere in memory. But we could also have, message2 pointing to nothing. And if I try to do something like to call  to upper, and if I run this, that's going to blow up with a null pointer exception. Because I'm trying to reference a method of a a method of an object that really does not exist.

And we don't like these null pointer exceptions because they cause our programs to stop, and we usually don't want our programs to blow up if we can help it.

Optionals let us wrap these variables in a little pretty box, and they provide us with some cool methods that can help us to detect whether or not we actually have any value in the box. And if we do, then we can proceed to do things with that value.

Example Szenario

public class OptionalTest {
    public static void main(String[] args) {
        Optional<String> optMsg = Optional.of("Hello");
        System.out.println(optMsg);
    }
}

output:

Optional[Hello]

Process finished with exit code 0

As you can see, it does print out the word hello, but it's surrounded by this optional thing because the object that we have here is not a string. It is an optional of a string.

of vs ofNullable

Optional present themselves with this word optional and then brackets and then whatever thing is inside of it. And then it's calling the two string method of whatever object is inside of the optional. Note that optionals are really just wrappers for other objects or for nothing at all.

Here we've got the msg to uppercase, right? Let's say that we wrap this message in an optional. In this particular case, I have to create an optional of the msg to object using the ofNullable Method, because we can see that msg2 is null. Now I can provide an alternative string if that value was actually null

public class OptionalTest {
    public static void main(String[] args) {
        String msg = "Hello";
        String msg2 = null;
        Optional<String> optMsg = Optional.ofNullable(msg2);
        String finalOutput = optMsg.orElse("alternative").toUpperCase();
        System.out.println(finalOutput);
    }
}

output:

ALTERNATIVE

Process finished with exit code 0

So the point of all of this is that it should now be impossible to get a null pointer exception as a result of msg2 value being null because of the Optional.ofNullable()

So what about the Optional.of() ? Let's run this code:

public class OptionalTest {
    public static void main(String[] args) {
        String msg = "Hello";
        String msg2 = null;
        Optional<String> optMsg = Optional.of(msg2);
        System.out.println(optMsg);
    }
}

output:

Exception in thread "main" java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:208)
	at java.base/java.util.Optional.of(Optional.java:113)
	at section11_loose_ends.datastore.OptionalTest.main(OptionalTest.java:10)

Process finished with exit code 1

The reason for that is that this of method is expecting that the value you're passing in to it is not null. So you could use an Optional.of so that you could intentionally test and make sure that the values that you were passing in to it were not null.

You should generally only be using Optional.of in two cases:

  1. First case is you're 100% certain that the values that you will be passing into it won't be null.
  2. You're not sure if the values might be null and you're ok with your program blowing up with a null pointer exception. Maybe in some cases you want that to happen, because the variables that shall be passing into the Optional.of 99.9% of the time should not be null. And if they are, then you've got bigger problems and you want to know about those problems, right? We don't want our program to just carry on happily when unexpected circumstances occur. In some cases, we want the program to blow up and die with a nice stack trace so that we can learn.

get

Let's run this and see what happens:

public class OptionalTest {
    public static void main(String[] args) {
        String msg = "Hello";
        String msg2 = null;
        Optional<String> optMsg = Optional.ofNullable(msg2);
        System.out.println(optMsg.get());
    }
}

output:

Exception in thread "main" java.util.NoSuchElementException: No value present
	at java.base/java.util.Optional.get(Optional.java:143)
	at section11_loose_ends.datastore.OptionalTest.main(OptionalTest.java:11)

Process finished with exit code 1

So because we're using the ofNullable, we're not blowing up online ten anymore. When we pass in this null value now, we're blowing up with this get(). It just tries to get the value out. And it doesn't do any checks and it doesn't give us any options. And in that case, to do what we're doing here is really no better than just not using Optionals at all and just having a value that could possibly be null and then trying to use it right? So no advantage to that.

orElseThrow

Let's say that we wanted to throw a different type of exception. This works like a supplier function. The thing that I'm going to return, though, will be something that extends runtime exception:

public class OptionalTest {
    public static void main(String[] args) {
        String msg = "Hello";
        String msg2 = null;
        Optional<String> optMsg = Optional.ofNullable(msg2);

        System.out.println(optMsg.orElseThrow(() -> new RuntimeException("Whoops, Nothing found... ")));
    }
}

output:

Exception in thread "main" java.lang.RuntimeException: Whoops, Nothing found... 
	at section11_loose_ends.datastore.OptionalTest.lambda$main$0(OptionalTest.java:11)
	at java.base/java.util.Optional.orElseThrow(Optional.java:403)
	at section11_loose_ends.datastore.OptionalTest.main(OptionalTest.java:11)

Process finished with exit code 1

We can call its constructor so that we can pass in some simple message like nothing found. Typically in real life, you don't want to be creating new runtime exceptions like this. You want a first go look through all of the standard exceptions that are already created with Java and see if there's one that's already existing whose name matches with your circumstance. So like if you were looking for a file and you couldn't find one, maybe there's a runtime exception that has something to do with file related exceptions.

And only after you've looked through all of the standard exceptions and couldn't find one that matches with your circumstance in terms of its name, then you could consider creating your own class that extends runtime exception or exception if that's your case for, like a checked exception or something.

orElseGet

This Method takes a supplier, which is one of the functional interfaces. It's an interface that represents a method are a function that doesn't take an input, but it supplies things or values.

We could have a method reference, like maybe there's some method somewhere else in our code that generates passwords. What you could have is something like basically, if they supplied the password, then go ahead and do whatever you were planning to do with the password. But if they did not supply the password, then maybe you're doing this orElseGet and then this orElseGet is pointing at a method reference to some fancy method that can generate new passwords for us each time we call it.

filter

This filter here works pretty much exactly like the filter in the streams API takes a predicate does the same stuff, except where the filter from the Streams API would return a stream again.

A stream of whatever this filter here returns an optional of whatever. Lets look at this Example:

public class OptionalTest_v2 {
    public static void main(String[] args) {
        String msg = "cat";
        Optional<String> optMsg = Optional.ofNullable(msg);
        System.out.println(optMsg.filter(s -> s.length() > 3).orElse("invalid"));
    }
}

output:

invalid

Process finished with exit code 0

The functionality allows you to use this if you wanted to ensure not only that there should be an object inside of the optional, but you can also have a chance to test that object for some type of property or behavior or whatever. Now, if we want to make sure that we get something more legit to print, which would be some type of a string, we can fall back on our familiar fallback methods like orElse, so we can do an orElse and then say something like "invalid", right?

Now let's test what happens, though, if we actually put legitimate strings in here and not only one with the length of three:

public class OptionalTest_v2 {
    public static void main(String[] args) {
        String msg = "cats";
        Optional<String> optMsg = Optional.ofNullable(msg);
        System.out.println(optMsg.filter(s -> s.length() > 3).orElse("invalid"));
    }
}

output:

cats

Process finished with exit code 0

Map Function on Optionals

What if we have some custom Class or Records of which we want to access specific fields and use Optionals this for? Let's say we have an optional of a person class and we want to obtain just the first name, for example, of this person. Obviously, you can't just call a getfirstName Method on this Object, because this isn't a person that we're working with right, Ii's an optional. So I would have to first peel off the optional layer to get to the person underneath so that I can then get to the first name.

So I could do a get, which we already know is generally not a good idea or I could do and orElse right. However, now the orElse is wanting me to specify an alternative object that it should return if there wasn't anything in the optional. And that might be a little annoying in this case, because the alternative object isn't the string anymore, it's actually a person. I'd have to supply a whole other person as a backup person and that feels a little awkward. We don't really want to have to do all of that right?

But there's another thing that we could do: there's a map function on optionals, just like from the Streams API. We can use the map function to basically convert or translate whatever our main starring object type was within the stream to something else. So we could go from a stream of strings to a stream of integers or longs or something like that.

public class OptionalTest_v3 {
    record Car(String make, String model, String color, Year year) {}
    record Person(String firstName, String lastName, Car car) {}
    public static void main(String[] args) {
        Person p1 = new Person("Tom", "Thumb", new Car("Tesla", "X", "Red", Year.of(2018)));
        Person p2 = new Person("Jerry", "Thumb", new Car("Tesla", "Y", "White", Year.of(2020)));

        Optional<Person> optPerson = Optional.of(p1);
        System.out.println(optPerson.map(person -> person.firstName)); // traditional lambda expression
        System.out.println(optPerson.map(Person::firstName)); // using a method reference
    }
}

output:

Optional[Tom]
Optional[Tom]

Process finished with exit code 0

So we still have an Optional which does not allow Nullable Values. Let's further improve this:

public class OptionalTest_v3 {
    record Car(String make, String model, String color, Year year) {}
    record Person(String firstName, String lastName, Car car) {}
    public static void main(String[] args) {
        Person p1 = new Person("Tom", "Thumb", new Car("Tesla", "X", "Red", Year.of(2018)));
        Person p2 = new Person("Jerry", "Thumb", new Car("Tesla", "Y", "White", Year.of(2020)));
        Person p3 = null;

        Optional<Person> optPerson = Optional.ofNullable(p2);
//        System.out.println(optPerson.map(person -> person.firstName)); // traditional lambda expression
        System.out.println(optPerson
                .map(Person::car)
                .map(Car::make)
                .orElse("Unknown firstName"));
    }
}

output:

Tesla

Process finished with exit code 0

Optionals in APIs and domain models

Let's say that you're modeling a person like in the last example and you say, a person will always have to have a firstName and lastName, but they may optionally own a Car. Not everybody owns a car, especially people who live in cities. So owning a car could be considered optional. So you might want to model your Person class with that concept baked right in.

If we go ahead and just make the Car field optional, this leads to the problem that we will get a map inside of a map in our Lambda expression with the method reference of .map(Car::make)

We can solve this my using a flatMap:

public class OptionalTest_v3 {
    record Car(String make, String model, String color, Year year) {}
    record Person(String firstName, String lastName, Optional<Car> car) {}
    public static void main(String[] args) {
        Person p1 = new Person("Tom", "Thumb", Optional.of(new Car("Tesla", "X", "Red", Year.of(2018))));
        Person p2 = new Person("Jerry", "Thumb", Optional.of(new Car("Tesla", "Y", "White", Year.of(2020))));
        Person p3 = null;

        Optional<Person> optPerson = Optional.ofNullable(p2);
//        System.out.println(optPerson.map(person -> person.firstName)); // traditional lambda expression
        System.out.println(optPerson
                .flatMap(Person::car)
                .map(Car::make)
                .orElse("Unknown firstName"));
    }
}

output:

Tesla

Process finished with exit code 0

Returning Optionals instead of throwing Exceptions

A lot of newer APIs that do things like we did before may opt not to throw an exception anymore, but instead to model the API such that it would just return an optional. Lets improve our Repository Class to store People in it and access them by ID.

public class RepositoryGeneric_v4<T extends RepositoryGeneric_v4.IDable<V>, V> {
    record Person(String fristName, String lastName, Long id) implements IDable<Long>{};
    interface IDable<U> {
        U id();
    }
    private List<T> records = new ArrayList<>();

    List<T> findAll() {
        return records;
    }

    T save(T record) {
        records.add(record);
        return record;
    }

    static <T,V> V encrypt(T data, Function<T, V> func) {
        return func.apply(data);
    }

    Optional<T> findById(long id) {
        return records.stream().filter(p -> p.id().equals(id)).findFirst();
    }

    public static void main(String[] args) {
        RepositoryGeneric_v4<Person, Long> pRepo = new RepositoryGeneric_v4<>();
        pRepo.save(new Person("Chuck", "Norris", 10L));
        pRepo.save(new Person("Max", "Müller", 20L));
        pRepo.save(new Person("Hans", "Wurst", 30L));

        Person foundPerson = pRepo.findById(30L).get();
        System.out.println(foundPerson);

//        System.out.println(pRepo.findAll());

        System.out.println(RepositoryGeneric_v4.<String, String>encrypt("Hello", m -> m.toUpperCase()));
        System.out.println(RepositoryGeneric_v4.<String, Integer>encrypt("Test", m -> m.hashCode()));
    }
}

output:

Jerry

Process finished with exit code 0

What if we now have the Case of an inexistent findById call:

        String fName = repo.findById(300L)

will return:

firstName not found

Process finished with exit code 0

Isn't that nice? We don't have to deal with an exception where we'd have to use a try catch block and then try it in and then catch it and then do some alternative business because we didn't get the thing that we were looking for the record that we were looking for right now.

Instead, we can just use the same code for both circumstances of whether or not we found the actual person from the data store from the repository. This is much more concise code than even having to do the try catch block and all of that business and most Java developers don't really care for doing a whole lot of try catch blocks because it just kind of looks nasty.

List of Optionals

See how you can filter out null Values in a List of Optionals Stream:

public class OptionalTest_v5 {
    record Car(String make, String model, String color, Year year) {}
    record Person(Long id, String firstName, String lastName, Optional<Car> car) implements RepositoryGeneric_v4.IDable<Long>, RepositoryGeneric_v4.Saveable {}
    public static void main(String[] args) {
        RepositoryGeneric_v4<Person, Long> repo = new RepositoryGeneric_v4<>();
        Person p1 = new Person(100L, "Tom", "Thumb", Optional.of(new Car("Tesla", "X", "Red", Year.of(2018))));
        Person p2 = new Person(200L, "Jerry", "Thumb", Optional.of(new Car("Tesla", "Y", "White", Year.of(2020))));
        Person p3 = null;
        Person p4 = new Person(400L, "Jake", "Thumb", Optional.of(new Car("Tesla", "3", "Blue", Year.of(2019))));
        Person p5 = new Person(500L, "Johnny", "Thumb", Optional.of(new Car("Tesla", "S", "Black", Year.of(2021))));

        List<Optional<Person>> people = List.of(Optional.of(p1), Optional.of(p2), Optional.ofNullable(p3), Optional.of(p4), Optional.of(p5));
        people.stream()
                .filter(Optional::isPresent) // filter out the null's
                .map(Optional::get) // unwrap the Optionals to the actual Objects
                .map(Person::firstName)
                .forEach(System.out::println);
    }
}

output:

Tom
Jerry
Jake
Johnny

Process finished with exit code 0

but you can also use the Stream API directly on an Object, like so:

public class OptionalTest_v5 {
    record Car(String make, String model, String color, Year year) {}
    record Person(Long id, String firstName, String lastName, Optional<Car> car) implements RepositoryGeneric_v4.IDable<Long>, RepositoryGeneric_v4.Saveable {}
    public static void main(String[] args) {
        RepositoryGeneric_v4<Person, Long> repo = new RepositoryGeneric_v4<>();
        Person p1 = new Person(100L, "Tom", "Thumb", Optional.of(new Car("Tesla", "X", "Red", Year.of(2018))));
        Person p2 = new Person(200L, "Jerry", "Thumb", Optional.of(new Car("Tesla", "Y", "White", Year.of(2020))));
        Person p3 = null;
        Person p4 = new Person(200L, "Jake", "Thumb", Optional.of(new Car("Tesla", "3", "Blue", Year.of(2019))));
        Person p5 = new Person(200L, "Johnny", "Thumb", Optional.of(new Car("Tesla", "S", "Black", Year.of(2021))));

        Optional<Person> p11 = Optional.ofNullable(p1);
        p11.stream()
                .map(Person::firstName)
                .forEach(System.out::println);
//
//        List<Optional<Person>> people = List.of(Optional.of(p1), Optional.of(p2), Optional.ofNullable(p3), Optional.of(p4), Optional.of(p5));
//        people.stream()
//                .filter(Optional::isPresent) // filter out the null's
//                .map(Optional::get) // unwrap the Optionals to the actual Objects
//                .map(Person::firstName)
//                .forEach(System.out::println);
    }
}

output: 

Tom

Process finished with exit code 0