Skip to content(if available)orjump to list(if available)

A better (than Optional) maybe for Java

_kidlike

`then` is definitely less intuitive than `map` considering that you actually want to, well, map to something else.

Semantically, `then` would be equivalent to `ifPresent`, but, well, then both are intuitive.

funcDropShadow

Nothing is intuitive on its own. Intuitiveness is a property of the relation between a thing and and some subject. Whether `map` is more intuitive than `then` depends on that subject. Without assuming a target audience, it is futile to design a library to be intuitive. Intuitive is that, for which we already have built an intuition.

ljlolel

In UX, there is nothing intuitive, only familiar

Although have you seen that video of a chimpanzee browsing Instagram?

Or a parrot using zoom?

ivan_gammel

Interesting attempt, however:

1. There was no need to implement Set interface to support foreach loop. Iterable would be sufficient. Set is too specific and is not an obvious choice (for return type interoperability in subclasses sometimes it would be more convenient to have List).

2. Moving from isPresent/get to try/catch checked exception will produce worse result. Either you wrap ‘get’ immediately and get more verbose code, or you catch somewhere else and loose the context and readability. In “throws Exception” methods absence of stack trace will be painful.

3. Renaming ‘map’ to ‘then’ is too late. “Then” was used in functional style libraries more than 15 years ago, Java 8 settled the naming convention and it’s better to follow it.

4. What Optional is often missing is the context of empty state. It would be cool to have an implementation of None with a Throwable cause (and orElseThrow throwing it wrapped).

occz

The real improvement is to lift nullability up to the type system. That way, every type acts as an optional by default, and the computer can infer when a nullable type has been null-checked.

This is what Kotlin does - it's truly freeing to work with.

DarkNova6

They are slowly getting there. See the JEP draft: Null-Restricted Value Class Types (Preview)

Source: https://openjdk.org/jeps/8316779

But it is already possible today using the Spring annoation "NonNullApi" on a package level. Then, you can allow nullable fields via @Nullable.

It's not perfectly elegant, but it works surprisingly well.

pjmlp

Usually I have bigger fish to fry in enterprise projects than a couple of NullReferenceExceptions.

What would be truly freeing is getting the right set of folks on the projects.

ajkjk

"the real fix is to move from the stone age to the iron age"

...suffice it to say, there are even better ages

occz

I don't know, I think Kotlin strikes a pretty good balance as far as languages go. What's your preference over it?

ajkjk

oh I think it's fine. I just find it a bit odd to be saying "the thing we should have from our type system is that it's a type system, it's truly freeing to work with". Like... yeah of course we should but it's a bit sad that we're getting exciting over basic functionality like that in 2025.

dehrmann

> The exception that is raised when an empty Maybe is used is a checked exception so you don't forget to handle it.

I think they acknowledged get() should have been getOrThrow() so it's clear what can happen, but I don't know if making it checked is an improvement.

> It implements the Collection interface, so it can be accessed with a for-loop.

I'm curious what the use cases are for this. I've used Optional, it can be annoying, it's mostly around it being verbose and APIs being inconsistent. It was never wishing I could treat it as a collection.

kelseyfrog

I'd imagine it's a poor-man's do notation.

When it comes to structured programming, languages without a do syntax are considered incomplete. It's like programming without ifs.

AtlasBarfed

Fro what language? Is that like groovy with?

eru

Do-notation is a thing in eg Haskell.

hackingonempty

Like Scala's for-comprehension.

porridgeraisin

Yeah I don't get the iterator thing either... Saw the implementation and I'm not sure what the usecase would be.

They have essentially implement a single element iterator for Some and a zero element iterator for None.

ai_

It's useful for combinators, like flatMap. For instance if you want to flatten an iterator of optional values into the values of all some elements.

dehrmann

Is it worth using a custom library so you can do

.flatMap(Maybe::stream)

instead of

.filter(Optional::isPresent).map(Optional::get)

sedro

It's useful and very common to use. For example, you could concatenate together command-line argument flags that may or may not be present.

stickfigure

`map()` makes Optional congruent with java Streams. Optionals are very frequently used with Streams, eg:

    someList.stream().map(Thing::foo).findFirst().map(Other::bar)...
And checked exceptions are a scourge. There's a reason post-Java languages like C#, Scala, and Kotlin abandoned them. Even lombok offers @SneakyThrows.

The Java Optional<?> is pretty good as-is. The main improvement I'm make is that it needs to be Serializable. And woven into the core collection classes.

crummy

Why do you want Optional to implement Serializable?

stickfigure

Literally to make it able to be serialized by Java.

There are many legitimate applications for Java serialization which do not suffer security concerns. You should never expose serialization for public APIs but that leaves private APIs and private storage systems (think message queues, disk caches, internal RPC, etc).

For private serialization, Java serialization has many advantages over something like JSON. You can serialize object graphs without concern for cycles. The contract is simple (it serializes fields, as opposed to dealing with weird bean naming conventions). It handles types and structures automatically. `transient` is built into the language. Polymorphism just works.

We use it heavily for ephemeral storage. It's great.

Timwi

To make it able to be serialized?...

Am I missing something?

crummy

Most of the serialization libraries I use (Jackson, gson) ignore the Serialization interface. I think it's only used for Java's now-rarely-used native serialization functionality.

lucumo

Only Java's built-in serialization still requires Serializable. And Java's built-in serialization is fundamentally vulnerable and should never be used.

Newer libraries dropped the need for Serializable, because whether or not an object is serializable is not just dependent on its class, but also on the features the serialization library supports.

evidencetamper

You don't need your optional to be serialized - it's just a container. You need what's inside your optional to be serialized.

derriz

I really dislike the use of exceptions - particularly checked exceptions - to handle control flow for things like this. I mean, it's not EXCEPTIONAL to have an empty Optional, right? Their use here is an anti-pattern to my eye.

Java (the language) should just drop the notion of checked exceptions. It would be fully backward compatible as the checked/unchecked distinction is a language feature not a JVM one.

They simply don't work with Stream and cause abstraction leaks everywhere (e.g. you have to handle IOExpection for operations on StringWriter). It obfuscates flow control.

Anyway the problem with Optional is not going to be fixed by renaming API methods and fiddling like this (sorry to be harsh). The reason Optional is broken is because there is so much legacy APIs which use null to represent "no result" and until all of these APIs are changed/deprecated, then Optional will never be a very useful tool.

As others have commented, the correct solution would be to ditch Optional completely along with the mass of flavors of @Nullable, @NotNull, etc that everyone uses along with having to use package_info.java everywhere and just make "nullability" part of the type system.

Kotlin got it right here. But it would be a breaking change in Java to make not-nullable the default. They could do it the opposite way to Kotlin - add a marker for "not-nullable" instead of one for "nullable" - e.g. "String!" would be a not-nullable String instead of say "String?" representing a nullable string.

dfe

I agree with you on your first statement. Exceptions for control flow is an awful API.

I also wholeheartedly agree with your comments that nullability should be a first-class language feature, part of the type system. I think what you describe is one of the active JEPs.

* Plain old String is "I don't know if it's nullable or not, so I'll let you dereference it without checking, but I may issue a warning.

* String! is "You're telling me it cannot be null so I will enforce that it is not null on the way in (e.g. when converting from String) and will issue no warning on the way out because it cannot be null.

* Finally, String? is "You're explicitly telling me it can be null, so I will force you to do a null check before converting to a String!"

But that's only part of it. I think Java desperately needs a ?. operator (Optional.map but without the Optional). I also think it needs a ?? operator (Optional.orElse but without the Optional).

So far I think Optional is more of a detriment to Java than a benefit. It feels like it was hacked in because without it some of the Stream APIs would be nightmarish given Java's current type system, and getting streams into the standard library API was too important a feature to wait for fixing the long-standing issues with nullability.

The only thing I disagree with you on is I do think there is some merit to checked exceptions when used sparingly. Capturing the idea that a method can fail for a reason that has to do with the program's input or environment (checked exception) vs. a reason that has to do with the program's code and cannot be solved without changing the program's code (RuntimeException) is a pretty neat thing.

I think the Streams API could have easily implemented terminal operations like forEach(ThrowingConsumer<E,X>). This could still be added.

I also think that the <X> ... throws X pattern should accommodate a union type like IOException|ParseException and that this should be inferred so that in this example of a throwing Consumer, whatever checked exceptions the method throws simply become the checked exceptions of the forEach method.

Conceptually, these are straightforward ideas. I suspect the limiting factor is figuring out how in the world to actually implement what I just described.

DarkNova6

This is an old suggestion. You don't have the slightest idea what kind of rabbit holes this will lead into. This has been discussed time and time again.

The much better approach is to keep it as-is but make Exceptions composable:

switch( this.networkOperation() )

case throws IOException ex -> "invalid"

case String expected -> return expected;

case Object obj -> throws new IllegalArgumentException(obj);

You get the best of both worlds. The caller is forced to handle expected errors at call-site (aka, where you have the most information). But you can still delegate the error to a higher level, such as Controllers where the error gets turned into a "500 Server Error".

The problem really only is that you are currently forced into creating a new try-catch block. If that goes away, you are essentially dealing with sum-types. This will be more elegant than heralded languages such as Go where you slavishly write "err != nil" statements at every turn.

derriz

"You don't have the slightest idea what kind of rabbit holes this will lead into."

Instead of making assumptions about what I know or don't know about programming language theory and type systems, could you list these rabbit holes?

Java inspired languages like Kotlin, Scala and C# all considered checked exceptions and all decided to drop them and I've never heard users of any of these language express any wish to re-introduce them. I've used all three professionally and I have no idea what "rabbit holes" you're talking about?

In fact there isn't a single widely used language besides Java that feature checked exceptions. Arguably Eiffel had them but that's seriously stretching the definition of "widely used".

m11a

> In fact there isn't a single widely used language besides Java that feature checked exceptions.

Not in Java's form, but I'd argue Rust's Result type basically achieves the same effect but with less syntax. A function call can return an error variant, and the caller must handle the case (even if that's just propagating it up with `?`), as opposed to letting it silently flow up the call stack (which is more like a Rust panic).

And I'd argue that Rust's error handling is excellent. The programmer is always forced to acknowledge function calls which may produce an error, yet is also able to say "I don't care" without much syntactic overhead.

stickfigure

This doesn't seem any different from try/catch? The syntax differences are minimal.

I don't mind try/catch, but I agree that checked exceptions should be dropped. Exceptions are for exceptional situations, which are myriad and rarely need to be handled except at some upper level (return 500, show error dialog, etc).

ivan_gammel

Oh, that actually does look different. Switch expression with exceptions would allow compact use of checked exceptions in method chaining.

List<URI> pages… var content = pages.stream().map(uri -> switch(fetch(uri)) { case Page model -> return model; case throws IOException -> return Page.notFound(uri); }).toList();

ivan_gammel

> the correct solution would be to ditch Optional completely along with the mass of flavors of @Nullable, @NotNull

How would you suggest to implement Stream::findAny without Optional?

derriz

Using my suggestion, the signature would simply be "T findAny();" - while "T!" would represent non-nullable T. So the opposite of the Kotlin/Typescript/etc. way where the default is not-nullable and you'd have something like "T? findAny();".

It would be a language level change and would require language level syntax extensions to provide mappings between T and T!.

Like how we're used to having to add "final" to most variable declarations, you'd add "!" to a class type indicate not-null. Contra-variance would allow you to add ! to return values without breaking interface compatibility so you could migrate your code base in steps. Actually to be honest I'm just throwing this idea out there, I haven't given it deep thought.

Without language level support, Optional is a broken design. You've just multiplied the state space: the Optional instance can be null, the Optional instance can be not-null but empty, the Optional instance can be not-null and non-empty but the content is null and finally the Optional instance can be not-null and non-empty and the contained value can be non-null. Java's type system doesn't distinguish.

ivan_gammel

>Actually to be honest I'm just throwing this idea out there, I haven't given it deep thought.

I think the problem is much more complex if done properly, Java way, with full consideration. E.g. Optional has fluent interface which often becomes useful to avoid lots of nested if’s. To emulate chaining with nullable values you do need ‘?’ syntax.

>the Optional instance can be null

When return type is Optional, it is an error in 100% cases to return null, so it’s a valid case for NPE. A vary rare problem.

> the Optional instance can be not-null and non-empty but the content is null

Optional of null content is empty by design. Did you mean something else?

norswap

While I agree exceptions should not be used for control flow except in the rarest of circumstances (every rule has exceptions — pun fully intended), I think checked exceptions are very useful.

I wrote a whole article about this, way back when: https://norswap.com/checked-exceptions/

But for the tl;dr: checked exceptions represent somewhat expected issues, things like IO errors. There are two okay ways to handle those: checked exceptions and result types, and one bad way: unchecked exceptions.

Unchecked exceptions end up being undocumented in the API, and people forget to handle them.

Result types add a lot of ceremony, as you have to unwrap them at every level of the call stack. I've seen code dominated by error checking logic many times in C and Go (and the frequent alternative is to ignore error, which is easy in C).

Checked exceptions are much less verbose, you just let the exception pass through and annotate the function signature with the checked exception. It's type-safe, self-documenting and easy.

Unchecked exceptions "leak", checked exceptions propagate at the type-level, which is exactly what you want — making sure every caller is aware of what exceptions might occur and letting them make an explicit decision to handle or pass the buck upwards.

By the way, checked exceptions are coming back in favour with "effects" which are basically a generalization of checked exceptions beyond the realm of errors, trying to encode things into the type system.

derriz

These arguments have been going back and forth forever. I don't think either of us would be able to change each other's position on this based on arguing either way.

I'll take a different tack - look at the empirical evidence. If checked exceptions were a good/useful/helpful language feature, then the feature would have been adopted by other languages, especially new ones. But this simply hasn't happened. Java is unique in having checked exceptions.

Wouldn't you think that there would have been proposals to introduce checked exceptions for C++, Python, JavaScript, C#, and Rust, Kotlin, etc. if they were a useful software engineering tool?

dfe

I used to think checked exceptions were stupid, so I'd catch and wrap them in a RuntimeException at the first opportunity just to get rid of them.

These days I've come around to your viewpoint that checked exceptions are great because all you have to do is declare them until you get to a point in the call hierarchy where you either can handle them or it's impossible to handle them so you wrap them.

In some cases, like with reflectively-invoked JUnit test methods, they only need to be declared because JUnit has its own exception handler. But you wouldn't believe the crazy amount of test code I see that eats exceptions instead of declaring them.

I think the confusion comes because with very simple programs you only ever see the point where you have to handle the checked exception, so a habit of writing catch blocks forms. It's only when designing libraries that you would declare checked exceptions as part of your method signatures and just allow them to naturally propagate. Therefore new Java programmers see all these extra catches and think it's just a burden they have to meet.

Another thing about Java that's bad for casual coding is that it exits 0 (success) when all non-daemon threads have ended, even if those threads ended because of an exception. It would be really nice if there was some internal flag set by the default uncaught exception handler that would cause a non-zero exit later when all threads are finished (which might be right then). Maybe I should submit a JEP.

blastonico

> The method names are more intuitive: e.g., then instead of map

But map makes total sense, considering that maybe is a monad.

edflsafoiewq

Rather than monads, map is more likely to be familiar from sequences, and an optional is just a sequence with length < 2.

eru

Well, monads get their map from functors, and they call it 'map' (or fmap) because of map on sequences.

Jaxan

I would expect different types of map and then:

    map :: (a -> b) -> Maybe a -> Maybe b
    then :: (a -> Maybe b) -> Maybe a -> Maybe b
Although for Maybe these are not too different.

coin

“then” is definitely less intuitive

puchatek

Intuition is in the eye of the beholder

eru

Well, it only needs to be a functor (in the Haskell sense) for map to make sense.

(Of course, all monads are also functors.)

voidhorse

True, but I would not be surprised if the vast majority of Java programmers have no clue what a monad is.

chii

> no clue what a monad is.

which is a good opportunity to introduce said nomenclature, and ideas to them, rather than renaming said method to something more "palatable".

Dansvidania

In my experience just name-dropping monads in non functional-programming oriented circles will get weird looks.

EDIT: I would even argue, as a fan of FP, that it's not immediately obvious how this terminology or concept even helps, from the point of view of a non purely functional programming language

xigoi

You don’t need to mention monads, just imagine an Optional as a list with at most one element.

null

[deleted]

stemlaur

I would make the abstract class "Maybe" a sealed class with None and Some being the only permitted implementation So that clients can use pattern matching (enhanced switch)

Hackbraten

Very good point.

dfe

This seems to be worse in every way to Optional.

The throwing of a checked exception is ludicrous. It's a classic case of abusing checked exceptions for flow control. The right approach is an isPresent() or isEmpty() call followed by a get() when you know it's present. Static analysis can ensure you don't screw this up.

Similarly, using then and thenMaybe instead of map and flatMap means it now has its own terminology different from Stream.

Being able to iterate it is dubious. You can already .stream() an Optional (to an empty or 1-element stream). You can also if (o.isPresent()) { o.get(); } which is hardly burdensome. I'm trying to figure out how a for-each loop is an improvement. But if you must: o.map(Collections::singleton).orElse(Collections.empty()). Or o.stream().toList().

I applaud the author for experimenting with different API designs, but this one is just not good. It causes more trouble than it solves.

Blackarea

Checked exception in a Optional.get is really the worst idea that you could possibly come up with.

Have a look at vavr (https://github.com/vavr-io/vavr) They did a fantastic job and the DX is so much better than anything java std has to offer. They have immutable equivalents for pretty much any java util data structure as well as either try option etc. To me the one lib that makes java actually feel fresh.

gylterud

Vavr is nice! But I understand the want to make the get exception checked. It makes it so that you can use try/catch as a Java-man’s do-notation, and not fear having forgotten to deal with the None case.

You can then make lots of utility functions which all get without handling it (only declare that they throw) and then you compose these into larger functions which make sure to catch at the end.

Having to declare it makes it really clear what the function actually does.

Blackarea

Can you provide an example. I just don't see it tbh. Especially since closures in java really don't work with checked exceptions at all.

That's why vavr avoids it at all costs. Eg: ```java assertEquals( Vector.of(1, 3), Vector.of(Some(1), None(), Some(3)) .filter(Option::isDefined) .map(Option::get) ); ```

If `Option.get` was throwing a checked you'd always have to wrap it in try-catch. I understand that this is a break with the type system (meaning it is not strong typed Maybe/Option in that sense). But otherwise you'd be worse of than with stupid null checks IMO.

DarkNova6

I really hate the inclusion of a "FastException" and "UnsafeUtils" class.

Many developers will consistently make the wrong choice thinking their complex (but cold) execution path needs to be extra sleek and fast. But the reckoning comes during debugging (most likely by somebody else) where you have extra levels of indirection and non-expected violations of typically default behavior.

rubenvanwyk

Or... Just use Kotlin?

gf000

Which has worse no real pattern matching like Java has?

rhdunn

I'd like Java to add support for the null coalescing operator `?.` and associated `!.` and `?:`. -- I find this easier to use and read than using Optional or null check code.

I find heavy use of Optionals for nullable values can quickly overwhelm the codebase and make it hard to read the control flow. Especially when interacting with other parts of the codebase.

E.g.

    var baz = foo?.bar?.baz ?: return false;
vs something like:

    var baz = foo.map(f => f.bar).map(b => b.baz).getOrElse(null);
or using `then`.