Java is not a safe language
lemire.me
lemire.me
Half of the bullet point you find in 75% of all languages (eg int overflow, nullability, race conditions) or are ideas which are a security scientist dream of (eg named arguments).
There is barely something specific to Java and more esoteric.
There is no real argument why programming languages as tools should stop at the level of "well you can't put a string into an int".
c = a + b
In a language without native memory management, it's rare that this type of bug could lead to disclosure of sensitive information. In the cases it would, it's easy enough to guard the overflow with a test such as if (MAX_VAL - b) < a then error.
Chasing safety features which result in difficult to reason about semantics will inevitably lead to low language adoption. I'd be very interested in approaches which guard difficult cases while maintaining ease of use. I'd love a statically typed language which automatically increases the size of numeric type on over/underflow.
> Chasing safety features which result in difficult to reason about semantics will inevitably lead to low language adoption.
Python solves this problem by not having overflow for integers at all, instead all integers are unlimited. It's still one of the most widely used languages in the world.
There is both a 64bit integer type and an “infinite” one, and you can change the implementation by a single type notation, in a completely safe way.
* APIs often do something similar by passing in an object with optional fields*
That's really not the same as the others though, as it's a convention. That's arguing that passing a Python dict or Namespace to a function is the same as named arguments in the function's definition itself. You can do the same in Java with Dictionary<string,object> if you want.
Also 4 years older, not a decade.
Apart from the integer overflows the other three are IMHO reasonable, not surprising, known to everyone, and manageable.
* Data races: no, the Java compiler will not try to construct a proof of data-race-free behavior. Instead there are some tools: immutability, java.util.concurrent.*, lock checkers libraries [1] if you want something a la rust, actors, execution pools, futures, atomics.
* Null safety: Some annotations can deal with that.
* Named arguments: IDEs (IntelliJ does) now show parameter names of the method to the programmer.
All in all, the arguments presented are weak IMHO and Java is pretty safe, and offers pretty good performance with this safety definition.
As for data leaks, I'll steal a comment from Lemire's site: “Memory leaks are memory safe in Rust”
There are multiple model checkers, static analysis (and no, I’m not just talking about some basic linting, but e.g. Checker Framework pretty much augments Java’s type system with many other compile-time verified features, not only adding null-safety, but checks for map keys, tainting, etc), and even a complete verification tool which adds a whole language to specify the behavior of your functions, and will statically verify what it can, but will add runtime checks for statically not-verifiable parts (JML) This in turn means that you can write a sort function and verify whether its output will uphold the sortedness property.
And then I didn’t even mention the observability of the platform (which is probably the best, or at least one of the best of any plaform), where you can stream many kind of interesting data in realtime from even a prod system, with almost zero overhead (JFR), tools like VisualVM, etc.
Correctness doesn’t stop at the language level, it probably barely starts there.
What the author means is that there are languages that can catch more bugs than Java. That is true, but there are languages that can catch more bugs than, say, Rust, too.
> Java does not trap overflows
True, or at least the built-in operators don't. However, unlike in C/C++, their behaviour is well defined under all conditions, and a lot of code depends on this behaviour. The operations that do trap overflows require opt-in through the Math class.
> Java allows data races
True, but in this case it's unclear if the cure is worse than the disease or not. Preventing some bugs in the language come with a real cost, and so what matters is the overall cost of avoiding a bug. We can look at three languages that address this problem — Erlang, Clojure, and Rust — and they all pay a price for that. The first two in performance and the last in language complexity.
> Java lacks null safety.
True, but I think this is fixable, and hope it will be addressed. There's ongoing research.
> Java lacks named arguments
This one is complicated. For one, watch for records, which are on their way of getting named arguments and will be able to address the instances of "lots of parameters." For another, Java's excellent IDE's all do show the parameter type alongside the positional argument. Finally, Java is not just a language but also a platform that is built to support separate compilation. Named arguments would make parameter names part of the ABI, and would make it more complicated and harder for units to evolve in a backward compatible way. Binary backward compatibility is a very useful property, and so the benefit of named argument (for all methods) probably does not justify their cost (records have specific constraints that could allow their compatible evolution while still supporting named arguments).
Regarding Java’s data races, they are “safe” as well for 32bit primitives and references (and in practice for 64bit primitives as well, but that is not spec-mandated). They offer no-out-of-thin-air protections, meaning that you can only observe values that were explicitly set by a thread. A prototypical example would be a non thread-safe counter being simultaneously incremented from many threads - in java it is guaranteed to have a value between 0 and the true count.
Reposting my comment from 2 years ago:https://news.ycombinator.com/item?id=21832009
A null pointer exception in Java is SAFE according to Cardelli -- it's a trapped error.
A program fragment is safe if it does not cause untrapped errors to occur. Languages where all program fragments are safe are called safe languages. Therefore, safe languages rule out the most insidious form of execution errors: the ones that may go unnoticed.
...
It is useful to distinguish between two kinds of execution errors: the ones that cause the computation to stop immediately, and the ones that go unnoticed (for a while) and later cause arbitrary behavior. The former are called trapped errors, whereas the latter are untrapped errors.
-- Type Systems, Luca Cardelli -- https://scholar.google.com/scholar?cluster=90442457768317510...
And practically speaking null pointer exceptions are easy to debug and fix.
It's really the memory safety, integer overflows, and data races (untrapped errors) that are difficult, and which the language should have mechanisms to avoid.
So this post is conflating two different things. It's criticizing Java for integers overflows that DON'T trap, and null pointers that DO trap!
This one struck me some day because this is why Math.abs can return a negative value. When passing Integer.MIN_VALUE, the result will be -2147483648.
There is now StrictMath.absExact that throws an exception in that case.
Regardless of that flag, Math.Abs will throw an OverflowException: https://referencesource.microsoft.com/#mscorlib/system/math....
Actually, my experience is the opposite - they check like hell, all over the place, reflexively, because a null pointer exception is a blaring emergency that has to be fixed RIGHT NOW, so I always see Java code written like this:
if (customer != null && customer.getOrder() != null && customer.getOrder().getLineItemList() != null) {
for (LineItem item : customer.getOrder().getLineItemList()) {
if (item.getProduct() != null && item.getProduct().getPrice() != null && item.getProduct().getPrice().getCurrency() != null) {
...
} // else don't do anything, don't even log an error, just continue on as if an empty product or price object is an ignorable error and create a much harder to diagnose problem than a null pointer exceptioncustomer?.order?.lineItemList?.forEach { item -> val currency = item?.product?.price?.currency
For Java to become a safer language it would need a better type system, and then to make backward-incompatible changes to standard libraries. Therefore, unlikely to ever happen.
Why is this on the front page?
> Java does not trap overflows
Fair enough but only relevant for specific problems/domains. Never encountered this in 10 years.
> Java allows data races
It does not, if you use its dead simple std lib concurrency collections. Also it brings all the concurrency primitives you'd ever ask for (mutex, semaphore, synchronized and god knows what) and guaranteed latest-read "volatile" keyword if you really want to go there. Which you shouldn't. Use AtomicReference, AtomicInteger, ConcurrentHashmap, etc. If you struggle with data races in Java, you are at best a Java junior programmer and should study it instead of writing FUD.
> Java lacks null safety
I like null. Matter of personal opinion. If you are an anti null fetishist, go with Optional. If you want to go even further, use validated @NonNull annotations.
> Java lacks named arguments
Personal opinion. Syntactic sugar I never missed. IntelliJ and VS Code offer inlay hints to overcame this if it really helps. May I also offer you some rainbow colored braces for dessert?
Lamire (the author of the blog post) is well know as the author of https://github.com/RoaringBitmap/RoaringBitmap which is in widespread use.
I suspect that weirdness has less to do with Lamire, but the weird expectations that it tries to "debunk".
Like no ones out here arguing that Java is some especially safe, super high validation language. Sure it's memory safety was a huge breath of fresh air compared to C/C++, but it's not in the same vein of reputation as an Eiffel or Ada or even Rust.
The moment you allow things that don't work well with the prover, people will use them, and the whole ecosystem becomes unsafe. And by extension, your code becomes unsafe, if the compiler can't reason about what happens when your code calls theirs.
Plus, unless you have just the right linter set up, you can still accidentally mess up and do something unsafe if the compiler lets you.
Java is completely safe by that definition. Now, you can have the seat-belt even tighter than that for various types of <domain> safe, but we are moving the goal-posts of the definition.
Also known as the pits of success.
Optional is convention based and @NonNull just throws the runtime exception at a better location, so in my experience neither come remotely close to providing null safety.
This is the exact same thing that languages with null-safety have “in-built”. Sure, language support is nice, but it is here and is working
> It does not, ... If you struggle with data races in Java
How can one struggle with data races if it does not allow it.
I've seen what GP was referring to in practice.
Also on the same page "Data races are mostly prevented through Rust's ownership system".
In short, the borrow checker allows a single owner at all times, or it allows references to the object, abiding these rules: it may have any number of read-only references (they are safe to share), OR a single read-write reference.
Safe Rust forbids this through ownership and move semantics along with lifetime analysis. You may create any number of immutable aliases to data, or exactly one mutable alias. You cannot mix the two.
Unsafe allows you to (kind of) sidestep the automatic checking, but its your responsibility to uphold those conditions.
Java's synchronized keyword changes how code is interpreted to use expensive locking and data fences in order to prevent logical race conditions.
Rust's semantics do not alter the compiled code or insert any locking behavior. They prevent you from writing incorrect code to begin with.
In other words, Rust doesn't do anything magical to deal with data races at run time. It prevents you from expressing programs that have data races in the first place. It's a compile time analysis, not a run time behavior.
The prime directive in programming is to write useful code.
An interesting thought experiment here is to consider that in this instance correct code relies upon two or more other instruction handlers behaving correctly: the JVM and (at least) the host CPU. Thus, even correct code can be vulnerable to problems (see: Speculative execution vulnerabilities). If the goal of programming is to write correct code, what % of errors on that correct code execution is permissible?
The whole argument strikes me as an appeal to a platonic ideal rather than reality.
My project managers always define it as "implements this vaguely worded set of contradictory requirements as well as this other set of undocumented contradictory requirements and is finished by tomorrow".
Everyone skips the hard part, and squabbles over whether it's okay or possible to redefine 'high quality' to include code written by people who never did the hard part.
Suffering by software's user's is tge inevitable result of this misclassification of the prime directive to be "write" oriented.
Java is not a sane language.