Gotcha roundup III — APIs
Streams are single-use and lazy, Optional.get misuse, try-with-resources close order, java.time immutability, ResultSet 1-indexing, ConcurrentModificationException.
API traps you will encounter in code and on the exam
Streams are single-use and lazy
Stream<String> stream = List.of("a", "b", "c").stream();
stream.forEach(System.out::println); // works
stream.forEach(System.out::println); // IllegalStateException — already consumed
A Stream pipeline does no work until a terminal operation (forEach, collect, count, findFirst, …) is called. Intermediate operations (filter, map, sorted) are lazy. Calling a terminal operation twice on the same stream throws IllegalStateException. Re-use the source collection, not the stream.
Optional.get() misuse
Optional<String> opt = findUser(id);
String name = opt.get(); // NoSuchElementException if empty!
// Safer alternatives:
String name = opt.orElse("anonymous");
opt.ifPresent(u -> System.out.println(u));
Optional.get() throws NoSuchElementException if the Optional is empty. Use orElse, orElseGet, orElseThrow, or ifPresent — they express intent more clearly and cannot silently throw.
try-with-resources close order
Resources are closed in reverse declaration order — last declared, first closed:
try (var a = openA(); var b = openB()) {
// use a and b
} // b.close() called first, then a.close()
If both close() and the body throw, the close exception is suppressed and the body exception propagates. Retrieve suppressed exceptions with Throwable.getSuppressed().
java.time objects are immutable
LocalDate date = LocalDate.of(2024, 1, 1);
date.plusDays(30); // silent no-op — returns a new LocalDate, discarded
date = date.plusDays(30); // correct — reassign the variable
Every method on LocalDate, LocalTime, LocalDateTime, ZonedDateTime, and Duration returns a new object; the original is unchanged. Forgetting to use the return value is a silent bug and a favourite OCP trap.
ResultSet is 1-indexed
JDBC ResultSet.getXxx(int columnIndex) counts from 1, not 0:
String name = rs.getString(1); // first column — not 0
int age = rs.getInt(2);
Using index 0 throws SQLException. The named overload rs.getString("name") is less fragile and is preferred in production.
ConcurrentModificationException
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) list.remove(s); // ConcurrentModificationException
}
Modifying a collection while iterating with a for-each loop throws ConcurrentModificationException. Safe alternatives:
iterator.remove()— the only safe remove during iteration.list.removeIf(s -> s.equals("b"))— the idiomatic one-liner.- Collect matches into a separate list, then call
list.removeAll(...).