Skip to content

Instantly share code, notes, and snippets.

@SwingGuy1024
Last active January 30, 2024 00:34
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SwingGuy1024/7ee92fb96c8a65134d6ea4821faa595f to your computer and use it in GitHub Desktop.
Save SwingGuy1024/7ee92fb96c8a65134d6ea4821faa595f to your computer and use it in GitHub Desktop.
Examples of bad usages of Optional that I've seen in production code.

Bad Use of Optional: Code Examples

I was disappointed by the introduction of Optional to the Java language, because I've been using @Nullable and @NotNull annotations for years, and it's a much simpler solution. The big advantage of this approach is that it makes null pointers a compile-time error rather than a run-time bug. Now that I've worked with Optional and seen it in action in production code, I've only strengthened my earlier view. Here are some examples, all taken from production code, of bad usages of Optional, with notes on what it would look like with the NullnessChecker annotation processor instead.

Example 1: Needless Verbosity

01 private void someMethod(Widget widget, ... <other parameters>) {
02   Optional<Widget> widgetOpt = Optional.of(widget);
03   if (!widgetOpt.isPresent) {
04     throw new BusinessException("Missing Widget");
05   }
06   // ... widgetOpt is never used again
07   // ... several lines later
08   Optional<Widget> widgetMaybe = Optional.of(widget); // this duplicates the previous Optional!
09   processData(widgetMaybe);
10   // ...more code
11 }

What's going on here? On lines 2 and 3, the coder is taking two lines to do a simple null check. Then widgetOpt is thrown away, and a second Optional of the same value is created on line 8, because they call a method that requires it.

The first use is needlessly verbose, and seems to be done just to avoid ever using the null keyword in the code, as if bansishing it will prevent null-pointer bugs. (It won't.) Lines 02 and 03 could have been made cleaner, more readable, and even marginally faster by writing if (widget == null) {

The second use of Optional is necessary only because the API they're using requires it.

To make matters worse, there's a bug in line 02 that didn't get caught. It says Optional.of(widget), but it should say Optional.ofNullable(widget)! So if a null value ever gets passed into this method, it will throw a NullPointerException instead of the BusinessException thrown on line 04. (The second use of Optional.of() in line 08 is fine, because we already know it's not null.) But nobody caught this error for two reasons. First, There was no unit test. Second, The code that called this private method had already made sure that widget was not null, so the test was unnecessary.

With the Nullness Checker

01 private void someMethod(Widget widget, ... <other parameters>) {
02   // ... several lines later
03   processData(widget);
04   // ...more code
05 }

The annotation processor defaults to @NonNull, so if I added a null-check, it would tell me that it's unnecessary. The annotation processor knows that the calling method already checks for null, so it's happy with this. If somebody later removes the test for null in the calling method, it will generate a compile-time error where this method is called.

Example 2: Misleading Return values

Here's a class that was generated by Swagger 2.0, using their Swing server generator, with the Java 1.8 option turned on:

@Controller
public class MenuItemApiController implements MenuItemApi {
    private final ObjectMapper objectMapper;
    private final HttpServletRequest request;

    @Autowired
    public MenuItemApiController(ObjectMapper objectMapper, HttpServletRequest request) {
        this.objectMapper = objectMapper;
        this.request = request;
    }

    @Override
    public Optional<ObjectMapper> getObjectMapper() { return Optional.ofNullable(objectMapper); }

    @Override
    public Optional<HttpServletRequest> getRequest() { return Optional.ofNullable(request); }
}

This has two private final members, and two getters that wrap the values in an Optional. But take a look at that constructor. It's annotated with @Autowired. Spring will automatically instantiate valid values for both of the parameters and inject them into the constructor. And they're final, so they'll never change to null. The methods are returning objects that can never be null, wrapped inside an Optional. Does the use of Optional here add clarity to the API? It actually does the opposite: It suggests that two never-null objects might actually be null, forcing users to write extra code to check for null values that they'll never see.

With the Nullness Checker

@Controller
public class MenuItemApiController implements MenuItemApi {
    private final ObjectMapper objectMapper;
    private final HttpServletRequest request;

    @Autowired
    public MenuItemApiController(ObjectMapper objectMapper, HttpServletRequest request) {
        this.objectMapper = objectMapper;
        this.request = request;
    }

    @Override
    public ObjectMapper getObjectMapper() { return objectMapper; }

    @Override
    public HttpServletRequest getRequest() { return request; }
}

Again, this is simpler, because @NonNull is assumed. The constructor can't get called with null values, so we don't bother with Optional.

Example 3: Misleading Parameters

Many people have written guidelines recommending against using Optional as parameter methods, but people do it anyway. A common variation of the code in example 1 looks like this:

01 private void someMethod(Optional<Widget> widgetOpt) {
02   if (!widgetOpt.isPresent) {
03     throw new BusinessException("Missing Widget");
04   }
05   // ... (More code)

Again I have to ask: What clarity is being added to the API by using Optional? To anyone looking at the API, it implies that null is a valid input value. But once you look at the code, it's clearly not. Putting Optional on a parameter that shouldn't be null results in a misleading API that's also more verbose to call, since the user must now write someMethod(Optional.ofNullable(widget)); instead of someMethod(widget); A good interface should take care of boilerplate details needed to perform its function. By using Optional in a parameter, this forces the user to add a bit of boilerplate that doesn't add any value.

With the Nullness Checker

01 private void someMethod(Widget widget) {
02   // ... (More code)

Again, @NonNull is assumed, so we don't need to check for null. A value that may be null will generate a compiler error.

Example 4: Seemingly Sensible Use

Here's a case where putting Optional on a method parameter doesn't mislead the user:

01 private void someMethod(Optional<Widget> widgetOpt) {
02   final Widget widget = widgetOpt.orElse(getDefaultWidget());
03   // ... (More code)

Here, finally, we can say that Optional is adding clarity to the API. Use of null instead of a Widget instance is actually allowed. Here, the API does not mislead anyone. Of course, users who choose to use null must be careful not to call Optional.of(widget). Instead, they should use Optional.ofNullable(widget) or Optional.empty(), but that's a fail-fast mistake, so it will probably get caught early. Unfortunately, so many developers wrap mandatory parameters inside Optional, that the meaning of this occasional valid use will often get lost anyway. And, there's a simpler way to write the API that doesn't add verbosity to the calling method:

01 private void someMethod(final Widget widgetOrNull) {
02   final Widget widget = widgetOrNull == null? getDefaultWidget() : widgetOrNull;
03   // ... (More code)

Simply renaming the parameter will provide the same information as Optional. Before Optional was invented, not many people did this, which is probably a shame, because it adds clarity without adding verbosity. It also eliminates the (minor) bug that will arise if the calling method uses Optional.of() instead of Optional.ofNullabe().

With the Nullness Checker

01 private void someMethod(@Nullable widget) {
02   widget = (widget == null) ? getDefaultValue() : widget;
03   // ... (More code)

The @Nullable annotation becomes part of the API, so users can see that a null value is allowed. The Nullness checker will figure out that widget is not null, and proceed from that assumption.

Example 5: Pointless

01 private void someMethod(Optional<Widget> widgetOpt) {
02   if (!widgetOpt.isPresent) {
03     throw new NullPointerException();
04   }
05   Widget widget = widgetOpt.get();
06   // ... (More code)

Yeah. I've seen this. Just get rid of lines 2 through 4. It does exactly the same thing.

With the Nullness Checker

01 private void someMethod(Widget widget) {
02   // ... (More code)

Once again, passing a possibly-null value will generate a compile-time error.

Example 6: OrElse(null)

01    public User findByUuid(@NotNull String userUuid) {
02        UserTable userTable = (UserTable)this.userTableDao.findByUuid(userUuid).orElse(null);
03        return userTable != null ? this.userServiceConverter.convertFromUserTable(userTable) : null;
04    }

Huh? (More on this later.)

@ayush-finix
Copy link

ayush-finix commented Apr 5, 2019

Cool set of real cases. My only concern is that it seems like most of these cases aren't very sensible in general. If you're never calling optional.map or flatmap, then the code is just going to be terribly verbose procedural code. I know you didn't ask for it, but here are my thoughts on the examples. As purely an aside, I also hate void methods, so I'm not a big fan of code using Consumer<X>

  1. First, this seems like it should be a method on the widget class itself, but if it can't be, it seems like the calling code should instead do optWidget.ifPresent(w -> processData(w));. If you need to combine multiple optionals together then do something, you're probably going to have to use a better functional library for java, where there's an actual optional.join/combine/zip type of method. There's some nice libraries where you can do things like optA.combine(optB, (a, b) -> doStuffWithNonNull(a, b)) or <Some Optional Class>.combine(optA, optB, optC, (a, b, c) -> dostuff(a, b, c))
  2. I've written tons of spring apps, and I don't think I've needed to actually have getters for injected components. I feel like having getters for those is a code smell in general, but it's generated code, so w/e. I agree that Optional getters for autowired fields is terrible.
  3. Agreed this is terrible, but not for the same reason. The calling code should clearly be doing some variation of widget.map(w -> someMethod(w)).orElseThrow(() -> new BusinessException()) Of course, if widget is coming from like the top of the call stack, this seems like a case where null check and throw as validation seems correct. For what it's worth, I don't throw exceptions, but use a library where Try<X, T extends Throwable> and Either<L, R> are available wrapper classes. It's like Optional, but without throwing away error information.
  4. An even better way to write that api is to write a method that does not accept null, and have the calling code figure it out with the map function. Having to write every function (of a non library codebase) super defensively is already a code smell. Figure out where you're validating your parameters, and the layers after that shouldn't need to handle nulls.
  5. Same problem as 4. Like why isn't this just optWidget.ifPresent(w -> func(w)) on the calling side. If you need to know if something actually happened, I'd suggest not having so many void methods and using useful return values.
  6. This method should clearly return an Optional and should be written
public Optional<User> findByUuid(@NotNull String userUuid) {
    return Optional.ofNullable(((UserTable)this.userTableDao.findByUuid(userUuid)))
               .map(ut -> this.userServiceConverter.convertFromUserTable(ut))
}

Could probably even get a method reference to do .map(userServiceConverter::convertFromUserTable) instead. If you have to keep the signature the same, the .orElse(null) can come at the very end.

public User findByUuid(@NotNull String userUuid) {
    return Optional.ofNullable(((UserTable)this.userTableDao.findByUuid(userUuid)))
               .map(ut -> this.userServiceConverter.convertFromUserTable(ut))
               .orElse(null)
}

And we can avoid an extra wrap if this.userTableDao.findByUuid can return Optional instead of just UserTable (I know hibernate can now return Optional<T> instead of T for it's find methods, but idk where your daos come from (I use jooQ)). As a side question, why would the client of something called userTableDao need to cast the response of a find method to a UserTable??? Like, how is not like usertabledao implements basedao<UserTable, UUID> or something?

TLDR: optional is not supposed to replace null checks, it's supposed to remove them. If you replace every == null with Optional.isPresents, you're adding a bunch of cost and missing like all of the benefits.

@ts14ic
Copy link

ts14ic commented Oct 5, 2020

I'm coming from a POV of someone who's very angry about crucial debugging information being lost in countless map/filter calls.
As Brian Goetz has stated, Optional "was not to be a general purpose Maybe type", and he (Goetz) would use something like .? or ?: for that purpose.
Looking at Kotlin, where such a feature is present, I saw it being used efficiently only for data structure traversing or trully optional logic (e.g. listener calls), but never chaining several stages of business logic.
I believe there's a large incentive to use it amidst the business logic specifically because of the appeal of chainable methods like map & flatMap. Coincidentally, Kotlin doesn't have map or flatMap. It only has ?.let, which acts like map, but it's naming doesn't encourage chaining business logic.

  1. <...> it seems like the calling code should instead do optWidget.ifPresent(w -> processData(w));

It's changing semantics and becomes undebuggable - there's no logging/fail fast or anything to explain why nothing happens, when something is expected to happen (the code before was failing fast or executing something).

  1. <...> Agreed this is terrible, but not for the same reason. The calling code should clearly be doing some variation of widget.map(w -> someMethod(w)).orElseThrow(() -> new BusinessException())

Same note as above - there's nowhere here to put any logging/fail-fast or anything to. The whole pipeline is just one single expression that either evaluates to something, or throws - for unknown reasons - was it the widget that was null? What is the someMethod that made it null?

  1. <...> An even better way to write that api is to write a method that does not accept null

This is where I wholeheartedly agree. I've noticed three general strategies for null handling:

  1. The cancer - when meeting a null - a null is returned/no behavior is performed.
  2. The ambiguous - when meeting a null, it's being treated as if it's something else - List null = emptyList(), Boolean null = false, Double null = 0.0 etc.
  3. The touch-me-not - when meeting a null, throw an exception.

TLDR: optional is not supposed to replace null checks, it's supposed to remove them.

With that mentioned, I totally prefer the touch-me-not way, because it forces to redesign the code in such a way that neither null nor Optional can be used:

  • sliding statements in such a way that a null check happens as early as possible in the call-stack and not required deep in the stack similar to your first example
  • failing fast when meeting null to encourage such ASAP checks
  • removing nullable fields entirely, when possible

That is, I don't see Optional as a way to "remove" null checks. It's still a null-check, albeit a fancy one.
Where I saw Optional being efficient, is forcing null checks in compile-time when receiving a value, and some refinement map pipelines often in return statements, like the one you proposed for example 6.
This again correlates to what Brian Goetz has said - "Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result""

p.s. By refinement I mean, pipelines, where if an empty is produced, it's immediately clear that the original value before any map was the reason an empty was produced.

@ayush-finix
Copy link

ayush-finix commented Oct 30, 2020

I'm coming from a POV of someone who's very angry about crucial debugging information being lost in countless map/filter calls.

I'll be honest, this is a problem with java util Optional, but it's not a problem with the concept of using an Optional type in general. Most of your complaints exist because there's no peekEmpty method equivalent on the java util Optional class. The wrong answer to this is to throw away allowing the compiler to understand nullability by removing all optionals. The right answer is to use libraries that did make a "fully usable" option/maybe class in java: like: vavr option: https://www.vavr.io/vavr-docs/#_option or cyclops option: https://www.javadoc.io/doc/com.oath.cyclops/cyclops/10.3.0/cyclops/control/Option.html. Look at those juicy convenience methods on the class. Remember, there's literally nothing special about the java util Optional class besides being in the sdk, and the compiler help comes from the fact that you can't pass an Optional<String> into a method that expects a String

As Brian Goetz has stated, Optional "was not to be a general purpose Maybe type", and he (Goetz) would use something like .? or ?: for that purpose.

You keep referencing this, but in many functional languages, the general purpose Maybe type is just a library class with no special syntax. Look at the option class available in scala: https://www.scala-lang.org/api/current/scala/Option.html or (if you want to go fancy) datatype in haskell: https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-Maybe.html. The random nullable type examples people give (like kotlin) with .? or ?: are just a very special instance of the flatmap method, and it somewhat bothers me when people think making syntax for a single case (nullability) instead of the general case (arbitrary flatmap) is somehow "special" (and it wouldn't even resolve your issues!). I'm actually really confused as to who the target people are that would want Optional<String> and String to work together, so I'll be honest, I'm lost as to what Brian means by language support for it (and unfortunately, he hasn't expanded on it in the answer).

As an example of what I mean, you can use the same ideas for nullability for exception chaining as well. Imagine two methods like

public Option<String, Exception> method1();
public Option<Integer, Exception> method2(String a);

invoked like

method1()
    .peekEmpty(() -> log.error("method1 returned null"))
    .flatMap(s -> method2(s).peekEmpty(() -> log.error("method2 returned null"))

Now imagine two methods like

public Try<String, Exception> method1();
public Try<Integer, Exception> method2(String a);

and invoke it like

method1()
    .peekFailed(e -> log.error("method1 had exception", e))
    .flatMap(s -> method2(s).peekFailed(e -> log.error("method2 had exception", e))

Similar code (there's a common pattern), but one handles methods returning null and one handles methods "throwing" an exception. You want another example? Replace Try/Option with Future and now you have async methods, and the best part? It's all composable. For try, you also get something way better than checked or unchecked exceptions, and the consumers of the code are forced to handle the exception (with an easy way to throw it up if they don't want to since it is java still). Now the compiler helps you deal with exceptions (using Try) or null (using Option), or even both with a signature like

public Try<Option<File>, Exception> getFile(Path path)

, and the method signatures convey what's happening and the compiler forces you to handle some error case and the case that the file might be empty because you can't pass a Try<Option<File, Exception>> to a method that expects a File. The reason I show this is that there's basically nothing special about the java util version of optional except it's neutered in terms of the methods that exist on it, but you can fix that and you didn't need introduce a single .? or ?: to do it.

As to the specific examples (which seem like they got changed since I responded)
Looking at

It's changing semantics and becomes undebuggable - there's no logging/fail fast or anything to explain why nothing happens, when something is expected to happen (the code before was failing fast or executing something).

Without making other assumptions, I'd agree with just doing a null check and throwing and changing processData to take a Widget instead of an Optional<Widget> (because taking in Optional<Widget> as a parameter in any method is almost always wrong). If you really want to dig into it though, I'd rather the calling code bomb out before even invoking this method in the first place because I'm not a big fan of throwing business exceptions from validations in methods really far down the call stack. Is it actually the case that somemethod should sometimes be called with null widgets and other times be called with actual widgets or could the places where widget might be null above it throw the exception instead and remove the null check entirely from this method? Like is this method validating data and doing work of some sort? If so, it feels like it's doing too much and you can separate validating (say user input) from the work that needs done if the input is good. Is it just a super defensive null check? If so, maybe just let it NPE?

This is where I wholeheartedly agree. I've noticed three general strategies for null handling:
The cancer - when meeting a null - a null is returned/no behavior is performed.
The ambiguous - when meeting a null, it's being treated as if it's something else - List null = emptyList(), Boolean null = false, Double null = 0.0 etc.
The touch-me-not - when meeting a null, throw an exception.

Look at the current example 4. That case is just handled with method overloading (literally an example that Brian gives for not using method parameters) with

someMethod() {
return someMethod(defaultWidget)
}

someMethod(Widget w)
...do work...
}

and client code can invoke it with

someMethod(optWidget.orElse(defaultWidget))

or if you don't want to expose default widget

optWidget.map(::somemethod).orElseGet(::somemethod))

No nulls necessary at all.

@ts14ic
Copy link

ts14ic commented Oct 30, 2020

Yes, I'm familiar with Scala Option, haskell's maybe and vavr, and the glorius chaining 🙂
And yes, as you said java.util.Optional is "neutered in terms of the methods that exist on it". That's exactly what IS special about Optional 😄 . The concept maybe not special, but java.util.Optional - is, it's not even a monad, like mentioned types.

That was my entire point.
This github gist is not about Option or maybe. This gist is about java.util.Optional, which lacks the features, because it was never designed to have those features in Java.
There's no for comprehension in the language - Scala & haskell have them.
There's no pattern matching - matching on an exception of a vavr Try looks terrible, IMO. All those $() and all the different entry points in order to import all of the static dsl is boring. It would be cool if I could do import vavr.dsl.Everything.*. A match on exception in vavr is much-much-much more verbose in vavr, compared to Scala's Try.

And to continue the story - I don't see any "peekEmpty" on Scala Option in my IDE, nor in the provided link (haven't checked Try).
And even if there was - I have difficulty understanding how it would be implemented.

method1()
    .peekEmpty(() -> log.error("method1 returned null"))
    .flatMap(this::method2)
    .peekEmpty(() -> log.error("method2 returned null")

How do you know that method2 returned null? If method1 returned null, then the monad transforms into an empty. Flatmap doesn't even call method2 at this point.

Cyclops I find strange. There's something off putting about it. I've read the dysfunctional java blog series, but apart from that, the documentation is lacking. And if I go inside the source code, there's no documentation there either, the code is malformatted and also harder to read, or discover what features a type offers.
For every type, there's a lazy alternative, and reading the dysfunctional series at some point it looked like "okay, time to stop. That's like triple nested concepts, for a toy example, and everything is lazy and cacheable, and it's a horror to imagine how it scales, if even a toy example looks like this".

There's also something offputing about Scala/vavr Try. This is where I prefer cyclops Try, which has the exception type indicated. Or Rust Result which again allows to indicate the error type.
AND BTW, Rust is that language where I absolutely love the Option and Result, because there it IS a general purpose type with minimal special syntax for Result. The language is built using these abstractions from ground up, and it shows.

But my point is, there's no library that suit my taste entirely :) Because if Cyclops might offer the abstractions I need, then there's the entire ecosystem, where every library might or might not work well with vavr/Cyclops or whatever

Update:
Also, the reason I brought up special syntax, is not because I find it necessary (Rust doesn't have any for Option for example), but because I transitioned into where I saw effective usage of that syntax in Kotlin - in places where logic is trully optional.

My initial message was about this:

I'm coming from a POV of someone who's very angry about crucial debugging information being lost in countless map/filter calls.

Optional doesn't offer peekEmpty, and I don't know how it would be implemented.

@ayush-finix
Copy link

ayush-finix commented Oct 30, 2020

Hey, that's fair. I'm just used to a lot of people disparaging the concept of an Optional type completely while using terrible examples of using it, and also the idea that you need kotlin style syntax to make Optional worthwhile. It sounds like you have already done your research =P, so I'm not going to preach to you. Although

The concept maybe not special, but java.util.Optional - is, it's not even a monad, like mentioned types.

Technically, it implements ofNullable and flatMap, what else do you need for it to be?

How do you know that method2 returned null? If method1 returned null, then the monad transforms into an empty. Flatmap doesn't even call method2 at this point.

I actually edited that after my initial post, you have to call it in the flatmap (as you'd expect). Turns out, I'm terrible at coding in a gist.

Last thing is that I actually use cyclops pretty extensively and it's held up well for most of my use cases. Only downside I've had is that I have to use the Vavr validation class, since idk what the hell the cyclops validation class is doing.

Fingers crossed that java 17 delivers on the pattern matching. We've got sealed types and record classes, so we're getting close to actually using normal switch expressions.

@ts14ic
Copy link

ts14ic commented Oct 30, 2020

Technically, it implements ofNullable and flatMap, what else do you need for it to be?

It was pointed by someone, that java's Optional is not a monad because some.map(v -> null) returns empty instead of some(null) - java Optional changes the monadic context that is.
It's a technicality and probably not that important in practice (and I find it lovely), but that is all I meant by saying it's not a monad 😄

Fingers crossed that java 17 delivers on the pattern matching

Yes-yes-yes

actually edited that after my initial post, you have to call it in the flatmap (as you'd expect).

It would still log method2 returned null even if method2 was not ever called?

@ayush-finix
Copy link

It would still log method2 returned null even if method2 was not ever called?

I have no idea what version you're looking at so I'm just going to copy it here.
Not with this (changed formatting to make it easier as well)

method1().peekEmpty(() -> log.error("method1 returned null"))
    .flatMap(
        s -> method2(s).peekEmpty(() -> log.error("method2 returned null")
    )

@ts14ic
Copy link

ts14ic commented Oct 30, 2020

Oh, yeah, I didn't refresh the page properly - you added much more details. I'll take a look, sorry for confusion.

But looking at this immediate snippet:

method1().peekEmpty(() -> log.error("method1 returned null"))
    .flatMap(
        s -> method2(s).peekEmpty(() -> log.error("method2 returned null")
    )

Yes, it does accomplish what is needed 🙂 . No such thing in java.util.Optional though, as far as I understand

@ayush-finix
Copy link

ayush-finix commented Oct 30, 2020

I mean, technically, with jdk 11+, you could do

Optional<?> m1 = method1();
m1.ifPresentOrElse(s -> log.info("method 1 success"), () -> log.error("method1 returned null"));
m1.flatMap(
        s -> {
            Optional<?> m2 = method2(s);
            m2.ifPresentOrElse(s -> log.info("method 2 success"), () -> log.error("method2 returned null")
            return m2;
        }
    )

but I can't imagine anyone is okay with this code.

Although one alternative (early returns)

m1 = method1();
if (m1 == null) {
    log.error("method1 returned null");
    return ?;
}
m2 = method2(m1)
if (m2 == null) {
    log.error("method2 returned null");
    return ?;
}
...

seems pretty ugly too (look at me, manually writing out the work flatmap does. I heard Go programmers like doing this.) and the other alternative seems bad as well (full if/else statements).

@SwingGuy1024
Copy link
Author

Thank you for all the comments. I'm delighted that my essay has attracted some attention. But I haven't updated this for a while, because I have another version where I've added more examples, all taken from production code. (I also removed all discussion of the nullness checker, because I wasn't happy with some of it, and I decided to limit the essay to a single topic.) You can find the revised essay at https://github.com/SwingGuy1024/Blog/blob/master/Bad%20Uses%20of%20Optional.md

(Given how that one attracted no attention and this one did, I wonder if I should move that one to a gist!)

@ts14ic
Copy link

ts14ic commented Nov 1, 2020

Hi @SwingGuy1024 , I've read your other version as well before this, and I liked it 🙂 .
I joined the conversation here mainly because it already had a comment and I wanted to jump in 😄 . It's only now that I've noticed that the other one was not a gist, so commenting on it was impossible, unless with issues

@ts14ic
Copy link

ts14ic commented Nov 1, 2020

BTW,
I can add one more "bad usage" from production code that I've seen: "Getting rid of the null keyword".
That is, I've seen places where code is written as in if (Optional.ofNullable(nullableVar).isPresent()) or if (Objects.isNull(nullableVar)).
The people who wrote this, sincerely believe Optional/isNull were introduced to replace the null keyword - which is one of the reasons I have read everything I could find, including your essays, as you call them 😄

@ayush-finix
Copy link

ayush-finix commented Nov 2, 2020

BTW,
I can add one more "bad usage" from production code that I've seen: "Getting rid of the null keyword".
That is, I've seen places where code is written as in if (Optional.ofNullable(nullableVar).isPresent()) or if (Objects.isNull(nullableVar)).
The people who wrote this, sincerely believe Optional/isNull were introduced to replace the null keyword - which is one of the reasons I have read everything I could find, including your essays, as you call them 😄

Maybe I'm biased mostly writing web services, but I find that it's actually entirely possible to keep null only at the edges of the system (api request deserialization, db fetches (for which you can wrap the nullable field getters), and third party libraries) and not use null at all inside the parts of the program you control (most of the rest of the business logic domain). None of the major libraries in my world (spring, jackson, jooq, hibernate) have a problem with this. However, blindly replacing all nullable fields with Optional or every null check with isPresent is, as you pointed out, not going to be the way that happens.

@ts14ic
Copy link

ts14ic commented Nov 2, 2020

but I find that it's actually entirely possible to keep null only at the edges of the system

Yep, I like this approach as well. It's what I called the "touch-me-not". Redesigning a system in such a way, that I don't have to deal with a null, and if I do - yell at it :D .
It's one of the things I like about protocol buffers - even if not a single field is set, all the getters would still return non-null values. There are little quirks, but overall it's nice.
It's also the Guava approach - they have it described a little in "UsingAndAvoidingNullExplained"

@SwingGuy1024
Copy link
Author

SwingGuy1024 commented Nov 2, 2020 via email

@SwingGuy1024
Copy link
Author

SwingGuy1024 commented Nov 2, 2020

one more "bad usage" from production code that I've seen: "Getting rid of the null keyword".
I agree. It's a very silly way to test for null. You'll also find it in lines 2 and 3 of Example 1. Except that one has a bug, because it calls of() instead of ofNullable()!

@SwingGuy1024
Copy link
Author

Cool set of real cases. My only concern is that it seems like most of these cases aren't very sensible in general.

Yeah. None of these are sensible. But I found all of these in production code. The people who wrote this code have no idea what the purpose of Optional is, which is why none of them are sensible.

@CannoNadav
Copy link

I mean, technically, with jdk 11+, you could do

Optional<?> m1 = method1();
m1.ifPresentOrElse(s -> log.info("method 1 success"), () -> log.error("method1 returned null"));
m1.flatMap(
        s -> {
            Optional<?> m2 = method2(s);
            m2.ifPresentOrElse(s -> log.info("method 2 success"), () -> log.error("method2 returned null")
            return m2;
        }
    )

but I can't imagine anyone is okay with this code.

Although one alternative (early returns)

m1 = method1();
if (m1 == null) {
    log.error("method1 returned null");
    return ?;
}
m2 = method2(m1)
if (m2 == null) {
    log.error("method2 returned null");
    return ?;
}
...

seems pretty ugly too (look at me, manually writing out the work flatmap does. I heard Go programmers like doing this. Weirdos.) and the other alternative seems bad as well (full if/else statements).

I haven't read the entire thread so sorry if I'm pushing at an open door but if you need logging(or any other aspect that isn't covered by the regular class) why not create a wrapper and use it instead? you can add a non-mandatory constructor argument to your fancy optional for some additional plugable logic to executed after each step. the map function could look something like -

public <U> MyFancyOptional<U> map(Function<? super T, ? extends U> mapper) {
    Optional<T> opt = this.getDelegate();
    Optional<U> newOpt = opt.map(mapper);

    // InvocationChainMetaData can carry meta data you'd want to propagate along with your calculation, e.g the current invocation number
    // InvocationChainHandler::handle can log on first occurrence of empty or whatever you need to do, and increment the invocation number
    InvocationChainMetaData newInvocationChainMetaData = getInvocationChainHandler().handle(newOpt, getCurrentInvocationChainMetaData())
    return new MyFancyOptional<>(newOpt,metaData);
}

I'd also mention using a dynamic proxy to stay interoperable with a regular Optional, but AFAIK dynamic proxy would only work for an interface and not a class right?

anyway wouldn't this be enough to cover your needs?
l

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment