Skip to Content
Course content

161: Effective Java Best Practices

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Think of learning Java syntax like learning how to use a hammer and a saw. After a few lessons, you can definitely build a table. It might even hold a lamp. But if you don't understand the "craft" of carpentry—like how wood expands with humidity or why you should use a dovetail joint instead of just nailing two boards together—that table is eventually going to wobble, creak, or collapse entirely. Effective Java is the difference between "the code works on my machine" and "this code can be maintained for five years without causing a midnight emergency call."

Here is how that carpentry mindset maps directly to your code:

  • Hammering a nail is like writing a loop that works. It gets the job done, but it's the most basic way to solve the problem.
  • Using a jig or a template is like using a proven Design Pattern. You stop guessing and start using a structure that is guaranteed to be square and stable.
  • Understanding the grain of the wood is like understanding how the JVM actually handles memory and objects. If you fight the grain, the wood splits; if you fight the JVM, you get OutOfMemoryError.
  • Building for longevity is like choosing immutability. A piece of furniture that doesn't shift or warp is a piece of furniture that lasts.

Stop Letting Your Objects Change Their Minds

One of the biggest mistakes I see developers make—and I did this for years—is making everything mutable. We think, "I'll just add a setter method here in case I need to change the value later." But in a complex system, that's like leaving every door in your house unlocked. Anyone can walk in and change the state of your object, and suddenly you're debugging a NullPointerException because some random service changed a field it wasn't supposed to touch.

The "Effective" way is to favor immutability. If an object represents a transaction or a user profile, it shouldn't change after it's created. Use final fields, or better yet, use records if you're on a modern version of Java. When you need to "change" something, don't modify the existing object; return a new instance with the updated value.

// The "Wobbly Table" way: Mutable and risky
public class Payment {
    private double amount; // Can be changed by anyone with a setter
    public void setAmount(double amount) { this.amount = amount; }
    public double getAmount() { return amount; }
}

// The "Master Carpenter" way: Immutable and rock solid
public record Payment(double amount, String currency, LocalDateTime timestamp) {
    // No setters. No risk. No surprises.
}

Stop Forcing Family Trees Where They Don't Fit

You've learned about inheritance, but here is a professional secret: we use it way less than the textbooks suggest. Inheritance creates a "fragile base class" problem. If you change one method in a parent class to fix a bug, you might accidentally break ten different child classes that were relying on that specific (and broken) behavior.

I always tell my mentees to prefer composition over inheritance. Instead of saying "A SmartPhone is a Camera," think "A SmartPhone has a Camera." By plugging a camera object into your phone object, you can swap the camera out or update it without risking the entire phone's stability.

Saying 'Maybe' Without Crashing the Program

We've all seen it: return null;. It's the most dangerous phrase in the Java language. It forces the person calling your method to remember to write an if (result != null) check, and the moment they forget, the app crashes.

Stop returning null to represent "nothing found." Use Optional<T>. It forces the caller to acknowledge that the value might be missing. It turns a potential runtime crash into a compile-time conversation.

// Avoid this: The caller has to guess if this returns null
public User findUserById(String id) {
    return database.lookup(id); 
}

// Do this: The signature tells the caller "You might not get a user"
public Optional<User> findUserById(String id) {
    return Optional.ofNullable(database.lookup(id));
}

// Now the caller is forced to handle the empty case gracefully:
findUserById("123").ifPresentOrElse(
    user -> System.out.println("Found: " + user.getName()),
    () -> System.out.println("User not found!")
);



📋 Practical Task

Refactoring the Brittle Order-Processing System

You have been handed a legacy Order class that is causing constant bugs in production. The current implementation uses public setters, returns null when an order item is missing, and uses a deep inheritance hierarchy that makes it impossible to modify without breaking other parts of the system.

Your Task: Refactor the following code to apply Effective Java best practices.

// LEGACY CODE TO FIX
class BaseEntity {
    public String id;
    public void setId(String id) { this.id = id; }
}

class Order extends BaseEntity {
    public double totalAmount;
    public String customerEmail;

    public void setTotalAmount(double totalAmount) { this.totalAmount = totalAmount; }
    public void setCustomerEmail(String email) { this.customerEmail = email; }

    public String getShippingAddress() {
        // Sometimes returns null if the user didn't provide one
        return database.getAddress(this.id); 
    }
}

Requirements for your refactor:

  1. Replace the Order class and its inheritance from BaseEntity with a record or a class using final fields to ensure immutability.
  2. Remove all setter methods.
  3. Change the getShippingAddress method to return an Optional<String> instead of a nullable String.
  4. Ensure the id is passed through the constructor rather than set via a method.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.