Skip to content

Java Programming

170 companion flashcards · AI-assisted study content · Open the deck →

This deck covers the core building blocks of Java programming, from understanding the runtime environment (the JVM, JDK, and JRE) to the foundational concepts of object-oriented programming. You'll work through the four pillars of OOP — encapsulation, inheritance, polymorphism, and abstraction — along with practical language features like constructors, access modifiers, the static and final keywords, method overloading and overriding, and how abstract classes differ from interfaces.

It's a great fit if you're just starting out with Java and want a clear, structured way to learn the essentials, or if you're preparing for a technical interview and need to refresh key definitions and distinctions. Intermediate learners returning to Java after time away will also find it useful for tightening up terminology and filling in gaps in their understanding of how the language is organized.

To get the most out of these cards, try working through a small batch at a time rather than tackling everything in one sitting. Since many of the concepts build on each other — like understanding access modifiers before diving into encapsulation, or grasping overloading before overriding — reviewing earlier topics briefly before moving forward will help the later material click into place. Mixing in some hands-on coding alongside the flashcards is a great way to reinforce what you're learning.

Java Platform and Memory Model

Java's reputation rests on the famous "Write Once, Run Anywhere" principle, and that promise is delivered by a layered software stack. The Java Virtual Machine (JVM) is an abstract computing machine that executes Java bytecode rather than native machine instructions. When you compile a Java source file with javac, you produce .class files containing platform-neutral bytecode. The JVM, sitting between the bytecode and the underlying operating system, translates instructions into native code at run time. The three-tier JDK ⊃ JRE ⊃ JVM relationship makes the division of responsibilities clear: the JDK (Java Development Kit) bundles the compilers and tools developers need, the JRE (Java Runtime Environment) supplies the standard libraries and JVM needed to run applications, and the JVM is the runtime engine that actually executes bytecode.

Internally, the JVM divides memory into regions with very different performance characteristics. Every thread gets its own LIFO stack of stack frames, where local variables (primitives and object references) and the operand stack live; stack allocation is extremely fast and memory is freed automatically when a method returns. All objects and class metadata live in the heap, which is shared and garbage-collected, organized into generational regions (Eden, Survivor, Old/Tenured) under the HotSpot JVM. Beyond heap and stacks, HotSpot reserves Metaspace (native memory for class metadata, replacing PermGen in Java 8), a code cache for JIT-compiled native instructions, direct memory used by NIO ByteBuffer.allocateDirect for off-heap allocations, and compressed oops that let 64-bit JVMs use 32-bit object references when the heap is under 32 GB. Stack size is tuned with -Xss, while heap size is bounded by -Xms and -Xmx.

Garbage collection is the JVM's automatic mechanism for reclaiming heap memory held by unreachable objects. Objects become GC-eligible when no live reference points to them, but the JVM provides only one non-deterministic knob (System.gc() is a hint, not a command). Different collectors serve different service-level agreements: Parallel GC maximizes throughput at the cost of stop-the-world pauses, G1 (the default since Java 9) balances latency and throughput by collecting regions concurrently toward a target pause time (-XX:MaxGCPauseMillis), and ZGC (since Java 11) achieves sub-millisecond pauses regardless of heap size, making it ideal for very large multi-GB heaps. The legacy Object.finalize() method, called at most once before an object is reclaimed, has been deprecated for removal; modern code prefers try-with-resources for deterministic cleanup and java.lang.ref.Cleaner (Java 9+) for cleaning off-heap resources via a daemon-thread action registered against the target object.

To balance memory pressure with caching, Java defines four reference strengths. Strong references are the normal Object o = new Object() form, which keep objects reachable and prevent collection. SoftReference objects are cleared only when the JVM is under memory pressure, making them well suited to memory-sensitive caches. WeakReference objects are cleared at the next garbage collection cycle; the canonical use is WeakHashMap, where an entry vanishes automatically once its key is no longer strongly reachable (though the value's strong reference to back-data must be managed carefully to avoid resurrection). PhantomReference objects are enqueued after the object's finalizer has run but before the memory is reclaimed, and are used to schedule post-mortem cleanup actions. The execution model itself also adapts at run time: hot methods are detected by the JIT compiler (C1/C2 in HotSpot, Graal in newer versions) and translated into native machine code, with tiered compilation balancing fast startup against steady-state speed, while AOT compilation via GraalVM Native Image produces standalone binaries with near-instant startup and lower memory at the cost of slower peak CPU and less reflection-friendly behavior. A .class file's structure mirrors this duality: starting with the magic 0xCAFEBABE, it carries minor/major version, a constant pool of UTF-8 literals and references, access flags, this/super class pointers, interface list, fields, methods, and attributes (code, line numbers, generic signatures, annotations) – the binary contract that the JVM parses on load.

Core Language Features

Java's type system rests on eight primitive data types that map directly to hardware: byte (8-bit), short (16-bit), int (32-bit), long (64-bit), float (32-bit IEEE 754), double (64-bit IEEE 754), char (16-bit unsigned Unicode), and boolean. Primitives are stored on the stack when local, have no identity or methods, and are not objects. When object behavior is required, the compiler will automatically box a primitive into its wrapper type (autoboxing: Integer i = 42;) and unbox it again on use (int n = i;). The compiler inserts Integer.valueOf() and intValue() calls under the hood, with one important caveat: unboxing a null wrapper throws NullPointerException, and for performance the IntegerCache keeps small values (\([-128, 127]\)) as singletons. Beyond wrappers, the int[] vs Integer[] distinction matters for performance: int[] stores raw 32-bit values contiguously with no boxing overhead, while Integer[] stores object references suitable for generics and reflection at the cost of heap allocation per element.

The String class is one of the most distinctive parts of the language. String is deliberately immutable—every modification returns a new String—and this design choice provides security (strings used in class loading, networking, and file paths cannot be tampered with), thread safety without synchronization, hash code caching, and the String Pool, a special heap region where the JVM interns string literals to save memory. When you write String s = "hello";, the JVM checks the pool first and reuses existing instances; the explicit intern() method adds a string to the pool. For mutable text manipulation, Java provides StringBuilder (fast, not thread-safe, the default for single-threaded code) and StringBuffer (synchronized, thread-safe, slower). Strings should always be compared with .equals() for value equality rather than ==, which compares reference identity: \( \texttt{new String("hi") == new String("hi")} \) is false even though both hold the same characters. The classic equality contract requires that x.equals(y) ⇒ x.hashCode() == y.hashCode(); otherwise HashMap and HashSet silently lose entries.

A handful of keywords shape class-level structure. The static keyword binds a member to the class rather than to any instance: static variables are shared across all objects, static methods can be called without creating an instance (as with Math.sqrt(25)), and static initialization blocks run exactly once when the class is loaded into the JVM. The final keyword has three flavors: a final variable cannot be reassigned (a constant), a final method cannot be overridden by subclasses, and a final class cannot be extended. Access modifiers control visibility along a four-step spectrum: public (everywhere), protected (same package plus subclasses), default/package-private (same package only, no keyword needed), and private (the declaring class only). The this and super keywords are essential for navigating class internals: this refers to the current object instance and is used to disambiguate fields from parameters (this.name = name), to invoke another constructor in the same class as this(args), and to pass the current object as an argument; super refers to the parent class and lets a constructor call its parent's constructor as super(args), invoke an overridden parent method via super.methodName(), or access a parent field that the child has hidden.

Finally, packages group related classes and interfaces into namespaces—declared with a package statement at the top of the file—avoiding naming collisions, controlling package-private visibility, and providing modular structure for larger code bases. Java also distinguishes several flavors of nested classes: a nested static class does not capture an outer instance and behaves like a top-level type; an inner (non-static) class holds an implicit reference to its enclosing instance and can access all of its members; a local class is declared inside a method and can access effectively-final locals; and an anonymous class is a one-off local class without a name. Lambdas replace most uses of anonymous classes for functional interfaces.

Object-Oriented Programming

Object-oriented design in Java rests on four pillars. Encapsulation bundles an object's data and methods together, restricting direct access by marking fields private and exposing them through public getter and setter methods; this protects the internal state and enforces data hiding. Inheritance lets one class acquire the properties and methods of another using the extends keyword, enabling code reuse and polymorphism along an "is-a" hierarchy (class Dog extends Animal). Polymorphism, literally "many forms," appears in two flavors: compile-time polymorphism via method overloading (same name, different parameter lists) and runtime polymorphism via method overriding (a subclass providing its own implementation of an inherited method), resolved through dynamic method dispatch. Abstraction hides implementation complexity and shows only the essentials, achieved through abstract classes (declared with the abstract keyword, capable of holding both abstract and concrete methods) and interfaces (a pure contract of abstract methods).

The distinction between abstract classes and interfaces has narrowed over time but remains important. An abstract class can declare constructors, hold instance variables, and mix abstract with concrete methods; it uses the extends keyword and supports only single implementation inheritance. An interface, by contrast, traditionally defined only abstract methods (implicitly public and abstract before Java 8), supported multiple inheritance through implements, and was the canonical way to express capabilities like Serializable. Since Java 8, interfaces can declare default methods (with bodies, allowing new interface methods to be added without breaking implementers) and static methods, which has shifted the design calculus: choose an abstract class when you need shared state or constructors, choose an interface when you need to express a capability without coupling to a particular base class. A default method is not the same as an abstract method – the former has a body and provides a fallback implementation; the latter has no body and forces implementing classes to provide one.

Constructors are special methods that initialize new objects. A constructor carries the same name as its class and has no return type; if you do not write one, the compiler inserts a default no-arg constructor that simply chains to the superclass. Constructors commonly accept parameters and use this(args) to delegate to another constructor in the same class. Method overloading declares multiple methods with the same name but different parameter lists (different number, type, or order of parameters), and the compiler picks the right one at compile time. Method overriding provides a specific subclass implementation of an inherited method with an identical signature, conventionally annotated with @Override so the compiler can verify the intent and prevent silent signature mismatches.

Java adds several modern features that complement classical OOP. An enum is a special implicitly final class that represents a fixed set of named constants; it can carry fields, declare an (always-private) constructor, implement interfaces, and override abstract methods per constant to express per-constant behavior. Records (Java 16+) are compact, immutable data classes whose compiler-generated constructor, accessors (named after the components, without "get"), equals, hashCode, and toString replace dozens of lines of boilerplate—ideal for DTOs and value objects. Sealed classes (Java 17+) restrict who can extend them via permits, enabling exhaustive pattern matching in switch expressions; permitted subclasses must be declared final, sealed, or non-sealed, all within the same module or package. Finally, while Java supports single implementation inheritance, it permits multiple inheritance of interface types, which can surface the diamond problem when two interfaces supply default methods of the same signature; the implementing class must then override the conflicting method and can call a specific super's version using the new InterfaceA.super.method() syntax. The marker interface pattern—an empty interface like java.io.Serializable or java.lang.Cloneable—supplies metadata the runtime checks via instanceof; since Java 5 the same role is often served by marker annotations such as @Deprecated or @Override, processed via reflection.

Generics and the Collections Framework

Generics let classes, interfaces, and methods operate on parameterized types, providing compile-time type safety and eliminating most casts. List<String> names = new ArrayList<>() lets the compiler reject a stray add(42) at compile time instead of producing a ClassCastException at run time. The diamond operator <> lets the compiler infer type arguments from the left-hand side, avoiding the redundancy of new ArrayList<String>() on the right, and since Java 9 the diamond is also permitted when creating anonymous inner classes. Type erasure is the process by which the Java compiler removes generic type information at compile time, replacing type parameters with their bounds or Object; List<String> and List<Integer> therefore reduce to the same List class at run time, ensuring backward compatibility with pre-generics code. Heap pollution occurs when a non-reifiable varargs array (e.g. T...) is stored, returned, or iterated unsafely, mixing the array's runtime component (Object[]) with the compile-time type; @SafeVarargs (Java 7+) asserts that a method's varargs array will not be exposed to heap pollution, applied to static, final, or (since Java 9) private methods, while @SuppressWarnings("unchecked") silences a specific compiler warning at a particular site.

Variance is expressed through bounded wildcards. PECS—the Producer Extends, Consumer Super rule—summarizes the discipline: when a method only reads from a parameter that is a producer, use ? extends T; when a method only writes into a parameter that is a consumer, use ? super T. The standard copy(List<? extends T> src, List<? super T> dest) pattern reads from src and writes to dest while keeping both signatures flexible. Upper-bounded type parameters like <T extends Number> restrict which types can stand in for T to Number or its subclasses; the unbounded wildcard <?> simply accepts any type. Raw types—using a generic class without its type arguments, like List instead of List<String>—disable the compiler's checks and are kept only for backward compatibility; always use parameterized types instead. The Collections Framework provides a unified architecture for storing and manipulating groups of objects: List supports ordered, indexable sequences that allow duplicates (ArrayList, LinkedList); Set forbids duplicates (HashSet, TreeSet); Map associates keys with values (HashMap, TreeMap, LinkedHashMap); and Queue orders elements in FIFO (PriorityQueue, LinkedList as a queue).

ArrayList is backed by a dynamic array—\(O(1)\) random access but \(O(n)\) insertions/deletions in the middle—while LinkedList is a doubly-linked list with \(O(1)\) head/tail mutations and \(O(n)\) random access; LinkedList also implements Deque. HashMap and HashSet back themselves with a hash table offering \(O(1)\) average lookups and no ordering; TreeMap and TreeSet use red-black trees giving \(O(\log n)\) operations and sorted traversal by natural order or custom Comparator. HashMap's internals illustrate how the framework trades simplicity for performance: an array of buckets, a key's hashCode() determines its bucket index, collisions are resolved with linked lists (and, since Java 8, with balanced trees when a bucket exceeds eight entries), and the load factor of 0.75 controls when the table resizes. LinkedHashMap augments a hash table with a doubly-linked list to preserve insertion order; in access-order mode it powers LRU caches via the removeEldestEntry override. ConcurrentHashMap replaces the global locks of legacy Hashtable with fine-grained bucket locks (and CAS for reads since Java 8) while providing weakly consistent iterators that never throw ConcurrentModificationException. EnumSet and EnumMap deserve special mention: they back themselves with single-word bitmasks and ordinal-indexed arrays respectively, giving them enormous speed and memory advantages whenever the keys are enum constants.

The contract between equals() and hashCode() is the cornerstone of correct collection behavior. The contract demands reflexivity, symmetry, transitivity, consistency, and that x.equals(null) is false; critically, if x.equals(y) returns true, then x.hashCode() must equal y.hashCode()—violating this rule causes entries to be lost in HashMap, HashSet, and Hashtable. For ordering, Comparable<T> defines natural order via compareTo(T o), while Comparator<T> defines custom order external to the class, supports multiple sorting strategies with thenComparing, and is the right choice when you cannot modify the class. For sorted collections, Comparable.compareTo() recommends (does not require) consistency with equals: a TreeSet will not deduplicate objects that compareTo to zero but differ by equals, while a HashSet would—precisely the famous "BigDecimal with different scale" gotcha. The Iterator interface provides hasNext(), next(), and remove(), and is the only safe way to mutate a collection while iterating; iterators are either fail-fast (throw ConcurrentModificationException on structural modification, as ArrayList and HashMap) or weakly consistent (work on a snapshot, as ConcurrentHashMap and CopyOnWriteArrayList). Two idioms dominate comparison choices in collections: List.sort(Comparator) mutates a list in place and is usually faster, while stream.sorted() fits inside larger pipelines and never modifies the source. The Map.compute family (computeIfAbsent, computeIfPresent, merge) handles atomic conditional updates, and modern conveniences such as removeIf (mutates in place, no allocation), Predicate.not(...), Map.getOrDefault, and ConcurrentHashMap's atomic operations let you avoid the classic get-then-check null-then-put pattern. Helper factories likewise have subtle differences: subList returns a view onto the original list (mutations are shared and structural changes outside the view are undefined), new ArrayList(collection) creates an independent copy, Arrays.asList(array) returns a fixed-size view backed by the original array (set() works, add()/remove() throw), and List.of(...) returns a truly immutable list (no nulls, may intern duplicates).

Functional Programming and the Streams API

The arrival of lambdas in Java 8 transformed the language from verbose anonymous classes into concise functional expressions. A lambda implements a functional interface—an interface with exactly one abstract method (SAM)—using the syntax (parameters) -> expression. Common built-in functional interfaces include Predicate<T> (T \(\to\) boolean), Function<T,R> (T \(\to\) R), Consumer<T> (T \(\to\) void), and Supplier<T> (() \(\to\) T). The @FunctionalInterface annotation is informative, not required, but it documents the SAM contract and ensures maintainers do not accidentally break it by adding a second abstract method. Default methods, static methods, and methods inherited from Object do not count toward the SAM count, so interfaces like Predicate<T> (with one abstract test plus a default and(...)) remain functional. Lambdas differ from anonymous inner classes: a lambda targets a SAM type via an invokedynamic call site that often avoids generating an extra class file, while an anonymous inner class creates a fresh named subclass at compile time and can extend a class or implement multiple abstract methods.

Method references are shorthand for lambdas that simply call an existing method. There are four kinds: static references like Math::sqrt (for x \(\to\) Math.sqrt(x)), instance references on a specific object like System.out::println (for x \(\to\) System.out.println(x)), instance references on an arbitrary object of a given type like String::length (for s \(\to\) s.length()), and constructor references like ArrayList::new (for () \(\to\) new ArrayList()). The same thinking underlies CompletableFuture's thenApply vs thenCompose: thenApply is the synchronous map (Function<T, U> \(\to\) CompletableFuture<U>), thenCompose is the asynchronous flatMap (Function<T, CompletionStage<U>> \(\to\) CompletableFuture<U>) that avoids CompletableFuture<CompletableFuture<U>>. Use thenCompose whenever the next step itself returns a future; use thenApply for synchronous value transformations.

The Stream API processes collections of objects in a functional style. A stream is a sequence of elements drawn from a source (a collection, array, generator, or I/O channel), supports lazy evaluation, and does not modify the source. Intermediate operations (filter, map, sorted, distinct, limit) return a new stream and do no work until a terminal operation triggers execution; terminal operations (collect, forEach, reduce, count, findFirst) produce a result or side effect. The map vs flatMap distinction mirrors the same idea in Optional and CompletableFuture: map applies a one-to-one transformation, while flatMap applies a one-to-many transformation that returns a Stream<R> and then flattens it—essential for splitting strings into words, optional unwrapping, or nested collections.

A few of the Stream API's finer points matter in production. findFirst() returns the first element in encounter order, while findAny() returns any element and may be non-deterministic on a parallel stream but is faster in exchange. Collection.stream() processes sequentially on the calling thread; Collection.parallelStream() splits the work across the common ForkJoinPool and only pays off for CPU-bound, stateless operations on large datasets. Spliterator is the internal traversal object that powers streams, reporting characteristics such as ORDERED, DISTINCT, SORTED, SIZED, and IMMUTABLE that help parallelization. The Collectors.toMap(keyMapper, valueMapper) convenience method throws IllegalStateException on duplicate keys and must be configured with a merge function or a custom map supplier; Collectors.joining(delimiter, prefix, suffix) builds a single string via an internal StringJoiner and produces prefix + suffix for empty streams. peek is an intermediate operation intended only for debugging and must not be used for side effects in production; forEach is the terminal counterpart guaranteed to run. Streams integrate with primitives through specialized streams: mapToInt produces an IntStream offering autoboxing-free sum(), average(), summaryStatistics(), and IntStream.range(start, end) (exclusive) vs rangeClosed (inclusive) factories. reduce(identity, accumulator) folds a stream into a single value with an associative operator; collect(supplier, accumulator, combiner) accumulates into a mutable container that can be merged for efficient parallel collection. Because the built-in functional interfaces forbid checked exceptions, lambdas that need to throw IOException cannot compile directly; the standard workarounds are catching and rethrowing as a runtime exception, defining a custom functional interface that declares throws Exception, or using a Lombok @SneakyThrows for the desperate. Java 11 added Predicate.not(p) as a static factory—equivalent to p.negate() but more useful with method references (e.g., list.removeIf(Predicate.not(String::isBlank))).

Concurrency and the Java Memory Model

Java supports concurrent execution of multiple threads within a single program. Threads can be created by extending Thread and overriding run(), by implementing the Runnable interface, or by submitting tasks to an ExecutorService—now the preferred approach in production code. Calling thread.start() hands the new thread off to the OS scheduler; calling thread.run() directly executes the body in the current thread and defeats the purpose. The Runnable and Callable interfaces look similar but differ in two important ways: Runnable.run() returns void and cannot throw checked exceptions, while Callable<V>.call() returns a value and is allowed to throw checked exceptions, returning a Future<V> when submitted to an executor. The functional trio Runnable/Supplier/Callable can also be summarized: Runnable is no-args/no-return/throws-anything, Supplier is no-args/returns-T/no-checked-exceptions, Callable is no-args/returns-V/throws-checked.

Shared state is the central hazard of multithreading. The synchronized keyword ensures that only one thread at a time can execute a method or block by acquiring the object's intrinsic lock (monitor). The volatile keyword provides a different guarantee: a volatile variable is always read from and written to main memory rather than a thread's local CPU cache, giving cross-thread visibility without acquiring a lock. volatile is appropriate for simple flags such as private volatile boolean running = true; it does not, however, guarantee atomicity for compound operations like counter++. For those, use AtomicInteger, AtomicLong, or AtomicReference, which leverage CPU compare-and-swap (CAS) instructions to provide lock-free, thread-safe updates via incrementAndGet(), compareAndSet(), and Java 8's accumulateAndGet(). Note that writes and reads of long and double are not guaranteed atomic without a modifier—the JVM may split a 64-bit value into two 32-bit operations, allowing a thread to read a torn value—so declare them volatile or guard them under synchronized.

The Java Memory Model (JSR-133) defines how threads interact through memory and is the foundation on which all this rests. Three guarantees matter: atomicity (reads and writes of 32-bit values and references are atomic, but long/double may be torn unless declared volatile or guarded by synchronized); visibility (writes by one thread must be seen by another, established by volatile, synchronized, or final fields under proper construction); and ordering (the happens-before relationship, which states that if action A happens-before B, then A appears to execute before B and the result is visible to B). Sources of happens-before include a thread's own actions before its Thread.join() returns, volatile writes before subsequent reads of the same variable, monitor unlocks before subsequent locks of the same monitor, and actions in a thread before any thread returning from Thread.start() on that thread.

For more sophisticated locking, the java.util.concurrent.locks package offers ReentrantLock with tryLock(), timed acquisition, and multiple Condition variables; ReentrantReadWriteLock allows concurrent readers but exclusive writers, ideal for read-mostly data; and StampedLock (Java 8+) adds an optimistic read mode that returns a stamp to validate later, providing extremely fast lock-free reads (with the caveat that StampedLock is not reentrant). For coordination among many threads, java.util.concurrent ships higher-level utilities: CountDownLatch waits for a fixed number of operations to complete and is single-use, CyclicBarrier makes N threads meet before proceeding and is reusable, Phaser supports dynamic party counts across phases, BlockingQueue implements the classic producer-consumer pattern via put()/take() that block on capacity (ArrayBlockingQueue is bounded and array-backed, LinkedBlockingQueue is optionally bounded with linked nodes, PriorityBlockingQueue is unbounded and ordered, SynchronousQueue hands off directly between threads with zero capacity, and DelayQueue releases elements only after a delay), and Semaphore maintains permits for resource pooling or mutual exclusion (one permit ≈ a lock, but acquisition/release can cross threads since the semaphore has no ownership).

The producer-consumer pattern, where producers block on a full queue and consumers block on an empty queue, is the backbone of work distribution in the JVM. It naturally provides backpressure and is exactly how ExecutorService work queues operate. The Fork/Join framework (Java 7+) uses a work-stealing ForkJoinPool to parallelize divide-and-conquer tasks; extending RecursiveTask<V> or RecursiveAction and calling fork() on subtasks and join() on results is the standard recipe, and parallelStream() uses the common pool by default. ExecutorService is the higher-level replacement for manual thread management: it manages a thread pool (FixedThreadPool, CachedThreadPool, SingleThreadExecutor via the Executors factory), and the configurable ThreadPoolExecutor exposes the seven core parameters (corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler) that govern how tasks are accepted and rejected. CompletableFuture<T> extends Future<T> and implements CompletionStage, supporting fully composable asynchronous pipelines: supplyAsync for kicking off work, thenApply/thenAccept/thenRun for transformations, thenCompose for flattening nested futures, thenCombine for joining two independent futures, exceptionally and handle for error recovery, and allOf/anyOf for aggregating many futures. Best practice dictates always shutting down ExecutorServices gracefully (shutdown, awaitTermination, shutdownNow, awaitTermination again) to release non-daemon threads, and prefer higher-level utilities like BlockingQueue over raw wait()/notify()—which require monitor ownership, are easy to misuse with lost wakeups, and lack fairness. Concurrent collections deserve the final note: CopyOnWriteArrayList rebuilds the array on every mutation and is wait-free for readers (best for read-mostly lists like listener registries), Collections.synchronizedList wraps a list with coarse-grained locking, and ConcurrentLinkedQueue provides a non-blocking MPMC queue for producer/consumer throughput.

Exception Handling, I/O, and Serialization

Java's exception model centers on the Throwable hierarchy. Throwables split into Error (serious, unrecoverable conditions such as OutOfMemoryError and StackOverflowError, which applications should not catch) and Exception (recoverable conditions). Exceptions are further divided into checked exceptions (subclasses of Exception other than RuntimeException, which must be either caught or declared in the method signature with throws) and unchecked exceptions (RuntimeException and its subclasses, which do not require explicit handling). Custom exceptions extend Exception or RuntimeException as appropriate and are thrown with throw new MyException("reason"), with the calling method declaring them via throws. When looking at a stack trace, the "Caused by" chain shows the root cause; the last "Caused by" is the original root cause and the first exception is what the application directly saw. The two keywords surrounding exception control are easily confused: throw is the statement that actually raises an exception, while throws is a method-signature clause that declares which exception types a method may propagate. Catching Throwable or Error is a code smell—the JVM is usually in an unstable state and you should let it report and die.

The try-with-resources statement (Java 7+) automatically closes any number of resources that implement AutoCloseable, even when exceptions are thrown during opening or use. Resources are closed in reverse order of declaration in a finally-equivalent block, and exceptions raised while closing one resource are attached to the primary exception via Throwable.addSuppressed() and retrieved with getSuppressed(). The three homonyms final/finally/finalize are similarly easy to mix up: final is the unchangeable modifier, finally is the block that always runs after a try/catch for cleanup (skipped only if the JVM exits via System.exit() or the thread is killed), and finalize is the deprecated legacy method Object.finalize() called by the garbage collector. Functional interfaces add a wrinkle to checked exceptions: because Supplier, Function, Consumer, and Predicate do not declare throws Exception, a lambda body that throws a checked exception (such as IOException from Files.lines) will not compile directly. The standard workarounds are catching and rethrowing as a runtime exception, defining a custom functional interface that declares throws Exception, or using a helper such as Lombok's @SneakyThrows.

Java's I/O libraries have evolved through three generations. BIO (blocking I/O) in java.io is the simple, classic model: one thread per socket, easy to reason about, but limited to a few hundred simultaneous connections. NIO (Java 1.4+) introduced non-blocking channels, buffers, and Selectors that let a single thread multiplex many connections—it is the foundation of high-performance servers like Netty and gRPC. NIO.2 (Java 7+) added true asynchronous I/O (AsynchronousChannel, AsynchronousFileChannel), the modern Path and Files APIs, and the WatchService. The class hierarchy splits into byte streams (InputStream/OutputStream for binary data) and character streams (Reader/Writer for text, with explicit charset control via StandardCharsets.UTF_8 to avoid platform-dependent bugs). BufferedInputStream wraps an InputStream for raw bytes and supports mark/reset; BufferedReader wraps a Reader and adds readLine() for text.

The NIO ByteBuffer is the fundamental primitive of high-throughput I/O, available in two flavors. ByteBuffer.allocate(n) creates a buffer backed by a byte[] on the JVM heap—fast to allocate and freed by GC, but the bytes must be copied when handed to native channels. ByteBuffer.allocateDirect(n) creates an off-heap buffer that can be passed to native channels without copying, ideal for high-throughput networking, at the cost of more expensive allocation and Cleaner-based reclamation. The Selector ties it together: opening a selector, registering non-blocking SelectableChannel instances with interest sets (SelectionKey.OP_READ, OP_WRITE, OP_ACCEPT, OP_CONNECT), looping on selector.select(), and iterating the selected keys is the canonical pattern for a single-threaded, many-connection server. File path handling follows a similar modernization: the old java.io.File is superseded by the immutable java.nio.file.Path and the static utilities in java.nio.file.Files (readAllBytes, write, lines, copy, walk), and Files.walk returns a lazy Stream<Path> while Files.walkFileTree drives a FileVisitor with preVisitDirectory, visitFile, visitFileFailed, and postVisitDirectory for non-trivial work over a tree.

Serialization converts an object to a byte stream for storage or transport. Any class opts in by implementing the Serializable marker interface, after which ObjectOutputStream and ObjectInputStream handle the bytes; on deserialization the JVM bypasses constructors and populates fields reflectively, which is why serialVersionUID should be declared explicitly for version compatibility. Externalizable goes further by giving the class full control: implementing writeExternal and readExternal, with a public no-arg constructor required for reconstruction—you decide exactly which fields to write and in what format, making externalizable output often smaller and more version-tolerant than default serialization. The main pitfall is security: deserialization gadget chains can execute attacker code, mitigated since Java 9 by validating input via ObjectInputFilter. The transient keyword excludes a field from default serialization—useful for derived values, non-serializable state, and sensitive data—while the volatile keyword concerns cross-thread visibility; the two are unrelated, and only the name is similar.

Modern Java, Design Patterns, and Best Practices

Java 9 introduced the Java Platform Module System (JPMS, Project Jigsaw), which groups packages into modules declared in a module-info.java file. The key clauses are requires (dependency on another module), exports (making a package's public types available to other modules at compile and run time), and opens (allowing reflective access to all types in the package, including private members, used by frameworks like Spring and Hibernate). The narrow form opens com.example.model to com.fasterxml.jackson reveals a package to a specific module only, while the bare opens form reveals it to every module. requires transitive re-exports a dependency transitively, and jlink can build custom runtime images containing only the modules the application needs. ServiceLoader implements the SPI pattern for runtime discovery: implementations register themselves in META-INF/services/ (or via module provides ... with ...) and are lazily instantiated and discovered by the application—JDBC, LoggerFinder, and ImageIO are well-known examples.

Reflection lets a program inspect and manipulate classes, fields, methods, and constructors at run time through java.lang.reflect. The entry point is the Class<?> object, obtained by String.class, obj.getClass(), Class.forName("com.x.Y"), or ClassLoader.loadClass(...). With reflection you can instantiate, read and write (including private) fields, and invoke methods, but doing so is slow, breaks encapsulation, is fragile across refactors, and is subject to additional restrictions under the module system—prefer compile-time solutions when possible. Dynamic proxies (java.lang.reflect.Proxy) generate a runtime class that implements one or more interfaces and dispatches every method call to an InvocationHandler; they underpin Spring AOP, Mockito mocks, and RPC stubs. The limitation is interface-only: for class proxies without an interface, you need ByteBuddy, CGLIB, or JDK 21's MethodHandles.Lookup.defineHiddenClass. The JVM's three built-in class loaders (bootstrap, platform, application) use parent-delegation: a class loader first asks its parent to load a class and only tries itself if the parent fails, preventing custom classes from spoofing core types like java.lang.Object.

Several smaller modern features polish everyday code. The var keyword (Java 10+) lets the compiler infer a local variable's type from its initializer, restricted to locals, for-loop indices, and lambda parameters; reassignment must be to a compatible type, and var should be avoided when the right-hand side is non-obvious. Text blocks (Java 15+) provide """-delimited multi-line strings with normalized indentation; embedded \ at line ends continues the next line. Switch expressions (Java 14+) replace verbose break-laden switches with an arrow syntax that returns a value and checks exhaustiveness for enums and sealed types. Pattern matching for instanceof (Java 16+) binds the matched object to a variable usable inside the true branch: if (obj instanceof String s) System.out.println(s.length());. In Java 21, switch patterns extend this to type-based dispatch over sealed hierarchies.

SOLID is the canonical set of five object-oriented design principles. The Single Responsibility Principle says a class should have only one reason to change. Open/Closed says classes should be open for extension but closed for modification. Liskov Substitution requires that subtype objects be substitutable for their base type without breaking behavior; the classic violation is modeling Square as a subclass of Rectangle, since independently setting width and height on a Square breaks the geometry that callers expect (the fix is composition, immutable value types, or splitting roles into separate abstractions). Interface Segregation prefers many small role-focused interfaces over one fat interface, and Dependency Inversion counsels depending on abstractions rather than concrete implementations. Composition over inheritance is a related rule of thumb: prefer "has-a" via embedded instances and delegation over "is-a" via subclassing to avoid the fragile base class problem and to allow swapping implementations at run time. Dependency Injection externalizes an object's dependencies (usually via constructor, setter, or field injection) so the object does not construct them itself, which is exactly what frameworks like Spring, Guice, and CDI automate.

Several classic Gang-of-Four patterns have natural Java expressions. Singleton ensures exactly one instance with global access; the thread-safe lazy version uses double-checked locking on a volatile field, but the enum singleton is both serialization-safe and reflection-safe and is widely considered best. Factory Method defines an interface for creating objects and lets subclasses decide which class to instantiate, decoupling client code from concrete types. Builder separates the construction of a complex object from its representation and shines when an object has many optional parameters (the telescoping-constructor antipattern); records plus Lombok @Builder reduce much of the boilerplate. Strategy encapsulates interchangeable algorithms behind a common interface; combined with lambdas the pattern often reduces to passing a function. Observer establishes a one-to-many notification dependency; the legacy java.util.Observable is deprecated, and modern code prefers custom listener interfaces, PropertyChangeListener, or reactive streams with backpressure. Decorator attaches responsibility at run time by wrapping an object with another of the same interface (Java I/O is the textbook example: new BufferedReader(new FileReader(file))). DAO abstracts persistence behind a domain-oriented interface (findById, save, delete) with one or more implementations—JDBC, JPA, or in-memory for tests—so business logic is decoupled from storage.

Two standard library areas round out modern Java practice. The java.time API (JSR-310, Java 8+) replaces java.util.Date and java.util.Calendar with immutable, thread-safe, fluent types: LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, and Period. Instant represents a UTC nanosecond-precision point on the timeline (the machine-friendly choice), LocalDateTime is a date-and-time without a zone (a clock on the wall that doesn't pin down a moment, useful for "what the clock reads" in some place but not for actual moments), and ZonedDateTime or OffsetDateTime is required when you need a real, unambiguous moment; Instant and ZonedDateTime are converted via ZoneId.systemDefault() or any specific zone. For lightweight string composition, String.join(delim, elements) and the mutable StringJoiner (with prefix/suffix) handle common cases; Collectors.joining(delimiter, prefix, suffix) does the same within a stream pipeline. Together, these features make modern Java code far more concise, safer, and easier to reason about than the equivalent Java 1.4 era—though careful attention to the SOLID principles, the Diamond problem on overlapping default methods, and the SAM contract on functional interfaces ensures that the new power is not abused.

Frequently asked questions

What is the JVM?

The Java Virtual Machine (JVM) is an abstract computing machine that executes Java bytecode. It provides platform independence by translating bytecode into native machine instructions at runtime, enabling the "Write Once, Run Anywhere" principle.

What is the difference between == and equals() in Java?

== compares reference equality (whether two variables point to the same object in memory).
.equals() compares value equality (logical content).
Example: new String("hi") == new String("hi") is false, but new String("hi").equals(new String("hi")) is true.

What are lambda expressions in Java?

Lambda expressions (Java 8+) provide a concise way to implement functional interfaces (interfaces with a single abstract method).
Syntax: (parameters) -> expression
Example: List.sort((a, b) -> a.compareTo(b));
They enable functional programming and are heavily used with the Streams API.

What are the eight primitive data types in Java?

Java has 8 primitives:
byte (8-bit), short (16-bit), int (32-bit), long (64-bit), float (32-bit), double (64-bit), char (16-bit Unicode), boolean (1-bit).
Primitives are stored on the stack (when local) and have no methods or identity. They are not objects; use wrapper classes (e.g. Integer) when object behavior is required.

What is the difference between map() and flatMap() in Java Streams?

map() applies a Function<T,R> to each element and returns a stream of R — one-to-one transformation.
flatMap() applies a Function<T, Stream<R>> and flattens the resulting streams into one — useful for one-to-many (e.g. splitting strings into words, optional unwrapping, nested collections).
Example: words.stream().flatMap(line -> Arrays.stream(line.split(" "))).

What is the difference between thenApply() and thenCompose() in CompletableFuture?

thenApply() takes a Function<T, U> and returns CompletableFuture<U> — transforms a value when the future completes (analogous to map()).
thenCompose() takes a Function<T, CompletionStage<U>> and flattens the result, avoiding CompletableFuture<CompletableFuture<U>> (analogous to flatMap()).
Use thenCompose() whenever the next step itself returns a future (chaining async calls). Use thenApply() for synchronous value transformations.

What is the module system (JPMS) introduced in Java 9?

The Java Platform Module System (Project Jigsaw) groups packages into modules declared in module-info.java:
module com.example.app {
  requires java.sql;
  exports com.example.api;
  opens com.example.model to com.fasterxml.jackson;
}

requires = dependency; exports = public API; opens = reflective access for libraries like Spring/JPA. requires transitive re-exports a dependency. Modules enable reliable configuration, strong encapsulation, and a smaller custom runtime image via jlink.

What is WeakHashMap and when is it appropriate?

WeakHashMap is a Map whose keys are held weakly — when a key has no strong references elsewhere, the entry is removed automatically at the next GC. Useful for caches where cache lifetime should match the lifetime of the key object.
Caveats:
• Size-based eviction is unreliable; entries can vanish at unpredictable times.
• Iteration is weakly consistent; it may produce stale entries briefly.
• The value holds a strong reference to the value object — be careful not to store a strong reference to the value back to the key, or the entry never dies.

What is Instant vs LocalDateTime in Java time API?

Instant — a point on the UTC timeline, machine-friendly, nanosecond precision; obtained from Instant.now(). Used for timestamps and calculations across zones.
LocalDateTime — a date and time without a time zone; "what the clock on the wall reads" in some unspecified place. Cannot represent an exact moment.
Use ZonedDateTime or OffsetDateTime when you need a time zone/offset; convert InstantZonedDateTime via ZoneId.systemDefault() or a specific zone.

What is the strategy pattern in Java?

Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. Each strategy is a class implementing a common interface; the context object delegates work to the strategy instance.
Example: interface PricingStrategy { double price(double base); } with RegularPricing, DiscountPricing, HolidayPricing implementations. Combined with lambdas, the pattern reduces to passing a function — but the explicit interface remains useful when state/configuration is involved.

Drill this topic

170 flashcards on Java Programming — free, no signup needed to start.

Study Java Programming flashcards

LearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.