5 ms·
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
by derriz 2y ago
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.
- ivan_gammel 2y ago> 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 2y agoUsing 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 2y ago>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?
- derriz 2y agoNo doubt the problem is more complex and like I said would need language level syntax support. Sorry yes you're correct, optional containing null is the same as an empty optional. So there are 3 cases, not 4. But saying that "it's a valid case for NPE" missed the point I believe as you're taking about dealing with the issue at runtime. The killer feature of having language level support for distinguishing null and not-null is that these errors are flagged at compile time, not runtime.
- ivan_gammel 2y ago>But saying that "it's a valid case for NPE" missed the point I believe as you're taking about dealing with the issue at runtime. What I mean is that this issue (null instead of Optional) is rare enough not to bother about it and let NPE happen. Language support for null/non-null values in general would be nice, of course.
- norswap 2y agoWhile 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/ 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 2y agoThese 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 2y agoI 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.
- DarkNova6 2y agoThis 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.
- stickfigure 2y agoThis 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 2y agoOh, 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();
- derriz 2y ago"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".
- dfe 2y agoI 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.