Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How I learnt to like optional

Avatar for Steinar Bang Steinar Bang
September 08, 2022

How I learnt to like optional

10 min lightning talk from JavaZone 2022, describing my process of going from ranting about optional and to rather liking it and using it in my own code.

Contains practical tips for how to traverse deeply nested Java data structures, without getting NPEs and without drowining in pages of code containing only null checks.

Avatar for Steinar Bang

Steinar Bang

September 08, 2022

Other Decks in Programming

Transcript

  1. I used to rant against Optional<> • They complain about

    JDK bloat and then they add this…? • What makes checking for value.isEmpty() more readable than value == null? • And you have to call value.get() to get at the value? More clutter in the code! • What is the benefit? • What? • Whatwhatwhat???? • Arrrgh! Useless bloat! • Also the uses of map() with something that is a single value and not a collection…? • And what is this flatMap() anyway…?
  2. Why did I change my mind? I often need data

    from deep down in complicated data structures Want this! And this! And this!
  3. Traversing hierarchical data structures The fragile way: Integer findLeafnodeValue() {

    return someService.findSomething().findOther().findNestedOther().someValue(); } • Any step in the chain may be null (and crash your program) • Two approaches to doing safe traversal – Return a default if a link in the chain isn’t present – Throw an exception with a meaningful log message when a value can’t be found
  4. Traversing hierarchical data structures with null checks Integer findLeafnodeValue() {

    Something something = someService().findSomething(); if (something == null) { throw new MyAppException("Unable to find Something when looking for leafnodeValue"); } Other other = something.findOther(); if (other == null) { throw new MyAppException("Unable to find Other when looking for leafnodeValue"); } NestedOther nestedOther = other.findNestedOther(); if (nestedOther == null) { throw new MyAppException("Unable to find NestedOther when looking for leafnodeValue"); } return nestedOther.someValue(); }
  5. Using Optional: the naive approach Integer findLeafnodeValue() { Optional<Something> something

    = someService().findSomething(); if (something.isEmpty()) { throw new MyAppException("Unable to find Something when looking for leafnodeValue"); } Optional<Other> other = something.get().findOther(); if (other.isEmpty()) { throw new MyAppException("Unable to find Other when looking for leafnodeValue"); } Optional<NestedOther> nestedOther = other.get().findNestedOther(); if (nestedOther.isEmpty()) { throw new MyAppException("Unable to find NestedOther when looking for leafnodeValue"); } return nestedOther.get().someValue(); }
  6. Using Optional: better Integer findLeafnodeValue() { Something something = someService().findSomething()

    .orElseThrow(() -> new MyAppException("Unable to find Something when looking for leafnodeValue")); Other other = something.findOther() .orElseThrow(() -> new MyAppException("Unable to find Other when looking for leafnodeValue")); NestedOther nestedOther = other.findNestedOther() .orElseThrow(() -> new MyAppException("Unable to find NestedOther when looking for leafnodeValue")); return nestedOther.someValue(); } (Note! Local variables aren’t Optional<Something>)
  7. What else can optional do? • Can it be used

    to conveniently provide a default value? • But before we proceed, fragile traversal reformatted: Integer findLeafnodeValue() { return someService .findSomething() .findOther() .findNestedOther() .someValue(); } • Note: I use the same example throughout the presentation • This reformatting makes it look more like other places in the presentation (but won’t help with the fragileness...)
  8. Using Optional: return default value instead of throw Integer findLeafnodeValue()

    { Something something = someService().findSomething().orElse(new Something()); Other other = something.findOther().orElse(new Other()); NestedOther nestedOther = other.findNestedOther().orElse(new NestedOther()); return nestedOther.someValue(); } • Similar to JavaScript “|| {}” pattern • Syntax a bit verbose
  9. Using Optional: chaining references (first take) Integer findLeafnodeValue() { return

    someService .findSomething().orElse(new Something()) .findOther().orElse(new Other()) .findNestedOther().orElse(new NestedOther()) .someValue(); } • More elegant than the previous example, but still verbose • Also, the default values are created whether they are used or not, which is wasteful
  10. Using Optional: chaining references (delay default create) Integer findLeafnodeValue() {

    return someService.findSomething().orElseGet(() -> new Something()) .findOther().orElseGet(() -> new Other()) .findNestedOther().orElseGet(() -> new NestedOther()) .someValue(); } • Default objects aren’t created until they are actually needed • But the syntax is even more verbose than the earlier example
  11. Using Optional: chaining references (method refs) Integer findLeafnodeValue() { return

    someService.findSomething().orElseGet(Something::new) .findOther().orElseGet(Other::new) .findNestedOther().orElseGet(NestedOther::new) .someValue(); } • Slightly less verbose • Slightly more efficient (no lambda creation required)
  12. Is there a better way? Specifically: is there a less

    verbose way to do this? Answer: yes, using flatMap() and/or map()
  13. Chaining with flatMap() and map() Integer findLeafnodeValue() { return someService.findSomething()

    .flatMap(s -> s.findOther()) .flatMap(o -> o.findNestedOther()) .map(n -> n.someValue()) .orElse(null); } • How does this work? ◦ When flatMap() or map() is called on an optional, its argument is only run for non-empty values ◦ If one method in the chain returns an empty optional, the following methods will be skipped and orElse() will be called • Probably not a good idea to return null as the default • Why flatMap() on most of them and then map() on the last one?
  14. Chaining with flatMap() and map(): method references Integer findLeafnodeValue() {

    return someService.findSomething() .flatMap(Something::findOther) .flatMap(Other::findNestedOther) .map(NestedOther::someValue) .orElse(null); } • Still probably not a good idea to return null as the default • Still a question of why flatMap() on most of them and then map on the last one?
  15. What is the difference between flatMap() and map()? Both flatMap()

    and map() returns an Optional<> from an incomprehensible argument: • <U> Optional<U> flatMap​ (Function<? super T,​ ? extends Optional<? extends U>> mapper) • <U> Optional<U> map​ (Function<? super T,​ ? extends U> mapper)
  16. The difference between flatMap() and map() • Both return Optional<>

    • Both take a functional argument • flatMap() expects its functional argument to return Optional<> • map() wraps the result of the function in Optional<> • Side Note: the wrapping done by map() has the same semantics as Optional.ofNullable(), which is very useful when dealing with legacy APIs
  17. So why map() on the last link in the chain?

    Because NestedOther::someValue returns Integer and not Optional<Integer>
  18. Better default: return a number Integer findLeafnodeValue() { return someService.findSomething()

    .flatMap(Something::findOther) .flatMap(Other::findNestedOther) .map(NestedOther::someValue) .orElse(-1); } • Won’t cause an NPE
  19. Or you can throw an exception on failure Integer findLeafnodeValue()

    { return someService.findSomething() .flatMap(Something::findOther) .flatMap(Other::findNestedOther) .map(NestedOther::someValue) .orElseThrow(() -> new MyAppException("Unable to find a leaf node value"); } • A lot more compact than the version that traverses • Arguably better than an NPE (can get a meaningful log message) • But no info about what step in the chain fails, the way our initial “good” approach for using Optional did
  20. Best solution: return Optional<Integer> Optional<Integer> findLeafnodeValue() { return someService.findSomething() .flatMap(Something::findOther)

    .flatMap(Other::findNestedOther) .map(NestedOther::someValue); } • Lets the caller decide whether to provide a default or throw • The caller can probably provide a more meaningful exception message
  21. Safe execution on method on leaf object someService.findSomething() .flatMap(Something::findOther) .flatMap(Other::findNestedOther)

    .ifPresent(NestedOther::incrementSomeValue); • No exception is thrown when a step in the chain is missing • The incrementSomeValue method is only called when there is a NestedOther to invoke it on
  22. Bonus: Use Optional<> to walk refs that returns null Optional.ofNullable()

    and the Optional.ofNullable() behaviour of Optional.map() can be used to walk a reference chain that uses null to signal no-value. If the classes in the example had returned values instead of Optional<> wrapped values: Optional<Integer> findLeafnodeValue() { return Optional.ofNullable(someService.findSomething()) .map(Something::findOther) .map(Other::findNestedOther) .map(NestedOther::someValue); } • No if-null checks and no NPEs
  23. Summing it up Optionals can be useful! Optionals can help

    create robust, readable and maintainable code! (no “if (xxx != null)” without getting NPEs) Just follow these guidelines • Use Optional<> in return values of methods to signal “no value” to callers • Never use Optional<> as the type of a method argument • Never have a field or local variable that is Optional<> • If you’re calling Optional.isEmpty(), Optional.isPresent() or Optional.get() you are probably doing something wrong. Try to reshape your code so it only proceeds with a valid value or use a default value or fail • Methods that are OK to use: Optional.map(), Optional.flatMap(), Optional.orElse(), Optional.orElseGet(), Optional.orElseThrow(), Optional.ifPresent()
  24. Blog posts and code examples • Blog posts behind this

    lightning talk ◦ https://steinar.bang.priv.no/2021/12/28/how-i-learnt-to-like-optional/ ◦ https://steinar.bang.priv.no/2022/01/18/chaining-optionals-using-flatmap-and-map/ • Code examples ◦ https://github.com/steinarb/optionaldemo