1. Core Java Fundamentals — Interview Q&A
1.1 OOP Principles (Encapsulation, Inheritance, Polymorphism, Abstraction)
Q1: Explain the four OOP pillars with a real-world enterprise example.
- Encapsulation: Bundling data and methods, restricting direct access via private fields and public getters/setters. Example: a
BankAccountclass hidesbalanceand exposesdeposit()/withdraw()with validation logic, preventing illegal direct mutation. - Inheritance: A subclass acquires fields/behavior from a superclass. Example:
SavingsAccount extends Accountreuses common transaction logic while adding interest calculation. - Polymorphism: One interface, multiple implementations. Compile-time (method overloading) vs runtime (method overriding via dynamic dispatch). Example:
PaymentProcessorinterface implemented differently byCreditCardProcessorandUPIProcessor. - Abstraction: Exposing only essential behavior, hiding implementation. Achieved via abstract classes/interfaces. Example: a
NotificationServiceinterface hides whether the implementation uses SMS, email, or push notifications.
Q2: Why does Java not support multiple inheritance of classes but allows it via interfaces? Multiple class inheritance causes the diamond problem — ambiguity when two parent classes define the same method signature with different implementations, and the compiler cannot resolve which to inherit. Interfaces avoid this because, prior to Java 8, they had no implementation. Since Java 8’s default methods reintroduced a limited diamond problem, Java resolves it by forcing the implementing class to override the conflicting method explicitly if two interfaces provide conflicting defaults.
Q3: Architect-level — How do you decide between favoring composition over inheritance in a service layer design? Inheritance creates tight coupling and breaks encapsulation across layers (the “fragile base class” problem) — a change in the superclass can silently break subclasses. Composition allows behavior to be assembled at runtime, supports better testability (mocking collaborators), and aligns with the Strategy/Decorator patterns commonly used in Spring service layers. Rule of thumb: use inheritance only for true “is-a” relationships with stable contracts; default to composition (“has-a”) for flexibility.
1.2 Data Types, Autoboxing, and Wrapper Classes
Q1: What is autoboxing/unboxing, and where does it commonly cause performance or correctness issues? Autoboxing is the automatic conversion of primitives to their wrapper objects (int → Integer); unboxing is the reverse. Issues arise in:
- Loops with wrapper types: repeated boxing/unboxing in tight loops causes unnecessary object creation and GC pressure.
- NullPointerException: unboxing a
nullInteger (e.g., in a ternary mixingintandInteger) throws NPE. - Collections of primitives:
List<Integer>boxes every element, which is far less memory-efficient than a primitive array for large datasets.
Q2: Why does Integer a = 127; Integer b = 127; a == b return true, but the same with 128 returns false? Java caches Integer objects in the range -128 to 127 via the Integer Cache (IntegerCache class), as part of the autoboxing specification (JLS). Values within this range reuse cached instances, so == (reference comparison) returns true. Outside this range, new objects are created each time, so == returns false. This is a classic trap question testing understanding of reference vs value equality — always use .equals() for wrapper comparisons.
Q3: Architect-level — When would you choose primitive arrays over boxed collections in a high-throughput system? In latency-sensitive systems (trading platforms, real-time pricing engines), primitive arrays (int[], long[]) avoid boxing overhead and improve cache locality since they’re stored contiguously in memory, unlike ArrayList<Integer> which stores references to scattered heap objects. Libraries like Eclipse Collections or Trove provide primitive-specialized collections for this exact reason.
1.3 String Pool, Immutability, and StringBuilder/StringBuffer
Q1: Why is String immutable in Java, and what are the architectural benefits? Strings are immutable because:
- Security: Used in classloading and network connections; mutability could allow tampering after validation.
- String Pool optimization: Immutability allows safe sharing of
Stringliterals in the String Constant Pool, reducing memory footprint. - Thread safety: Immutable objects are inherently thread-safe, requiring no synchronization when shared across threads.
- HashCode caching: Since the value never changes,
Stringcaches its hashCode after first computation, speeding up repeated use asHashMapkeys.
Q2: Explain the difference between String s = "abc" and String s = new String("abc").
"abc"literal: JVM checks the String Pool first. If"abc"exists, it returns the existing reference; otherwise it creates and interns it.new String("abc"): Always creates a new object on the heap, bypassing the pool, even if"abc"already exists there. You can force pool reference reuse via.intern().
This matters because == comparisons behave differently: literal-to-literal comparison may return true (pool reuse), while new String(...) comparisons return false unless .intern() is used.
Q3: When should you use StringBuilder vs StringBuffer vs String concatenation (+)?
- String
+in a loop: Each concatenation creates a new String object (O(n²) behavior for n concatenations) — avoid in loops. - StringBuilder: Mutable, not thread-safe, optimal for single-threaded string building (most common case).
- StringBuffer: Mutable, thread-safe via synchronized methods, but with the overhead of synchronization — use only when multiple threads genuinely mutate the same buffer concurrently (rare in practice; usually better to use thread-local builders instead).
Q4: Architect-level — How does the JVM optimize String concatenation at compile time? Since Java 9, the compiler often uses StringConcatFactory.makeConcatWithConstants (invokedynamic-based) rather than chaining StringBuilder.append() calls, allowing JVM-level optimization strategies to evolve independently of bytecode generated by javac. This is a key change from pre-Java 9, where + concatenation always compiled directly to StringBuilder calls.
1.4 Access Modifiers and Packages
Q1: Explain the four access modifiers and their visibility scope.
| Modifier | Same Class | Same Package | Subclass (diff package) | Different Package |
|---|---|---|---|---|
private |
✅ | ❌ | ❌ | ❌ |
| default (no modifier) | ✅ | ✅ | ❌ | ❌ |
protected |
✅ | ✅ | ✅ | ❌ |
public |
✅ | ✅ | ✅ | ✅ |
Q2: Why would an architect enforce package-private (default) visibility for certain classes in a layered architecture? Package-private visibility enforces module boundaries without requiring Java’s full module system (JPMS). For example, in a hexagonal architecture, internal adapter implementations can be package-private so only the package’s public interface is exposed to other layers — preventing accidental coupling to implementation details and enabling safe refactoring.
Q3: How do access modifiers interact with Java Modules (JPMS, Java 9+)? JPMS adds a layer above package-private/public: even a public class is inaccessible outside its module unless the module explicitly exports the package via module-info.java. This allows architects to expose a stable public API while keeping internal packages (even with public classes) completely hidden from consuming modules — critical for designing maintainable library boundaries.
1.5 Static vs Instance Members, Static Initialization Blocks
Q1: What’s the execution order when a class is loaded, considering static blocks, instance blocks, and constructors? Order:
- Static variables and static blocks — executed once, in the order they appear, when the class is first loaded.
- Instance variables and instance initializer blocks — executed every time an object is created, in declaration order.
- Constructor body — executed last, after instance initializers.
If inheritance is involved: parent static blocks → child static blocks → parent instance blocks/constructor → child instance blocks/constructor.
Q2: Why use a static initialization block instead of just initializing static variables inline? Static blocks are necessary when:
- Initialization logic is complex (loops, conditionals, exception handling) and can’t be expressed in a single inline assignment.
- Multiple static fields need to be initialized together with shared logic.
- You need to catch exceptions during static initialization (inline field initializers can’t have try-catch).
Example: loading configuration from a properties file into static fields at class-load time, wrapped in try-catch to handle IOException.
Q3: Architect-level — What are the pitfalls of static state in enterprise applications (e.g., Spring-managed beans)? Static fields are shared across the entire JVM, not scoped to a request or session — this becomes dangerous in multi-tenant or concurrent web applications, where static mutable state can leak data between threads/requests or create race conditions. Static state also undermines testability (can’t easily reset/mock between tests) and complicates dependency injection, since static members exist outside the Spring container’s lifecycle management. Best practice: avoid mutable static state; use it only for true constants or stateless utility methods.
1.6 final, finally, finalize — Differences
Q1: Differentiate final, finally, and finalize().
| Keyword/Method | Purpose |
|---|---|
final |
Modifier preventing reassignment (variables), overriding (methods), or extension (classes) |
finally |
Block guaranteed to execute after try/catch, regardless of exception, used for cleanup |
finalize() |
Deprecated (since Java 9) method called by GC before object reclamation — unreliable, not deterministic |
Q2: Why was finalize() deprecated, and what replaced it? finalize() was deprecated because:
- No timing guarantee — GC may never call it, or delay it indefinitely, risking resource leaks (file handles, sockets).
- Performance overhead — objects with
finalize()require an extra GC pass since they need finalization before reclamation. - Exception swallowing — exceptions thrown in
finalize()are ignored, hiding bugs.
Replacement: try-with-resources combined with the Cleaner API (java.lang.ref.Cleaner) for deterministic and safe resource cleanup, or implementing AutoCloseable explicitly.
Q3: Can a final variable be a mutable object? Explain with an example relevant to concurrency. Yes — final only prevents reference reassignment, not mutation of the object’s internal state. Example: final List<String> list = new ArrayList<>(); — you can still list.add("x"), but cannot do list = new ArrayList<>(). This distinction matters in concurrency: declaring a field final does not make it thread-safe; you still need synchronization or thread-safe collections if the object itself is mutable and shared across threads.
1.7 Exception Handling (Checked vs Unchecked, Custom Exceptions, try-with-resources)
Q1: Differentiate checked and unchecked exceptions, with architectural guidance on when to use each.
- Checked exceptions (extend
Exception, excludingRuntimeException): Must be declared or handled (throwsor try-catch). Used for recoverable conditions the caller is expected to handle, e.g.,IOException,SQLException. - Unchecked exceptions (extend
RuntimeException): Not enforced by the compiler. Used for programming errors or conditions that are typically unrecoverable at the call site, e.g.,NullPointerException,IllegalArgumentException.
Architect-level guidance: In modern enterprise Java (especially Spring-based microservices), many teams prefer unchecked exceptions even for business-rule violations (e.g., a custom InsufficientFundsException extends RuntimeException), because checked exceptions tend to pollute method signatures across layers and create tight coupling between layers via throws declarations — particularly painful with functional interfaces and Streams, which don’t support checked exceptions in lambdas at all.
Q2: How do you design a custom exception hierarchy for a microservice? Typical pattern:
public abstract class ServiceException extends RuntimeException {
private final String errorCode;
public ServiceException(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
}
}
public class ResourceNotFoundException extends ServiceException { ... }
public class ValidationException extends ServiceException { ... }
This allows a global @ExceptionHandler (or @ControllerAdvice in Spring) to catch ServiceException at a high level and map errorCode to consistent HTTP status codes and client-facing error payloads, while still allowing fine-grained catches where needed.
Q3: Explain try-with-resources and how it improves on traditional try-finally for resource management. try-with-resources (Java 7+) automatically closes any resource implementing AutoCloseable/Closeable at the end of the block, even if an exception occurs — without needing an explicit finally block.
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
// use resources
} catch (SQLException e) {
// handle
}
Key advantage over manual finally: it correctly handles suppressed exceptions — if both the try block and the close() call throw exceptions, the original exception is preserved as primary and the close() exception is attached via getSuppressed(), rather than silently overwriting it (a common bug in hand-written finally blocks).
Q4: Architect-level — How should exception handling differ between a synchronous REST API and an asynchronous/event-driven microservice?
- Synchronous REST: Exceptions map cleanly to HTTP status codes via centralized handlers (
@ControllerAdvice), with structured error responses (RFC 7807 Problem Details is a common standard). - Asynchronous/event-driven (Kafka consumers, message queues): There’s no caller waiting for an HTTP response, so exceptions must be handled via dead-letter queues (DLQ), retry policies with backoff, and idempotent reprocessing — since simply throwing an exception doesn’t “return” an error to anyone; it can silently halt a consumer thread or cause message loss if not handled deliberately.