Search code examples
java-8java-stream

Traverse complex object using Optional stream


I want to iterate a list with nested list of object similar to below

Country {
  String name;
  State capital
  List<State> states;
}

State {
  String name;
  City capital
  List<City> cities;
}

City {
  String name;
}

I am using below code to get the capital list of all states with name 'XYZ'

List<Country> countries = getCountriesList();

if (countries != null) {
    countries.stream()
            .filter(Objects::nonNull)
            .filter(country -> CollectionUtils.isNotEmpty(country.getStates()))
            .flatMap(country -> country.getStates().stream())
            .filter(Objects::nonNull)
            .filter(state -> "XYZ".equalsIgnoreCase(state.getName()))
            .map(State::getCapital)
            .filter(Objects::nonNull)
            .map(capital -> capital.getName())
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
}

How can I refactor this to avoid using non null at each step?


Solution

  • I'm not sure why you would want to avoid using nonNull. The current way of doing it is perfectly readable, and it clearly shows your intentions. If I really were to nitpick, I would remove the filter for empty states,

    .filter(country -> CollectionUtils.isNotEmpty(country.getStates()))
    

    since this is already covered by the flatMap in the next line. Also, you can use .toList() instead of collect since Java 9.

    That said, if you really want to get rid of the nonNull filters, you can do so by using a lot of Optionals. The capital and capital name null checks can be done by flat mapping to the optional's stream, and the state null check can be combined with the "XYZ" state name filter on the next line.

    That gives us:

    countries.stream()
        .filter(Objects::nonNull)
        .flatMap(country -> country.getStates().stream())
        .filter(state -> state != null && "XYZ".equalsIgnoreCase(state.getName()))
        .flatMap(state -> Optional.ofNullable(state.getCapital()).stream())
        .flatMap(capital -> Optional.ofNullable(capital.getName()).stream())
        .toList();
    

    The first null check is a bit hard to get rid of, unless you have a known country that has no states:

    public static final Country EMPTY = new Country("", null, List.of());
    

    Then you could use Objects.requireNonNullElse to default to the EMPTY country:

    countries.stream()
        .flatMap(country -> Objects.requireNonNullElse(country, Country.EMPTY).getStates().stream())
    

    Really though, with each nonNull you avoid, you are just making the code harder to read.