Skip to main content

Java's Coding Tips and Tricks

· 14 min read
Linh Nguyen
T-90MS Main Battle Tank
thumbnail

Here are some non-exhaustive battle-tested tips and tricks for coding with Java (yes, yes, I hear you muttering about Kotlin, Go, or C#, but if Java is currently keeping the lights on and ramen in your bowl, then buckle up and keep reading).

Prefer Returning java.util.Optional Over null

AKA: Stop Playing Russian Roulette with null pointers.

The full deep-dive article is here, where I geek out about java.util.Optional like it's the best thing since sliced bread. But here's the TL;DR version with a side of common sense:

Do this and sleep better at night:

import java.util.Optional;

public Optional<User> getUser(UUID id) {
// Find active user with "true"
// We're wrapping it like a responsible adult
return Optional.ofNullable(fetchUser(id, true));
}

Why? Because when you return null, your only option is the boring old null check (yawn), and you are tempted to forget this (just like how C or C++ developers forget to free the memory). But with Optional, you get access to a whole arsenal of awesome methods like map, filter, stream, flatMap, and more! It's like upgrading from a rusty bicycle to a Tesla.

Pro Tip

Optional is for return values only! Don't go crazy and start using it for fields or method parameters. That's like wearing a tuxedo to do yard work (and because you can pass a null in place of the Optional object, yikes).

Want the full story? Check the article linked above, where I ramble extensively about this beautiful piece of Java engineering!

Prefer Returning Empty Collections or Empty Arrays Over null

Still sailing on the HMS Anti-Null, I see!

Joshua Bloch already roasted this topic to perfection in "Effective Java" (item 43), so I'll just give you the highlight reel:

Be the hero your codebase deserves:

public List<User> getUsers() {
return isAuthorized ? fetchUsers() : Collections.emptyList();
}

Here's your emergency cheat sheet (screenshot this, print it, tattoo it on your arm):

  • For Lists: Use List.of() (Java 9+) or Collections.emptyList(). Both are fine, pick your poison.

  • For Sets: Go with Set.of() (Java 9+) or Collections.emptySet(). Your call, captain.

  • For Maps: Choose from Map.of(), Map.ofEntries() (Java 9+ gang) or the classic Collections.emptyMap().

Trust me on this one: your future self will send you a thank-you card. Your teammates will stop giving you the stink eye. Your team lead might even crack a smile. And most importantly, you won't be debugging a nasty NullPointerException at 2 AM while surviving on energy drinks and regret.

Exception Handling: Don't Let Your Errors Vanish Into the Void

One of the cardinal sins in Java development is the dreaded "exception swallowing": catching an exception and then doing absolutely nothing with it. This practice is tantamount to committing programming heresy, and here's why you should never, ever do it.

The Anti-Pattern: Swallowing Exceptions

// DON'T DO THIS - Exception swallowing
try {
riskyOperation();
} catch (Exception e) {
// Silence is NOT golden here
}

When you swallow exceptions like this, you're essentially creating a black hole where errors disappear without a trace. Your application continues running, potentially in an inconsistent state, and you have no idea what went wrong when things inevitably break later.

Instead, always handle exceptions properly by either logging them appropriately or rethrowing them:

// For exceptional cases - use WARN or ERROR level
try {
connectToDatabase();
} catch (SQLException e) {
logger.warn("Database connection failed, retrying with backup", e);
// Handle the fallback logic
}

// For expected cases - INFO level is sufficient
try {
parseOptionalConfig();
} catch (ConfigNotFoundException e) {
logger.info("Optional config file not found, using defaults", e);
// Continue with default configuration
}

Why This Matters

Preserving the original exception in your rethrow statement maintains the complete stack trace, showing you exactly where the problem originated. Without it, you'll spend countless hours debugging issues that could have been immediately obvious with proper exception chaining.

Remember: exceptions are your friends trying to tell you something went wrong. Don't silence them! Listen to what they have to say!

Beware of Method Calls That Introduce Non-Idempotent Values

The Good, The Bad, and The Randomly Different

The Good: Normal getters are your best friends. They're reliable, predictable, and won't surprise you at 3 AM when you're debugging production issues. Call user.getEmail() as many times as you want. Not like it's going to suddenly decide to return a different email address just to mess with you.

The Bad (and Sneaky): Methods like LocalDateTime.now(), Random.nextInt(), System.currentTimeMillis(), and their mischievous cousins. These little rascals return something different every time you invoke them. It's like asking "What time is it?" and getting a different answer each nanosecond: technically, is correct, but can turn your code into a house of cards.

The Million-Dollar Question

So here's the thing: Do you explicitly want different values each time?

If you're building a timestamp logger or generating random passwords, then yes, embrace the chaos!

But if you're doing something like this:

// Don't do this!!!
// You're asking for trouble!!!
if (someCondition(LocalDateTime.now()) && anotherCondition(LocalDateTime.now())) {
processEvent(LocalDateTime.now());
}

Congratulations! You've just created a temporal paradox where three different timestamps might be involved in what should be a single moment in time.

The Solution: Introduce a Variable (Your IDE Is Smarter Than You Think)

Instead, let your IDE be your wingman. In IntelliJ IDEA, the "Introduce Local Variable" refactoring (usually Ctrl + Alt + V or Cmd + Alt + V) is basically your free ticket:

// Much better now that everyone's on the same page
LocalDateTime now = LocalDateTime.now();

if (someCondition(now) && anotherCondition(now)) {
processEvent(now);
}

The Horror Production Story: JWT TTL Edition

Here's where things get really spicy. Imagine you're working with JWT tokens and their time-to-live (TTL) values. Every nanosecond matters in this game, and if you're not careful, you'll create a bug so subtle and elusive that it'll keep you awake at night, questioning your life choices and wondering why you chose this career in the first place:

// This is a recipe for disaster and sleepless nights
JwtBuilder builder = Jwts.builder()
.setIssuedAt(Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()))
.setExpiration(Date.from(LocalDateTime.now().plusHours(1).atZone(ZoneId.systemDefault()).toInstant()));

Those two LocalDateTime.now() calls might happen microseconds apart, and suddenly your JWT's TTL isn't exactly one hour. It might be 59 minutes, 59 seconds, and 999,999 microseconds. Close, but still wrong, and in the world of security tokens, "close" is often synonymous with "broken."

The fix? Introduce that variable and save your sanity:

LocalDateTime now = LocalDateTime.now();

JwtBuilder builder = Jwts.builder()
.setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()))
.setExpiration(Date.from(now.plusHours(1).atZone(ZoneId.systemDefault()).toInstant()));
Pro Tip: Even the Good Guys Benefit

Here's a bonus nugget: even those well-behaved, idempotent methods can benefit from the "introduce variable" treatment. Sure, calling user.getName() five times in a row won't break anything, but extracting it to a variable makes your code cleaner and more maintainable. Don't expect miracles in performance, because modern JVMs are goddamn smart and will optimize the hell out of your code anyway, but your fellow developers (including future you) will appreciate the clarity.

Final Takeaway (Yes, I am using A.I to generate this)

Remember:

  • in Java, consistency isn't just a virtue, for it's a survival skill. Your code should be predictable, not a source of existential dread. So the next time you see a method that might return different values, ask yourself: "Do I want chaos, or do I want to sleep peacefully tonight?"

  • Check the source codes to make sure if the repeatedly calling a method is safe or not. Choose wisely.

Favor Unambiguous Date Time Units

The full article is here, you can take a look, then return back.

The Bottom Line

Keep your backend simple and sane with unambiguous time units. Push all the timezone conversion headaches to the client side where they belong. Your frontend developers have been getting away with making things look pretty for too long. It's time they earned their paychecks by handling some actual logic for once.

One source of truth for time, minimal complexity, maximum sanity. Your future self will thank you when you're not debugging timezone-related bugs at 2 AM while questioning your life choices.

Bonus: Converting Back and Forth

Sometimes you'll need to convert between these types, and here's where things get interesting:

var ldtToInstant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();

Notice something? You need a timezone to provide the time context: either to convert from local date time to universal time, or vice versa. That's exactly why you can't completely ditch ZonedDateTime, even if you wanted to. Not everyone lives in the UTC +0 ideal land where time conversions are just academic exercises. The timezone becomes the bridge between your local reality and the universal truth. It's like having a translator who speaks both "what time my users think it is" and "what time the universe knows it actually is."

Comparing Objects with Null Safety

The tip was so long that I got a dedicated article here.

Remember the Lazy Evaluation, Too!

Let's take this simple null-coalescing example:

var object = object == null 
? getDefaultObject()
: object;

A simple but elegant solution when you want to assign a default value to a variable if the object is null. It's like a safety net for your variables, minus the circus.

Then, you get fancy and create a helper method to adhere to the DRY principle:

// Sweet, sweet juicy usage of generics
// that would make Joshua Bloch shed a single tear of pride
<T> T getOrDefault(T object, T defaultValue) {
return object == null
? defaultValue
: object;
}

Also, FYI:

DRY principle stands for Don't Repeat Yourself (ironically, I just repeated that)

And then, riding high on your DRY-fueled motivation, you refactor your entire code base like a caffeinated code warrior. You feel invincible. You feel productive. You feel... oh so proud of yourself.

Hold your horses, cowboy!

Before your brain drowns in dopamine, and you start updating your LinkedIn with "Refactoring Ninja" as a skill, let me burst your bubble with a question:

What do you think will happen in this code snippet?

var object = getOrDefault(object, expensiveComputation());

Plot twist: The expensiveComputation() will ALWAYS execute, regardless of whether our lovely object is null or not. Surprise!

The consequences? Could range from wasting precious CPU cycles (your laptop fan is already judging you) to the dreaded "oh no, I've accidentally launched the nuclear missile twice" scenario. That's programmer speak for "your non-idempotent operations fired twice and caused chaos that should've only happened once, but here we are, with two charges on the customer's credit card and an angry email in your inbox."

The fix? Use lazy evaluation. Or in layman's terms, use Supplier<T>. Think of it as saying "I'll tell you the answer... but only when you actually need it."

Specifically, don't delete your current helper method yet (we're collectors, not destroyers). We'll add an overloaded one:

// Are you ready to get your OCP certificate?
// This is the bliss of Generics Usage (chef's kiss)
<T> T getOrDefault(T object, Supplier<? extends T> defaultValueSupplier) {
return object == null
? defaultValueSupplier.get()
: object;
}

And then, modify the expensive method call like this:

var object = getOrDefault(object, () -> expensiveComputation());

You can now rest easy knowing that expensiveComputation() will only be called if your shiny object is null. Otherwise? It stays asleep. Lazy. Unbothered. Living its best life.

And to think that the humble ternary operator above could masterfully handle both eager and lazy evaluation. Truly a How do you do, fellow kids moment for the ages.

But wait, don't delete the original helper method! (Yes, I know I already said this, but some of you are trigger-happy with that delete key.) It's still useful. Let's talk about when to use which:

  • The eager evaluation version is perfect for already computed values (simple getters, defined constants, that sort of thing). It's like having fast food: already prepared, ready to go, no waiting.

  • The lazy evaluation one is suitable when the default value requires invoking some rather expensive computations. But here's the kicker: using lazy evaluation on already computed values is wasteful. You're creating a Supplier<T> wrapper just to contain a value that's already sitting right there. It's like gift-wrapping a gift that's already unwrapped. Inefficient and slightly ridiculous.

Choose your approach carefully! Know what you need to do, and know which method to use (both versions are lovingly supported by Apache Commons Lang 3 libraries, bless their hearts). Our null-coalescing task is just a simple one, but the same principle can also be applied to other stuff, for example: hit the database only if the cache does not contain our desired value.

When ..., Do For 1 or 2

So you're sitting there, proud of yourself, thinking "I'll use varargs to make my method super flexible!" And honestly? Good call. Varargs are neat. Your method can now accept any number of parameters, and you feel like a programming wizard.

public void processItems(String... items) {
for (String item : items) {
System.out.println("Processing: " + item);
}
}

But then reality hits: what if 90% of your calls only use ONE parameter? You know, like this:

processItems("singleItem");

You could remove the varargs and go back to a simple parameter, but wait... you don't want to throw away that flexibility you just added. Other parts of your code might be using multiple parameters. Classic dilemma.

Here's the trick: craft another overload with just one parameter.

public void processItems(String item) {
System.out.println("Processing: " + item);
}

public void processItems(String... items) {
for (String item : items) {
System.out.println("Processing: " + item);
}
}

"Why bother?" you ask. Well, performance, my friend. Even creating a varargs array with one or zero elements costs something. And if you're a strict performance acolyte who breaks out in hives at the word "overhead", this should matter to you.

Sure, the JVM, with all its magical optimizations and runtime wizardry, might save you. The JIT compiler could inline things, optimize away the array allocation, and make everything butterflies and rainbows. But here's the thing: help yourself first. Don't rely on something you may never fully understand, especially in production environments where your rebellious DevOps buddy decides to fine-tune the JVM settings in ways that make your local development environment look... quaint. Out of place. Like wearing a tuxedo to a beach party.

TL;DR: Fend for yourself. Don't just rely on JVM optimizations.

This becomes especially important if you're building something reusable. Maybe you have these cute little toy services pretending to work like microservices (we've all been there). When that one method gets called thousands of times per second, those tiny array allocations start adding up like pennies in a piggy bank, except way less fun.

So yeah, add that single-parameter overload. Your future self, staring at performance metrics at 2 AM, will thank you.

Defensive to the Rescue!

The detailed explanation of defensive copy technique is here.

The End?

Leave a comment below, and tell me some of the tips and tricks you've been using to great successes!