The JVM itself is incredible, all the advantages of a fully native language with nearly none of the downsides, while being basically "third place" in speed behind C/CPP, and maintaining a gap in performance between the next closest competitors. The JVM also can do party tricks that no other language can with instrumentation and runtime class definitions.
I certainly appreciate the amount of R&D Oracle is investing into the Java Virtual Machine. While I use the open source version of the JVMs, I also wouldn't mind also kicking some cash into the back into the devpool. Business with Oracle does seem to be a bit of a poison pill unfortunately. Leaves us "small budget" firms in a pickle. We know the ecosystem needs funding, but the option to engage Big Red is not enticing.
I really wish Java had been like 5 years ahead on its modernizing push. That 10 year gap between 5 and 8 where the language __barely__ changed, while competitor languages were blasting, were just killer. Some might argue the reason the 8+ renaissance has been so successful is because it let other languages beta test, and Java took the winning ideas. Maybe so. But it gave Golang time to get big, which I like a lot less than Java. Especially when people try to use it as anything but a scripting language for grpc and a runtime for protobuf.
But I think with virtual threads, esp when they work with synchronized right, will finally put Java back on top of Go. People complain about Java, but imo Java's problems are mostly accidental and acknowledged, and they do often get improved or fixed. Whereas Go seems to mostly be happy with how it is exactly right now, and are loathe to improve the many pain points. So much gocode is needlessly obtuse, and it's not like the obtuseness actually accomplishes their goal. I've still seen just god-awful Gocode, with layers and layers of interfaces in between you and getting anything done. Very java-esque, despite their OOP resistance being specifically targeting not falling prey to Java's propensity for towering class hierarchies.
Well there was also like 10-15 year period where Java was taken over by enterprise factoryClassFactoryFactoryClassFizzBuzz architectural cancer that really made things suck as a want to be user of the language.
If you just stuck to the core language and the JVM, it's kind of always been a nice place to code even if the language was changing glacially.
Where can I learn about modern Java like you describe? I only remember Java from the days of "enterprise factoryClassFactoryFactoryClassFizzBuzz architectural cancer" and I hate the language due to that old memory, but I'm willing to try again. A lot of companies are using it in the industry I'm in, and it would be worthwhile to learn, but searching Google for Java resources is as much cancer as the enterprise stuff is.
I haven't used it myself, but I've heard good things about Spring Boot.
For other stuff I usually just write in a dialect I like to use, and be very selective about libraries and other bits I use. It's fortunate that the standard library isn't very ClassFactory and is pretty batteries included so you can get a lot done without too much fuss.
In many ways it's not terribly dissimilar to how one might approach using C++ or Perl or any other big language with lots of different ways to do things that you can pick and choose from.
Exactly. I've been using Java since 2000, and I haven't once written an AbstractFactoryFactoryService. To be fair, I skipped over early versions of Spring.
The Java ecosystem is vast and there is very little you can say about it generally.
It's trivially easy to write Java programs without (ab)using inheritance. It's trivially easy to write Java programs in a functional style. There are countless libraries that will provide just about any programming interface you like, the only hard part is picking.
Peanut gallery comment is C# because while you can do the old "enterprise factoryClassFactoryFactoryClassFizzBuzz architectural cancer" it doesn't fight you when you try not to.
Want to pass a pure function that takes structs as arguments around, sure.
I know HN is not the place for this (because Microsoft), but I really like C#.
The ecosystem has been MIT since 2016, and the last few releases have been solidifying the AOT proposition.
It just has so many features that allow it to compete with C/C++ on the low level, but it also provides exceptional high level niceties (which admittedly can be slow, some LINQ for example).
I know it's not fashionable, especially in a Linux-centric world, but I have used it extensively for many years (on essentially every platform), and it's lightweight (in most instances) and has the features and tooling to make development less stressful.
Every time I use Java, it just feels overwhelming and slow.
I like C# too, but both languages seem to perform pretty similarly in my experience. The biggest difference seems to come from third party libraries being more optimized in one framework than the other.
Once Project Loom receives some more love (I believe that's scheduled for the next LTS, Java 25) I'll be very interested to see how Java's green threads will compare to C#'s async/await. I think Java's model may help programmers write more optimal code easier, but only if they can pull off their scheduling improvements.
C# does have massive advantages when calling native code, though. Java FFI is always just kind of a pain and C# FFI is pretty much trivial.
Garbage collection is orthogonal to "native", by which I take you to mean compiled to native machine code as opposed to something like Java's bytecode compilation or an interpreted language.
Go, D, Common Lisp (implementation dependent), and others offer GC and native compilation.
If you count automatic reference counting like Objective-C's ARC, you can add it and Swift to the list of natively compiled languages with GC.
I don’t disagree, but I was responding to a claim that the JVM has _all_ the advantages of fully native languages. Fully native allows you to choose to have no runtime, or a garbage collector, and in some environments (realtime, OS kernel, AAA games) that is overwhelmingly the preferred option.
You can in theory turn off gc but in practice no one does this because nearly everything is written with the assumption that allocations will be cleaned up by the gc. The compiler has no switches to help you run in a non-gc’d state. At best you can control when you run the gc.
The funny thing about GraalVM is that some workloads are actually slower because of some of the dynamic optimisations the JVM applies. Picking the right garbage collector can make a Java program execute faster than when precompiled to native instructions.
I'm sure the same would happen when writing C for the JVM. Tricks like dynamically rearranging/compressing pointers and altering structs to better suit the cache lines and data locality properties of the system code is running on can probably boost any major C program, but that's an incredibly complex change to a C program that the JVM just gives you for free.
Java is 3-5x slower than compiled languages, with horrible and uncontrollable tail latencies due to garbage collection. The bytecode thrashes the icache and the language is overly abstracted and too verbose. You have to install a huge runtime to use it. Many frameworks abuse the language to make things monkey patched/non-obvious. There's a reason many projects are rewritten in native languages.
Java does have a place, however, for I/O bound workloads where developer productivity matters more than performance. You can write a lot of horrible code in Java very quickly.
in fact, most projects don't have the kind of performance requirements that would make native a necessity. I say, java should be the default, and native have a place.
Has anyone ever noticed major differences between different JVM distributions? I've used multiple distributions including TEMURIN and Corretto and never noticed behavioral differences. I've never run Java at massive scale or performance critical code though.
Usually the official JVM has access to newer and better versions of the garbage collectors more quickly as well as debugging tools. For example, the JDK 8 from oracle had the java flight recorder, a fantastic bit of tooling for debugging prod. I don't think the non-oracle versions got it until 10 or 12.
The other JVM distributions are a drop-in replacement for Oracle. It seems like most big customers that aren't still running old versions of Java have already moved away from Oracle to different distributions.
But Oracle is still exclusive on GraalVM. If you've got on the Native Image bandwagon (which is pushed heavily by new frameworks like Quarkus, Micronaut and Helidon), your lock-in to Oracle is stronger, at least as things currently stands. To make it worse, some features are only available in the paid Enterprise Edition. The pricing seems much better now (you no longer pay by core), but it's a far cry from open source. Unfortunately, some of these features are quite critical if you want native code that matches JVM performance.
We test on all of them since we build observability solutions. We run a ridiculous amount of tests on every commit. There are minor differences, but not stuff you would notice when you build an app. If you do notice it then you have a bug in your code. IBM is probably the biggest difference since they do a lot of work on their JVM and have the most changes.
From my experience working at a big bank -- non-technical decision makers love big O. Something about paying for licensing and the support that comes with it makes them feel warm and fuzzy.
>Something about paying for licensing and the support that comes with it makes them feel warm and fuzzy.
SRE here, the warm and fuzzy is lets them dodge any accountability. Their teams push out bad code, blame it on Java Runtime, engage Oracle and in chaos of an outage, push out real fix and claim "Oracle fixed their mistakes". When I was Windows Ops type, I had a manager that wanted to call Microsoft for every outage just so management would think we are on it, even if the problem was not Microsoft at all.
Backslaps all around for management, bonuses intact and everyone happy.
"Wait, doesn't this impact revenue?" Most companies I know who use Java are type that get deeply embedded in their customers and it's not revenue impacting, just revenue delayed.
Or government, or any early users of high demand databases. Quite a bit of vendor lock in once you have started using Oracle. There are a lot of choices for databases now but Oracle was the best RDBMS around in the 90s.
But, you aren't wrong about management and licensing. I have been working to excise any Oracle software from use at work to eliminate the licensing cost. But, management wants to maintain some kind of external support for everything. They want somebody to call when internal staff can't handle it. Kind of an insurance policy for when shit hits the fan.
The irony is that Oracle makes sure that things are complicated and buggy enough to require support.
I once had to use a patch to be able to run an installer. That should have been the low point of my Oracle career but no! I've also had a patch fail after it had passed all prerequisite tests and after it had been installed on an identical machine: support told me the patch could have been modified. I can't even rely on installing something in a dev environment first, because they might change the patch! I hate Oracle with a passion.
The R&D cost on both VMware and Java has do be fairly high, so maybe there's some advantage to having fewer customers and just targeting R&D towards their needs. It just seems like a dead end, how are they going to bring in new customers?
No new companies are going to base their solution on Oracles Java, or Oracle DB. So are they just hoping that some company grows really big and decides that they want to give a large chunk of profit to Oracle?
In my opinion Java was basically a mistake. It's not that the design choices were dumb with the information we had at the time by smart people, but with the benefit of hindsight they were almost all duds.
I can pinpoint several mistakes in that blog post:
* 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.
Thanks for feedback. I wonder what you would classify as a "mistake", though. I don't want to say anything that's incorrect, and definitely invite corrections.
> 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. :-)
Didn't read the post but I highly disagree with your comment. I use mainly rust and I don't think it would be where it is today without being able to stand on the shoulders of giants like java.
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.
Actually that post is very much a mistake. Everything in it is wrong.
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.
> Newer JVMs are fantastic at allocating and releasing it in a way that beats C performance for some cases.
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".
> And yet the care and feeding (tuning) of the GC consumes countless millennia of human time per year.
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.
> [needing to spend huge amount of time tuning the GC] is a feature not a bug.
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.
The JVM itself is incredible, all the advantages of a fully native language with nearly none of the downsides, while being basically "third place" in speed behind C/CPP, and maintaining a gap in performance between the next closest competitors. The JVM also can do party tricks that no other language can with instrumentation and runtime class definitions.
I certainly appreciate the amount of R&D Oracle is investing into the Java Virtual Machine. While I use the open source version of the JVMs, I also wouldn't mind also kicking some cash into the back into the devpool. Business with Oracle does seem to be a bit of a poison pill unfortunately. Leaves us "small budget" firms in a pickle. We know the ecosystem needs funding, but the option to engage Big Red is not enticing.
I really wish Java had been like 5 years ahead on its modernizing push. That 10 year gap between 5 and 8 where the language __barely__ changed, while competitor languages were blasting, were just killer. Some might argue the reason the 8+ renaissance has been so successful is because it let other languages beta test, and Java took the winning ideas. Maybe so. But it gave Golang time to get big, which I like a lot less than Java. Especially when people try to use it as anything but a scripting language for grpc and a runtime for protobuf.
But I think with virtual threads, esp when they work with synchronized right, will finally put Java back on top of Go. People complain about Java, but imo Java's problems are mostly accidental and acknowledged, and they do often get improved or fixed. Whereas Go seems to mostly be happy with how it is exactly right now, and are loathe to improve the many pain points. So much gocode is needlessly obtuse, and it's not like the obtuseness actually accomplishes their goal. I've still seen just god-awful Gocode, with layers and layers of interfaces in between you and getting anything done. Very java-esque, despite their OOP resistance being specifically targeting not falling prey to Java's propensity for towering class hierarchies.
Well there was also like 10-15 year period where Java was taken over by enterprise factoryClassFactoryFactoryClassFizzBuzz architectural cancer that really made things suck as a want to be user of the language.
If you just stuck to the core language and the JVM, it's kind of always been a nice place to code even if the language was changing glacially.
Where can I learn about modern Java like you describe? I only remember Java from the days of "enterprise factoryClassFactoryFactoryClassFizzBuzz architectural cancer" and I hate the language due to that old memory, but I'm willing to try again. A lot of companies are using it in the industry I'm in, and it would be worthwhile to learn, but searching Google for Java resources is as much cancer as the enterprise stuff is.
I haven't used it myself, but I've heard good things about Spring Boot.
For other stuff I usually just write in a dialect I like to use, and be very selective about libraries and other bits I use. It's fortunate that the standard library isn't very ClassFactory and is pretty batteries included so you can get a lot done without too much fuss.
In many ways it's not terribly dissimilar to how one might approach using C++ or Perl or any other big language with lots of different ways to do things that you can pick and choose from.
Your question seems odd to me. You had to learn to write AbstractFactoryFactoryService. Just forget it? You don't need a book.
If you're used to writing nice concise code in other languages, just keep doing that, but in Java.
Exactly. I've been using Java since 2000, and I haven't once written an AbstractFactoryFactoryService. To be fair, I skipped over early versions of Spring.
The ecosystem rewards inheritance and patterns. The language is and always was highly opinionated about OOP which is a flawed paradigm.
The Java ecosystem is vast and there is very little you can say about it generally.
It's trivially easy to write Java programs without (ab)using inheritance. It's trivially easy to write Java programs in a functional style. There are countless libraries that will provide just about any programming interface you like, the only hard part is picking.
This is the classic example: https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...
I've tried to follow along with it, but man I just can't - it's that crazy. It does work though
Peanut gallery comment is C# because while you can do the old "enterprise factoryClassFactoryFactoryClassFizzBuzz architectural cancer" it doesn't fight you when you try not to.
Want to pass a pure function that takes structs as arguments around, sure.
The JVM does not have ‘all the advantages of a fully native language’: fully native code does not need a runtime, or a garbage collector.
I know HN is not the place for this (because Microsoft), but I really like C#.
The ecosystem has been MIT since 2016, and the last few releases have been solidifying the AOT proposition.
It just has so many features that allow it to compete with C/C++ on the low level, but it also provides exceptional high level niceties (which admittedly can be slow, some LINQ for example).
I know it's not fashionable, especially in a Linux-centric world, but I have used it extensively for many years (on essentially every platform), and it's lightweight (in most instances) and has the features and tooling to make development less stressful.
Every time I use Java, it just feels overwhelming and slow.
I like C# too, but both languages seem to perform pretty similarly in my experience. The biggest difference seems to come from third party libraries being more optimized in one framework than the other.
Once Project Loom receives some more love (I believe that's scheduled for the next LTS, Java 25) I'll be very interested to see how Java's green threads will compare to C#'s async/await. I think Java's model may help programmers write more optimal code easier, but only if they can pull off their scheduling improvements.
C# does have massive advantages when calling native code, though. Java FFI is always just kind of a pain and C# FFI is pretty much trivial.
Garbage collection is orthogonal to "native", by which I take you to mean compiled to native machine code as opposed to something like Java's bytecode compilation or an interpreted language.
Go, D, Common Lisp (implementation dependent), and others offer GC and native compilation.
If you count automatic reference counting like Objective-C's ARC, you can add it and Swift to the list of natively compiled languages with GC.
I don’t disagree, but I was responding to a claim that the JVM has _all_ the advantages of fully native languages. Fully native allows you to choose to have no runtime, or a garbage collector, and in some environments (realtime, OS kernel, AAA games) that is overwhelmingly the preferred option.
in Java you can turn off the GC completely
and you can also compile to native code
You can in theory turn off gc but in practice no one does this because nearly everything is written with the assumption that allocations will be cleaned up by the gc. The compiler has no switches to help you run in a non-gc’d state. At best you can control when you run the gc.
> nearly everything is written with the assumption that allocations will be cleaned up by the gc
zero garbage Java it is relatively common in algorithmic trading
> but in practice no one does this
that's funny, I could swear we do
Lots of languages that compile to fully native code still have a runtime and a garbage collector as a part of that.
Some would say a garbage collector is an advantage a fully native language doesn't have.
??? There are lots of examples of this.
D, Common Lisp, Go. All natively compiled (or can be in the CL case) with garbage collection.
You can bolt a conservative GC onto things compiled by an ordinary C compiler.
https://en.wikipedia.org/wiki/Boehm_garbage_collector
I haven't used it, but does GraalVM Native Image take care of that?
https://www.graalvm.org/latest/reference-manual/native-image...
About the GC we can agree to disagree. But about the runtime check out GraalVM which compiles Java to fully native standalone executables.
The funny thing about GraalVM is that some workloads are actually slower because of some of the dynamic optimisations the JVM applies. Picking the right garbage collector can make a Java program execute faster than when precompiled to native instructions.
I'm sure the same would happen when writing C for the JVM. Tricks like dynamically rearranging/compressing pointers and altering structs to better suit the cache lines and data locality properties of the system code is running on can probably boost any major C program, but that's an incredibly complex change to a C program that the JVM just gives you for free.
Java is 3-5x slower than compiled languages, with horrible and uncontrollable tail latencies due to garbage collection. The bytecode thrashes the icache and the language is overly abstracted and too verbose. You have to install a huge runtime to use it. Many frameworks abuse the language to make things monkey patched/non-obvious. There's a reason many projects are rewritten in native languages.
Java does have a place, however, for I/O bound workloads where developer productivity matters more than performance. You can write a lot of horrible code in Java very quickly.
> Java does have a place
in fact, most projects don't have the kind of performance requirements that would make native a necessity. I say, java should be the default, and native have a place.
Has anyone ever noticed major differences between different JVM distributions? I've used multiple distributions including TEMURIN and Corretto and never noticed behavioral differences. I've never run Java at massive scale or performance critical code though.
Usually the official JVM has access to newer and better versions of the garbage collectors more quickly as well as debugging tools. For example, the JDK 8 from oracle had the java flight recorder, a fantastic bit of tooling for debugging prod. I don't think the non-oracle versions got it until 10 or 12.
The other JVM distributions are a drop-in replacement for Oracle. It seems like most big customers that aren't still running old versions of Java have already moved away from Oracle to different distributions.
But Oracle is still exclusive on GraalVM. If you've got on the Native Image bandwagon (which is pushed heavily by new frameworks like Quarkus, Micronaut and Helidon), your lock-in to Oracle is stronger, at least as things currently stands. To make it worse, some features are only available in the paid Enterprise Edition. The pricing seems much better now (you no longer pay by core), but it's a far cry from open source. Unfortunately, some of these features are quite critical if you want native code that matches JVM performance.
We test on all of them since we build observability solutions. We run a ridiculous amount of tests on every commit. There are minor differences, but not stuff you would notice when you build an app. If you do notice it then you have a bug in your code. IBM is probably the biggest difference since they do a lot of work on their JVM and have the most changes.
Good to know some things never change. I used to work on z/OS and it was 50/50 if IBM would release a new major Java version the same year as Oracle.
What’s stopping 9 out of 10 from leaving?
From my experience working at a big bank -- non-technical decision makers love big O. Something about paying for licensing and the support that comes with it makes them feel warm and fuzzy.
>Something about paying for licensing and the support that comes with it makes them feel warm and fuzzy.
SRE here, the warm and fuzzy is lets them dodge any accountability. Their teams push out bad code, blame it on Java Runtime, engage Oracle and in chaos of an outage, push out real fix and claim "Oracle fixed their mistakes". When I was Windows Ops type, I had a manager that wanted to call Microsoft for every outage just so management would think we are on it, even if the problem was not Microsoft at all.
Backslaps all around for management, bonuses intact and everyone happy.
"Wait, doesn't this impact revenue?" Most companies I know who use Java are type that get deeply embedded in their customers and it's not revenue impacting, just revenue delayed.
Or government, or any early users of high demand databases. Quite a bit of vendor lock in once you have started using Oracle. There are a lot of choices for databases now but Oracle was the best RDBMS around in the 90s.
But, you aren't wrong about management and licensing. I have been working to excise any Oracle software from use at work to eliminate the licensing cost. But, management wants to maintain some kind of external support for everything. They want somebody to call when internal staff can't handle it. Kind of an insurance policy for when shit hits the fan.
The irony is that Oracle makes sure that things are complicated and buggy enough to require support.
I once had to use a patch to be able to run an installer. That should have been the low point of my Oracle career but no! I've also had a patch fail after it had passed all prerequisite tests and after it had been installed on an identical machine: support told me the patch could have been modified. I can't even rely on installing something in a dev environment first, because they might change the patch! I hate Oracle with a passion.
But the article claims that only 1 of 10 wants to remain. That means 9 not necessarily. If they don’t want to, what’s the reason to stay?
I'm not clear what you are asking? The point seems to be that that is exactly what is happening?
Maybe they don't want to leave either.
The decision makers like the kickbacks.
millions of dollars in shitty legacy Java systems
90%+? It might make more sense to support the 90%+ case than the rest.
I wonder if at some point this is going to hurt Oracle through lack of Oracle Java support for Open Source libraries and even ISV software.
Certainly it feels a bit risky to use Oracle Java, not only because of Oracle (shudder), but also due to potential support risk.
Presumably Oracle is playing the VMware playbook - less customers paying more for a suite of enterprise tools.
The R&D cost on both VMware and Java has do be fairly high, so maybe there's some advantage to having fewer customers and just targeting R&D towards their needs. It just seems like a dead end, how are they going to bring in new customers?
No new companies are going to base their solution on Oracles Java, or Oracle DB. So are they just hoping that some company grows really big and decides that they want to give a large chunk of profit to Oracle?
“Only 1 in ten signatories in deals with the devil doesn’t regret decision in hindsight”
In my opinion Java was basically a mistake. It's not that the design choices were dumb with the information we had at the time by smart people, but with the benefit of hindsight they were almost all duds.
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.
I can pinpoint several mistakes in that blog post:
* 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.
Thanks for feedback. I wonder what you would classify as a "mistake", though. I don't want to say anything that's incorrect, and definitely invite corrections.
> 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. :-)
Worse is better[1].
1. https://cs.stanford.edu/people/eroberts/courses/cs181/projec...
Didn't read the post but I highly disagree with your comment. I use mainly rust and I don't think it would be where it is today without being able to stand on the shoulders of giants like java.
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.
Actually that post is very much a mistake. Everything in it is wrong.
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.
> Newer JVMs are fantastic at allocating and releasing it in a way that beats C performance for some cases.
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.
> And yet the care and feeding (tuning) of the GC consumes countless millennia of human time per year.
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.
> [needing to spend huge amount of time tuning the GC] is a feature not a bug.
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.
[dead]