Oracle Java SE 21 common mistakes
Oracle Java SE 21 · Understand the traps and how to avoid them.
Builds a formatter with DateTimeFormatter.ofPattern("yyyy-mm-dd") and expects it to print the year, month and day, then is surprised by an output like 2024:07:06 or by a DateTimeParseException when parsing.
Pattern letters are case-sensitive and each letter maps to a single field: M is month-of-year, m is minute-of-hour, d is day-of-month, D is day-of-year, y is year-of-era, Y is week-based-year, h is clock-hour-of-am-pm and H is hour-of-day. Writing mm silently requests minutes, and parsing with a pattern that has no month field fails because the date cannot be resolved.
What to do instead
Memorise the case table and use uuuu-MM-dd for ISO dates (uuuu is proleptic-year, yyyy is year-of-era); when parsing, verify every field the target type needs is present in the pattern, and prefer DateTimeFormatter.ISO_LOCAL_DATE when the format is ISO-8601 so the pattern letters never enter the picture.
Writes a switch expression whose arrow case has a block body, e.g. case 1 -> { total + 1; }, or uses return inside a case arm, and expects the expression to evaluate to that value.
A switch expression must produce a value on every path and its arms are not statements. A block arm needs an explicit yield, and return inside an arm exits the enclosing method instead of yielding. Exhaustiveness is also enforced: an expression over an enum must list every constant or provide a default, otherwise it does not compile.
What to do instead
Use the expression form case 1 -> total + 1; and, when an arm must be a block, finish it with yield total + 1;. Confirm exhaustiveness for enum and sealed-type selectors by covering all constants or adding a default arm.
Overrides equals(Object) in a domain class but not hashCode(), then puts instances in a HashSet or uses them as HashMap keys and finds that contains()/get() returns false for an object that is equal to the one stored.
Object.hashCode is a contract partner of equals: equal objects must return equal hash codes. Hash-based collections locate the bucket from hashCode first, so two equal instances with identity hash codes land in different buckets and equals is never consulted.
What to do instead
Always override hashCode together with equals, deriving it from the same fields (for example with Objects.hash or Objects.equals), or make the type a record so the compiler generates a consistent pair.
Combines related exception types in a multi-catch clause, e.g. catch (IOException | FileNotFoundException e), or orders a supertype catch before a subtype catch, and expects the code to compile.
A multi-catch parameter is implicitly final and its alternatives must be disjoint; alternatives related by subclassing are rejected because the parameter type would be ambiguous. Likewise, a catch block for a subtype after a catch for its supertype is unreachable code, which is a compile-time error.
What to do instead
List only sibling types in a multi-catch (catch (IOException | SQLException e)) and order catch clauses from most specific to most general; only one of them can be present when one is a subtype of the other.
Creates a collection with List.of(...) or Arrays.asList(...) and then calls add(), remove() or sort() on it, expecting a normal mutable list.
List.of and its siblings return fully immutable collections and throw UnsupportedOperationException on any mutator. Arrays.asList returns a fixed-size view backed by the array: set() is allowed, but add() and remove() throw UnsupportedOperationException.
What to do instead
Copy into a mutable implementation first, e.g. new ArrayList<>(List.of(1, 2, 3)) or new ArrayList<>(Arrays.asList(array)), and remember that toArray() on a fixed-size view gives an independent array.
Stores a Stream in a local variable and consumes it twice, for example calling count() and then forEach() on the same reference, or looping over it again in a later statement.
A stream is a single-use pipeline: the terminal operation consumes it and any further use throws IllegalStateException ("stream has already been operated upon or closed"). Intermediate operations are also lazy, so nothing executes until the terminal operation runs.
What to do instead
Treat the stream as a one-shot value: rebuild it from the source each time, or capture the source in a Supplier<Stream> and call get() per pass; if results are needed twice, collect once into a List or Map and reuse the collection.
Packages classes into a jar and launches it with java -jar app.jar, but the program fails at startup with "no main manifest attribute".
With the -jar form the launcher ignores the classpath and reads the entry point only from the Main-Class attribute of META-INF/MANIFEST.MF, so a jar built without that attribute has no entry point regardless of how many classes with main methods it contains.
What to do instead
Set the attribute when packaging, e.g. jar --create --file app.jar --main-class com.acme.Main -C out ., or launch with java -cp app.jar com.acme.Main. Also check that the main method signature is exactly public static void main(String[] args).
Passes a Runnable to a new Thread but calls t.run() instead of t.start(), then expects the task body to execute on a separate thread.
run() is an ordinary method call, so the body executes synchronously on the calling thread and the new thread never begins execution; start() is what registers the thread with the scheduler and then invokes run() on the new thread. Calling start() a second time throws IllegalThreadStateException.
What to do instead
Call start() exactly once per Thread instance; if you want the calling thread to execute the task, call the Runnable directly, and for pools submit the task to an ExecutorService instead of creating a Thread.
Calls Files.delete(path) on a path that may not exist and expects it to return false or do nothing, as File.delete() does.
Files.delete is a void operation that throws NoSuchFileException when the entry is absent, and DirectoryNotEmptyException for a non-empty directory. The boolean-returning behaviour belongs to java.io.File, not to the NIO.2 Path API.
What to do instead
Use Files.deleteIfExists(path) when absence is acceptable, or catch NoSuchFileException explicitly; before deleting a directory tree, resolve the order (files before their parent directories) or use a recursive walk.
Binds PreparedStatement parameters starting at index 0 or reads ResultSet columns starting at index 0, expecting zero-based indexing.
JDBC uses 1-based indexes for both setXxx parameter positions and ResultSet getXxx column positions; index 0 is out of range and raises a SQLException at runtime, often only when the query executes, so the mistake surfaces late.
What to do instead
Bind parameters from 1 (setInt(1, id)) and read columns from 1, preferring the String-label overloads (rs.getString("name")) or ResultSetMetaData.getColumnCount to guard loop bounds.