Hacker News new | ask | show | jobs
by abadger9 1416 days ago
I've used Java my entire career and i'm fortunate for it. I appreciate how readable the code is (unlike my experience with Erlang, Haskell, etc), typically i don't have foundational issues in the web framework (once again had some with Haskell). Everything works, if I need to do low latency, there's great libraries and resources, if i need to build a simple internal tool, it can be done effortlessly.

I think python is the same way, the ecosystem is so rich that you can really do anything you want (until you get into low latency).

4 comments

Agree with all of that except readability. Java has some language deficiencies (no first-class functions, "streams" missing for almost twenty years and added too late to be done elegantly) and some cultural norms (mutable everything, overuse of inheritance) that make it a chore to read most of the Java code you encounter in the wild, including the source code of the libraries you depend on. Figuring out the behavior of one method routinely means taking a tour through implementation details in three or four superclasses, factories, factory factories, and all the other Java clichés that are legendary but also absolutely real.

You can do better in your own code, but you still have exposure to the code in the library ecosystem.

Worth it, though. It really does seem like there's a Java library for everything.

Modern Java has first-class functions, pattern matching with structural binding, records/pure immutable data classes, the whole 9 yards:

    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)
        }
    }
Unfortunately, records are only guaranteed to be 'shallowly' immutable. It would be great if future Java versions provided a straightforward way to enforce immutability.
> records/pure immutable data classes

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.

You are right, though I seldom find it a problem in practice. Also, OOP sort of makes immutability hard to define (e.g. is a getter with an internal counter of accesses immutable? In a way, it is. Also, Rust’s internal mutability pattern is similar).

Nonetheless, recently more and more standard classes are made deliberately immutable, and there was a proposal for frozen arrays as well (not sure on their status).

> You are right, though I seldom find it a problem in practice.

Although it's easy to to tell people that mutation is confusing and to avoid it, enforcing that is much easier if the compiler is on your side and will prevent mutation with const.

I've encountered unnecessary mutation (introducing implicit assumptions on the order of calls, and making things more confusing) constantly in both Java and C++, but enforcing const in C++ cuts down on that. Or at least, it forces a const_cast which I won't approve without a really good reason.

You're right about Rust of course, internal mutability is possible and maybe even common with RefCell, but culturally it seems like that's avoided. On the other hand, mutability is extremely common in Java.

Maybe I'm just traumatized from some of the horrific code heavily using mutation I've seen over the years.

JDK 17 record classes:

  record User(String name, Integer age, Boolean isActive) {}
https://docs.oracle.com/en/java/javase/18/language/records.h...
If you have mutable members you can still absolutely do myRecord.member().methodThatMutates().

The only way to stop this is to remove the mutable methods from the interface entirely, which is what I'm complaining about.

First class functions are as modern as the Iphone 5.
Pedantically, I want to point out that first class functions predate the original iPhone by a large margin ...
I meant lambda’s introduction’s date in Java.
I haven't programmed in Java since 2005, so forgive me. But isn't a lambda automatically converted to an object of the necessary single-method type? That feels a little magical and suggests that there really isn't such a thing as a genuine first-class function in Java, as there is no way to define a function, and no universal type to assign to such a thing.
Well, the standard library does have some function types, like Function<A,B>, Supplier<A>, etc. You can just initialize a variable of these types and pass them around. I don’t see how are these not genuine first-class entities. The lambda syntax is also quite pleasant.

(Also, behind the scenes they often compile to static functions and called through the invokedynamic instruction)

This is destructuring but not pattern matching.
How is this not pattern matching?

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.
Can you pattern match deeply? Can you make a case for ADD where the first param is a MULT with the second param of the MULT being INT(5)?
It is underway (destructors are not yet finalized).
Does it support matching patterns in data structures?

    f [ [1, _]. [3, _] ] = ...

    f [ [], [10, 20] ] = ...
Given that java doesn’t have too much syntax sugar for data structures, I doubt this will come, but other kinds of destructors will be possible.
This is a matter of preference, I think. As a non-erlang practicioner I find Erlang to be far more readable than Java (which is good, because IMO Erlang authors have a habit of writing spaghetti code that's poorly organized). As a non-practicioner at some point in Java you run into annotations? Pragmas? and you stop being confident that your mental model of the code is correct. When I run into those I throw my hands up and start cursing.
I believe it's unfortunate people judge the JVM ecosystem by Java. Scala and Kotlin are so much more attractive options if you write code for a living. Between this and the devolution from maven to gradle it's not a surprise junior developers are JVM-shy.

I see companies downshifting to unmaintainable toys such as Python even in data engineering circles. It's really odd that mobile developers with Kotlin (and front-end ones with Typescript) are getting ahead of backend ones in adoption of modern languages.

Once Loom and Valhalla get merged to an LTS the remaining vestiges of bad old Java will have been gone. I really hope Graal goes mainstream too. That will hopefully blow out of the water the golangs of the world. But those are platform-level improvements any JVM language will benefit from.

Out of curiosity, what issues do you see with gradle given your statement about it compared to maven?
Maven is declarative. Once you know how to build and deploy one repository you can do it in any other. Including projects started a decade ago. Five commands is all you need to use it. It's trivial to have a monorepo with the classical 3-tier pom file structure.

It takes a 2/3-of-your-screen plugin configuration to build a Scala project. And you can simply copy that configuration without even thinking about it to another service.

I believe that making the "<dependency>" declaration a one-liner would fix 80% of what's wrong with maven :)

Every single Gradle project I have worked with has its own structure. Which happens even across repositories owned by the same team. There are DSL flavors (Groovy and Kotlin), both are actually used and differ slightly. The wrapper. Its storage is based on Ivy, not Maven so you double the number of Internet replicas on your HDD. But it's still better than SBT ;)

>if I need to do low latency

If the JVM is considered low latency I shudder to think what is high latency.

Java is pretty fast. Second most popular language in HFT. Can get it to a few tens of micros. Not as fast as C++ at sub 5 micros. So good enough for many latency sensitive apps.
> Not as fast as C++ at sub 5 micros.

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...

> and it consistently performed a lookup at 1 nanosecond.

TBH I'm skeptical that you are measuring what you think you are measuring. There are a lot of micro-benchmarking pitfalls, like dead code elimination, loop-invariant code motion, unrolling, and other issues. Unless you actually looked at the machine code coming out of the compiler, you're measuring something you don't understand. E.g. 1 nanosecond is roughly 3-6 instructions. That 100% means the hash lookup has been inlined into the benchmarking loop.

Are your hashtables mostly empty? Really small? Lots of easy hits (or easy misses)? Because the slow cases (actually looking up) are going to be hairier and may not be inlined.

Did you benchmark against Java's HashMap? Because it is also very, very, very fast for simple cases.

It looks like caching definitely skewed the results a bit. You can take a look at the linked code yourself. Worst case was still only around 80 nanoseconds which is definitely slower, but still orders of magnitude faster than "sub 5 micros".

Don't take my word for it though, you can take a look at the Robin Hood benchmarks[0]. Robin Hood unordered map is a competitive hash map that's performed much better than the STL for me in many cases. They average a 4 nanosecond lookup speed for a hash map with 2000 elements and an integer key.

> Did you benchmark against Java's HashMap?

I benchmarked against C#, which has a runtime that performs similar if not better than the JVM. The C# code was a ~~few microseconds~~ around 130 nanoseconds. Which is still very fast, but up to 100x slower. (And yes, this was after warming up the code. I used benchmark dot net[1] here.). This is a really easy benchmark to set up. If you doubt me you can write a couple of benchmarks in under an hour and compare yourself.

[0]: https://martin.ankerl.com/2019/04/01/hashmap-benchmarks-04-0...

[1]: https://benchmarkdotnet.org/articles/overview.html

Did I read that right? The C# version is computing SHA256 hashes?
I'm talking about an end to end HFT system in C++.
All our market data feed handlers are sub 5 micro in the 99th. All in Java.
Java is heavily used in high-frequency trading. I believe it's the most popular language after C++.
Indeed. Shutoff Garbage collection completely and it can work. (And make sure your Java code creates no garbage - which is a new type of programming in and of itself)
Maybe I’m taking your comment wrong, why is this a bad thing? What other GC’d language just lets you turn it off?
I don't think their comment is intended to be negative really -- looks more like appreciative of the option, while cognizant of the fact that using it introduces a new challenge.
It's not about turning it off. You just don't allocate on the hot path.
Edit: My bad - unfortunate wording. I didn't mean this as negative at all. It's cool (if niche) ability.
So is python. Doesn't mean you use it for your latency critical software.
Depends on the use case, but if you are working on web servers or other long lived processes the JVM is pretty close to native and can even beat native code thanks to JIT compilation.