Java Streams with Optional how transforming works

Viewed 155

I can't understand the concept of optional and streams when used with map() vs flatMap().

I understand this code :

public String getCarInsurance(Optional<Person> optPerson) {
    return optPerson.flatMap(Person::getCar) //Returns Optional<Car>
            .flatMap(Car::getInsurance) //Returns Optional<Insurance>, .map would give Optional<Optional<Insurance>>
            .map(Insurance::getName) //no need for flatmap because getName returns String, not Optional<String>
            .orElse("UNKNOWN"); //returns value or Unknown
}

But, I can't understand this code :

 public Set<String> getCarInsuranceNames(List<Person> persons) {
        return persons.stream()
                .map(Person::getCar) //Convert the list of persons into Stream Optional<Car> 
                .map(optCar -> optCar.flatMap(Car::getInsurance)) //I don't understand this line
                .map(optIns -> optIns.map(Insurance::getName))
                .flatMap(Optional::stream)
                .collect(toSet());
    }

edit:

Why in this case we need .map(optCar -> optCar.flatMap(Car::getInsurance)), to get Stream Optional<Insurance> ? But in first code .map(Car::getInsurance) was enough to get Optional<Insurance>.

I understand why we need .flatmap instead of .map in first code, but I can't understand the pipeline of Stream in second code.

In second code .map(Person::getCar) returns Stream Optional<Car> and then we use .map(optCar -> optCar.flatMap(Car::getInsurance))> to get Stream Optional<Insurance> . Why wouldn't .flatmap(Car::getInsurance) get us Stream Optional<Insurance> ? This what I fail to understand.

edit: Domain classes below if needed.

public class Insurance {
    public String getName() {
        return name;
    }   
}

public class Car {
    Optional<String> name;
        public Optional<String> getName() {
            return name;
        }

        public class Person {
            Optional<Car> car;
            public Optional<Car> getCar(){
                return car;
            }
        }
2 Answers

If you could keep both the code segments alike, it would help you understand

public String getCarInsurance(Optional<Person> optPerson) {
    return optPerson.flatMap(Person::getCar) //Returns Optional<Car>
            .flatMap(Car::getInsurance) //Returns Optional<Insurance>, .map would give Optional<Optional<Insurance>>
            .map(Insurance::getName) //no need for flatmap because getName returns String, not Optional<String>
            .orElse("UNKNOWN"); //returns value or Unknown
}

public Set<String> getCarInsuranceNames(List<Person> persons) {
    return persons.stream()
            .map(person -> person.getCar() // chained in a similar manner as above
                    .flatMap(Car::getInsurance)
                    .map(Insurance::getName))
            .flatMap(Optional::stream)
            .collect(Collectors.toSet());
}

or much cleaner if you could improve the existing contracts of your APIs to

public Optional<String> getCarInsurance(Person person) {
    return person.getCar() //Returns Optional<Car>
            .flatMap(Car::getInsurance) //Returns Optional<Insurance>, .map would give Optional<Optional<Insurance>>
            .map(Insurance::getName); //no need for flatmap because getName returns String, not Optional<String>
}

public Set<String> getCarInsuranceNames(List<Person> persons) {
    return persons.stream()
            .flatMap(person -> getCarInsurance(person).stream())
            .collect(Collectors.toSet());
}

It's done to avoid nesting of Optional. optCar is of type Optional<Car>. If we invoked map(optCar -> optCar.map(Car::getInsurance)) instead of map(optCar -> optCar.flatMap(Car::getInsurance)), then we would end up having Stream<Optional<Optional<Insurance>>>.

But nesting optional does not make much sense, if we have optA of type Optional<A> and there is a method A#getB, that returns Optional<B>, then we can simplify the code by invoking optA.flatMap(A::getB), which will result in Optional<B>:

Optional<B> optB = optA.flatMap(A::getB);
Optional<Optional<B>> optOptB = optA.map(A::getB); // don't do that

optA.flatMap(A::getB) works intuitively - if optA is empty or if optA has value, but optA.getB() is empty, then the result of the flatMap will be an empty Optional<B>. Only if optA has a value and optA.getB() is not null, the result will not be empty.

In the first snippet flatMap was enough, because it's an operation on Optional, whereas in the second snippet we're operation on a Stream.

Related