Top 100 Java Interview Questions
Top 100 Java interview questions covering OOP, collections, multithreading, Java 8+ features, JVM internals, and design patterns.
Java is a high-level, class-based, object-oriented programming language developed by Sun Microsystems (now owned by Oracle) and released in 1995. Its most famous principle is "Write Once, Run Anywhere" (WORA) — Java code is compiled into platform-independent bytecode that runs on any device with a Java Virtual Machine (JVM). Java is statically typed, strongly typed, and manages memory automatically through garbage collection. It is one of the most widely used languages in the world, powering enterprise backends, Android apps, and large-scale distributed systems.
These three acronyms represent different levels of the Java ecosystem. The JVM (Java Virtual Machine) is the runtime engine that executes Java bytecode — it handles memory management, garbage collection, and translates bytecode to machine code for the host OS. The JRE (Java Runtime Environment) is the JVM plus the standard class libraries needed to run Java programs — end users need the JRE to run Java applications. The JDK (Java Development Kit) is the complete development package: JRE plus the compiler (javac), debugger, and other tools needed to write and compile Java programs. Developers need the JDK; end users only need the JRE.
Java has eight primitive data types: byte (8-bit integer, -128 to 127), short (16-bit integer), int (32-bit integer, most commonly used), long (64-bit integer, uses L suffix), float (32-bit floating point, uses f suffix), double (64-bit floating point, default for decimals), char (16-bit Unicode character), and boolean (true or false). Primitives store actual values directly in memory (stack), not object references, making them more efficient than their wrapper class counterparts.
int is a primitive data type that stores a 32-bit signed integer value directly. It cannot be null, cannot be used as a generic type parameter, and is more memory-efficient. Integer is the wrapper class for int — an object that wraps the primitive value. It can be null, can be used in collections (like List<Integer>), and provides utility methods like Integer.parseInt(), Integer.MAX_VALUE. Java automatically converts between them via autoboxing (int → Integer) and unboxing (Integer → int).
Autoboxing is the automatic conversion that Java performs from a primitive type to its corresponding wrapper class. For example, assigning an int to an Integer variable: Integer x = 5; — the compiler automatically wraps 5 in an Integer object. Unboxing is the reverse: automatic conversion from a wrapper class to its primitive. For example: int y = x;. While convenient, autoboxing can cause performance issues and NullPointerExceptions if you accidentally unbox a null Integer. Be mindful of autoboxing in tight loops or large collections.
In Java, == compares references (memory addresses) for objects — it checks if two variables point to the exact same object in memory. For primitives, it compares values directly. .equals() compares content/value — it checks if two objects are logically equal. For example, two different String objects with the value "hello" would return false with == (different objects) but true with .equals() (same content). Always use .equals() when comparing object content. Override equals() in custom classes to define meaningful equality.
A String in Java is immutable — once created, its value cannot be changed. Any operation that appears to modify a String (like concat or replace) actually creates a new String object. This design was intentional for several reasons: security (strings are used as keys in HashMaps and network connections — immutability prevents tampering), thread safety (immutable objects can be shared between threads without synchronization), and performance via the String pool (the JVM can cache and reuse String literals). String objects are stored in a special area of the heap called the String Constant Pool.
String is immutable — every modification creates a new object, making it inefficient for repeated string building. StringBuilder is mutable and allows in-place modification without creating new objects — ideal for building strings in loops. It is not thread-safe but faster because it has no synchronization overhead. StringBuffer is identical to StringBuilder but thread-safe (all methods are synchronized) — use it only in multi-threaded contexts where multiple threads modify the same buffer. For single-threaded string building, always prefer StringBuilder over StringBuffer for performance.
A class is a blueprint or template that defines the state (fields/attributes) and behavior (methods) of objects. It is the fundamental unit of object-oriented programming in Java. A class definition describes what objects of that type will look like and what they can do, but does not itself occupy memory for the instance data. Example: a Car class might have fields like color and speed, and methods like accelerate() and brake(). Every Java program is built from classes, and the main() method inside a class is the entry point of the program.
An object is a runtime instance of a class. While a class is the blueprint, an object is the actual entity created from that blueprint — it occupies memory in the heap and has its own copy of the class's instance fields. You create an object with the new keyword: Car myCar = new Car();. Objects have three key characteristics: state (the current values of its fields), behavior (the methods it can execute), and identity (a unique reference/address in memory). In Java, objects are always accessed through references — you never manipulate the object directly.
A constructor is a special method that is automatically called when an object is created with new. It has the same name as the class and no return type (not even void). Constructors initialize the object's state: public Car(String color, int speed) { this.color = color; this.speed = speed; }. If you do not define any constructor, Java provides a default no-argument constructor that initializes fields to their default values. Constructors can be overloaded (multiple constructors with different parameters). One constructor can call another using this() (constructor chaining).
Inheritance is an OOP mechanism where a class (subclass/child) acquires the properties and behaviors of another class (superclass/parent) using the extends keyword. This promotes code reuse — common fields and methods are defined once in the parent and reused in all children. Example: class Dog extends Animal — Dog inherits all non-private fields and methods of Animal. Java supports single inheritance for classes (a class can extend only one class) but multiple inheritance through interfaces. The super keyword accesses parent class members. Every class in Java implicitly extends Object.
Method overloading is defining multiple methods with the same name but different parameter lists (different number, type, or order of parameters) within the same class. Java determines which overloaded version to call at compile time based on the argument types — it is a form of compile-time (static) polymorphism. Example: add(int a, int b), add(double a, double b), and add(int a, int b, int c) are three valid overloads. The return type alone does not constitute overloading — two methods with the same name and parameters but different return types will not compile.
Method overriding is when a subclass provides its own implementation of a method already defined in its superclass, with the same method signature (name, parameter types, and return type). The overriding method is selected at runtime based on the actual object type — this is runtime (dynamic) polymorphism. The @Override annotation is not mandatory but strongly recommended as it causes a compile error if the method does not actually override anything. Rules: you cannot override static, final, or private methods. The overriding method cannot have a more restrictive access modifier.
Encapsulation is the OOP principle of bundling data (fields) and the methods that operate on that data within a single class, while restricting direct access to the fields from outside the class. This is implemented by declaring fields as private and providing public getter and setter methods to control access. Encapsulation protects the internal state of an object from unauthorized or invalid changes — a setter can validate the new value before setting it. It also hides the implementation details, so the internal representation can change without affecting other classes that use the object.
Polymorphism means "many forms" — it allows one interface to be used for a general class of actions. In Java, there are two types. Compile-time polymorphism (method overloading): the correct method is chosen at compile time based on the argument types. Runtime polymorphism (method overriding): the correct method implementation is chosen at runtime based on the actual object type, not the reference type. Example: Animal a = new Dog(); a.sound(); — even though the reference type is Animal, Dog.sound() is called at runtime. This is enabled by dynamic method dispatch.
An abstract class is a class declared with the abstract keyword that cannot be instantiated directly — you must create a concrete subclass that provides implementations for all abstract methods. Abstract classes can have a mix of abstract methods (declared without a body) and concrete methods (with a body). They can also have constructors, instance fields, and static members. Use abstract classes to define a common template for a family of related classes, providing shared implementation while forcing subclasses to implement specific behaviors. Example: an abstract Shape class with an abstract area() method, implemented differently by Circle, Rectangle, etc.
An interface is a reference type in Java that defines a contract — a set of abstract methods that implementing classes must provide. Interfaces support multiple inheritance: a class can implement multiple interfaces. Before Java 8, interface methods were all abstract and public. Since Java 8, interfaces can have default methods (concrete methods with a default implementation) and static methods. Since Java 9, they can have private methods. All fields in an interface are implicitly public static final. Use interfaces to define capabilities (e.g., Comparable, Serializable, Runnable) that unrelated classes can implement.
An abstract class can have constructors, instance variables, and a mix of concrete and abstract methods. A class can extend only one abstract class. Use it when you want to share state and provide partial implementation among closely related classes. An interface cannot have constructors or instance state (only constants). A class can implement multiple interfaces. Use it to define a capability or contract for unrelated classes. Since Java 8, the distinction blurred with default methods in interfaces, but the key difference remains: abstract classes are for "is-a" relationships with shared state, while interfaces are for "can-do" capabilities without state.
Java has four access modifiers that control the visibility of classes, methods, and fields. private: accessible only within the same class. default (no modifier): accessible within the same package (package-private). protected: accessible within the same package and by subclasses in other packages. public: accessible from everywhere. The general best practice is to use the most restrictive modifier possible — private for fields (accessed via getters/setters), public only for the API that other classes need to use. This principle of least privilege is fundamental to encapsulation.
The static keyword means a member belongs to the class itself rather than to any instance of the class. Static fields are shared across all instances — changing the value in one place changes it for everyone. Static methods can be called without creating an object: Math.sqrt(), Integer.parseInt(). Static methods cannot access instance (non-static) fields or methods directly. A static block (static { }) runs once when the class is first loaded by the JVM, useful for static initialization. Inner classes can also be static — static nested classes do not hold a reference to the enclosing instance.
The final keyword has different meanings depending on where it is used. A final variable can be assigned only once — it is a constant. If it is a primitive, the value cannot change; if it is a reference, the reference cannot be reassigned (though the object's state can still change). A final method cannot be overridden by subclasses. A final class cannot be subclassed — for example, String, Integer, and Math are all final. Making a class final prevents inheritance and can be used for security or design reasons when the class's behavior must not be altered.
Garbage collection (GC) is Java's automatic memory management mechanism that reclaims memory occupied by objects that are no longer reachable by any live reference in the program. Developers do not manually free memory (unlike C/C++), which eliminates memory leaks from forgetting to deallocate and dangling pointer errors. The JVM's garbage collector runs in the background, periodically identifying unreachable objects and freeing their memory. The primary GC regions are the heap (where objects live) and its generations: Young Generation (Eden + Survivors) and Old Generation. You cannot force GC, but System.gc() suggests it (the JVM may ignore it).
The stack stores method call frames — each thread has its own stack. Local variables, method parameters, and return addresses are stored here. Stack memory is automatically managed (LIFO) and is very fast. Objects cannot be stored on the stack (only references to objects). The heap is shared among all threads and is where all objects and their instance fields are allocated. It is managed by the garbage collector. Heap access is slower than stack. A StackOverflowError occurs when the stack runs out of space (usually from infinite recursion). An OutOfMemoryError occurs when the heap is full and GC cannot free enough memory.
A package is a namespace that organizes related classes and interfaces together, similar to folders in a file system. Packages serve two purposes: they prevent naming conflicts (two classes can have the same name if they are in different packages) and they provide access control (package-private classes are only accessible within the same package). Declare a package with package com.example.myapp; at the top of a source file. Import classes from other packages with import java.util.List;. Java has built-in packages like java.util, java.io, and java.lang (automatically imported).
The instanceof operator checks whether an object is an instance of a specific class or implements a specific interface, returning a boolean. Example: if (animal instanceof Dog) { Dog dog = (Dog) animal; }. It is commonly used before a cast to avoid ClassCastException. In Java 16+, pattern matching for instanceof was standardized: if (animal instanceof Dog dog) { dog.bark(); } — the variable dog is automatically declared and cast within the if block, eliminating the explicit cast. instanceof returns false if the reference is null.
Type casting is converting a value from one type to another. Widening (implicit) casting converts a smaller type to a larger one automatically without any explicit cast — e.g., int to long, float to double. This is safe and lossless. Narrowing (explicit) casting converts a larger type to a smaller one and requires an explicit cast operator: int x = (int) 9.99; — this can lose data (9.99 becomes 9). For object casting, you can cast a supertype reference to a subtype, but it throws ClassCastException at runtime if the actual object is not of that type. Always check with instanceof before casting.
The this keyword is a reference to the current object — the instance on which the method or constructor is being called. It is used to resolve ambiguity when instance field names clash with parameter names: this.name = name;. It is also used to call another constructor in the same class (constructor chaining): this(defaultValue); — must be the first statement. You can pass this as an argument to other methods. In anonymous classes or lambdas, this refers to the enclosing class instance, not the anonymous class. Static methods do not have access to this because they are not called on instances.
The super keyword refers to the parent (superclass) of the current class. It has three uses: accessing an overridden method from the parent class (super.methodName()), accessing a parent class field that is hidden by a subclass field (super.fieldName), and calling the parent class constructor (super(args) — must be the first statement in the subclass constructor). Every constructor implicitly calls super() if you do not explicitly call it, which chains all the way up to the Object constructor. super cannot be used in static contexts.
Wrapper classes are object representations of Java's eight primitive types: Byte, Short, Integer, Long, Float, Double, Character, and Boolean. They are needed because generics and collections like List, Map, and Set cannot work with primitives — they require objects. Wrapper classes also provide utility methods (e.g., Integer.parseInt(), Double.isNaN()) and constants (e.g., Integer.MAX_VALUE). Java 5 introduced autoboxing and unboxing to automatically convert between primitives and their wrapper classes, making the distinction mostly transparent in day-to-day code.
A try-catch block is the fundamental mechanism for handling exceptions in Java. Code that might throw an exception is placed inside the try block. If an exception occurs, execution jumps to the matching catch block that handles that exception type. Multiple catch blocks can follow a single try for different exception types. The optional finally block executes always — whether an exception was thrown or not — and is used for cleanup (closing files, database connections). Since Java 7, a single catch can handle multiple exception types: catch (IOException | SQLException e). Exceptions not caught will propagate up the call stack.
Checked exceptions are exceptions that the compiler forces you to handle — either catch them with try-catch or declare them in the method signature using throws. They extend Exception (but not RuntimeException) and represent recoverable conditions like file not found (IOException) or SQL errors (SQLException). Unchecked exceptions (runtime exceptions) extend RuntimeException and do not need to be explicitly handled or declared — they represent programming errors like null pointer access (NullPointerException), array index out of bounds (ArrayIndexOutOfBoundsException), or illegal arguments (IllegalArgumentException). Error classes (like OutOfMemoryError) are also unchecked but represent unrecoverable JVM conditions.
throw is a statement used inside a method body to explicitly throw an exception object: throw new IllegalArgumentException("Value must be positive");. It transfers control to the nearest matching catch block or propagates up the call stack. throws is a declaration in the method signature that declares what checked exceptions the method might throw, warning callers that they need to handle them: public void readFile() throws IOException. A method can declare multiple exceptions with throws. Think of it this way: throw is the action of throwing, throws is the warning on the label.
An array in Java is a fixed-size, ordered collection of elements of the same type. Once created, its size cannot change. Arrays are objects and are stored in the heap. Declare and create: int[] numbers = new int[5]; or int[] numbers = {1, 2, 3, 4, 5};. Access elements with zero-based indices: numbers[0]. Accessing an index out of bounds throws ArrayIndexOutOfBoundsException. Multi-dimensional arrays are arrays of arrays: int[][] matrix = new int[3][3];. The length property (not a method) gives the array size. For dynamic collections, use ArrayList instead.
The enhanced for loop (also called for-each) provides a cleaner syntax for iterating over arrays and any class that implements the Iterable interface: for (String name : names) { System.out.println(name); }. It eliminates the need for index management and is less error-prone than traditional index-based loops. Drawbacks: you cannot modify the underlying collection while iterating (throws ConcurrentModificationException), you cannot access the current index, and you cannot iterate in reverse. For these cases, use a traditional for loop, an Iterator, or Java 8 streams with forEach.
The ternary operator (? :) is a shorthand for a simple if-else expression that evaluates a condition and returns one of two values. Syntax: condition ? valueIfTrue : valueIfFalse. Example: String result = (age >= 18) ? "adult" : "minor";. It is an expression (it returns a value), unlike an if-else statement. Use it for simple, inline assignments where both branches are short. Avoid nesting ternary operators as this significantly reduces readability. Unlike if-else, both branches of the ternary operator must be expressions that produce a value.
A default constructor is a no-argument constructor that Java automatically generates if you do not define any constructor in your class. It initializes instance fields to their default values: 0 for numeric types, false for boolean, null for object references. Example: new MyClass() works even if you never wrote a constructor — Java provided the default one. However, if you define any constructor with arguments, Java no longer generates the default constructor. If you still need a no-argument constructor along with parameterized ones, you must explicitly define it: public MyClass() { }.
Abstraction is the OOP principle of exposing only the essential features of an object while hiding the internal implementation details. It lets users interact with a simplified interface without needing to understand the underlying complexity. In Java, abstraction is achieved through abstract classes and interfaces. For example, when you call list.add(item), you don't need to know whether the underlying implementation is an ArrayList or LinkedList — the interface hides those details. Abstraction reduces complexity, increases code maintainability, and allows implementation to change without affecting the user of the interface.
The String pool (or String constant pool) is a special memory area in the Java heap where String literals are stored and reused. When you create a String literal like String s = "hello";, the JVM first checks if "hello" already exists in the pool. If it does, it returns a reference to that existing object instead of creating a new one — saving memory. Two literals with the same value will point to the same object in the pool: String a = "hello"; String b = "hello"; a == b; // true. Strings created with new String("hello") always create a new object outside the pool. Use intern() to manually add a String to the pool.
These three keywords are completely unrelated despite similar spelling. final is a modifier: makes a variable a constant, a method non-overridable, or a class non-inheritable. finally is a block in exception handling that always executes after the try-catch block — used for cleanup code (closing resources). finalize() is a method in the Object class that the garbage collector calls before an object is destroyed — it was meant for cleanup but is unreliable and deprecated since Java 9. Always use try-with-resources instead of finalize for resource cleanup, as the GC may never call finalize or may call it much later than expected.
The Java Collections Framework (JCF) is a unified architecture of interfaces, implementations, and algorithms for working with groups of objects. The core interfaces are Collection (the root), List (ordered, allows duplicates), Set (no duplicates), Queue (FIFO ordering), and Map (key-value pairs, not a true Collection). Common implementations include ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap, and PriorityQueue. The utility class Collections provides static methods for sorting, searching, reversing, and synchronizing collections. Always program to the interface, not the implementation: List<String> list = new ArrayList<>();.
Both implement the List interface but use different internal data structures. ArrayList uses a dynamic array — random access is O(1) (get by index is instant), but insertion/deletion in the middle is O(n) because elements must be shifted. It is better for read-heavy workloads and when frequent index-based access is needed. LinkedList uses a doubly-linked list — insertion/deletion at head or tail is O(1), but random access is O(n) because you must traverse from the start. LinkedList also implements Deque, so it can be used as a stack or queue. In practice, ArrayList outperforms LinkedList for most use cases due to better cache locality.
HashMap stores key-value pairs and allows one null key and multiple null values. Internally, it uses an array of "buckets" (an array of linked lists/trees). When you call put(key, value), Java calls key.hashCode() to compute the hash, then uses it to determine which bucket to store the entry in. If two keys have the same bucket (hash collision), they are stored in a linked list (or a red-black tree if the list exceeds 8 elements — Java 8 optimization). Average time complexity for get/put is O(1); worst case (all keys collide) is O(n). HashMap is not thread-safe — use ConcurrentHashMap for concurrent access.
The primary difference is thread safety. HashTable is a legacy class (since Java 1.0) where all methods are synchronized, making it thread-safe but slow due to the overhead of acquiring a lock on every operation. HashMap is not synchronized and therefore faster in single-threaded environments. HashMap allows one null key and multiple null values; HashTable does not allow any null keys or values. HashMap is part of the Collections Framework (implements Map); HashTable extends the old Dictionary class. For thread-safe maps, prefer ConcurrentHashMap over HashTable — it uses lock striping for much better concurrency performance.
HashMap does not guarantee any order of iteration — the order of entries may seem random. LinkedHashMap extends HashMap and maintains a doubly-linked list across all entries, guaranteeing that iteration order is the insertion order (the order in which key-value pairs were added). This makes LinkedHashMap slightly slower and more memory-intensive than HashMap, but the predictable iteration order is often worth it. LinkedHashMap also supports access-order mode (pass true as the third constructor argument), which moves recently accessed entries to the end — this is the basis of building an LRU (Least Recently Used) cache.
HashSet stores elements using a HashMap internally. It offers O(1) average performance for add, remove, and contains operations but makes no guarantee about the order of elements — iteration order is unpredictable. TreeSet stores elements in a red-black tree, keeping them sorted in natural order (or by a provided Comparator). All basic operations are O(log n). TreeSet also provides useful navigation methods like first(), last(), headSet(), and tailSet(). Use HashSet when you only care about uniqueness and fast lookups; use TreeSet when you need elements in sorted order or need range views of the set.
An Iterator is an object that enables traversing a collection one element at a time without exposing the underlying data structure. The Iterator interface has three methods: hasNext() (returns true if more elements exist), next() (returns the next element), and remove() (removes the last element returned by next() — the safe way to remove during iteration). To get an iterator: Iterator<String> it = list.iterator();. Iterating with an iterator while modifying the collection through the collection itself throws ConcurrentModificationException. Use it.remove() instead of list.remove() for safe removal during iteration.
A fail-fast iterator immediately throws ConcurrentModificationException if the collection is structurally modified (elements added or removed) after the iterator was created — except through the iterator's own remove() method. Most Java collection iterators (ArrayList, HashMap, etc.) are fail-fast. They detect modification using an internal modCount. A fail-safe iterator works on a snapshot or copy of the collection and does not throw exceptions if the original is modified. Iterators for ConcurrentHashMap and CopyOnWriteArrayList are fail-safe. The tradeoff: fail-safe iterators may not reflect the latest state of the collection.
Generics allow classes, interfaces, and methods to operate on types specified as parameters, providing type safety at compile time without requiring casts. Example: List<String> is a list that can only hold Strings — trying to add an Integer would be a compile error. Before generics (Java 1.4), collections held Object references requiring explicit casts everywhere and causing runtime ClassCastException. Generic methods: public <T> T getFirst(List<T> list). Bounded type parameters: <T extends Comparable<T>> restricts T to types that are comparable. Due to type erasure, generic type information is removed at compile time — at runtime, List<String> is just List.
A thread is the smallest unit of execution within a program. Java is multi-threaded, meaning multiple threads can run concurrently within the same process, sharing the same heap memory but each having its own stack. Create threads by either extending Thread class and overriding run(), or implementing the Runnable interface and passing it to a Thread: new Thread(runnable).start(). Call start() (not run()) to begin execution in a new thread — calling run() directly executes it on the current thread. Thread states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED.
The synchronized keyword is used to control access to a block of code or a method so that only one thread can execute it at a time, preventing race conditions. When applied to a method, the thread must acquire the intrinsic lock (monitor) of the object before executing. Synchronized on a static method uses the class's lock. You can also synchronize on a specific object: synchronized (sharedObject) { }. While synchronized ensures thread safety, it comes with performance cost (lock contention) and risk of deadlock. For fine-grained control, the java.util.concurrent.locks package provides more flexible alternatives like ReentrantLock.
The volatile keyword ensures that a variable's value is always read from and written to main memory, not from a thread's local cache. Without volatile, each thread may cache a variable's value locally, leading to stale reads where one thread sees an outdated value. Making a field volatile guarantees visibility — all threads see the most recent write. However, volatile does NOT guarantee atomicity. For example, count++ involves three steps (read, increment, write) and is not atomic even on a volatile variable. For compound actions, use synchronized or java.util.concurrent.atomic classes like AtomicInteger.
A deadlock occurs when two or more threads are each waiting for a lock held by the other, creating a circular dependency that causes all involved threads to wait forever. Example: Thread A holds Lock 1 and waits for Lock 2; Thread B holds Lock 2 and waits for Lock 1 — neither can proceed. Four conditions are necessary for deadlock (Coffman conditions): mutual exclusion, hold and wait, no preemption, and circular wait. Prevention strategies include: always acquiring locks in a consistent order, using tryLock() with a timeout, or using higher-level concurrency utilities like java.util.concurrent that handle locking internally. Detect deadlocks using thread dumps or tools like VisualVM.
The Executor framework (in java.util.concurrent) provides a high-level replacement for manually creating and managing threads. Instead of new Thread(runnable).start(), you submit tasks to an executor: executor.submit(runnable). The framework decouples task submission from execution policy. ExecutorService manages thread pools. Factory methods in Executors: newFixedThreadPool(n) (n reusable threads), newCachedThreadPool() (grows as needed), newSingleThreadExecutor() (one thread, serial execution), newScheduledThreadPool() (for delayed/periodic tasks). Always shut down executors when done: executor.shutdown().
Lambda expressions, introduced in Java 8, provide a concise way to represent an instance of a functional interface (an interface with exactly one abstract method) using an anonymous function. Syntax: (parameters) -> expression or (parameters) -> { statements; }. Example: list.sort((a, b) -> a.compareTo(b)) instead of an anonymous Comparator class. Lambdas can capture effectively-final local variables from the enclosing scope. They enable functional programming patterns and make code significantly more readable, especially with streams and collection operations. Under the hood, lambdas are implemented using invokedynamic bytecode instructions, not as anonymous inner classes.
A functional interface is an interface that has exactly one abstract method (SAM — Single Abstract Method). They are the target types for lambda expressions and method references. The @FunctionalInterface annotation is optional but recommended — it causes a compile error if the interface has more than one abstract method. Java 8 built-in functional interfaces in java.util.function: Predicate<T> (takes T, returns boolean), Function<T,R> (takes T, returns R), Consumer<T> (takes T, returns void), Supplier<T> (takes nothing, returns T), BiFunction<T,U,R>, UnaryOperator<T>. Existing interfaces like Runnable and Comparator are also functional interfaces.
The Stream API provides a functional approach to processing collections of elements. A stream is a sequence of elements supporting sequential and parallel aggregate operations — it does not store data (unlike a collection). Operations are: intermediate (lazy, return another stream): filter, map, flatMap, sorted, distinct, limit; and terminal (eager, produce a result or side effect): collect, forEach, reduce, count, findFirst. Streams are consumed once. Example: list.stream().filter(s -> s.startsWith("A")).map(String::toUpperCase).collect(Collectors.toList()). For parallel processing, use parallelStream().
Optional<T> is a container object that may or may not contain a non-null value, introduced in Java 8 to reduce NullPointerExceptions. Instead of returning null from a method when no value is available, return Optional.empty(). Callers are then forced to explicitly handle the empty case: optional.isPresent(), optional.get() (throws if empty), optional.orElse(defaultValue), optional.orElseGet(supplier), optional.orElseThrow(). Optional supports functional operations: optional.map(), optional.filter(), optional.ifPresent(). Do not use Optional as a field type or method parameter — use it only as a return type. Do not call get() without checking isPresent().
A method reference is a shorthand notation for a lambda expression that calls an existing method. Instead of x -> x.toUpperCase(), write String::toUpperCase. There are four kinds: static method reference (ClassName::staticMethod), instance method reference on a specific instance (instance::method), instance method reference on an arbitrary instance of a type (ClassName::instanceMethod), and constructor reference (ClassName::new). Method references are more readable than lambdas when the lambda just calls an existing method without any additional logic: list.forEach(System.out::println) vs list.forEach(x -> System.out.println(x)).
Default methods, introduced in Java 8, allow interfaces to provide concrete method implementations. Before Java 8, adding a new method to an interface would break all implementing classes. Default methods solve the backwards-compatibility problem — you can add new methods to interfaces without breaking existing implementations. Declare with the default keyword: default void log(String message) { System.out.println(message); }. Implementing classes can override default methods. If a class implements two interfaces with the same default method, the class must override it to resolve the ambiguity. Default methods enable evolution of interfaces over time — they are the reason Java could add stream-related methods to existing collection interfaces.
Comparable is implemented by a class to define its natural ordering. The class implements compareTo(T other), and this single ordering is used by default in sorting and sorted collections. Example: Integer, String, and Date all implement Comparable. Comparator is an external comparison strategy — a separate object that defines how two objects should be compared. It is used when you need multiple sort orders, or when you cannot modify the class. Example: Comparator.comparingInt(Person::getAge). Collections.sort and List.sort() accept a Comparator. Use Comparable for the class's primary sort key, Comparator for secondary or alternative orderings.
Reflection is the ability of a program to examine and modify its own structure and behavior at runtime. Through the java.lang.reflect package, you can inspect classes (Class.forName("com.example.MyClass")), list fields and methods, invoke methods dynamically, and even access private members. Reflection is used by frameworks like Spring (dependency injection, annotation processing), Hibernate (ORM mapping), and JUnit (test discovery). It bypasses compile-time type checking, which makes it powerful but risky — improper use leads to security vulnerabilities and performance overhead. Reflection is generally a tool for framework authors, not application code.
Arrays.asList() returns a fixed-size list backed by the original array — you can update elements (set), but cannot add or remove (throws UnsupportedOperationException). Changes to the list affect the original array and vice versa. It allows null elements. List.of() (Java 9+) returns a truly immutable list — any attempt to add, remove, or set throws UnsupportedOperationException. It does not permit null elements (throws NullPointerException). List.of() is preferred for creating small, permanent collections. new ArrayList<>(List.of(...)) creates a mutable copy. Collections.unmodifiableList() wraps any list in an unmodifiable view but the backing list can still be changed.
Try-with-resources (Java 7+) automatically closes resources that implement the AutoCloseable interface at the end of the try block, regardless of whether an exception is thrown. Before this feature, you had to close resources manually in finally blocks, which was verbose and error-prone. Example: try (BufferedReader br = new BufferedReader(new FileReader(file))) { return br.readLine(); }. The resource is closed automatically when the try block exits. Multiple resources can be declared: the last-declared resource is closed first. If both the try body and the close() method throw exceptions, the close() exception is suppressed (accessible via getSuppressed()).
A PriorityQueue is a queue where elements are ordered by their natural ordering (if they implement Comparable) or by a provided Comparator, not by insertion order. The head of the queue is always the smallest element (min-heap by default). Operations: offer() adds an element (O(log n)), poll() removes and returns the head (O(log n)), peek() returns the head without removing (O(1)). PriorityQueue does not permit null elements. Iteration over a PriorityQueue does not guarantee any particular order — only the head is guaranteed to be the minimum. Use it for scheduling algorithms, Dijkstra's shortest path, and any problem requiring "process the most important item first" semantics.
CountDownLatch is a synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes. Initialize it with a count: CountDownLatch latch = new CountDownLatch(3). Each worker thread calls latch.countDown() when it finishes (decrements the count). The waiting thread calls latch.await(), which blocks until the count reaches zero. Once the count reaches zero, it cannot be reset — for a reusable latch, use CyclicBarrier. Common use cases: waiting for all services to start before serving requests, waiting for all results before computing a summary, and testing concurrent scenarios.
A BlockingQueue is a thread-safe queue that blocks on certain operations: put() blocks if the queue is full (until space becomes available), and take() blocks if the queue is empty (until an element is available). This makes it ideal for the Producer-Consumer pattern without manually managing wait/notify. Implementations: ArrayBlockingQueue (bounded, array-backed), LinkedBlockingQueue (optionally bounded, linked), PriorityBlockingQueue (unbounded, sorted), SynchronousQueue (no capacity, direct handoff between threads). BlockingQueues are thread-safe and avoid the boilerplate of synchronized blocks for inter-thread data exchange.
A Semaphore maintains a set of permits. acquire() takes a permit (blocking if none available) and release() returns a permit. It controls the number of threads that can access a resource simultaneously. A semaphore with one permit acts as a mutex. A semaphore with N permits allows up to N threads to access a resource concurrently — useful for connection pool management, rate limiting, or controlling access to a fixed number of resources. Unlike synchronized, semaphores are not tied to a specific thread — any thread can call release(), even one that did not call acquire(), enabling flexible signaling patterns.
A Deque (Double-Ended Queue) supports adding and removing elements from both ends efficiently. It combines the functionality of both a Stack (LIFO) and a Queue (FIFO). The Deque interface provides methods for both ends: addFirst()/addLast(), removeFirst()/removeLast(), peekFirst()/peekLast(). Main implementations: ArrayDeque (resizable array, no null elements, faster than Stack and LinkedList for stack/queue use) and LinkedList (node-based). Java's Stack class is legacy and thread-synchronized — prefer ArrayDeque for stack and queue operations in single-threaded code.
A TreeMap is a NavigableMap implementation backed by a red-black tree. It stores key-value pairs in sorted order by key — either natural ordering (if keys implement Comparable) or by a provided Comparator. All basic operations (get, put, remove, containsKey) run in O(log n). TreeMap's unique advantage is its navigation methods: firstKey(), lastKey(), floorKey(k) (largest key ≤ k), ceilingKey(k) (smallest key ≥ k), headMap(k) (view of entries with keys < k), and tailMap(k). Use TreeMap when you need sorted key traversal or range queries; use HashMap when order does not matter and O(1) performance is needed.
The Collections utility class provides static methods for operating on collections. Key methods: sort(list) and sort(list, comparator) for sorting, binarySearch(list, key) for searching (requires sorted list), reverse(list), shuffle(list), min(collection) and max(collection), frequency(collection, obj), unmodifiableList(list) (returns read-only view), synchronizedList(list) (returns thread-safe view), emptyList()/singleton(obj) for immutable utility collections, nCopies(n, obj) for a list of n copies. These methods save you from reimplementing common algorithms and promote code reuse with existing collections.
A WeakReference holds a reference to an object that does not prevent the object from being garbage collected. When only weak references point to an object, the GC is free to collect it. Useful for memory-sensitive caches — if memory is low, the cache entries are collected automatically. WeakHashMap is a Map implementation where keys are held with weak references. When a key is no longer referenced elsewhere in the program, the entry is automatically removed from the map. This makes WeakHashMap ideal for caches and canonicalized mappings (like the String pool concept) where you want the map entry to disappear when the key object is no longer used elsewhere in the application.
CopyOnWriteArrayList is a thread-safe variant of ArrayList where all mutative operations (add, set, remove) are implemented by making a fresh copy of the underlying array. This means reads never block and do not require synchronization — very efficient when reads vastly outnumber writes. The iterator of CopyOnWriteArrayList is fail-safe — it operates on a snapshot of the array at the time the iterator was created and never throws ConcurrentModificationException. However, it is expensive for write-heavy workloads because of the array copying. Best suited for listener/observer lists where iteration is far more common than modification.
The Java Memory Model (JMM) defines the rules for how threads interact through memory — specifically, when writes made by one thread become visible to other threads. By default, threads may cache variable values locally and not immediately flush to main memory. The JMM establishes happens-before relationships: if action A happens-before action B, then A's memory effects are visible to B. Happens-before is established by: thread start (all writes before start() are visible to the started thread), locking/unlocking the same monitor, volatile reads/writes, and joining a thread. Understanding the JMM is critical for writing correct concurrent code without data races.
ConcurrentHashMap is a thread-safe implementation of HashMap designed for high-concurrency scenarios. Unlike Hashtable (which locks the entire map), ConcurrentHashMap uses lock striping — in Java 8, it uses CAS (Compare-And-Swap) operations and synchronized blocks on individual buckets, allowing concurrent reads and writes to different buckets simultaneously. This gives far better throughput than full synchronization. Reads (get) never block. ConcurrentHashMap does not allow null keys or values (to avoid ambiguity between a missing key and a key mapped to null). For atomic compound operations, use methods like putIfAbsent(), computeIfAbsent(), and merge().
CompletableFuture (Java 8) is a powerful implementation of Future that supports non-blocking asynchronous programming with a fluent callback API. Unlike plain Future (which only supports blocking get()), CompletableFuture lets you chain operations: thenApply() (transform the result), thenAccept() (consume without returning), thenCompose() (flatMap for futures), thenCombine() (combine two futures). CompletableFuture.supplyAsync(supplier) runs the task in ForkJoinPool. Error handling: exceptionally(), handle(). Combine multiple: allOf() (wait for all), anyOf() (wait for any). This enables readable async pipelines without nested callbacks.
The Fork/Join framework (Java 7) is designed for work that can be recursively broken into smaller tasks and executed in parallel, then combined (divide and conquer). It uses a work-stealing algorithm — idle threads steal tasks from the queues of busy threads, maximizing CPU utilization. Extend RecursiveTask<V> (returns a result) or RecursiveAction (no result) and override compute(). Inside compute, if the problem is small enough, solve it directly; otherwise, split it into subtasks with fork() and wait with join(). The ForkJoinPool.commonPool() is used by parallel streams and CompletableFuture by default. It is ideal for CPU-intensive divide-and-conquer algorithms like merge sort or matrix multiplication.
Records (Java 16, finalized from preview in 14) are a special class declaration designed for transparent, immutable data carriers. record Point(int x, int y) { } automatically generates: a canonical constructor, private final fields, public getters (x() and y()), and correct equals(), hashCode(), and toString() implementations. Records cannot extend other classes (they implicitly extend Record), cannot have non-static instance fields beyond the record components, and are implicitly final. You can add compact constructors for validation, custom methods, and static fields. Records are the Java equivalent of Kotlin data classes and are ideal for DTOs, value objects, and immutable data.
Sealed classes and interfaces (Java 17) restrict which other classes can extend or implement them. public sealed class Shape permits Circle, Rectangle, Triangle { } — only the explicitly listed classes are allowed to be subclasses. Each permitted subclass must be declared final, sealed itself, or non-sealed (re-opens the hierarchy). Sealed classes enable exhaustive pattern matching in switch expressions — the compiler can verify all subtypes are handled without a default case. This brings algebraic data type (sum type) modeling to Java, similar to Kotlin sealed classes. They are particularly useful for modeling domain states, results, and discriminated unions.
Pattern matching for switch (Java 21, finalized) extends switch expressions to match on type patterns, guard patterns, and record patterns. Example: String result = switch (obj) { case Integer i -> "int: " + i; case String s when s.length() > 5 -> "long string"; case null -> "null"; default -> "other"; };. This eliminates lengthy if-instanceof-cast chains. With sealed classes, all subtypes can be covered exhaustively. Guard patterns (when clause) add conditions beyond type matching. This is a major step towards functional-style pattern matching in Java, aligning it with modern languages like Kotlin, Scala, and Haskell.
Text blocks (Java 15, standard) are multi-line string literals that avoid the need for most escape sequences. Delimited by triple quotes """, they preserve the formatting of the text while stripping common leading whitespace (indentation). This makes embedding JSON, SQL, HTML, or XML in Java code much more readable: the text block """ { "name": "Alice" } """ produces a clean JSON string without manual concatenation and escaping. The closing """ position determines the indentation level stripped. Escape sequences like \n still work, and the new \ line terminator suppresses the newline. Text blocks are just String values at runtime.
A ClassLoader is responsible for loading Java class files (.class bytecode) into the JVM at runtime. The JVM uses a delegation hierarchy: Bootstrap ClassLoader (loads core Java classes from rt.jar/modules), Extension/Platform ClassLoader (loads extension libraries), and Application ClassLoader (loads classes from the classpath). When a class is requested, the child loader asks the parent first (delegation model) — this prevents malicious classes from replacing core Java classes. You can create custom ClassLoaders for dynamic class loading, hot deployment (reloading classes without restarting), or loading from non-standard sources (databases, networks). This is how application servers like Tomcat isolate different web applications.
The Singleton pattern ensures only one instance of a class is created throughout the application's lifetime. The classic implementation: private constructor (prevents instantiation from outside), a private static field holding the single instance, and a public static method returning that instance. The thread-safe, lazily-initialized version uses double-checked locking with a volatile field: check if null outside the synchronized block, synchronize, check again inside, create if still null. The simplest thread-safe approach is the enum singleton: enum MySingleton { INSTANCE; } — it is serialization-safe, reflection-proof, and thread-safe by design. Another elegant approach is the initialization-on-demand holder pattern, which uses static inner class for lazy initialization without synchronization overhead.
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It is especially useful when an object has many optional parameters — it avoids telescoping constructors and the mutable JavaBean approach. A Builder class has methods for each parameter (returning this for chaining) and a build() method that creates the final immutable object. Example: new Pizza.Builder("large").addTopping("cheese").addTopping("mushroom").build(). Lombok's @Builder annotation generates Builder code automatically. The Builder pattern is used extensively in Java APIs — StringBuilder, ProcessBuilder, and HttpClient.Builder are all examples.
The Factory pattern provides an interface for creating objects while allowing subclasses or implementations to decide which class to instantiate. It decouples object creation from the code that uses the objects. The Factory Method pattern defines an interface for creating an object but lets subclasses decide the concrete class. The Abstract Factory creates families of related objects. Example: ShapeFactory.createShape("circle") returns a Circle without the caller knowing the concrete type. In Java, static factory methods are common: Integer.valueOf(), List.of(), Optional.of(). Advantages: encapsulates object creation logic, makes code more testable by allowing mock factories, and supports the Open/Closed Principle.
The Observer pattern defines a one-to-many dependency between objects: when one object (the Subject/Observable) changes state, all registered observers are automatically notified and updated. This enables loose coupling — the subject does not need to know anything about its observers except that they implement a common interface. Java provides java.util.Observable (deprecated in Java 9) and java.util.EventListener. Modern approaches use custom listener interfaces, property change support, or reactive libraries (RxJava, Project Reactor). The pattern is the foundation of event-driven programming, GUI frameworks, and publish-subscribe messaging systems. Spring's ApplicationEventPublisher is a common real-world example.
The Strategy pattern defines a family of algorithms, encapsulates each one as a class implementing a common interface, and makes them interchangeable. The client selects the algorithm at runtime without changing the code that uses it. Example: a Sorter class with a SortStrategy interface — you can swap between BubbleSortStrategy and QuickSortStrategy without changing Sorter. In modern Java, strategies are often just lambdas or method references since they are instances of functional interfaces. The pattern promotes the Open/Closed Principle (open for extension, closed for modification) and is used extensively: Collections.sort(list, comparator) is the Strategy pattern with Comparator as the strategy.
Java NIO (New I/O), introduced in Java 1.4, provides an alternative I/O API focused on non-blocking, buffer-oriented, and channel-based I/O. Traditional java.io is stream-oriented and blocking — each operation blocks until completed. NIO uses Channels (bidirectional connections) and Buffers (intermediate data containers) instead of streams. The key feature is the Selector — a single thread can monitor multiple channels for readiness (data available, connection complete), enabling one thread to handle thousands of connections. This is the basis of high-performance servers. Java 7's NIO.2 (java.nio.file) added the Files and Path APIs, greatly improving file system operations.
Serialization is the process of converting an object's state into a byte stream for storage (file, database) or transmission (network). Deserialization reconstructs the object from the byte stream. A class must implement java.io.Serializable (a marker interface with no methods) to be serializable. The serialVersionUID is a version identifier — if the class changes and the UID does not match the deserialized data, an InvalidClassException is thrown. Mark fields with transient to exclude them from serialization (e.g., passwords, cached values). Security warning: Java serialization has known vulnerabilities (deserialization gadget chains) — prefer modern alternatives like JSON (Jackson, Gson) or Protocol Buffers for data exchange.
The Java Platform Module System (JPMS), introduced in Java 9 (Project Jigsaw), allows you to organize code into modules — named, self-describing groups of packages with explicit declarations of what they export and what they require. Each module has a module-info.java file: module com.example.myapp { requires java.sql; exports com.example.myapp.api; }. Benefits: strong encapsulation (internal packages cannot be accessed by default, unlike jars), explicit dependencies (the JVM verifies that all required modules are present at startup), and smaller JREs (you can create minimal custom runtimes with only the needed modules using jlink). The JDK itself was modularized — java.base is the foundational module all others implicitly depend on.
The var keyword (Java 10) enables local variable type inference — the compiler infers the type from the initializer expression, so you do not need to repeat the type. var list = new ArrayList<String>(); is equivalent to ArrayList<String> list = new ArrayList<String>();. var only works for local variables with initializers — it cannot be used for fields, method parameters, return types, or without an initializer. The inferred type is the static type at compile time, so it is just syntactic sugar — not dynamic typing. Use var to reduce verbosity especially with complex generic types, but avoid it when the type would not be obvious to a reader without knowing the right-hand side.
A heap dump is a snapshot of all objects in the JVM heap at a given moment — it captures what is in memory (object types, sizes, references, and object count). It is used to diagnose memory leaks and excessive memory usage. Generate with: jmap -dump, VisualVM, or configure the JVM with -XX:+HeapDumpOnOutOfMemoryError. Analyze with Eclipse MAT or VisualVM. A thread dump is a snapshot of all threads' current states — it shows what each thread is doing, what locks it holds, and what it is waiting for. It is used to diagnose deadlocks, thread starvation, and CPU bottlenecks. Generate with jstack, kill -3, or VisualVM. Look for BLOCKED threads and circular lock dependencies in deadlock analysis.
Java offers several GC algorithms for different use cases. Serial GC uses a single thread — suitable only for small apps with small heaps. Parallel GC uses multiple threads for minor/major collections — good throughput but pauses. G1 (Garbage-First) GC (default since Java 9) divides the heap into equal-sized regions, prioritizing collection of regions with most garbage — balances throughput and pause times. ZGC (Java 15+) is designed for ultra-low pause times (<10ms) regardless of heap size — most collection work is done concurrently with the application. Shenandoah (OpenJDK) is similar to ZGC. Choose GC based on requirements: throughput (Parallel), low-latency (ZGC, Shenandoah), or balanced (G1).
toString() is an instance method — calling it on a null reference throws a NullPointerException. Every class inherits it from Object, and the default implementation returns the class name plus the object's hash code. You should override it to provide meaningful output. String.valueOf() is a static method that is null-safe — if the argument is null, it returns the string "null" instead of throwing. It is overloaded for all primitive types. In practice, String.valueOf(obj) internally calls obj.toString() if obj is not null. Use String.valueOf() when the object might be null, and toString() when you are certain it is not null.
Reactive programming is a programming paradigm centered on asynchronous data streams and the propagation of change. It handles backpressure — when producers emit data faster than consumers can process — by allowing consumers to signal how much data they can handle. Java supports it through the java.util.concurrent.Flow API (Java 9), which standardizes the Reactive Streams specification with four interfaces: Publisher, Subscriber, Subscription, and Processor. Major frameworks: Project Reactor (used by Spring WebFlux) provides Mono<T> (0-1 elements) and Flux<T> (0-N elements). RxJava provides Observable, Flowable, Single, and Maybe. Reactive is ideal for I/O-bound, high-concurrency scenarios.
The Decorator pattern attaches additional responsibilities to an object dynamically by wrapping it in decorator objects that share the same interface. This provides a flexible alternative to subclassing for extending functionality. The decorator wraps the original component and adds behavior before or after delegating to it. Java I/O is the classic example: new BufferedInputStream(new FileInputStream(file)) — BufferedInputStream decorates FileInputStream with buffering. You can stack decorators: new DataInputStream(new BufferedInputStream(new FileInputStream(file))) adds type-aware reading on top of buffering. Each decorator adds one responsibility, following the Single Responsibility Principle while enabling combinatorial feature extension without class explosion.
A memory leak in Java occurs when objects that are no longer needed remain referenced and cannot be garbage collected. Despite automatic GC, Java can still leak. Common causes: static fields holding collections that grow indefinitely, listeners/callbacks not deregistered after use, caches without eviction policies, long-lived objects holding references to short-lived ones (e.g., a static reference to an Activity in Android), and non-closed resources (streams, connections). Detection: heap dump analysis with Eclipse MAT or YourKit, monitoring heap growth with -verbose:gc. Prevention: use WeakReference for caches, remove listeners when done, use try-with-resources for I/O, and use profilers to catch leaks early in development.
Both Runnable and Callable represent tasks to be executed by a thread or executor, but they differ in two ways. Runnable has a run() method with no return value (void) and cannot throw checked exceptions. Callable<V> has a call() method that returns a value of type V and can throw checked exceptions. Callable is submitted to an ExecutorService via submit(), which returns a Future<V> that can be used to retrieve the result or check for exceptions. Runnable can be used with both Thread and ExecutorService. For tasks that need to return results or propagate checked exceptions, Callable with Future (or CompletableFuture) is the right choice.
Annotations are metadata attached to classes, methods, fields, or parameters that provide information to the compiler, tools, or runtime. They are declared with @interface. Built-in annotations: @Override (compiler check), @Deprecated, @SuppressWarnings, @FunctionalInterface. Meta-annotations configure the annotation itself: @Retention (SOURCE — discarded after compilation, CLASS — in bytecode but not at runtime, RUNTIME — available via reflection), @Target (where it can be applied), @Inherited, @Repeatable. Annotation processors (APT) run at compile time to generate code — this powers Lombok, Dagger, and Room. Runtime annotations are read via Reflection — this powers Spring's @Autowired, @RequestMapping, and JUnit's @Test.
Virtual threads (Java 21, finalized) are lightweight threads managed by the JVM rather than the OS. Traditional Java threads are platform threads — each maps to an OS thread, which is expensive (1-2MB stack, OS scheduling overhead). Creating thousands of them is not practical. Virtual threads have a tiny memory footprint and are managed by the JVM on a small pool of carrier (OS) threads. When a virtual thread blocks (e.g., on I/O), it is unmounted from its carrier thread, which is then free to run other virtual threads. This makes the blocking, synchronous programming model as scalable as async/reactive code. Create them: Thread.ofVirtual().start(runnable) or Executors.newVirtualThreadPerTaskExecutor(). This is a game-changer for server applications that handle many concurrent blocking operations.