DEV Community

Cover image for Everything Bad in Java is Good for You

Everything Bad in Java is Good for You

Shai Almog on June 13, 2023

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 w...
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.