Java SE 8 Programmer II study materials
Java SE 8 Programmer II study resources

Java SE 8 Programmer II common mistakes

Java SE 8 Programmer II · Understand the traps and how to avoid them.

Mistake 1

Assuming a lambda that assigns to a captured local variable or mutates a loop counter compiles, e.g. writing `int count = 0; list.forEach(x -> count++);` and expecting it to work.

Candidates learn that lambdas reduce boilerplate but skip the effectively-final rule: captured locals must be final or effectively final, and lambda parameters cannot shadow enclosing locals. The compiler error is reported at the lambda body, which is far from the declaration, so it is misread as a type error.

What to do instead

Before writing any lambda, scan the body for assignments to variables declared outside it. If a counter is needed, use an int[]/AtomicInteger accumulator, a mutable field, or better, use stream().filter(...).count() instead of a side-effecting lambda. Memorise that the error message points to the captured variable, not the functional interface.

Mistake 2

Writing `list.remove(i)` inside an enhanced for-loop over the same list, or calling `map.put` on a key while iterating `keySet()`, and expecting no runtime error.

The for-each loop hides an Iterator, and structural modification invalidates it. Candidates see no explicit iterator.next() call, so the connection between list.remove and ConcurrentModificationException is invisible.

What to do instead

Use the explicit Iterator and call iterator.remove(), or use Collection.removeIf(Predicate) / map.entrySet().removeIf(...). Note that removeIf is the Java 8 idiom and is safe. Practise recognising that ConcurrentModificationException is a fail-fast detection, not a threading guarantee.

Mistake 3

Declaring `List<Number> nums = new ArrayList<Integer>();` or `List<Object> objs = new ArrayList<String>();` and believing generics are covariant like arrays.

Arrays are covariant in Java (Object[] a = new String[1] compiles), so candidates transfer that intuition to generics. The compile error looks like a simple type mismatch and is often 'fixed' by adding raw types, which then produces unchecked warnings.

What to do instead

Remember the rule: generics are invariant. Use a wildcard at the use site - List<? extends Number> for reading, List<? super Integer> for writing - and remember the PECS mnemonic (Producer Extends, Consumer Super). Never silence the problem with a raw type.

Mistake 4

Calling a terminal operation on a stream twice, e.g. `Stream s = list.stream(); s.forEach(...); s.count();`, or reusing a stream returned from a method.

Candidates treat streams as collections. Because streams are lazy and consumed by the terminal operation, the second call throws IllegalStateException: stream has already been operated upon or closed, which is not obviously a stream-reuse problem.

What to do instead

Treat every stream as single-use: obtain a fresh stream from the source each time, or wrap it in a Supplier<Stream<T>> if reuse is genuinely required. Also distinguish intermediate operations (lazy, return a stream) from terminal operations (eager, consume the stream) before writing any pipeline.

Mistake 5

Assuming `Files.move` or `Files.copy` will overwrite an existing target, or that `Files.delete` returns false instead of throwing when the file is missing.

Candidates map these to shell commands (mv, cp, rm -f) whose default behaviour differs. In NIO.2, both move and copy throw FileAlreadyExistsException unless StandardCopyOption.REPLACE_EXISTING is supplied, and delete throws NoSuchFileException.

What to do instead

Memorise the option enums explicitly: REPLACE_EXISTING, COPY_ATTRIBUTES, ATOMIC_MOVE for copy/move; Files.deleteIfExists when silent behaviour is wanted. Also remember Files.move across file stores may degrade to copy-and-delete and then cannot be atomic.

Mistake 6

Confusing `Path` resolution and normalisation: expecting `Paths.get("/a/b").resolve("/c")` to yield `/a/b/c`, or expecting `relativize` to work between a relative and an absolute path.

The rule that an absolute argument to resolve discards the receiver is counter-intuitive, and candidates conflate resolve (combine) with relativize (compute the gap). Both silently produce surprising but legal results rather than exceptions.

What to do instead

Drill the four cases: absolute argument to resolve returns the argument; relative argument appends; relativize requires both paths to be both absolute or both relative, otherwise IllegalArgumentException; normalize only removes . and .. textually and does not touch the file system. Trace each expression on paper before answering.

Mistake 7

Assuming `LocalDateTime` carries a zone or that `Instant` can be formatted directly with a `DateTimeFormatter` pattern such as `yyyy-MM-dd`.

Candidates mix the three type families. LocalDateTime has no offset, so atZone is required to get a ZonedDateTime; Instant is a point on the timeline without calendar fields, so formatting it directly throws UnsupportedTemporalTypeException.

What to do instead

Classify every type first: LocalDate/LocalTime/LocalDateTime are zone-less; Instant is machine time; ZonedDateTime/OffsetDateTime carry rules. Convert with atZone, toInstant, atOffset. For Instant, format via DateTimeFormatter.ofPattern(...).withZone(ZoneId.of("UTC")).

Mistake 8

Writing a try-with-resources whose resource does not implement `AutoCloseable`, or expecting a `close()` exception to replace the body exception.

Candidates remember try-with-resources as syntax but forget the type constraint and the suppression rule. The body exception is primary; close exceptions are attached as suppressed, which is the reverse of intuition.

What to do instead

Check the resource type declares AutoCloseable (or Closeable); a plain object will not compile. When asked about exceptions, state that the body exception propagates and getSuppressed() holds the close failures. Also remember resources are closed in reverse declaration order.

Mistake 9

Designing a multi-threaded class with `synchronized` methods but leaving a mutable field public, or assuming `volatile` makes a compound operation such as `count++` atomic.

Candidates equate 'some synchronisation' with thread safety and conflate visibility with atomicity. volatile guarantees visibility and ordering but not atomicity, so read-modify-write still races.

What to do instead

Apply the rule: all access to mutable shared state must use the same lock, and fields should be private. Use AtomicInteger/LongAdder for counters, ConcurrentHashMap for maps, and java.util.concurrent collections rather than wrapping with Collections.synchronizedMap and then iterating without external synchronisation.

Mistake 10

Using `Statement` with string concatenation for user input, or calling `executeQuery` for a DML statement such as `UPDATE`, and then wondering why nothing changed or why a syntax error appears.

Candidates learn JDBC as a sequence of calls and do not distinguish the two execution paths. executeQuery expects a ResultSet, so DML either throws or silently does nothing; concatenation invites quoting bugs and SQL injection.

What to do instead

Use PreparedStatement with ? placeholders and setXxx for every value. Choose the method by statement type: executeQuery for SELECT, executeUpdate for INSERT/UPDATE/DELETE (returns a row count), execute when the type is unknown. Close resources in try-with-resources in the order ResultSet, Statement, Connection.