DEV Community

Cover image for Everything Bad in Java is Good for You
Shai Almog
Shai Almog

Posted on • Originally published at debugagent.com

Everything Bad in Java is Good for You

Everything Bad is Good for You is a pop culture book that points out that some things we assume are bad (like TV) have tremendous benefits to our well-being. I love the premise of disrupting the conventional narrative and was reminded of that constantly when debating some of the more controversial features and problems in Java. It’s a feature, not a bug…

One of my favorite things about Java is its tendency to move slowly and deliberately. It doesn’t give us what we want right away. The Java team understands the requirements and looks at the other implementations, then learns from them. 

I’d say Java’s driving philosophy is that the early bird is swallowed by a snake.

Checked Exceptions

One of the most universally hated features in Java is checked exceptions. They are the only innovative feature Java introduced as far as I recall. Most of the other concepts in Java existed in other languages, checked exceptions are a brand new idea that other languages rejected. They aren’t a “fun” feature, I get why people don’t like them. But they are an amazing tool.

The biggest problem with checked exceptions is the fact that they don’t fit nicely into functional syntax. This is true for nullability as well (which I will discuss shortly). That’s a fair complaint. Functional programming support was tacked onto Java and in terms of exception handling it was poorly done. The Java compiler could have detected checked exceptions and required an error callback. This was a mistake made when these capabilities were introduced in Java 8. E.g. if these APIs were better introduced into Java we could have written code like this:

api.call1()
    .call2(() -> codeThatThrowsACheckedException())
    .errorHandler(ex -> handleError(ex))
    .finalCall();
Enter fullscreen mode Exit fullscreen mode

The compiler could force us to write the errorHandler callback if it was missing which would satisfy the spirit of the checked exceptions perfectly. This is possible because checked exceptions are a feature of the compiler, not the JVM. A compiler could detect a checked exception in the lambda and require a specially annotated exception handling callback. 

Why wasn’t something like this added? 

This is probably because of the general dislike of checked exceptions. No one attempted to come up with an alternative. No one likes them because no one likes the annoying feature that forces you to tidy up after yourself. We just want to code, checked exceptions force us to be responsible even when we just want to write a simple hello world…

This is, to a great extent, a mistake… We can declare that main throws an exception and create a simple hello world without handling checked exceptions. In large application frameworks like Spring, checked SQLException is wrapped with a RuntimeException version of the same class. You might think I’m against that but I’m not. It’s a perfect example of how we can use checked exceptions to clean up after the fact. Cleanup is performed internally by Spring, at this point the exception-handling logic is no longer crucial and can be converted to a runtime exception.

I think a lot of the hate towards the API comes from bad versions of this exception such as MalformedURLException or encoding exceptions. These exceptions are often thrown for constant input that should never fail. That’s just redundant and a bad use of language capabilities. Checked exceptions should only be thrown when there’s cleanup we can do. That’s an API problem, not a problem with the language feature.

Null

Pouring hate on null has been trending for the past 15+ years. Yes, I know that quote. I think people misuse it.

Null is a fact of life today, whether you like it or not. It’s inherent in everything: databases, protocols, formats, etc. Null is a deep part of programming and will not go away in the foreseeable future.

The debate over null is pointless. The debate that matters is whether the cure is better than the disease and I’m yet unconvinced. What matters isn’t if null was a mistake, what matters is what we do now.

To be fair, this directly correlates to your love of functional programming paradigms. Null doesn’t play nicely in FP which is why it became a punching bag for the FP guys. But are we stepping back or stepping forward?

Let’s break this down into three separate debates:

  • Performance

  • Failures

  • Ease of programming

Performance

Null is fast. Super fast. Literally free. The CPU performs a null check for us and handles exceptions as interrupts. We don’t need to write code to handle null. The alternatives can be very low overhead and can sometimes translate to null for CPU performance benefits. But this is harder to tune. 

Abstractions leak and null is the way our hardware works. For most intents and purposes, it is better.

There is a caveat. We need the ability to mark some objects as non-null for better memory layout (as Valhalla plans to do). This will allow for better memory layout and can help speed up code. Notice that we can accomplish this while maintaining object semantics, a marker would be enough.

I would argue that null takes this round.

Failures

People hate NullPointerException. This baffles me. 

NullPointerException is one of the best errors to get. It’s the fail-fast principle. The error is usually simple to understand and even when it isn’t; it isn’t far off. It’s an easy bug to fix. The alternative might include initializing an empty object which we need to verify or setting a dummy object to represent null. 

Open a database that has been around long enough and search for “undefined”. I bet it has quite a few entries… That’s the problem with non-null values. You might not get a failure immediately. You will get something far worse. A stealth bug that crawls through the system and pollutes your data.

Since null is so simple and easy to detect there’s a vast number of tools that can deal with it both in runtime and during development. When people mention getting a null pointer exception in production I usually ask: what would have been the alternative?

If you could have initialized the value to begin with then why didn’t you do it? 

Java has the final keyword, you can use that to keep non-null stateful values. Mutable values are the main reason for uninitialized or null values. It’s very possible that a non-null language wouldn’t fail. But would its result be worse? 

In my experience, corrupt data in storage is far worse. The problem is insidious and hides under the surface. There’s no clue as to the origin of the problem and we need to set “traps” to track it down. Give me a fail-fast any day.

In my opinion, null has this one hands down…

Ease of Programming

An important point to understand is that null is a requirement of modern computing. Our entire ecosystem is built on top of null. Languages like Kotlin demonstrate this perfectly, they have null and non-null objects.

This means we have duplication. Every concept related to objects is expressed twice, and we need to maintain semantics between null and non-null. This raises the bar of complexity for developers new to such languages and makes for some odd syntax.

This in itself would be fine if the complexity paid off. Unfortunately, such features only resolve the most trivial non-issue cases of null. The complex objects aren’t supported since they contain null retrieved from external sources. We’re increasing language complexity for limited benefit.

Boilerplate

This used to be a bigger issue in the past but looking at a typical Java file vs. TypeScript or JavaScript the difference isn’t as big. Still, people nitpick. A smart engineer I know online called the use of semicolons in languages: "Laziness". 

I don’t get that. I love the semicolon requirement and am always baffled by people who have a problem with that. As an author it lets me format my code while ignoring line length. I can line break wherever I want, the semicolon is the part that matters. If anything, I would have loved to cancel the ability to write conditional statements without the curly braces e.g.:

if(..) x();
else y();
Enter fullscreen mode Exit fullscreen mode

That’s terrible. I block these in my style requirements; they are a recipe for disaster with an unclear beginning or end. 

Java forces organization, this is a remarkable thing. Classes must be in a specific file and packages map to directories. This might not matter when your project is tiny, but as you handle a huge code base, this becomes a godsend. You would instantly know where to look for clues. That is a powerful tool. Yet, it leads to some verbosity and some deep directory structures. But Java was designed by people who build 1M LoC projects, it scales nicely thanks to the boilerplate. We can’t say the same for some other languages.

Moving Fast

Many things aren’t great in Java, especially when building more adventurous startup projects. That’s why I’m so excited about Manifold. I think it’s a way to patch Java with all the “cool stuff” we want while keeping the performance, compatibility and stability we love.

This can let the community move forward faster and experiment, while Java as a platform can take the slow and steady route. 

Final Word

Conventional wisdom is problematic. Especially when it is so one-sided and presents a single-dimension argument in which a particular language feature is inferior. There are tradeoffs to be made and my bias probably shines through my words. 

However, the cookie cutter counterpoints don’t cut it. The facts don’t present a clear picture to their benefit. There’s always a tradeoff and Java has walked a unique tightrope. Even a slight move in the wrong direction can produce a fast tumbling-down effect. Yet it maintains its traction despite the efforts of multiple different groups to display it as antiquated. This led to a ridiculous perception among developers of Python and JavaScript as “newer” languages.

I think the solution for that is two-fold. We need to educate about the benefits of Java's approach to these solutions. We also need solutions like Manifold to explore potential directions freely. Without the encumberment of the JCP. Having a working proof of concept will make integrating new ideas into Java much easier.

Top comments (25)

Collapse
 
zirkelc profile image
Chris Cook

I work with Java from time to time when I create extensions for a Neo4j database. It is much more fun now, than it was ten years ago. I stumbled upon the same issues with exceptions and null values in the context of FP.

What I'd like to see in Java are simple JSON-like object and array types like var obj = { name: "Joe" }. I think in Java they exist as Record types, but still need to be defined statically in a separate file. I think FP-style functions filter, map, reduce in combination with these object/array literals would enable much nicer programming in Java.

Collapse
 
nlxdodge profile image
NLxDoDge • Edited

From Java 14 you can indeed use records (Source

 public record Person (String name) {}
Enter fullscreen mode Exit fullscreen mode

And then us it easily:

Person testPerson  = new Person("Joe");
Enter fullscreen mode Exit fullscreen mode

Hooking in your Functional Programming (I think you meant), you can already do things like:

import java.util.stream.Stream;

public class Main {
  record Person(String name) { }

  public static void main(String[] args) {
    System.out.println(
        Stream.of(new Person("Joe"), new Person("Frank"), new Person("Elise"))
        .map(Person::name)
        .filter(name -> !name.startsWith("J"))
        .toList());
  }
}
Enter fullscreen mode Exit fullscreen mode

It has some ways to go, but everything is extensible anyways.

Collapse
 
zirkelc profile image
Chris Cook

That looks actually better than I expected. Thank you for this example!

It would be even nicer if this were possible:

public class Main {

  public static void main(String[] args) {
    System.out.println(
        Stream.of({ name: "Joe" }, { name: "Frank" }, { }) 
        .map(Object::name)
        .filter(name -> !name.startsWith("J"))
        .toList());
  }
}
Enter fullscreen mode Exit fullscreen mode

Just declaring { name: "Joe" } or new { name: "Joe" } (like C#) should be enough to define a record.

Collapse
 
codenameone profile image
Shai Almog

These are called Tuples and Manifold supports them!
See my recent series covering it.

But yes @nlxdodge is right that records are pretty great.

Collapse
 
zirkelc profile image
Chris Cook

Cool, I didn't know that Java supports extending the language with features via compiler plugins.

Collapse
 
giulio profile image
Giulio "Joshi"

Your wonderful article surely needs a proper response, damn me and my tight schedule.

Talking from experience here, I'd say this is lovely to have a bit more narrative on the ...most odd()* parts of the Java language and ecosystem, and what led them in that direction.

It come to me a little enlightening why these trade-off came short in Java world as soon as the .
Picking from your Failure section I can picture two different context, just by comparison.

In the first one you got a very self reliant application, able to determine in full all the data structures, where the developers are in total control of how these changes and they own it in full. Failing fast in this context is gold, and NullPointerException is a way of handling that, possibly catching up all the errors during development.

In the second context you're interfacing with very unstable remote API and highly mutable or versioned data you want to treat but not really own.

Developers main concern here is having a resilient application, able to keep functioning and resisting minor structural changes and flexible data, with uncontrollable null values.

This means that Optional<> becomes king, and a NullPointerException doesn't really help because they'll pop out randomly at runtime, even during perfectly valid requests.

This is just to say that Java had a way to become tolerant out of its own "best use case scenario", even when the context requires a lot of stretch.

  • Odd is a personal opinion, of course.
Collapse
 
livioribeiro profile image
Livio Ribeiro • Edited

Your take on checked exceptions contradicts the one on nulls, if checked exceptions are good, why checked nulls are bad?

Also, you are wrong about kotlin, there is no duplication between the nullable and non nullable references, the difference is that you are forced to check for nullability and, after that point, the compiler understands that the value cannot be null.

Developers can make decisions that are not known to be bad until it's too late, and java is not free of that

Collapse
 
codenameone profile image
Shai Almog

Great point.

But no. Imagine the NPE was a checked exception... The checked exceptions should be used for very specific things where we MUST do a cleanup or have well defined failures. E.g. database connection, IO, etc. These will fail and we need to write code for failure. A null is something we can address, once we do that it will never fail.

When I said duplication in Kotlin I meant in terms of syntax and mental capacity. We need to make a decision early on when writing Kotlin code about the nullability status and if it changes then we either need to start adding null checks in the "wrong place" or start going back and change a whole chain of calls.

It's 100% true that Java is not free of bad decisions. The part that peeves me is that pretty much every discussion of nullability presents it as a 100% bad feature and repeats the same broken arguments. This is a tradeoff with advantages that go both ways.

Collapse
 
livioribeiro profile image
Livio Ribeiro

Look at what Rust did to nullability, there is no null is Rust (at least in safe Rust), you have to use the Option type and deal with the presence or absence of the value. Under the hood the compiler optimizes everything and a None uses the same memory as a null in C.

And in Kotlin there is not so much in terms of mental capacity of the syntax, if you want a reference to be nullable, you mark it as nullable and the compiler forces you to check it before using it:

// this will compile
fun length(value: String?): Int {
    if (value == null) {
        return 0
    }
    // "value" is treated as non-null from now on
    return value.length
}

// but this will not
fun length(value: String?): Int {
    return value.length
}
Enter fullscreen mode Exit fullscreen mode

And let's not forget some features like ?. and ?: to make the code less verbose.

Kotlin will also validate return types, you cannot return null nor a nullable if the return type is not marked as such, this will also prevent NPEs so you do not fail at runtime (possibly in production).

I had to deal with projects that failed with NPEs in production, and that was no fun at all. It was only in Java 14 that the NPE message pointed out what as null. Nullability checks at compile time would have helped prevented this. Failing at compile time is failing fast enough?

I do not hate Java, nowadays I kind of prefer Java over Koltin since Kotlin tries to be too much clever and this can make your code unreadable if you are not careful, but we need to be fair with its strenghts.

Thread Thread
 
codenameone profile image
Shai Almog

Rust is great but it's super complex to get right. It's also sometimes hard to map C based APIs to Rust in part because of its very different approach to everything. The alternative to Rusts approach is manual allocations or reference counting. Both suck.

What I'm saying about Kotlin is that it forces a location where nullability is deliberated even if it doesn't matter. Most developers implicitly choose non-null and end up creating more elaborate code since an API now requires a value even if it will never be used. There's a vilification of null that isn't justified.

I had to deal with projects that failed with NPEs in production, and that was no fun at all.

Sure. I got a few of these but not as much is recent years since we learned to do CI properly and integrated tools like Sonar Qube. IntelliJ/IDEA also highlights most potential failures well before a commit. The null pointers I saw in production over the past decade or so, wouldn't have shown in compile time.

Collapse
 
aminmansuri profile image
hidden_dude

I find your article refreshing and agree with it.

One problem with a lot of languages today is the idea that they all have to have all sorts of features to "keep up". When in fact, they each support different programming styles.

The only thing that annoys me about Oracle Java is that they come out with a new version like every week and you can't easily tell if version 17 is one I need to pay attention to or should I wait till version 20.

In the Sun days you could tell because they had major and minor versions. So you knew that 1.3 was a big deal compared to 1.2.2.

Collapse
 
codenameone profile image
Shai Almog

Yes this is hard to keep track of. I suggest keeping up only with LTS releases which makes this more manageable. Specifically 11, 17 and the upcoming 21.

Collapse
 
digeomel profile image
digeomel

You're trying to address the boilerplate issues in Java and the only thing you can come up with is the semicolon?! Really?!?! If you want to address the boilerplate and other bad things with Java, just take a Kotlin vs Java comparison page and see what Java could have been.

Collapse
 
codenameone profile image
Shai Almog

Do that. Please. Most of these aren't applicable for current Java assuming you ignore standalone functions. Especially if you use Lombok or Manifold.

Collapse
 
digeomel profile image
digeomel

If by "current" you mean version > 18, then I'm afraid you are out of touch with reality and I will refer you to this meme:
reddit.com/r/ProgrammerHumor/comme...
And this video:
youtube.com/watch?v=Ibjm2KHfymo
Java is used mostly in corporate environments where the Oracle lobbyists are doing a great job pushing outdated products like Weblogic, running on Java 8.

Thread Thread
 
codenameone profile image
Shai Almog

What's your complaint? That they can't go back in time and update Java 8?

That is an unfair standard that you don't apply to any other language, platform or runtime. Python 2.7 shipped for years with Mac OS. Installing or updating python on a Mac was a nightmare of conflicts and failures.

The reason this is more common with Java is due to its success in the conservative enterprise environment where none of its competitors have a foothold. Most of these environments don't even use containers yet which is part of the problem. With migration to containers using the latest and greatest becomes much easier.

Also both Manifold and Lombok work just fine with Java 8 which is already the minimal version for most installations.

Thread Thread
 
digeomel profile image
digeomel

My complaint is that you wrote an article to make a point, that all bad things in Java are actually good for developers, yet you're not presenting the bad things that the vast majority of developers have to deal with in practice. You didn't specify which version of Java you're referring to, and judging from the article, I assume it covers all bad things from at least version 8 and onwards. Now, if "current" Java (21?) has no bad things, that's great, you can write an article and try to convince people to use Java 21 over e.g. Kotlin or Scala or any other JVM language. But if you really want to address the bad things that Java is taking the heat for, and you mention boilerplate/verbosity, you can't just write about the semicolon, as if there's nothing else verbose about the language. You wrote:
"This used to be a bigger issue in the past but looking at a typical Java file vs. TypeScript or JavaScript the difference isn’t as big."
What's a "typical" Java file for you? A typical Java file for me doesn't come even close to TypeScript/JavaScript in terms of verbosity and boilerplate. In fact whenever I see Java developers write JavaScript/TypeScript code, they repeat the same mistakes that they learned writing Java, because they cannot think in any other way. The code I get to see every day working in a corporate environment is just terrible. At this point, if we were to migrate to Java 21 to enjoy the good things that you're talking about, we might as well ditch all Oracle products and switch to a more modern (and free) stack altogether. But this is not going to happen, so we're stuck with the bad things. And this will be my last comment here.

Thread Thread
 
codenameone profile image
Shai Almog

Most of the things I discuss still apply. Even with Java 8 the only difference is verbosity and even that isn't as bad especially with Manifold or Lombok.

Your complaint is against enterprise deployment policies, not against Java.

A typical Java file for me doesn't come even close to TypeScript/JavaScript in terms of verbosity and boilerplate.

Feel free to provide an example. There are some places where Java is at an inherent disadvantage due to its static nature. But tools like Manifold make things like JSON parsing as easy in Java as they are in JavaScript.

Collapse
 
ttww profile image
Thomas Welsch

Another bad thing in Java: Missing "unsigned" int/short/long/byte types...

Collapse
 
codenameone profile image
Shai Almog

As a guy that does a lot of low level system programming I'm a bit conflicted about that. But no, I don't think it was a mistake. Unsigned is one of those things that makes sense in some edge cases. Since we don't have structs and unions that let us map things directly in memory, the layout of the memory doesn't matter. So we can't do some of the tricks we do in C when working with unsigned values.

Without all of these then unsigned becomes just another bit which seems like a bit much when we have a long and BigInteger.

Collapse
 
ttww profile image
Thomas Welsch

The code you have to write without those types is much uglier and harder to read. I'm also doing system level stuff and image operations. It does simple makes no sense to handle a few million pixels with BigInteger objects. So you have to expand to long via extra instructions and/or doing some brain knots to get what you want.... From my point of view it was a big mistake. I'm doing some instrument controlling actually in C#, that's much easier, even if I doing Java since version 1 :-(.
Don't hit me, but I also like the out/ref parameter sometimes and the $"{value}" string construction (the new java construct for that is also much uglier).
I don't like, if the language force you to use stupid tricks to reach your goal.

Thread Thread
 
codenameone profile image
Shai Almog

Yes. I usually go into C to do that sort of stuff but I do have plenty of code that does the byte cast or & 0xff nonsense. I think it's Java focusing at what Java is good at. It's also probably part of the reason why C# is doing so well in the gaming industry compared to Java. Doing low level graphics in Java always included some hassle.

If you like the ${value} syntax check out my recent series on manifold, you can use that syntax in Java with it.

Thread Thread
 
ttww profile image
Thomas Welsch

Cool :-) Many thanks for that hint.

Collapse
 
feoktant profile image
feoktant • Edited

The biggest problem with checked exceptions is the fact that they don’t fit nicely into functional syntax.

This point is not true. Spring started to use unchecked exceptions before FP in Java. The biggest problem with CE was just rethrowing it, and your method signature becomes a disaster. You just cannot understand how to deal with error, and so just add more thrown exceptions.

No one attempted to come up with an alternative.

Wrong. Spring said use unchecked exceptions, C# community have looong conversation should they or not to have it, with conclusion - no. It was 2003, before FP hype.

Functional languages has alternative - Either (Scala, Haskell) / Result (Rust).


And one last but not least - checked exceptions are slow.

Collapse
 
codenameone profile image
Shai Almog

I said it was the biggest problem not the only reason people don't like it or the only reason languages chose to avoid it.

Spring removes the checked aspect but does that after dealing with the checked exception. Rethrowing is a feature, not a bug. It means the method explicitly declares behavior enforced by the compiler.

And one last but not least - checked exceptions are slow.

Nope. Checked exceptions aren't slow. Since the JVM has no knowledge of their existance they have the exact same overhead as regular exceptions.