Everything the adaptive question bank can teach and test in Java, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
Assigning or passing a variable copies its contents: the number itself for a primitive, the pointer for an object. A method that reassigns its parameter therefore changes nothing for the caller, but one that mutates the object it received changes it for everyone holding that pointer. Java passes everything by value; for objects, the value copied is the reference.
The widths: byte 8 bits, short and char 16, int 32, long 64, plus float and double as 32- and 64-bit IEEE 754. That is why int math gives the same answer on every JVM, unlike int in C. boolean is the exception: the JLS gives it just the values true and false and leaves its storage size to the JVM.
An int runs from -2147483648 to 2147483647, one more negative value than positive. Negating the minimum overflows back to itself, so Math.abs returns it unchanged. That makes Math.abs(hash) % n able to yield a negative index; Math.floorMod(hash, n) with a positive n cannot, and Math.absExact, added in Java 15, throws ArithmeticException instead.
Its range is 0 to 65535, enough for one Basic Multilingual Plane code point but nothing above U+FFFF, so most emoji and many historic scripts take a surrogate pair of two chars. Because char is also an unsigned number, 'A' + 1 is the int 66 rather than the letter B, and 'a' + 'b' + "c" prints 195c. Cast with (char) to get a character back.
if (count) and (boolean) x are both compile errors, and no cast exists in either direction. Write the conversion out: count != 0 gives a boolean, flag ? 1 : 0 gives an int. The payoff is that the C slip if (x = 5) cannot compile, since an int is never a valid condition. Only a boolean assignment such as if (done = true) gets through.
Static fields, instance fields and array slots are zeroed when the storage is created, so a read sees 0, false, the zero char or null rather than garbage. A local variable gets no default, and javac rejects any read it cannot prove follows an assignment. A field you forgot to set still bites later, as a NullPointerException far from the omission.
var list = new ArrayList<String>(); fixes the type as ArrayList<String> (not List), and assigning a String to it later is a type error. Since Java 10 it works for locals with an initializer, loop variables and try-with-resources, and since Java 11 for lambda parameters. var x = null;, var f = () -> 1; and var a = {1, 2}; fail: nothing to infer from.
final List<String> names = new ArrayList<>(); forbids names = other but allows names.add("x"), and a final array keeps every slot writable. For real immutability store an unmodifiable copy, such as List.copyOf(list). A final field does add one guarantee: other threads see its constructor-set value once construction ends, provided this did not escape.
Autoboxing calls Integer.valueOf, which must return the same object for -128 to 127, so two boxed 127s are == while two boxed 128s usually are not. The JVM may cache more: HotSpot's -XX:AutoBoxCacheMax=2000 makes boxed 1000s identical too, so the result can flip with a flag. Compare boxed values with equals, or unbox them first.
Putting null in an Integer is legal; the exception fires wherever that value is converted to int. That can be arithmetic, a comparison such as count > 0, a call to a method taking int, or an assignment to a primitive: int n = map.get(key) throws right there when the key is missing. A missing map key is a common source of that null.
B. Conversions, promotion and casting
The chain also admits char into int and wider, but short and char never widen into each other and byte to char needs a cast, since one side is signed and the other is not. Overload resolution tries widening before boxing or varargs, so with f(long) and f(Integer) both declared, f(1) calls f(long).
Between integer types, (byte) 200 keeps only the low eight bits, 0xC8, which read as signed are -56, and (int) 3_000_000_000L gives -1294967296. The sign can flip and no exception is thrown. If you would rather fail, Math.toIntExact throws ArithmeticException when a long does not fit. Casts from float or double follow other rules.
A float has 24 significant bits and a double 53, fewer than the 32 of int or the 64 of long. So (float) 16777217 reads back as 16777216 and 123456789 stored in a float becomes 123456792. The JLS rounds to the nearest representable value and never throws, and javac does not warn even under -Xlint:all.
The rule: if either operand is double, both become double; otherwise float; otherwise long; otherwise both become int. So 100000 * 100000 overflows to 1410065408 even when assigned to a long, because the multiply runs in int first. Write 100000L * 100000 so one operand forces long before the overflow can happen.
Since a + b on two bytes is computed as an int, storing it back needs (byte) (a + b), which wraps on overflow. Two cases hide the rule: a constant expression that fits, such as byte c = 1 + 2;, compiles without a cast, and so does b += a, because compound assignment casts for you. short and char behave the same way.
JLS 15.26.2 defines x op= y as x = (T) (x op y), with T the type of x, and that holds for every compound operator. So i *= 1.5 with i at 5 stores 7, dropping the fraction, and k += someLong can wrap. The longhand i = i * 1.5 is rejected as lossy. javac stays silent unless you pass -Xlint:lossy-conversions, a lint added in JDK 20.
Integer.MAX_VALUE + 1 evaluates to Integer.MIN_VALUE: the result keeps the low 32 bits of the true sum. The JLS says integer operators signal overflow in no way; division and remainder by zero are the arithmetic cases that throw. When a wrong total is worse than a crash, use Math.addExact, multiplyExact or toIntExact, which throw ArithmeticException.
NaN becomes 0, (int) -2.7 is -2, and out-of-range values pin to Integer.MIN_VALUE or MAX_VALUE (or the long bounds). Saturation applies only to int and long targets: (byte) 300.0 first becomes the int 300, then keeps its low eight bits, giving 44. For rounding rather than truncation, use Math.round or Math.floor instead of a cast.
Grouping runs left to right, so 1 + 2 + "a" is "3a" while "a" + 1 + 2 is "a12". Objects go through toString(), and both a null reference and a toString() that returns null become "null". One trap: a char[] prints as [C@ plus a hex hash, its identity, even though String.valueOf(chars) would give its contents.
C. Operators and expression evaluation
Precedence decides grouping, not timing: in a() + b() * c() the calls still run a, then b, then c. A compound assignment reads its left side before the right side runs, so with m at 5, m += (m = 3) leaves 8. Side effects inside one expression are fully defined in Java, unlike C, but code that leans on them is still hard to read.
Truncation toward zero rounds a negative quotient up, so -7 / 2 is -3, not the floor value -4; Math.floorDiv(-7, 2) gives -4. One case defies the sign rules: Integer.MIN_VALUE / -1 overflows and returns Integer.MIN_VALUE, and the JLS says no exception is thrown. Dividing by zero, by contrast, does throw.
Java keeps (a / b) * b + (a % b) == a with truncating division, which forces the remainder to share the dividend's sign, so 7 % -3 is 1. That breaks the odd test n % 2 == 1 for negative n, which yields -1. Use n % 2 != 0, or Math.floorMod(-7, 3), which returns 2. Floating % truncates the same way: -7.5 % 2 is -1.5.
Integer / and % by zero both throw ArithmeticException: / by zero. Floating division follows IEEE 754 instead: 1.0 / 0 is Infinity, -1.0 / 0 is -Infinity, and 0.0 / 0 or 5.0 % 0 is NaN, all without an exception. One double operand makes the whole operation floating, so 1 / 0.0 quietly returns Infinity where 1 / 0 throws.
Every <, > or == involving NaN is false and != is true, so a NaN slips past a check like if (x < 0) reject unnoticed. Double.isNaN(x) says the same thing more readably. Boxed values flip both rules: Double.equals treats NaN as equal to NaN and 0.0 as unequal to -0.0, the reverse of == on primitives.
Neither 0.1 nor 0.2 has an exact binary form, so in double the sum is 0.30000000000000004; in float it happens to round to 0.3f, which makes the bug look random. Compare with Math.abs(a - b) < eps. For money use BigDecimal built from a String, since new BigDecimal(0.1) preserves the binary error digit for digit.
The skip is what makes a guard like s != null && s.length() > 0 safe; with & the second test still runs and throws NullPointerException on a null s. Reach for the eager forms only when the right side has a side effect you need every time. On integers & and | are bitwise, so the same symbol does two jobs depending on operand types.
When one branch is int and the other Integer, the conditional as a whole has type int. So Integer r = flag ? cached : 0; unboxes cached and throws if it is null, even though the target is an Integer. Make both branches the same reference type, for example flag ? cached : Integer.valueOf(0), and the null passes through untouched.
i++ saves the old value, increments the variable, then yields the saved value, so in i = i++ the assignment writes the original back over the increment. ++i yields the new value. Mixing both in one expression is legal and defined (with w at 5, w++ + ++w is 5 + 7 = 12), but keep increments as separate statements so readers need not work it out.
1 << 32 is 1, not 0, because only the low 5 bits of the count are used (6 for long), and 1 << -1 shifts by 31. >> copies the sign bit while >>> fills with zeros, so -8 >> 1 is -4 but -8 >>> 1 is 2147483644. A byte is first promoted to a sign-extended int, so (byte) (b >>> 4) with b at -1 is still -1.
D. Statements, control flow and definite assignment
Each case label is only an entry point into one shared block, so execution runs on through later labels until a break, return, throw or the closing brace. Deliberate fall-through lets labels share code, but a forgotten break is silent; javac flags it only under -Xlint:fallthrough. Writing case 1, 2 -> shares code without that risk.
With ->, each arm runs alone. An arm that is a single expression supplies the switch expression's value directly; only a block arm, or a colon-style switch expression, needs yield. A return cannot leave a switch expression, and javac rejects the attempt. Arrow labels and switch expressions became standard in Java 14.
Without a default, adding a constant or subtype breaks compilation at every switch that misses it, which is the point. Constant labels alone can never cover every int or String, so such switch expressions need a default. Since Java 21 a switch statement must be exhaustive too once it uses a pattern or case null, though a plain enum switch statement may still skip constants.
Over an array the loop variable is a copy, so assigning to it leaves the array alone. Over a collection the loop hides an Iterator, and calling list.remove inside usually throws ConcurrentModificationException, though with ArrayList removing the second-to-last element just ends the loop and skips the last one. Use iterator.remove(), removeIf or an indexed loop.
break outer; leaves the statement labeled outer, and continue outer; starts that loop's next iteration. A label can sit on any statement, even a plain block, but continue needs a loop. When nesting gets deep, moving the loops into a method and using return often reads better than a label. Java reserves goto as a keyword and never uses it.
Each resource is closed before any catch or finally of the same statement runs, so the handler can no longer use it. Reverse order means a wrapper declared after the stream it wraps closes first. A resource whose initializer threw was never assigned and is not closed, and a null resource is skipped. Since Java 9 you may name an existing effectively final variable.
getSuppressed() on the caught exception returns the close failures, last-declared resource first, and a printed stack trace lists them under 'Suppressed:'. If the body succeeds and only close() fails, that close exception becomes the primary one. A hand-written finally { r.close(); } has the opposite flaw: its exception replaces the body's, which is lost.
A finally that completes abruptly wins: its return replaces the try block's result and erases any in-flight exception without a trace. javac warns only under -Xlint:finally. The reverse surprises people too: a finally that merely reassigns the variable being returned changes nothing, because the return value was already captured.
The analysis follows control flow but ignores the values of ordinary expressions, so after if (x > 0) a = 1; if (x <= 0) a = 2; javac still treats a as unassigned, though one branch always runs. Constant expressions are the exception: if (true) a = 1; counts. Assign on every path with if/else or a switch expression instead of a dummy initial value.
The deliberate exception is if (false) { ... }, which compiles so a constant flag can switch code off, while while (false) { ... } is rejected. A while (true) with no break makes everything after it unreachable, but any reachable break inside keeps the following code legal, even one that never fires at runtime. The rules read structure and constant expressions, never runtime values.
E. Classes, construction and initialization
Through Java 24 (without preview), the this(...) or super(...) call had to be the first statement. Java 25, via JEP 513, allows statements before it as long as they do not use the object under construction, so you can validate arguments first. With no explicit call javac inserts super(), which fails to compile if the superclass lacks a no-argument constructor.
Static initialization runs once, at the first instance creation, static method call, or use of a non-constant static field; reading a compile-time constant does not trigger it. On each new, the superclass constructor completes before this class's field initializers and instance blocks run, top to bottom. A constructor that begins with this(...) runs them only once.
The superclass constructor finishes before the subclass's field initializers run, so an override it calls sees 0, false or null, even in a final field whose initializer calls a method. Call only private, final or static helpers from a constructor. Since Java 25 (JEP 513) a subclass may assign its own fields before super(...), but only fields with no initializer.
A subclass field with the same name is a second, separate field, and every instance carries both. Which one obj.name reads depends only on the declared type of obj, so after A a = new B();, a.name gives A's value while a.describe() runs B's method. Hiding is legal but confusing; keep fields private and expose them through methods.
Static calls bind at compile time to the declared type, so A ref = new B(); ref.who() runs A.who(), and -Xlint:static warns that the call should name the class. Putting @Override on a static method is a compile error, and an instance method cannot replace a static one or the reverse. Call statics through the class name to keep the binding obvious.
Package-private, the default with no modifier, means visible within the same package and nowhere else, not even to subclasses elsewhere. protected adds subclass access, but from another package a subclass may touch an inherited instance member only through its own type, so reading prot on a variable typed as the superclass fails while this.prot compiles.
Creating an inner instance needs an enclosing object, as in outer.new Inner(). If the class uses that object, javac stores a hidden reference that keeps it alive as long as the inner one lives; compiling for release 18 or newer, javac omits an unused field unless the class is serializable. A listener held by a long-lived cache can leak a whole object graph this way, so make nested classes static unless they need the outer instance.
Inside the method the parameter is an ordinary array, and callers may pass one directly. Resolution runs in three phases (widening first, then boxing, then varargs), so f(1) prefers f(long) or f(Integer) to f(int...). Calling f() passes an empty array, while f((String[]) null) hands the method a null array instead of an array holding null.
javac picks the overload from the arguments' declared types, so Object o = "str"; print(o); calls print(Object) even when print(String) exists. With f(String) and f(Integer), f(null) is ambiguous because neither is more specific; with f(Object) and f(String) it quietly chooses String. Cast the argument to pick explicitly.
F. Interfaces, inheritance and polymorphism
An interface can declare constants and methods but no instance fields or constructors, so a class can implement many without inheriting conflicting state. An abstract class can hold fields, constructors and protected members, at the cost of using up the single superclass slot. Choose an interface for a capability, an abstract class to share implementation and state.
Added in Java 8, they let List gain sort and Iterable gain forEach without breaking existing classes. A default cannot override equals, hashCode or toString from Object, and a method inherited from a superclass always beats a default. Having no fields of its own, a default works by calling the interface's other methods.
javac reports the two interfaces as incompatible until your class overrides the method, and inside that override A.super.hello() calls one parent's version. There is no conflict when one interface extends the other, since the more specific default wins, or when a superclass supplies the method, since a class method always beats a default.
Since Java 8 you call one as Validator.of(...), naming the interface. Unlike a static method on a class, it is not reachable through an implementing class or an instance: both Impl.of() and an unqualified of() inside Impl fail with 'cannot find symbol'. That keeps factory helpers next to the type without leaking into every implementor.
A lambda can target any interface with a single abstract method, annotated or not. Default and static methods don't count, nor do abstract redeclarations of public Object methods, which is why Comparator qualifies despite declaring equals. The annotation pays off later: adding a second abstract method then fails at the interface rather than at every lambda.
For Animal a = new Dog(); a.speak(x), javac fixes the signature from Animal and the argument types, and the JVM then runs Dog's override of that signature. An overload is never re-chosen at runtime. Private and static methods cannot be overridden, and fields never dispatch, so all three resolve from the declared type alone.
Since Java 5, Object get() may be overridden by String get(), so callers holding the subclass type need no cast; javac adds a bridge method so callers of the wider signature still reach the override. Primitive returns must match exactly: an int method cannot override a long one. Change a parameter type and you get an overload instead.
The classic case is public boolean equals(Point p): it compiles, but HashSet and List.contains call equals(Object), which you never overrode, so lookups fail. Adding @Override turns that silent mistake into a compile error saying the method does not override anything, the main reason to put it on every override.
Callers holding the supertype handle only what its method declares, so an override throwing a broader checked exception would slip past their catch. Declaring a subclass such as FileNotFoundException for IOException, or nothing at all, is fine. Unchecked exceptions sit outside the rule: an override may add IllegalStateException freely.
G. The Object contract
Symmetry breaks easily: a wrapper that reports itself equal to a plain String cannot get the same answer back, because String.equals knows nothing of it. Take an Object parameter, return false for null or a foreign type, compare only identity-defining fields, and use Double.compare for double fields so NaN and -0.0 behave.
HashMap and HashSet locate entries by hash first and equals second, so equal objects with different hashes land in different buckets. Collisions between unequal objects are legal but slow lookups down; a constant hash piles everything into one bucket. The default hash may differ between runs of the same program, so never persist it.
The inherited hash is per instance, so a second object equal to the first almost always hashes to a different bucket. set.contains(sameInstance) still works, which hides the bug in quick tests, while contains(new Point(1, 2)) returns false and adding it creates a duplicate. Override both together, for example with Objects.hash, or use a record.
A HashMap files each entry under the hash computed at put. After a key field changes, a lookup with that same object searches the new bucket and misses, and a lookup with a key equal to the old value reaches the right bucket but fails equals. The entry still counts in size() and appears when iterating. Use immutable keys.
If a LabeledPoint subclass also compares its label, a Point using instanceof says it equals the LabeledPoint while the reverse says no. Comparing getClass() restores symmetry but means no subclass instance, even one adding nothing, can ever equal its parent. Making the class final, as every record is, removes the dilemma.
The text is getClass().getName() plus '@' plus Integer.toHexString(hashCode()), so a nested class prints with a $, like Outer$Node@1b6d3586, and a class that overrides hashCode changes the suffix. The Java 21 docs promise only that identity hashes are distinct where practical, so read the suffix as an opaque tag.
BigDecimal is the standard case: 1.0 and 1.00 compare as 0 but are not equals, since scale differs, so a HashSet keeps both while a TreeSet keeps one. Sorted collections decide membership with compareTo alone. If your ordering ties objects that are not equal, document it, or break ties with another field.
Subtraction wraps when the true difference leaves the int range: Integer.MIN_VALUE - 1 comes out as Integer.MAX_VALUE, so the comparator calls the smallest number the larger. Sorts then return wrong orders, and List.sort may throw IllegalArgumentException for a contract violation. Integer.compare and Comparator.comparingInt cannot overflow.
Object.clone copies fields one for one, so the copy shares every list or array the original points to. It throws CloneNotSupportedException unless the class implements Cloneable, an interface with no methods, and it creates the object without running a constructor, skipping any checks there. Array clone() is fine for flat arrays; nested rows stay shared.
Deprecated since Java 9 and marked for removal by JEP 421 in Java 18, a finalizer may run late, in arbitrary order or never, and it can resurrect the object. Release resources with try-with-resources on an AutoCloseable, keeping a Cleaner action only as a safety net. Running with --finalization=disabled, added in Java 18, shows whether your code still depends on finalizers.
H. Strings and text
Methods like toUpperCase, replace and trim return their result and leave the receiver alone, and may hand back the very same object when nothing needs changing. So s.toUpperCase(); on its own line does nothing visible; write s = s.toUpperCase();. Immutability is what makes strings safe as HashMap keys and shareable across threads without locks, and it lets equal literals share one object.
Constant expressions are pooled as well: "a" + "b" == "ab" is true, and so is the same concatenation through a final local set to a literal. Anything built at runtime, such as s + "b" with a non-final s, or new String(...), is a distinct object until you call intern(), which returns the pooled copy.
Because literals and compile-time constants share pooled objects, an == check passes on hard-coded test strings and fails on input read from a file, a form or the network. Use a.equals(b), or Objects.equals(a, b) when either side may be null, and write "yes".equals(input) so a null input gives false instead of a NullPointerException.
StringBuffer dates from Java 1.0 and synchronizes its methods; StringBuilder, added in Java 5 with the same API, does not, and its docs recommend it as faster. Sharing a buffer across threads still needs outer locking for compound actions, because the lock covers one method call at a time, never a check followed by an append. Inside one method, use StringBuilder.
Each s += piece builds a new string and copies everything gathered so far, so total copying is quadratic in the number of pieces. The invokedynamic concatenation of JEP 280 in Java 9 speeds up a single expression, not this loop. Create one StringBuilder before the loop and call append, or use String.join or Collectors.joining.
Characters above U+FFFF, such as most emoji, take a surrogate pair, so a three-symbol string can report length() 4 and charAt can return a lone surrogate. Count with codePointCount, iterate with codePoints(), and avoid substring cuts that split a pair. Even code points differ from what users see: an accent or a flag can combine several.
"a.b.c".split(".") returns an empty array: every character matches, and the empty strings left over are all trailing. Escape with "\\." or wrap the separator in Pattern.quote; | has the same trap. A leading separator still yields an empty first element, and split(",", -1) keeps the trailing empties.
Since Java 15, the indentation removed is the smallest found among the non-blank lines and the closing """ line. Moving the closing delimiter left of the content adds that much indentation to every line; moving it right changes nothing. Ending the last content line with the delimiter also drops the final newline. Trailing spaces are stripped; \s keeps one.
With a Turkish default locale, "title".toUpperCase() produces a capital I with a dot above, so comparing against "TITLE" fails, and "TITLE".toLowerCase() produces a dotless i. For keys, protocol words and enum lookups call toUpperCase(Locale.ROOT), or use equalsIgnoreCase, which ignores the locale.
It returns the difference of the first differing char values (or of the lengths, if one string is a prefix), so every uppercase ASCII letter sorts before every lowercase one: "Z".compareTo("a") is negative and a sorted list reads Cherry, apple, banana. Accented letters land after 'z'. Use String.CASE_INSENSITIVE_ORDER for simple cases and java.text.Collator for a given language.
I. Arrays
arr.length reads a final field, with no parentheses, unlike String.length() and List.size(). To grow an array you allocate a bigger one and copy, which is what Arrays.copyOf and ArrayList do behind the scenes. Valid indexes run from 0 to length - 1; anything else, negative values included, throws ArrayIndexOutOfBoundsException.
Because a String[] counts as an Object[], the compiler accepts storing an Integer through the Object[] view. Every array remembers its real element type and checks each store at runtime, so the error surfaces as ArrayStoreException far from where the array was created. Generic lists close this hole at compile time by being invariant.
Numeric slots start at 0 or 0.0, char at the zero character, boolean at false, and references at null. So new String[3] holds three nulls, not three empty strings, and calling a method on an element throws NullPointerException. Fill it in a loop or with Arrays.fill; a fresh int[] of counters, by contrast, is ready to use.
new int[3][4] builds all twelve slots, but new int[3][] builds only the outer array, whose rows stay null until assigned, and each row may then get its own length. Loop bounds should read grid[i].length rather than assume a rectangle. Cloning the outer array copies row references, so the copy and the original share rows.
Arrays inherit Object.equals and hashCode, so two arrays with identical contents are unequal and hash differently, which breaks them as map keys or set members. Arrays.equals and Arrays.hashCode compare one level deep; nested arrays need Arrays.deepEquals. A record with an array component inherits the same problem in its generated equals.
set works and changes the array, and later writes to the array show through the list. Any size change, add or remove, throws UnsupportedOperationException. Passing an int[] gives a List<int[]> of size 1, because generics cannot hold primitives. Wrap it as new ArrayList<>(Arrays.asList(...)) when you need a growable copy.
Stability, keeping equal elements in their original order, is guaranteed for Arrays.sort(Object[]), so sorting by name and then by department keeps names ordered within each department. For primitives it cannot be observed, since equal ints are indistinguishable. The Java 21 docs mark both algorithm names as implementation notes, not specification.
J. Records, enums, sealed types and pattern matching
For record Point(int x, int y) the compiler writes the constructor, x(), y() and value-based equals, hashCode and toString. equals compares primitive components with the wrapper's compare, so a double NaN equals NaN and 0.0 differs from -0.0. Extra instance fields are a compile error. Records are standard since Java 16.
Written as Range { ... } with no parameter list, it works on the parameters themselves: reassigning lo there changes what gets stored, because the fields are set from the parameters after the body finishes. Assigning this.lo directly is a compile error. Throwing an exception here is the idiomatic way to reject invalid input.
The component field is final, but it holds whatever list the caller passed, so both the caller and anyone calling items() can still add to it. Copy in the compact constructor with items = List.copyOf(items);, which also rejects null elements. Arrays are worse, since nothing makes them read-only; clone them on the way in and on the way out.
Records use the component name exactly, so Point(int x) gets x(), with no getX() and no setters. Tools that discover properties through the JavaBeans get prefix may find nothing unless they explicitly support records. You may declare an accessor yourself, but the JLS requires it to be public, return the component's type and declare no throws.
Instantiating an enum is a compile error, clone is final in Enum, reflection refuses with 'Cannot reflectively create enum objects', and deserialization hands back the existing constant. That makes == a safe comparison for enum values, and a one-constant enum a sound way to write a singleton.
Declaring abstract int apply(int a, int b); on the enum forces every constant to supply a body, so a new constant cannot forget it. Each such constant is an anonymous subclass, so PLUS.getClass() returns something like Op$1; call getDeclaringClass() when you need the enum type. This replaces a switch on this inside one shared method.
ordinal() is the position in the declaration, counted from 0, so inserting a constant mid-list or reordering the list renumbers everything after it and silently remaps stored data. Persist name() instead, or an explicit code field you control. The Enum docs describe ordinal as meant for structures like EnumSet and EnumMap.
Each permitted subclass states how the hierarchy continues: final closes it, sealed restricts it again, non-sealed reopens it to anyone, and a class naming none of the three is a compile error, though a record (implicitly final) or an enum needs none. Permitted classes must share the module, or the package when in the unnamed module, and permits can be left out if they share the file. Standard since Java 17.
Since Java 21, a switch with case Circle c and case Square s compiles without a default when those are the only permitted subtypes. Leaving the default out is deliberate: adding Triangle later turns every incomplete switch into a compile error. If a subtype slips past the check through separate compilation, the switch throws MatchException at runtime.
o instanceof Line(Point(var x1, var y1), Point end) checks the type, pulls components out through their accessors and binds x1, y1 and end in one step. A null never matches a record pattern. Bindings follow control flow: after if (!(o instanceof Line l)) return;, l stays in scope for the rest of the method. Standard since Java 21.
K. Exceptions
Leaving an IOException unhandled fails to compile; an unhandled NullPointerException does not. A catch or throws is required only for Throwable, Exception and subclasses outside the RuntimeException and Error branches. Catching a specific checked type the try block cannot throw is also an error. java.util.function interfaces declare no exceptions, so stream lambdas must wrap checked ones.
Clauses are tried top to bottom, and the first whose type matches wins. A catch (Exception e) above catch (IOException e) would leave the second unreachable, so javac rejects it, reporting that the exception has already been caught. Order clauses from most specific to most general; unrelated types may appear in any order.
Since Java 7, catch (IOException | SQLException e) shares one handler. The alternatives may not be subclasses of one another, since the narrower one would be redundant, and javac rejects assigning to e. Rethrowing e unchanged lets the method declare exactly the listed types rather than their common supertype.
Pass the original as the cause, as in new ServiceException("lookup failed", e), and the printed trace shows both, the inner one under 'Caused by:'. Building the wrapper from e.getMessage() alone keeps one line of text and drops the frames where the problem began. getCause() lets handlers inspect the original, and initCause sets one after construction.
catch (Exception e) {} turns a failure into wrong results that surface much later, with nothing pointing back to the cause. If ignoring an exception is truly correct, say so: name the variable ignored, or _ since Java 22, and add a comment with the reason. Otherwise log it with its stack trace, rethrow it, or wrap it as a cause.
Throwable sits above both Exception and Error, so a catch on it also absorbs OutOfMemoryError, StackOverflowError and linkage errors, which the Error docs class as severe failures that ordinary code should leave alone. Carrying on afterward may mean running in a broken state. Catch Exception at a boundary, and let errors propagate or log and rethrow them.
Constructing an exception calls fillInStackTrace, which walks the current stack, so creation cost grows with call depth, while throwing and catching a prebuilt instance is cheap. Exceptions used for control flow can call the Throwable constructor with writableStackTrace set to false. A reused instance reports where it was created, not where it was thrown.
A blocking call such as Thread.sleep clears the thread's interrupt status when it throws InterruptedException, so an empty catch erases the only record that someone asked the thread to stop. Either declare throws InterruptedException or call Thread.currentThread().interrupt() in the catch, so loops and executors further up still see the request.
L. Generics
The compiler checks type arguments, then replaces each type parameter with its bound, or Object, and inserts casts. So new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() is true, o instanceof List<String> is rejected for an Object operand, and two overloads differing only by List<String> versus List<Integer> clash.
An array checks every store against its element type at runtime, and after erasure T has no runtime type to check, so new T[10] and new List<String>[3] are compile errors while new List<?>[3] is allowed. Common workarounds: use a List<T>, cast new Object[n] with an unchecked warning, or call Array.newInstance with a Class<T> token.
A raw List accepts add(42) with only an unchecked warning, and the stray element fails later as a ClassCastException. Erasure also reaches past the class's own parameter: a method called on a raw receiver loses all its generics, so List<String> names() on a raw Foo returns plain List, and for (String s : raw.names()) will not compile.
If List<String> were a List<Object>, code holding the wider view could add an Integer to a list of strings. Arrays permit exactly that and pay with a runtime ArrayStoreException; generics refuse the assignment at compile time instead. When a method should accept lists of several element types, use a wildcard parameter rather than the supertype.
List<?> means a list of one specific but unknown type, which is why elements read back as Object and adding anything except null fails. That suits code like size(), printing, or clear() that never cares about the element type. Unlike a raw List, it keeps every compile-time check in place.
A List<? extends Number> might really be a List<Integer> or a List<Double>, so the compiler cannot let you add a Number, yet every read is safely a Number. It is not truly read-only: you can still add null, remove elements or call clear(). Use it for parameters a method only reads from.
A List<? super Integer> could be a List<Integer>, List<Number> or List<Object>, and an Integer fits all three, so adding one is safe. Reading is the opposite: the element might be any of those types, so get returns only Object. Use it for parameters a method writes into, like the destination of a copy.
A parameter the method reads from takes ? extends T, and one it writes to takes ? super T, the exact shape of Collections.copy(List<? super T> dest, List<? extends T> src). A parameter used both ways gets no wildcard. The Java Tutorials phrase this as 'in' and 'out' variables, and advise against wildcards in return types.
The bound requires T to be comparable with its own kind, so inside the method a.compareTo(b) accepts another T. The JDK usually loosens it to <T extends Comparable<? super T>>, as in Collections.sort, so a subclass that inherits compareTo from its parent still qualifies. Enum<E extends Enum<E>> uses the same self-reference.
A List<String>... parameter is really an array of raw List, so code can store a List<Integer> into it with no error until a later read throws ClassCastException. javac warns of possible heap pollution. @SafeVarargs silences that at every call site, and the JLS allows it only on constructors and static, final or private methods, with private accepted since Java 9.
M. Collections
A List promises position: indexes and an order you control, and it typically allows duplicates. A Set promises only uniqueness by equals, so the iteration order of a HashSet is not part of any contract. Map is not a Collection at all; it promises one value per key. A Queue promises a head, but the implementation decides which element sits there, so a PriorityQueue iterator walks in no particular order.
ArrayList.get(i) is constant time, while LinkedList.get(i) walks node by node from the nearer end. Even add(i, x) is linear for both: the linked list must first walk to position i, while the array does one fast bulk shift. As a queue, the docs describe ArrayDeque as likely faster than LinkedList too, which leaves the linked list very few real niches.
hashCode picks a bucket and equals finds the entry within it. Once the entry count exceeds capacity times load factor (12 for the default 16 and 0.75), the table about doubles and every entry is rehashed, so presize a map you know will be big. In OpenJDK 21, adding to a bucket that already holds 8 entries turns it into a red-black tree if the table has at least 64 buckets, and resizes the table otherwise.
HashMap iterates in bucket order, which depends on hash codes and table size, and its docs warn the order may change over time. LinkedHashMap threads a list through its entries, so iteration follows insertion, and putting an existing key again keeps its original spot. Two exceptions: the access-order constructor reorders on every get, and Java 21's putFirst and putLast move entries on purpose.
A TreeMap compares keys only with compareTo or its comparator, and two keys that compare as 0 are one key. Under String.CASE_INSENSITIVE_ORDER, putting "a" then "A" leaves a single entry that keeps the first key "a" with the second value, and containsKey("A") is true though no stored key equals "A". An ordering inconsistent with equals still works, but the map no longer honors the general Map contract.
The iterator records a modification count and compares it on each next(), so the failure shows up at the following step, not at the change. The check is best effort: removing the second-to-last element of an ArrayList inside a for-each throws nothing, because hasNext() returns false first and the last element is silently skipped. Treat the exception as a bug detector, never as a guarantee.
Iterator.remove deletes the element the last next() returned and keeps the iterator's bookkeeping in step; calling it twice without a next() between throws IllegalStateException. removeIf, on every Collection since Java 8, takes a predicate and does the iterating for you. An indexed loop that removes while counting up has its own bug: it skips the element that slides into the freed index.
Collections.unmodifiableList wraps the original: writes through the wrapper throw UnsupportedOperationException, but whoever holds the original can still change it, and the view shows it. List.copyOf (Java 10) takes a snapshot instead, rejects null elements, and generally skips the copy when given a list from List.of. Neither one freezes the elements, so a list of mutable objects can still appear to change.
The checks run eagerly, at creation, and go further: Set.of also rejects duplicate elements with IllegalArgumentException, though List.of(1, 1) is legal. Queries can reject null as well: in OpenJDK, List.of(1).contains(null) throws NullPointerException rather than returning false, which the List docs allow. The iteration order of Map.of and Set.of is unspecified and may change, so never depend on it.
SequencedCollection (JEP 431) gives List, Deque, LinkedHashSet and SortedSet the same getFirst, getLast, removeLast and reversed methods, and SequencedMap brings matching entry methods to LinkedHashMap and TreeMap. reversed() returns a live view. A sorted set throws UnsupportedOperationException from addFirst and addLast, and getFirst() on an empty list throws NoSuchElementException.
N. Lambdas, method references and functional interfaces
A lambda body shares the scope around it, so this, super and an unqualified toString() all refer to the enclosing object, and a lambda parameter may not reuse the name of a local already in scope. An anonymous class opens a new scope where this is the anonymous instance. When a lambda needs to refer to itself, for recursion say, use a method reference, a field or an anonymous class instead.
The rule covers locals and parameters, not fields, which are reached through this and may change freely. A classic for (int i = 0; ...) index cannot be captured because the loop reassigns it, but an enhanced-for variable can, since each iteration gets a fresh one. Wrapping state in a one-element array or an AtomicInteger compiles, but only moves the mutation onto the heap, where concurrent callers race.
Integer::parseInt calls a static method, System.out::println binds a receiver you already have, String::length leaves the receiver as the first argument, and ArrayList::new or int[]::new construct. A bound receiver is evaluated once, when the reference is created, so name::length on a null name throws NullPointerException right there, while the equivalent lambda fails only when called.
So String::length fits Function<String, Integer>, and String::startsWith fits BiFunction<String, String, Boolean>, with the receiver first and the method's own argument second. Ambiguity is the failure mode: Integer::toString will not compile as Function<Integer, String>, because both the static toString(int) and the instance toString() fit that shape. Write the lambda instead.
f.andThen(g) runs f first and feeds its result to g; f.compose(g) runs g first and feeds f. With f doubling and g adding one, f.andThen(g).apply(5) is 11 while f.compose(g).apply(5) is 12. Read andThen left to right like a pipeline, and compose as nesting, f(g(x)). IntUnaryOperator and its long and double siblings have both methods too, while Consumer and BiFunction offer only andThen.
and and or short-circuit like && and ||, so in notNull.and(isLong) the second test never sees a null. negate() needs a predicate object, which is clumsy with a method reference; Predicate.not (Java 11) wraps one directly, as in filter(Predicate.not(String::isEmpty)). Because evaluation runs left to right, put the cheap or guarding test first.
A Supplier hands over the recipe instead of the result: Logger.log(Level, Supplier<String>) builds the message only if that level is logged, and orElseGet runs only when the Optional is empty. A Supplier caches nothing, so each get() repeats the work. Creating a capturing lambda has a cost of its own, which the Objects.requireNonNull docs point out, so defer only work that is expensive.
Function<Integer, Integer> boxes every argument and result into an Integer object, while IntUnaryOperator works on raw int values. The names follow a scheme: IntFunction<R> takes an int, ToIntFunction<T> returns one, IntToLongFunction converts. Apart from BooleanSupplier, only int, long and double get variants, so a char widens to int. IntStream and its sum() follow the same idea.
Function.apply declares no exceptions, so p -> Files.readString(p) fails with an unreported IOException, yet the same body compiles as a Callable, whose call() declares Exception. The usual fixes are catching inside the lambda and rethrowing as UncheckedIOException, or declaring your own functional interface with a throws clause. Rethrowing unchecked costs callers the compiler's reminder to handle it.
O. Streams and collectors
filter and map only build a recipe; nothing is read from the source until collect, count or forEach pulls. Elements then flow one at a time through the whole pipeline, which is why findFirst can stop early; a stateful step like sorted is the exception. The common surprise: a pipeline with no terminal operation does nothing, so its map or peek side effects never happen.
After a terminal operation the pipeline is spent, and a second one fails with "stream has already been operated upon or closed". Chaining two operations off the same intermediate stage fails the same way. Keeping a Stream in a field or returning one for repeated use is the usual cause; keep the source collection, or a Supplier<Stream<T>>, and build a fresh stream each time.
They stop pulling once the answer is known, so Stream.iterate(1, x -> x + 1).anyMatch(x -> x > 100) returns true after examining 101 elements. A short-circuiting step is required but does not guarantee termination: if nothing ever matches, anyMatch on an infinite stream runs forever, and a sorted() placed before limit must see every element first, so it never finishes.
A parallel stream splits work across threads, so a lambda that adds to a shared ArrayList races with itself; a quick test pushing 100,000 elements through such a map kept only a fraction of them, and the same code can throw instead. Hidden state also breaks when the runtime skips work it does not need. Return values from the lambda and let toList() or a collector build the result.
map turns each element into exactly one result, so mapping lines with split(" ") yields a Stream<String[]>. flatMap expects each element to become a stream and splices those streams together, giving a Stream<String> of words. The tell is an unwanted nested type such as Stream<List<T>>. Java 16's mapMulti meets the same need without building an inner stream per element.
In Java 21, list.stream().peek(System.out::println).count() prints nothing: the size of a List source is already known, so no element needs to flow. Put a filter before count and the elements print again. peek exists for debugging; logic that must run for every element belongs in forEach or the terminal operation, never in a step the runtime is free to drop.
A parallel reduce folds each chunk starting from the identity, then combines the chunks. Summing 1 to 1000 with an identity of 1 instead of 0 gave 500,501 sequentially and a larger number in parallel, because the 1 was added once per chunk. Subtraction is not associative, so a parallel reduce(0, (a, b) -> a - b) disagrees with the sequential answer. Addition, min, max and string concatenation are safe.
reduce assumes immutable values and produces a new one at each step, so reducing strings with String::concat copies the growing string every time, quadratic work. collect instead fills a mutable container such as a StringBuilder or ArrayList, giving each parallel chunk its own container and merging them at the end. Using reduce to add into one shared list is a data race under parallelism.
Two elements that produce the same key make Collectors.toMap fail with IllegalStateException, and the message names the key. The three-argument form takes a merge function, such as Integer::sum to count or (a, b) -> a to keep the first. A null value throws as well, NullPointerException this time, even though a HashMap itself accepts null values.
groupingBy(classifier) alone gathers each group into a List; a second argument reshapes the groups, so groupingBy(String::length, counting()) yields counts as Long values and mapping(f, toSet()) transforms before collecting. The map type is unspecified unless you pass a factory such as TreeMap::new, so do not depend on its order. Groups exist only for keys that actually occur.
Parallel streams run on ForkJoinPool.commonPool(), which OpenJDK sizes by default to one fewer worker than available processors, with the calling thread helping. Every parallel stream in the JVM shares that pool, so one that blocks on IO starves the rest. Splitting and merging cost real time, and small inputs, sources that split poorly such as LinkedList, and order-bound steps like limit often lose to sequential.
P. Optional and null discipline
Its API note aims it at method results where returning null would invite mistakes. As a parameter it forces callers to wrap arguments and still lets them pass null; as a field it adds an object per instance and is not Serializable; in a collection, an empty element should just be left out. An Optional variable that is itself null defeats the whole point, so never assign null to one.
get() throws NoSuchElementException on an empty Optional, the same crash as the null dereference it was meant to prevent, and its name hides that it can fail. The no-argument orElseThrow() (Java 10) behaves identically with an honest name, and orElseThrow(() -> new NotFound(id)) lets you choose the exception. Better still, avoid unwrapping with map, orElse or ifPresentOrElse.
orElse(x) is an ordinary method call, so Java evaluates x first whether or not a value is present; orElse(loadDefault()) runs the load every time. orElseGet(() -> loadDefault()) passes a recipe that runs only when the Optional is empty. Use orElse for constants and values already computed, and orElseGet whenever the fallback is costly or has side effects.
Use Optional.of(x) when null would be a bug: it throws NullPointerException at the point of wrapping, close to the cause. Optional.ofNullable(map.get(k)) turns a possibly-null legacy result into empty. A common slip is Optional.of(map.get(k)), which turns every missing key into a crash. Note that map already wraps its result as if by ofNullable, so a null from the mapper gives empty.
If findUser returns Optional<User> and User::address returns Optional<Address>, then findUser(id).map(User::address) gives Optional<Optional<Address>>, while flatMap hands back the inner Optional directly. Use map when the step returns a plain value and flatMap when it already returns an Optional. Every step after an empty result is skipped, so one fallback at the end covers the chain.
Q. Threads, the memory model and synchronization
A data race, in JLS terms, is two accesses to one variable, at least one a write, with no happens-before order between them. A race condition is a logic error where the outcome depends on timing. They are independent: if (!m.containsKey(k)) m.put(k, v) on a ConcurrentHashMap has no data race yet is still a race condition, fixed by one atomic call such as putIfAbsent.
If action A happens-before action B, B sees A's writes. The edges you use daily: unlocking a monitor before a later lock of it, a volatile write before a later read of that field, Thread.start() before the started thread's first action, and a thread's actions before another thread returns from join() on it. The relation is transitive, which is how one volatile flag publishes ordinary writes made before it.
Without a happens-before edge the JIT may hoist a read out of a loop. On HotSpot, a thread spinning on a plain boolean stop kept running after another thread set it, while the same loop on a volatile flag exited at once. Thread.sleep in the loop does not help, since the JLS gives it no synchronization effect. And a plain long or double write may land as two 32-bit halves, so a racing reader can mix two writes.
A volatile write happens-before every later read of that field, and writes made before it become visible along with it, so it suits flags and publishing a reference. It does not make compound actions atomic: four threads each running a million vcount++ on a volatile int finished far below four million. Use AtomicInteger.incrementAndGet or a lock whenever the new value depends on the old one.
The read, the add and the write are separate steps, so two threads can both read 5 and both write 6, and one increment vanishes without any error. Losses grow with contention and can stay invisible in light testing. x += 1 and ++x have the same flaw. Fixes: AtomicInteger, a LongAdder for a heavily updated counter read rarely, or synchronized around the update.
Exclusion only holds between threads that lock the same object. An instance synchronized method locks this and a static synchronized method locks the Class object, so the two never exclude each other. The lock is released on any exit, including an exception. Readers need the lock too: a getter without synchronized gets neither exclusion nor the visibility edge, and may see stale data.
The JVM counts each acquisition by the owning thread, and the monitor is free only after the same number of releases. That is what lets one synchronized method call another on the same object, or an override call its super version, without deadlocking itself. ReentrantLock behaves the same and exposes the count through getHoldCount(), so every lock() needs its own unlock().
Thread 1 holds A and waits for B while thread 2 holds B and waits for A; the JLS neither prevents this nor requires the JVM to detect it, so both wait forever. Acquiring locks in one global order, for example by account id, makes the cycle impossible. To diagnose a live one, jstack prints "Found one Java-level deadlock" with the cycle, and ThreadMXBean.findDeadlockedThreads reports it from code.
The JLS permits a waiting thread to wake with no notify at all, and even a real notification can find the condition already consumed by a thread that took the lock first. So recheck with while (!ready) lock.wait();, never if. Calling wait or notify without holding that object's monitor throws IllegalMonitorStateException. Prefer notifyAll unless every waiter waits for the same condition.
JLS 17.5 promises that a thread seeing an object also sees the constructor's values of its final fields, provided this did not escape during construction; non-final fields get no such promise. In double-checked locking, a reader that skips the lock can see the new reference before the constructor's writes. Declaring the field volatile adds the happens-before edge that was missing.
R. java.util.concurrent and virtual threads
shutdown() refuses new tasks but lets queued ones run, and returns at once; awaitTermination then blocks until they finish or the timeout passes, returning false on timeout. shutdownNow() interrupts running tasks and returns those never started. A forgotten shutdown can keep the JVM alive, because default pool threads are non-daemon. Since Java 19, try-with-resources calls close(), which shuts down and waits.
A CPU-bound thread keeps a core busy, so threads beyond the core count only add switching. A thread blocked on IO uses no core, so keeping the cores busy takes about cores × (1 + wait time / compute time): 8 cores and tasks that wait 9 ms per 1 ms of work need roughly 80 threads. For IO work on Java 21, virtual threads remove the question, and a Semaphore caps load on the backend.
get() waits with no limit, so prefer get(timeout, unit), which throws TimeoutException. The task's own exception arrives as the cause of an ExecutionException, so inspect getCause() before deciding what failed. A task sent with submit whose Future nobody reads fails silently, because the exception is stored, not printed; execute instead hands it to the thread's uncaught-exception handler.
thenApply maps a result, thenCompose chains a step that itself returns a CompletableFuture without nesting one inside another, and thenCombine waits for two independent futures. A non-async callback runs on whichever thread completes the stage, or on yours if it is already complete, so slow work there stalls someone else. The Async variants use ForkJoinPool.commonPool() unless you pass an executor.
An exception completes the stage exceptionally and skips every dependent thenApply or thenAccept, and nothing is logged. You learn of it only by joining or attaching a handler: join() throws CompletionException, get() throws ExecutionException. Handlers on later stages usually receive the error wrapped in a CompletionException, so unwrap getCause() before checking its type. whenComplete sees the failure but passes it on.
In OpenJDK 21, writes lock only the affected bin, using a compare-and-swap for an empty bin and the bin's first node as the lock otherwise, while reads take no lock. Iterators never throw ConcurrentModificationException but may miss later changes. computeIfAbsent runs atomically, so keep its function short and never touch the map from inside it. Null keys and values are rejected.
compareAndSet(expected, next) succeeds only if the value still equals expected; on failure you reread and retry, which updateAndGet does for you. Its function may therefore run more than once and must have no side effects. CAS checks equality, not history: a value that went from A to B and back to A passes. When that matters, AtomicStampedReference pairs the reference with a version stamp.
With a bounded queue such as new ArrayBlockingQueue<>(100), put blocks when the queue is full, so a fast producer slows to the consumer's pace instead of filling the heap. In the same situation offer returns false and add throws IllegalStateException. The trap is new LinkedBlockingQueue<>() with no capacity, which means Integer.MAX_VALUE; Executors.newFixedThreadPool runs on such an unbounded queue.
tryLock(timeout, unit) gives up instead of waiting forever, lockInterruptibly lets another thread cancel the wait, and newCondition gives one lock several wait sets, such as notFull and notEmpty. A fair lock favors the longest waiter at a real throughput cost, yet the untimed tryLock() ignores fairness. The price is discipline: unlock in finally, or a single exception leaks the lock.
Final in Java 21 (JEP 444), a virtual thread occupies a carrier platform thread only while running; most blocking JDK calls unmount it so the carrier can run another. That buys throughput for IO-bound work, not speed: CPU-bound code gains nothing past the core count. Some file operations still capture the carrier, and the scheduler compensates with extra threads. Virtual threads are always daemon threads.
A virtual thread is meant to run one task and end, so Executors.newVirtualThreadPerTaskExecutor() starts a fresh one per submission. Pools existed because platform threads are expensive; virtual ones are not. If the real goal is limiting load, say 20 concurrent calls to a backend, guard the call with a Semaphore. Reused threads also carry ThreadLocal values into unrelated tasks.
Through JDK 23, a virtual thread that blocked inside synchronized, or waited for a monitor, held its carrier, so enough of them could stall the scheduler, and the advice was to switch to ReentrantLock. JEP 491 in JDK 24 lets it unmount there too, and migrated code need not be reverted. Pinning remains when native code calls back into Java and blocks, and while blocking in a class initializer.
Final in Java 25 (JEP 506), ScopedValue.where(USER, u).run(task) binds USER for the duration of task and everything it calls; afterward the binding is gone, so nothing leaks into the next task a pooled thread runs. Callees can read it but not set it, and a nested where rebinds it only for its inner call. Outside any binding, get() throws. In Java 21 it was a preview API.
Subtasks forked in a StructuredTaskScope cannot outlive its block: by default, if one fails the others are interrupted and join() throws, and closing the scope waits for every subtask to end, so a subtask that ignores interruption delays the close. The API was still a preview through JDK 27, its seventh round, so it needs --enable-preview, and its shape changed between previews.
The two fit because each supplies what the other needs. A subtask forked in a StructuredTaskScope reads the parent's scoped-value bindings without any copying, which is safe only because the scope guarantees every subtask finishes before the binding ends. A task handed to an ExecutorService or ForkJoinPool has no such guarantee, so it does not inherit the binding at all.
S. Dates and times
A Date handed to another object can be changed later through setTime, so defensive copies were mandatory, and Calendar months count from 0, so set(2024, 1, 1) means February. SimpleDateFormat is not synchronized, and its docs tell you to give each thread its own. The java.time API (Java 8) fixes all three with immutable values and a thread-safe DateTimeFormatter you can keep in a static field.
An Instant counts from the 1970 epoch in UTC, the same moment everywhere, which suits logs and stored timestamps. A LocalDateTime like 2024-06-01T09:00 names no moment until you add a zone: in New York it is 13:00 UTC, in Tokyo 00:00 UTC. Storing event times as LocalDateTime loses that for good, so keep Instant inside the system and attach a zone only to display.
ZoneOffset.ofHours(-5) is always five hours behind UTC, while ZoneId.of("America/New_York") knows the offset flips between -05:00 and -04:00 for daylight saving. Add a day across the March change and the OffsetDateTime keeps -05:00 while the ZonedDateTime moves to -04:00. Use offsets for protocols and databases that store one, and zones for future local times, since the rules change.
plusMonths changes the month field, then pulls an invalid day back to the last valid day, so 2024-01-31 becomes 2024-02-29. That makes month arithmetic lossy: adding one month twice from 31 January lands on 29 March, while plusMonths(2) gives 31 March, and subtracting the month again returns 29 January. Compute recurring dates from the original anchor, never by chaining.
A Period counts years, months and days; a Duration is an exact length in seconds and nanoseconds. Across the March 2024 daylight change in New York, noon plus Period.ofDays(1) is noon the next day, only 23 hours later, but noon plus Duration.ofDays(1) is 13:00. An Instant accepts days as 24 hours but rejects months, which have no fixed length, with UnsupportedTemporalTypeException.
On 10 March 2024, New York clocks jumped from 02:00 to 03:00, so asking ZonedDateTime.of for 02:30 silently gives 03:30. On 3 November, 01:30 happened twice, and java.time picks the earlier offset, -04:00, unless you call withLaterOffsetAtOverlap(). Neither case throws, so a job scheduled at a local time in that window can run late or twice; schedule by Instant instead.
date.plusDays(5); on its own line compiles and changes nothing, because a LocalDate cannot be modified; you need date = date.plusDays(5). The with... methods return copies too, not setters. The payoff is that values are thread-safe and safe to share or use as map keys. They are also value-based classes, so compare them with equals or isEqual, never with ==.
LocalDate.now() reads the system clock, so a test of whether a coupon has expired depends on when it runs. Give the class a Clock and call LocalDate.now(clock): production passes Clock.systemDefaultZone(), tests pass Clock.fixed(...) to pin the moment or Clock.offset to shift it. The fixed clock's zone matters too, since one instant falls on different dates in different zones.
T. I/O, files and serialization
The clearest difference is error reporting: File.delete() returns false and never says why, while Files.delete(path) throws an IOException such as NoSuchFileException or DirectoryNotEmptyException. Files also covers symbolic links, file attributes and directory walking. The two interoperate, so you can migrate gradually with file.toPath() and path.toFile().
Files.readAllLines returns a complete List<String>, so memory grows with the file and a multi-gigabyte log can end in OutOfMemoryError; its docs say it is not meant for large files. Files.newBufferedReader or Files.lines hand you one line at a time and hold little more than the current one. All three read UTF-8 when you pass no charset, and fail on malformed input rather than guess.
The stream from Files.lines, Files.list or Files.walk keeps a file or directory open, and finishing a terminal operation does not close it, so open it in try-with-resources. Leaked descriptors pile up until the process hits its open-file limit. Errors arrive differently too: a read failure after the call returns surfaces as UncheckedIOException from the stream operation that triggered the read.
Text is bytes plus an encoding, and a Reader is where decoding happens: InputStreamReader turns bytes into chars using a charset you should name. Casting each byte from an InputStream to char works only for ASCII; the euro sign takes three bytes in UTF-8 and comes out as three wrong characters. Use streams for binary data such as images, and readers and writers for text.
Before JDK 18 (JEP 400), FileReader, FileWriter and new String(bytes) used a charset chosen from the operating system and locale, so a file written on a Mac could decode as garbage on Windows. The Files read methods were already UTF-8. -Dfile.encoding=COMPAT restores the old behavior, and naming the charset, as in new FileReader(f, UTF_8) (Java 11), removes the ambiguity.
Each read() on a raw FileInputStream is a call into the operating system, so reading one byte at a time pays that cost per byte. BufferedInputStream fetches a block, 8,192 bytes by default in OpenJDK, and serves later reads from memory; in a quick test on a 20 MB file the unbuffered loop ran about 50 times slower. Reading into your own byte[] array gets the same benefit.
Path.of("config.yml") and new File("config.yml") resolve against the user.dir property, normally the directory the JVM was started from, so the same jar finds the file when launched from one folder and fails from another or inside an IDE. For files shipped with the code, use getResourceAsStream, which searches the class path. For user files, build paths from a known base.
readObject creates instances of whatever serializable classes the stream names and runs their deserialization hooks before your cast executes, so a crafted stream can chain hooks in ordinary library classes into harmful behavior. Validating the result afterward is too late. Avoid Java serialization for external input; if you must accept it, install an ObjectInputFilter (Java 9) that allows only the classes you expect.
Without an explicit serialVersionUID, Java computes one from the class's details, so adding a method can make old bytes fail with InvalidClassException; declare private static final long serialVersionUID = 1L;. A transient field is skipped on write and, since neither constructor nor field initializer runs for a serializable class, reads back as 0, false or null even with = 5 in its declaration.
U. Class loading, linking and initialization
Loading finds the bytes and creates the class; linking verifies it, prepares static fields at their zero values and resolves symbolic references, which may happen later; initialization runs the static initializers. The JVM may link eagerly or lazily, but initialization waits for the first triggering use, so a program can start, print, and only then fail when it first touches a missing class.
Creating an instance, calling a static method, or assigning or reading a non-constant static field initializes the class that declares the member. Not triggers: Foo.class, new Foo[10], loadClass, and reading a compile-time constant. Reading an inherited static through a subclass initializes only the superclass that declares it. Class.forName(name) initializes by default, unlike ClassLoader.loadClass.
A static final primitive or String set by a constant expression is compiled into each reading class as a literal, so the reader keeps no link to the field. Change LIMIT from 10 to 99, recompile only the declaring class, and callers keep printing 10 until they are recompiled too. Reading such a constant never initializes its class. Initialize from a method call, such as Integer.parseInt("10"), to keep a real reference.
ClassLoader.loadClass first checks the classes it already loaded, then asks its parent, and so on up through the platform loader to the bootstrap loader, calling its own findClass only if they all fail. So a class the parent can see always wins, and you cannot replace java.lang.String from the class path. Custom loaders should override findClass, not loadClass, to keep that order.
At run time a class is the pair of its binary name and its defining loader. Load Plugin through two sibling loaders and you get two Class objects that share a name but not a type, so casting an instance of one to the other fails with the baffling "class Plugin cannot be cast to class Plugin", followed by the two loaders. Their static fields are separate too. Load shared types once, from a common parent loader.
ClassNotFoundException is a checked exception from an explicit lookup by name, such as Class.forName or loadClass. NoClassDefFoundError is an Error thrown when code compiled against a class needs it at run time and cannot have it: either the class file is missing, in which case a ClassNotFoundException is usually its cause, or the class's static initialization already failed.
If a static initializer throws something that is not an Error, the first attempt wraps it in ExceptionInInitializerError, whose cause is the real problem. The JVM then marks the class erroneous, so every later use throws NoClassDefFoundError "Could not initialize class". Since JDK 18, that error's cause names the original exception and repeats its stack trace; on older JDKs, search the logs for the first ExceptionInInitializerError.
The JVM cannot trust that a class file came from javac: it may be hand-crafted, or compiled against an older version of a library. At link time the verifier checks each method for operand stack overflow and underflow, valid local-variable use and correct operand types, rejecting violations with VerifyError. From class file version 50 (Java 6), stack map frames in the file let the verifier check types instead of inferring them.
V. The JVM: memory, garbage collection and performance
Each thread has its own stack of frames, one per active call, holding locals, parameters and intermediate values, all discarded when the call returns. Every object and array lives on the heap that all threads share. So a local variable is private to its thread, but the object it references is not: pass that reference to another thread and the object's fields are shared.
StackOverflowError means one thread's call chain outgrew its stack, almost always unbounded recursion; the repeating frames in the trace point at the loop, and a bigger -Xss only postpones it. OutOfMemoryError means a memory area, usually the heap, could not satisfy a request even after collection. Read its message: "Java heap space" and "Metaspace" point at different problems.
Scope is a compile-time idea; the collector only asks whether some chain of references leads to the object from a root such as a live thread's stack or a static field. So an object left in a static map survives long after its variable is gone, and two objects that point only at each other are still collected. The JLS even allows treating a local never used again as dead early.
A young collection's cost tracks the number of objects still alive, not the amount of garbage, so a young generation full of dead temporaries is cleared quickly. Survivors of several collections move to the old generation, which is collected less often and at higher cost. Objects that live just long enough to be promoted, then die, push work into those costlier collections.
The collector frees only unreachable objects, so a Java leak is a reference you forgot: a map that only grows, a listener added on every page view and never removed, a ThreadLocal left set on a pooled thread. The symptom is a live set that keeps climbing under steady load until "Java heap space" errors arrive. A heap dump shows which root still holds the objects.
A soft reference is cleared at the collector's discretion as memory runs short, and always before an OutOfMemoryError; a weak reference is cleared once its target is only weakly reachable; a phantom reference's get() always returns null and only signals reclamation. WeakHashMap holds keys weakly but values strongly, so a value that refers to its own key keeps the entry alive.
Every collector trades total work against stop-the-world pauses and memory. In JDK 21, the Parallel collector maximizes throughput and tolerates long pauses, G1, the default on most hardware, aims for a pause-time goal, ZGC keeps pauses under a millisecond at some throughput cost, and Serial suits small heaps. The tuning guide's advice: let the VM choose, and switch only for a measured problem.
HotSpot starts by interpreting bytecode, then compiles methods that prove hot, first quickly and later with heavier optimization based on observed behavior. Timing the first iterations measures that transition, not your code. Optimizations rest on assumptions, such as a call site seeing only one class; when a new class arrives, the compiled code is discarded and execution falls back. Warm up, or use JMH.
W. Packages, modules and the toolchain
The JLS leaves this to tools, and javac will compile package com.acme; from a file in any folder. The mismatch bites at lookup: -sourcepath and the class loader search for com.acme.Hello only at com/acme/Hello under a root, so a misplaced class is not found, and running it by its folder name fails with NoClassDefFoundError "Hello (wrong name: com/acme/Hello)". Mirror the package in the directory tree.
An import only tells the compiler how to expand a simple name; class files always store fully qualified names, so two versions of a class that differ only in imports compile to the same bytecode, line numbers aside. Unused imports therefore cost nothing at run time. An import must still resolve, though: importing a package that does not exist is a compile error, and java.util.* excludes java.util.function.
With import java.util.*; and import java.awt.*; both present, the name List is ambiguous, and using it is a compile error, though the imports alone are fine until you do. Adding import java.util.List; settles it, because a single-type import shadows on-demand ones. The trap is upgrades: a class newly added to one wildcard-imported package can break code that compiled yesterday.
requires M lets your module read M, and requires transitive M passes that readability on to modules that require yours. exports P makes the public types of package P usable by other modules at compile time and run time. opens P grants reflective access, private members included, but only at run time. Every module implicitly requires java.base, and a package you neither export nor open stays internal.
Exporting and opening are separate grants. String.class.getMethod("length") works because java.base exports java.lang, yet setAccessible(true) on the private value field throws InaccessibleObjectException because java.lang is not opened. A public method in the non-exported jdk.internal.misc fails with IllegalAccessException. Since JDK 17 (JEP 403), only explicit flags such as --add-opens relax this.
Everything on the class path lands in one unnamed module, which reads every module and exports all its packages, so pre-module code runs unchanged. The catch runs one way: a named module cannot requires the unnamed module, since it has no name. A plain jar moved to the module path becomes an automatic module, named by its manifest's Automatic-Module-Name or its file name, which named modules can require.
-source 8 -target 8 sets the language level and bytecode version but still compiles against the running JDK's library, so a call to String.isBlank(), added in Java 11, compiles with only a warning and fails with NoSuchMethodError on a Java 8 runtime. --release 8, added in JDK 9 by JEP 247, compiles against the recorded Java 8 API and rejects the call as "cannot find symbol".
X. Annotations and reflection
SOURCE annotations, such as @Override, vanish after compilation. CLASS is the default when @Retention is absent: the annotation is written into the class file, but getAnnotations() cannot see it. So a custom annotation that a framework reads at run time silently does nothing unless it is declared @Retention(RUNTIME). Bytecode tools can still read CLASS annotations from the file itself.
@Target(ElementType.METHOD) makes placing the annotation on a field or class a compile error, catching misplaced markers early. Without @Target, Java 21 allows the annotation on any declaration but not in type positions such as List<@NonNull String>, which need TYPE_USE. @Target says nothing about visibility at run time; that is the separate job of @Retention.
Writing public boolean equals(Point p) declares a new overload, so HashSet and List.contains keep calling Object.equals(Object) and your logic never runs. With @Override the compiler rejects it, since nothing in a supertype matches. The same check catches a misspelled name such as hashcode, and a supertype method that is later renamed or removed. It works for interface methods too.
getMethod searches the class by name and parameter types, so do it once and keep the Method. Each Method.invoke also checks access, boxes arguments into an Object[] and wraps any exception in InvocationTargetException. A MethodHandle is access-checked once, when it is created, and then invoked directly. Since JDK 18 (JEP 416), core reflection itself is built on method handles.
Type arguments exist only at compile time, so new ArrayList<String>().getClass() and new ArrayList<Integer>().getClass() return the same ArrayList class. Declarations keep them: a field declared List<String> reports its argument through getGenericType() as a ParameterizedType. That is why libraries capture a type by having you write an anonymous subclass, whose generic superclass records the argument.
getDeclaredMethods returns every method the class itself declares, private ones and compiler-generated bridge methods included, but nothing inherited. getMethods returns public methods from the class, its superclasses and its interfaces, Object's among them. So a private helper appears only in the first list, and getDeclaredMethod("pub") on a subclass that merely inherits pub throws NoSuchMethodException.
Proxy.newProxyInstance creates a class at run time that implements the interfaces you list and routes every call to your InvocationHandler, which is how a framework adds logging or transactions without touching your code. It cannot proxy a class: passing ArrayList throws IllegalArgumentException. Calls to equals, hashCode and toString reach the handler too, so one that returns null for them breaks hash-based collections.
Y. Java 25 language frontier
Final in JDK 25 (JEP 512), a file holding only void main() { IO.println("Hi"); } compiles to an implicitly declared final class in the unnamed package, one you cannot name or create with new. IO is in java.lang, so it needs no import, but its methods are not statically imported, so write IO.println. A compact file also sees every public type that java.base exports, without imports.
Final in JDK 25 (JEP 511), import module java.sql; imports on demand every public top-level type in the packages that module exports, plus those of modules it requires transitively, and it works in class-path code. Ambiguity arrives in bulk: importing modules java.base and java.sql brings two Date classes, so using Date fails until a single-type import such as import java.sql.Date; shadows them.
Final in JDK 25 (JEP 513), statements may come before super(...) or this(...) to check or transform arguments and throw early. That prologue may not read fields, call instance methods or use this, but it may assign its own class's fields that have no initializer. A subclass can then set its fields before the superclass constructor runs, so an overridden method called from there sees real values.
Z. Release cadence and support
OpenJDK ships a feature release every March and September: JDK 25 in September 2025, JDK 26 in March 2026, JDK 27 in September 2026. Oracle lists 8, 11, 17, 21 and 25 as LTS and plans one every two years, next JDK 29 in September 2027. The every-fourth rhythm began after 17; from 11 to 17 was six releases. A non-LTS release is superseded once the next one ships.
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.