1. 32

    Someone on Reddit put it best:

    Java devs never fail to be parodies of themselves.

    This is so spot on.

    Java is actually a good language, it’s the ecosystem that kills it for me. By “the ecosystem”, I don’t just mean the tooling (e.g. Java build tools are universally awful AFAICT), but the developers themselves. So much Java I read is just “magic”. Magic annotations, magic dependency injection, interfaces over classes that there is only one of etc. etc.

    The author points out very good failures in the way its been architected, but the code in the OP isn’t all that strange looking to me as Java, and that’s a pretty damning statement.

    I wish Java had a better audience around it. The Kotlin ecosystem seems better, but I’ve never used it.

    1. 23

      (edit: my first pass at this came off a little overly negative in a way that I think betrays the seriousness of my point)

      I’m not sure Java actually is a good language. My first introduction to Java was as the primarily language that was used in college, and even as a pretty green programmer them (I’d started writing C and C++ a few years earlier in high school, but I was definitely not a good programmer), I found it awfully questionable. In the intervening 15 years I’ve managed to avoid it, until quite recently. It’s been really eye-opening to see how the language has evolved in that time, but not really in a good way. Perhaps it’s because I’ve largely written OOP off as a bad idea that shouldn’t have ever taken off like it did, and is certainly outstaying it’s welcome, but I find that Java, and even the JVM itself, to be a masters class in solving the wrong problem in the most complex possible way. The complexity in java seems to be like glitter, you can’t touch anything with getting covered in it, and once it’s on you you’ll never get it off. Even working with people that I generally hold in high regard as developers, I see that the ecosystem has forced them into patterns and architecture that I think is questionable- except it’s not because to do anything better would be to try to work against every single design decision in the language and ecosystem. There’s simply no reasonable way to write good Java, the best you can reasonably hope for is to write as little java as possible, and hope the absurd complexity giltter doesn’t spread to all of your connected services by way of the blind “the whole world is Java” assumptions that the JVM ecosystem wants to make on your behalf.

      I say Java here, but realistically I think that all JVM languages end up falling into the same gravitational well. I’ve been using Kotlin lately, and from what I’ve seen of Scala and Clojure they are all infected by the same inescabable fractally wrong view of the world that is imposed by the JVM, by way of the JVM itself being born from the primordeal ooze of bad decisions and oop-kool-aid that led to Java in the first place. Kotlin in particular suffers from being not only unable to escape the Java ecosystem, but also from generally being a poorly designed language. Everything it adds to java, it adds in such a superficial and impotent way that a slight breeze knocks over the facade and you realize you’re stuck back in the Kingdom of the Nouns all over again.

      1. 7

        I tend to agree about the java part. The constructs at your disposal requires you to write very very verbose code, even for simple things. But I disagree about the JVM bit. I find it a pretty good runtime system. Although it tend to eat its fair share of RAM, the GCs and the JIT are first class. Overall, you get pretty decent perf without too much thought. Also, Having written a lot of clojure, it’s vastly different from java, couldn’t be further from the Kingdom of the Nouns.

        1. 13

          The JVM feels to me like it was written for a world that just didn’t really ever happen. It promised cross platform compatibility, that never really materialized since there are only two real meaningful places where the jvm is heavily used these days (x86 linux servers and arm Linux phones). Even where the jvm itself is running on multiple platforms, it’s not running the same workloads across it. We would have been every bit as well off with a toolset that made native cross compilation feasible (go and rust), and probably would have been no worse off even with the old C and C++ cross compilation story. Love it or hate it, JavaScript is what actually fulfilled the promises that java made and never was able to keep.

          Other promises that JVM made either never made sense- language interoperability always existed before java, and exists outside of it now. All the JVM did was fracture the environment by making it nearly impossible to produce native code- it’s a vampire if you look at it in terms of interoperability, unless you want to use gcc to compile your java code. The isolation and runtime management is, consistently, 90% of the work involved in deploying any java application I’ve used, and at the end of the day everyone does that work twice now because most workloads are getting deployed in cloud native containers anyway- so the JVM is superfluous there. GC is a pain in the JVM and has been done as well elsewhere without requiring the rest of the baggage of its runtime and jitter.

          Looking at performance, I’m dubious that it has much going for it. It’s still not a contender in the same space as C or C++, and in many cases the pain of native interop make it slower than even python because python can rely on native code for a lot of heavy lifting. I’ve even seen fairly well optimized JVM code fail to keep up with reasonably (perf) naive Haskell.

          Even with instrumentation, the supposed killer feature of the jvm, I have yet to see anything I can’t get out of a native application with native tooling and instrumentation, and the case is getting weaker by the day as more and more application telemetry moves up and down the stack away from the application itself and into either tracing layers in front of services, or tooling built around things like ebpf that live very low down in the system and allow you to instrument everything.

          The JVM is at best a middle-of-the road performance language with a largely superfluous ecosystem. It might have been a good idea when it was created, and I have no doubt a lot of smart engineering went into its implementation, but it’s time we give it up and realize it’s a sunken cost that we need to leave to the history books as a quirky artifact of the peculiar compute and business environment of the early 90s.

          1. 3

            Clojure’s okay (and still way better than Java, IMO) but suffers from pretty poor error handling compared to other Lisp environments.

          2. 4

            I really don’t like how Java makes optimization of its runtime overcomplicated, then has the gall to make you deal with the complexity. There is no reason to be manually tuning GC and heap sizes when every other runtime, including CLR implementations, can deal with this efficiently and automatically. They might be complex, unlike the JVM, they’re not complex and making you deal with that complexity.

            1. 3

              Just curious what you dislike about Kotlin’s design? It seems like you make two points: that Kotlin can’t escape Java and, separately, that it’s poorly designed. I agree with the former, but in light of the former, I find Kotlin to be pretty well-designed. They fixed Java’s horrible nullness issues and even kludged in free functions to the JVM, which is neat. Data classes are a band-aid, but still help for the 80% of cases where they can apply. Same with sealed classes (I’d much prefer pattern matching akin to Rust, Swift, OCaml).

              1. 11

                My biggest issue is that everything feels like a kluge. Null tracking at the type level is fine, but they didn’t really go far enough with the syntax to make it as useful as it could have been- rust does better here by allowing you to lift values from an error context inside of a function. The language tries to push you toward immutability with val and var, but it’s superficial because you’re getting immutable references to largely mutable data structures without even getting a convenient deep copy. Extension methods are a fine way of adding capabilities to an object, but you can’t use them to fulfill an interface ala go, or outright extend a class with an interface implementation ala Haskell typeclasses, so you’re left with basically a pile of functions that swap an explicit argument for a this reference, and in the process you are conceptually adding a lot of complexity to the interface of an object with no good associated abstraction mechanism to be explicit about it. Even the nature of the language as a cross platform language that can target jvm, llvm, and web asm seems fundamentally flawed because in practice the language itself seems to lack enough of a stand alone ecosystem to ever be viable when it’s not being backed up by the jvm, and even if you did have a native or web ecosystem the design choices they made seem to be, as far as I can tell, about the worst approach I’ve ever seen to cross platform interoperability.

                Ultimately the features they’ve added all follow this pattern of having pulled a good idea from elsewhere but having an implementation that seems to not fulfill the deeper reason for the feature. The only underlying principle seems to be “make java suck less”. That is, of course, a bar buried so low in the ground it’s in danger of being melted by the earths core, and I would say they did cross over that bar- kotlin does suck less than Java, but what’s astonishing to me is how for such a low bar they still seem to have managed to cross over it just barely.

                1. 4

                  I share every single one of those sentiments, but I’ve excused many of them specifically because of the limitations of being a JVM language. (interfaces at class definition, no const, no clones)

                  I’ve almost taken it upon myself to periodically go and correct people in the Kotlin subreddit that val does not make things immutable, and that Kotlin has not cured the need for defensive copies in getters.

                  I think the idea of making Kotlin cross-platform is totally stupid for those same reasons you point out. All of that is a limitation of wanting to be on the JVM and/or close to Java semantics. Why they hell would you want to export that to non-JVM platforms?

                  Thanks for the response.

            2. 12

              I have a completely opposite opinion. Java is not the best language out there, I prefer Scala and Kotlin, but the selling point for me is the ecosystem: great tooling (tools simply work in lots of cases), great platform (lots of platforms are covered, really awesome backward compatibility, stability), great API (it might be far fetched, but I have a feeling that Java’s stdlib is one of the most feature-packed runtimes out there, if not the most?). The “magic” is the same problem as everywhere else; it’s magic until you know the details. Also just because there’s a dependency injection trend in the backend development world, it doesn’t mean that you should use DI in different projects. Interfaces of classes are a Java thing; it wouldn’t exist if the language was more sophisticated.

              Maybe I’m comparing Java’s ecosystem to C++ – because with C/C++, the tooling is in appalling state, the standard library is awful and I’m not sure what it tries to achieve at times. So I guess I have a very low standards to compare to :P

              1. 3

                Java has an incredibly rich ecosystem, that’s true. What annoys me though, is that every single tool in the Java ecosystem is written in Java, meaning you have a ton of CLI programs which take a couple of seconds just to heat up the JVM. Once the JVM is hot and ready to actually do work, the task is over and all that JIT work is lost.

                At least C/C++ tooling is fast :p

                1. 2

                  That’s true, JVM startup time is a pain point. But there are several walkarounds for that:

                  • some tools use build server approach (gradle), so that startup time is less slow ;)
                  • some tools like sbt (scala build tool) use build server + shell approach, and it’s possible to use a thin client to invoke a command on this build server (e.g. ‘sbt-client’ written in rust). This makes Scala compilation take less time than compiling a C++ application.
                  • GraalVM native-image is pushed right now, which allows to compile JVM (java, kotlin, scala) application to native code without the use of JRE. This allows writing tools that have non-noticeable startup time, just like tools written in e.g. Go. I was testing some of my small tools with it and it was able to compile a small Clojure app to native code. This tool had same startup speed than a C++ application. Unfortunately, GraalVM can’t compile every app yet, but they’re working on it ;)

                  Also C/C++ tooling is fast, but C++ compilation is nowhere near being fast. Changing one header file often means recompilation of the first half of the project. Bad build system (e.g. in manually written Makefiles) that doesn’t track dependencies properly sometimes produces invalid binaries that fail at runtime, because some of the compilation units weren’t recompiled when they should be. It can be a real mess.

              2. 9

                I wish Java had a better audience around it.

                That doesn’t seem terribly likely to happen.

                1. Java was never aimed at programmers who value power, succinctness, and simplicity - or programmers who want to explore paradigms other than OO (although newer versions of the lanaguage seem to be somewhat relaxing the Kingdom of Nouns[1] restrictions). It was intended to improve the lives of C++ programmers and their ilk[2].

                2. Java is frequently used in large, corporate, environments where programmers are considered (and treated as) fungible. “The new COBOL”, as it were[3].

                3. The JVM itself allows programmers not falling into (1) and (2) to abandon the Java language itself - Clojure, Scala, Kotlin, and Armed Bear Common Lisp spring (heh) to mind. Most of the best JVM programmers I know aren’t actually using Java. Most of the ‘Java shops’ I’ve worked with in the past decade are now, really, ‘JVM shops’.

                My observation is that most - to be clear, not all - people who continue using Java in 2020 are forced to do so by legacy codebases, and / or companies that won’t let them adopt new languages, even JVM languages. I honestly believe this is the proximate cause of the audience problem you describe. (Not the root cause, mind you).

                Edited: I’m amused by the fact that the first two, nearly concurrent, replies both reference Yegge’s nouns blog post :)

                [1] http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html

                [2] “We were after the C++ programmers. We managed to drag a lot of them about halfway to Lisp.” - Gosling. http://www.paulgraham.com/icad.html

                [3] https://www.infoworld.com/article/3438158/is-java-the-next-cobol.html

                1. 5

                  Java is a horrible language. No mortal can mentally hold on to a class hierarchy where inheritance is more than a few levels deep. Furthermore it is a bad way to add “another layer of abstraction” , because it just paints you more and more into a corner.

                  (Clojure is a great language, you can add layers of abstraction to solve problems, without just digging yourself deeper.)

                  1. 3

                    But one can write Java programs without abusing inheritance, and even pretty much without inheritance.

                    1. 1

                      Yes. I agree that Java is a horrible language, but class inheritance doesn’t even make the list of things I find poor about it.

                  2. 3

                    I don’t agree that Java is a good language at all, but I wanted to hard-agree at the distaste for magic annotations and DI frameworks.

                    1. 1

                      Java is actually a good language, it’s the ecosystem that kills it for me. By “the ecosystem”, I don’t just mean the tooling (e.g. Java build tools are universally awful AFAICT), but the developers themselves. So much Java I read is just “magic”. Magic annotations, magic dependency injection, interfaces over classes that there is only one of etc. etc.

                      I don’t agree with this - in my experience, “magic” is “code that integrates my application-specific functionality with a massively feature-rich general purpose framework”. It’s magic in the sense that you need to understand the enclosing framework to understand why those annotations are there and their semantics, but they do real work. Work I’d have to do myself if I didn’t use them.

                      You don’t see this much in other languages, but it’s because “massively feature-rich general purpose frameworks” aren’t common outside of Java. The ones that do exist seem to have punted on important architectural decisions - you don’t need a dependency injection framework if your data layer objects are just global values (I’m looking at you, Django).

                      I’ve definitely felt this urge before - why do I need all this spring crap? Then I end up re-implementing half of that magic myself and not as well.

                      1. 1

                        What language has tooling that you like? Curious what you are comparing the Java build tools with

                        1. 1

                          Rust and Go (at least since Go modules) I find both intuitive and fast. I am still not sure how to properly build a Java application without an IDE.

                          1. 1

                            $ ./gradlew build

                      1. 7

                        Very good summary with a ton of interesting links, I’ll probably get back to them later.

                        One point the author missed however, is about organizational complexity. Quite often in my experience, the decision is made to move to a distributed system to reflect the structure of the organisation. That’s a solution (with its set of trade-offs) to the problem of “how to have 50 engineers working on the same project?”.

                        1. 4

                          Agreed I think I touch on that briefly as one of the real reasons people currently use microservices where in theory with better tools for separately updated hot-reloaded dynamic libraries with stable interfaces could solve the problems people split services based on teams for without adding the asynchrony.

                        1. 25

                          Hm, a language that uses product types instead of sum types to represent “result or error” is certainly not doing exceptions right.

                          1. 6

                            Exactly

                            If a function returns a value and an error, then you can’t assume anything about the value until you’ve inspected the error.

                            You can still use the returned value and ignore the error. I’m not calling that “right”

                            1. 2

                              That is intentional and useful. If the same error has different consequences when called by different callers, the error value should be ignored. However, when writing idiomatic Go, you generally return a zero value result for most error scenarios.

                              Assigning _ to an error (or ignoring the return values altogether) is definitely something that could be trivially caught with a linter.

                              1. 1

                                I still think it’s a mistake to allow both to coexist. In this case, you need an additional tool to catch something. Whereas with proper sum types, it is simply not possible to access a return value if you get an error. What you do with that is up to you.

                                val, err := foo()
                                if err != nil { /* log and carry on, or ignore it altogether */ }
                                bar(val) // <- nothing prevent you from doing that, and it very likely a mistake
                                
                            2. 4

                              Thought the same. It gets this right:

                              Go solves the exception problem by not having exceptions.

                              And here it goes wrong:

                              Instead Go allows functions to return an error type in addition to a result via its support for multiple return values.

                              1. 4

                                Another case of “Go should have been SML + channels”, really. That would have been as simple, cleaner, and less error prone. But what can you expect from people who think the only flaw of C is that it doesn’t have a GC…

                                1. 1

                                  Not just GC but also bounds checking.

                                  1. 1

                                    That’s so obvious that I forgot about it. I don’t know of any modern language that doesn’t have a (bound-checked) array type. On the other hand they decided to keep null nil…

                                    1. 1

                                      You have to keep in mind that C is most certainly not a modern language & they were intentionally starting with C & changing only what they felt would add a lot of value without adding complexity.

                                      1. 1

                                        For sure, I just don’t get why anyone would want that. 🤷 I guess.

                                        1. 2

                                          Primarily linguistic simplicity in the service of making code easier to read.

                              1. 9

                                I worked in haskell on a system not too far from the one you describe (also CQRS based). There is definitely a fine balance to find between encoding invariants into the type system and adding some tests. Sometimes, it’s possible to have the compiler do the work for you, but it’s not always desirable. The two main drawbacks are the increase in compile time and the steeper learning curve for this bit of the codebase.

                                For the events, we had many issues with backward compatibility for them. I believe tests may be faster and simpler to maintain. Another approach would be to encode the version in the event type, that’s quite easy. But then you need to have some custom logic to convert each version into a different type that is then used once you’re past the boundary of the DB. It’s definitely doable, but it’s quite a lot of work.

                                For all the other specific questions you’re asking, I believe you’ll be better off with some testing instead of trying to put all of that in the type system. Also, I have no idea if it’s even possible when it comes to time (timeout, throttling).

                                The article mentionned by u/iocompletion (https://lobste.rs/s/olecii/experiences_moving_from_tests_strong#c_5w8nap) is a very good read for a middle ground approach which I believe can get you very far.

                                1. 3

                                  I really enjoyed this talk about how types and tests complement each other: https://lobste.rs/s/hiwykl/types_testing_haskell