Nothing but dirty FUD by someone whose Java skills are at best Junior level.
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?
I love Lamire's blog posts and think he's brilliant, but I'd have to agree with the GP (though more gently) that this really does read like a list of "Things to Watch out for: My First 6 month of Java".
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.
Perhaps the intent was seeing what crazy HN thread would be started :) My reading of the last paragraph was that there is no safe language since in the end it's always up to the programmer.
Well, I did have to debug a Rust program with valgrind/miri so it is not like safety is a singular axis (not shitting on Rust, I really like the language, but it is a low level language exposing low-level constructs thus low-level bugs can rear their ugly heads)
A safe language isn't a language that lets you do things safely, it's a language that will not let you do them unsafely, or will at least make it hard to do so.
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.
When we call a language “safe”, that formally means that there’s a proof that no well-formed program in the language can do something undefined.
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.
Null pointer exceptions by far the most frequent exception I encounter. Not only on the development side, but also as a user.
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.
@NonNull and similar annotations can and are used by static analyzer programs to completely eliminate null exceptions. For example Checker Framework can do that and much more.
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
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.
And they are mostly niche ideas that in practice gives overly verbose code filled with boilerplate making it just as insecure as languages that take a more pragmatic approach.
Overflows are a major source of security vulnerabilities. NULL values are a major source of logic bugs. Data races are the most common bugs in parallel and concurrent applications.
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".
Is there really a way around this? overflow should be an error in most languages - but isn't for a variety of reasons such as simplifying the construct
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.
Haskell has it as well, in a very clever way in my opinion. Number literals don’t have a type themselves, they are inferred to be the correct type based on context/available type notations, etc.
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.
> Javascript,
> 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.
That's a known "feature" of most languages that perform modular arithmetic by default, since the negative of Integer.MIN_VALUE is Integer.MIN_VALUE in two's complement arithmetic. Languages that automatically promote to bignums by default (e.g. Lisp) do not suffer from this issue unless modular arithmetic is explicitly requested by the programmer.
To be slightly pedantic, C# gives you the option to throw an exception in that case (with either checked blocks or setting the CheckForOverflowUnderflow compiler flag to true), but the default behavior is to allow overflow.
The default actually depends on project language IIRC. C# has overflow checking disabled by default, VB.net has it enabled by default. Not sure about F#.
> Data races are mostly prevented through Rust's ownership system: it's impossible to alias a mutable reference, so it's impossible to perform a data race.
Rust does prevent data races (as in multiple threads writing to the same memory location is explicitly forbidden by the borrow checker), but it doesn’t prevent race conditions, which is a more general family of concurrency bugs.
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.
A data race occurs when two or more parallel processes access the same data and at least one of them is mutating it.
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.
Sounds more or less (and pardon me if I'm oversimplifying) Java's "synchronized" keyword that most people stay away from just because it slows the code down so much you don't get the benefit of parallelization.
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.
Rust type system has primitive traits Send and Sync that formalize behavior expected for safe multithreaded concurrency. This way compiler can statically analyze the program and determine if it's safe.
The term "safe language" doesn't have a well-defined meaning, but the closest one that can be used in a binary context is "a language that doesn't have undefined behaviour." This definition is also flawed and often misleading, but is quite traditional and widely used, and by that definition, Java is a "safe language."
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.
Safety as a concept evolves, I would say Haskell-like is a good target to aim at today, for a safe language.
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.
There is not one single definition of safe, as perfect safety does not exist. So Java being 'safe' depends on the definition
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”
The prime directive in programming is to read and understand the code the code and tools you are relying on (the hard part).
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.
How is correct defined? Per the language spec? The architecture of the system it's meant to execute on? It's a very unclear target to try and aim at, as a definition, and strikes me as a rather academic approach to how things should work.
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".
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.
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!
> When a function receives an object, this object might be null. That is, if you see ‘String s’ in your code, you often have no way of knowing whether ‘s’ contains an actually String unless you check at runtime. Can you guess whether programmers always check
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 exception
If I'm understanding this correctly, it's still a problem: if the product's price element or the order's line item list is null, for example, that shouldn't just be silently ignored. The transaction should fail, loudly and visibly (like with an exception) so that the root cause of the invalid data can be examined. The way most programmers code it is to just ignore the bad data and potentially ship an item for free, or without charging tax, or charge the customer but not ship the item, etc. etc. This is why so many software development organizations have huge QA departments.
I still write null checks if needed but for deeply nested objects, it comes down to only checking for null at the end of the chain instead of in Java where you have null checks each step of the way.
Other than what other commenters have already mentioned, Java has probably one of the best tools for actual correctness verification, due to it being huge both in academia and in production environments.
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.
64 comments
[ 4.4 ms ] story [ 120 ms ] threadWhy 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.
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.
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....
https://doc.rust-lang.org/nomicon/races.html
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.
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.
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.
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”
[1] https://checkerframework.org/manual/#lock-checker
The prime directive in programming is to write useful code.
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.
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".
Java is not a sane language.
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!
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:
customer?.order?.lineItemList?.forEach { item -> val currency = item?.product?.price?.currency
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.