Don’t call it a comeback: Java is still champ(github.com) |
Don’t call it a comeback: Java is still champ(github.com) |
To me it’s always felt like some kind of assembly language humans shouldn’t deal with directly.
This one, existing technology would make Java DX on par with the dynamic languages
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.
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).
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.
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.
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.
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.
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.
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.
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.
[1] https://stackoverflow.com/questions/35892063/which-nonnull-j...
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.
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.
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.
This is not entirely true. Intel in particular pays a lot of attention to Java performance when benchmarking processor designs, see for example https://www.intel.com/content/www/us/en/developer/articles/t... .
> 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.
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 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.
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.
1. https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...
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.
(Those 16 years are four years at Google, 10 years in a pair of startups, now 1.5 years at a medium-sized acquirer.)
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.
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.
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.
I love your use of the word “cleanse”.
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.
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.
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 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).
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.
If only project Loom will get out there so I wont be coding everything like Javascript I'd be happy. :)
https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
- 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.
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.
If you apply some discipline, I think Java is a great language.
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.
Same here. Nothing more to add.
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.
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.
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 have to add that JavaScript would greatly benefit from a solid base class library
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 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.
Stuff like Microprofile, Quarkus, ActiveMq, Tomcat, and even JakartaEE are gravy on the cake.
The real reason why Java is a champ is because outsourced labour uses them. https://blog.jetbrains.com/idea/2020/09/a-picture-of-java-in...
It’s one of a few frameworks that makes writing Java less tedious again.
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.
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?
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.
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.
EDIT: typos
Might want to give some evidence of this. Also, C# has no plans of green threads. It went the async/await way which is a pain.
At least that is my experience running the only .NET team at a company that primarily runs Java.
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.
The vast majority of Java (=running on the JVM) software these days is server-side, without any user interface.
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.
Been a Java dev for ten years and I drive a Toyota Corolla....
...but also in my spare time I play around with Clojure and dream of buying a '69 SS Camaro
Here it is fine I think, the writer of the comment is just explaining their point of view.
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.
https://www.fuelexpress.net/blog/just-for-fun/4597-2/
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:
https://cars.usnews.com/cars-trucks/chrysler/300/2011/reliab...
(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.
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 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.
Go uses two build tools for any non-trivial projects. One write in go.mod and another in Make :D (see this - https://github.com/kubernetes/kubernetes/tree/master/build/r...)
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.
It’s primarily used by enterprise shops. You’d never brag you about driving a van. It’s not fashionable. It’s kinda ugly.
But…it gets the job done.
> 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.
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.
Anyways.
A Chrysler PT Cruiser. Odd looking, beloved by its fans, and a reasonably adequate (if quirky) to drive.
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.
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.
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.
- a much better concurrency model, that gets you parallelism for free just by adding cores
- no global gc pauses, low-latency
- fantastic operational tools (trace debugging, remote shell, etc.)
- Erlang/OTP gives you great middleware out of the box, including including queues, pub-sub, service monitoring, database, etc.
- Honestly just a more expressive core language.
All that said, I don't really have anything against Java. It is a great language for the server, but it is not the only choice.
"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.
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)
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.
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.
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.
[1] https://inside.java/2022/05/30/sip053/#:~:text=ZGC%20was%20d....
[1] Reduced them to always well below 1 millisecond.
Java's GCs are incredible and you have a menu of algorithmic options that let you avoid whatever problem you're worried about.
True concurrent GC have been available since late 2000s. Azul had a read-barrier GC as well - effectively a pauseless GC (or pauses under 1ms)
This is a feature, not a bug. Which is riskier?
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.
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.
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.
That's a terribly broad generalization. There are multiple other options that are entirely sane.
Experienced C developers have switched to it. Actually it gets even better, they are extremely productive in it. Don't trust me, check github.
I wonder why. /s
Will the next WhatsApp be written in Java?
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.
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.
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…
Also, wondering how much of that overhead in the C program is due to malloc (can be overcome by memory arena)
Please explain. What kind of crazy jack write servers in C? (Real question, trying to learn here)
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.
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.
static void main() {
BiFunction<Integer, Integer, Integer> add = (Integer x, Integer y) -> x + y + 5;
Integer result = addTo(10, add);
Integer result2 = addTo(10, (x, y) -> x + y + 5);
}
static Integer addTo(Integer acc, BiFunction<Integer, Integer, Integer> addFn) {
return addFn.apply(acc, 5);
}
Integer eval(Expression e) {
return switch (e) {
case INT(var value) -> value
case ADD(var left, var right) -> eval(left) + eval(right)
case MULT(var left, var right) -> eval(left) * eval(right)
}
}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.
If the JVM is considered low latency I shudder to think what is high latency.
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.
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.
This is true and hard to communicate to people that haven't lived it.
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).
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.
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.
I do intend to dig into it a bit more once I feel like I have mastered Java
> 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.
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.
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.
wut? do you mean MB? most small web services(jetty, jdbc/pg driver/json) I have work on use below 500MB
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.
I like to believe that records with the upcoming ‘withers’ will be an adequate answer (if a bit too late) to this whole question.
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.
>Issue #597 : It's written in java but doesn't support my blu-ray player
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.
Sure, if we are talking about desktop applications and we ignore the exceptions (Intellij / Minecraft).
https://github.com/gohugoio/hugo
There is a Docker file and some shell scripts, but you build the executable using "go install".
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 I was reacting to bullen's assertion that "To use anything else on the server is madness.", and to the rather exaggerated performance claims.
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.
But bullen was arguing that Java is the best for servers of all types, which is what I object to.
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.
With Java programs without a Java GUI, I never had problems.
What can I say? They get me from claim A to claim B.
So are you saying that analogies should only be used like secondary steering wheels?
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...
A more versatile set of analogirs are of course ATVs, Aircraft, or even the humble legs.
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!)
I could go on for hours.
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.
You can't. 'null' represents an empty optional.
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.
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.
> CSPs, for example, which in many ways are superior to actors
how is csp superior to actors?Or is it that people interested in doing ground-breaking garbage collection research tend to target Java?
G1 offers pause time goals so you can balance throughput or latency but it still has global pauses.
ZGC doesn't because it doesn't pause your app. Its design doesn't allow any tradeoffs there.
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.
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 ;)
It's a shame Java does not have value types, which makes nulls a much rarer thing to have to deal with.
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).
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`
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
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!
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.
I’m sure most people on HN are aware there are new versions of Java now.
Edit: ‘there seems’ in my original comment should be ‘there seemed’ which may have caused confusion.
Let’s not read more into arbitrary version numbers.
It also provides memory safety and multiprocessor support.
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.
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.
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.
You can always let your IDE also do some of the work, at least for a quick reference: https://www.jetbrains.com/help/idea/get-started-with-kotlin....
Of course, when you get into slightly different idioms or design pattern implementations, things might require a bit more mental effort and manual work.
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.)
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).
Erlang lets you do this for "free".
Managing K8s microservices is easily an order of magnitude more work than managing them in Erlang as applications.
This can be significant too.
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).
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.
They are fine until they need to scale and iterate quickly without downtime.
English is source code lingua franca, and every reasonable dev should understand this point.
It is kind of more complicated with tech literature though.
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.
Same with Java, several databases written in it, such as QuestDB, Pinot, Presto.
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 {
....
}https://docs.oracle.com/javase/tutorial/essential/concurrenc...
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.
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.
Spring is worst thing happened with Java.
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.
[0] - https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
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.
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)
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.
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.
Java is cool language and has many nice features, but its type system is not its major advantage.
https://arxiv.org/abs/2112.07880
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.
Green threads are an amazing fit for web apps. I made a career out of using those for web apps and API apps.
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.
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.
[0]: https://gist.github.com/ambrosiogabe/66a6e2fdc77e6a600e570f4...
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.
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.
[0]: https://martin.ankerl.com/2019/04/01/hashmap-benchmarks-04-0...
Wonder why the jab; Personally I'd trust my own code more than redis'
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.
Of course it's ambiguous because Java can mean both the language, and the wider ecosystem of libraries, languages, JVMs, tools etc.
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."
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.
Reverse engineer: https://dev.mysql.com/doc/workbench/en/wb-reverse-engineer-create-script.html
Forward engineer: https://dev.mysql.com/doc/workbench/en/wb-forward-engineering-sql-scripts.html
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.
WSDL: https://en.wikipedia.org/wiki/Web_Services_Description_Language
WADL: https://en.wikipedia.org/wiki/Web_Application_Description_Language
SoapUI: https://www.soapui.org/
OpenAPI: https://en.wikipedia.org/wiki/OpenAPI_Specification
OpenAPI Generator: https://openapi-generator.tech/
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.
Fwiw, the JVM now has a noop garbage collector so this is easy enough to benchmark.
But it is easy to monitor with Java’s killer observability.
Lisp I Programmers Manual, 1960
https://www.softwarepreservation.org/projects/LISP/book/LISP...
Chapter 6.3, Page 95, The Free-Storage List and the Garbage Collector
@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();
// ...
} fun calc1(@RequestParam @NotNull @Size(max=10) @Pattern(regexp = "^\\d{4}-\\d{1,2}-\\d{1,2}$") date:String,
@RequestParam @NotNull @Size(max=5) @Pattern(regexp = "^\\d{2}:\\d{2}$") time:String,
@RequestParam @NotNull @Size(max=40) @Pattern(regexp = "^[-_'A-Za-z/\\d]{2,50}$") zone:String,
): Map<String, Any> {
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.However, defining the validation schema in the parameter list is ugly too. Define it elsewhere and have it run through a validation middleware.
Just as ridiculous is the HttpSecurity method chaining “DSL”.
- 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
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.
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.
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 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?
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).
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.
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
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.
[1] Example from https://kotlinlang.org/docs/type-safe-builders.html
html {
head {
title {+"XML encoding with Kotlin"}
}
body {
h1 {+"XML encoding with Kotlin"}
p {+"this format can be used as an alternative markup to XML"}
}
}- 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").
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.
True. Out of curiosity: what's the use case for this?
Surely side-effects-out-of-your-system is enough?
Scheduling is deterministic, which is a VERY useful property to have. See ex. http://erlang.org/pipermail/erlang-questions/2006-December/0... about changing scheduler determinism.
Here is a simple echo server running on Virtual thread (using same old blocking APIs, handle 5m persistent connections) - https://github.com/ebarlas/project-loom-c5m/blob/main/src/ma...
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?
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.
That comes at some extreme costs and implications to what the VM is capable of achieving, but it is something that's pretty impressive.
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.
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.
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.
[0]: https://gist.github.com/ambrosiogabe/ba6bd0fa80588c2fd2ca26d...
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.
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.
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.
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.
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 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.
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.
But programs that store their data as XML rather than some proprietary binary or text format are awesome.
However, I'm sure a lot of that code still exists.
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.
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.
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.
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.
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.
You seem to have a lot of opinions about my apps from your own unfortunate experiences. Hubris is a word that brings to mind.
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.
Being able to handle it correctly is just an additional benefit.
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.
No issues anywhere. There's only one point of failure here: bugs in the code generator.
Example?
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.
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).
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.
record User(String name, Integer age, Boolean isActive) {}
https://docs.oracle.com/en/java/javase/18/language/records.h...The only way to stop this is to remove the mutable methods from the interface entirely, which is what I'm complaining about.
(Also, behind the scenes they often compile to static functions and called through the invokedynamic instruction)
In Scala, I would write:
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:https://docs.scala-lang.org/tour/pattern-matching.html
The fact that Java happens to use "switch" instead of "match" is one of syntax, not semantics.
JDK 17/18 introduces Sealed Types, which allow you to create ADT's
sealed interface Expr {
record INT(Integer value) implements Expr {}
record ADD(Expr l, Expr r) implements Expr {}
// etc
}
When you "switch" over sealed types, the switch expression is exhaustive if all members have branches and requires no default case + is typesound.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.
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.
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.
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.
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.
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.
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.
Again, you seem have very strong opinions on incorrect assumptions. I’m kinda bemused by that.
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.
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.
You can't get out-of-thin-air primitive values (no word tearing).
But you can still observe states that cannot be explained by any possible sequential interleaving of your program. E.g. your program can do:
a = 0;
b = 0;
a = 1;
b = 1;
and another thread that does: print(b)
print(a)
may observe 4 different combinations of values, surprisingly including also this one: { a = 0, b = 1 }Why a corruption at a structure level should be considered less serious than tearing at a word level?