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