Memra

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

NORMAL ~/memra/learn/java-from-zero/gotchas-apis utf-8 LF