Skip to content

Common Java Pitfalls

Status: 🟢 Active  |  Owner: Java Guild

NullPointerException

Use Optional as a return type for values that may be absent. Never return null from a public method.

// āŒ Caller has no indication the result may be absent
public Order findById(String id) {
    return repository.find(id);  // may return null
}

// āœ… Explicit absence signalled by return type
public Optional<Order> findById(String id) {
    return Optional.ofNullable(repository.find(id));
}

// āŒ Never chain without a null check
order.getCustomer().getAddress().getCity()

// āœ… Safe navigation
String city = Optional.ofNullable(order)
    .map(Order::getCustomer)
    .map(Customer::getAddress)
    .map(Address::getCity)
    .orElse("Unknown");

Catching Exception or Throwable

// āŒ Catches OutOfMemoryError, interrupted signals, and everything else
try {
    processOrder(order);
} catch (Exception e) {
    log.error("Error");  // And swallowed — no rethrow!
}

// āœ… Catch the specific type you can handle
try {
    processOrder(order);
} catch (OrderValidationException e) {
    log.warn("Invalid order: {}", e.getMessage());
    throw new BadRequestException(e.getMessage(), e);
}

Mutable Statics and Shared State

Avoid static mutable fields. They are hard to test, cause concurrency problems, and create hidden coupling.

// āŒ Shared mutable state
private static List<Order> pendingOrders = new ArrayList<>();

// āœ… Inject state through the constructor
public class OrderProcessor {
    private final OrderRepository repository;

    public OrderProcessor(OrderRepository repository) {
        this.repository = repository;
    }
}

equals() and hashCode() Contracts

When you override equals(), you must override hashCode() to maintain the contract. Use Java record or IDE generation to avoid manual mistakes.

// āœ… Use record for value objects — equals/hashCode/toString are generated
record OrderId(String value) {}

// If you must use a class — always override both
@Override
public boolean equals(Object o) { ... }

@Override
public int hashCode() { return Objects.hash(id); }

String Concatenation in Loops

String concatenation with + in a loop creates O(n²) object allocation.

// āŒ Creates a new String on each iteration
String result = "";
for (String part : parts) {
    result += part;
}

// āœ… Use StringBuilder
StringBuilder sb = new StringBuilder();
for (String part : parts) {
    sb.append(part);
}
String result = sb.toString();

// āœ… Or use String.join / Collectors.joining for simple cases
String result = String.join(", ", parts);

Modifying Collections During Iteration

// āŒ Throws ConcurrentModificationException
for (Order order : orders) {
    if (order.isExpired()) {
        orders.remove(order);
    }
}

// āœ… Use removeIf
orders.removeIf(Order::isExpired);

// āœ… Or collect to a new list
List<Order> active = orders.stream()
    .filter(Predicate.not(Order::isExpired))
    .toList();

References


Last reviewed: 2025-Q4  |  Owner: Java Guild