Skip to Content
Course content

160: Writing Clean, Idiomatic Java

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

By this point in the course, you know how to make Java work. You can write loops, handle exceptions, and build class hierarchies. But there is a massive difference between code that compiles and code that is idiomatic. When I review PRs from junior devs, I often see "C-style Java"—code that works perfectly fine but feels clunky and noisy to anyone who has spent years in the ecosystem. Writing idiomatic Java is about reducing the cognitive load for the next person reading your code.

The Noise of Imperative Filtering

Let's look at a common scenario: you have a list of Order objects, and you need to extract the email addresses of customers who spent more than $100, but only if the order has actually been shipped. The naive approach is to do exactly what you'd do in a procedural language: create a temporary list, loop through the data, and use nested if-statements.

public List<String> getHighValueCustomerEmails(List<Order> orders) {
    List<String> emails = new ArrayList<>();
    for (Order order : orders) {
        if (order != null) {
            if (order.getTotal() > 100.0) {
                if ("SHIPPED".equals(order.getStatus())) {
                    String email = order.getCustomerEmail();
                    if (email != null) {
                        emails.add(email);
                    }
                }
            }
        }
    }
    return emails;
}

This code is "safe," but it's exhausting to read. You're forcing the reader to track the state of the emails list and navigate a "pyramid of doom" created by those nested if-statements. I call this "how" code—you are telling the computer how to move the data, rather than what data you actually want.

Declarative Flow with Streams

Idiomatic Java leverages the Stream API to turn that imperative mess into a pipeline. Instead of managing a temporary list and manually iterating, you describe the transformation of the data. Here is how I would write the same logic:

public List<String> getHighValueCustomerEmails(List<Order> orders) {
    return orders.stream()
        .filter(Objects::nonNull)
        .filter(order -> order.getTotal() > 100.0)
        .filter(order -> "SHIPPED".equals(order.getStatus()))
        .map(Order::getCustomerEmail)
        .filter(Objects::nonNull)
        .toList();
}

Notice the shift. We've moved from "loop and check" to a series of filters and a map. It reads like a sentence: "Take the orders, remove nulls, keep the expensive ones, keep the shipped ones, grab the emails, remove null emails, and put them in a list." It's cleaner, it's more concise, and it's much harder to introduce a bug—like accidentally modifying the original list—because the stream approach treats the source data as immutable.

When "Clean" Becomes "Too Clever"

Now, a word of caution. I've seen developers swing too far in the other direction, trying to cram an entire business process into a single 50-line stream chain. This is where "idiomatic" becomes "obfuscated." If you find yourself nesting streams inside streams or using complex collectors that require a PhD to decipher, you've gone too far.

The trade-off here is debuggability. You can't put a breakpoint inside a stream pipeline as easily as you can inside a for-loop. If the logic is truly complex—say, involving multiple database calls or intricate conditional branching—stick to a well-structured loop or break the stream into smaller, named methods. The goal is clarity, not showing off how many method references you can fit on one line.

Handling Absence Without Nulls

Finally, let's talk about the billion-dollar mistake: null. The naive way to handle a missing value is to return null and hope the caller remembers to check for it. The idiomatic Java way is to use Optional<T>. This forces the caller to acknowledge that the value might not be there, moving the error from a runtime NullPointerException to a compile-time design decision.

Instead of public Customer findCustomer(int id) returning null, use public Optional<Customer> findCustomer(int id). Now, the person using your method can't just blindly call customer.getName(); they have to use .ifPresent(), .orElse(), or .map(). It makes the contract of your API explicit.




📋 Practical Task

Refactoring the Legacy Inventory Reporter

You have been handed a legacy class used for generating inventory reports. The current implementation is written in a very imperative, "noisy" style that is difficult to maintain. Your task is to refactor the generateLowStockAlerts method to be idiomatic Java.

Requirements:

  • Replace the manual for loop and nested if statements with a Stream pipeline.
  • Use Objects::nonNull for null checks.
  • Use method references (e.g., Product::getName) where applicable.
  • Ensure the final result is returned as an unmodifiable list (use .toList()).
public class InventoryReporter {
    public record Product(String name, Integer quantity, Boolean discontinued) {}

    public List<String> generateLowStockAlerts(List<Product> products) {
        List<String> alerts = new ArrayList<>();
        for (Product p : products) {
            if (p != null) {
                if (!p.discontinued()) {
                    if (p.quantity() != null && p.quantity() < 10) {
                        alerts.add("Low stock: " + p.name());
                    }
                }
            }
        }
        return alerts;
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.