dkarl 2 days ago

Definitely underrates the impact of annotations. I'm personally not a fan of the way annotations are used to implicitly wire together applications, but I have to admit the impact. Maybe 5/10 is fair in light of the wide range of positive and extremely negative ways annotations can be used.

So many of these features were adopted after they were proven in other languages. You would expect that since Java took such a slow and conservative approach, it would end up with extremely polished and elegant designs, but things like streams ended up inferior to previous developments instead of being the culmination. Really disappointing. Java is now a Frankenstein's monster with exactly as much beauty and charm.

  • dhosek 2 days ago

    I used to joke that the direction spring was heading was that you’d have an application be a boilerplate main method with a dozen lines of annotations. Then I actually encountered this in the wild: we had an app that sent updates from db2 to rabbitmq, and the application literally was just configuration via annotations and no actual Java code other than the usual spring main method.

    • 9dev 2 days ago

      Is that strictly bad, though? Being able to run an enterprise service by setting configuration values declaratively, and get all the guarantees of a well-tested framework, seems like a pretty good thing.

      Yes, it’s weird how that’s still Java, but using standard components and only using code as glue where it’s absolutely necessary seems very similar to other engineering disciplines to me.

      • SkiFire13 2 days ago

        I think the general adversity against this specialized configurations is that they often tend to be fairly limited/rigid in what they can do, and if you want to customize anything you have to rewrite the whole thing. They effectively lock you into one black box of doing things, and getting out of it can be very painful.

        • 2muchcoffeeman 2 days ago

          Spring boot just provides what they think are reasonable defaults and you provide some specifics.

          You can always inject your own implementation if needed right?

          • arcbyte a day ago

            Spring is written by a committee of junior developers trying to implement the ideas from a committee of slightly less junior developers. Which is to say it's a huge unintelligible mess. As the other commenter attest, it works really well if you need the one thing it does - unfortunately the moment you need something slightly different you are forced to dig deep into the insane internals and it is a disaster. This moment comes very quickly too in every project because it was written by people with no experience in real projects.

          • jsight 2 days ago

            In theory, yes. In practice, I've found that things get really complicated as soon as you start trying to interact with the spring lifecycle. Figuring out how to customize things and manage priority is the trickiest thing in Spring, IMO.

            • lenkite 2 days ago

              Please use the facilities of the framework for debugging any lifecycle issues. I was surprised when I found out that people did not use `--debug` for example or enabled logging of application startup events.

              If you prefer GUI, Intellij even has a Spring Debugger: https://www.jetbrains.com/help/idea/spring-debugger.html

              • Anonyneko a day ago

                I stopped using Spring Debugger because for some inexplicable reason enabling it makes breaking at the breakpoint take minutes instead of seconds. For quite a while I didn't realize this was the problem, as IntelliJ started having it turned on by default at some point.

              • jsight 20 hours ago

                Fair point, honestly. It could easily be a skill issue from not doing this particular type of thing all that often.

      • dhosek 2 days ago

        Oh, I think it’s quite wonderful really. There are cases where the limited nature of some configuration-based things ends up being a mess (one that comes to mind is a feature in Spring Data where you can extend a DAO bean into a rest service through annotations, but it turns out that this feature (at least when I last tried working with it), is so rigid as to be nearly useless in actual practice. But our codeless application was a bit of brilliance, I think.

    • bdangubic 2 days ago

      this is exactly why spring succeeded. I need to run a scheduled job, @EnableScheduling then @Scheduled(cron = “xxxxxx”) - done. I need XYZ, @EnableXYZ the @XYZ… sh*t just works…

      • taftster 2 days ago

        And then I realize I need to change that schedule. And would like to do it without recompiling my code. Oh, and I need to allow for environment specific scheduling, weekdays on one system, weekends on others. And I need other dependencies that are environment specific.

        I much prefer Spring's XML configuration from the old days. Yeah, XML sucks and all that. But still, with XML, the configuration is completely external from the application and I can manage it from /etc style layouts. Hard coding and compiling in dependency injection via annotations or other such behaviors into the class directly has caused me grief over the long term pretty much every time.

        • sass_muffin 2 days ago

          I do realize you were intending to give examples of why you don't think annotations aren't very extensible, but it is an odd example as all those things can still be achieved via annotation, since the annotations can accept values loaded from env specific properties.

          • le-mark 2 days ago

            Exactly this, it’s great fun to have a surface level understanding of a topic and post derisively for internet points; rather then spend the time and effort to actually learn about the subject at hand!

            • taftster 2 days ago

              I'm not digging for "internet points". Yes, superficial replacement values can be retrieved from the environment. But I guess we have to give you a more imagined or sophisticated example then to make the point to you?

              How about varying implementations of a service interface. Let's say I have a Scheduler interface and I want to have multiple implementations; maybe one is CronScheduler, another is RandomScheduler, another is BlueMoonScheduler. Each of these schedulers have their own properties and configuration values. I might want to choose, per environment or deployment context, which service implementation to use.

              Annotation configuration makes this (near?) impossible to dynamically wire and configure these scenarios and make them tailored to the environment or deployment scenario. Annotations are generally "static" and do not follow a configuration-as-code approach to application deployment.

              An external configuration file, as offered by Spring's original (less "favored") XML style, allowed a more composable and sophisticated configuration-as-code approach. Maybe that's a negative, putting that much power into the hands of the service administrator. But as I stated originally, in my experience, having statically defined annotation driven configuration and/or dependency injection has caused more problems than it has solved.

              • tpmoney 19 hours ago

                > Annotation configuration makes this (near?) impossible to dynamically wire and configure these scenarios and make them tailored to the environment or deployment scenario. Annotations are generally "static" and do not follow a configuration-as-code approach to application deployment.

                Off the top of my head, you could drop a `@Profile` or `@ConditionalOnProperty` annotation on each of your different schedulers and pick them at launch time simply by adding the profile you want to the arguments or the environment. That assumes you want to choose one for the whole app. If you want to have different ones in different locations, you can dynamically load beans in code. Or if you want them loaded entirely with annotations, you could define differently named beans for each context, and include `@Qualifier`s on them and in the locations they're being used.

                Which isn't to say that annotations are perfect, but dynamic runtime configuration is sort of core to how spring operates, and annotations are no exception to that.

              • bdangubic 2 days ago

                Annotation configuration makes this (near?) impossible to dynamically wire and configure these scenarios and make them tailored to the environment or deployment scenario

                all of your scenarios are trivial to implement with annotations

          • vlovich123 2 days ago

            > env specific properties

            And then if you want to change a value at runtime you have to restart the executable?

            • petersellers 2 days ago

              Most web application servers work this way. It also works really well in practice using modern CD tools - update your configuration and perform a gradual rollout of all your application servers to reflect the updated configuration.

              • bdangubic 2 days ago

                people will argue crazy sht here on HN like changing schedule is a thing* that needs instant gratification and god forbid you have to bounce a service to read an updated configuration … :)

            • happymellon 15 hours ago

              I have to deal with this at work for rolling db credentials, actuator refreshes are fine for this purpose.

              Would be nicer if we could handle creds like it wasn't 1992, but this does the job too.

        • nunobrito 2 days ago

          The only real-world usage I see for annotations are in GSON (the @Expose) and JUnit with @Test.

          Never really came across with any other real cases where it solves a pressing issue as you mention. Most times is far more convenient to do things outside the compiled code.

          • taftster 2 days ago

            Agreed. These are good examples where annotations seem like a good fit. Being able to tell a processor that a method or field is "special". Test methods, serializable hints, etc.

            It's kind of like, when annotations were delivered to Java, lots of projects thought they were just the next greatest thing. I followed right along at the time, as well.

            But over time, finding the value in the configuration-as-code approach to application deployment, I definitely feel that annotations have been abused for many use cases.

        • bdangubic 2 days ago

          all trivial things you are listing, every single one…

        • happymellon 15 hours ago

          You do know about environmental variables?

        • paulddraper 2 days ago

          So…pass that variable to your annotation.

          I’m not seeing the point?

      • vbezhenar 2 days ago

        Yeah, it works until it isn't. And they good luck debugging it. I'd prefer simple obvious linear code calling some functions over this declarative magic any day.

            cronService.schedule("xxx", this::refresh);
        
        This isn't any harder than annotation. But you can ctrl+click on schedule implementation and below easily. You can put breakpoint and whatnot.
        • bdangubic 2 days ago

          never had any issues debugging as I am never debugging the scheduler (that works :) ) but my own code.

          and what exactly is “cronService”? you write in each service or copy/paste each time you need it?

          • vbezhenar 2 days ago

            `cronService` is an injected instance of some class, probably provided by framework. My point is to demonstrate alternative, imperative way of defining cron tasks, as compared to declarative one.

            • vips7L a day ago

              A better name would probably be CronScheduler or something else. “Service” is always overused and doesn’t actually describe or mean anything.

          • alex_smart 2 days ago

            You can write your own libraries?

            My goodness. What a question!

            • throwaway7783 2 days ago

              Whole point of spring is so you don't have to write your own libraries. Batteries included and all.

              • alex_smart 2 days ago

                What if I told you - you can use a batteries-included framework, and still write your own libraries specifically only for things you want your own version of and want to share across projects?

                The problem isn't that I don't know how to use a batteries included framework. The problem is that you guys don't know there is even an option to reuse your code by writing libraries.

                • throwaway7783 a day ago

                  Why are you assuming you cannot write your own library and use it with Spring? It's not an either/or, just like most frameworks are. You are framing it as if there is only one way to do it in spring.

                  Please do not project things like "you guys don't even know..". I'm one of "you guys" , and have built production code in a variety of languages and frameworks. So this "you guys" knows exactly what he/she is talking about.

                  • alex_smart a day ago

                    > Why are you assuming you cannot write your own library and use it with Spring?

                    I am not. I am literally saying the exact opposite.

                    • throwaway7783 a day ago

                      Okay, that's fair. I don't even know anymore what point you are trying to make.

                      • alex_smart a day ago

                        I was making exactly the point that you can use your own libraries along with a batteries included framework like Spring Boot.

                        I don't even understand what the source of confusion is. I literally said exactly the same thing in the comment you first you replied to.

              • bdangubic 2 days ago

                @alex_smart hand rolls it all :)

            • bdangubic 2 days ago

              you write your own database driver? encryption?

              • alex_smart 2 days ago

                Why would I write my own database driver or encryption just because I wanted to implement my own "cronService"?

              • blackoil 2 days ago

                If you want to build an application from scratch, you must first create the universe.

                • alex_smart 2 days ago

                  All this because I told one guy who asked "what exactly is “cronService”? you write in each service or copy/paste each time you need it?" that they can reuse code by writing a library instead of copy/pasting it?

                  If this is the level of incompetence encouraged by a framework, I would avoid using it just to avoid the chance of hiring people like you.

                  Just kidding. Spring boot is great. But yeah, I would fire people with this attitude without blinking an eye.

                  • bdangubic a day ago

                    * Why would I write my own database driver or encryption just because I wanted to implement my own "cronService"?*

                    how do you decide whether you will write your own or pull in a dependency? this is a legit question. you did start this with writing your own “cronService” (which is about as insane as writing your own database driver) so asked about it.

                    • alex_smart a day ago

                      > you did start this with writing your own

                      I really did not. I only said that if you were to create your own cronService, you can reuse it by creating your library rather than copy pasting code (which is obviously insane).

                      > which is about as insane as writing your own database driver

                      No, it is not. Spring Boot’s support for async jobs and scheduled jobs is lacking. A lot of people roll their own. Including yours truly.

                      It is also much easier than writing a database driver so there is that.

                      • bdangubic a day ago

                        Spring Boot’s support for async jobs and scheduled jobs is lacking.

                        Can you elaborate? What exactly is lacking and what version of Spring are you using?!

                        • alex_smart 13 hours ago

                          Compare with the functionality offered by async job systems of other full stack frameworks - eg django with celery and rails with solid-queue. It’s not even close.

                          I am on the latest version of Spring Boot.

                          • bdangubic 8 hours ago

                            I am not saying there aren't more robust scheduling libraries, people still use Quartz a lot in the Java ecosystem - was just wondering what specifically are you up against that you cannot solve with Spring's scheduling?

      • skeletal88 2 days ago

        Then you need to deploy it on multiple nodes and neex to make sure it only runs once for each run of the cron, etc.

        • didntcheck 2 days ago

          I believe Quartz is the go-to solution for this. It's not part of Spring but it offers a similar annotation-driven interface, but with distributed locking via a database

          • bdangubic a day ago

            Absolutely! But Quartz is also quite heavy. If all you need is to ensure scheduled jobs run in a clustered environment there are more “lighweight” options

        • bdangubic 2 days ago

          while not working on out of the box clustered this is trivial issue to address

  • nine_k 2 days ago

    Absolutely. It seems that the author never touched Spring, for instance, or a dependency-injection framework of any kind. Annotations allow to do things in a completely different way, removing tons of boilerplate.

    I'd give annotations 9/10 at least.

    (And I lost the interest in the rest of the article, given such a level of familiarity with the subject matter.)

    • __float 2 days ago

      My experience using Dagger (2) was so unpleasant that it really soured me on the possible uses of this feature.

      I understand the benefits of dependency injection, but to be totally honest I'm more likely to take the Go-style approach of wiring it all up manually, even if it's a bit of extra boilerplate. The indirection and abstractions built up in DI frameworks is rarely worth it IMO.

      • malfist 2 days ago

        Dagger is an absolutely pain in the ass. Its also damn good. Once you understand the archane syntax of their error messages its a lot easier (but still not easy) to use.

        Harder than spring, but less magic than spring

      • vips7L a day ago

        Dagger is by far the worst UX experience for a DI framework. Really you should try anything else. For compile time DI like dagger you can try Avaje Inject:

        https://avaje.io/inject/

    • Groxx 2 days ago

      and that's before even touching on the compilation steps they can add, which are a pluggable codegen and macro system that is also integrated into IDEs, which is completely missing from almost every other language.

      • jayd16 2 days ago

        Good stuff. If that excites you, check out Roslyn source generators too.

    • nmadden 2 days ago

      Do you really think that in 26 years of professional Java programming I’d have never touched Spring? I’ve been using Spring since it was first released. I’ve found CVEs in Spring (https://spring.io/security/cve-2020-5408). Trust me when I say that my dislike for Spring (and annotations) is not based on ignorance.

      • nine_k a day ago

        It's perfectly possible to work for 26 years with Java and not ever seriously touch Spring, or AWT, or Swing, or the EE bits, etc. Java is sprawling, and a corporate backend developer and a mobile frontend developer may have little intersection in the big libraries, and even the approaches, they use.

        It's perfectly fine to never have touched Spring. What surprised me is not acknowledging that not only are annotations used to do clerical things like @Override or @Deprecated, and not only to do some weird wiring with @Injected or @RequestBody, but allow to add large custom steps in transforming the code. Annotation processors are a huge comptime interface that can do, and routinely does, wild things, unimaginable in Go, the kind of code transformations you would expect in Lisp or Python.

        I suspect the latter should have interesting security implications, too.

        • nmadden a day ago

          Java is sprawling now. It wasn’t 26 years ago.

          • nine_k a day ago

            Just 20 years ago it was already sprawling.

      • ruszki a day ago

        But your dislike can be a lot of other things which are not objective the slightest sense. I mean, your whole article is a subjective piece. Also, stating about something that you “dislike” something as large as Spring as a whole is usually a huge red flag for anybody, just like how liking it without reservations is also a huge red flag.

        • nmadden a day ago

          Yes, of course it’s (largely) subjective. But I have actually read much of the source code of Spring. I know it _very_ well.

          • ruszki a day ago

            From my point of view, it seemed from you article that you didn't even really understand functional languages in two decades (at least Haskell), which it seemed you to try to state also. So that doesn't matter too much.

            Anyway, I just wanted to say, that it's totally pointless to state something like "I know it well"... Say what's your problem with it, "I don't like it" doesn't add anything to the conversation. I'm quite sure whatever you would say as problems, most people would agree, maybe there would be even tips under it how to prevent it. That will never happen with the kind of comments which you made above.

      • AtlasBarfed a day ago

        Spring was a great DI framework that I only use for DI.

        All the big magic annotations are for Enterprise.

        Okay, I've occasionally done a couple spring boot rest, which was ... Fine ... As long as you didn't have to do anything even remotely and complicated, but it keeps you in this weird box of middle performance.

        If you've ever been on any large Enterprise spring Java project, you know what the future of vibe coded enterprise is bringing.

  • vbezhenar 2 days ago

    I don't really feel that Java uses proven features.

    For example they used checked exceptions. Those definitely do not seem like proven feature. C++ has unchecked exceptions. Almost every other popular language has unchecked exceptions. Java went with checked exceptions and nowadays they are almost universally ignored by developers. I'd say that's a total failure.

    Streams another good example. Making functional API for collections is pretty trivial. But they decided to design streams for some kind of very easy parallelisation. This led to extremely complicated implementation, absurdly complicated. And I've yet to encounter a single use-case for this feature. So for very rare feature they complicated the design immensely.

    Modules... LoL.

    We will see how green threads will work. Most languages adopt much simpler async/await approach. Very few languages implement green threads.

    • rzwitserloot 2 days ago

      > For example they used checked exceptions.

      Those are from java 1.0 and thus don't appear to be relevant to the part of the discussion I think this part of the thread is about (namely: "Why doesn't java crib well designed features from other languages?").

      > Java went with checked exceptions and nowadays they are almost universally ignored by developers.

      They aren't.

      Note that other languages invented for example 'Either' which is a different take on the same principle, namely: Explicit mention of all somewhat expectable alternative exit conditions + enforcing callers to deal with them, though also offering a relatively easy way to just throw that responsibility up the call chain.

      The general tenet (lets lift plausible alternate exit conditions into the type system) is being done left and right.

      • vips7L a day ago

        All modern languages are adopting a checked error system: Rust, Swift, Kotlin, Zig, Gleam; they all have some type of error you must handle.

        The problem with Java is that they haven’t added the syntax to make dealing with those errors easy. It’s boiler plate hell.

        • rectang a day ago

          Yeah, unwrapping a Result in Rust can often be done via a single character, the chainable `?` operator.

          That’s not the only issue, though: Java also shunts both checked and unchecked exceptions through the same mechanism, conflating them. It’s no wonder that Java’s problematic implementation of checked exceptions has poisoned people against the concept.

      • jayd16 2 days ago

        I suppose you could actually solve it by having a promise that catches the exception like your suggesting with an either result. The C# Task API can do this. It has it's own headaches where now the developer has to pay attention to observing every exception.

        Java could do something similar but they have enough promise types already.

    • bigstrat2003 2 days ago

      > For example they used checked exceptions. Those definitely do not seem like proven feature.

      Checked exceptions are an awesome feature that more languages should have. Just like static typing is a good thing because it prevents errors, checked exceptions are a good thing because they prevent errors.

      • rectang 2 days ago

        Idealized checked exceptions are isomorphic to Rust's `Result` type, which is great.

        Java's implementation of checked exceptions has some issues, though.

        * "Invisible control flow", where you can't tell from the call site whether or not a call might throw (you need to check the signature, which is off in some other file, or perhaps visible in an IDE if you hover).

        * Java has both checked and unchecked exceptions, but they go through the same try-catch mechanism, failing to make a clean distinction between recoverable errors and unrecoverable bugs. (In e.g. Rust and Go, recoverable errors go through return values but unrecoverable errors go through panics.)

        In the end, Java's exception design simultaneously requires a lot of effort to comply with, but makes it difficult to understand when you've successfully locked things down.

        • vips7L a day ago

          Do you not have to check the signature to see what a function can return when using Results? It’s off in another file too.

          > failing to make a clean distinction between recoverable errors and unrecoverable bugs

          Recoverability is context specific. One persons panic may just be another error case for someone else. I think this is one thing that programmers miss when talking about this topic. It is really up to the caller of your function if something should panic. You can’t make that decision for them.

          • rectang a day ago

            The point of avoiding invisible control flow is not to identify the type of the error, but to identify all locations in the code where recoverable errors may occur.

            > One persons panic may just be another error case for someone else.

            We can make a strong distinction between recoverable errors which the programmer anticipated (e.g. this I/O operation may fail) versus unrecoverable errors resulting from unanticipated bugs which may leave the process in an unsound state, such as divide-by-zero or out-of-bounds array access[1].

            There are some problem domains where even unrecoverable errors are not allowable, and programmers in those domains have to grapple with the panic mechanism.

            But for the rest of us, it is useful to be able to distinguish between recoverable and unrecoverable errors — and to know how we have handled all possible sites which could result in recoverable errors.

            [1] Joe Duffy explains it well: https://joeduffyblog.com/2016/02/07/the-error-model/#bugs-ar...

        • samus 2 days ago

          > * "Invisible control flow", where you can't tell from the call site whether or not a call might throw (you need to check the signature, which is off in some other file, or perhaps visible in an IDE if you hover).

          Never found this this to be a problem. It is really common to all implementations of exceptions, not just checked ones. And when you write code the compiler will yell at you. In monadic code,

          • rectang a day ago

            Invisible control flow is common to all implementations of unchecked exceptions (Java, C#, C++, Python, Ruby, etc). It means that any code, anywhere, at any time, can throw an exception which may represent a recoverable error.

            People are used to that, and one common strategy is to not worry too much about handling individual exceptions but to instead wrap a big `try` block around everything near the outer boundary of your code. It’s good enough for many purposes and yields a high initial development velocity, but is comparatively fragile.

            With Languages like Rust, Go, and Swift, only unrecoverable errors trigger the panic mechanism. Every call site where a recoverable error may occur is identifiable — in Rust via Result, `unwrap()`, the `?` operator, etc, in Go via returned Err (though unlike Rust you can discard them silently), and in Swift via the `try` operator.

            You can still develop quickly by just unwrapping every Result, but unlike languages with invisible control flow, you can easily audit the codebase and go back to harden every site where a recoverable error may occur — yielding a level of robustness which is difficult to achieve in languages with unchecked exceptions.

            • peterashford 21 hours ago

              As someone who uses Go professionally now and Java previously, I disagree with your take on unchecked exceptions. I think that Panics and errors is worse than just using exceptions. I think the latter is just simpler to deal with. At the end of the day, when error handling is complex, its complex with either approach - you have to think carefully about what's happening in the problem domain and come up with a robust solution. What the code level adds is complexity: the Go approach is more complex and consequently worse. I love Go coding but its error handling sucks and I would gladly have the Java approach instead.

    • JavierFlores09 2 days ago

      Stuart Marks and Nicolai Parlog recently had a discussion about checked exceptions in the Java channel [0]. In short, while they mentioned that there are certainly some things to improve about checked exceptions, like the confusing hierarchy as well as the boilerplate-y way of handling them, they're not necessarily a failed concept. I do hope they get to work on them in the near future.

      0: https://www.youtube.com/watch?v=lnfnF7otEnk

      • vbezhenar 2 days ago

        They are absolutely failed concept in Java. Every first popular library uses unchecked exceptions, including famous Spring. Java streams API does not support checked exceptions. Even Java standard library nowadays includes "UncheckedIOException". Kotlin, Scala: both languages grown from JVM and do not support checked exceptions.

        • samus 2 days ago

          Spring has a special error handling strategy where the whole request is allowed to fail, punting error handling off to the caller.

          A lot of code that throws checked exceptions is simply dangerous to use with Java streams because the execution order of stream operation is not obvious and possibly non-deterministic. For this reason, streams were never intended to also handle errors. Reactive frameworks are much better at that.

          The UncheckedIOException is for situations where you really cannot throw a checked exceptions, such as inside an iterator. Which might lead to ugly surprises for the API user;

          • vbezhenar 2 days ago

            > A lot of code that throws checked exceptions is simply dangerous to use with Java streams because the execution order of stream operation is not obvious and possibly non-deterministic.

            From the type system PoV, they could have just written something like `interface Runnable<X> { void run() throws X; }` and now `forEach` would have been written like `<X> void forEach(Runnable<X> r) throws X`. And repeat that for all stream operations, promoting `X` everywhere, so if your mapper function throws SQLException, the whole pipeline will throw it.

            It even works today with some limitation (there's no way for `X` to get value of `SQLException|IOException` union type), but with some generic improvements it could work.

            But they decided to not touch this issue at all. So now people will fool compiler with their tricks to throw checked exceptions like unchecked (or just wrap them with UncheckedXxxException everywhere, I prefer that approach).

            • samus 10 hours ago

              At the risk of repeating myself: streams were simply never designed with error handling in mind. Aborting everything at the first error is just the most simplistic error handling strategy. Reactive streams frameworks should be preferred for this.

      • peterashford 21 hours ago

        I agree. The only issue for me is that they could be less verbose but given that I'm always using an IDE, this is in practice a non-issue

    • jsight 2 days ago

      I agree with you w/r/t the streaming parallelization. I remember huge arguments about this on some of the lists back in the day, because that design decision had lots of ridiculous consequences.

      Eg, mutable state capture in lambdas is largely restricted because of the thought that people would use parallel threads within the stream processing blocks. That decision lead to lots of ugly code, IMO.

      I've also never seen a need to try to parallelize a simple stream processing step.

      • peterashford 21 hours ago

        I've used Streams before to good effect but I can't say I'm in love with the design. Seems overly verbose.

    • samus 2 days ago

      Checked exceptions are for errors that are not possible to prevent. How else should the caller know which exceptions are really likely to happen?

      Modules absolutely achieved their primary goal: stopping libraries from accessing JDK internals without the application's knowledge. The ecosystem is slow on the uptake since split packages and access to internal APIs is endemic, but it is happening ever so slowly. I wish libraries could opt into not being part of the unnamed module.

      Virtual threads were designed with explicit cooperation of the community, with the explicit goal of making it easy to switch as much existing code over to it as possible. I really don't understand the scepticism there. Most other languages went with promises or reactive streams because they were inspired by how functional programming languages do it.

      • vbezhenar a day ago

        > Checked exceptions are for errors that are not possible to prevent. How else should the caller know which exceptions are really likely to happen?

        The same way, caller can know which exceptions are really likely to happen in TypeScript, C++, Python. Or in modern Java which avoids checked exceptions anyway. By reading documentation or source code. That's perfectly fine and works for everyone.

        And you highlighted one big issue with checked exceptions. You've claimed that those exceptions are "really likely to happen".

        When I'm writing reading data from resource stream, the data that's located next to my class files, IO Exceptions are really unlikely to happen.

        Another ridiculous example of this checked exception madness:

            var inputStream = new ByteArrayInputStream(bytes);
            var outputStream = new ByteArrayOutputStream();
            inputStream.transferTo(outputStream); // throws IOException? wtf???
        
        This code can throw OutOfMemoryError, StackoverflowError, but never IOException. Yet you're forced to handle IOException which doesn't happen. And that's the issue with checked exceptions.

        There's no correspondence between checked exceptions and likelihood of their occurence. NullPointerException probably happens more than any checked exception. The division between checked exceptions and unchecked exceptions is absolutely arbitrary and makes sense only at caller place, never in called function signature.

        > Modules absolutely achieved their primary goal: stopping libraries from accessing JDK internals without the application's knowledge. The ecosystem is slow on the uptake since split packages and access to internal APIs is endemic, but it is happening ever so slowly. I wish libraries could opt into not being part of the unnamed module.

        "Slow" is an understatement. I don't see this happening at all. Last time I tried to write very simple application with modules, I spent so many hours banging my head over various walls, that I probably will not do another attempt in a foreseeable future.

        • clanky a day ago

          There's a recent Reddit thread with some good discussion around modules where Ron Pressler took part. tl;dr: the architects acknowledge uptake outside the JDK has been slow, largely because there have always been few benefits to modules, and secondarily because build tool support is lacking (perhaps because of the first reason). At some point they may begin providing additional benefits to modularization which may help stoke demand.

          https://www.reddit.com/r/java/comments/1o37hlj/reopening_the...

        • samus a day ago

          > The same way, caller can know which exceptions are really likely to happen in TypeScript, C++, Python. Or in modern Java which avoids checked exceptions anyway. By reading documentation or source code. That's perfectly fine and works for everyone.

          This provides no automatic verification that indeed all likely error situation that can and should be handled were indeed handled. The very idea is that you have to opt in to not handle a checked exceptions. Result types don't carry a stack trace; apart from that I'm not convinced that they are interently better. In fact, I'd argue that handling a Result and an exception looks much the same in imperative code.

          > When I'm writing reading data from resource stream, the data that's located next to my class files, IO Exceptions are really unlikely to happen.

          Java class loaders can do anything including loading resources from the network. Which is admittedly not that common these days after the demise of applets.

          > ByteArrayInputStream -> ByteArrayOutputStream

          The general assumption behind IO interfaces is that the operations might fail. These two classes are oddballs in that sense. Note that the other write methods in `ByteArrayOutputStream` don't declare checked exceptions.

          Since the compiler cannot prove that an exception will never be thrown (essentially due to Rice's theorem) there are always going to be false positives. The issues with checked exceptions therefore boil down to API design and API abuse.

          Re Errors: the programmer cannot do anything about it and might make matters worse by trying to do so. Preventing an OutOfMemoryError relies on whole-system design so peak memory consumption is kept under control. Also the StackOverflowError, can in no way be prevented nor handled by the caller. Therefore both of them are `Error`s, not `Exception`s.

          > NullPointerException probably happens more than any checked exception.

          Patently untrue, as network connections break down and files cannot be accessed all the time.

          The NullPointerException indicates a bug in the application. By the very reason it occurs, the current thread cannot continue execution normally. After a checked exception, it very much might. Though I would very much like to not have to handle exceptions in static initializer blocks - there is no good way to react to any problem happening there.

          > "Slow" is an understatement. I don't see this happening at all.

          All of this is slow-moving, I completely agree, but due to backwards compatibility concerns the ecosystem cannot be weaned off the issues that the JPMS is designed to prevent in a short time.

          • vbezhenar a day ago

            > This provides no automatic verification that indeed all likely error situation that can and should be handled were indeed handled.

            And some people write code in Python which provides no automatic verification whatsoever.

            Actually unchecked exceptions are very similar to dynamically typed languages. And that's fine. As Python and other languages proved by their mere existence: dynamic typing is not inherently bad. Sometimes it's good. I think that for error handling, it's good.

            > Java class loaders can do anything including loading resources from the network. Which is admittedly not that common these days after the demise of applets.

            Technically they can, but in my particular case I know very well, that my resources are residing inside of JAR file. And if that JAR file happened to reside on the failed HDD block, that's not going to be a recoverable error.

            When we're talking about IO Exceptions, it's almost always failed operation which requires complete abort. It's either failed hardware, or, typically, disconnected client. Can't do much about it, other than clean up and proceed to the next client.

            And the same could be said about SQL Exceptions. Like 99% of SQL exceptions are application bugs which are not going to be handled in any way other than wind up and return high level HTTP 500 error or something like that. There are cases when SQL exception should be handled, but those are rare. Yet JDBC developers decided that programmers must execute error handling rituals on every call site.

            > The NullPointerException indicates a bug in the application. By the very reason it occurs, the current thread cannot continue execution normally.

            That's not exactly true. In JVM, NullPointerException is absolutely well defined and you can continue execution after catching it. You might suspect, that logical application state is corrupted, but sometimes you know well that everything's fine (and in most cases you hope that everything's fine, if your Spring MVC handler threw NPE, Spring is not going to crash, it'll continue to serve requests). It's not C++ with its undefined stuff. JVM is pretty reliable when it comes to every error, including NPE, stack overflow or OOM. Latter is special, because even handling error might prove challenging, when memory allocations fail, but JVM will not hang up or crash.

            • samus 10 hours ago

              > And some people write code in Python which provides no automatic verification whatsoever.

              Python is a language with almost no static validation whatsoever. It would be very odd if it cared about checked exceptions. This dynamism makes big Python code bases infuriating to work with.

              > When we're talking about IO Exceptions, it's almost always failed operation which requires complete abort. It's either failed hardware, or, typically, disconnected client. Can't do much about it, other than clean up and proceed to the next client.

              If this is the case then the solution is to add it to the `throws` list.

              > That's not exactly true. In JVM, NullPointerException is absolutely well defined and you can continue execution after catching it.

              Why would I catch a NullPointerException instead of fixing my application? The JVM is indeed completely fine, but processing still cannot continue because that code simply does not exist.

    • vips7L a day ago

      Checked errors aren’t universally ignored by developers. Rusts main error system is checked. Swift has really introduced checked typed throws. Kotlin is introducing checked error unions. Checked exceptions are the same thing.

    • dkarl 2 days ago

      You're absolutely right about checked exceptions. However, I think they're an exception (forgive me) from the pattern of Java mostly sticking to the strategy of, we can build a practical, industrial, reasonably performant language that has all these nice bits from other languages: garbage collection, portable bytecode, no pointer arithmetic, collections in the standard library, etc.

      I think streams are a great example of what I was saying about Java failing to take advantage of coming last. Scala (probably among others, but Scala was right there on the JVM) had already demonstrated that it was possible to enable simple, readable code for simple use cases, while also enabling complex and powerful usage. And the experience of Scala had shown that there's little demand for parallel collections outside of extremely niche use cases where people tend to use specialized solutions anyway. Somehow Java, with this example staring them in the face, managed to get the worst of both worlds.

      • lenkite 2 days ago

        Totally Hard disagree on streams - I used parallel streams in my last Job nearly all the time. They are critical for cpu-intensive tasks involving large datasets. And they do involve just a single code change in the consuming code. Sequential to parallel processing can be done via one `.parallel`.

        I always believed it was a major plus point for Java compared to other languages. I am even surprised to hear otherwise. How should parallel processing of streams work in your opinion, then ? Just saying it be unsupported would be laughable considering hardware today.

        I would rate this feature 9/10. The fact that the author has rated it 1/10, shows he hasn't really worked on large, parallel processing of data - in Java anyways.

        • dkarl a day ago

          In the companies I've worked at, those kinds of workloads have been done in Spark or in Beam (GCP Dataflow, etc.)

    • stirfish 2 days ago

      You've never used parallel streams? They're my favorite way to do parallel computation in Java, and very easy if you've structured your problem around streams.

      • vbezhenar 13 hours ago

        Code with parallel streams wouldn't even pass my review. The server processes multiple requests simultaneously. It makes no sense to smash all cores in one request. It'll cause bad latency for other requests and will not increase throughput.

        There might be use-cases, but I've yet to encounter them.

        And when I need parallel computation, I can just use good old ExecutorService. Few more lines, but that's OK for a task that arises once in a 10 years.

    • paulddraper 2 days ago

      Go implements green threads.

      It might only be one language, but it’s a pretty big one

  • jsight 2 days ago

    IDK, I've worked in projects that didn't use the magic so much and I honestly think it was worse.

    Instead of a config class and a bunch of apps with @Inject Config config;, we'd have giant *Config classes. Each one would have lots of methods like:

    @Bean public fooProducer(FooConfig config, BazProvider provider, BarProvider barProvider, SoapClient soapClient) {...}

    Want to know how they were produced? Find usages on the class' constructor.

    The magic @Inject and @Autowired annotations don't seem worse than that to me.

  • zeroq 2 days ago

    The list generally oversimplifies a lot of things, but you're on point with annotations.

    Mandatory personal anecdote:

    I'm not a java guy, but I've been around java since '99, and few years ago I was moved to a strictly java team. Upon introduction I decided to show them my party trick and implemented live a pseudo "wolf3d" in one day. As usual, java devs were sort of impressed by the fact that you can do graphics and user input in java, because nowadays that's extremely rare for most of them. I got my approval and in return I asked them to give me a quick one day deep dive into Spring.

    At the end I was presented with a basic hello world project that was comprised mostly of... EMPTY CLASSES I mean literally, class Foo {} END OF FILE!

    Of course these empty classes had at least 5 lines of annotations on top of class declaration and in the end it somehow pushed the framework into the right direction, but oh my fucking god, I was throwing up in my mouth for the rest of the week.

  • tormeh 2 days ago

    It's seriously puzzling. I just don't get how it's possible to look at what so many others have done better, and somehow design something worse. For what reason? Consistency with the rest of the language, possibly? But is that really so important. Do they just not want to tackle certain parts of the compiler?

    • rzwitserloot 2 days ago

      OpenJDK redesigns massive swaths of the compiler every other month.

      The true explanation, at least the way OpenJDK says it, is that designing language features is more complex than a casual glancer can fathom, and there's 30 years of "Java is in the top 5 most used languages on the planet, probably #1 especially if focussing on stuff that was meant to be supported for a long time" to think about.

      From personal experience, essentially every single last "Just do X to add (some lang feature) to java; languages A and B do it and it works great!" would have been bad for java. Usually because it would cause a 'cultural split' - where you can tell some highly used library in the ecosystem was clearly designed before the feature's introduction.

      Even if you introduce a new feature in a way that doesn't break existing code, it's still going to cause maintainability headaches if you've cornered the pillars of the ecosystem into total rewrites if they want to remain up to date with the language. Because they will (or somebody will write an alternative that will) and you've _still_ 'python 2 v python 3'd the language and split the baby in the half.

      For what its worth, I think the OpenJDK team doesn't take this seriously enough, and a number of recently introduced features have been deployed too hastily without thinking this through. For example, `LocalDate`, which has 'this should be a record' written all over it, is not a record. Or how the securitymanager is being ditched without replacements for what it is most commonly used for here in the 2020s. (To be clear: Ditching it is a good idea, but having no in-process replacement for "let me stop attempts to access files and shut down the JVM, not for security purposes but simply for 'plan B' style fallback purposes" - that's a bit regrettable).

      I'm nitpicking on those points because on the whole OpenJDK is doing a far better job than most languages on trying to keep its ecosystem and sizable existing codebase on board _without_ resorting to the crutch of: "Well, users of this language, get to work refactoring everything or embrace obsoletion".

      • halffullbrain 2 days ago

        But ... LocalDate predated records by 6 years?

        Eventually, I guess there'll be backwards compatible "pattern extractors" functionality retrofittable to existing "record-like" classes. This has been hinted at on several occasions.

        • rzwitserloot 9 hours ago

          > But ... LocalDate predated records by 6 years?

          Yes, exactly - now you're getting it. Or rather I get the feeling I failed to explain it well.

          ArrayList predates generics.

          However, ArrayList does have generics.

          That's because generics were added to the language in a 'culturally backwards compatible' way: Existing libraries (i.e. libraries that predate the introduction of generics, such as ArrayList) could modify themselves to add support for generics in a way that is backwards compatible for the library: Code written before they added it continues to work and compile fine even against the new release that added generics.

          The same principle applied to records would mean that LocalDate could have been updated to turn into a record in a way that is backwards compatible.

          And it really works that way.. almost. You really can take an existing class (defined with `class`) and change it into a record (defined with `record`) and all existing code continues to work just fine. However, this means all your properties now neccessarily get an accessor function that is named after the property. And that is a problem for LocalDate specifically: It already has accessors and they are named e.g. `getYear()`. Not `year()`. That means if LocalDate were to be rewritten as a record, one of two very nasty options must be chosen:

          * Break backwards compatibility: As part of upgrading code you must change all calls to `.getYear()` into calls to `.year()`. It's a total ecosystem split: Every dependency you use comes in 2 flavours, one with calls to getYear and one with year, and you must use the right ones. This is truly catastrophic.

          * Have both methods. There's year() and also getYear() and they do the same thing. Which is the lesser evil by far, but it makes very clear that LocalDate predates `record`. Contrast to ArrayList: It is not all that obvious that ArrayList predates generics. It does, but if you were to design ArrayList from scratch after generics are introduced you'd probably make the same code. Maybe the signature of `remove` would have been `remove(T)` instead of the current `remove(Object)`.

          Instead, obviously then, the best choice is to not make it a record. And that's my point: The best possible (possibly here perfection is the enemy of good, but, I'd have done it differently) way to deploy records would have included some way for localdate to turn into a record without the above dilemma.

          Perhaps simply a way to explicitly write your accessors with some marker to indicate 'dont generate the default `year()` - THIS is the accessor for it'.

          Had that feature been part of record, then LocalDate could have turned into one in a way that you can't really tell.

        • MBCook 2 days ago

          Records are great, but objects work.

          Date flat out doesn’t. We needed something in the standard library to fix that. It should’ve happened long before it did.

          • halffullbrain a day ago

            Totally agree, I was just pointing out to GP why LocalDate wasn’t a record. (Date and Calendar should never be used in new code. Note how the new date/time API doesn’t have any conversions to/from old types (Date+Calendar), they only in the opposite direction)

            Can’t wait for destructuring support for classes, though.

            • MBCook a day ago

              Yes! Ever since I learned about it and was able to use it in some other languages I’ve been waiting for destructuring in Java to hit prime time.

    • dcminter 2 days ago

      Some of the weirder choices have been the result of a desire to avoid making breaking changes to JVM bytecode.

      Also there was a long period when changes were very lumpy - it could be multiple years for a feature to make it into the release, and anything that might screw up other features got a lot of pushback. Then other conventions/tools emerged that reduced the urgency (e.g. the Lombok stuff)

      Edit: I should add that it's now on a fixed 6-monthly release cycle which IMO works much better.

  • rzwitserloot 2 days ago

    That's not how the OpenJDK sees things. They tend to think that the features they deliver are at best mildly informed by other languages. Not out of some sense of hubris, but out of a sense of pragmatics: Simply copy and pasting features from other languages into java - that would produce a frankenstein.

    For example, java is somewhat unique in having lambda syntax where the lambda *must* be compile-time interpretable as some sort of 'functional type' (a functional type being any interface that defines precisely 1 unimplemented method). The vast, vast majority of languages out there, including scala which runs on the JVM, instead create a type hierarchy that describe lambdas as functions, and may (in the case of scala for example) compile-time automatically 'box'/'cast' any expression of some functional type to a functional interface type that matches.

    Java's approach is, in other words, unique (as far as I know).

    There was an alternate proposal available at the time that would have done things more like other languages does them, completely worked out with proof of concept builds readily available (the 'BGGA proposal'). The JVM would autogenerate types such as `java.lang.function.Function2<A, B, R>` (representing a function that takes 2 arguments, first of type A second of type B, and returns a value of type R), would then treat e.g. the expression:

    `(String a, List<Integer> b) -> 2.0;`

    As a `Function2<String, List<Integer>, Double>`, and would also 'auto-box' this if needed, e.g. if passing that as the sole argument to a function:

    ``` void foo(MyOperation o) {}

    interface MyOperation { Double whatever(String arg1, List<Integer> arg2); } ```

    This proposal was seriously considered but rejected.

    The core problem with your comment is this:

    Define the terms "polished" and "elegant". It sounds so simple, but language features are trying to dance to quite a few extremely different tunes, and one person's 'elegance' is another person's 'frankensteinian monster'.

    The same mostly goes for your terms "beauty" and "charm", but, if I may take a wild stab in the dark and assume that most folks have a very rough meeting of the minds as to whatever might be a "charming" language: I know of no mainstream long-term popular languages that qualify for those terms. And I think that's inherent. You can't be a mainstream language unless your language is extremely stable. When you're not just writing some cool new toy stuff in language X - you're writing production code that lots of euros and eyeballs are involved in, and there's real dependence on that software continuing to run, then you __must__ have stability or it becomes extremely pricey to actually maintain it.

    With stability comes the handcuffs: You need to use the 'deprecation' hammer extremely sparingly, essentially never. And that has downstream effects: You can't really test new features either. So far I have not seen a language that truly flourishes on the crutches of some `from future import ...` system. That makes some sense: Either the entire ecosystem adopts the future feature and then breaking _that_ brings the same headaches, or folks don't use these features / only for toy stuff, and you don't get nearly the same amount of experience from its deployment.

    Said differently: If java is a frankenstein, so is Javascript, C#, Python, Ruby, Scala, and so on. They have to be.

    I'd love to see a language whose core design principles are 100% focussed on preventing specifically that. Some sort of extreme take on versioning of a language itself that we haven't seen before. I don't really know what it looks like, but I can't recall any language that put in the kind of effort I'd want to see here. This is just a tiny sliver of what it'd take:

    * The language itself is versioned, and all previous versions continue to be part of the lang spec and continue to be maintained by future compilers. At least for a long time, if not forever.

    * ALL sources files MUST start with an indication about which version of the language itself they use.

    * The core libraries are also versioned, and separately. Newer versions are written against old language versions, or can be used by source on old language versions.

    * The system's compilers and tools are fundamentally operating on a 'project' level granularity. You can't compile individual source files. Or if you can, it's because the spec explains how a temporary nameless project is implied by such an act.

    * All versions ship with a migrator tool, which automatically 'updates' sources written for lang ver X to lang ver X+1, automatically applying anything that has a near-zero chance of causing issues, and guiding the programmer to explicitly fixing all deprecated usages of things where an automated update is not available.

    * The language inherently supports 'facades'; a way for a library at version Y to expose the API it had at version X (X is older than Y), but using the data structures of Y, thus allowing interop between 2 codebases that both use this library, one at version X and one at version Y.

    That language might manage the otherwise impossible job of being 'elegant', 'simple', 'mainstream', 'suitable for serious projects', and 'actually good'.

  • torginus 2 days ago

    which is kinda horrifying - it means the framework designers didn't find the language powerful enough to express app logic, and hotglued their own custom arbitrary behavior on top of it.

    Clear language code should be endeavor to be readable/understandable when printed on a sheet of paper by anyone, acceptable code should be understandable by anyone who knows a bit about the technologies and has some IDE support.

    Garbage code is what you have when the code in question is only understandable when you actually run it, as it uses arbitrary framework logic to wire things together based on metadata on the fly.

    • Groxx 2 days ago

      people have been gluing other languages on top of languages practically forever - it's a DSL.

      no single language is ideally suited for every situation, it's not inherently a sign of failure that someone makes a DSL.

      and since annotations are part of the language, this is still all "the language is flexible enough to build the framework [despite being wildly different than normal code]" so I don't think it even supports that part.

  • jayd16 2 days ago

    The ratings are really all over the place. Jshell is a 6/10? What is the rubric?

zeroq 2 days ago

Joshua Bloch's work on Collections for Java2 was absolutely formative for me.

I was just starting real programming, I knew naming was hard so I was using thesaurus almost as extensively - if not more - than the reference manual.

But his work defined designing API for me for life. Stuff we take for granted, and we often overlook as seemingly trivial.

Let's say you have a collection type that has a method ``put``. It takes two arguments - an object you want to insert, and an index you want to put it at. Which argument should go first? Could index be optional? What value should it default to? Does the function returns anything? A boolean to indicate whether insertion was successful? Or the index at which the object was put? If latter how you indicate an error?

All of these seems seemingly trivial but he and his team worked on that library for over a year and he throughly documented their work in series of presentations.

And we can't forget about his java puzzlers, absolute gem.

SerCe 2 days ago

My read is that it's easy to be quite negative on Java features when you're not the person they were designed for. For example, the main "customer" of the module system is the JDK itself. The main customer of NIO/2 is the low-level libraries like Netty.

I highly recommend the Growing the Java Language talk by Brian Goetz to anyone who's interested in the philosophy behind evolving the modern Java language [1]. And Don’t be misled by the title, it’s not just about Java, it’s about software design.

[1]: https://www.youtube.com/watch?v=Gz7Or9C0TpM

  • executesorder66 2 days ago

    >For example, the main "customer" of the module system is the JDK itself

    As mentioned in TFA, "The general advice seems to be that modules are (should be) an internal detail of the JRE and best ignored in application code"

    So yeah, why expose it to those who are not the "main customer"?

    • SerCe 2 days ago

      > So yeah, why expose it to those who are not the "main customer"?

      How did modules affect you as a user? I'd guess that you had to add `--add-opens`/`--add-exports` during one of the JDK migrations at some point. And the reason you had to do it was that various libraries on your classpath used JDK internal APIs. So modules provided encapsulation and gave you an escape hatch for when you still have to use those libraries. How else would you do it while still achieving the desired goal?

    • vips7L 2 days ago

      It’s just too complex. They should have went with the internal modifier.

  • adgjlsfhk1 2 days ago

    modules get a -10 because they were massively breaking

  • paulddraper 2 days ago

    Yeah modules isn’t for end users, at least not for the most part

haunter 2 days ago

I'm sorry, please don't hate me (I'm tired and don't have anything better to do) https://files.catbox.moe/ge4el3.png

  • alex_smart 2 days ago

    Why so much hate for modules? They seem to be almost universally disliked by everyone on this thread and I don't understand why.

    • ivan_gammel 2 days ago

      Modules are weird. In Java world there exists consensus on dependency management via Maven-style repositories (Maven Central is the primary distribution channel) and all tools support it. You handle your dependency tree outside of your code and just import packages from libraries available on classpath. It’s possible to continue doing that that without using modules, so the case for using them is still unclear to many people. Where the actual hate may have come from is an old story with migration from Java 8 to Java 9, where modules hidden the access to certain internal APIs, breaking some libraries which relied on them, making that migration painful. Today their value is probably 0/10, but there’s no reason to hate.

      • alex_smart 2 days ago

        The use case for modules is to have a unit of organizing code (deciding what is visible and accessible to who) at a higher level of abstraction than a package. It allows library authors to be much more explicit about what is the public API of their library.

        Ever wrote "List" in Intellij and instead of importing "java.util.List" Intellij prompts you to choose between like twenty different options from all the libraries you have included in your classpath that have implemented a public class named "List"? Most likely 90% of the libraries did not even want to expose their internal "List" class to the world like that but they got leaked into your classpath just because java didn't have a way to limit visibility of classes beyond packages.

        • ivan_gammel a day ago

          Yes, but at what cost? Many libraries can solve visibility problem with package level visibility. And modules cost a lot: dependency management was a non-goal for them, so anyone who wants to use module path instead of classpath, has to declare dependencies twice. It was a big mistake not to integrate modules with a de facto standard of Maven.

          • alex_smart 14 hours ago

            > Many libraries can solve visibility problem with package level visibility

            The only way of doing this would be to put all classes in the same package. Any nontrivial library would have hundreds of classes. How is that a practical solution?

            • ivan_gammel 2 hours ago

              Well designed non-trivial libraries should be open for extension and thus should not hide most of the implementation details, leaving the risk of too tight coupling to users. E.g. if I‘m not 100% satisfied with some feature of a library, I should be able to create a slightly modified copy of implementation of some class, reusing all internal utilities, without forking it. So no, modules as means to reduce visibility are not as cool as you think. And given the specific example of the list, it’s possible to filter out irrelevant suggestions for auto-complete or auto-import in IDE settings.

        • ayewo 2 days ago

          To add to this, modules make it easy for external tools like GraalVM native-image to produce self-contained binaries that are smaller compared to the standard practice of distributing fat binaries (i.e. large JARs).

    • hashmash 2 days ago

      Directly or indirectly many (or most) projects ended up depending on something which was using an unsupported backdoor API because it provided a marginally useful capability. The module system restricted access to these APIs and everything stopped working, unless you added some magic command line arguments to gain access again.

      So for most people, the initial impression of modules is negative, and then they just decided to rule the feature out completely. This has created a sea of useless criticism, and any constructive criticism is hardly observed. Improvements to module configuration (combine it with the classpath), would go a long way towards making modules "just work" without the naysayers getting in the way.

      • alex_smart 2 days ago

        >Directly or indirectly many (or most) projects ended up depending on something which was using an unsupported backdoor API because it provided a marginally useful capability. The module system restricted access to these APIs and everything stopped working, unless you added some magic command line arguments to gain access again.

        Is it even theoretically possible for a project like this to not run into these kind of issues? Like literally the project's goal is to enable library authors to be more explicit about their public API. So breaking use cases that use unsupported backdoor APIs very much seems like a predictable and expected result?

      • laughing_man a day ago

        Early on there were things you couldn't do without using com.sun.* classes, so people got used to doing that. It's been many years since that was all fixed, though.

        • cesarb a day ago

          > It's been many years since that was all fixed, though.

          AFAIK, there's still no replacement for sun.misc.Signal and sun.misc.SignalHandler, so I think "that was all fixed" is false.

    • sothatsit 2 days ago

      They are only useful for a small group of people, and their addition broke/complicated lots of people's builds in non-trivial ways.

      • alex_smart 2 days ago

        I have a company with ~20 developers only. And already I have an ArchUnit test stating that public classes within "com.companyname.module.somepackage.internal" cannot be used anywhere outside "com.companyname.module.somepackage".

        Surely almost everyone who has worked in a large enough codebase and thought about large-scale modularity can see the use case for a unit of abstraction in java higher than a package?

        • nmadden 2 days ago

          Yes, in theory they are good. In practice they cause enormous amounts of pain and work for library maintainers with little benefit to them (often only downsides). So, many libraries don’t support them and they are very hard to adopt incrementally. I tried to convert a library I maintain to be a module and it was weeks of work which I then gave up and reverted. As one library author said to me “JPMS is for the JDK itself, ignore it in user code”.

          Given how much of a coach and horses modules drove through backwards compatibility it also kind of gives the lie to the idea that that explains why so many other language features are so poorly designed.

    • ysleepy a day ago

      They're fine, but they're incompatible with building fat-jars ro have single file deployment and dead to me because of that. Spring does some ugly jar-in-jar custom classloader stuff which I hate out of principle because it's spring.

      Oracle hates that people build fat-jars and refuses to adress the huge benefit of single file deployables.

  • kittko 2 days ago

    thank you so much. this should be included in the original post.

pixelmonkey 2 days ago

A cool thing about Doug Lea's java.util.concurrent (received a 10/10 rating here) is that its design also inspired Python's concurrent.futures package. This is explicitly acknowledged in PEP 3148[1] (under "Rationale"), a PEP that dates back to 2009.

[1]: https://peps.python.org/pep-3148/

  • swuecho 2 days ago

    That is why I think of java when using python's concurrent.future package.

w10-1 2 days ago

It's nice to review the features, but the history of Java isn't really about features or even programmer popularity.

(1) It was the first disruptive enterprise business model. They aimed to make everyone a Java programmer with free access (to reduce the cost of labor), but then charge for enterprise (and embedded and browser) VM's and containers. They did this to undercut the well-entrenched Microsoft and IBM. (IBM followed suit immediately by dumping their high-end IDE and supporting the free Eclipse. This destroyed competition from Borland and other IDE makers tying their own libraries and programming models.)

(2) As an interpreted language, Java became viable only with good JIT's. Borland's was the first (in JDK 1.1.7), but soon Urs Holzle, a UCSB professor, created the HotSpot compiler that has seeded generations of performance gains. The VM and JIT made it possible to navigate the many generations of hardware delivering and orders-of-magnitude improvements and putting software in every product. Decoupling hardware and software reduced the vertical integration that was killing customers (which also adversely affected Sun Microsystems).

btw, Urs Holzle went on to become Google employee #8 and was responsible for Google using massively parallel off-the-shelf hardware in its data centers. He made Google dreams possible.

  • oofbey 2 days ago

    The business plan originally was to sell CPUs that ran Java natively. And these would be fast. That idea failed miserably.

travisgriggs 2 days ago

Ah Java. The language I never got to love. I came of coding age during the “camps” era of object oriented stuff: Eiffel, Smalltalk, CLOS, C++, etc. Java, from 95ish to oh 98ish, was like a giant backdraft. Completely sucked the air out of the room for everything else.

Does anyone remember the full page ads in WSJ for programming language, that no on quite yet knew what it really was? So my formative impressions of Java on were emotional/irrational, enforced by comments like:

“Of Course Java will Work, there’s not a damn new thing in it” — James gosling, but I’ve always suspected this might be urban legend

“Java, all the elegance of C++ syntax with all the speed of Smalltalk” - Kent Beck or Jan Steinman

“20 years from now, we will still be talking about Java. Not because of its contributions to computer programming, but rather as a demonstration of how to market a language” — ??

I can code some in Java today (because, hey, GPT and friends!! :) ), but have elected to use Kotlin and have been moderately happy with that.

One thing that would be interesting about this list, is to break down the changes that changed/evolved the actual computation model that a programmer uses with it, vs syntactic sugar and library refinements. “Languages” with heavy footprints like this, are often just as much about their run time libraries and frameworks, as they are the actual methodology of how you compute results.

  • laughing_man a day ago

    In the early days javascript wasn't far enough along that you could make a web app without a lot of compromises, and the cross-platform nature of java was a big, big plus. I worked on an internal java client application that was developed on Linux for end users on PC. Ten years after we released the first version, and while it was still under development, a group of powerful managers at our company demanded they be issued Macs instead of the PC corporate standard.

    When IT asked us if our application worked on Mac, we shrugged and said "We don't have a Mac to test it. We've never run it on a Mac. We won't support it officially, so if there are Mac specific bugs you're on your own. But... it should work. Try it."

    And it did work. All the Mac users had to do was click on our Webstart link just like the PC users. It installed itself properly and ran properly. Never had a single issue related to the OS. Before Java was introduced that was an unobtainable dream in a full-featured windowed application.

  • hoolajoop2 2 days ago

    > I can code some in Java today (because, hey, GPT and friends!! :) ),

    I love GPT. Such a marvellous tool. Before ChatGPT came along, I had no medical experience. Thanks to GPT and friends, I am now a doctor. I've opened a clinic of my own.

  • KyleBerezin a day ago

    For whatever reason, gpt-5 writes java code like it is 1995. I think it was trained on decompiled code.

marginalia_nu 2 days ago

I think the author is sleeping on Java assertions.

I really like the feature, and it's really one of the features I feel Java got right.

The syntax is very expressive, and they can easily be made to generate meaningful exceptions when they fail.

It's also neat that it gives the language a canonical way of adding invariant checks that can be removed in production but run in tests or during testing or debugging (with -da vs -ea).

You could achieve similar things with if statements, and likely get similar performance characteristics eventually out of C2, but this way it would be harder to distinguish business logic from invariant checking. You'd also likely end up with different authors implementing their own toggles for these pseudo-assertions.

  • rwmj 2 days ago

    I'm quite surprised that he said asserts are not found in production code. Is that really so? I rarely write Java, but in C code we use asserts (in production code) all the time. It's not uncommon for functions to contain 2 or 3 asserts.

    • dcminter 2 days ago

      I very rarely see assertions in "real" Java code; I think the author is right - in fact the place I see them the most often is in unit tests where they've been used by mistake in place of an assertion library's methods!

      I don't know why they're not more popular.

      • nunobrito 2 days ago

        Good conversation, I had no idea what assertions are beyond JUnit.

    • dkarl a day ago

      I think they're used less because the main reason for using them in C isn't a consideration in Java, and also they don't accomplish the same thing.

      In C, asserts are used as sanity checks, and when one is violated, there's often reasonable suspicion that that memory corruption has occurred, or that memory corruption will occur if the code proceeds in the current state. Aborting the process, leaving a core dump for analysis, and starting fresh is often the safest thing to do to avoid the unpredictable results of corrupted state, which can be insidiously subtle or insanely dramatic. In my experience writing server-side C++, we always ran it in production with asserts enabled, because code that continued to run after memory was corrupted led to the the bugs that were the most destructive and the most mysterious.

      Memory corruption is rare enough in Java that 99.9% of code completely ignores the possibility. Also, if you did suspect memory corruption in a Java program, an assert wouldn't help, because it would only throw a runtime exception that would probably get caught and logged somewhere, and the process would continue serving requests or whatever else it was doing.

    • marginalia_nu 2 days ago

      If you're only doing like CRUD endpoints, they may be less useful, but that's hardly the extent of Java production code. I certainly use asserts in production code quite a lot in Java, though the use biases toward more low level functions, rarely in high level application logic.

  • brap 2 days ago

    What are the pros of making this a keyword vs just a standard function?

    • hashmash 2 days ago

      At the time the feature was added, there was no way to make a parameter to a function be lazily evaluated. Something like `assert(condition, "error: " + stuff)` would eagerly concatenate the string even when the condition is always true (which it should be). Nowadays, the error parameter can be specified as a lambda, which can potentially be optimized to be just as cheap as the existing assert feature.

    • dcminter 2 days ago

      You can disable them at runtime (e.g. in prod) to avoid the performance overhead once you're satisfied the codebase is thoroughly enough tested.

      For some things like requiring arguments to be non-null static checks with annotations have superseded them (in a confusing way inevitably - I think there are three different common non-nullness annotations).

    • marginalia_nu 2 days ago

      The pros are that it can generate better error messages, without putting that responsibility on the programmer. Something that would otherwise require a preprocessor or some form of metaprogramming.

MarkMarine 2 days ago

I’m sure there are better ways to do streams on the JVM, scala being a great example, but however imperfect the implementation is streams are such a net positive I can’t imagine the language without them. I pine for the streams API when I write go.

  • MBCook 2 days ago

    I totally agree with the criticism about exceptions. If you need exceptions inside a stream it turns into a mess.

    Overall I agree with you. They are significantly better, even if a little verbose, than not having them. Love cleaning up old loops with a tiny stream that expresses the same thing more compactly and readably.

    He’s also right on the parallel benefits not really being a thing I’ve ever seen used.

    • peterashford 21 hours ago

      I'm in agreement, too. The intersection of streams and exceptions was not pretty. Streams are still good, but the feature is a little ugly

rsynnott 2 days ago

I feel this is overly harsh on Collections. You have to take into account just how awful that which it replaced was.

> Java Time: Much better than what came before, but I have barely had to use much of this API at all, so I’m not in a position to really judge how good this is.

Again, it is hard to overstate just _how_ bad the previous version is.

Though honestly I still just use joda time.

  • wpollock 2 days ago

    >Again, it is hard to overstate just _how_ bad the previous version [of Java time] is.

    The original Java Time classes were likely a last-minute addition to Java. They were obviously a direct copy of C language time.h. It feels as if the Java team had a conversation like this: "Darn, we ship Java 1.0 in a month but we forgot to include any time functions!" "Oh no! We must do something!" "I know, let's just port C time.h!"

  • MBCook 2 days ago

    In my mind Java really got usable in 1.5 with collections and generics.

    When you didn’t have collections everything was a complete pain. But after they were added you still had to cast everything back to whatever type it was supposed to be when you got stuff out of collections. Which was also a huge pain.

    I know all the arguments about how genetics weren’t done “correctly“ in Java. I’ve run into the problems.But I’m so glad we have them.

b_e_n_t_o_n 2 days ago

I haven't written much Java but I am learning Kotlin and I really appreciate the language and the whole JVM ecosystem. Yeah yeah, Gradle is complicated but it's waaaaay easier to figure out than my adventures with Cmake, and when I read Java code there is a certain comfort I feel that I don't get with other languages, even ones I'm experienced with like Go. Java feels a bit like a stranger I've known my whole life, same with Kotlin. Perhaps despite all its flaws, there is a certain intrinsic quality to Java that has helped make it so popular.

  • peterashford 21 hours ago

    I think you might be getting at one of my favourite features of Java. It's a pretty straightforward simple language. It deals with complexity well by not being too clever. I think Go has that too, except for its error handling and perhaps channels

  • dcminter 2 days ago

    Maven was peak Java build tool. I detest Gradle. Some people just hate XML enough to doom us all.

    • Defletter 14 hours ago

      Can't speak for everyone, but I think a substantial part of the shift from Maven to Gradle was the ability to write build scripts: you didn't need to write a plugin. I'm hoping that Maven (and Gradle) can take advantage of JEPs 458 and 512 to allow people to write build scripts for that Java projects in Java.

      - https://openjdk.org/jeps/458

      - https://openjdk.org/jeps/512

    • MBCook 2 days ago

      I wouldn’t say I’m a fan of maven, but it is absolutely one of the best build/dependency tools I’ve ever used despite its warts.

      So many things, even if they came much later, are somehow much worse.

sedro 2 days ago

Autoboxing's evil twin, auto-unboxing should knock the score down a few points.

  Integer a = null;
  int b = 42;
  if (a == b) {} // throws NullPointerException
  • dcminter 2 days ago

    Or my favourite...

      Short w = 42;
      Short x = 42;
      out.println(w == x); // true
      Short y = 1042;
      Short z = 1042;
      out.println(y == z); // false
    • prein 2 days ago

      Once, after we had an application go live, we started getting reports after a few hours that new users were unable to log in.

      It turns out, somewhere in the auth path, a dev had used `==` to verify a user's ID, which worked for Longs under (I believe) 128, so any users with an ID bigger than that were unable to log in due to the comparison failing.

    • kittko 2 days ago

      I’ll bite. Why does this not work as you’d expect?

      • dcminter a day ago

        I'm comparing object references (pointers) not the value boxed by the object (what the pointer points to).

        For performance reasons boxed Short objects are interned when they represent values in the range -127 to +128 so for 42 the pointers will point to the same interned object after 42 is autoboxed to a Short. Whereas 1042 is outside this interning range and the autoboxing creates two distinct objects with different pointers.

        It's very simple but (a) non-obvious if you don't know about it and (b) rather wordy when I spell it out like this :)

        In general in Java you want obj.equals(other) when dealing with objects and == only with primitives, but autoboxing/unboxing can cause confusion about which one is dealing with.

        In other other words, the surprise ought to be that w == x is true, not that y == z is false!

    • sedro 2 days ago

      That's another gotcha-- interning of strings and boxed primitives.

      Are there linters for this sort of thing? I don't write Java much any more.

      • dcminter 2 days ago

        > Are there linters for this sort of thing?

        Yes and they're pretty good so it's rarely an issue in practice. Using == on object references will indeed usually get you yelled at by the linter.

        • MBCook 2 days ago

          I’ll say from experience that even if your IDE highlights that by default people won’t pay any attention to it and the bug will get in.

          I’ll be happy when it’s fixed.

    • hashmash 2 days ago

      If JEP 401 is ever delivered (Value Classes and Objects), then this sort of problem should go away.

      • mrkeen a day ago

        Do Java problems go away? I thought the selling point was that your huge un-rewritable enterprise software will crash tomorrow like it crashed yesterday.

  • commandersaki 2 days ago

    I'm not much of a PL nerd, what should it do?

    • blandflakes 2 days ago

      Return false! They aren't equal. But of course we're comparing a reference to a primitive, so we either lift the primitive to a reference, or lower the reference... so here we are.

    • mrkeen a day ago

      Fail to compile when you assign something which isn't an integer to an integer.

      • michaelcampbell a day ago

        None of that code does that; the "issue" is with `==` between an int and an Integer. I'd accept a failure to compile _that_, but that does kind of kill the utility of the 99.9% of times where auto-boxing and unboxing is syntactically simpler.

  • thiht 2 days ago

    Seems reasonable to me

tombert 2 days ago

Interesting; I actually have grown pretty fond of NIO.

I will acknowledge that the interface is a bit weird, but I feel like despite that it has consistently been a "Just Works" tool for me. I get decent performance, the API is well documented, and since so many of my coworkers have historically been bad at it and used regular Java IO, it has felt like a superpower for me since it makes it comparatively easy to write performant code.

Granted, I think a part of me is always comparing it to writing raw epoll stuff in C, so maybe it's just better in comparison :)

  • cesarb a day ago

    There's one thing I deeply dislike with Java NIO: ClosedByInterruptException. If a thread is interrupted (for instance, due to a future.cancel(true) or similar), and it happens to be in the middle of a NIO operation, the channel will be closed, even if it's not owned by that thread. That single thing makes using NIO far more brittle than the older Java IO.

zylepe 2 days ago

I haven’t used markdown in javadoc yet but this seems like at least 3/10? I often want to put paragraphs or bulleted lists in javadoc and find myself wanting to use markdown syntax for readability in the code but need to switch to less readable html tags for tooling to render it properly.

  • MBCook 2 days ago

    Personally it’s fine, I haven’t used it though.

    I really wish Javadoc was just plain text that honored line breaks. I really don’t care about the fact I can put HTML in there, that just seems dumb to me. I get you can’t remove it but I would be happy if you could.

    I do like markdown. But I don’t see myself ever using it in a Javadoc.

  • ronyeh 2 days ago

    I hate using html in comments.

    Markdown in javadoc is at least 7/10 for me. Improves comment readability for humans while allowing formatted javadocs.

    • metaltyphoon a day ago

      I wish C# would add this too. I despise having to write docs in XML style

zkmon 2 days ago

Applets (Java 1.1 - that's where I started),

Servlets (Together with MS ASP, JSP/Servlets have fuelled the e-commerce websites)

I think Java dominated the scene mostly because of its enterprise features (Java EE) and the supporting frameworks (Spring etc) and applications (Tomcat, Websphere, Weblogic etc) and support from Open source (Apache, IBM)

Arwill 2 days ago

-10 for modules is fair, only 4 for lambdas is not. My programming style changed after using lambdas in Java, even when using a different programming language later that doesn't have lambdas as such.

  • MBCook 2 days ago

    Lambdas + streams is fantastic. I think if you didn’t have them streams would just be a total mess to use.

parallax_error 2 days ago

I can’t believe lambdas got a 4/10! I’m a student so maybe my opinion will change when I work on “real” code but I really like their conciseness

  • peterashford 21 hours ago

    I've been a developer for over three decades now and I agree with you - Lambdas are really good.

fleventynine 2 days ago

Still no unsigned integer types in the standard library after 26 years?

  • mightyham a day ago

    In one of James Gosling's talks he tells a funny story about the origin of this design decision. He went around the office at Sun and gave a bunch of seasoned C programmers a written assessment on signed/unsigned integer behaviors. They all got horrible scores, so he decided the feature would be too complicated for a non-systems programming language.

    • fleventynine 20 hours ago

      Non-systems languages still need to interact with systems languages, over the network or directly. The lack of unsigned types makes this way more painful and error-prone than necessary.

  • MBCook 2 days ago

    It’s rare I have to do bit math but it’s so INCREDIBLY frustrating because you have to do everything while the values are signed.

    It is amazing they haven’t made a special type for that. I get they don’t want to make unsigned primitives, though I disagree, but at least makes something that makes this stuff possible without causing headaches.

    • hashmash 2 days ago

      Sometimes I'd like to have unsigned types too, but supporting it would actually make things more complicated overall. The main problem is the interaction between signed and unsigned types. If you call a method which returns an unsigned int, how do you safely pass it to a method which accepts a signed int? Or vice versa?

      Having more type conversion headaches is a worse problem than having to use `& 0xff` masks when doing less-common, low-level operations.

      • fleventynine 2 days ago

        > If you call a method which returns an unsigned int, how do you safely pass it to a method which accepts a signed int?

        The same way you pass a 64-bit integer to a function that expects a 32-bit integer: a conversion function that raises an error if it's out of range.

        • hashmash 2 days ago

          This adds an extra level of friction that doesn't happen when the set of primitive types is small and simple. When everyone agrees what an int is, it can be freely passed around without having to perform special conversions and deal with errors.

          When trying to adapt a long to an int, the usual pattern is to overload the necessary methods to work with longs. Following the same pattern for uint/int conversions, the safe option is to work with longs, since it eliminates the possibility of having any conversion errors.

          Now if we're taking about signed and unsigned 64-bit values, there's no 128-bit value to upgrade to. Personally, I've never had this issue considering that 63 bits of integer precision is massive. Unsigned longs don't seem that critical.

      • MBCook 2 days ago

        Like I said, I understand why they don’t.

        I think the only answer would be you can’t interact directly with signed stuff. “new uint(42)” or “ulong.valueOf(795364)” or “myUValue.tryToInt()” or something.

        Of course if you’re gonna have that much friction it becomes questionable how useful the whole thing is.

        It’s just my personal pain point. Like I said I haven’t had to do it much but when I have it’s about the most frustrating thing I’ve ever done in Java.

taspeotis 2 days ago

Which release did they add the URL class that checks for equality by connecting to the internet? 10/10

  • __float 2 days ago

    That mis-feature has been present since Java 1.0, so (as with checked exceptions mentioned above) it's not entirely within the scope of this post.

  • mrkeen a day ago

    Bitten hard by this one. I kept a set of Urls to scan. Not all my Urls made it into the set because of IP address equality.

1a527dd5 2 days ago

I owe Java a lot. Programming clicked for me when I was taught OOP in Java, my other programming module with event-driven design in C# which I hated.

Fast forward a few years later, and I'm actually at a C# shop.

Fast forward a decade, I'm at the same shop. I adore C# and I fondly remember my foray into Java.

I left Java around the time Streams were becoming a thing. I thought it looked like a mess, and then I ran into LINQ in C# land. Swings (pun intended) and roundabouts.

adben a day ago

A little bit bias and opinionated... Tho limited, lambdas and streams were a paradigm shift so big that revamped the love for java into functional programming... Not just a set of features introduced in Java 8.

  • pachouli-please a day ago

    >little bit bias and opinionated Well, we are reading an individual's blog after all.

hdpe a day ago

Great list, even if I disagree with many of the ratings!

But astonished that Optional isn't mentioned either there or in the comments. A second way to represent no-value, with unclear and holy-war-ushering guidance on when to use, and the not exactly terse syntax I see everywhere:

Optional<Ick> ickOpt = Optional.ofNullable(ickGiver.newIck()); ickOpt.flatMap(IckWtfer::wtf).ifPresentOrElse((Wtf wtf) -> unreadable(wtf)), () -> { log.warn("in what universe is this clearer than a simple if == null statement?!"); });

  • skribanto a day ago

    Well, it can make some chained function composition easier.

      Ick1 result1 = potentiallyNull1();
      Ick2 result2 = (result1 == null) ? null : potentiallyNull2(result1);
      Ick3 result3 = (result2 == null) ? null : potentiallyNull3(result2);
    
    vs

      Ick3 result3 = potentiallyNone1()
        .flatMap(potentiallyNone2)
        .flatMap(potentiallyNone3);
    
    You could maybe move the null check inside the method in the former and it cleans it up a bit, but in the latter you can have methods that are explicitly marked as taking NonNull in their type signature which is nice.
  • munksbeer 10 hours ago

    I don't grumble about Java much, I make my living with it and enjoy it. But Optional is a bane.

    We use NullAway and I just never use Optional unless it really, really makes sense.

linuxhansl 2 days ago

Didn't Java 1.3 (Sun's JDK) introduce the JIT? I remember talking to colleagues about what a joke Java performance was (we were working in C++ then). And then with Java 1.3 that started to change.

(Today, even though I still C++, C, along with Java, I'll challenge anyone who claims that Java is slower then C++.)

  • winternewt 2 days ago

    Maybe not slower once it has warmed up, though for memory-bandwidth bound use cases I would still say the lack of mutable records has you fighting the language to get reasonable cache locality (and everybody will hate your code for not being good Java). The fact that everything is a pointer kills the CPU execution pipeline and cache.

    But even for I/O bound applications it still feels slow because excessive memory usage means more swap thrashing (slowing down your entire OS), and startup time suffers greatly from having to fire up VM + loading classes and waiting for the JIT to warm up.

    I can start a C/C++/Rust based web server in under a second. The corresponding server in Java takes 10 seconds, or minutes once I have added more features.

  • hashmash 2 days ago

    The first official JIT became available in JDK 1.1, in 1997. The Symantec JIT was available as an add-on sometime in mid 1996, just a few months after JDK 1.0 was released. Even better performance was possible with GCJ, available in 1998.

    The release of HotSpot was in 1999, and became default with JDK 1.3 in 2000. It took JIT compilation to the next level, making tools like GCJ mostly obsolete.

anotherevan 2 days ago

Don't know what version it was, but default functions in interfaces I think is an underrated feature that comes in extremely useful on occasion.

brap 2 days ago

Wow I can’t believe try with resources is so old! I’ve been working with Java for years and only learned this exists recently, I thought it must be relatively new. 14 years!

  • dcminter 2 days ago

    It was such a lifesaver for doing raw database stuff. The boilerplate for making sure Connection, PreparedStatement, ResultSet and so on were all released properly was a huge pain before that.

    • MBCook 2 days ago

      Despite it being available at the time I too didn’t learn it until much later. I really wish I had known.

foolfoolz 2 days ago

the biggest things to change java have been type inference, lambdas, records, streams (functional ops on collections), and pattern matching. these are all must-have features for any modern programming language. at this point any language without these features will feel old and legacy. it’s impressive java was able to add them all on decades after release, but you do feel it sometimes

newAccount2025 2 days ago

For my part, returning to Java a couple years back after 15+ years away, streams + var/val were my favorite discoveries.

Supermancho 2 days ago

Can someone explain why developers like var?

  • N70Phone 2 days ago

    Previously (or if you simply don't use var), a lot of java code takes the form of

      BeanFactoryBuilder builder = new BeanFactoryBuilder(...);
    
    This is just straight up a duplicate. With generics, generic parameters can be left out on one side but the class itself is still duplicated.
  • pgwhalen 2 days ago

    It reduces (often repetitive) visual noise in code, which can make it more readable. I wouldn’t recommend using it in all cases, but it’s a good tool to have.

  • guax 2 days ago

    To me var is what makes modern java somewhat readable and more bearable. It was always a joke that it takes too long to write anything in java because of the excessive syntax repetitions and formalities. To me that joke is heavily based on a reality that modern Java is tackling with this quality of life features.

    • whartung 2 days ago

      I get the attraction to var, but I, personally, don't use it, as I feel it makes the code harder to read.

      Simply, I like (mind, I'm 25 year Java guy so this is all routine to me) to know the types of the variables, the types of what things are returning.

        var x = func();
      
      doesn't tell me anything.

      And, yes, I appreciate all comments about verbosity and code clutter and FactoryProxyBuilderImpl, etc. But, for me, not having it there makes the code harder for me to follow. Makes an IDE more of a necessity.

      Java code is already hard enough to follow when everything is a maze of empty interfaces, but "no code", that can only be tracked through in a debugger when everything is wired up.

      Maybe if I used it more, I'd like it better, but so far, when coming back to code I've written, I like things being more explicit than not.

      • CrimsonRain 2 days ago

        1. Just because you can use var in a place, doesn't mean you should. Use it where the type would be obvious when reading code like

          var myPotato = new PotatoBuilder.build();
        not like

          var myFood = buyFood();
        
        where buyFood has Potato as return type.

        2. Even if you don't follow 1, IDEs can show you the type like

          var Potato (in different font/color) myFood = buyFood();
      • nunobrito 2 days ago

        Yes, well expressed. For that case using var is not a wise approach.

        It does help when writing:

            var x = new MyClass();
        
        Because then you avoid repetition. Anyways, I don't ever use "var" to keep the code compatible with Java-8 style programming and easier on the eyes for the same reasons you mention.
    • mi_lk 2 days ago

      I have the opposite feeling. var makes it easier to write but harder to read/review. Without var you know the exact type of a variable without going through some functions for example

  • speed_spread 2 days ago

    It's called type inference and it's the way things should be. You get the same types but you don't have to spell them out everywhere. Java doesn't even go all the way, check OCaml to see full program inference.

    • miningape 2 days ago

      OCaml's type inference is truly amazing, makes it such a delight to write statically typed code - reading it on the other hand...

      But I think that's easily solved by adding type annotations for the return type of methods - annotating almost anything else is mostly just clutter imo.

      • Supermancho 2 days ago

        Annotations would be a substitute for writing the return type. Extra code for a shortcut seems like the worst solution.

        • miningape 2 days ago

          I don't mean Java annotations, those would be too clunky - in OCaml a type annotation is really just adding `: <typename>` to variables, function return types, etc.

          so fibonacci could look like this

          ```

          let rec fib n =

            match n with
          
            | 0 -> 1
          
            | 1 -> 1
          
            | _ -> fib (n - 1) + fib (n - 2)
          
          ```

          or with annotations it becomes this:

          ```

          let rec fib (n: int): int =

            // Same as above :)
          
          ```
  • flykespice 2 days ago

    It's another thing they adopted from Kotlin, since Kotlin is supposed to be a "better java". Now Java is retroactively adopting Kotlin freatures.

    • speed_spread 2 days ago

      Kotlin didn't invent type inference, it's a feature from ML.

      • raddan a day ago

        And not only that, Java was like 40 years late to the party. ML had Hindley-Milner type inference back in 1978! I can only imagine how foreign ML must have felt compared to other languages at the time.

      • flykespice a day ago

        I never said Kotlin invented type inference, just that the syntax was straight adopted from kotlin

        • speed_spread a day ago

          What syntax? val and var were used by Scala before Kotlin. Lombok also has val. Java just needed to pick a keyword.

  • tofflos 2 days ago

    It's terse and it lines up the variable names.

  • Traubenfuchs 2 days ago

    It‘s a harmful code smell: It often obfuscates the type, forcing you to actively check for the type and should not be used.

    • a57721 2 days ago

      It is used for things like "Foo x = new Foo()" where the type is obvious.

    • newAccount2025 2 days ago

      Your IDE can do that?

      • bigstrat2003 2 days ago

        Languages should not be designed around the assumption that people use an IDE.

      • MBCook 2 days ago

        It can. But seeing the text there is faster than having to hover to see what the type is.

        It exists. It’s fine. People obviously like it.

        Some don’t, I’m one of them. I don’t see the advantage is very big at all. I don’t think it’s worth the trouble.

        But that’s me.

      • noisy_boy 2 days ago

        What if I'm just looking at a pull request on GitHub? Sure I can check out the branch etc but that's just adding more friction.

        • Traubenfuchs a day ago

          Bingo.

          Sometimes I doubt most hacker news commentors have ever worked in big corpo environments where you have to regularly review large PR in some broken webapp like GitHub.

          • vips7L a day ago

            If you haven’t used GitHub plugins in your ide to review you should really give it a try. It’s really night and day.

    • Supermancho 2 days ago

      This is what it looks like to me. If you wanted to do this, why not use a scripting language where you can use this kind of practice everywhere? In Java, I don't expect to have to look up the return type of something to discover a variable type. Graciously, I can see how you can save rewriting the Type declaration when it's a function return you want to mutate.

      Generally, you save some keystrokes to let other people (or future you) figure it out when reading. It seems like bad practice altogether for non trivial projects.

      • guax 2 days ago

        Modern IDEs will show you the type of anything at all times. I do not understand your point unless you're doing raw text editing of Java source.

        Those keystrokes are not just saved on writing, they make the whole code more legible and easier to mentally parse. When reading I don't care if the variable is a specific type, you're mostly looking whats being done to it, knowing the type becomes important later and, again, the IDE solves that for you.

        • Supermancho 2 days ago

          > Modern IDEs will show you the type of anything at all times. I do not understand your point unless you're doing raw text editing of Java source.

          The word "String" "Integer" et al. + "var" is too much real estate for being explicit. Sometimes, I'm looking at the decompiled source from some library that doesn't have a source package available.

          > Those keystrokes are not just saved on writing, they make the whole code more legible and easier to mentally parse.

          This is incorrect. Repeating it doesn't make it true. For trivial code (<10 lines) probably seems fine at the time. Lots of bad practices start with laziness.

          Changing practice because an author thinks a function is small enough when it was written, is a recipe for unclean code with no clear guidelines on what to use or expect. Maybe they rather put the onus on a future reader; this is also bad practice.

nunobrito 2 days ago

Fully agree with most votings but 3/10 text blocks?!

That has got to be one of the most useful recent features. :-)

The pleasure of just copying and paste text in plain ASCII that looks as intended rather than a huge encoded mess of "\r\n"+ concatenations.

But ok, I'm just an ASCII art fan. ^_^

  • MBCook 2 days ago

    String sql = “Not having “ +

    “to break up “ +

    “SQL statements” +

    “like this for readability “ +

    “thus making them hard to edit “ +

    “was incredibly useful at my job.”;

    (Note: I put a subtle bug in there because it always happened)

    SQL injection is horrible, but people were managing to do that all these years after prepared statements anyway without text blocks. I really don’t think they made things worse. Same thing with embedding HTML in the code. They were gonna do it anyway.

JanisErdmanis 2 days ago

> if you wanted to store an integer in a collection, you had to manually convert to and from the primitive int type and the Integer “boxed” class

I have never worked with Java. What is this? Why would one want to have a class for an Integer?

  • iceboundrock 2 days ago

    Primitive variables in Java, such as `int`, `boolean`, and `double`, store their actual values directly in memory. When they are local variables inside a method, this memory is typically allocated on the thread's stack. These primitives do not have the structure or overhead of an object, including the object header used by the Garbage Collector (GC) to manage heap-allocated objects.

    If a primitive value must be treated as an object (e.g., when stored in a Java Collection like ArrayList or when passed to a method that requires an object), Java uses a process called `boxing` to wrap the primitive value into an instance of its corresponding Wrapper class (e.g., Integer, Boolean, Double). These Wrapper objects are allocated on the heap and do possess the necessary object header, making them subject to the GC's management.

    • wpollock 2 days ago

      Aside from this, having such a class provides a convenient place to hold all the int utility functions, and the same for the other primitive types.

  • morcus 2 days ago

    It's because the collection types (and generics) don't support primitives, only objects. So you been to stuff the primitives into objects to use them with a lot of the standard library.

    • dcminter 2 days ago

      One of the more amusing bugs I had to figure out resulted from the fact that some of the autoboxed values get cached, resulting in peculiar behaviour when someone managed to reflectively change the boxed primitive value...

      i.e. something like:

        Integer x = 42
        highlyQuestionableCode(x);
        println(x); // "24" WAT?
      
      I'm a fan of JEP-500...

      https://openjdk.org/jeps/500

    • JanisErdmanis 2 days ago

      That doesn't sound very pleasant.

      • dcminter 2 days ago

        Well, it was annoying until autoboxing came in.

          // Before autoboxing
          list.add(new Integer(42));
        
          // After autoboxing
          list.add(42);
        
        Mostly it's a non-issue now. If you're desperately cycle/memory constrained you're likely not using Java anyway.
        • marginalia_nu 2 days ago

          You can get pretty good performance out of Java these days, so long as you know to avoid stuff like boxed primitives and the streams api, as they generally have god-awful memory locality, and generally don't vectorize well.

          • dcminter 2 days ago

            Yeah, I know there are even oddballs using it for HFT and the like - I like Java a lot, but even I find that a bit peculiar.

            Edit: actually, if someone here is using it for something like that I'd love to hear the rationale...?

            • marginalia_nu a day ago

              I've worked adjacent to that space (high performance fin-tech java), enough that I feel qualified to answer.

              It's mostly a trade-off. Java's tooling, reliability and ecosystem is some of the best around. Even though building high performance software in Java is a bit of a pain, looking at the bigger picture, it's often still worth it.

            • ivan_gammel 2 days ago

              Not „oddballs“ for sure. Java established itself as the primary enterprise language for fintech in 2000s and since then there was and there is no reason to switch. It offers everything business needs including vast supply of workforce.

        • commandersaki a day ago

          If you're desperately cycle/memory constrained you're likely not using Java anyway.

          Java Cards would like to have a word with you. But yeah I know what you mean.

      • Arwill 2 days ago

        There are libraries, like fastutil, that provide collections for primitive types.

      • coldtea 2 days ago

        Either the same of this feature or always hiding the boxing (e.g. a Python int is actually a wrapper object as well, with some optimizations for some cases like interning) is the case in almost all languages.

        • JanisErdmanis 2 days ago

          I personally use Julia, which does not have such boxing issues. Rust, C, C++, and Fortran also avoid boxing like this. Perhaps Go is also free from such boxing? Python does it, that's true.

  • raspasov 2 days ago

    I think your intuition is correct: you probably don’t.

    That’s also very likely changing. Lookup “project Valhalla”. It’s still a work in progress but the high level goal is to have immutable values that “code like a class, work like an int”.

    PS When I say “changing”: it’s being added. Java tries hard to maintain backward compatibility for most things (which is great).

simonklee 2 days ago

java.util.Date and java.util.Calendar are the two packages I remember struggling with as a new programmer. Which I guess is solved with java.time after Java 8.

rr808 2 days ago

Java is great, Spring ruined the platform.

  • JackFr 2 days ago

    Clearly someone who never knew EJBs.

    (I know the irony of Spring is that it became what it replaced. But it got a good ten or fifteen years of productivity before it began getting high on its own supply. )

  • as1mov 2 days ago

    Spring and the associated enterprise spaghetti developers have done more damage to the platform rather than the language itself. I've managed to work for almost a decade with Java without using Spring at this point (and count myself lucky for it), but the chances of finding a new job with the same requirement are slimmer and slimmer now.

  • __float 2 days ago

    As someone who has used Java pretty extensively for a few years (but in an environment where Spring was forbidden), why is that?

    • MBCook 2 days ago

      You’ll find differing opinions.

      As someone who has worked on code bases that did not have spring that really should have and had to do everything manually: when used well it’s fantastic.

      Now people can certainly go majorly overboard and do the super enterprise-y AbstractBoxedSomethingFactoryFacadeManagerImpl junk. And that is horrible.

      But simple dependency injection is a godsend. Ease of coding my just adding an annotation to get a new component you can reference anywhere easily is great. Spring for controllers when making HHTP endpoints? And validation of the data? Love it!

      Some of the other modules like Spring Security can be extremely confusing. You can use the Aspect Oriented Programming to go overboard and make it nearly impossible to figure out what the hell is happening in the program.

      Spring is huge, and it gets criticized for tons of the more esoteric or poorly designed things it has. But the more basic stuff that you’re likely to get 90+ percent of the value out of really makes things a lot better. The relatively common stuff that you’ll see in any Spring Boot tutorial these days.

      • rr808 2 days ago

        Actually I really dont like DI and its a core of my dislike. I can easily create objects directly and pass in dependencies, its a lot easier to debug and see what is going on as opposed to Spring magic.

        • MBCook 2 days ago

          Passing a couple objects seems easy. But I’ve seen where it leads in a large program.

          Passing things down layer after layer gets old. High level stuff takes tons of parameters due to all the layers below.

          You end up with God objects that mostly just contain every other object someone might want to reference.

          And you know what? That object starts to feel like a great place to put state. Cause it’s already being passed everywhere.

          So instead of using Spring to get a ThingService to work with your Thing at the spot you need it, suddenly all the code has access to all the stuff and states. And like the temptation of The One Ring programmers use it.

          Now you have a spaghetti rats nest. Where is the code that deals with the state for a Gloop? It’s now everywhere. Intertwined with the code for a Thing. And a Zoob. It doesn’t need to be. But it is.

          It becomes almost impossible to unit test things. Because everything can do/see everything. Untangling and extracting or replacing any part of the whole is a Herculean job.

          You don’t need Spring for something tiny. And maybe it’s possible to go without dependency injection in a large app and keep things manageable and easy to understand.

          In my career I’ve mostly seen the mess. I’ve helped try to untangle it by slowly introducing Spring.

          I’d rather have it, with its organization and standard patterns, than the Wild West of whatever any programmer or junior decided to do over the last 15 years and how that has evolved. In a complex application with lots of programmers coming and going I find it a net benefit.

        • michaelcampbell a day ago

          > Actually I really dont like DI and its a core of my dislike. I can easily create objects directly and pass in dependencies...

          So you DO like DI, you just do it explicitly. Which is fine.

        • noisy_boy 2 days ago

          I mean if you just follow the common usage patterns of Spring Boot, it is not really magic - just stick to the core annotations and not more. It does require one to have basic familiarity with Spring Boot but I don't think that is an unreasonable ask. And like many things, people can go totally overboard and make it an impenetrable mess but I think that is on the programmer, not the framework.

    • mrkeen a day ago

      The beautiful and essential technique of DI (dependency inversion) got namesquatted hard by DI (dependency injection).

      Before, you used to write "loosely coupled" software by decoupling your business logic from your IO to keep it testable. You could take virtually anything worth testing, 'new' it in a unit test, and bob's your uncle.

      Now you write "loosely coupled" software by keeping the coupling between components, but also couple them to a bunch of Spring dependencies too (check your imports!). Now you can't instantiate anything without Spring.

lenkite 2 days ago

Sorry, but several of these ratings are just plain wrong. I am not even a Java developer these days, but it is extremely clear that author did not work in a domain that rightfully leveraged the Java features he rates as of little use.

He would have been better served by opening a poll - that would have opened his eyes to the use of these features.

Its like giving operator overload a rating of 1 in C++/Python. Sure, if you don't find any need for it in your domain, it would look stupid to you.

AbuAssar 2 days ago

so java 22, 23, 24 are all released in 2024?

  • dcminter 2 days ago

    No, it's a 6 month release cadence. You might be confusing the initial release with a point release which are less regular. Edit: oh, my bad, I see the article author had the wrong year for 24.

      22 was March 2024
      23 was September 2024
      24 was March 2025
      25 was September 2025
    
    This is much better than the old "release train" system where e.g Java 5 and Java 6 were released in Sept 2004 and Nov 2006 respectively!
harladsinsteden 2 days ago

I don't know what to make of this list...

Very strange reasoning and even stranger results: Streams 1/10?! Lambdas (maybe the biggest enhancement ever) a mere 4/10?!

Sorry, but this is just bogus.

  • tofflos 2 days ago

    I will make any excuse to use Streams but understand the negativity. They are difficult to debug and I feel the support for parallelism complicated, and in some cases even crippled, the API for many common use cases.

  • nunobrito 2 days ago

    I'm that author. It has been more than a decade and still won't use streams nor lambdas. Makes the code too difficult to write and debug for me.

    Really prefer to have more lines of code and understanding very clearly what each one is doing, than convoluting too many instructions on a single line.

    • harladsinsteden a day ago

      For me it's the opposite: If I had to write the code that I usually use lambdas for in any other way then _that_ would be very difficult to write and to debug.

      Especially when writing JavaFX code which is full of callbacks and event handlers I really don't see any other (useful) option.

      Can lambdas be misused? Of course they can - but so can every other code construct.

    • samus 2 days ago

      I bet you don't like how it looks when you put huge code blocks inside a lambda. Me neither. But that's an issue with coding style; it forces you to extract processing code into a method. I'd argue the opposite way - imperative syntax constructs make spaghetti code too easy to work with.

  • foresterre 2 days ago

    They're a bit verbose, the interfaces are slightly convoluted and some basic operations are missing from the standard library.

    It's also a little convoluted to work with different types of data.

    For this one, I wish they would have taken a bit more inspiration from other languages and spent the time to make it more readable.

    That said, I generally like streams a lot, and they do reduce the amount of branching, and having less possible code execution points makes testing easier too.

iceboundrock 2 days ago

It appears that most of the good changes are imported from C#.

  • coldtea 2 days ago

    Most of original C# was imported from Java, so there's that...

  • sjrd 2 days ago

    Or Scala. Or Kotlin. Or any of the other languages that had most of these features years if not decades before Java. ;)

    • marginalia_nu 2 days ago

      Yeah that's very much an explicit design philosophy of Java, dating way back. Let other languages experiment, and adapt what proves useful.

      It hasn't worked out in terms of delivering perfect language design, but it has worked out in the sense that Java has an almost absurd degree of backward compatibility. There are libraries that have had more breaking changes this year than the Java programming language has had in the last 17 releases.

      • agos 2 days ago

        What other language made them think checked exceptions were a good idea?

        • bigstrat2003 2 days ago

          They are a good idea. They solve the problem that you don't know where an exception is coming from (the "invisible control flow" complaint), and let the compiler help you to avoid mistakes when refactoring. There is zero valid reason to hate on checked exceptions.

          • gbear605 2 days ago

            The only problem with them is that they don’t work well with lambdas (and related features like Stream). If you need to call a method that throws a checked exception inside of a Stream, there’s no good way to pass it up other than re-throwing it as unchecked or some other hack (like collecting all the thrown exceptions in a separate loop).

            A different implementation of lambdas that allow for generic exceptions would probably solve it, but then that introduces other issues with the type system.

            My other complaint is that the standard library didn’t have enough pre-made exceptions to cover common usecases.

            • samus 2 days ago

              When you use lambdas you lose control over when and how often your code gets executed. Since checked exceptions are very often thrown by code that has side effects, I'd consider the friction to be a feature.

              > collecting all the thrown exceptions in a separate loop

              It's really not comfortable to do so in Java since there is no standard `Either` type, but this is also doable with a custom collector.

              • gbear605 a day ago

                > Since checked exceptions are very often thrown by code that has side effects, I'd consider the friction to be a feature.

                This is true, but I think that it’s partly true because checked exceptions are cumbersome here. In my ideal world, the majority of functions would throw exceptions, testing cases that today are either missed or thrown as unchecked exceptions.

        • ivan_gammel 2 days ago

          Some inspiration came from C++, Modula-3, CLU etc. (note, inspiration, not validation of the idea)

          They exist since v1, which had very different philosophy than Java of 2010s-2020s. 1990s were an interesting time in language design and software engineering. People started reflecting on the previous experiences of building software and trying to figure out how to build better, faster, with higher quality. At that time checked exceptions were untested idea: it felt wrong not to have them based on previous experience with exceptions in C++ codebases, but there were no serious arguments against them.

        • vips7L 2 days ago

          They are a good idea. Checked errors are so important for correctness. HN’s darling Rust exclusively uses checked errors.

          • ahoka a day ago

            Rust has panic, although intended for “unrecoverable” errors only.

        • hocuspocus 2 days ago

          I assume it was the other way around, a slight twist to exceptions, only enforced by the compiler (the JVM doesn't care about checked/unchecked) probably seemed a cheap and reasonable way to implement explicit error handling. Given that Java ergonomics of the time didn't offer any convenient and performant way to return multiple values instead.

          • MBCook 2 days ago

            I always thought of it as a reaction to the “your program can throw anywhere for any reason due to an exception” nature of C++.

            So they added checked exceptions. That way you can see that a function will only ever throw these two types of exceptions. Or maybe it never throws at all.

            Of course a lot of people went really overboard early on creating a ton of different kinds of exceptions making everything a mess. Other people just got into the habit of using RuntimeExceptions for everything since they’re not checked, or the classic “throws Exception“ being added to the end of every method.

            I tend to think it’s a good idea and useful. And I think a lot of people got a bad taste in their mouth early on. But if you’re going to have exceptions and you’re not going to give some better way of handling errors I think we’re probably better off than if there were no checked exceptions at all.

        • peterashford 21 hours ago

          Checked exceptions are a good idea.