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(...).