Java Tips, Tricks & Traps
118 entries covering modern Java (21–25): language basics, object-oriented design, modern features (records, sealed classes, virtual threads, pattern matching), concurrency, collections, exceptions, design patterns, and classic traps — each with a detailed explanation and a runnable code sample. Built as a study guide for really understanding Java, not just passing an interview about it.
No JDK installed? Practice Java basics in your browser with WasmCoffee — no install, runs entirely in the tab.
Primitive Types and Their Wrapper Classes
Java has 8 primitive types; each has a corresponding wrapper class that enables use in generics and collections, but autoboxing introduces subtle pitfalls.
Java 21 (core language)String Is Immutable
String objects are immutable in Java; every modification creates a new object. The string pool and intern() affect identity equality, making == vs .equals() a classic interview trap.
StringBuilder vs StringBuffer
StringBuilder is the go-to for mutable string construction in single-threaded code; StringBuffer is its thread-safe but slower predecessor — almost never needed in modern Java.
var — Local Variable Type Inference
var lets the compiler infer the type of a local variable from its initializer, reducing boilerplate — but it's not dynamic typing and has strict limits.
Switch Expressions
Switch expressions (Java 14+) replace the error-prone fall-through statement form with arrow labels, a yield keyword, and the ability to return a value — eliminating most break bugs.
The Enhanced For Loop
The enhanced for loop iterates over any Iterable or array cleanly, but it cannot provide an index, cannot remove elements safely, and iterates a copy of the loop variable.
Arrays vs ArrayList
T[] is a fixed-size, covariant array; ArrayList<T> is a resizable, type-safe list. Arrays.asList() returns a fixed-size list backed by the original array — a common trap.
Java Is Pass-by-Value
Java is strictly pass-by-value — but for objects, the value passed is the reference (memory address). Reassigning a parameter never affects the caller; mutating the referenced object does.
Java 21 (core language)The equals() / hashCode() Contract
equals() and hashCode() must be overridden together and satisfy a strict contract; violating it silently breaks HashMap, HashSet, and all hash-based data structures.
Text Blocks
Text blocks ("""...""") provide multi-line string literals that automatically strip common leading whitespace (incidental whitespace), making embedded JSON, SQL, and HTML readable.
Sealed Classes
Sealed classes (Java 17+) restrict which classes can extend or implement them, enabling the compiler to enforce exhaustive switch and model closed type hierarchies.
Java 17+ (standard)Pattern Matching for instanceof
Pattern matching for instanceof (Java 16+) eliminates the redundant cast after a type check by introducing a binding variable that is already typed and in scope.
Modern String Formatting
Java offers String.format(), String.formatted() (Java 15+), printf, and MessageFormat for string formatting; use %n for platform-neutral newlines and know the common format specifiers.
Numeric Literals and Underscores
Java supports binary (0b), octal (0), and hex (0x) integer literals, plus underscore separators in numeric literals for readability — underscores may not appear at the start, end, or next to a radix prefix.
The final Keyword
final means different things depending on context: for variables it means assign-once; for methods it prevents overriding; for classes it prevents inheritance; for parameters it prevents reassignment inside the method.
Class vs Object
A class is a blueprint; an object is an instance of that blueprint created at runtime. Reference variables hold memory addresses (references), not the objects themselves.
Java 21 (core language)Constructors and this / super Chaining
Constructors initialise objects; this(...) chains to another constructor in the same class; super(...) chains to the parent; every constructor must call one of these if not relying on the implicit no-arg super().
The Four Pillars of OOP
Object-oriented programming rests on four pillars: encapsulation, inheritance, polymorphism, and abstraction — each with a distinct purpose and Java mechanism.
Java 21 (core language)Method Overloading
Overloading lets you define multiple methods with the same name but different parameter lists; the compiler selects the best match at compile time via a fixed resolution order: exact match → widening → boxing/unboxing → varargs.
Java 21 (core language)Method Overriding
Overriding lets a subclass provide a specific implementation of a method declared in a superclass; the selection happens at runtime (dynamic dispatch), not compile time. @Override is mandatory best practice.
Abstract Classes
An abstract class cannot be instantiated and may contain abstract methods (no body) that subclasses must implement, alongside concrete methods, fields, and constructors — use it when you want to share state or a partial implementation.
Java 21 (core language)Interfaces with Default and Static Methods
Since Java 8, interfaces can have default methods (inherited implementations) and static methods; Java 9 added private methods — enabling rich mixin-style abstractions and backwards-compatible API evolution.
Interface vs Abstract Class Decision Matrix
Choose an interface for a capability/contract that unrelated classes can share; choose an abstract class for a shared skeleton (state + partial implementation) within a single inheritance hierarchy.
Java 21 (core language)Generics and Type Erasure
Generics provide compile-time type safety; type erasure removes generic type information at runtime. A List<String> is just a List in bytecode — causing instanceof limits and unchecked cast warnings.
Bounded Wildcards (PECS)
Upper-bounded wildcards (? extends T) allow reading from a generic structure; lower-bounded (? super T) allow writing — remembered with the PECS mnemonic: Producer Extends, Consumer Super.
Static Members and Inner Classes
static fields and methods belong to the class, not instances; static nested classes don't hold a reference to the outer instance; non-static inner classes do — and that hidden reference is a memory-leak trap.
Key Object Methods to Override
Object provides toString(), equals(), hashCode(), clone(), and finalize() — only toString(), equals(), and hashCode() should routinely be overridden; prefer records for data classes.
Access Modifiers
Java has four access levels — public, protected, package-private (no keyword), and private — controlling visibility of classes, fields, methods, and constructors across packages and inheritance hierarchies.
Composition Over Inheritance
Favour composition (has-a) over inheritance (is-a) to avoid brittle base-class coupling; delegation wraps an object and forwards calls, giving you flexibility to change the implementation without breaking the class hierarchy.
Java 21 (core language)Enums in Depth
Java enum is a full class — enum constants can have fields, constructors, and methods; EnumSet and EnumMap provide highly efficient enum-keyed set/map implementations.
Records
Records (Java 16+) are transparent, immutable data carriers that automatically generate a canonical constructor, component accessors, equals(), hashCode(), and toString() from the record header.
Records — Custom Methods and Wither Pattern
Records support custom accessor overrides, additional constructors, and the "wither" pattern for creating modified copies — because records are immutable, you produce a new instance rather than mutating state.
Java 16+ (standard)Records vs Lombok
Records are the standard Java alternative to Lombok @Value; records cannot have mutable fields or extend classes, but they require no annotation processing and are a first-class language feature.
Sealed Classes + Records as Algebraic Data Types
Combining sealed interfaces with records creates type-safe algebraic data types (sum types); exhaustive switch over the sealed type provides ML/Haskell-style pattern matching in Java.
Pattern Matching in Switch
Pattern matching for switch (Java 21) allows matching on types, records, and guarded patterns (when), with null handling and full exhaustiveness checking over sealed types.
Record Deconstruction Patterns
Record deconstruction patterns let you unpack record components directly in instanceof checks and switch cases — including nested records — providing concise structural decomposition.
Virtual Threads (Project Loom)
Virtual threads (Java 21, Project Loom) are lightweight JVM-managed threads that dramatically reduce the cost of blocking I/O — you can create millions of them without the memory and scheduling overhead of OS threads.
Java 21+ (standard)Structured Concurrency
Structured concurrency (StructuredTaskScope, Java 21+) treats a group of related concurrent tasks as a single unit of work, so failures and cancellations propagate cleanly and threads don't leak.
ScopedValue vs ThreadLocal
ScopedValue (Java 21+) is the modern, structured alternative to ThreadLocal — immutable within a scope, works correctly with virtual threads, and automatically cleaned up.
Sequenced Collections
Java 21 introduced SequencedCollection, SequencedSet, and SequencedMap — new interfaces that give all ordered collections a uniform API for accessing and modifying the first and last elements.
Unnamed Classes and Instance main
Unnamed classes (JEP 445/495) eliminate the boilerplate of public class ... { public static void main(String[] args) { ... } } for simple programs — a single main() method in a file is enough.
Optional — Best Practices
Optional<T> is a container for a possibly-absent value; use it as a return type to signal absence without null, but avoid it as a field type, parameter type, or in collections.
Streams — Basics
Stream<T> is a lazy sequence of elements supporting pipeline-style operations — filter, map, flatMap, collect, reduce — that are evaluated only when a terminal operation is invoked.
Streams — Collectors
Collectors provides ready-made terminal collectors: groupingBy, partitioningBy, joining, toUnmodifiableList, and counting — and you can compose them for multi-level aggregation.
Method References
Method references (::) are shorthand for lambdas that simply call a single existing method — four kinds: static, bound instance, unbound instance, and constructor references.
Functional Interfaces
Functional interfaces have exactly one abstract method; java.util.function provides the common ones — Function, Predicate, Supplier, Consumer, BiFunction — and they compose via andThen, compose, and, or, negate.
Lambdas vs Anonymous Classes
Lambdas are concise functional interface implementations; they cannot capture mutable local variables (only effectively-final), have no this reference of their own, and should never be serialized.
Streams — Gatherers (Java 22+)
Stream.gather(Gatherer) (Java 22+) adds a powerful stateful intermediate operation API — enabling sliding windows, prefix-while, deduplication by key, and other patterns that filter/map cannot express.
Value Classes (Project Valhalla)
Value classes (Project Valhalla, value class) are identity-free objects whose instances can be flattened in memory like primitives — eliminating heap allocation overhead for small, immutable data types.
String Templates (Preview)
String templates (JEP 430/459) embed expressions directly in strings with \{expr} syntax using a template processor — safer than String.format because the processor validates and transforms the result before it becomes a string.
Creating and Starting Threads
Java threads can be created by subclassing Thread or implementing Runnable; always call start() (not run()) to launch a new OS thread; daemon threads do not prevent JVM shutdown.
The synchronized Keyword
synchronized gives a thread exclusive access to a block or method using the object's intrinsic monitor lock; it also establishes a happens-before relationship, guaranteeing visibility of writes.
The volatile Keyword
volatile guarantees visibility (writes are immediately visible to all threads) and prevents instruction reordering — but it does not guarantee atomicity for compound operations like i++.
ExecutorService
ExecutorService decouples task submission from thread management; always use shutdown()+awaitTermination() or try-with-resources (Java 19+) to drain the queue gracefully, and prefer submit() over execute() to capture exceptions.
CompletableFuture
CompletableFuture enables non-blocking async pipelines: thenApply transforms the result, thenCompose chains async steps, allOf awaits multiple futures, and exceptionally handles failures.
Concurrent Collections
java.util.concurrent provides thread-safe collections: ConcurrentHashMap (lock-striped map), CopyOnWriteArrayList (read-heavy lists), and BlockingQueue (producer-consumer channel) — each with different performance profiles.
Locks and Conditions
ReentrantLock offers more flexibility than synchronized — timed and interruptible lock attempts, fairness, and Condition variables; ReadWriteLock allows concurrent reads; StampedLock adds optimistic reading.
Atomic Classes
AtomicInteger, AtomicLong, AtomicReference, and LongAdder provide lock-free atomic operations using hardware CAS (Compare-And-Swap) — more scalable than synchronized for simple counter/state updates.
ThreadLocal
ThreadLocal<T> gives each thread its own independent copy of a variable — useful for per-thread context like database connections or user sessions, but a memory-leak trap in thread-pool environments if not explicitly removed.
Deadlock Detection and Avoidance
Deadlock occurs when two or more threads each hold a lock the other needs; avoid it with consistent lock ordering, tryLock with timeout, or lock-free designs; detect it with jstack or JFR.
The Fork/Join Framework
ForkJoinPool and RecursiveTask/RecursiveAction implement divide-and-conquer parallelism using work stealing — idle threads steal tasks from busy threads' queues, maximising CPU utilisation for CPU-bound parallel computation.
Virtual Threads — Deep Dive: Pinning and Observability
Virtual threads are pinned to their carrier OS thread when blocking inside synchronized — preventing other virtual threads from running on that carrier; use ReentrantLock instead and observe pinning via JFR.
List Implementations Compared
ArrayList is the default general-purpose list (O(1) random access); LinkedList is a doubly-linked list (O(1) insert/remove at ends, O(n) access); Vector is legacy and synchronised — avoid it.
Set Implementations Compared
HashSet is the default O(1) unordered set; LinkedHashSet maintains insertion order; TreeSet maintains sorted order (O(log n)); all three have different null and thread-safety characteristics.
Map Implementations Compared
HashMap is the general-purpose O(1) map; LinkedHashMap preserves insertion (or access) order; TreeMap is sorted; Hashtable is legacy — avoid it; EnumMap and IdentityHashMap are specialised.
HashMap Internals
HashMap stores entries in an array of buckets; collision entries are stored in a linked list that treeifies to a red-black tree at 8+ entries (Java 8+); a good hashCode distribution minimises collisions.
Comparable vs Comparator
Comparable<T> defines a class's natural ordering (in-class); Comparator<T> defines an external, reusable ordering. Comparator.comparing(keyExtractor) and chained thenComparing enable fluent multi-key sorting.
The Collections Utility Class
java.util.Collections is a utility class of static methods for operating on collections: sorting, shuffling, searching, wrapping, and creating singleton/empty/unmodifiable views.
List.of(), Set.of(), Map.of() and copyOf()
List.of(), Set.of(), Map.of() (Java 9+) return truly immutable, null-rejecting collections; List.copyOf() makes an immutable snapshot of an existing collection.
Deque and Stack
Deque (double-ended queue) can be used as both a stack (LIFO) and a queue (FIFO); prefer ArrayDeque over LinkedList for better performance; avoid the legacy Stack class entirely.
PriorityQueue
PriorityQueue is a heap-based priority queue: poll() always removes the smallest element (or the comparator-minimum); add/offer is O(log n); peek is O(1); iteration order is not sorted.
Iterator and ConcurrentModificationException
Iterator allows safe traversal and removal; ListIterator adds bidirectional traversal; modifying a collection while iterating via for-each throws ConcurrentModificationException — use iterator.remove() or removeIf() instead.
Checked vs Unchecked Exceptions
Checked exceptions must be declared or handled at compile time; unchecked exceptions (RuntimeException and its subclasses) do not. The design tradeoff: checked exceptions force callers to consider failure modes but pollute APIs.
Try-with-Resources
try-with-resources automatically closes AutoCloseable resources in reverse declaration order, even if an exception is thrown; exceptions from close() are suppressed and accessible via getSuppressed().
The Exception Hierarchy
Throwable is the root; Error represents unrecoverable JVM failures (don't catch); Exception is the recoverable root; RuntimeException is the unchecked subclass. Know where to catch and what to rethrow.
Custom Exceptions
Create custom exceptions by extending Exception (checked) or RuntimeException (unchecked); always provide a message constructor and a cause-preserving constructor; keep exception class names clear and domain-specific.
Multi-catch and Rethrowing
Multi-catch catch (A | B e) handles unrelated exceptions together; rethrowing after catching allows logging + propagation; precise rethrow type inference (Java 7+) lets you declare only the specific thrown types even when rethrowing from a broad catch.
Optional vs Exceptions
Optional models the absence of a value as a return type — it is not a substitute for exceptions that report genuine failures; use Optional for expected absence (not-found), and exceptions for unexpected errors.
Singleton Pattern
The Singleton pattern ensures a class has exactly one instance; the enum singleton is the safest Java implementation — it's thread-safe, serialization-safe, and immune to reflection attacks.
Java 21 (core language)Builder Pattern
The Builder pattern solves the telescoping-constructor problem by providing a fluent API to assemble complex objects step-by-step; in Java 16+, records and their wither pattern reduce the need for Builders on simple immutable types.
Java 21 (core language)Factory Method Pattern
Factory methods decouple object creation from clients — they can return subtype instances, cache instances, or vary the concrete type — overcoming the limitations of constructors (List.of(), Optional.of() are classic examples).
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable; in modern Java, functional interfaces and lambdas make Strategy trivial — Comparator and Predicate are canonical examples.
Observer Pattern
The Observer pattern defines a one-to-many dependency so that when one object changes state, all its dependents are notified; java.util.Observable is deprecated — use event listeners, PropertyChangeSupport, or a reactive library.
Decorator Pattern
The Decorator pattern wraps an object with additional behaviour by implementing the same interface and delegating to the wrapped object — the java.io stream hierarchy is the canonical Java example.
Dependency Injection Basics
Dependency Injection (DI) supplies objects with their collaborators from outside rather than creating them internally — making code testable, loosely coupled, and easy to reconfigure; constructor injection is the preferred form.
Java 21 (core language)SOLID Principles
SOLID is five object-oriented design principles — Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion — that guide writing maintainable, extensible Java code.
Java 21 (core language)The Integer Cache Trap
Integer.valueOf(n) caches instances for n in [-128, 127] — so == returns true in that range but false outside it; always use .equals() to compare Integer objects.
String Concatenation in a Loop
Using + to concatenate strings inside a loop is O(n²) because each + creates a new String; use StringBuilder explicitly in complex cases, or String.join() / Collectors.joining() for collections.
Floating-Point Equality
Never use == to compare double or float values; 0.1 + 0.2 != 0.3 because of IEEE 754 binary representation; use a tolerance comparison or BigDecimal for exact decimal arithmetic (e.g., money).
Null Pointer Exceptions in Modern Java
Java 14+ shows exactly which variable was null in NPE messages; use Objects.requireNonNull, Optional, or @NonNull annotations to prevent NPEs early; avoid returning null from methods.
Serialization Pitfalls
Java serialization is dangerous: missing serialVersionUID causes InvalidClassException on class changes; transient fields are skipped; deserialization of untrusted data is a remote code execution vulnerability.
Covariant Array Trap
Java arrays are covariant (String[] is an Object[]), meaning type-unsafe assignments compile but throw ArrayStoreException at runtime; generics are invariant and catch these errors at compile time.
Interface Default Method Diamond Problem
When a class inherits two interfaces with conflicting default methods, it must override to resolve the ambiguity — the compiler rejects the ambiguity; a superclass method always wins over an interface default.
Checked Exceptions in Streams and Lambdas
Functional interfaces in java.util.function do not declare checked exceptions, so lambdas that throw checked exceptions don't compile; the fix is a wrapper method, a sneaky-throw helper, or switching to unchecked exceptions.
Map Compute Patterns vs get-null-put
Replace verbose get-null-check-put patterns with computeIfAbsent, computeIfPresent, compute, merge, and getOrDefault — they are atomic on ConcurrentHashMap and more readable everywhere.
JVM Runtime Data Areas
The JVM divides memory into the Heap, Metaspace, JVM Stacks, Program Counter (PC) Registers, and Native Method Stacks; understanding their boundaries is essential for tuning and diagnosing OutOfMemoryErrors.
Java 21+ (HotSpot architecture)Garbage Collectors in Modern Java
Modern Java features multiple production garbage collectors: G1GC (default throughput/pause balance), Generational ZGC (sub-millisecond pauses at terabyte scale), and Shenandoah (concurrent compacting).
Java 21+ (G1GC, ZGC, Shenandoah)Java Memory Model & Happens-Before Guarantee
The Java Memory Model (JMM) defines how threads interact through shared memory; the happens-before relationship guarantees that memory writes by one thread are visibly observed by another without instruction reordering.
Java 21 (JLS Chapter 17)ClassLoader Hierarchy and Dynamic Loading
Java loads compiled classes dynamically via a hierarchical ClassLoader delegation model (Bootstrap, Platform, Application), maintaining security, isolation, and modular runtime boundaries.
Java 21+ (JPMS integration)JIT Tiered Compilation and Escape Analysis
HotSpot executes bytecode through an interpreter and tiered JIT compilers (C1 client compiler for rapid startup, C2 server compiler for aggressive peak optimization), leveraging runtime profiling and escape analysis.
Java 21 (HotSpot C1 & C2 compilers)Java Flight Recorder (JFR) & Low-Overhead Production Profiling
Java Flight Recorder (JFR) is an always-on, low-overhead (<1% CPU) event tracing and profiling framework integrated into the JVM kernel, paired with JDK Mission Control (JMC) for diagnostics.
Java 21+ (Built into OpenJDK)GraalVM Native Image vs HotSpot JIT
GraalVM Native Image compiles Java bytecode ahead-of-time (AOT) into standalone native machine binaries, achieving instant startup (<10ms) and minimal memory footprints at the expense of dynamic runtime optimizations.
Java 21+ (GraalVM CE & Oracle GraalVM)Java Platform Module System (JPMS)
Project Jigsaw introduced JPMS and module-info.java, replacing brittle classpaths with explicit module dependencies, strong encapsulation, and reflection barriers.
Reference Types: Strong, Soft, Weak, and Phantom
Java provides four reference strengths (Strong, Soft, Weak, Phantom) allowing developers to cooperate with the garbage collector for caches, canonical mappings, and resource cleanup.
Java 21 (java.lang.ref package)Safe Publication & Thread-Safe Object Initialization
Publishing an object reference across threads without proper synchronization exposes partially initialized state; understand the four safe publication idioms guaranteed by the Java Memory Model.
Java 21 (JLS §17.5 Final Field Semantics)Unnamed Variables & Patterns
The single underscore _ denotes an unnamed variable or pattern in Java 22+, signaling intentionally unused parameters in lambdas, exception handlers, try-with-resources, and record deconstruction.
Flexible Constructor Bodies: Statements Before super()
Flexible constructor bodies allow statements to precede super(...) or this(...), permitting argument validation, preprocessing, and defensive copies before invoking superclass constructors.
Stream Gatherers: Custom Intermediate Operations
Stream Gatherers enhance the Java Stream API with user-defined and built-in intermediate operations like windowFixed, windowSliding, fold, and scan, eliminating the historic limitations of the Stream pipeline.
Foreign Function & Memory API (FFM)
The Foreign Function & Memory (FFM) API replaces brittle JNI with safe off-heap memory management (Arena, MemorySegment) and direct native C library invocations.
Module Import Declarations
import module java.base; imports all public packages exported by an entire module in a single line, vastly streamlining beginners, scripts, and modern microservices.
Primitive Types in Patterns, instanceof, and switch
Java 23 expands pattern matching to all primitive types, permitting instanceof int, primitive deconstruction, and type-safe range matching in switch.
Class-File API: Parsing and Generating Bytecode
The JDK Class-File API provides a standard, evolution-proof library for parsing, generating, and transforming JVM class files, replacing third-party dependencies like ASM and Byte Buddy.
Java 24 (JEP 484 Final)Concurrency Synchronizers: CountDownLatch, CyclicBarrier, and Semaphore
CountDownLatch (one-shot countdown), CyclicBarrier (reusable thread rendezvous), and Semaphore (permit-based rate limiter) solve thread coordination scenarios without manual wait/notify locks.
Java 21 (java.util.concurrent)CopyOnWriteArrayList vs Collections.synchronizedList
CopyOnWriteArrayList optimizes lock-free reads by cloning the backing array on every write, while Collections.synchronizedList serializes all operations behind a single mutual-exclusion lock.
NavigableMap & NavigableSet: Submaps, Floor, and Ceiling
NavigableMap and NavigableSet extend SortedMap/SortedSet with closest-match queries (floor, ceiling, lower, higher) and flexible range views (subMap, headMap, tailMap).
Math.clamp() and Exact Arithmetic in Java
Math.clamp() safely bounds values within a range, while Math.addExact() and multiplyExact() prevent silent integer overflow by throwing ArithmeticException.
Template Method vs Strategy Pattern
Template Method uses inheritance to fix an algorithm skeleton with abstract hook steps; Strategy uses composition and functional interfaces to swap interchangeable algorithms at runtime.
Java 21 (Modern idiomatic patterns)Double-Checked Locking Trap and Broken Idioms
Double-checked locking without the volatile keyword is broken due to instruction reordering; understand the CPU memory barrier mechanics and why the Initialization-on-Demand Holder idiom is cleaner.