https://blog.habets.se/2022/08/Java-a-fractal-of-bad-experim...
Java has been an extremely successful technical failure. With Oracle, it's also gotten a tonne of bureaucratic/legal/CYA cost.
https://blog.habets.se/2022/08/Java-a-fractal-of-bad-experim...
Java has been an extremely successful technical failure. With Oracle, it's also gotten a tonne of bureaucratic/legal/CYA cost.
* Java has had escape analysis for decades. What it doesn't have is value objects. But there are plans for that in the works.
* The rant against bytecode is... I mean, there's a reason why you've had projects like LLVM whose (original) goal was to bring something like JVM bytecode to native code. And it's not for portability reasons, it's for the ability to do things like install-time optimizations. Also note that, for example, iOS distribution also relies on the ability to deliver bytecode instead of native code for those kinds of late optimization opportunities.
* The C++ standard library absolutely uses exceptions for all sorts of silly errors. std::vector::at, for example, throws an exception on a range error. The reason why this isn't obvious is that the more common methods just go for undefined behavior on errors instead of using exceptions, which is hardly a design to be emulated.
* I'm confused as to why the author hates try/finally so much, given that it's morally equivalent to RAII and defer. And there's no mention of the try-with-resources, which brings an interface much like RAII to Java.
> Java has had escape analysis for decades. What it doesn't have is value objects. But there are plans for that in the works.
I'm not sure what you mean this contradicts. Let's say it lands. Which of my points does it invalidate? (also, you know, Java has had ~30 years to land this)
Edit: ah, I see what you mean. Basically you're saying that because of escape analysis, Java can avoid stuffing literally all objects onto the heap for the GC to collect. Sure, that's one of the millions of heroics that Java has added to manage the mistake. The GC has also added more genius algorithms than maybe any other code in history. And yet, a very large percentage of all that work is just to work around a mistake in the language. And it'll never be fully mitigated.
> The rant against bytecode is[…]
Reasonable people can disagree on the values, but what's the mistake?
> iOS distribution also relies on the ability to deliver bytecode instead of native code for those kinds of late optimization opportunities.
So did (does?) android. I remember those "optimizing apps" loading screens on every update. This is actually exactly my point: None of this requires that the IR / bytecode is, itself, "executable".
> The C++ standard library absolutely uses exceptions for all sorts of silly errors
Nowhere near Java. It's a difference is philosophy, don't you agree? Comparing C++ and Java, you'd agree that Java doubled down on exceptions, right?
And that's what I'm saying the mistake is.
I'm not saying that std never throws errors. But that's also not the case in Rust or Go. But I'm sure you agree that panic in Rust or Go is worlds apart from exception use in Java?
C++ (standard library API, at least) is much much closer to Rust and Go, than to Java. Like, no contest.
> I'm confused as to why the author hates try/finally so much
Not "try" so much as "finally". Compared to RAII or Go's "defer", it's a very convoluted and (in my experience from various codebases) very error prone way to hopefully remember to undo everything (and if a resource acquisition is added, to remember to release it way down there in different code).
It's morally equivalent to defer, yes, but unlike RAII it's not automatic. Even compared to Go's defer and (not guaranteed to be called) finalizer, it's easier to get wrong.
Defer is better than finally, but RAII is better than both.
> try-with-resources, which brings an interface much like RAII to Java.
True. I have no criticism of that pattern, which is why I didn't criticize it. :-)
OOP is great for big applications which is where Java is still a leader. If you have statics all over then you have a problem.
Heap was the right decision when Java was made. Newer JVMs are fantastic at allocating and releasing it in a way that beats C performance for some cases. There are edge cases where it can't do that which is why we're getting Valhalla which will solve that edge case.
The file API you're referring to is out of date. NIO access has been around for ages.
I agree that generics have a lot of issues, but Java has some of the cleanest error messages around. You dug up a single problem there which plagues pretty much any language that has a complex type system. The alternative is untyped languages that have their own problems.
Bytecode is fantastic. You're confusing historic VM implementation (the Pascal VM) which were inspirational but unrelated. The value Java provides there is amazing, first modern JVMs provide a level of performance that rivals native code. But the biggest benefit isn't in performance: it's observability and extensibility. Thanks to the stability of bytecode developers have built debugging and observability tools for Java that are both performant and powerful. Having worked in this industry I can tell you that no other language comes close to that power. This isn't even a contest, it's Java then everything else. It also opened the gate for many other languages that run on the JVM and interoperate with Java fantastically well.
Modern Java uses UTF-8 for Strings and for internal representation of Strings when applicable.
I suggest reading a bit about modern GCs. Xms/Xmx are important but modern heap allocation and the things done by the newer GCs is at a completely different level.
Misuse of checked exceptions in some cases is a pain point for me too. I'd also add that when Lambdas were added the designers didn't do the work required to solve the issue with checked exceptions there. Which is indeed a shame. Having said that, this is a wide problem in pretty much every language. Exceptions make a lot of sense in terms of performance/flow.
I disagree about finally though. It's a powerful control flow tool that allows us to remove code duplication. No, it doesn't look great. But the alternatives (e.g. Go) look worse.
And yet the care and feeding (tuning) of the GC consumes countless millennia of human time per year.
This, as my post says, is not even an inherent problem with GC, but with Java assuming an eventually sufficiently smart GC.
But also yes, successive Java GC implementations have done huge heroics. They've gotten better and better. A million PhDs have been minted inventing amazing technology in this space. But we've been promised the ultimate compacting pauseless GC for decades, and yet I see outage after outage of huge systems root caused to "look, we found a brand new way that the GC can explode (or just thrash) on us".
And (for all its many flaws), Go chose a different strategy, and avoided a class of problems. They learned from Java's mistake in this case. There's still GC issues (though Go is also getting better and better), but nowhere close.
> Java has some of the cleanest error messages around
One can make the case for which is better (well, least bad) of Java or C++ error messages, but neither can come close to Rust error message quality and cleanliness.
And Rust does have a developed type system.
But fair enough, let's call it opinion. Which is a disclaimer I have at the top of the post. Like I said I'm impressed that Java managed to make error messages worse than C++, but you can disagree.
> Modern Java uses UTF-8 for Strings and for internal representation of Strings when applicable.
Ok, that sounds like I'm not up to date (probably nowhere near). It's not an issue I've fought in years. But glad to hear they fixed that… "when applicable". I'll read up on that. Thanks.
> finally
Go is not my favorite language, but here I think it's better than Java. RAII beats defer, but at least defer makes the programmer add the cleanup at resource acquisition, not four pages further down, possibly behind a null pointer check in case the finally clause triggers before the resource acquisition.
I respect your opinion about aesthetics, but I don't accept that my opinion on finally/defer/RAII is "wrong".
I appreciate the feedback. Thanks.
This is a feature not a bug. When I write C++ code I need to make a lot of decisions in terms of memory behavior that would deeply impact performance. I can benchmark locally but production is the part that matters and I'm mostly blind there. An observability solution might be able to detect problems if I used the default C allocator under the hood, but in that case my performance would also be terrible to begin with.
Java lets me detect production performance issues and even pinpoint them to specific objects in a shipping application. This can translate to feedback to developers that could be very valuable. But it also allows me to tune GC behavior in production to get better performance with zero code changes.
Furthermore, the object deletion cost is deferred so if your app works in performance peeks it can be very efficient assuming it has enough memory allocated to it.
The downside is that Java will probably always need more RAM than non-GCd languages (the amount varies). That's a reasonable tradeoff for most since RAM is still cheaper than CPU/GPU.
> But we've been promised the ultimate compacting pauseless GC for decades, and yet I see outage after outage of huge systems root caused to "look, we found a brand new way that the GC can explode (or just thrash) on us.
There is no "one size fits all". Java has such GCs but right now the default JVM ships with several GCs since every workload has different requirements. This means you need to tune it which is a challenge.
If you see bad performance and don't know what to do then I would blame your observability provider. Good ones actually provide recommendations on how to improve a thrashing JVM.
> One can make the case for which is better (well, least bad) of Java or C++ error messages, but neither can come close to Rust error message quality and cleanliness.
Rust is great. But it is a young language with less baggage. It has no binary compatibility to deal with and it doesn't offer many of the language features Java (or for that matter C++) have. Those are all good things for Rust, but I wouldn't build the type of apps I build in Java using Rust.
> Like I said I'm impressed that Java managed to make error messages worse than C++, but you can disagree.
I just spent 20 minutes the other day looking at 100 errors in std::vector which I didn't touch at all due to closing brackets appearing in the wrong place... C++ error messages are at a different level compared to Java. Maybe I'm too used to Java but I never spend time on these things and the IDE is fantastic at handling 98% of the stuff.
> Go is not my favorite language, but here I think it's better than Java. RAII beats defer, but at least defer makes the programmer add the cleanup at resource acquisition, not four pages further down, possibly behind a null pointer check in case the finally clause triggers before the resource acquisition.
Go was built for simple quick solutions at the system level. Typically in such cases you would need to handle the error as it happens.
Java was built for libraries/frameworks/APIs. E.g. if Spring runs into an error it can't handle it since it doesn't know whether you want to show the error to the user, recover, log etc. Throw is the only reasonable option. This lets me build a nesting doll of libraries and dependencies that can defer decision making and even add a systemic solution that eventually handles that. E.g. in Java I can define a recovery policy (retry/circuit breakers etc.) with an annotation and the system would handle all of that for me.
In a typical Java backend application I don't write catch at all. You let the exceptions propagate and then write error handlers at the top level to define behaviors for the system/user. This also makes tracking issues in production much easier as you get a clean stack trace that includes all the details you need to fix most issues.
I think you missed my point about how Java differs from other GC powered languages in this regard. Mistakes in the Java language has needless cost impacts on the operational environment. In simple terms: it produces an needlessly large amount of garbage.
Java made decisions assuming (like I said) that "oh a sufficiently smart GC will automate that". And that was incorrect. If they'd known that was incorrect, then they would have made a different choice. Hence it's a mistake.
The ability to tune GC in itself is a feature, sure. But that's not what I'm talking about. I'm talking about language choices that made the problem orders of magnitude larger than it needed to be. Did you read the linked "Go Does Not Need a Java Style GC" (which, sigh, is now partially behind a "create an account"-wall).
> The downside is that Java will probably always need more RAM than non-GCd languages
I'm not worried at all about that downside. It's a trade-off. But I wouldn't call it a mistake or a problem.
> There is no "one size fits all".
That's my point. Largely there's no "one size fits all" exactly because of language mistakes. They thought there would be, and they were wrong.
> [error messages] Rust is great. But it is a young language with less baggage.
Sure, but that was not the topic. Java absolutely does not have "some of the cleanest error messages around". In my opinion/experience they're much worse than C++ error messages. And that's just about the lowest bar in the world.
Python error messages are also much better. I can't think of any (real) language, compiled or not, with worse error messages than Java.
> Maybe I'm too used to Java
And I'm used to C++ ones, and it's a factor of course. But 20-30 lines of "C#3 extends Foo<Pair<K#3,V#3>[…]" from the post was not hyperbole. I took three lines from an actual message and just anonymized the names (the names would not have helped you. They did not help me nor domain experts). I showed it to very experienced Java developers and they brushed it aside, asking instead to see the code. The error message gave them almost zero information about the root cause, which sounds similar to your C++ example.
> Throw is the only reasonable option
Well, that's a bit of circular logic. Throw is the only reasonable option because the API was designed such that it is the only reasonable option. And the language and the standard library steered the design that way.
In the post and not, I try to stay away from simply rehashing the decades old debate of exceptions vs return values for errors, but I do add the observation that every Java system I've seen has logs and/or graphs with basically (uncaught) exceptions per second. The other languages that have something like this are fundamentally different. I expect that kind of thing in Erlang, because it's part of the philosophy. But NPE exceptions in Java? That's basically "bugs triggered per second". It's not as simple as pinpointing a root cause mistake in Java language (or stdlib) design, but an observation that the outcome of these Java code bases are a buggy mess.
I know this invites "sounds like a skill issue", but you don't get to a large code base without that. Not for this definition of large. This happens with world class Java developers running the show, and it happens in corp drone telco industry code bases. It's a consequence of the language like buffer overflows and memory leaks are a consequence of using C++, no matter the skill.
1. https://cs.stanford.edu/people/eroberts/courses/cs181/projec...
There are a lot of things that oop languages pioneered that are now in other languages. In rust specifically, there is a mixture of features taken from functional and oop languages that are a pleasure to use.
I think the post is pretty important context, in fact it's the main thing, so this "comment without reading" is a bit too reddit, and not enough hackernews.
> I don't think it would be where it is today without being able to stand on the shoulders of giants like java.
The present is a function of the past. Not sure what that's supposed to prove. I do think that Rust (and to a lesser extent Go) has learned from Java some things not to do.
So I do think that the decisions that seemed like a good idea at the time, and proved to be mistakes, are the reasons we do not have those same mistakes in Rust and (to a lesser extent) Go.
If they hadn't been mistakes, then Rust and Go would repeat them, wouldn't you say?
> There are a lot of things that oop languages pioneered that are now in other languages.
Yes, but most of the innovation in Java specifically have since been walked back by newer languages. That's my point.
> In rust specifically, there is a mixture of features taken from functional and oop languages that are a pleasure to use.
Indeed. But what did Java invent, that Rust took in?
If we take C++, and move in the direction of 1) more OOP, 2) More heap usage, 3) More exceptions, 4) Worse error messages, 5) virtual machines, 6) GC, 7) pre-zoned memory, then that's Java.
For literally all of those bullet points Rust went the opposite way.
1) less OOP, 2) structs are one whole heap object, not one per field, 3) less exceptions, 4) Way better error messages, 5) native code again, 6) not GC, 7) "regular" allocators.
So I agree with you if you mean that by standing of the shoulders of Java you mean "let Java be a warning example of what not to do".
If you mean something else, then I don't know what you're talking about. And since you say you replied without reading the post, probably neither do you.
Edit: by the way by "more heap usage" I don't mean more bytes. I mean e.g. a struct with members is in Java many objects on the heap, but in C++, Rust, and Go, it's a single one.