Alexander Obregon's Substack

Share this post

User's avatar
Alexander Obregon's Substack
Some Java Features That Come With Hidden Costs
Copy link
Facebook
Email
Notes
More
Java and JVM

Some Java Features That Come With Hidden Costs

Mostly stuff I found out by accident while researching

Alexander Obregon's avatar
Alexander Obregon
Apr 23, 2025
∙ Paid

Share this post

User's avatar
Alexander Obregon's Substack
Some Java Features That Come With Hidden Costs
Copy link
Facebook
Email
Notes
More
Share

Over the last few years of writing code, and writing about it almost every day, I’ve come across a handful of Java features that looked simple at first but ended up being more expensive than I expected. Not always in a bad way, but enough that I started paying closer attention.

Here are a few that stood out to me. They’ve been interesting to look into, and maybe they’ll save you some trouble or just give you a better feel for what’s actually happening. There are also two extras at the end for paid subscribers.

String Concatenation and the Compiler’s Rewrite

When you write something like "a" + b + "c", the compiler rewrites it differently depending on the Java version. After Java 9, it uses an invokedynamic call that lets the JVM decide how to build the string at runtime. That might involve a StringBuilder, a byte buffer recipe, or something else. Before that, it just expands directly into a new StringBuilder() sequence.

Simple code like that looks harmless, and it usually is. But once it runs inside a large loop, the overhead becomes more noticeable. I started seeing the impact when logging or building up messages thousands of times in a row. Even though the compiler optimizes string building, each step still creates a new string. Those small costs start to add up. These days, if I know that part of the code runs frequently, I try to keep things a bit leaner.

Autoboxing in Loops

This one took me a while to catch. When you use types like int with something like List<Integer>, Java will automatically box and unbox the values for you.

Each of those int values gets wrapped into an Integer object. If you do this kind of thing a lot, it can lead to more memory use and slower performance than you’d expect. These objects still need to be collected by the garbage collector later. In performance-sensitive code, I try to use primitive arrays or libraries that avoid boxing altogether.

Using Exceptions to Guide Flow

I’ve written code before that relied on exceptions to fall back to something else. It works, but it’s a lot heavier than it seems.

The JVM creates a full stack trace when you throw an exception. That means walking the call stack, grabbing method names and line numbers, and putting together the full picture of how your program got there. It’s not cheap.

If something really is exceptional, it’s fine. But if I’m using exceptions to handle something that’s part of the normal flow, I try to refactor it out.

Enum Switches and Compiler Tricks

Switching over enums looks clean:

Behind the scenes, javac creates an extra helper class (usually something like YourClass$1) that holds a static int[] mapping each enum constant’s ordinal() to the switch case labels. This helper class sits next to your code, not inside the enum. If you add, remove, or reorder enum constants and only recompile part of the codebase, that array can get out of sync. It won’t break anything as long as all the classes that use the enum get recompiled together. Still, it was eye-opening to see how much extra bytecode the compiler adds to make a switch look that simple.

finally Blocks in Multi-Exit Methods

If you use a finally block in a method that has more than one return point, the compiler actually duplicates the finally code in all of those places.

When a method has multiple return points and a finally block, the compiler copies the cleanup logic into each exit path. The JIT usually folds these copies together at run time, so there’s a slight cost in the form of a larger .class file, which you might not notice unless you look at the bytecode, even if its super small.

Lambdas and Stream Pipelines

I like writing code with stream() and lambdas, but I’ve also seen how they can pile up layers you don’t always think about.

Each stream stage adds a lightweight pipeline object. No collections are built until a terminal operation. On large datasets, the extra allocations can show up in a profiler, so in hot paths I sometimes fall back to a plain loop to make things easier.

Conclusion

Java sure has its quirks. Most of the time, things work how you’d expect, but once I started going through the documentation while researching for articles, I began noticing little tradeoffs that don’t always show up in surface-level examples. These are just a few I run into the most while writing. It’s stuff I wouldn’t have thought twice about early on, but now it’s hard not to notice.

If you liked this post or want to see more like it, feel free to subscribe. I’m always writing about the stuff most tutorials skip over. I’ve got more on the way.

If you’re a paid subscriber, there are two extra quirks below. They’re a little more specific, and they took me a bit to figure out the first time I ran into them.

Keep reading with a 7-day free trial

Subscribe to Alexander Obregon's Substack to keep reading this post and get 7 days of free access to the full post archives.

Already a paid subscriber? Sign in
© 2025 Alexander Obregon
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share

Copy link
Facebook
Email
Notes
More