10 ms·
Java is fast, code might not be
- koakuma-chan 7mo agoAs much as I love Java, everybody should just be using Rust. That way you are actually in control, know what's going on, etc. Another reason specifically against Java is that the tooling, both Maven and Gradle, still stucks.
- krona 7mo ago> That way you are actually in control Programming in Rust is a constant negotiation with the compiler. That isn't necessarily good or bad but I have far more control in Zig, and flexibility in Java.
- koakuma-chan 7mo agoYes, there is a learning curve to Rust, but once you get proficient, it no longer bothers you. I think this is more good than bad, because, for example, look at Bun, it is written in Zig, it has so many bugs. They had a bug in their filesystem API that freezed your process, and it stayed unfixed for at least half a year after I filed it. Zig is a nice C replacement, but it doesn't have the same correctness guardrails as Rust.
- krona 7mo agoAssuming we're talking about the same bug, The filesystem API freeze wasn't caused by Zig's lack of correctness guarantees, but a design flaw in Bun's implementation.
- dryarzeg 7mo agoMaybe I'm stupid, but I never actually understood people who blame programming languages for bugs in software. Because sure, it's good to have guardrails, but in my opinion, if you're writing a program and there's a bug, unless this bug lies somewhere in implementation of compiler/interpreter/etc, you can't blame the tooling, It's you who introduced this bug. It was your mistake. It's cool when your tooling warns you about potential bugs or mistakes in implementation, but it's still your responsibility to write the correct code. If you pick up a hammer and hit your finger instead of the nail, then in most cases (though not always) it’s your own fault.
- chuckadams 7mo agoWhen millions of users constantly make the same mistake with the tool, there may be a problem with the tool, whether it's a defect in the tool or just that it's inappropriate for the job. Blaming the user might give one a righteous feeling, but decade after decade that approach has failed to actually fix any problems.
- dryarzeg 7mo agoThat's why I say "in most cases" - so not always, actually. There might be problems with tools, I'm not trying to deny that. And by the way, what if some (or even most) of the users just don't have enough skill to use the tool properly? Again, there could be a problem with tool, yes, but you can't always blame only tools for mistakes users make.
- koakuma-chan 7mo agohttps://github.com/oven-sh/bun/issues/18192 https://github.com/oven-sh/bun/issues/18192 I am talking about this bug. It looks like it is still unfixed, in the sense, there is a PR fixing it, but it wasn't merged. LOL. Regardless of whether this specific bug would be caught by Rust compiler, Bun in general is notorious for crashing, just look at how many open issues there are, how many crashes. Not saying that you cannot make a correct program in Zig, but I prefer having checks that Rust compiler does, to not having them.
- j-vogel 7mo agoI'm a fan of Rust too. But there are millions of Java applications running in production right now, and some of them are running these anti-patterns today. Not everyone has the option to rewrite in a different language. For those teams, knowing what to look for in a profiler can make a real difference without changing a single dependency.
- koakuma-chan 7mo agoI think that right now it is easier than ever to rewrite your app in Rust, due to LLMs. Unfortunately there are still people out there who dismiss this idea, and continue having their back-end written in much inferior languages, like JavaScript or Python. If your back-end is written in Java, you aren't even in the worst spot.
- kykat 7mo ago"You think" is cheap, try doing it, rewrite an existing library in rust and see how it goes. Doing a rough prototype is easy, but the real work starts after that.
- piva00 7mo agoGradle does suck, it gives too much freedom on a tool that should be straightforward and actively design to avoid footguns, it does the opposite by providing a DSL that can create a lot of abstractions to manage dependencies. The only place I worked where the Gradle configuration looked somewhat sane had very strict design guidelines on what was acceptable to be in the Gradle config. Maven on the other hand, is just plain boring tech that works. There's plenty of documentation on how to use it properly for many different environments/scenarios, it's declarative while enabling plug-ins for bespoke customisations, it has cruft from its legacy but it's quite settled and it just works. Could Maven be more modern if it was invented now? Yeah, sure, many other package managers were developed since its inception with newer/more polished concepts but it's dependable, well documented, and it just plain works.
- koakuma-chan 7mo agoI would disagree that either "plain works" because to even package your app into a self-contained .jar, you need a plugin. I can't recall the specifics now, but years ago I spent many hours fighting both Maven and Gradle.
- looperhacks 7mo agoYou "need a plugin" in the sense that every component of maven is a "plugin". The core plugins give you everything you need to build a self-contained jar - if you wanted to, you don't even have to configure the plugins, if you want to write a long cli command instead.
- piva00 7mo agoWell, yes? It's a feature provided by a plugin, like any other feature in Maven, you declare the plugin for creating a fat-jar or single-jar and use that. It's just some lines of XML configuration so it plain works. Like I said, it's not hypermodern with batteries included, and streamlined for what became more common workflows after it was created but it doesn't need workarounds, it's not complicated to define a plugin to be called in one of the steps of the lifecycle, and it's provided as part of its plugin architecture. I can understand spending many hours fighting Gradle, even I with plenty of experience with Gradle (begrudgingly, I don't like it at all) still end up fighting its idiocies but Maven... It's like any other tool, you need to learn the basics but after that you will only fight it if you are verging away from the well-documented usage (which are plenty, it's been battle-tested for decades).
- jayd16 7mo agoNot knowing what's going on in Java is a personal problem. The language and jvm have its own quirks but it's no less knowable than any other compiler optimized code. The debugging and introspection tooling in Java is also best in class so I would say it's one of the more understandable run times. Gradle does suck and maven is ok but a bit ugly.
- dionian 7mo agoLLMs take the whole argument away. Yes, maven/gradle/sbt suck to work with. But now you can just generate it.
- dionian 7mo agoI've been using maven for 20+ years, gradle for 10? ant for 5 before that. sbt for 15. I've written custom plugins for all of them. I know them quite well, unfortunately. I use LLMs to maintain them now. I keep the build files simple. It was an inconvenience before, but a trifle now.
- computerdork 7mo agoActually, I like Maven. It's perfect for code that is broken into medium-sized projects, which makes it great for service-oriented architectures (would have said microservices here instead, but think we're learning that breaking our services too finely down is generally not a good idea). Yeah, it seems like Maven is designed to build just one project with relatively little build-code (although, figuring out versioning of the libs used in your build can get tricky, but guessing this is how it is in most languages). It's still one of my favorites build tools for many situations.
- spopejoy 7mo agoLOL I wish. LLMs massacre gradle code all the time. Once you're past boilerplate generation and doing anything remotely unusual they can't stop hallucinating broken shit that they insist works.
- ActorNightly 7mo agoLets look at Java in modern day. * Most mature Java project has moved to Kotlin. * The standard build system uses gradle, which is either groovy or kotlin, which gets compiled to java which then compiles java. * Log4shell, amongst other vulnerabilities. * Super slow to adopt features like async execution * Standard repo usage is terrible. There is no point in using Java anymore. I don't agree that Rust is a replacement, but between Python, Node, and C/C++ extensions to those, you can do everything you need.
- deleted 7mo ago[deleted]
- shermantanktop 7mo agoI’ll never understand the impulse to tell the entire world what to do based on your own personal preferences and narrow experiences. It gets a reaction, though, so great for social media.
- pjmlp 7mo agoRust has no place other than deployment scenarios where any kind of automatic resource management, be it tracing GC or reference counting, is not wanted for, either due to technical reasons, or being a waste of time trying to change people's mindset.
- r_lee 7mo ago[flagged]
- bearjaws 7mo agoJavaScript can be fast too, it's just the ecosystem and decisions devs make that slow it down. Same for Java, I have yet to in my entire career see enterprise Java be performant and not memory intensive. At the end of the day, if you care about performance at the app layer, you will use a language better suited to that.
- j-vogel 7mo agoFair point on ecosystem decisions, that's basically the thesis of the post. These patterns aren't Java being slow, they're developers (myself included) writing code that looks fine but works against the JVM. Enterprise Java gets a bad rap partly because these patterns compound silently across large codebases and nobody profiles until something breaks.
- maccard 7mo agoMy experience with the defaults in JavaScript is that they’re pretty slow. It’s really, really easy to hit the limits of an express app and for those limits to be in your app code. I’ve worked on JVM backed apps and they’re memory hungry (well, they require a reallocation for the JVM) and they’re slow to boot but once they’re going they are absolutely ripping fast and your far more likely to be bottlenecked by your DB long before you need to start doing any horizontal scaling.
- wiradikusuma 7mo agoCompile it to native (GraalVM) and you can get it fast while consuming less memory. But now your build is slow :)
- maccard 7mo agoThe minute a project has maven in it the build is slow. Don’t even get me started on Gradle…
- FatherOfCurses 7mo ago"Enterprise Java" Factories! Factories everywhere!
- kyrra 7mo agoFirst request latency also can really suck in Java before hotpathed code gets through the C2 compiler. You can warm up hotpaths by running that code during startup, but it's really annoying having to do that. Using C++, Go, or Rust gets you around that problem without having to jump through the hoops of code path warmup. I wish Java had a proper compiler.
- bombcar 7mo agoDo none of the JVMs do that? GraalVM?
- user3939382 7mo agoMy architecture builds a command registry in Clojure/JVM which runs as a daemon, the registry is shared by a dynamically generated babashka (GraalVM) shell that only includes whitelisted commands for that user. So for the user, unauthorized commands don’t even exist, and I get my JVM app with no startup overhead.
- rileymichael 7mo agothe best way is via CRaC (https://docs.azul.com/crac/ https://docs.azul.com/crac/) but only a few vendors support it and there’s a bit of process to get it setup. in practice, for web applications exposing some sort of `WarmupTask` abstraction in your service chassis that devs can implement will get you quite far. just delay serving traffic on new deployments until all tasks complete. that way users will never hit a cold node
- zelphirkalt 6mo agoBut then we can complain about the long start time for each instance or JVM. It is choosing a different trade-off.
- rileymichael 6mo agostart time generally isn't a huge concern for web applications (outside of serverless) since you've got the existing deployment serving traffic until its ready. if you're utilizing kubernetes, the time to create the new pods, do your typical blue-green promotion w/analysis tests etc. is already a decent chunk of time regardless of the underlying application. if you get through it in 90 seconds instead of 60, does that really matter?
- tripple6 7mo agoDo good, don't do bad. Okay.
- abound 7mo agoI don't think that's a charitable take of the article. To many programmers, it wouldn't be obvious that some of these footguns (autoboxing, string concatenation, etc) are "bad", or what the "good" alternatives are (primitives, StringBuilder, etc). That said, the article does have the "LLM stank" on it, which is always offputting, but the content itself seems solid.
- tripple6 6mo agoOf course it's not. I don't see any reason of posting an article that repeats the very same basics of Java or literally any programming language. Simple basics. Sure, if a particular programmer is not aware of these, the particular programmer would be very surprised that any of their operations are not imaginary O(0) (if they'd even care).
- andrewmcwatters 7mo ago[dead]
- ryguz 7mo ago[flagged]
- liampulles 7mo agoUnderstanding algorithmic complexity (in particular, avoiding rework in loops), is useful in any language, and is sage advice. In practice though, for most enterprise web services, a lot of real world performance comes down to how efficiently you are calling external services (including the database). Just converting a loop of queries into bulk ones can help loads (and then tweaking the query to make good use of indexes, doing upserts, removing unneeded data, etc.) I'm hopeful that improvements in LLMs mean we can ditch ORMs (under the guise that they are quicker to write queries and the inbetween mapping code with) and instead make good use of SQL to harness the powers that modern databases provide.
- j-vogel 7mo agoAuthor here. DB and external service calls are often the biggest wins, thanks for calling that out. In my demo app, the CPU hotspots were entirely in application code, not I/O wait. And across a fleet, even "smaller" gains in CPU and heap compound into real cost and throughput differences. They're different problems, but your point is valid. Goal here is to get more folks thinking about other aspects of performance especially when the software is running at scale.
- PathOfEclipse 7mo agoMy experience profiling is that I/O wait is never the problem. However, the app may actually be spending most of it's CPU time interacting with database. In general, networks have gotten so fast relative to CPU that the CPU cost of marshalling or serializing data across a protocol ends up being the limiting factor. I got a major speedup once just by updating the JSON serialization library an app used.
- cogman10 7mo agoEasy to get wrong as well. There's a balance with a DB. Doing 1 or 2 row queries 1000 times is obviously inefficient, but making a 1M row query can have it's own set of problems all the same (even if you need that 1M). It'll depend on the hardware, but you really want to make sure that anything you do with a DB allows for other instances of your application a chance to also interact with the DB. Nothing worse than finding out the 2 row insert is being blocked by a million row read for 20 seconds. There's also a question of when you should and shouldn't join data. It's not always a black and white "just let the DB handle it". Sometimes the better route to go down is to make 2 queries rather than joining, particularly if it's something where the main table pulls in 1000 rows with only 10 unique rows pulled from the subtable. Of course, this all depends on how wide these things are as well. But 100% agree, ORMs are the worst way to handle all these things. They very rarely do the right thing out of the box and to make them fast you ultimately end up needing to comprehend the SQL they are emitting in the first place and potentially you end up writing custom SQL anyways.
- null-phnix 7mo ago[dead]
- jandrewrogers 7mo agoYou can write many of the bad examples in the article in any language. It is just far more common to see them in Java code than some other languages. Java is only fast-ish even on its best day. The more typical performance is much worse because the culture around the language usually doesn't consider performance or efficiency to be a priority. Historically it was even a bit hostile to it.
- steve1977 7mo agoWhich, to be fair, in many cases is ok. If you just need to churn out LOB apps for worker drones as cheap as possible, performance is probably not the most important factor.
- this_user 7mo agoPerformance is really not Java's issue. Even bad Java code is still substantially faster than the bulk of modern software that is based on technologies like Python or JavaScript/Node.js.
- KronisLV 7mo agoThis might also be why I heard colleagues saying “Nono, listen, these ‘N+1 problems’ and our nested service calls aren’t an issue because it works well enough” until it eventually didn’t. I’d rather not have bad code in any language. Modern Java runtimes are pretty good, though.
- array_key_first 7mo agoNo - this is not entirely true. Many of Java's fundamental design decisions lead to unexpected slowness that just does not happen in other languages. For example, appending to a string in a loop. That only happens because of how Java handles strings. In C++, that's very fast. As fast as it can get, really. Since it all goes into the same buffer that gets mutated and expands at a good growth rate. Basically, equivalent to StringBuilder, but that's just all strings. Or, boxing. C++ doesn't have to box generics to store them in a container.
- comrade1234 7mo agoAlso finding the right garbage collector and settings that works best for your project can help a lot.
- zvqcMMV6Zcr 7mo ago> Exceptions for Control Flow This one is so prevalent that JVM has an optimization where it gives up on filling stack for exception, if it was thrown over and over in exact same place.
- j-vogel 7mo agoAuthor here. Great callout. That's the -XX:+OmitStackTraceInFastThrow optimization, been around since JDK 5. The C2 compiler detects exceptions thrown repeatedly from the same site and starts reusing a preallocated instance without filling the stack trace. Good for performance, but it makes debugging harder in production since you lose the trace. You can disable it with -XX:-OmitStackTraceInFastThrow if you need the traces back.
- dust-jacket 7mo agoah, thank you. Haven't worked in java for a bit now, but that was the only one I read where I was like "I'm sure we didn't have to avoid this when I worked on java". The rest were all very familiar. Well, apart from the new stuff. I think most of my code was running in java 6...
- wood_spirit 7mo agoA subject close to my heart, I write a lot of heavily optimised code including a lot of hot data pipelines in Java. And aside from algorithms, it usually comes down to avoiding memory allocations. I have my go-to zero-alloc grpc and parquet and json and time libs etc and they make everything fast. It’s mostly how idiomatic Java uses objects for everything that makes it slow overall. But eventually after making a JVM app that keeps data in something like data frames etc and feels a long way from J2EE beans you can finally bump up against the limits that only c/c++/rust/etc can get you past.
- polothesecond 7mo ago> And aside from algorithms, it usually comes down to avoiding memory allocations. I’ve heard about HFT people using Java for workloads where micro optimization is needed. To be frank, I just never understood it. From what I’ve seen heard/you have to write the code in such a way that makes it look clumsy and incompatible with pretty much any third party dependencies out there. And at that point, why are you even using Java? Surely you could use C, C++, or any variety of popular or unpopular languages that would be more fitting and ergonomic (sorry but as a language Java just feels inferior to C# even). The biggest swelling point of Java is the ecosystem, and you can’t even really use that.
- yunnpp 7mo agoI am very interested about this and would like an authoritative answer on this. I even went as far as buying some books on code optimization in the context of HFT and I was not impressed. Not a single snippet of assembly; how are you optimizing anything if you don't look at what the compiler produces? But on Java specifically: every Java object still has a 24-byte overhead. How doesn't that thrash your cache? The advice on avoiding allocations in Java also results in terrible code. For example, in math libraries, you'll often see void Add(Vector3 a, Vector3 b, Vector3 our) as opposed to the more natural Vector3 Add(Vector3 a, Vector3 b). There you go, function composition goes out the window and the resulting code is garbage to read and write. Not even C is that bad; the compiler will optimize the temporaries away. So you end up with Java that is worse than a low-level imperative language. And, as far as I know, the best GC for Java still incurs no less than 1ms pauses? I think the stock ones are as bad as 10ms. How anyone does low-latency anything in Java then boggles my mind.
- taspeotis 7mo agoKnock Knock Who’s there? long pause Java
- ackfoobar 7mo agoThe premise of this joke is dead since 2020, when ZGC was production ready.
- victor106 7mo agothis is great, so practical!!! any other resources like this?
- titzer 7mo agoFor fillInStackTrace, another trick is to define your own Exception subclass and override the method to be empty. I learned this trick 15+ years ago. It doesn't excuse the "use exceptions for control flow" anti-pattern, but it is a quick patch.
- ivan_gammel 7mo agoGod, please make me unsee it. That‘s a cool trick that turns into an anti-pattern itself if abused.
- hiyer 7mo agoI ran into 5 and 7 in a Flink app recently - was parsing a timestamp as a number first and then falling back to iso8601 string, which is what it was. The flamegraph showed 10% for the exception handling bit. While fixing that, also found repeated creation of datetimeformatter. Both were not in loops, but both were being done for every event, for 10s of 1000s of events every second.
- wood_spirit 7mo ago(Perhaps a good library for timestamp code in data pipelines https://github.com/williame/TimeMillis https://github.com/williame/TimeMillis)
- hiyer 7mo agoThanks! I'm using Instant.parse at present and this is supposedly 37x faster. Will definitely give it a try.
- wood_spirit 7mo agoAnd report back please! :)
- cmovq 7mo agoWhen you're using a programming language that naturally steers you to write slow code you can't only blame the programmer. I was listening to someone say they write fast code in Java by avoiding allocations with a PoolAllocator that would "cache" small objects with poolAllocator.alloc(), poolAllocator.release(). So just manual memory management with extra steps. At that point why not use a better language for the task?
- ablob 7mo agoYou might have an application for which speed is not important most of the time. Only one or two processes might require allocation-free code. For such a case, why would you burden all of the other code with the additional complexity? Calling out to a different language then may come with baggage you'd rather avoid. A project might also grow into these requirements. I can easily imagine that something wasn't problematic for a long time but suddenly emerged as an issue over time. At that point you wouldn't want to migrate the whole codebase to a better language anymore.
- cogman10 7mo agoBad idea. I've made a pool allocator before, but that was for expensive network objects and expensive objects dealing with JNI. Doing it to avoid memory pressure generally means you simply have a bad algorithm that needs to be tweaked. It's very rarely the right solution.
- gf000 7mo agoNot sure why you are down voted. Depending on how its used it could actually be detrimental to performance. The JVM may optimize many short lived objects better than a pool of objects with less reasonably lifetimes.
- cogman10 7mo agoThis is the second time this week on HN that I've seen people suggesting object pools to solve memory pressure problems. I generally think it's because people aren't experienced with diagnosing and fixing memory pressure. It's one of the things I do pretty frequently for my day job. I'm fortunate enough to be the "performance" guy at work :). It'll always depend on what the real issue is, but generally speaking the problem to solve isn't reinventing garbage collection, but rather to eliminate the reason for the allocation. For example, a pretty common issue I've seen is copying a collection to do transformations. Switching to streams, combining transformation operations, or in an extreme case, I've found passing around a consumer object was the way to avoid a string of collection allocations. Even the case where small allocations end up killing performance, for example like the autoboxing example of the OP, often the solution is to either make something mutable that isn't, or to switch to primitives (Valhalla can't come soon enough). Heck, sometimes even an object cache is the right solution. I've had good success reducing the size of objects on the heap by creating things like `Map<String, String>` and then doing a `map.computeIfAbsent(str, Function.identity());` (Yes, I know about string interning, no I don't want these added to the global intern cache). Regardless, the first step is profiling (JFRs and heap dumps) to see where memory is spent and what is dominating the allocation rate. That's a first step that people often skip and jump straight to fixing what they think is broken.
- ww520 7mo agoThe autoboxing in a loop case can be handled by the compiler.
- kpw94 7mo agoThe Autoboxing example imo is a case of "Java isn't so fast". Why can't this be optimized behind the scenes by the compiler ? Rest of advice is great: things compilers can't really catch but a good code reviewer should point out.
- vbezhenar 7mo agoWhy should compiler optimize obviously dumb code? If developer wants to create billions of heap objects, compiler should respect him. Optimizing dumb code is what made C++ unbearable. When you write one code and compilers generates completely different code.
- carlmr 7mo agoThe problem is rather that Java doesn't have generics and structs, so you're kind of forced to box things or can't use collections.
- vbezhenar 6mo agoNo, in the example they provided, programmer wrote obviously stupid code. It has nothing to do with necessity: Long sum = 0L; for (Long value : values) { sum += value; } I also want to highlight that there are plenty of collections utilizing primitive types. They're not generic but they do the job, so if you have a bottleneck, you can solve it. That said, TBH I think that adding autoboxing to the language was an error. It makes bad code look too innocent. Without autoboxing, this code would look like a mess and probably would have been caught earlier.
- carlmr 6mo ago>They're not generic but they do the job, so if you have a bottleneck, you can solve it. But that's the thing, in other languages you don't need a workaround to work on primitives directly.
- kllrnohj 7mo ago
- cogman10 7mo agoNitpick just because. Orders by hour could be made faster. The issue with it is it's using a map when an array works both faster and just fine. On top of that, the map boxes the "hour" which is undesirable. This is how I'd write it long[] ordersByHour = new long[24]; var deafultTimezone = ZoneId.systemDefault(); for (Order order : orders) { int hour = order.timestamp().atZone(deafultTimezone).getHour(); ordersByHour[hour]++; } If you know the bound of an array, it's not large, and you are directly indexing in it, you really can't do any better performance wise. It's also not less readable, just less familiar as Java devs don't tend to use arrays that much.
- Okx 7mo agomaybe it would be a little better to use ints rather than longs, as Java lists can't be bigger than the int max value anyways. Saves you a cache line or two.
- cogman10 7mo agoFair point, but it is possible this isn't a list but rather some sort of iterable. Those can be boundless. Practically speaking, that would be pretty unusual. I don't think I've ever seen that sort of construct in my day to day coding (which could realistically have more than 1B elements).
- wood_spirit 7mo agoAlso zap the timestamp instant objects if you really need speed; see https://github.com/williame/TimeMillis https://github.com/williame/TimeMillis
- jerf 7mo agoAny non-trivial program that has never had an optimizer run on it has a minimal-effort 50+% speedup in it.
- Okx 7mo agoThe code: public int parseOrDefault(String value, int defaultValue) { if (value == null || value.isBlank()) return defaultValue; for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); if (i == 0 && c == '-') continue; if (!Character.isDigit(c)) return defaultValue; } return Integer.parseInt(value); } Is probably worse than Integer.parseInt alone, since it can still throw NumberFormatExceptions for values that overflow (which is no longer handled!). Would maybe fix that. Unfortunately this is a major flaw in the Java standard library; parsing numbers shouldn't throw expensive exceptions.
- deepsun 7mo agoAnd it will fail with "-"
- j-vogel 7mo agoGood catches from several of you. The original fix dropped the try-catch entirely which was a regression for overflow and edge cases like a bare "-". Updated the post to keep a try-catch around the final parseInt as a safety net. The pre-validation still avoids the expensive path for the common cases, which is the core point. Appreciate the feedback.
- zahlman 7mo agoNot to mention it's extra work in the case where the input usually is valid.
- spankalee 7mo agoAvoiding Java's string footguns is an interesting problem in programming languages design. The String.format() problem is most immediately a bad compiler and bad implementation, IMO. It's not difficult to special-case literal strings as the first argument, do parsing at compile time, and pass in a structured representation. The method could also do runtime caching. Even a very small LRU cache would fix a lot of common cases. At the very least they should let you make a formatter from a specific format string and reuse it, like you can with regexes, to explicitly opt into better performance. But ultimately the string templates proposal should come back and fix this at the language level. Better syntax and guaranteed compile-time construction of the template. The language should help the developer do the fast thing. String concatenation is a little trickier. In a JIT'ed language you have a lot of options for making a hierarchy of string implementations that optimize different usage patterns, and still be fast - and what you really want for concatenation is a RopeString, like JS VMs have, that simply references the other strings. The issue is that you don't want virtual calls for hot-path string method calls. Java chose a single final class so all calls are direct. But they should have been able to have a very small sealed class hierarchy where most methods are final and directly callable, and the virtual methods for accessing storage are devirtualized in optimized methods that only ever see one or two classes through a call site. To me, that's a small complexity cost to make common string patterns fast, instead of requiring StringBuilder.
- LtWorf 7mo agoSometimes running strace on jvm software you will see some sycall patterns that are incredibly inefficient.
- brabel 7mo agoYeah, Java is pretty fast despite the fact that it still has these kinds of obviously suboptimal things going on. I love how Zig, D and Rust do exactly what you say: parse the format string at compile time, making it super efficient at runtime (no parsing, no regex, just the optimal code to get the string you need). I say this but I write most of my code in Java/Kotlin :D . I just wish I could write more low-level languages for super efficient code, but for what I do, Java is more than enough.
- uraura 7mo agoI thought those were common sense until I worked on a program written by my colleague recently.
- Izkata 7mo ago"Java is slow" is a reputation it earned in the 90s/2000s because the JVM startup (at least on Windows) was extremely slow, like several seconds, with a Java-branded splash screen during that time. Even non-technical people made the association.
- marginalia_nu 7mo agoJava's start up speeds were greatly improved by the engineers at Oracle, but thankfully we invented springboot and guice to get around that problem.
- spwa4 7mo agoJava IS fast. The time between deciding to use Java and Oracle's lawyers breaking down your door is measured in just weeks these days.
- gf000 7mo agoJokes are only funny when they have an ounce of truth to them. Oracle was the one who open-sourced the whole of the JDK, and is the main contributor to OpenJDK by far, which is completely open-source with the same license as the Linux kernel. It's fine to criticize them on e.g. Oracle db licenses and stuff like that, but they have been excellent stewards of Java and all the bad language around this java lawyering stuff is just FUD.
- latchkey 7mo agoWhen they say that AI will replace programmers, I think of this article and come to terms with my own job security. Most of this stuff is just central knowledge of the language that you pick up over time. Certainly, AI can also pick this stuff up instantly, but will it always pick the most efficient path when generating code for you? Probably not, until we get benchmarks into the hot path of our test suite. That is something someone should work on.
- sgbeal 7mo agoSlight correction: > StringBuilder works off a single mutable character buffer. One allocation. It's one allocation to instantiate the builder and _any_ number of allocations after that (noting that it's optimized to reduce allocations, so it's not allocating on every append() unless they're huge).
- abitabovebytes 7mo ago[dead]
- seu 7mo agoI'm a bit surprised to see those examples, because there's nothing really new here. These are typical beginner pitfalls and have been there for at least a decade or more. Or maybe it's because I learned java in the late 90s and later used it for J2ME, and then using things like StringBuilder (StringBuffer in the old days) were almost mandatory, and you would be very careful trying to avoid unnecessary object allocations.
- larsnystrom 7mo agoI remember writing Java for our introductory programming course at university around 2010. I was already familiar with object oriented programming in PHP at the time, so I just wrote the Java code like I would write PHP. I was absolutely astounded at the poor performance of the Java app. I asked one of our tutors and I can still remember him looking at the code and saying something along the lines of ”oh, you’re instantiating objects in a loop, that’s obviously going to be slow”. Like, what? If I can do this performantly in freakin PHP, how can Java, the flagship of OOP, not have fast instantiation of objects? I’m still shaking my head thinking about it.
- zahlman 7mo agoDid you actually benchmark that task against similarly-architected PHP code?
- lern_too_spel 7mo agoYour tutor misdiagnosed the issue. These allocations in a tight loop would have used bump allocation on Java 6 in 2010, and the young generation would have used a copy collector, which would have freed those objects more cheaply than any unspecialized malloc/free. It would have beaten the pants off of PHP's reference counting GC.
- larsnystrom 6mo agoI remember now what the problem was. I was instantiating a new ArrayList in a loop. The solution to the performance issue was to use a Vector instead. I was used to just writing PHP arrays when I wanted a list of something, and since they’re dynamically sized I thought the analogue in Java was ArrayList, which is also dynamically sized. But somehow that was extremely unperformant in Java.
- layer8 7mo agoFor #5, the “fix” [0] is incomplete, because you will still get a NumberFormatException when the value is out of range. For int, you could check if there are more or less than 10 digits, and use parseLong() when there are exactly 10 digits. For long, you can use BigInteger when there are exactly 18 digits. After skipping any leading zeros, of course. Or you could just replicate the JDK’s parsing implementation and change the part where it throws NumberFormatException (at the possible cost of foregoing JIT intrinsics). A second bug is that Character.isDigit() returns true for non-ASCII Unicode digits as well, while Integer.parseInt() only supports ASCII digits. Another bug is that the code will fail on the input string "-". Lastly, using value.isBlank() is a pessimization over value.isEmpty() (or just checking value.length(), which is read anyway in the next line), given that the loop would break on the first blank character. It makes the function not be constant-time, along with the first point above that the length of the digit sequence isn’t being limited. [0] public int parseOrDefault(String value, int defaultValue) { if (value == null || value.isBlank()) return defaultValue; for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); if (i == 0 && c == '-') continue; if (!Character.isDigit(c)) return defaultValue; } return Integer.parseInt(value); }
- zahlman 7mo ago> Accidental O(n²) with Streams Inside Loops Man that code looks awful. Really reminds me of why I drifted away from Java over time. Not just the algorithm, of course; the repetitiveness, the hoops that you have to jump through in order to do pretty "stream processing"... and then it's not even an FP algorithm in the end, either way! Honestly the only time I can imagine the "process the whole [collection] per iteration" thing coming up is where either you really do need to compare (or at least really are intentionally comparing) each element to each other element, or else this exact problem of building a histogram. And for the latter I honestly haven't seen people fully fall into this trap very often. More commonly people will try to iterate over the possible buckets (here, hour values), sometimes with a first pass to figure out what those might be. That's still extra work, but at least it's O(kn) instead of O(n^2). You can do this sort of thing in an elegant, "functional" looking way if you sort the data first and then group it by the same key. That first pass is O(n lg n) if you use a classical sort; making a histogram like this in the first place is basically equivalent to radix sort, but it's nice to not have to write that yourself. I just want to show off what it can look like e.g. in Python: def local_hour(order): return datetime.datetime.fromtimestamp(order.timestamp).hour groups = itertools.groupby(sorted(orders, key=local_hour), key=local_hour) orders_by_hour = {hour: len(list(orders)) for (hour, orders) in groups} Anyway, overall I feel like these kinds of things are mostly done by people who don't need to have the problem explained, who have simply been lazy or careless and simply need to be made to look in the right place to see the problem. Cf. Dan Luu's anecdotes https://danluu.com/algorithms-interviews/ https://danluu.com/algorithms-interviews/ , and I can't seem to find it right now but the story about saving a company millions of dollars finding Java code that was IIRC resizing an array one element at a time. (Another edit: originally I missed that the code was only trying to count the number of orders in each hour, rather than collecting them. I fixed the code above, but the discussion makes less sense for the simplified problem. In Python we can do this with `collections.Counter`, but it wouldn't be unreasonable to tally things up in a pre-allocated `counts_by_hour = [0] * 24` either.) ---- Edit: > String.format() came in last in every category. It has to... StringBuilder was consistently the fastest. The fix: [code not using StringBuilder]... Use String.format() for the numeric formatting where you need it, and let the compiler optimize the rest. Or just use a StringBuilder if you need full control. Yeah, this is confused in a way that I find fairly typical of LLM output. The attitude towards `String.format` is just plain inconsistent. And there's no acknowledgment of how multiple `+`s in a line get optimized behind the scenes. And the "fix" still uses `String.format` to format the floating-point value, and there's no investigation of what that does to performance or whether it can be avoided.
- EricRiese 7mo agoThis is a Spring specific gripe and I know this blog post doesn't assume Spring, but I hate seeing `new ObjectMapper()`. Spring Boot auto configures an ObjectMapper for you and you probably want the customization it gives you, including `java.time` handling and classpath scanning. I've wrestled with so many bugs caused by not using the `ObjectMapper` bean.
- dionian 7mo agowell yeah Jackson is slow.
- computerdork 7mo agoHave always really liked Java, but yeah, Spring overall has been terrible for the language. Autowiring is against the principles of a typesafe programming language - Don't make me guess what what object is going to be attached to a reference. And if you do, at least figure out what linked object is at compile time, not at run time. Spring autowiring makes Java seem as a whole unnecessarily complex. Think it should be highly discouraged in the language (unless it is revamped and made apart of the compiler). ... not sure how this applies to the ObjectMapper, as I haven't programmed in Java in awhile. ... and my gripe doesn't apply to SpringBoot though:)
- hiddew 7mo ago> Autowiring is against the principles of a typesafe programming language Constructor autowiring is the application of the inversion of control and dependency injection pattern. If there was no autowiring, you could autowire the components together just the same with normal code calling constructors in the correct order. Spring just finds the components and does the construction for you.
- computerdork 6mo agoYeah, for me at least, personally believe inversion of control should be used more surgically instead of blanketing the system with it. On the one hand, freeing your application layer from direct dependencies with the lower-level objects conceptually seems like a good idea, but think in practice, this is hardly ever helpful especially when used for every dependency. At least from my experience, seems like we don't change the objects we use that often, that once a object is set on a reference var, a very, very large majority of them won't change. And because of this, seems like that for most objects dependencies, we should just new them directly, and if later on we do need to change them, then at that time, we can refactor the code to use more abstraction (like inversion of control) to break the direct dependency, but only for the code that needs it (or if there is a important situation where having a direct dependency could be highly problematic in the future, like to a DB provider). It's like the performance optimization problem. One guideline that is often quoted is that it's best not to over optimize the performance of your code, until you actually can test it in real-world test cases, because you very often optimize things that aren't the bottleneck. Same with the over usage of inversion of control. Spring makes it so we're using IOC everywhere, but it's just adding unnecessary complexity. Think that if inversion of control is used, should be used mainly at a higher level, for components instead of on every class like often happens. But even for components, think you should be careful when deciding to do so. ... and agreed, you could just use the factory pattern instead of Spring.
- pregnenolone 7mo agoJava really isn't fast unless you're basically writing C style Java. Abstracting is expensive in Java and if one can't abstract, what's the point of writing Java in 2026? After all these years value types aka Valhalla are still in a soon™ state. If it weren't for Project Loom, which is genuinely awesome, I wouldn’t see any justification for using Java in this day and age.
- zmmmmm 7mo agoString concatenation in a loop is a 1990's era Java footgun. It's interesting the things that have persisted vs been cast aside. Very significant design decisions have been enforced on far less grounds than the stupidity of how the default String concatenation operator works.
- inglor_cz 7mo agoThis. I learnt Java in 2004-5, when 1.4 was dominant and O'Reilly books were a much better training resource than whatever you found on the Internet. Concatenating of thousands of individual Strings was already considered a well-known performance killer back then. Interesting to see this in the wild in 2026.
- IsTom 7mo agoDoes Java not have some kind of helper function to join strings? Why'd anyone write loops like that?
- tombert 7mo agoI think that the `sychronized` keyword in Java was a mistake. I've seen classes that are meant to be used by multiple threads where literally every method has `synchronized` because "that was the only way they could get it to work". Of course, if literally every method is synchronized it literally can't actually be used by multiple threads, it just looks like it is. Generally speaking I work pretty hard to avoid any kind of locks. Locks can be an anti-pattern in my mind: for a lot of problems, if I am reaching for a lock, it's because I haven't actually thought through the problems well enough. They're a bandaid and they create potential choke-points in the app. I also think that they're a crappy fix to try and shoehorn non-concurrent patterns into a concurrent landscape. I personally think that making something thread-safe and concurrent while also being maintainable and fast is a hard problem, and I think lazily trying to add threads into concurrent applications is a good way to write terrible code that is impossible to debug. Obviously no accounting for taste, but when I write programs now, I kind of always make them concurrent-first (generally using and/or reinventing the actor model). I try and build my initial algorithm to accept that concurrency is inevitable and start that from the get go. I can't remember the last time I reached for `synchronized`, though every now and then I do have to reach for ReentrantLock, and I always feel dirty doing so.
- drtse4 7mo agoA huge mistake, I've seen a lot of code where clearly the author thought that just adding "synchronized" would have solved any concurrency issue. And no one even talks about how synchronized is implemented, basically a monitor on the object. The point of view is usually also wrong, they focus on the method call flow while they should think about protecting access to shared data.
- tombert 6mo agoYeah. The more I learn about concurrency (which I think is a fair amount at this point), the more I respect Erlang. It was so ahead of its time with this stuff. As I said, I feel like when I reach for a lock, about 95% of the time it’s because I don’t really understand the problem well enough.
- drtse4 7mo ago0. Fix your algorithms, code optimization comes later 1. Avoid abstraction as much as possible, convoluted flow control and reduce useless objects creation 2. Learn how to manage concurrency correctly, focus on the data being accessed by multiple thread and focus on sequential access 3. Don't use bloated frameworks (all of them) 4. Consider rewriting common libraries following the principles above and with only the functionalities you actually need. Easy 10x improvement, try it.
- coldtea 7mo ago>Same app. Same tests. Same JDK. Same AI slop.
- ndkap 6mo agoI feel like most of these things can be avoided if people just think what they are going to write in abstract terms before writing the code. If they do it like this, I think they are going to choose the best Data Structure for the job, which is what most of this article was.