Java was actually not bad for me in class when learning software design and data structures, but soul-crushing to work with in the real world. Mindless, unnecessary use of getters/setters, interfaces, and AbstractFactoryImpls. Hiding almost every piece of functionality behind 10+ layers of indirection. Dependency Injection with Spring. They all make it feel like Java draws folks who actually __enjoy__ writing bloated, over-engineered garbage.
YMMV, of course, and yes, there are modern frameworks other than Spring (Play is supposedly pleasant to work with), but life is too short to try to sort out that mess.
I plan to stick to Elixir as much as I can, for as long as I can. When the language clicked for me, it was the biggest breath of fresh air since I decided to pursue programming as a profession, and even cooler than when I discovered Python/Django.
Edit: obviously, Java is not all bad, and not all (or even most) people who use it fit the description above. But something about the ecosystem seems to draw those types (no pun intended) disproportionately.
I have been learning Elixir for past couple of months. After a long time, I am in love with programming+design again. It has honestly given my brain a much needed refresh.
Of course, it can definitely be attributed, at least partly, to just moving to functional programming.
Every day I look forward to 6'o clock, so I can stop working and continue on my own projects.
You will also find Clojure just as refreshing / enlightening. My company uses Elixir but I’ve also done a lot of Clojure, give it a shot if you haven’t.
Seconded. Clojure was a real turning point for me but be warned - once you have the veil lifted re OOP you'll not want to work with Java/C# ever again.
Clojure is such a joy in my programming life. Even though I don't use it in my job, learning it made me change partially my programming style in Java, such as functions decomposition.
Sadly I don't see any chance that I'm going to use it for real job in the future. The market is too narrow for this language. Hard fact that If I don't use it for the job, I cannot reach to its full potential. Guess it should stay as my hobby language for now.
I feel this so much, been working with F# in some sub projects. C# is hard to go back to. Though Microsoft's route of stealing wholesale from the former is making it gradually more palatable.
I think it depends a lot on where you work and what sorts of projects you work on. I've been programming professionally in Java for the last 16 years and I have never encountered Spring. Nor do I typically see the stereotypical sort of FactoryFactoryManagerImpl complexity bloat. It is something you need to watch out for and steer clear of, in particular when selecting frameworks / libraries, but you can definitely live in the Java ecosystem without being pulled too far down this road. (The introduction of lambdas helped.)
(Those 16 years are four years at Google, 10 years in a pair of startups, now 1.5 years at a medium-sized acquirer.)
> I think it depends a lot on where you work and what sorts of projects you work on
Correct. These frameworks are synonymous with "Enterprise Java", which you're unlikely to encounter at startups on one end, or Google at the other, but at many places in-between
> I think it depends a lot on where you work and what sorts of projects you work on.
Agreed, even if my experience has been the exact opposite of yours. I've seen Spring be used in my country a lot, although recently Spring Boot.
On one hand, I really dislike how code is sometimes exchanged for annotations that cannot be debugged with breakpoints easily (unless you dig in to the code where the breakpoint is defined and then come up with breakpoint conditions that are triggered exactly for your code, but not the other 500 places in your app where that annotation is used) and don't allow for easy customization (vs just changing a few lines of code in the actual method body), or sometimes just don't work altogether (e.g. trying to log method execution times with something like AOP, which didn't work with Spring calls sometimes).
On the other, it's better than some of the in-house frameworks that I've seen, with sparse comments written in Lithuanian (which I really don't speak) and just bad in most respects. Honestly, most off the shelf open source frameworks/libraries that are actively supported should be good enough, be it Quarkus, Dropwizard, or something else entirely. Though at least something that's widely known and has lots of questions and answers surrounding common use cases in places like the documentation and StackOverflow. Otherwise, you're in for a bad time.
As a polish-russian lithuanian I do read Lithuanian, russian and bits of polish but... it really is super surprising to see how people abuse unicode-encoded source code. Linux drivers with comments in Chinese are evil!
Translating foreign languages isn't that hard, thankfully, at least as long as the terms are written clearly enough and the language doesn't get too niche.
However, to me it feels like English should remain the "lingua franca" of working in ICT, since most mainstream programming languages are already geared towards English and most of the popular frameworks/libraries also adopt the language, in addition to most of the actual learning materials and books out there. Otherwise we'd soon run into the problem of lots of fragmentation and missing out on useful information.
Or just what we had in Latvia, where a bunch of scholars tried making "Latvian versions" of various English terms, to mostly confusing and useless results, since everyone knows the English ones but very few actually want to use the Latvian alternatives. For example, "DevOps" became "izstrāddarbināšana", which sounds kind of awkward and needlessly long even in our language.
I'm not sure how doctors would work across borders when there would be localized names for over 200 bones that they'd need to learn in each language. Or at what pace software/libraries would move forwards if even changing a simple message would require translations in thousands of languages (which are oddly an order of magnitude more plentiful than we have countries).
As a counterpoint to my own argument, domain code (for a particular system in a particular country) in the local language might sometimes be more convenient to use, rather than developers with insufficient English knowledge choosing the wrong translated terms or even letting typos sneak in. There was this presentation a while ago where someone from Germany I think talked about how the domain code is in their local language and seeing two languages mixed a lot in the same service/class was actually telling of a separation-of-concerns violation, which I found amusing.
Old school xml based Spring is horrible and if that was your only exposure, I understand your aversion.
But spring-boot has an almost zen like quality once you get that it favor convention over configuration. When I was a Java developer, I'd usually use spring-boot with the following dependencies to make the experience better:
- lombok: to generate the boilerplate: constructors, getters, setters, equals, hashcode...
- mybatis-spring-boot-starter: mybatis is a sql resultset mapper, you write the sql and it maps the results. I find that ORM like Hibernate or Eclipselink are a complexity trap: easy things are really easy but hard things are incredibly complicated, mybatis avoid that.
It's worth noting that these days the "Java ecosystem" includes languages like Kotlin which eliminate all that boilerplate, and in fact are impressively boilerplate free. So you don't have to use stuff like Lombok.
Of course it's ambiguous because Java can mean both the language, and the wider ecosystem of libraries, languages, JVMs, tools etc.
I've used Lombok, and I wouldn't describe it as "generating boilerplate," and yes, I see that GP used those words first.
Instead, Lombok allows me to mark fields with an annotation and then use getters and setters that never actually exist in my code. In practice, it's just adding an annotation in places to control functionality, which is not what I usually think of when I read "generate boilerplate."
> Sorry, but the popularity of tools which generate boilerplate for you is, in my opinion, one of the biggest indictments of the whole ecosystem.
I couldn't disagree more. I can understand disliking the fact that we need getters/setters in the first place, or perhaps dynamic things like annotations which aren't "normal" code, but static code generation is something that the industry absolutely should embrace.
Model driven development should be more common.
For example, if you can just draw a few ER diagrams and have MySQL Workbench forward engineer SQL migrations for you to check them over (especially if you need to change 20 tables), why shouldn't you take advantage of that? Having a model of your schema that you can generate from the live schema and then transform it into either a set of fresh migrations or just a delta for bringing an older schema version up to date.
I've actually used these two approaches to save bunches of time for personal projects in the past, even though I took the SQL output and put it into dbmate migration tool.
But databases are just one example. Remember SOAP?
Despite being hard to use, one of the best points of SOAP was WSDL - Web Services Definition Language files which allowed you to have a fully functional description of a particular API contained in a single file. Back when REST started replacing it, there was also WADL, but that didn't really go anywhere. The claim that web service endpoints (REST or otherwise) should be largely dynamic/schemaless is basically a lie, because in most languages you'll indeed want to work with particular fields for the objects that you expect to be returned, or domain classes that shouldn't be that different in practice from what you expect.
The beauty of WSDL was that you could get the file from a live version of an API, feed it into something like SoapUI and get a fully working API client, upon which you could build a test suite. Or even better, generate client code for your language of choice, so instead of making HTTP calls and wondering about how to initialize the client, you could start using library code much sooner, with parameters and methods already created for you.
Some time later, OpenAPI came along, but the wisdom of SOAP was basically lost, since it took a while for projects like OpenAPI Generator to pop up and even now the approach of generating client (or even server!) code based on some specification seems to be utterly lost on the industry.
Want more examples? What about database schemas and mapping them to your ORM/other persistence layer solution? I think if you're writing your own model code, you're doing something wrong, regardless of the language that you use. You'll probably mess up or miss relation mapping with something like Hibernate, will miss out on some comments for autocomplete in Laravel and just generally will have an inconsistent persistence layer that will make you waste time.
In most cases, starting with the schema first and using one of the available generation solutions to fill in the application side of the persistence layer seems like the only sane options. Sure, some might prefer to handle migrations in the app side, like Ruby's Active Record Migrations, or something like Liquibase, which are also passable approaches, as long as you don't create a bad schema just so it fits your application.
Java JPA entity generator example: https://github.com/smartnews/jpa-entity-generator
Java generator to get DDL from JPA: https://github.com/Devskiller/jpa2ddl
PHP (Laravel) generator to get models from schema: https://github.com/krlove/eloquent-model-generator
Ruby Active Record Migrations: https://guides.rubyonrails.org/active_record_migrations.html
In most cases, when you integrate two systems, APIs or just bits of code, one side should be the source of truth and the other should match it as closely as possible (problems with data types aside). Somehow the industry doesn't really know how to do this well, though thankfully projects like gRPC prove that it's perfectly doable in a modern setting.
When this isn't done, you end up with either technologies that are a bit too green to be used successfully (e.g. picking GraphQL to supposedly deal with dynamic data and then being a month late with actually shipping), or having to waste your own time writing "boilerplate" that you will actually need, because although it could and should be generated, it is actually needed (to query that API, access that database, migrate that schema).
I do agree in regards to the pointless Spring XML boilerplate, which is indeed useless.
Spring Boot is still Spring with its annotation madness. Take parameter validation, for example. Crazy, full-screen-width multiple annotations stuffed inside the method parameter list. What on earth is wrong with doing validation as other frameworks do, ie. in the method body?
Why write many lines of validation when one line can suffice?
@Entity
class User {
@NotBlank
String username;
}
// Use site
public User addUser(@Valid User user) { ... }
as opposed to
class User {
String username;
public void validate() throws ValidationException {
if (username.isBlank()) throw new ValidationException("username is empty");
}
}
// Use site
public User addUser(User user) {
user.validate();
// ...
}
Why stuff everything in the param list? No other framework I can think of proliferates annotations like this. Spring annotations being Java-based means I must suffer Java's ridiculous inability to handle regex metacharacters - even after introducing raw string literals in Java 13. Kotlin handles this perfectly but once I'm in Spring annotation-land Spring's touted Kotlin compatibility goes out the window.
Sure, but nothing is stopping you from writing a `validate()` method right? It also appears to me that having so many validations for a base type `String` is indicative that there should be a `Date`, `Time`, and `Zone` type respectively for each of the arguments.
Code like this is expected in Spring https://hastebin.com/jevibugiqu.less . Literally all the classes I see are filled with annotations. Can we even write code in pure java without a single annotation ? Theres a lot of magic going on with annotation. It works great while it is running. God forbid there is some issue and we have to figure it out where all the magic is happening.
That is actually a JPA mapping for your persistence layer, which is one of the relatively few things where lots of annotations should be acceptable (except for the inevitable runtime problems due to foreign data mappings): https://en.wikipedia.org/wiki/Jakarta_Persistence
It's not even that bad, when you consider something like myBatis, which can be useful for cases where you need complex dynamic SQL for your models, but can also involve boilerplate if you don't have codegen: https://mybatis.org/mybatis-3/sqlmap-xml.html
Though in regards to service code and such, I agree, it can get out of hand and be a total mess sooner rather than later. Not being able to properly debug this magic is just the cherry on the top.
It was the @Query annotation which finally killed Spring for me. So I have to stuff all my SQL into an annotation now if I want raw JDBC? Madness. **ck that.
Nothing in Spring-boot force Hibernate upon you. I stated that I used to use mybatis, a resultset mapper... it's a lot closer to JDBC and if you really need raw JDBC, have a datasource injected where it's needed.
Exactly. I feel calling SpringBoot as polished turd would be wrong. It's not even polished, just turd. Where ever I have been forced to use it is 4-5 times the amount of code that would normally be needed for such functionality. Large number of bloated Spring libraries and converts normal compile time errors to runtime exceptions. But tools at VMWare are relentless in pushing it through enterprises.
DSL is a general name to any embedded language inside a host language. C++'s templates can be used to write DSLs, they are compile-time checked. Kotlin's lambda syntax can also create very convincing DSLs[1] to describe hierarchical structures\processes, and they are fully typed, compile-time checked constructs.
There are far more to DSLs than hacky strings (regex) or chaining method calls at runtime (fluent APIs). It's a very general pattern.
head {
title {+"XML encoding with Kotlin"}
}
body {
h1 {+"XML encoding with Kotlin"}
p {+"this format can be used as an alternative markup to XML"}
}
}
But this article specially mentions Spring as some next generation rocket fuel to send Java even higher.
The problem I see is as far Java ecosystem goes Spring is big, respected dev framework to be used liberally anywhere. So if a developer like me call Spring a revolting piece of shit software it is just me asking to become jobless.
I have worked with Spring and Spring Boot extensively, and I also don't understand why people like it. It is convenient when it works, but otherwise it's a hindrance. It adds a significant layer of complexity to understanding how an application works, and to debugging.
Java has reached the status of Cobol - it is immortal because it is everywhere and has been around a long time.
Because of that, there are a lot of Java devs.
Our team works in Go, and so we get a few Java devs in once in a while as new positions open up. The biggest change for them is to get out of archonaut mode and stop overdesigning everything. After going through a 3-6 month cleanse, it's fun to see them complain about the legacy Java stuff and how overly complicated it is and how slow it is to build and test compared to Go.
Modern java (check this - https://www.infoq.com/articles/data-oriented-programming-jav...) is really not that bad. Golang is like far more verbose than even JDK 8 (9 old release) for most parts. The issue is, many java users have only experience with spring ecosystem for doing most of the stuffs. There are far better alternatives in java ecosystem. There are other JVM languages like Kotlin with provides 100% interop with the ecosystems and is far better language than go.
That’s funny because Go is much less expressive than even Java. If anything, their overdesign seems to come from their juniority and badly ingrained “best-practices”, nothing inherent in the language.
The problem with Java is much more with the ecosystem than the language, for all the reasons you describe. Clean, straightforward software can be written in Java but it seldom is.
I'm exactly the opposite! I hated Java in school. I was going to switch careers because of it.
I eventually graduated and my second job was a Java SWE.
I realized it's not just you sitting in a void on a theoretical BS questions. You have a team to talk to and get help from. If you forget what a HashMap is called, you don't get stuck trying to declare a HashArray with an automatic fail grade on the same level as someone who did literally nothing and handed in literally nothing.
Then I got a few years experience as a SWE and I realized my original assumption WAS correct. You are stuck in a void answering BS leetcode questions, and you DO automatically fail if you get stuck declaring a HashArray with otherwise perfect logic.
Indeed, technical interviews are so fun. Especially the fully automated ones, but honestly even the ones with interactive humans they still can't comprehend how somebody could POSSIBLY mistake a HashMap as a HashArray unless you were literally fake trash.
I’ve looked at Kotlin but haven’t used it. It seems like it was designed carefully, and conceptually, I think Jetpack Compose is pretty cool. Is there a (non-Spring-like) Kotlin server-side framework you would recommend taking a look at?
Java's adequate. It's like a Toyota Corolla (insert your boring car of choice here if you don't feel this one works for the analogy). Not the prettiest, not the fastest, not the most efficient. But it gets you from point A to point B with little fuss or muss. I totally get why companies adopt and standardize on it.
Do I use it for personal projects? Nope. Because it's not fun to "drive". For that, I pick the equivalent of a Mazda Miata (insert exciting car of choice), which for me is usually a Lisp.
My problem with Java is that C# exists which does huge portions of the stuff Java excels at just… better.
Before, .NET only ran on Windows which disqualified it from many serious applications server deployments. Today, this is no longer the case for everything but cross platform GUI libraries, and even those are an option if you're okay with not having Linux support.
It's more akin to the old car you had just before you bought your Corolla. It was also pretty decent, but it was starting to have issues.
The JVM ecosystem is quite expansive and it's capable of running many things that would be a pain to develop in house (or pay a third party developer for). However, Java isn't the only popular JVM language anymore and you're no longer forced to use Java to interact with its surrounding ecosystem.
The language and the ecosystem around it aren't going to go away for at least a decade, but I think it'll slowly move the way of COBOL and FORTRAN: perfectly good languages, with perfectly good situations they outperform their competitors in, practically exclusively used for niche use cases and legacy software systems.
> but I think it'll slowly move the way of COBOL and FORTRAN
That could be true of any language that's achieved great popularity, but in 2022 Java is the language that dominates server-side development (by plurality, not majority), it is a technological leader in areas of compilation, garbage collection, and low-overhead in-production profiling, no other single language looks posed to seriously challenges Java's dominant position on the server (as PHP seemed for a while), and, at 27, it is more popular than COBOL was in its (rather short) heyday.
But it is true that Java is among a select set of languages (alongside Python, JavaScript, and C) that have managed to achieve a level of success sufficient to become "a COBOL" or "a Fortran" someday.
> no other single language looks posed to seriously challenges Java's dominant position on the server (as PHP seemed for a while)
What do you think about JavaScript in this context? JavaScript seems at least as popular among today's full-stack web developers as PHP was 15-20 years ago. V8 might not be as optimal a runtime environment for servers as HotSpot, but that won't necessarily diminish the language's popularity.
JavaScript is very popular overall, possibly more than Java, and it is, without a doubt, the dominant client-side language, but its prospects on the server are currently not as rosy as they were five years ago. It's never materialised as a solid, responsible choice for big, important, server-side applications, and it's lost the coolness factor it had when Node was young.
The market is much more fragmented than it was in, say, 2004, and so Java is not likely to regain its anomalous overwhelming dominance it had back then anytime soon, but no other language seems posed to do that, either.
First of all, what most people call "the JVM" is really called Java. Java = libraries + VM + tools + language. Of the ~8 MLOC in the JDK, the Java language compiler and Javadoc make up around 200KLOC; the VM makes up about 1.5M. What is sometimes colloquially known as "the JVM" is really everything except the language, of which the actual JVM is pretty small.
Second, while the JDK is pretty modular (e.g. you can use the runtime and tools but not the frontend compiler as "JVM languages" do), we design every feature by looking at the platform as a whole. E.g. the relatively new records feature is implemented in the language, libraries, and VM. That's why some newer Java platform languages have more baggage than the Java language, because their features are not harmonious with the direction of the libraries and VM (e.g. Kotlin's coroutines and data classes).
Finally, it is true that back in the '90s, James Gosling set out Java's strategy to put most innovation in the runtime and keep the language very conservative (he called it "a wolf in sheep's clothing"), and we follow that strategy to this day, because it's turned out to be very successful. While there are lots of programmers who want more adventurous, feature-rich languages -- and many of them hang out here -- the vast majority of programmers don't. Many more people complain that the Java language is changing too quickly than changing too slowly. So it is thanks to the conservative language that the platform is so popular, which allows us to have the resources to innovate in the runtime.
So while <10% of people using Java do so with another language (no single alternative language has gained more than 5% of the platform's ecosystem) -- and we're happy we can accommodate everyone -- the conservative language is a necessary component of making the runtime state-of-the-art. A combination of an innovative runtime and a conservative platform is what most people want, and that combination is the main benefit.
C# is not a better Java by any means, Java has a more diverse and greater ecosystem, you have plenty of choice for tooling, IDEs, libraries, platforms, etc... With C# you are pretty much stuck with Microsoft which everyone knows what that means (.net core runs everywhere, but still attached to msft in many ways), if you are serious about the C# ecosystem, you need to use Windows, C# IDEs outside of visual studio are mediocre at best. If you like running a operating system that doesn't give a f** about your privacy and developing for that platform then sure. There is a reason why C# (even with Msft huge lobbying efforts, not only to the goverments but to dev communities) have been relegated to boring(and sometimes dying) industries which get huge discounts for using Azure.
I agree with most of your points, but also really dislike Visual Studio. Rider has very good compatibility (unless you care about the constant hangs and crashes and buggy, nonsensically laid out eye sore UI and dumb defaults and ...) and VSCode (the one with all the proprietary Microsoft stuff) is quite usable. What kind of blew me away was finding out that they support Jupyter notebooks now.
The Visual Studio team seems to dislike dotnet supporting other platforms, but it's one of the most critical things for its survival. The more the VS team hates it, the more likely it's to be the right choice (probably a good rule of thumb for building IDEs too).
Microsoft's multi-platform play is only skin-deep. Witness the lag in features for Visual Studio for Mac which will not be getting .Net MAUI compatibility until a long time after the Windows version.
I don't feel its the biggest pain to be honest, at least not in the .NET world where the Async API is so persuasive. I don't think the paradigm would suit Java - the API changes to make it ubiquitous would make it take too long to add value. People seem to code with it (the Async workflow) just fine, and it's a decent model to write some async algorithms. For example firing a few in parallel, starting a task but leaving it running and awaiting it later, etc. The advantage for me is that there is some overhead with Async dispatch - the abstraction of coroutines, green threads, etc is cheap but isn't free and maybe seeing where it could prop up isn't always a bad thing. There is value in knowing that an operation could be async, at least in my time programming and it has a cost that you may not want to pay, defer, delegate to someone else, etc especially in hot loops (e.g. IO).
As a single anecdote working in both languages in my current job in a cross platform environment when having to write Java it feels just that bit more painful, and just that bit harder to get the same performance for the class of apps I write. YMMV.
Google "SCO lawsuit Microsoft". For years around the same time Microsft also extorted huge sums from Red Hat and Suse Linux for unproven patents which they claim Linux source code infringed. The two companies figured it would cost them less to pony-up rather than fight M$ in court.
- Mature support for async/await since 2012. Yes, Project Loom is coming, a decade later..
- Support for generic-aware value types (struct vs. class) and low-level features like stackalloc: very valuable for high-performance scenarios and native FFI. See for instance https://github.com/ixy-languages/ixy-languages. In comparison, Java doesn't even have unsigned integers. Yes, Project Valhalla is coming someday.
As well, debatable to some folks, but: properties (get/set); operator overloading; LINQ > Java streams; extension methods; default parameters; collection initializers; tuples; nullable reference types; a dozen smaller features
Let me continue: no type erasure, type safe generics, switch case guards, native tuples, async/awiat in loops and lambdas, the language becomes more and more functional with every version, and on top of that, .NET Framework has quite coherent API/interfaces and tooling is much better.
The single type erasure is a huge difference that makes those two languages quite different.
Sure. Reified generics, along the lines of C#[1], rather than Java, is the only sane design. Erasure of generics works fine in languages that have the "parametricity" property, where you cannot observe any characteristics of a polymorphic value. That doesn't hold as soon as you have casting.
Java generics have a couple of unfixable problems.
For one, you can have List<Foo> and List<Bar> both be passed to a place where Object is expected. But getting them back from that place and trying to recover what was known at creation time, would normally be done with a cast. But List<Foo> and List<Bar> are not runtime distinguishable from each other. Such a polymorphic cast in the code is just syntactic sugar; you'll get a compile-time warning, but no runtime check! E.g. You can get a naked List and cast it to a List<Bar> and then go ahead and try to use it like a List<Bar>. It will only fail when you get an actual non-Bar thing out of the list. It pointedly won't fail if you take your List<Bar> and pass it to other generic code that does more generic stuff with it. And it won't fail if the list is full of nulls, is empty, etc. In essence, it's completely dynamically typed at that point. The static generic types are lies. In reality, all generic Java code compiles down to non-generic code with runtime casts everywhere. That costs performance and means that you can screw up.
Second, erasure also means that you cannot be polymorphic over primitive types in Java, as the VM doesn't support that in the bytecode. So you can't write really basic stuff like Vector<T>, array sorting code, and now, closures and lambdas, that manipulate primitives. All that has to be duplicated, once per primitive type, and for objects. Or you have to box stuff. So they added implicit boxing (autoboxing) so you don't have to type those characters. But they are there in the code. You're stuck with either duplicating for performance (hand-doing template specialization, if you will), or just creating an assload of garbage.
[1] I say "along the lines of". I designed Virgil's generics system not knowing C#, but working from what I knew from ML. It ended up with a lot of the same choices, but I mapped ML's "unit" onto "void" and that works out nicely for zero-arg and/or zero-return functions. You can even have an Array<void> in Virgil! And none of it creates boxes or introduces unsafe casts.
Your first point is kind of making a big deal out of something that isn’t that big of a deal. The trivial solution is to just.. use the language’s static typing and don’t pass typed objects as Object. With a generic function nigh everything can be done type-safely (remember, if you don’t use class casts, your generic code will be completely type safe). Sure, there are some edge cases, but they are so edge-cases of an edge case that I really don’t think it is a great differentiator between the two platforms.
Your second point is true though, and the fix is something that requires at least 6 PhD’s combined, but they are working on it.
I mean, I agree. The static warnings will generally steer you away from the bad cases. From a source-language standpoint, if you don't consider the second-order effects of this design choice, it accomplished a fair fraction of the original goals. But more complex patterns I've seen people try to pass around java.lang.Class objects as a reification. That doesn't really work, because it's skin deep.
With significant experience with reified generics, there's just a lot of patterns that you can't do in Java. I wrote a whole PLDI paper about it back in 2013. Reified generics mean you can do stuff like ad-hoc polymorphism without direct language support. Ironically enough, the little trick of hiding type arguments with subtyping but getting them back with casts is a powerful dynamic tool.
What I find strange in these erasure-reification “wars” is that so many other languages get a pass. And I sort of understand that, languages without a runtime seldom have reflection, or only in some primitive form, and most language on top of a runtime are dynamically typed. So outside of guest languages, Java and C# are unique in this aspect, and Java does use reflection very heavily, where I can imagine that restricted access to the whole type may be a hindrance.
Virgil doesn't have reflection, so not a lot of metadata needed at runtime. It does full monomorphization (like MLTon), so you can't have polymorphic recursion. Of course that could go exponential, but in practice I see something like 20%-30% space overhead. I have a tendency to use polymorphism for really generic datastructures, like lists, vectors, maps, but I use tuples a ton and now I added algebraic datatypes. It's a lot of fun and the compiler generates pretty good code and compiles fast--full optimized bootstrap in < 400ms).
Of course everyone worries about exponential code blowup. I have a slightly broken implementation of specialization-up-to-machine-rep, but that's not turned on because of some bugs. I think that's the way I'll go in the future.
It is certainly possible to do a type-passing scheme. The built-in interpreter can interpret polymorphic code using dynamic type environments, but can also run monomorphized code. The interpreter is slow.
"Static generic types are lies" yes, and it doesn't matter that much. You got the safety at compile time and if you didn't do an unsafe cast then you preserved it.
The issue with primitive types and boxing is certainly noted. Hopefully Valhalla will address it and more.
The problem with reified generics is that the same variance model must be adopted by all guest languages on the runtime. Hence you basically don't see any guest languages on the CLR, and efforts by languages such as Scala to port to the CLR failed due to problems interoping with C#. I think one of the JVM engineers "pron" has discussed this multiple times.
> The problem with reified generics is that the same variance model must be adopted by all guest languages on the runtime.
I think this is a fair observation, but it really boils down to "a dynamically typed compilation target is an easier target", which isn't all that surprising.
> such as Scala to port to the CLR
I am not as familiar with Scala's saga here, but I've heard multiple conflicting reports from Scala insiders, so I think this is a more complicated issue than just the generics model of the CLR.
Benchmarks generally show C# outperforming Java, but not to a degree that most people would worry about. Performance-sensitive applications are still going to go for something native.
One of C#'s biggest assets is Anders Hejlsberg of Turbo Pascal fame. He's been in charge of C# since its inception. He's done a very good job of keeping the language clean and concise, and generally ahead of Java when it comes to adopting features like generics and functional programming.
I would put it differently: C# has a higher performance ceiling due to it having access to lower level controls (mostly value types), but for idiomatic code, there really is no such difference (and if there is any, I would say it goes towards Java). Also, for programs having very dynamic allocation patterns, Java’s GCs are the state-of-the-art and are basically impossible to beat.
> Performance-sensitive applications are still going to go for something native.
What’s up with Java in HFT stuff then? I’ve never understood this: from what I understand in order for it to work, you have to intentionally avoid doing many allocations, and that seems like you’ll be throwing massive amounts of the ecosystem (Javas biggest strength) away.
Can't really be objective here. You could also argument this makes it worse, but it gives you to take shortcuts for things Java would make very hard or cumbersome (unsafe code, properties).
I do not know if the problems are related to the Java language or to the typical Java programmers, but Java is the only programming language where I have seen a strong correlation between the programming language used to implement some application and a low quality of that application.
During decades of experience with various programs, whenever I was surprised that a program seemed to be abnormally slow, or it had an unusually ugly GUI, or it had a GUI that was impossible to customize, e.g. it was impossible to change the default font or the size of the font, or it had various other annoying features seldom encountered in other programs, I discovered that it was written in Java.
Most of these annoying programs where commercial programs, some of them very expensive programs.
The most recent Java problem that I have encountered was last year, when I could not install some expensive program, because the installer crashed immediately.
After some time wasted to discover the reason, the conclusion was that the installer was written in Java and it always crashed immediately on any computer to which a 30-bit monitor was connected.
That program had both Windows and Linux versions, but both crashed in the presence of a 30-bit monitor, so the Java installer was of the kind "write once, crash anywhere".
The workaround was to disable the GUI of the installer and make an installation from the command line.
There are some 15 years since I use only 30-bit monitors, but this was the first time when I have seen such a behavior (probably because I avoid Java programs, due to past experiences). Googling has shown that this was actually a well known bug in Java installers, which had not been solved in a long time.
I'm with you, even as a Java developer I can immediately recognize when something was written in Java: it's ugly and slow. When it crashes, you know they missed a NPE or tried to use multiple threads.
Minecraft's menus absolutely have that hacky terribleness feeling. It's still much better than Minecraft Bedrock (the C++ reimplementation), but it's also noticeably behind everything else
Java UI and applets have been abandoned long ago (>10 years). This was partly due to webapps becoming the standard, but also the realization that for desktop apps non-native UIs just suck.
The vast majority of Java (=running on the JVM) software these days is server-side, without any user interface.
Precisely, and exactly why the programming world is an absolute, cargo-culting mess of Javascript frameworks and libraries these days. Sure, React is winning the mindshare war now, but it's just the least ugly baby winning the beauty pageant at the county fair.
If Javascript produces much better UX than Swing/JavaFX then surely that's the right tool for the job? SPA with Angular or React isn't the only choice anymore as Hotwire/Stimulus and similar libraries (Unpoly, Inertia, htmx) testify, allowing you to handle your routing and state management on the server while preserving partial updates on the client.
I think that’s true but, Android applications are java/kotlin and run on google’s version of the jvm. there are many Uis people interact with that no longer have any java smell.
What seems to be little known is that Java began life as a language for programming set-top boxes with lots of UI, and continues to be used for Blue-Ray disc menus/apps. Sun and Oracle also invested quite a bit into developing JavaFX as an attempt at "RIAs" running in browsers when it was clear Applets were going nowhere, yet JavaScript wasn't quite as developed as it is today, and Flash wasn't seen as a choice for complex apps.
> Java is the only programming language where I have seen a strong correlation between the programming language used to implement some application and a low quality of that application.
Funny, I hear this sentiment more about PHP which deserved its poor reputation in the early days but is actually quite a nice language now and has been for a good long bit.
I think the biggest problem some people have with certain programming languages is that they don't "look" a certain way they prefer (syntax-wise), or don't have enough modern buzzwords associated with them.
Car analogies aren't intrinsically bad, they are just the go-to analogy when someone wants to jam an analogy where it isn't needed. Analogies in general should be used sparingly, and only in an explanatory setting. Instead people use them as a rhetorical device in arguments, which usually reduces the argument to arguing about the analogy (and you can usually shuffle around the parts of an analogy to flip the meaning somehow).
Here it is fine I think, the writer of the comment is just explaining their point of view.
Analogies should only be used like trains: An efficient, well thought out path from point A to B on a vehicle that is piloted by a pro. Car analogies look appealing -- they look more flexible, but this can also lead to people taking overly circuitous routes to get to the point... maybe the driver just doesn't know what they are doing, maybe the road system is poorly optimized because it needs to hit lots of points, or maybe the driver is like a sneaky cab driver that intentionally takes an inefficient path to increase the fare.
The wider availability also leads to a situation where cars are mostly piloted by random people with no particular qualifications other than a basic license. This can result in lots of car crashes. Of course, it is also possible for a train to get derailed, but this is a rare occurrence. On the other hand a derailing can result in more damage... ah... hmm, I forget where I was going with this...
I think the car analogy is just extending a base analogy. There could be other siblings. Perhaps AnimalAnalogy could extend BaseAnalogy and we could talk about animals instead? ;)
Indeed. All cars suffer the same speed limits and traffic, so how effective your car is ultimately depends on how you drive within those constraints. The analogy slyly condenses everything down to some car-manufacturer-marketing version of "fun", betraying both spirited drivers and whatever topic it is applied to.
Since we're throwing out analogies, Java-the-language is more like a riding lawnmower. It is capable of getting you to your destination, but will be slow and painful the entire time. There is probably some external constraint that you'll be forced to endure this, like losing your license.
This was something / part of the plot of The First $20 Million Is Always the Hardest. I recall the main character getting a car (turned out to be rented) that was really fast and taking the team out driving on 101 in rush hour traffic to make this point.
I remember an intermediate OO class in Java 20 years ago... the existence of the El Camino was proof of the terribleness of both multiple inheritance and car analogies.
The Corolla is known for reliability and fuel economy. It's the car you'd buy for your fleet so you could ignore it, and then you would ignore it, and some day, people would start assuming you are going out of business or something because all your fleet vehicles are from the late 90s.
On the other hand, the 300 claims to be those things, but has terrible fuel economy, and apparently likes to unexpectedly spin out. They claim that new ESP magically fixes the problem, even though they've been claiming such things for years. It's actually supposed to be a luxury car or something, but apparently for some reason it is used as a fleet vehicle, where it is a poor fit.
Also, since it is a Chrysler, I assume it is unreliable. Oh look, I'm right:
(In fairness, the newer ones are supposedly reliable. Sort of like how Java is supposedly pause free now.)
Filling out the rest of the analogy:
fuel economy => Java startup costs, and asymptotic / constant factor slow downs because it doesn't support zero cost abstraction.
spinning out => The GC, of course
magic ESP => The next GC solves the pause / thrashing issues (promised every year since at least 1998)
reliability => Constant API churn, the 8 => 11 => 17 => 20 treadmill, and (of course) log4j.
targeting luxury segment => Java targeting toasters, IoT and web browsers back in the 90s, but only being able to run on hilariously overbuilt and power hungry enterprise boxes instead. Also, the classloader's open world assumption.
> targeting luxury segment => Java targeting toasters, IoT and web browsers back in the 90s, but only being able to run on hilariously overbuilt and power hungry enterprise boxes instead. Also, the classloader's open world assumption.
There are over three billion active Android devices in the world, since 2014 they have been running Android Runtime runtime environment, which uses Java (or Kotlin) bytecode. Before 2014 Android used Dalvik, which did the same thing. This is running on some pretty low powered devices.
I have very vague knowledge of cars, but I know for a fact that your characterization of Java is just bad, no matter how good you car analogy-creation may be.
I won’t even correct the others, but how come Java is unreliable? Like, I have a hard time thinking of anything in the category of computer programs that would fair better.
Have you ever had to maintain an old enterprisey Java thing? It's pure hell.
Basic dependencies (like JUnit) break API compatibility every few years, and then they stop distributing the old version for new JVMs, so you're forced to port your code. The GC falls over in production at the least convenient times. People somehow decide everything should be stringly-typed, and that the best choice between .properties, xml and JSON is "all of the above, and also this in-house DSL". There's this common pattern called "vendoring" where you load multiple incompatible versions of the same library into the same JVM, and exploit loopholes in classloader semantics to prevent it from noticing, and ungracefully exiting (which is what it really, really should do).
There's no way to use language improvements and static analysis to improve program semantics over time (like there is in, say C++) because none of the language level abstractions are sound.
For example, you have a line like "static Foo foo = new Foo()". Guess what? foo can be null sometimes. Here's an unrelated problem: Think you have a function that doesn't throw exceptions? Nope. Some third party garbage can throw errors during normal operation instead. Think that eliminating down casts in your program means it won't throw ClassCastException on unexpected lines? That hasn't been true since generics were invented. Think Optional gets rid of null pointer exceptions? Nope. Instead, it's actually a tri-value null. After all, you can always create null references to an Optional (and, looping back to the beginning of the paragraph, you can't avoid creating null references to Optionals in idiomatic code!)
> Have you ever had to maintain an old enterprisey thing? It's pure hell.
FTFY. And yes, I have worked on old Java apps, they are not worse than any other app that lived for a similarly long timeframe (and the fact that there seem to be more old monstrosity in Java may just mean that it actually manages to do its work written in java, and not fail in some other language).
Why don’t you have a bin repo for old junit versions? Nonetheless, not updating is just technical debt that will have to be paid once either way. Regarding GC, it was never as bad as its name in my opinion, but it improved dramatically in recent years. If it fails it is more than likely a programmer error (which is very easy to debug thanks to the JVM’s killer observability).
Regarding XML and .properties, this is related to enterprisyness not java, nor the JVM. These are meaningful abstractions to a degree but are overdone badly more often than not. Vendoring is not really a hack, it is a correct choice from the JVM’s PoV (in short, a canonical name and a classloader pair is unique inside the JVM), but it can be abused, and application servers kind of do so indeed.
Wtf, Java has probably the best tools when it comes to static analysis. It actually has a well-defined specification of what has to happen under nigh every circumstance, and Java is huge in academy as well so different kind of analysis is an active research topic, especially that Java is also huge in the industry.
Runtime exceptions are a thing everywhere, I again fail to see how are they relevant here, and Java is completely type safe with generics, your statement regarding that is completely false. If you don’t have casts in your program, it can’t fail with classcastexception (reflection-hackery aside).
Optionals are a mistake but the only fault lies in those who put a null inside.
> Optionals are a mistake but the only fault lies in those who put a null inside.
If you, or a third-party exposed a "Optional", then it's the API mistake. You're never supposed to expose an Optional as either input or output; they're clunky implementation details of the Java Maybe monad.
I wouldn’t use Java again because I am done dealing with the brokenness of Maven and Gradle. They are the opposite of “little fuss and muss.” The Go build tools fit your analogy better.
people shit on maven, but i say it's much better than many other built tooling - npm, make, or custom scripts.
The only thing need getting used to is that you cannot and should not stray from the maven model - fit your project's built into the maven model, rather than try to twist maven to do your bidding.
Maybe I haven't built anything large enough to feel the pain, but I find npm acceptable. Go's module system seems more secure, but I had little fuss using npm to build a simple web app.
I don’t know, probably some truck would be a better analog. It ain’t sexy, but it was made to be a blue collar language, and it is the one that ships cargo nigh everywhere.
You're not alone as someone else made a similar observation in a previous thread[1]:
> To me Java is like a garbage truck. You get to work, start it up, do a nearly invisible but absolutely essential duty, then, at the end of the day, you turn it off and go home. No one dreams about garbage trucks or puts one in a car show but they’re there and ready to go right back to work when you are.
Personal rant: I've never understood the hate towards XML. It's a practical and flexible markup language that does not depend on whitespace or quoting every bloody thing. Plus, every markup language invented since has had to re-invent XML things that, surprise surprise were actually needed. Paths, schemas, comments, etc.
I get frustrated with JSON because of things I could do in XML that I can't do in JSON without breaking the spec. And that doesn't even cover how annoyingly verbose anything done in JSON is.
XML means you have to think about how you're architect information exchange much more thoughtfully, because XML is much more strict and follows very explicit rules. It also means you have to develop a schema for your interchange.
With JSON, you can, more or less, just serialize as is, not much thought in the world, and it can be understood by the client. You're not obligated by the protocol to do anything about developing a schema or anything of that nature.
Yes, parts of the XML world have now bled over (defined schema and path algorithms being the big ones I think) but they're optional for better or worse.
Everyone takes the path of least resistance when given the opportunity at the end of the day. This is especially true of software engineers I've found.
Eh, not really. You have the option of defining schemas, of course, but it's not required.
In practice, I think most of us used XML the same way that people use JSON today: here's some data from my app, figure out how to pull out the bits you need. No high ceremony required.
I think people dislike XML precisely because it's so flexible and unopinionated. It means that when you're parsing a new XML source, you have to look for data that might be encoded in two or three little niches. God help you if it's encoded in each of them, and if the data therein is contradictory. Basically, it's easy to "hold it wrong" in a way that harms consumers.
> I think people dislike XML precisely because it's so flexible and unopinionated.
You're probably right.
> Basically, it's easy to "hold it wrong" in a way that harms consumers.
I feel a bit like this could equally apply to things like GraphQL. I spend more time reading the docs on how someone's internal data model is built than I do writing GraphQL queries. And if I get that data model slightly wrong, my query's garbage.
But that's probably a separate, vendor specific (coughnewrelic), rant.
Every "contract" language (e.g. .graphql) I've used so far has it's ups and downs.
One that keeps biting us in GQL, for instance, is that it won't let you define a union type for mutation inputs, so you end up needing N methods like `GetByX(X)`, `GetByY(Y)`, `...` instead of a single `Get(X|Y|...)`. Not to mention support for versioning message types, etc...
FWIW: I think proto3 is probably the best I've used, at length and in production. Granted it has its "warts", but the idioms to circumvent them are fairly well-documented and agreed upon, even if they're a bit "ugly" in the syntactical sense.
FWIW pt 2: Like yourself, I think, I would not consider JSON to be appropriate as a schema-defining "contract" language in 2022, for a company that plans to be around in 5 years. There are too many better options available.
Over-engineering (relative to the complexity of the task you want to solve).
Enterprise solutions are often so complex and generic that they can theoretically do and interoperate with anything, but are also hard to get started with and use well.
People like to start with simple things and expand from there instead of buying a whole house when they just want a sink (am I using the phrase right?). In my opinion this makes a lot of because it avoids unnecessary complexity and sensitizes people to why some complexity is in fact necessary.
I think it also got horribly abused by some use cases. People tried to allow programming in it (e.g. if statements and loops) when an actual DSL would have been a better solution.
I know that was my last straw before I started assuming that anything that required XML would be painful to work with. It wasn't an entirely fair assumption, but it was correct often enough that I used it as a helpful heuristic.
I think the other smell is not using serializers with XML. If when you consume a document you’re not getting an actual useful object back of course you’re going to hate it.
That's the point of the schema—use it to generate compliant serialisation/deserialisation code, and use that code to generate/read your schema-compliant XML.
No issues anywhere. There's only one point of failure here: bugs in the code generator.
XML was built for a very limited set of purposes - to create a common base markup for various document formats. It was almost purpose-built to create something like XHTML - a mixed content system where you can do semantic decoration of textual media content, and extend it with things like SVG and MathML.
The problem is that it was _not_ built as a cross-platform data structure interchange format, but it wound up being used for that way more than its intended purpose. This was partially because of the extensibility story - companies could agree on a common base format, and define their own extensions to add additional data. However this was a pain - the XML tooling was often generic to support both kinds of usage, and the language itself was ambiguous because the expectation that the underlying document being described would have document-specific clarifications on use and tooling.
JSON is an object notation - it is a way to transmit hierarchal data. It has limited extensibility in the sense that you can define rules for data processing, such as 'ignore things you don't understand' or 'name things which are not agreed upon with URI rather than short names'.
Trying to use JSON to represent the content model of HTML will just cause pain, because thats not what it was built for. It isn't even re-inventing things from XML, it is just cramming a square peg in a round hole.
Neither format was built to be a configuration file format for users to hand-edit config. As a result, they suffer limitations in their syntax and features (closing elements in XML, quoted property names and lack of comments in JSON being the most commonly cited). TOML is one popular choice for this sort of use case.
It's not the way Java is done now because, as noted, it was awful. Java is almost 30 years old now. Developers who worked in Java 20 years ago had a pretty different experience than it is now.
However, I'm sure a lot of that code still exists.
It’s very similar to Java: there’s a not bad language buried under huge layers of bad practice and design by committee which most people are stuck using, with a side serving of market failure around the developer experience (e.g. Maven, Spring on the Java side, tools being built on the orphaned libxml meaning that you’re stuck in the early 2000s level of XPath, etc. in many cases).
XML 1.0 was decent but I soured on most of the “standards” based on it after too many iterations of chasing through a chain of specs pulled in by reference where you had to read a bunch of ponderous W3C documents and non-working examples to learn that the spec authors hadn’t correctly modeled the problem, nobody had time to work on any of this, and the only extent implementations either weren’t compatible or had a lot of tedious workaround code. Bonus points if they were replacing a legacy format and ended up with a result which still required deep familiarity with the original format but was also much less efficient.
These problems are cultural. JSON certainly isn’t immune to this but the Java/XML world has more people who felt the need to LARP as Very Serious Architects designing extremely expensive systems. Things like Atom show grownups can use XML, too, but they’re notably the exception.
In Java, the most direct counterparts I see are the places where people felt like they should copy the Sun library developers for code which is far less universally used and took on a huge support cost building abstractions and customization points which were largely unused, often only for security exploits.
This is precisely one of my complaints against XML, it does depend on whitespace. In JSON, I know that any excess formatting whitespace can safely be removed. But excess formatting whitespace is part of the document in XML and I can't know in general whether it can be safely removed or not.
Java/.NET have first class support for XML and SOAP, most other languages don't. It's too easy to use XML in those languages. Other languages don't have that tooling or support and it's just a slog.
Once upon a time I was deeply in the world of XML Schema, XSLT, and XQuery. It was actually pretty cool.
But using complex, poorly specified, possibly Turing-complete "config" files written in a markup language that isn't the primary language your app is written in is a serious code smell.
It means you would either be better off using an embedded scripting language (like Groovy) or a better core language.
I think Spring, Maven (and ant before), JSP, and J2EE descriptors gave XML a bad name, as fields of application where a markup language wasn't adequate. XML was meant as a simplification of SGML to formalize and extend HTML on the web, but was overhyped as serialization format for everything and anything for all the wrong reasons, among them that XML sold well to management.
It can be argued that, despite not a primary use case for markup, XML has found a useful niche in b2b service payloads and government, banking, and health services in particular. The use case for those might have been "web services" in the original sense where simple CSS-like transforms and styles are applied to payloads for display in browsers, but the JSON community hasn't brought forward a serious replacement for XML Schema, so XML payloads kindof keep sticking around in long-term projects.
xml also scales much better with deeply nested documents (such as html documents).
A deeply nested JSON document is difficult to navigate in. Even with prettyprint it is counting indentation to find out what kind of info this level has.
Take the html page for news.ycombinator.com and convert the html document into JSON format. It becomes unreadable.
I personally use Racket (Lispish) just because it is so much fun to build whatever I want and customize it. It just feels good to have my small projects just fit me perfectly.
Scala is a ring of four Corollas welded by the edges of their rooftops so that rolling over only changes the set of wheels you're using. Also, all wheels are omni directional so you can drive sideways when required.
The only problem Java has is experienced C programmers don't build servers from scratch with it yet.
Once they take that responsibility, the debate will be over because:
"While I'm on the topic of concurrency I should mention my far too brief chat with Doug Lea. He commented that multi-threaded Java these days far outperforms C, due to the memory management and a garbage collector. If I recall correctly he said "only 12 times faster than C means you haven't started optimizing"." - Martin Fowler https://martinfowler.com/bliki/OOPSLA2005.html
"Many lock-free structures offer atomic-free read paths, notably concurrent containers in garbage collected languages, such as ConcurrentHashMap in Java. Languages without garbage collection have fewer straightforward options, mostly because safe memory reclamation is a hard problem..." - Travis Downs https://travisdowns.github.io/blog/2020/07/06/concurrency-co...
"Inspired by the apparent success of Java's new memory model, many of the same people set out to define a similar memory model for C++, eventually adopted in C++11." - https://research.swtch.com/plmm
This combined with the fact that Java doesn't crash and you can easily hot-deploy the classloader (maybe 100 lines) means nothing can compete that doesn't copy everything Java does VM + GC (hello C#, please don't downvote).
To use anything else (than JavaSE without heavy deps.) on the server is madness.
I once worked on a Java-based server at Google that had to answer requests with millisecond latency. In order to achieve this, the server had to block garbage collection most of the time. Periodically, each instance of the server would ask the load balancers to stop sending requests to it, so that it could then safely run the garbage collector, and then ask for traffic to return.
We likely would not have used Java had we foreseen needing to do this. I believe some time after I left, it was all rewritten in C++.
Sadly the garbage collection debate is full of people who want GC to be the answer to everything, and have chosen their arguments and beliefs through confirmation bias to support their desire. Many of these "GC is faster!" arguments come from that, but it just ain't true most of the time. Performant GC is incredibly complex and incredibly complex systems tend to fall over if you don't use them in exactly the right way.
Worse, GC is extremely bad at managing resources other than local memory. In complex distributed systems or other software that manages external resources, GC languages tend to be ill-equipped for the job because they lack explicit resource-management tools like RAII. In Java, you sometimes see frameworks where everything has a `dispose()` method and complex systems are built to make sure that `dispose()` method gets called... this is a failure, it should be handled by destructors.
GC is generally nice and convenient for application engineering, but a poor fit for systems engineering. The boundaries between the two are admittedly fuzzy.
This is kind of a fair comment, but kind of not, because Java performance and GC internals have really advanced a lot in the last decade. It would really help if you qualified approximately when this was.
> Sadly the garbage collection debate is full of people who want GC to be the answer to everything
Good point. I think more recently, the Java world is very aware of this. Native and off heap memory has been getting more use in performance sensitive stuff for quite a while. You can totally just (essentially) malloc and free in Java, if you really need to for performance.
That said, if you want to be able to make your code accessible to a wider audience, GC is a must. There are tons of junior and midlevel developers who don't really have experience working with non-GC application code (and in many cases are intimidated by it!), and you will be restricted from hiring any of them if you use a non-GC language.
If that's ok, then that's ok, but with Java you can still do a little off heap stuff in the critical part you need to, encapsulate it behind a safe API, while letting the juniors run amok in the rest of the codebase.
If one considers applications as GUI programs running on consumer hardware like desktops, laptops or smartphones Java is in reality not at all great at this task. It’s better than Rust sure, but definitely worse than C++, C#, Swift or Objective-C which have access to the native UI frameworks of their respective platforms.
I’ve had the opportunity to observe Java’s floundering in this area for a couple of decades now and the only decent Java application that I can tolerate is an IDE from Jetbrains. I’m not happy with the performance or UI, but the alternatives are often worse, because in an unexpected twist they’re written in JavaScript.
It's worth observing that ZGC can actually do sub-millisecond pauses. That didn't exist when you were at Google and it's still pretty new (and low throughput until they make it generational). But it's definitely on the edge of being able to handle even that sort of demanding latency requirement, at least as long as you aren't overloading the servers.
You're getting into hard real time territory there. That's a different universe and will require special considerations even in C. I think it's fair enough for people to discuss server performance and assume that the context is our normal programming universe.
Speaking of this, have you of LMAX Disruptor[0]? You probably have. And yeah, having fine control over the memory allocation and garbage collection in such a high-level language (multi-platform vm!) is akin to building a ship in a bottle. However, in which stack creating multi-level distributed stock exchange with sub-milisecond response times is easy?
That's a strategy called arena allocation or region-based memory-management, and it doesn't have to be tied to the lifetime of a process or interpreter.
There's nothing special about PHP, which if I understand correctly uses reference-counting with conventional garbage collection to handle reference cycles. As I understand it this strategy is non-competitive in all performance metrics, compared against modern garbage collectors.
but if you're going to do that, the same could be done with java, with less pain for the remaining parts of the application that does not require such methods of memory allocation.
no, you cannot control allocation via regular means. What i meant is that you make pooling in java, and that effectively makes it work like an arena allocator (in that you end up setting a bit/boolean flag to allocate/deallocate an object).
But the pooling can be constrained to just one portion of your app - the hot loop or the bit that needs the low latency. The remaining code - loading resources at startup for example - can just be regular old java that's easy to write.
> I once worked on a Java-based server at Google that had to answer requests with millisecond latency.
That's... not really normal, though, and sounds like the exception that proves the rule. For the vast majority of applications, Java will perform better, be easier to develop, and be safer to run, than an equivalent server written in C or C++.
At my previous job we used to run realtime audio through a Java server (RTP streaming). I do remember in the Java 6 days that the GC would need tuning to ensure that GC pauses wouldn't delay audio to the degree that there would be dropouts or perceptual delays. But with Java 8 (and later releases), which came with better GC implementations, those problems just went away. Sure, realtime audio is usually fine with even up to 100ms pauses (or even 200ms, sometimes) -- so this is much more tolerant than your sub-ms example -- but we rarely saw anything even remotely that long with the more modern JVM GC implementations, without really having to tune behavior that much, or at all. Meanwhile, P99 stats for most JVM services were in the low to mid tens of milliseconds, and anything longer was always due to calling out to external services, like relational DBs.
For the rare case like Google's, sure, it's absolutely expected and appropriate to need to use a non-GC'd language instead, at least for some things. For pretty much anyone else, the JVM is more than adequate, or can be made adequate with some reasonably simple GC tuning.
> Many of these "GC is faster!" arguments come from that, but it just ain't true most of the time.
I don't agree. I think it is true most of the time. But I think many people don't think about what they mean by "faster". Faster as in throughput? Sure, a modern, performance-oriented GC (like the JVM's) can very easily beat manual memory management there. Latency? Well, ok, that can be a bit harder, so you need to evaluate things on a case by case basis, and possibly do some GC tuning to get the latencies you need. But even then, you can usually do just as well (or better) on the latency axis as well. Just not always. But I don't subscribe to the "But sometimes..." school of objections. Yes, sometimes some technologies don't work for certain use cases. That's fine. Choose your technology wisely. But in Java's case, it really is just "sometimes". Not most of the time.
Let me reiterate, though: Google is not the common case! By a long shot! It is an outlier, and it's expected that a company like Google will have to deviate from the mainstream to reach its performance targets sometimes. But also consider that (from what I understand) even Google has a ton of services written in Java, and they... work just fine, no?
> In complex distributed systems or other software that manages external resources, GC languages tend to be ill-equipped for the job because they lack explicit resource-management tools like RAII.
Eh. I initially thought of this as a problem, but in practice, I've rarely run into an issue with this sort of thing. Maybe it's because there's still vestiges of the C programmer in me that will always think about memory ownership and lifecycle, even when writing in a GC'd language, but I've rarely had my own issues (or seen issues written by others) where someone has forgotten a `.close()` or `.dispose()` on something. The "try with resources" syntax can help here too, even though IMO it can be kinda cumbersome.
And as much as the Java docs tell you to essentially never override `finalize()`, it can be a useful "last ditch" tool to ensure that any "manual dispose" owned references get cleaned up, and you can also add logging there; I'll often do something like "Got to finalize() without disposing of Foo instances: BUG!". I also appreciate when third-party authors who write `dispose()` methods also do this. It's not perfect by any means, but IMO the convenience of relying on garbage collector rather than manual memory management far outweighs this downside.
Lately I've been writing a lot of Rust, and I'm enjoying the sort of "middle ground" approach, where I don't have to think about memory management as much, but don't have to worry about GC performance, either. Certainly Rust doesn't eliminate these concerns; I still need to think about ownership and object lifetimes, but it's never "oh crap, I forgot to free() something and there's a memory leak", or "oh crap, I tried to use something after free()ing it and crashed", it's more like "ugh, rustc doesn't agree with me that this object lives long enough and refuses to compile it". Annoying, but I'd rather find this out at compile-time than runtime.
But then I'll go back to writing Java or Scala after being in a Rust project for a while, and remember how nice it is to just not have to think about these things.
> ...but a poor fit for systems engineering.
Absolutely agreed. But I would not call writing distributed network servers "systems engineering", even though I do agree that the boundary between systems and applications engineering is indeed fuzzy.
> But I would not call writing distributed network servers "systems engineering",
I would agree that a web application server is usually application engineering even if you are running 1000 replicas of it. I would not write these in C++. (Well that's a lie, I probably would but I would admit it was a terrible idea.)
On the other hand, the container engine that orchestrates the web app server is systems engineering, as is the database it talks to for storage.
Kubernetes, a container orchestration engine, is written in golang, a language with garbage collection. Some database engines have also been written in golang.
Same with Java, several databases written in it, such as QuestDB, Pinot, Presto.
Sure, I mean, finalizers have been deprecated as long as I can remember. But -- as you say -- they still work, and I expect will continue to work for quite a while. And, as you suggest, probably forever for existing bytecode that targets older JVM versions.
I do very much appreciate that "deprecated" in Java only means "there's a better way to do this" and absolutely does not mean "this will go away". Serious platforms never break backwards compatibility.
Imo Erlang's scalability benefits are oversold. Unless you're a national carrier the performance of Java far outweighs Erlang's scalability. Instead, Erlang's undersold killer feature is its robust support for hot code reloading. It enables almost any part of the system to be upgraded without affecting running processes. Other languages and VMs can't do that because they optimize call frames differently and rely on mutable state. It's like building a new road on top of an existing one without impacting traffic. Almost magic and very important for always on services like emergency numbers.
I feel like Erlang somewhat missed its timing window on this, though. With platforms like AWS, GCP, and Azure, you don't really worry about upgrade downtimes, because you just roll out a new fleet of servers (or new fleet of pods, if you're using something like Kubernetes), and then drain the traffic from and decommission the old fleet.
Certainly there are a lot of companies managing real physical servers, where this is not as feasible, but I think the ease of server provisioning makes hot code reloading just not that important to most engineering teams.
(And personally I'd be wary of using something like that, considering that the rollback path -- in that case that you deploy bad code and need to back it out -- sounds not as robust.)
only works if your application consists of stateless requests (like http traffic).
If your application require a persistent connection (e.g., you're a real time streaming service, or video), you will have to write some application specific code to migrate the current state over to the new cluster being provisioned. This is actually quite hard to get completely right, and quite application specific (i mean, a very simple method would be to store some sort of identifying token, and when the new connection re-establishes, you reload the state back from when the disconnection happens).
All that said, Erlang is a very sharp knife, built for one specific purpose. This is just not true:
"a much better concurrency model"
If your program is CPU-bound, where real threads are king, then Erlang is a pretty poor solution. Because you cannot have real threads in Erlang, even if you wanted to. Number crunching or string processing are not its forte.
Also, most enterprise software does not really benefit from green threads anyway, because the scale they are running on easily handles blocking JDBC and http request calls. Not all companies are google or want to run a telephone switch that handles >million concurrent calls...
And finally, if you go Erlang, you get actors, whether you like them or not. You cannot have CSPs, for example, which in many ways are superior to actors. You don't get to choose your concurrency model, it is chosen for you. If your use-case suits this model, great. If not... not so much.
There are responses to each of those points, but ultimately you are right that there are tradeoffs. My point was really that Java isn't the only sane server-side language, and that Erlang is an entirely sane option.
The one thing I will say is that, when I say Erlang has a better concurrency model, I mean that it is much easier to write correct concurrent code in Erlang than in Java.
Biggest benefit with Erlang for me is preemptive scheduling. Has a perf hit, but you don't get brownouts. Java lacks this and this results in hanging threads. A lot monitoring has to be there to detect these conditions .
Project loom makes this somewhat better, but at its core, java threads are not preemptible
ZGC has sub-millisecond pauses https://malloc.se/blog/zgc-jdk16
And afaik azul's C4 collector has no global pauses, only per-thread pauses (which are also short)
But that's not performance category. And, btw, Rust has all 4 of them. If we're talking about safety guarantees then let's add data race freedom to the mix. Or use-after-free protection for non-memory resources. ;)
Java is cool language and has many nice features, but its type system is not its major advantage.
Malloc and friends have much more CPU overhead than a modern GC, so if you do choose all of these, brace yourself to a really micromanaged programming.
That's a common wisdom, but there is little if any evidence to support it. True GC costs are simply more hidden and harder to measure. A malloc/free pair might be slightly slower than Java's new (not by much really - go measure), but invoking `new` is not the only cost of GC.
Also modern low pause GCs have way higher CPU overhead than the old STW ones. Try to set a low pause GC pause target to 0.1 ms or set the heap limit to 110% of live memory and see what happens to CPU consumption. Last time I tried, the app didn't even start - got OOM nearly instantly. There is a reason the study above measures at 3x oversized heap.
The cost of GC is lower than malloc/free only if you give it a lot more memory than you actually need. Which in many cases is a good tradeoff to make, but it is good to know such tradeoff between memory and CPU exists.
And finally there is one more thing that's not particularly a trait of GC, but rather a limitation of current Java - it is much easier to write a C++/Rust program with low number of heap allocations than in Java. The fact that malloc is slightly slower does not matter if you invoked it 10x less frequently.
Your point on low-latency GCs is indeed fair (read barriers vs write barriers), and I should have probably specified ref counting where the overhead is much more apparent (and is a fairer comparison).
But regarding malloc-only, fragmentation also comes into picture which does have a non-negligable effect. And while Java does like to “heap”-allocate, it happens foremost on thread-local buffers and are used pretty much as an arena allocator. Even without escape analysis, these are very cheap all around.
> And while Java does like to “heap”-allocate, it happens foremost on thread-local buffers and are used pretty much as an arena allocator. Even without escape analysis, these are very cheap all around.
If that was true, it would be fairly easy for Java to come close to C++, C, Rust, Pascal in the "binary trees" microbenchmark in The Computer Language Benchmarks Game. This microbenchmark is the one that stresses dynamic heap allocation, and is traditionally favoring bump-allocation, so compacting GCs should have an easy win, shouldn't they?
The problem is: the best Java implementation loses this benchmark by far on the CPU part (~2.5x worse) and terribly on the memory part (~20x worse) when using the default stop-the-world GC.
I attempted to run this benchmark using ZGC with OpenJDK 17, and here are the run times depending on the heap size:
250 MB: OOM
300 MB: 27.3 s
500 MB: 12.4 s
750 MB: 8.4 s
1G: 7.8 s
2G: 5.1 s
16G: 5.6 s
For comparison, the best Rust program on the same computer runs in 0.8 seconds and takes about 150-280 MB of RAM (max RSS, varies from run to run).
And the C version #5 that uses malloc (no arenas) is still about the same speed as Java at 2G: 5.4 seconds.
The binary tree benchmark is made to stress test the GC algorithm, and Java beats out every single managed language by a huge degree. Low-level languages don’t do the same thing for this test, so I don’t really see the point of comparing C vs Java on this test - unless using raw memory buffers would be allowed for java as well.
In a comment above you've said Java memory allocation had performance of arena allocators. This benchmark shows such statement is very far from thruth.
It is not even close.
The best you can shoot for with Java ZGC is the overhead level of naive C malloc/free (not arena) and only if you give it 5x more heap. You can do better only if you are ok with switching to a GC that does STW, in addition to overblown heap. But even with parallel, STW GC Java is still far from from arena allocators. Which is what I have said in the first comment about picking one feature: throughput, low pauses or memory eficiency.
I didn’t disagree with your last sentence, but nor is the benchmark evidence against “Java memory allocation having the performance of arena allocators”. The benchmark hardcodes additional information about the problem not available/encodeable in Java according to the rules, so Java has to decide at runtime the lifecycle of each object. That of course have an overhead, but it is not allocation rate, or deallocation rate, as both of those are just pointer bump and reuse region, with the surviving objects copied to another region.
> That of course have an overhead, but it is not allocation rate, or deallocation rate, as both of those are just pointer bump and reuse region
Sure. GC makes some particular operations faster at the expense of adding overhead in a few other places (thrashing the caches by scanning the heap, using precious memory bandwidth to move stuff around, pausing threads of the app from time to time). However, from the developer and end-user perspective it does not matter that allocation is a pointer-bump and deallocation is a no-op. What matters is the performance of all the things together.
If there is a performance problem caused by heap allocation, I find it much easier to locate it in a traditional program using manual memory management under-the hood, than in a managed app with tracing GC. This is because the cost is directly associated with the code doing the allocations / deallocations. In managed app, the cost is spread out through unrelated code.
Java also has this for at least a few years now. G1GC and ZGC in JDK17 offer no global stops unless absolutely necessary, and ZGC even has latency targets.
No Erlang can't share memory between cores atomically.
Erlang can only scale 1-to-1 things like phone calls.
Java is the ONLY language that scales many-to-many with stable non-blocking IO and concurrent parallelism that shares memory atomically AND doesn't crash.
I've been writing (primarily) web applications for the past 25 years or so, with PHP, .NET, and Ruby. "Scaling many-to-many with stable non-blocking IO and concurrent parallelism that shares memory atomically AND doesn't crash" has never once been an issue that has come up in all of that time.
Think MMO here, which is the "metaverse" thing they keep talking about.
Simulating 3D reality over the network IS the ONLY thing humans can do now that doesn't HAVE to burn all the energy we got left while giving use a tool to experiment without risk.
So I'm building the final 3D action MMO game engine.
The p50 speed up from delaying memory management comes with a trade off, namely you get Garbage Collection pauses, bad p99, and spend your effort tuning the Garbage Collector instead of your code.
Does it really? With Java’s G1GC the only knob you may end up having to tune is the max target pause time - which pretty much chooses between better p50 and worse p99 va worse p50 and better p90 - aka throughput vs latency, that are almost inherently opposite ends of the same spectrum. It is not GC-specific that any improvement will fail to increase one of those.
So the actual tradeoff is more whether you want better throughput or tail-latency. To improve the latter, you have a singular command line option of using ZGC as well.
Have you seen the output of JVM flight recorder or some other tool used to observe memory allocation/reclamation patterns? The JVM is very lazy when it comes to GC - its algorithms are crazy good and can catch up with really high allocation rates as well. So the maximum pause time’s unit doesn’t really say much, and when it comes to dynamic memory allocation, the JVM is the best choice according to the biggest web services operating on terabyte sized heaps.
With Java 17 I found that using ZGC eliminated [1] the GC pauses that yield bad P99 latencies without any additional tuning. However, it has lower throughput than G1GC so your P50 advantages will diminish.
[1] Reduced them to always well below 1 millisecond.
In my experience, this is not related closely to GC and are apparent in many systems. A great book on optimizations explicitly mentions that “improving one aspect of performance may degrade another”.
Low pause collectors exist explicitly for this purpose. ZGC and Shenandoah both trade throughput (more concurrent collections) for extremely low pause times: https://malloc.se/blog/zgc-jdk16
Does that mean they also trade _memory_ for low pause times? Because, in our application, the memory required for avoiding significant GC pauses is at least 4x the amount of memory we actually need at any given time.
Have you measured that with ZGC? Because due to using virtual pointers specifically, ”third-party” tools may report higher virtual memory usage than what it actually uses.
> Java's GCs are incredible and you have a menu of algorithmic options
can these options be used in separate parts of a single app ? e.g. the app I'm developing in C++ has parts that do realtime audio, others that do GPU rendering, others that do classic Qt Widgets GUI, others that do offline computations on datasets - and they all have different performance characteristics and need different memory management schemes to get the best out of each, all while being in a single process; there's reference counting, tree-based allocation, pooled, linear, a GC-ish thing which ensure that memory is freed in specific non-realtime threads... Can that be done with Java or is one tied to a single GC implementation for a given execution of a process?
Actually it does if you use the GraalVM Native Image AOT compiler. It's a sort of pseudo-JVM that gets linked into your app and provides the following features:
1. Instant startup due to AOT compilation and a cached heap. Can start faster than C!
2. No warmup.
3. Can create native code shared libraries.
4. Offers isolates, which are segregated heaps that do GC separately but run in-process and which can communicate with each other.
The tradeoffs are that unless you buy the more advanced edition, peak performance is lower due to lack of JIT profiling, you may need to write configs and do other fiddling to ensure the AOT compilation doesn't miss any code that's accessed via reflection, it takes a long time to compile, and you can't dynamically load bytecode (which some libraries do behind the scenes transparently).
What are the advantages of isolates vs running multiple JVM processes other than avoiding slightly higher memory usage and some small context switch overhead on IPC - which should be negligent as presumably communication between isolated components would be coarse grained?
I can give you an example: in the Bitwig digital audio workstation, you can choose whether external third-party plug-ins (small audio processors) are in the host process or in external processes communicating through shared memory. I just tried with a very simple plug-in (3 band EQ) to see how much I can stack in either case:
- In the "in-process" case I can stack ~1400 plug-ins on a single channel before I hear a crack in the sound.
- In the "shared memory" case I can go up to ~200 at most. And I'm confident that they really did the very best things possible for the implementation to be performant.
So for me the "things isolated in their own process" means literally getting seven times less out of my system than in the host process (and that's frankly unuseable, definitely not "negligent").
10 years ago the algorithms you could pick were serial, parallel, and CMS.
Today we've removed CMS, added G1GC, and added ZGC.
G1GC is similar to the parallel in the way it operates but is divided up enough to control for latency.
ZGC is fundamentally different from the parallel collector or G1GC. Suggesting it is the same is to suggest you know nothing of the changes that have happened.
Now-a-days, if you are tuning your GC you are almost certainly doing the wrong thing.
While the JVM offers a plethora of levers to pull in case you are hyper concerned about different things, the heuristics are VERY good. Mucking with the fine grained details can disable heuristics and ultimately give you worse results than if you just left stuff alone.
The levers to pull are algorithm, max memory, and max pause time. All other levers should be left alone unless you've got GC logs to back up what you think needs changing. (And even then... Do you really?)
Typically, the better route is flight recorder and eliminating wasted allocations.
An uncommon but not unheard method around that (in services that have diurnal patterns) is to throw memory at it and collect once a day during the down time. You can take the server out of service, restart it, and put it back in service.
This is kind of hard to do in practice unless you’re really watching where you’re allocating memory. It’s very easy in Java to end up accidentally throwing a bunch of allocated memory on the heap without realizing it.
Fwiw, the JVM now has a noop garbage collector so this is easy enough to benchmark.
Concatenating lots of Strings instead of using a StringBuffer will do the trick. Sure, people who are familiar with Java know not to do that. But I’ve seen plenty of production code that concatenates.
Early Lisp (even pre-lispm) had no GC at all -- it just allocated until it ran out. Memory reclamation was done by saving the heap to tape and then reading it back -- only live objects were saved. So it was kind of GC with extra steps; the first GC algorithms removed the steps.
> This combined with the fact that Java doesn't crash
Huh? Doesn't crash in what way vs. C? I can still deref a null pointer and blow up.
Java's perfectly fine, and I have no idea why you'd write C any more, but if you care about (extreme) performance and not crashing, Rust seems the obvious modern choice here.
One of the weird things about Java is that there's a big low-latency/high-performance Java community around that stems from Island (now owned by NASDAQ) using Java as the platform/language for their matching engine. Then, talent flow from that team resulted in lots of proprietary trading shops using Java to low-latency trading/order execution.
If you do that in C, your program may run “just fine” on the surface forever, yet it can silently corrupt all of its data. Java can catch NPEs and handle them appropriately (e.g. a web server might just answer server error 500, but it will continue to run with well-defined semantics).
I don’t think the two is comparable. Even Rust will go off the happy path with a single use of unsafe, and there is no sailing back from there, while a Java program can’t crash in a UB-like way.
Sure, but only with similar frequencies as an LLVM/compiler frontend bugs. A JVM bug may also be harder to debug, due to all the moving parts, but realistically, it’s always the program’s fault, and those are taken care of.
To directly answer you though, no, I haven’t hit a JVM bug that was apparent (so no segfault or anything like that, except when playing with sun.misc.unsafe).
I have worked on big projects with up to ~100 developers. On the C projects, someone's null-pointer dereference brings down the whole process. On the Java projects, the event handler or daemon thread has an exception handler at the top that logs the error and keeps executing. This is a huge difference in behavior.
Sometimes, you want to fail fast and be forced to fix that bug. More often, I want to keep doing whatever I can to test and develop the system and find more bugs without restarting the whole thing.
But this is generally not a good idea. I find software that does not crash early and visibly but instead tries continuing despite an obvious bug very brittle and often causing trouble because it can take a long time before operators notice the problem. If you accumulate enough bugs of this type, you get a mess that "kinda works" but is full of surprises.
Yeah, I implemented an “atcrash” handler as a library which I embedded into multiple projects. But it was only so I could walk the stack, dump a stack trace into the terminal, and tell the user, “Please email this to the developer.” And then as cleanly as possible shut down the process. In C, as you say, not much else can be safely done.
In Java if you encountered a null pointer exception or out of bounds error at the global level, there is also no guarantee you can safely continue, and dumping the stack and terminating is the only sensible option. Sure, the JVM is ok, but your app state might be already corrupt.
The JVM is not corrupted by a NPE or out of bounds access unless you are using native code.
For example, I worked on a large X-Windows/Motif application with many developers. If some library dereferences a null pointer, it is game over. Kill the app. In a Java Swing app we built for a similar use case, we just trap the exception in the AWT event dispatcher, log it, and keep going. Yes, sometimes an exception has ruined the global state in some catastrophic way, but this is rare. We can keep running and debugging without restarting the whole app.
Another example is a web app: Any exception that bubbles up to the main request handler loop is likely (in our architecture) to happen before any change is made to the persistent store. Log it and handle another request. This makes debugging a lot easier than restarting the whole app.
This has been my experience with software written in Java by developers who think it was not their experience.
> In a Java Swing app we built for a similar use case
...oh no. I actually tend to associate Swing UI with weird, undefined behaviors, and whenever I'd look under the hood, it would nearly _always_ be due to them trying to swallow runtime exceptions. Pure hubris, as far as I'm concerned.
> Another example is a web app: Any exception that bubbles up to the main request handler loop is likely (in our architecture) to happen before any change is made to the persistent store. Log it and handle another request.
Please, please, _PLEASE_ just crash the server. The OS will handle it, it will be OK, and systemd will even do the right thing and give up if your thing crashes _persistently_, including all the fancy stuff like keeping track of how often it crashes.
I've done too much cleaning up after overly-confident superstar architects, your examples just brings painful memories. I beg you stop trying to be clever and just log the error and crash the app.
There is some wisdom to what you say. I've experienced some Java apps that basically enter an exception/logging loop, quickly filling the system with gigs of logs full of 100 line stack traces. Eventually someone is alerted and has to clear the logs and kill -9 the app.
I have the logs from my deployed apps. I know exactly how many times my users are getting those exceptions. We use those logs to fix the bugs and improve our apps.
You seem to have a lot of opinions about my apps from your own unfortunate experiences. Hubris is a word that brings to mind.
You do realize that “I have logs from my deployed desktop apps” only makes things sound worse?
And if it’s professional environment - that creates _powerful_ biases. Essentially, I’ve seen apps with developers acting _exactly_ like you, up to and including the line about logs. You might be the one exception - but somehow I doubt it, and I certainly won’t trust your word on it. Automatically logging desktop activity makes me trust you less, if anything. In particular, I don’t believe you prioritize issues the way your users do. That would, literally, be a first.
I haven't said that the JVM would be corrupt by NPE. But C program state would also not be, and you could do the same handling as in Java.
But if your code invoked an NPE or bounds check, then it means either the algorithm is incorrect or the data it processes are already corrupt by incorrect processing earlier. Continuing in such situation increases the risk, because you don't have any guarantees which parts of the application state might have been already corrupted by the bug. I've seen many many times an NPE was a result of a shared data corruption caused by a data race. JVM does not guarantee thread state isolation, so one bug can break the state of all threads.
> Log it and handle another request
That gives you only a false sense of safety. A Java app (and any other app) may die for many other reasons you can't handle. You need to be prepared for that anyway if you want a reliable service. But if you are prepared for that, and your server can restart in 0.1 second, you don't win anything by recovering from NPEs.
Well, then consider deterministic failing a plus, as your C program may just continue along silently, with corrupted application state, and you might never learn of the problem (you are not likely to meet truly nullptr null pointers, they are just likely uninitialized, where all bets are off whether they are valid addresses)
Being able to handle it correctly is just an additional benefit.
Ok, but now you're speaking about a different thing - memory safety. And that is an obvious advantage of Java over C. But C is a very low bar to compare to. There are dozens of other languages which are memory safe and some of them offer stronger guarantees than Java.
Your Java program may continue along silently, with corrupted application state, because it invoked a race condition, and you might never learn of the problem.
But even race conditions are well-defined in Java: you can’t get so called out-of-thin-air values. Some updates may not be visible from another thread, and of course dead/live locks are on the table (but they are everywhere, even the actor model doesn’t solve those).
The possibility of these very rare events does not change my mind about best practice for many kinds of deployed applications. I have production apps running right now using this pattern. The users would not be served better by stopping the whole app. We do capture the logs and fix the bugs encountered.
Dereferencing null is undefined behaviour in C. Probably it could be handled with platform-specific code, but handling it correctly not the easiest thing to do. Other than dereferencing null, there're so many ways to accidentally blow up C code and something like reading uninitialised memory is truly undefined behaviour which can't be worked around.
Java does not have undefined behaviour at all. Dereferencing null would throw NPE which is ordinary exception, completely fine to handle or suppress or whatever. There's no concept of uninitialised memory. The only sources of undefined behaviour in Java is calling native code or using unsafe methods which are very rare and usually located in well tested library code.
Even stack overflowing is defined behaviour and you can easily recover from it.
In theory, yes, but when you run low on memory (heap, PermGen space, whatever), you can run into unusual situations. You may have a bunch of threads spinning with exceptions, continually retrying, making no progress. You'll have no choice but to kill -9 your process and restart.
Loom does a small fraction of what the Erlang VM is capable of doing.
For one, the Erlang VM has built-in preemptive green thread scheduling, which means it can suspend your green thread at ANY instruction, not just when an IO call is in progress.
Loom is a step in the right direction, but Erlang is in its own universe for what it was designed for.
It will take more than green threads to be equivalent to Erlang. Erlang's concurrency model depends upon a completely non-shared memory model, which Java will never have.
Edit: and programming it in Java will always be more laborious than in Erlang. Sure, you could put Erlang on the JVM (as the Loom folks want to do) but then is Java really the winner here, or did Oracle just make an alternative BEAM?
Surely having a shared memory model increases the design space? Or is this about memory management performance?
A benchmark should settle things in that case. Maybe it will be a draw! Erlang's almost-no-op deallocation vs Java's world-class JIT. Some applications are bound to be better suited to one specific side. I guess we'll see; but it cannot be denied that with Loom Java started to compete on a non-void part of Erlang's land.
Absolutely. And I'm not particularly partisan in this: I like Erlang ( the language in general as well as the concurrency model) and if Java moves closer to that then all the better.
While interesting, I'm not sure how that will translate into practice. How easy is it to hire up an organization of Erlang developers, versus, Java developers? I suspect Java's features as compared to Erlang will be good enough, that the specific use-cases enabled by knowledgeable Erlang developers will be non-competitive in the general developer labor market, but I am open to being wrong/educated.
there have been a handful of infrastructure projects in java: kafka and cassandra come to mind immediately, but there are certainly others.
i think what has always sort of steered me away has been a few things:
1) i've long been interested in snappy, interactive and realtime local and server things. and the jvm can do this stuff, but usually it's better suited for server side use where it can be warmed up appropriately. this may also just be a personal dogma that i need to get over.
2) every language and runtime has its quirks where you have to breakdown the illusions of the language itself and understand the internals to get the best performance, but i feel like java is the worst instance of this, where the tricks that people go through in order to control the gc seem excessive.
3) i like simple and concise programs where i feel like the java ecosystem encourages code sprawl. hello world has so much weird stuff and a serious java project has thousands of small source files where computer aided programming (ie. a proper java ide) is not really optional. i find this not only to be unergonomic, but also challenging for being able to quickly understand what a foreign codebase is doing in a short timeframe. maybe it's different from the perspective of a primary developer on one of these large projects though (and possibly quite pleasant).
4) for some reason, i've always liked feeling closer to the metal. even scripting languages "feel" closer because of the jvm abstraction. this is probably also a personal dogma.
> He commented that multi-threaded Java these days far outperforms C, due to the memory management and a garbage collector.
people who like java say these sorts of things all the time. i haven't seen a head to head comparison that confirms it though. (although i have not looked). regardless my understanding is that once a jvm is warmed up, it can be quite performant-- and that it has opportunities for live optimization that you don't get in a classic c/c++ runtime.
i don't think c/c++ will be going away as long as operating systems are still written in c. a java operating system seems... strange to me. but that said, i think java has proven itself in terms of high performance application server software.
No doubt, Java GC is a feat of engineering. But it has to be to avoid being swamped by the lack of mechanical sympathy between the language and memory management. Pretty much everything you do generates lots of unnecessary litter, which then must be eventually collected.
Except in very narrow circumstances, I've not ever seen Java perform at the level of C or C++, especially when I/O is involved. Java typically needs 2-4x the memory and a lot more cores to do the same job. That's fine if you bought the machine and the machine is big enough, but if you're leasing an EC2, why pay more every hour?
These days, my C, C++, and Java days are behind me. We do mostly Go, and couldn't be happier. The GC and language have really good mechanical sympathy, and the bad memories of tuning Java GC are fading. We pay a small tax in terms of CPU and memory over using C, but not remotely like what we saw with Java. And yes, we built prototypes of critical use cases in C just to see how 'bad' Go would be, and were very pleasantly surprised that Go compared very well indeed.
Reading your message brought flashbacks of 2006, when C and C++ programmers were politely but passionately explaining to Java programmers why the Java ecosystem was a hopeless pile of bloat.
I would have thought that in 2022 the failure of Java to conquer any kind of market on the most popular platforms and 2nd most popular or any kind of popular plus the rise of languages like Swift, Go or Rust would have made that point perfectly if not painfully clear. Even in Android, Java’s big success story, both the VM and the language were either outright replaced or made legacy.
I suppose it’s a matter of perspective, but I like to think of the server not as Java’s last kingdom, but more as Java’s last refuge before being finally banished.
Note that I don’t dislike Java. I’ve used it successfully on Android and it helped us achieve our goals of writing a passably performant application. Neither do I think it’s a bad technology, like many would argue. It is very uninspiring and bureaucratic though…
I don’t know, other than not having the same hype as “back then”, Java does better than ever. It may not have stuck its original target, but it is bigger than anything has ever been on the server side, with all Fortune 500 companies having heavy investment in the platform.
But what kind of C program is he comparing against? Single threaded ones? Not heavily optimized ones? Optimized concurrent C programs that use locks rather than lock-free data structures? Or heavily optmized C programs with lock-free data structure? It is unclear to me.
Also, wondering how much of that overhead in the C program is due to malloc (can be overcome by memory arena)
you really dont need redis if you have java. I see zero reasons to offload my datastructures via TCP, instead of have them locally. If need be, replicated them.
Usually you need redis to scale up horizontally, meaning with multiple servers/processes, and still keep consistency in your data. In general redis is a very useful tool that provides various features, even pub sub.
well, trivially - really. Machines can communicate via TCP and UDP, pretty much the same way you communicate with a web server - but more efficiently (than http). There are tons of protocols/frameworks for replicated and distributed maps which are significantly more effective than redis, esp when hitting the local one.
Writing one such yourself, is sort of, rite of passage.
While redis is well understood and (relatively) easy to use - it's just a TCP offloaded map (and few other datastructures), all well implemented and with non-blocking IO but nothing groundbreaking. Heck, it's even single threaded by design - no vertical scaling, and no L3 cache communication.
Java does not (yet) have green threads, which limits the number of connections you can serve per machine. You can go to Scala though (it has them) and still stay on the JVM.
I assure you that people writing C++ on the server know how to use lockfree structures and optimized allocators. Using something with a garbage collector when you have access to all that is madness.
Comparing Java with C is about as strawman-ny as it gets, you're literally picking on one of the oldest and dumbest languages in production usage. "The PDP-7 assembler that thinks itself a programming language", in the hilarious words of one wise guy.
The problem Java experiences (not 'experienced') is that it isn't the 1990s anymore. People aren't easily hyped, not smart people at least. Programming language research, already way ahead of Java in 1995, moved far ahead some more. The internet and open source and free online education and social media means smart language designer(s) can now begin a baby project and put it on github and wait their luck for a corporation or organization that will support it, and they can wait a decade+ on hobby mode till the luck comes. Java's competition isn't C, I highly doubt it ever was, it's dozens of far superior programming languages that were born after 1995, and some of those that were born before but weren't very populer because internet and open source barely existed in the 1990s and they weren't made by a corporation.
Even if you keep overfitting the requirements function time and time again to gradually approach java (by adding "vast libraries" and "performance" to it, although neither of those things are specific to Java), you will eventually reach a very narrow niche with Java and Kotlin squarely in the middle, I know which one I'm going to choose if I'm willing to maintain my sanity and not drown in 'paper-work programming' that is the developing experience with Java. It's as easy as opening a source file in an IDE and naming it with a '.kt' extension rather than '.java', I'm in love with it.
Companies use Java extensively ? So it's a COBOL then: Old, everywhere, infrastructure-critical, but an utter failure as a programming language in every way that is not "runs my old program I'm too afraid to re-write". This is forgivable in the case of COBOL because it's a literal proto-language, one of the earliest of the species. It's not forgivable in the case of a language designed when Smalltalk and ML and Haskell were around.
The history of Java is the story of a corporation stumbling upon the idea of a VM (1960s stuff) and deciding that it's so good that they need something like this under their name right now, and not giving the slightest shit about the design of the language that sits on top of said VM. And they were right, the idea of a VM is really so fucking good that Java's aweful design was tolerated, until somebody relized running on the JVM no more means writing Java than running on x86 means writing x86 assembly, and wrote the first non-Java JVM language. And because VMs are so fucking beautiful, you get all Java code for free.
>doesn't copy everything Java does VM + GC
Come on, write those 2 words on any half-decent academic research repository and you will get hits from the fucking 1960s. Java is younger than Lisp, Smalltalk, Self, Haskell, Python and the same age as Ruby. All of those languages do VMs and GCs. Java's hotspot is taken from Self, that's documented stuff. And "Copying" isn't copying if it's a better implementation of the interface, JSON didn't "copy" XML, they reimplemented its interface better.
I've used Java my entire career and i'm fortunate for it. I appreciate how readable the code is (unlike my experience with Erlang, Haskell, etc), typically i don't have foundational issues in the web framework (once again had some with Haskell). Everything works, if I need to do low latency, there's great libraries and resources, if i need to build a simple internal tool, it can be done effortlessly.
I think python is the same way, the ecosystem is so rich that you can really do anything you want (until you get into low latency).
Agree with all of that except readability. Java has some language deficiencies (no first-class functions, "streams" missing for almost twenty years and added too late to be done elegantly) and some cultural norms (mutable everything, overuse of inheritance) that make it a chore to read most of the Java code you encounter in the wild, including the source code of the libraries you depend on. Figuring out the behavior of one method routinely means taking a tour through implementation details in three or four superclasses, factories, factory factories, and all the other Java clichés that are legendary but also absolutely real.
You can do better in your own code, but you still have exposure to the code in the library ecosystem.
Worth it, though. It really does seem like there's a Java library for everything.
Unfortunately, records are only guaranteed to be 'shallowly' immutable. It would be great if future Java versions provided a straightforward way to enforce immutability.
As far as I know, Java has final, which means that particular reference can't be re-assigned, but the object referred to remains mutable. You have to resort to e.g. having separate immutable and mutable interfaces or whatever to restrict a someone from mutating your object.
If you want an immutable data class more than one level deep, I don't know if there's a convenient way to do that like there is with const in C++ (or the default behaviour in Rust).
But I'm not a Java programmer, I haven't really kept up with the language. Happy to be proven wrong.
You are right, though I seldom find it a problem in practice. Also, OOP sort of makes immutability hard to define (e.g. is a getter with an internal counter of accesses immutable? In a way, it is. Also, Rust’s internal mutability pattern is similar).
Nonetheless, recently more and more standard classes are made deliberately immutable, and there was a proposal for frozen arrays as well (not sure on their status).
> You are right, though I seldom find it a problem in practice.
Although it's easy to to tell people that mutation is confusing and to avoid it, enforcing that is much easier if the compiler is on your side and will prevent mutation with const.
I've encountered unnecessary mutation (introducing implicit assumptions on the order of calls, and making things more confusing) constantly in both Java and C++, but enforcing const in C++ cuts down on that. Or at least, it forces a const_cast which I won't approve without a really good reason.
You're right about Rust of course, internal mutability is possible and maybe even common with RefCell, but culturally it seems like that's avoided. On the other hand, mutability is extremely common in Java.
Maybe I'm just traumatized from some of the horrific code heavily using mutation I've seen over the years.
I haven't programmed in Java since 2005, so forgive me. But isn't a lambda automatically converted to an object of the necessary single-method type? That feels a little magical and suggests that there really isn't such a thing as a genuine first-class function in Java, as there is no way to define a function, and no universal type to assign to such a thing.
Well, the standard library does have some function types, like Function<A,B>, Supplier<A>, etc. You can just initialize a variable of these types and pass them around. I don’t see how are these not genuine first-class entities. The lambda syntax is also quite pleasant.
(Also, behind the scenes they often compile to static functions and called through the invokedynamic instruction)
enum Expr:
case INT(value: Int)
case ADD(left: Expr, right: Expr)
case MULT(left, Expr, right: Expr)
def eval(e: Expr): Int = e match
case INT(value) => value
case ADD(l, r) => eval(l) + eval(r)
case MULT(l, r) => eval(l) + eval(r)
This "match" syntax is the example given in the Scala docs for Pattern Matching:
This is a matter of preference, I think. As a non-erlang practicioner I find Erlang to be far more readable than Java (which is good, because IMO Erlang authors have a habit of writing spaghetti code that's poorly organized). As a non-practicioner at some point in Java you run into annotations? Pragmas? and you stop being confident that your mental model of the code is correct. When I run into those I throw my hands up and start cursing.
I believe it's unfortunate people judge the JVM ecosystem by Java. Scala and Kotlin are so much more attractive options if you write code for a living. Between this and the devolution from maven to gradle it's not a surprise junior developers are JVM-shy.
I see companies downshifting to unmaintainable toys such as Python even in data engineering circles. It's really odd that mobile developers with Kotlin (and front-end ones with Typescript) are getting ahead of backend ones in adoption of modern languages.
Once Loom and Valhalla get merged to an LTS the remaining vestiges of bad old Java will have been gone. I really hope Graal goes mainstream too. That will hopefully blow out of the water the golangs of the world. But those are platform-level improvements any JVM language will benefit from.
Maven is declarative. Once you know how to build and deploy one repository you can do it in any other. Including projects started a decade ago. Five commands is all you need to use it. It's trivial to have a monorepo with the classical 3-tier pom file structure.
It takes a 2/3-of-your-screen plugin configuration to build a Scala project. And you can simply copy that configuration without even thinking about it to another service.
I believe that making the "<dependency>" declaration a one-liner would fix 80% of what's wrong with maven :)
Every single Gradle project I have worked with has its own structure. Which happens even across repositories owned by the same team. There are DSL flavors (Groovy and Kotlin), both are actually used and differ slightly. The wrapper. Its storage is based on Ivy, not Maven so you double the number of Internet replicas on your HDD. But it's still better than SBT ;)
Java is pretty fast. Second most popular language in HFT. Can get it to a few tens of micros. Not as fast as C++ at sub 5 micros. So good enough for many latency sensitive apps.
Try sub 5 nanos. I was curious awhile ago at how fast C++ hash set lookup was compared to C#, and it consistently performed a lookup at 1 nanosecond. I tested with up to 6GB of data and then stopped because it was taking longer to generate random data then it was to run the benchmark 10,000 times.
C++ benchmarks here[0]. It's a bit more complicated then just a pure lookup since I was pulling some code out of a larger app, but the benchmark is only measuring the lookup speed. I did the C# benchmarks with BenchmarkDotNet or something like that, I can never remember the exact name.
> and it consistently performed a lookup at 1 nanosecond.
TBH I'm skeptical that you are measuring what you think you are measuring. There are a lot of micro-benchmarking pitfalls, like dead code elimination, loop-invariant code motion, unrolling, and other issues. Unless you actually looked at the machine code coming out of the compiler, you're measuring something you don't understand. E.g. 1 nanosecond is roughly 3-6 instructions. That 100% means the hash lookup has been inlined into the benchmarking loop.
Are your hashtables mostly empty? Really small? Lots of easy hits (or easy misses)? Because the slow cases (actually looking up) are going to be hairier and may not be inlined.
Did you benchmark against Java's HashMap? Because it is also very, very, very fast for simple cases.
It looks like caching definitely skewed the results a bit. You can take a look at the linked code yourself. Worst case was still only around 80 nanoseconds which is definitely slower, but still orders of magnitude faster than "sub 5 micros".
Don't take my word for it though, you can take a look at the Robin Hood benchmarks[0]. Robin Hood unordered map is a competitive hash map that's performed much better than the STL for me in many cases. They average a 4 nanosecond lookup speed for a hash map with 2000 elements and an integer key.
> Did you benchmark against Java's HashMap?
I benchmarked against C#, which has a runtime that performs similar if not better than the JVM. The C# code was a ~~few microseconds~~ around 130 nanoseconds. Which is still very fast, but up to 100x slower. (And yes, this was after warming up the code. I used benchmark dot net[1] here.). This is a really easy benchmark to set up. If you doubt me you can write a couple of benchmarks in under an hour and compare yourself.
That's a link to the benchmark framework that I used. The C# benchmark are in a separate gist that I didn't feel like digging up. This is the C# benchmarks[0]. All the interfaces and indirection is the result of me adapting this from a separate comment. But the benchmark is just testing `HashSet.TryGetValue`.
Edit: I just re-ran the benchmarks because I didn't have the results pasted in the snippet (which I've now done so I don't keep getting this wrong haha). The C# HashSet takes around 130 nanoseconds, not microseconds. So it's not orders of magnitude slower, but it is still more than a 2x slow down and up to a 100x slow down in the case of an integer key.
Indeed. Shutoff Garbage collection completely and it can work. (And make sure your Java code creates no garbage - which is a new type of programming in and of itself)
I don't think their comment is intended to be negative really -- looks more like appreciative of the option, while cognizant of the fact that using it introduces a new challenge.
> This is because Zing uses a unique collector called C4 (Continuously Concurrent Compacting Collector) that allows pauseless garbage collection regardless of the Java heap size.
This is incorrect. Firstly, C4 triggers two types of safepoints: thread-local, and jvm-wide. The latter can easily go into the region of ~200 micros for heaps of ~32GB even when running on fast, overclocked servers.
Secondly, the design of C4 incurs performance penalty for accessing objects due to memory barriers. This impacts median latency noticeably.
You might not believe me, but ask Gil and he'll openly admit it. This article was written by someone who:
1) doesn't know how C4 works
2) doesn't analyze relevant metrics from their JVMs
Depends on the use case, but if you are working on web servers or other long lived processes the JVM is pretty close to native and can even beat native code thanks to JIT compilation.
I recently started a side new project in Java targetting GraalVM with language version 17.
Aside from Java's innate finickyness, it has been an unexpected pleasure. I think a lot of it has to do with its static typing (I typically work in dynamic languages, and it's nice knowing that if the program compiles it likely works), and how simple the language keeps its primitives.
But you need really good tooling to use it, like a powerful IDE with good autocompletion and refactor support. It is way too verbose to type everything out yourself, and the verbosity means manually refactoring takes lots of changes around the program to manifest.
The sheer amount of code out there to import is immense, there seem to be libraries for anything and everything!
So far, it seems like the time I lose to its pickiness, I gain back with IDE features, static typing, and the ease of understanding it (because it is so verbose).
I'm also not hot on how it seems to steer everything into a factory pattern, but so far I've been able to avoid that for most things.
> But you need really good tooling to use it, like a powerful IDE with good autocompletion and refactor support. It is way too verbose to type everything out yourself, and the verbosity means manually refactoring takes lots of changes around the program to manifest.
One of the ironies of Java has been that its strictness and verbosity can make it hard to develop, but that strictness also enabled the development of powerful IDEs. What feels like a hassle for a 100 line file becomes an asset for a million-line project because of the safety, discoverability, and refactoring it enables.
Yes. I worked many years in both Java and C#.
In Java every project and codebase I saw was pretty much the same.
In c# when I look at other people's projects I often feel like it's a foreign language. You can make it look like C++ with unsafe blocks. You can nake it look like ruby with dynamic variables. You can write sql-like statements.
As much as I love writing in C#, I prefer reading other people's Java.
Indeed; I haven't seen a ton of bad code in it (not saying that it's not out there, just that I haven't been exposed to much of it), but when I needed to dig into how something works, so far it has been very easy to scan and mentally parse what's going on.
My main problem with Java is that it's picky, and has some static typing, but it also has heaps and heaps of loopholes in the type system (that turn into runtime exceptions), so it's this weird compromise where everything is substandard. I've found that Go "feels" a lot more like a dynamically typed language, but still has the good IDE support that you're talking about.
If you want to see how much progress there has been in production-quality statically typed languages, write some multithreaded code in Rust. In addition to being memory safe without a GC, the compiler also confirms that your code is threadsafe.
Both those guarantees can be violated using the "unsafe" keyword; Java has similar mechanisms that break memory safety. Java doesn't provide meaningful thread safety, in the same way that C's malloc/free don't provide meaningful memory safety -- it's possible to write thread- and memory-safe programs in both languages, but the Java compiler doesn't really help out much with thread safety, just like the C compiler doesn't typically check for use-after-free, etc. Go's thread safety semantics are closer to Java's than Rust's (though multithreaded programming in Go is more ergonomic than in the other two languages).
I've read enough critiques of Rust to be put off from trying it out at this time. And there's a lot of things I don't like about Go. (Zig is interesting though)
Unfortunately Rust doesn't have the kind of library support that Java does, and I don't really want to roll my own on some things (the side project will never get done if I get lost in the weeds)
For this project the choice fell between Java and C#, and Java won because I was familiar with the library code that does what I want to do.
Besides, Java has Swing, which is about the only cross-platform GUI toolkit I can stand to work with (GTK as a close second)
> but it also has heaps and heaps of loopholes in the type system (that turn into runtime exceptions)
I've yet to run into this in places where I don't really expect it. Do you have some examples of where this becomes a problem? I wrote a bunch of reflection code that triggered a lot of RuntimeErrors, but that was foreseeable as its reflection, the whole point is to figure out types and whatnot during runtime. And at that point, I just fall back to how I write code in dynamic languages.
> In addition to being memory safe without a GC, the compiler also confirms that your code is threadsafe.
No, not at all. Rust verifies that your code has no data-races. That is an absolutely tiny subset of all race conditions, that are simply not verifiable statically.
Data races, in my experience, are not a particularly small subset of practical race conditions, particularly in Rust which tends to shy away from unnecessary mutable aliasing. YMMV of course.
I absolutely love what rust does, and in practice in “ordinary programs” data races probably come up more often than other kinds of race conditions, I just wanted to express that one should still very much pay attention to concurrent code.
I have, it's a nice language but I didn't like the mental tax of translating Java code to Kotlin in my head whenever I had to read up on how to do something
I do intend to dig into it a bit more once I feel like I have mastered Java
I hear you, and had to do the same for a while. Eventually your brain migrates over and it becomes second nature. The ability to copy Java code and paste it as Kotlin helps tremendously, especially in the beginning.
> I have, it's a nice language but I didn't like the mental tax of translating Java code to Kotlin in my head whenever I had to read up on how to do something
Of course, when you get into slightly different idioms or design pattern implementations, things might require a bit more mental effort and manual work.
I've heard from a few corners that Kotlin is "Java but better", but I've also heard that the tooling is pretty lacking if you aren't using JetBrains stuff. True?
Agreed. I hate digging through a mountain of annotations to try and decipher what additional side effects some method might have (additional to any existing side effects). I have never seen a codebase where annotations were some necessary compromise; I feel like whatever crucial parameters is being passed through an annotation could have just as easily been put into the arguments and made a part of the method signature like they're supposed to be. Annotations were a mistake.
I don’t know, I think it’s closer to some LISP magic macro than Lispers would like to admit. They are extremely powerful, and thus can be responsible for some very ugly code, but when used responsible, they are huge productivity wins.
Well it is just another design pattern, i love annotations but it might be better even without it, Spring boot takes care of most of the things and it only needs very minimal annotation which makes sense.
The time I used vert.x in my day job was by far the best Java experience in my career. Completely different language. And the vert.x maintainers have been all around great wrt to responding to issues and/or accepting contributions. Great framework. It has it's quirks and limitations but overall I absolutely loved working with it.
That's exactly what was meant. With Loom Java will hopefully not have the same function split as languages like JavaScript (or C# for that matter) where you have to add lots of async/await everywhere; and instead will have something like Go (where everything is async).
Async stuff in JavaScript light years ahead of Java's Future madness. Loom might help but I'm not optimistic about it. For example Spring already kind of deprecated blocking http web client and new reactive WebClient is terrible. Will they create yet another BlockingWebClient? No they'll ask you to call `block` everywhere and write reactive nonsense filters if you need to enhance it.
That may be true, but JavaScript forces you to bisect your libraries (and functions) into Those that Understand Async and Those That Don't [0]. There appears to be no path forward if you want to avoid that.
It's very difficult to write generic, reusable higher-order code that shouldn't care if it's doing a sync or async operation. Java at least is building a foundation in the right direction.
If you mean that you can't write `map` which would work for sync and async functions with the same code, that's extremely rare problem IMO. If you really need that and don't want to write two versions of code, you can wrap blocking code with promises and use promise API from now on.
It's not at all a rare problem if you work in codebases involving async code. Many `Promise` APIs exist solely to work around that wart. Higher-order functions are the bread and butter of JS, and increasingly Java (especially modern Java).
But async stuff is a must in JS due to it being single-threaded (yeah I do know about webworkers).
Just spawning a thread with a scope which will fork them at the end is just better from every conceivable way. Easier to grasp, easier to maintain, easier to debug - all of which are quite important when concurrency is at the table.
I don't understand the part about go, does that mean go doesn't require async but gives you the functionality of async? (I never tried go)
The problem with async is that we can separate when to start a task and when to ask for its result. The compiler can just add await everywhere an async function is called, it is trivial, but you don't get the flexibility of async. If everything is treated as async, you will need to await everything (perhaps some syntactic sugar to allow for immediate await)
> If everything is treated as async, you will need to await everything
In go you work rather differently. You let tasks go off and do their thing, and provide a channel to communicate. Pulling a response from the channel is the 'await'. A good part of go's magic is that these tasks - goroutines - don't result in large amount of blocked threads.
Java will soon have the building blocks of something rather similar to goroutines.
OK I see, but I think these two approaches address different problems? The approach used by go is more powerful than async but also more verbose. The implementation is also different, async can be implemented with a generator but goroutine can't.
I was doing live coding in an interview yesterday and the interviewer said "you have a problem in your code, you put var, and this is Java". I had to explain that the language has modernized quite a bit.
I don't really write in Java these days, but when I did the biggest pain points for me were:
- IDE: IntelliJ and before that, Eclipse, were painfully slow to use. Even now occasionally if I have to launch Android Studio I have to wait for Gradle and various other things. The entire IDE gets sluggish while it's doing indexing and all sorts of weirdness. vim integration was quite poor at the time, I don't know if things have changed since.
- Verbosity: It always feels like I am writing boilerplate and long names. I remember trying to write something with websockets and no matter what library I picked there was a ton of boilerplate to write for just connecting to a socket and sending a message.
If given a choice I'd much rather write in Python, C#, Ruby or even Typescript. The language feels very dated - or perhaps there are newer ways to do things that I'm totally unaware of.
IDE: Borland JBuilder was a king. I am still using it
Websockets: you can write your own in around 400 lines of code. I did it. The specification/RFC is really simple. Just a few bits above TCP
Trouble with java is that it does not scale up or down in terms of ram.
Minimum RAM for a sever doing something normal over tcp is measured in gigabytes.
Big servers > 16gb get difficult to manage at runtime.
You have to scale with more VMs.
You can write useful C servers that are very small, especially if you compile with musl. And run the same code for 1000kcc (not a typo)
When you have big arrays of memory storing everything as Objects/pointers gets messy and inefficient. But any big heap is hard to manage and keep response times consistently low.
My other gripe os that "write once run anyway" is no longer close to true, since Oracle.
Mac, Linux x64 and Windows 64 are your only sane options. If you look at the compile targets list for c or rust you can see write once run anywhere working on a lot more cpus. True, you might have Arch specific code but it works, and most Java does not port from Linux container to Windows for example.
A statically compiled Binary is often easier to move across systems, because a java app is rarely a single jar. It's usually >5gb of app specific jvm and libs and config files.
> Trouble with java is that it does not scale up or down in terms of ram.
> When you have big arrays of memory storing everything as Objects/pointers gets messy and inefficient. But any big heap is hard to manage and keep response times consistently low.
Java can handle very large, TB-sized heaps (under normal circunstances, about 52TB). But when you're dealing with TB-sized heaps, anything is slow - including programs not made in Java.
> You can write useful C servers that are very small, especially if you compile with musl. And run the same code for 1000kcc (not a typo)
Anyone can build stuff with musl, but musl is in general a lot less optimized that GNU glib, so I guess it really only makes sense on resource-constrained environments - not those where you're handling TBs of heap.
> A statically compiled Binary is often easier to move across systems
I guess you're talking about jart/cosmopolitan here, not your average musl application - a statically compiled Binary usually needs to be recompiled to work on other systems.
> because a java app is rarely a single jar.
Uberjars are very common, the norm those days.
> It's usually >5gb of app specific jvm and libs and config files.
On my experience even if you "accidentally" bundled the whole Java SDK together with your app, you're not getting over 250MB for Java.
Unless you're counting the whole OS, in that case, even the musl application isn't that small anyways.
When I say move across systems, I mean from vm to vm not from lin to win.
While a .class file will technically run on any jvm. A java application is usually more complicated and Uber jars don't work if you have stuff in meta-inf e.g. Spring.
I am primarily a java dev and I have no tools or own code that run with java -jar some.jar.
> While a .class file will technically run on any jvm.
Not really, JDK 11 and further finally started removing deprecated implementation details, so you can't really run any class on any JVM - you ideally lockstep development and production JDK versions.
> A java application is usually more complicated and Uber jars don't work if you have stuff in meta-inf e.g. Spring.
This is more about the java application being complicated, and messing up with JAR metadata, not about stuff "not working"; in my experience, uber-jars built by Spring Boot "just work".
> I am primarily a java dev and I have no tools or own code that run with java -jar some.jar.
Most of my deployments were fleets of Spring Boot based services deployed with Containers with `RUN java -jar /app/my-project-name-1.0-RELEASE.jar`
> Minimum RAM for a sever doing something normal over tcp is measured in gigabytes.
Some of my Java services run with around 256-512 MB of RAM per instance, in Spring Boot containers (though Quarkus and Dropwizard can be even more conservative outside of enterprise bloat situations).
That said, I'm inclined to agree in general, as I've seen monolithic Spring applications that refuse to run with anything less than 2-4 GB of RAM given to them.
Perhaps even worse yet, there's no way for you to properly cap the resource usage within containers so you'd end up with more aggressive GC inside of containers but without OOM issues, when they're given a memory limit. -Xmx regularly gets exceeded because that's only a part of the equation and the creators of JVM didn't really think of a case where you should be able to give the whole thing a number of MB/GB it's allowed to use, which would never be exceeded, whatever it takes.
> My other gripe os that "write once run anyway" is no longer close to true, since Oracle. Mac, Linux x64 and Windows 64 are your only sane options.
Agreed, then again with most "business" software running in x86 OCI containers seems like the only sane option. Though I guess it depends on the environment that people have grown accustomed to and how they've been burnt in the past (e.g. I'd never want to use package managers or worse yet, extract random tar/zip archives for "business" software, never install Tomcat on the system directly etc., only use containers that are 1:1 the same in all environments where they're run).
Of course, web dev is just a part of the greater picture, so there's lots of nuance there as well. Personally, SIM cards running Java seems like insanity to me, though: https://en.wikipedia.org/wiki/Java_Card
Then again, I'd say the same about Python implementations that are geared towards embedded setups, personally even Go seems like a better fit if someone didn't want to use C/C++/Rust, though platform support would still be a relevant question.
I think Java gets a lot of flak because it is so popular within enterprises. And we all know what kind of code gets written in non-IT corporates. Add to that the fact that Java allows you to do a lot means a lot of crazy code was conjured up over the years which then had to be maintained and developed.
If you apply some discipline, I think Java is a great language.
Being taught intro Java in high school (mid 2000s for me) was excruciatingly boring and caused me to write off majoring in computer science or working as a programmer.
Today I'm a software engineer with experience in JavaScript, Ruby, Python, Elixir... maybe it's time for me to give Java another try.
>Today I'm a software engineer with experience in JavaScript, Ruby, Python, Elixir... maybe it's time for me to give Java another try.
Nah. If you have to use it, Java's really not that bad. But I would never choose it for a personal project. Its' strengths are in all of the concerns that come with enterprise development.
Significant performance improvements, several new GCs, stream API, type inference, records, pattern matching, switch expressions, lambdas, default interface methods, 6 month release cadence, try-with-resources, GraalVM, many new APIs (like `java.time`), JFR open sourced, virtual threads (in preview), value types (in the works), improved native interop/FFI (in the works), etc. are more than just a "slight improvement".
> Being taught intro Java in high school (mid 2000s for me) was excruciatingly boring and caused me to write off majoring in computer science or working as a programmer.
You might want to start with C. C is a lot more enjoyable to learn and use, and once you know C you basically already know Java. (Assuming you understand OO concepts in general, anyway)
Sun Microsystems had a link to my blog on the Java home page for about a year. I had attended the first Java World Tour, blogged about it, and for 15 years I was the first “hit” searching for “Java consultant”. Thank you Java.
All that said, I don’t use Java much anymore, preferring to use Clojure when I need the rich JVM ecosystem. I do follow new Java language features and usually try them.
I spent almost all of my career writing Ruby before I ended up at a company that required me to write mostly Kotlin (a bit of scala here and there too). After a few years of Kotlin I don't want to use anything else. There's a bit more ceremony to getting things set up but the experience of writing Kotlin with Intellij IDEA has been so wonderful I'm happy to keep doing it.
Funny how the revival of Java is similar to the revival of .NET. After some stale years, both languages refined themselves and are back in the top competition (.NET in the VM space, Java in the language space).
It really shows which languages can re-invent themselves (Java, .NET, PHP, ..) while some fail (Fortran, Basic, Pascal, Perl, ..). Not sure where JS is ;). Python seems to have survived the 2/3 schism by now.
I guess both JavaScript and Python reinvented themselves through gradual typing. It makes Python quite a different language and JavaScript literally a different language.
My most recent small (1k LoC) Python project is “fully typed” and therefore practically type safe (not strictly speaking though). Lots of the large libraries are typed as well, which is important.
For Python, work around the GIL might be the next evolutionary step, that time in the name of performance.
I agree. It seems like the road for dynamic languages goes through optional typing. And it is logical because their weak spot right now are huge systems.
I have to add that JavaScript would greatly benefit from a solid base class library
I wrote a lot of small Java programs when I was in school, and Eclipse felt like being on developer steroids compared to using an IDE with a dynamic language. My software just worked and was pretty fast. I have mostly worked with Python since I became a pro developer and I often wish it was a more "boring" language.
I think some developers took "write once, run anywhere" as a challenge, which is why I still have to keep virtual machines with ancient Java versions around to configure and use certain remote IP-based KVMs, certain IPMI functions, certain older fibre channel switches, certain poorly thought out IP cameras, and so on.
I might be wrong but I think they stopped working on major new versions for a decade. There seems to be Java 6 or 1.6 or whatever for a really long time. Like Netscape's version 4.
To someone who has not written Java since High School in the '00s, I find the ecosystem a little confusing.
My recent experience was this: I have a Windows box in my basement that hosts a Spigot Minecraft server for some friends and their kids. This box also hosts my UniFi controller software.
The UniFi controller software requires Java 8, which is what Oracle offers for download on their website.
Spigot usually requires the latest version, at the time I ran into this problem it was 17. You have to go out of your way and actually download the whole JDK for any Java version newer than 8.
So I download and install JDK 17, which replaces the JRE 8 I already had running UniFi. Spigot works, and the UniFi controller appears to start up and function. However I quickly notice that the UniFi app is behaving erratically, and after some troubleshooting and googling, I learn that Java 17 is in fact not backwards compatible with Java 8, so both of them need to be installed.
Of course, Java doesn't make installing multiple versions side by side easy. It's doable, but even then they built no mechanism for a jar file to specify which runtime it needs and automatically run with it. So I had to write my own startup scripts for these apps calling the specific runtime they required.
None of this was particularly onerous to figure out and deal with, but your average user is going to get stuck and feel frustrated. And it left a bit of a sour taste in my mouth because .NET has done a pretty good job of avoiding exactly these kinds of problems.
Well, first of all there is indeed no JRE anymore, one is not supposed to install anything. The java program is supposed to be packaged up with a lean runtime executable that only contains the necessary parts, as this has been the trend for a long time.
But in practice there are indeed a lot of jar files everywhere, that have to be run through a “JRE”. It sounds a lot like a package manager program to be honest (taken care by nix for example). But otherwise, I recommend sdkman to manage multiple java versions.
1.6 and 1.8 and the like were major releases. While LTS doesn’t have a well-defined meaning for OpenJDK, in general 11+6*n are the versions that are considered LTS (due to other vendors providing paid support for those).
Let’s not read more into arbitrary version numbers.
But that is not the java naming scheme. Java stared using normal major version numbers back in the Java 5 days. They've developed major releases continuously for decades.
Java has been very good at backwards compatibility for me except when they removed some unsupported internal classes from the 1.9 JDK, but there existed a workaround for still using it.
I don't worry about security problems in the JDK or major libraries any more than anything else. I'm not sure where your security concerns are coming from.
I'm extremely happy I have not had to use it beside one liners for the last 12 years of my professional freelancer career. I still remember the times when Java people were laughing at me when I was telling them that I'm mostly a python developer.
JVM is a masterpiece.
Java improved, but legacy is there.
I now prefer and use whenever possible Kotlin, I can do more writing less code and still retaining JVM performance.
The CDI specification is what takes Java from "good" to "incredible". The dependency injection pattern makes Java a hybrid functional language, where all the state can be stored in the CDI container. This eliminates a whole class of bugs and simplifies a codebase allowing for pervasive use of composition.
Stuff like Microprofile, Quarkus, ActiveMq, Tomcat, and even JakartaEE are gravy on the cake.
Here's a typical example of the pain of Java language evolution. As an ex-Perl developer and frequent user of Ruby and Python I am accustomed to being able to write a regex without having to escape metacharacters such as \d (digit) and \w (word character) as is necessary in Java where regular expressions are merely strings fed into the Pattern.compile() method. So imagine my elation when I discovered that raw string literals were being previewed in Java 13. Then imagine my horror when I discovered that regex metacharacters somehow missed the party and STILL had to be escaped inside raw string literals. WTF!?
The JVM is pretty great. I could certainly nitpick things, but it's pretty great. Java is pretty decent. However, I would say that Java lost a lot of time and even now there are some decently rough edges.
Java didn't evolve as a language for a while and that left the door open to other languages and other non-JVM ecosystems a lot. As the article notes, Java 8 was a breath of fresh air, but it was minimal in some ways. Lambdas and streams were great additions to the language. However, Java 8 came out in 2014. That's quite late to the game, in my opinion.
C# is probably the closest competing language/ecosystem (albeit constrained to Microsoft for much of its life). In 2007 (7 years earlier) C# 3.0 had lambdas, the `var` keyword, properties, object initializers, the equivalent of streams (and really better), nullable types for value types (like int), etc. In some ways, Java has caught up - and C# lost a lot of time being constrained to the Microsoft ecosystem. However, in other ways it hasn't.
I just want a POJO: frankly, this has been a problem that Java hasn't solved and it's been well over a decade where everyone has known it's a problem. No, records don't solve it. In Kotlin, I can make a data class and it's easy. In Scala, I think they're case classes. In C# I have properties where I can say: `class Person { string Name { get; set; } }`. I can see that it's just a boring property without having to look at method bodies. If there's something special, that get or set can have a body to do stuff and it becomes really clear that it's something special. Getters and setters are a wonderful way to set traps for others on your team or for yourself a year later because you look at a class with 15 items and it's going to have 90 lines of getters/setters + another 30 lines of an empty line between each method. You look and just decide "yea, I'm sure this doesn't have special behavior" and go about your business just to get bitten later.
I want to be able to instantiate data easily: With Java, I can do `var person = new Person(); person.setName("Johnny");`, but that becomes pretty tedious and error-prone when instantiating a large object. With records you have a constructor, but then you're dealing with positional arguments and it's hard to understand. When reading the code, you don't necessarily know what each of the inputs means. Maybe your IDE puts the argument names in. When filling it out, I've found IDEs to only be somewhat helpful. With C#, I can do `new Person { }` and then hit the suggestion key combo inside the brackets in my IDE and it'll offer to fill out all the properties so that I get something like:
new Person {
Name = "",
Age = 0,
Address = ""
}
That means I don't forget about fields (as can happen if you're just doing `person.setX()` all the time). It's easy to see what is what when reading it. I can delete fields I don't want to initialize at the time. Yes, maybe immutable objects are the One True Way, but C# lets me choose (I can label properties with an initializer `init` rather than a setter `set` and then they're immutable).
Kotlin offers stuff like this too because it's really useful toward creating code that's easy to create and maintain. Go also lets you initialize structs in a similar fashion.
Java has come back to us a decade or more late with records. They're not bad, but they're only offering one thing. They don't cover what C#, Kotlin, Go, and other languages have offered for so long.
The annoying thing about Java is that it doesn't feel pragmatic a lot of the time. It feels like the language hates stealing ideas from others. It's Java: people steal ideas from Java, not the other way around. People do crazy things just to get POJOs including Immutables (http://immutables.github.io), AutoValue (https://github.com/google/auto/), Lombok (https://projectlombok.org), Joda Beans (https://www.joda.org/joda-beans/), and maybe more. They generate lots of code at compile time or do funky runtime stuff.
It just feels like Java misses the pragmatic stuff and still kinda doesn't want to handle that. I feel a bit silly harping on things like POJOs and setting data on a new object, but that's a big part of day-to-day stuff and it definitely pushes users away from Java towards languages that seem "better" simply because they don't have Java's oddly strong attachment to not offering simple value objects. Yes, again, records do something - but it feels like Java ignored how people are using Kotlin, Go, C#, and more and didn't go for something that would have been as widely applicable and pragmatic as it could have been.
Java has a lot of great stuff like great GCs (yes), lots of cool research, great performance, and Project Loom is really exciting. I just wish the language would lean a little more practical.
I really like the C# object initializer syntax, and it looks like the next version might get rid of the last problem I had with it. Right now it does not play nice with nullable types, if a property is declared non-nullable you have to use a constructor or some ugly trick to circumvent the warnings. As far as I understand this will be fixed in .NET 7 (not sure if the decision is final, though) and you can get the full benefit of non-nullable types when using object initializer syntax.
In general there has been quite some effort to allow a programming style in C# with less of the ceremony and verbosity that is often associated with C# and Java. And to me this does make working with C# more pleasant.
If you perhaps happen to know or have a pointer: how would that work in C#? A field not initialised will be null, and if it’s non-nullable… what’s its value going to be?
The check would be moved to the object initialization, so the compiler would tell you if you use the flexible object initialization syntax and left some non-nullable properties uninitialized.
The situation regarding POJOs are annoying, but I believe Java maintainers are fighting the good fight: introducing them in the language would hardcode this arguably bad pattern indefinitely. One might say that with the amount of code written this way, properties are already here to say, but I personally find Java’s stance on feature addition much more sane than C# cool and quick, buy maybe too fast one.
I like to believe that records with the upcoming ‘withers’ will be an adequate answer (if a bit too late) to this whole question.
C# interfaces with methods, properties, and events is just so nice from a readability standpoint.
I know we like to pretend that Java now has first-class functions since Java 8, but without easier to use functional signatures they're just too much of a chore. Who the heck wants to write a new interface in a separate file to do this.
I'm grateful for the JVM and think it's probably better than the CLR for targeting a high level language (as evidenced by the continued existence of Scala, Kotlin, and Groovy). But Java, even post-Loom and post-Valhalla, will never be the promised land for quick-to-read-and-grok code.
That's my point, we don't have much more than that. Even a 2-arg function is missing. You can supplement with something like Vavr or just write your own, but even then it's not pleasant to read "Function3<A, B, C, D>".
Typescript, Kotlin, and Scala all do this way better. Here's an example from Kotlin (it's an extension function, but focus on the combiner lambda):
fun <T, R> Collection<T>.fold(
initial: R,
combine: (acc: R, nextElement: T) -> R
): R {
....
}
Just nitpicking, but there is a BiFunction generic class in Java, but I understand your point. Yeah, some syntactic sugar could come in handy, though I don’t think that it would be a real productivity boost, more just an annoyance.
Anybody have experience with the JNI interop with native libs?
Is it better to implement something natively in a compiled library and link it in from Java or better to rewrite it in Java? What about lifetimes of objects - who "owns" an object - the runtime or the lib?
Unless absolutely necessary, I would say avoid using it - the JVM ecosystem is almost completely pure in terms of being written almost entirely in itself. If it is a must, then I recommend looking into the new Panama APIs that help a lot with scope, can autogenerate code from C headers, etc.
I’ve tried that and it didn’t work reliably for me. The jet brains runtime release works but it’s a bear to figure out which download to use.
Regardless, it’s ridiculous that at this point Java doesn’t have full hotswap. All the hooks are there (as is apparent from the error messages when a hotswap fails) and the dcevm is being maintained by jetbrains employees. It needs an internal champion at oracle/sun.
What should happen when you remove an existing field, remove a method already used, change its initial value, etc? You will quickly get some incorrect state by blind hot swapping, and it is not trivial to do in a mutable object graph.
Clojure (and other lisps) can do it well because their scope of changes can be really small.
Nonetheless, method hot-swap is well-defined and is implemented by OpenJDK.
Dcevm handles all those reasonably well. The current version doesn’t support changing super classes but the old version did. This is in dev mode so it doesn’t need to be perfect, just right enough most of the time.
> The biggest thing Java is missing is full hot swap
The biggest thing missing in Java is an answer for the billion-dollar mistake. Real world Java is plagued by NPEs because a lot of Java is written by low caliber programmers. Java + functional error handling would be a monumental improvement.
Can't really blame Java for that, though - if everybody standardized on, say, Haskell (or whatever we might agree is the "gold standard" for programming), the low caliber programmers would find a way to do something stupid in it, too. The only way to get around low caliber programmers is to raise the standard, but any suggestion of raising (or even setting) a standard for programming invites accusations of "gatekeeping" (a gate that really, really, really ought to be kept).
Hmm. Can't I? I know why Sun made Java. They wanted a platform to develop applications that didn't require the skill of a competent C++ programmer. They were targeting lower caliber coders.
I'm a pragmatist; yes, Java programmers would still find escapes, but they'd do it less and so the net number of flaws would be smaller. As jayd16 points out, there is a pragmatic way to deal with this; provide a compiler mode that eliminates null dereferences and rework the standard library to accommodate this. Simple and obvious. Afterwards you can throw the switch on whatever code your facing and you'll know if you're dealing with crap or not.
It doesn't fully solve the problem, but @lombok.NonNull helps a lot. It makes it clear which properties shouldn't be null, and catches NPEs closer to the source. Incidentally, lombok in general does wonders for boilerplate reduction.
Which @NonNull? There's javax.validation.constraints.NotNull, org.springframework.lang.NonNull, org.checkerframework.checker.nullness.qual.NonNull, org.jetbrains.annotations.NotNull, android.support.annotation.NonNull and a bunch of others[1]. The proliferation of Not|NonNull is evidence that I'm right, no matter how hard I get downed on HN.
I didn't say it wasn't a problem - if I could wave a magic wand and get rid of the concept of null in Java I would. That isn't what we're discussing though - you said, "The biggest thing missing in Java is an answer for the billion-dollar mistake [- NPEs]". I've provided what I consider to be at least a partial answer. If you care about avoiding NPEs in Java, it's a pretty good solution.
Realistically, null is so fundamental to the Java language that removing it would arguably result in a different language entirely. Certainly all existing java codebases would have to be refactored. The same goes for exceptions. That's obviously not an option when one of your primary selling points is backwards compatibility, so I'm not really sure what kind of solution you're looking for here.
The answer to your SO link notwithstanding, I would argue the @lombok.NonNull is at least one of the best options, as it actually generates a null check that is executed at runtime. This makes it more powerful than most of the other solutions.
> Realistically, null is so fundamental to the Java language that removing it would...
Again, as jayd16 pointed out a solution has been retrofitted to C#, Java's great nemesis. I don't accept the argument that this is somehow infeasible. Just make null assignments (including potential ones coming from libraries) an error and allow this feature to be scoped to your source files. Eventually the practice becomes ubiquitous. It's been done again and again in many languages and their various 'strict' modes.
The only actual problem here is that Java language developers aren't feeling sufficient pressure to address it. They should, but they're not, and that's sad. That sort of sadness is a common theme with Java.
While this could be solved by introducing one into the standard lib, it is not that big of a problem in practice as nullability checkers understand all of these annotations.
Agree, love that tool. Unfortunately it does not fully support Java 8, and that seems unlikely to change. I have never used it on a large project, I don’t think that compile times are good.
You could always try C#. They have a non-null compile mode where variables are non-nullable by default. They did the work to mark up core libraries and also have some pragmatic handling of olde nullable calls in 3rd party libraries.
After mostly using Node and Go for the last 3 years I kind of miss Java honestly. Unfortunately it's still terminally uncool in my local job market so even mentioning it would be a bad career move.
With Unity, Avalonia/Uno Platform, ASP.NET Core, NativeAOT and compact self-contained trimmed JIT deployments, the sheer cross-platform, cross-workload and deployment flexibility proposition of C# far surpasses Java even if we count in JVM + Kotlin for additional points of the latter.
Now let's talk performance of most commonly used web frameworks. I will save you the trouble of reading long text. Just check https://www.techempower.com/benchmarks/#section=data-r21&tes... and search the tabs for the more popular Java frameworks like Spring, Spark, Struts, Grails, Wicket, etc.
You may find yourself surprised, finding most of them in the bottom 25th percentile. Now scroll back to the top of the page and check where ASP.NET Core is. That's right, more often than not, in the top 10. All that performance, and you get it out of box just by using defaults and then some more. The only exception I see is Vert.X which is both mentioned across the web and also present in the top of the list.
Now, you may say that it's not very representative and there are entries of dubious usefulness in production scenarios (looking at you Just.js). And you would be right. However, the way to get most performance from ASP.NET Core is not by using tricks but rather simply writing code like in Node.js with app.MapGet("/users", delegate) and friends.
Despite all this, I still think JVM technologies like Hotspot or GraalVM have an upper hand over what .NET JIT/NAOT is capable of. However, keep in mind the out-of-ordinary performance gains that C# gets with each subsequent release. In areas with significant possibility of improvement like arm64 codegen quality, moving from .NET 6 to upcoming .NET 7 will yield you up to 40% performance improvement from JIT alone. And it was done in significant part by changing the code of JIT/Runtime that used to be x86_64-first to being cross-platform oriented (e.g. Vector codepaths becoming plat-agnostic, correct atomics being emitted for ARM, etc.).
.NET Framework used to be stagnant. After becoming OSS, .NET is the polar opposite, getting significant improvements in all of its areas with each release be it runtime code, standard library, language features or supported usage scenarios.
I think today, C# and .NET are mostly being held back by decades of legacy libraries and decisions, which you may consider avoiding in favor of newer solutions, regardless if those are in BCL or community-driven libraries. Still, sometimes people simply use code in such a way that unreasonably kills its performance.
But as long as you avoid known gotchas, your C# code will easily perform in production at the speed of Rust, C++ or C.
and it still enforces terribly strict OOP patterns onto the developer which is almost never the right way to develop software if you care about performance even a little.
I'm not sure you fully appreciate how tailored the JVM, and hotspot in particular, are to executing OOP oriented code. One great example I can think of is polymorphic methods. In C++ for example, you have to explicitly declare a class method as "virtual" in order for it to be polymorphic - i.e. the version called at runtime is tied to the runtime object instance, not the compile time type. This is because in order to do this in C++, there needs to be an extra lookup in the vtable to find the function address for every virtual function call at runtime. If C++ made all its methods virtual, it would take a significant performance hit from the extra vtable lookup for every function invocation.
In Java, all methods are virtual by default. Java also does the equivalent of a vtable lookup at runtime for function calls, but it has something C++ doesn't have - the hotspot optimizer. For any call site that is executed enough to affect runtime performance, the hotspot optimizer will optimize away the vtable lookup if there are only 1 or 2 method versions called at that site at runtime. This is true for the vast majority of cases. For most of the other cases, where you have 3 or more possible method implementations that could be invoked at a given call site, you would probably have to have something like a vtable lookup at that call site whether you use OOP or not (switch statement, if-else, explicit table of function pointers, etc), so you're not losing performance there either. The end result is, the JVM gets polymorphic methods basically for free in terms of performance.
This is just one example, there are many other clever things the JVM does to make OOP code performant. I don't have a citation, but I do recall seeing a talk (maybe by James Gosling?) where he mentioned that one of the primary design goals of Java was to make "doing the right thing" from an OOP perspective also the best option for performance.
it doesn't matter how much the HotSpot JVM is tailored to OOP code. CPUs and RAM are not tailored to OOP in any way.
Procedural code will always be more efficient with CPU and RAM, arrays will always be faster than Lists, being able to control datatype sizing to the byte will always outperform classes, and so on.
java is very fast, please do not misunderstand me. java is much better than it used to be, as well.
Java is not a language chosen when performance is a concern. Java is chosen when you have a giant pile of developers of various levels of skill and you want to pile a ton of rules and linters on them all so they write software in the same way.
> it doesn't matter how much the HotSpot JVM is tailored to OOP code
This is obviously false. The JVM, or any other compiler for that matter, is what translates the OOP code into CPU instructions. If it does that optimally, it won't matter what design pattern the top-level compiled language used. If it does it poorly, then it will.
> CPUs and RAM are not tailored to OOP in any way.
> Procedural code will always be more efficient with CPU and RAM...
First of all, arrays vs Lists and data type sizing are orthogonal to procedural vs OO code. You can write OO code that uses arrays and procedural code that uses lists, and datatype sizing is more related to your choice of language and compiler toolchain than your design patterns.
I think what you're trying to say here is that the performance ceiling for a low level language using simple language primitives (if-else and vanilla function calls instead of classes and polymorphism) that compiles to a binary is higher than that for a high level language that compiles to an intermediate language (or an interpreted language). This is generally true for small code paths - if you need to do a bunch of matrix operations, or data crunching for a small well defined problem, you can generally do it faster in C/C++ than in Java if you put in enough effort. The ceiling part matters though - in general, you have to put in a lot of skill and development effort to realize these differences, and this often grows super-linearly with the size of your codebase for low level languages. If you have a large application that has a lot of code, your overall performance will usually be higher with a high level language because the average performance for any particular part will be much better. Sure, given infinite time and resources you could theoretically do better in C, but nobody has that.
This is reflected in the approach most professionals take in practice when it comes to perf optimization - write most of your code in a high level language like Java or Python because on average it will be faster and less buggy for any reasonable amount of developer effort. For pieces of code that absolutely have to run as fast as possible, write them in C and call out to them from the high level language.
I guess the point I'm trying to make here is lots of people choose java precisely because performance is a concern. It does better than most other high level languages out there, and your average performance for a large codebase will be much better than something like C/C++ given the same amount of effort. As others have noted, it does multithreading better than most too, which is another major performance consideration. I don't care if my Java code is half the speed of the C equivalent if I can easily run 40 cores at once - or 40000. I think languages like Rust and Swift may let us have the best of both worlds in the future, but that remains to be seen. For now, the only time lower level procedural languages win is when you need a relatively small codebase to run absolutely as fast as possible.
ah, the "sufficiently smart compiler" argument. There is no such compiler, FYI, and there may not ever be. Not for every situation that developers are creating in Java today.
it's a matter of hitting your CPU cache as often as possible. Neither javac nor the runtime has any freaking clue how to do that, but you, as a developer, do. When you write Java, you can't do a lot to control how that JVM arranges data or how it fetches it from RAM, partially because of OOP, partially because of autoboxing/unboxing, so you wind up missing the cache a lot more unless you create a bunch of primitive types and use those, and that that point, why are you writing Java?
I'm not saying Java is bad. I'm saying that it is not the fastest language you can use. You seem to be arguing that it is, or that it could be. It cannot.
> average performance for a large codebase will be much better than something like C/C++ given the same amount of effort.
absolutely not. Maybe if you're fully skilled in Java and new to C or C++, but if you are equally skilled in both, you would never choose Java for performance alone.
There is an interesting presentation titled death of optimizing compilers, where it is claimed that programs increasingly are either in their hot path where performance is absolutely crucial and not even C/C++ is sufficiently low-level (you can achieve 2-3 orders of magnitude faster code by hand-optimized assembly), or on cold paths where even Bash would suffice.
Nonetheless, I found that it is easy to find these hot spots and they are trivial to “fix” in java for optimal performance, by simply using a primitive array. At every other place, Java is more than fast enough even with the occasional “jumping around”.
Also, haven’t seen a study on that but would be interested in Java’s defragmentation skills (due to moving GC). Malloc implementations can fragments a lot, and cold path code jumps around a lot. Wouldn’t be surprised if Java would fair quite well here.
I would love to see a video game implemented in Bash with the hottest spots implemented in Assembly, just to see how poorly that would perform.
I work for a company that has AWS Lambdas which are invoked 10s of trillions of times per year, with about 45% of that happening in a single month, and another 45% happening 6 months later, also within a single month.
I cannot imagine what our AWS bill would be like if those lambdas ran Bash. 100x larger? At least.
Performance matters. It's not obvious how much it matters until you look, but it matters. There are zero users who will complain that an application makes them wait less than it did previously. There are zero managers who will complain that their AWS bill gets lower because code takes less time to run and uses less RAM.
I have no idea why anyone would even begin to argue the opposite.
I didn’t write that I agreed with said presentation, but it was interesting, and a good take away is that if you have such a scorching hot loop that iterates over megabytes of data, a compiler may not be sufficient even in low-level languages. (Think of video codecs). And the other side, routine optimizations done on cold paths may not be as worthy (the bash line was me exaggerating the effect - but just think of any Python deployment, they are basically that, slow code calling into optimized libs in C and Fortran)
I'm curious about the cost of hosting if you're using plain ol' VMs instead of lambdas (EC2, etc.). What are those functions written in? Unless you're going to say C/C++/Rust or a similar language, I'm not sure there will be any significant variance.
OOP is fine. OOP and functional programming written by skilled programmers both end up being basically the same thing -- they are just different techniques to achieve encapsulation, control over dispatch, control over state, etc.
Java has supported (mostly) functional programming constructs since 1.8 (which was 2014), so you can realistically use it without doing too much OO.
I've observed, though, that people who complain about OO in Java usually write top-down procedural code rather than functional-style code, which is far, far worse.
YMMV, of course, and yes, there are modern frameworks other than Spring (Play is supposedly pleasant to work with), but life is too short to try to sort out that mess.
I plan to stick to Elixir as much as I can, for as long as I can. When the language clicked for me, it was the biggest breath of fresh air since I decided to pursue programming as a profession, and even cooler than when I discovered Python/Django.
Edit: obviously, Java is not all bad, and not all (or even most) people who use it fit the description above. But something about the ecosystem seems to draw those types (no pun intended) disproportionately.