Core Java Interview Questions and Answers
These questions span Java fundamentals through modern Java 21 features β what hiring teams actually test for Java developer, backend engineer, and data engineer positions.
Object-Oriented Programming
Q1. Explain the four pillars of OOP with Java examples.
Encapsulation β bundle data and methods, restrict direct access:
public class BankAccount { private double balance; // Hidden state
public void deposit(double amount) { if (amount > 0) balance += amount; }
public double getBalance() { return balance; }}Inheritance β subclass inherits and extends parent behavior:
abstract class Shape { abstract double area();}class Circle extends Shape { double radius; Circle(double r) { this.radius = r; }
@Override double area() { return Math.PI * radius * radius; }}Polymorphism β same interface, different behavior:
Shape s1 = new Circle(5);Shape s2 = new Rectangle(4, 6);System.out.println(s1.area()); // 78.54 β runtime dispatchSystem.out.println(s2.area()); // 24.0Abstraction β expose what, hide how (via abstract classes or interfaces).
Q2. What is the difference between an abstract class and an interface in Java?
// Abstract class β partial implementation, single inheritanceabstract class Animal { String name; Animal(String name) { this.name = name; } // Can have constructor abstract void makeSound(); void breathe() { System.out.println("breathing"); } // Concrete method}
// Interface β contract, multiple implementation (Java 8+: default methods)interface Flyable { int MAX_ALTITUDE = 10000; // Implicitly public static final void fly(); // Implicitly public abstract default void land() { // Default method (Java 8+) System.out.println("landing"); } static Flyable createBird() { return new Bird(); } // Static method (Java 8+)}Key differences:
- A class can implement multiple interfaces but extend only one class
- Abstract classes can have constructors, mutable fields, any access modifier
- Interfaces are best for capability contracts; abstract classes for shared implementation hierarchies
- Java 9+ interfaces can have
privatemethods for code sharing between defaults
Q3. What is the difference between == and .equals() in Java?
==compares references (memory addresses) for objects; values for primitives.equals()compares logical equality (content) β must be overridden meaningfully
String a = new String("hello");String b = new String("hello");
System.out.println(a == b); // false β different objectsSystem.out.println(a.equals(b)); // true β same content
Integer x = 127;Integer y = 127;System.out.println(x == y); // true β Integer cache (-128 to 127)
Integer p = 200;Integer q = 200;System.out.println(p == q); // false β outside cache rangeAlways use .equals() for object comparison. The Integer cache gotcha is a classic interview trap.
Collections & Generics
Q4. Explain the Java Collections Framework hierarchy.
Iterable βββ Collection βββ List (ordered, allows duplicates) β βββ ArrayList β O(1) get, O(n) insert/delete in middle β βββ LinkedList β O(n) get, O(1) insert/delete at known node β βββ Vector β synchronized (prefer CopyOnWriteArrayList) βββ Set (no duplicates) β βββ HashSet β O(1) add/contains, no order β βββ LinkedHashSet β insertion order, O(1) β βββ TreeSet β sorted order, O(log n) βββ Queue βββ PriorityQueue β heap-based, O(log n) insert/poll βββ ArrayDeque β double-ended queue, O(1) amortized
Map (key-value, not Collection) βββ HashMap β O(1) avg, no order βββ LinkedHashMap β insertion order βββ TreeMap β sorted by key, O(log n) βββ ConcurrentHashMap β thread-safe, no full lockingQ5. What happens internally in a HashMap when you put an element?
map.put(key, value);- Compute
key.hashCode()and apply hash spread:hash = (h = key.hashCode()) ^ (h >>> 16) - Index into the bucket array:
index = hash & (capacity - 1) - If bucket is empty: create a new
Node<K,V>and place it - If bucket has entries: traverse the linked list (or tree if long):
- If
key.equals(existingKey): replace value - Otherwise: append new node
- If
- If the list in a bucket exceeds 8 entries AND capacity β₯ 64: convert to red-black tree (O(log n))
- If load factor exceeded (default 0.75): resize and rehash (O(n) operation)
Key insight: equals() and hashCode() must be overridden consistently. Two equal objects must have the same hashCode; two objects with the same hashCode need not be equal (hash collision).
Multithreading & Concurrency
Q6. What is the difference between synchronized, ReentrantLock, and volatile?
synchronized β intrinsic lock on an object or class. Simplest; non-interruptible; no timeout.
synchronized (this) { /* critical section */ }public synchronized void increment() { count++; }ReentrantLock β explicit lock with more control:
Lock lock = new ReentrantLock();if (lock.tryLock(200, TimeUnit.MILLISECONDS)) { try { /* critical section */ } finally { lock.unlock(); }}Advantages: tryLock() with timeout, interruptible locking, fair ordering option, multiple condition variables.
volatile β guarantees visibility (reads/writes go to main memory, not CPU cache). Does NOT guarantee atomicity for compound operations.
volatile boolean running = true; // Thread-safe flag β one thread stops another// But: volatile int count; count++; NOT thread-safe (read-modify-write)Q7. What are virtual threads in Java 21 and why are they significant?
Virtual threads (Project Loom, JDK 21 stable) are lightweight threads managed by the JVM, not the OS. Millions can be created without memory exhaustion:
// Platform thread (OS-backed, ~1MB stack, thousands max)Thread.ofPlatform().start(() -> { ... });
// Virtual thread (JVM-managed, tiny footprint, millions possible)Thread.ofVirtual().start(() -> { ... });
// Or with executortry (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 1_000_000; i++) { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); // Doesn't block OS thread! return processRequest(); }); }}Why significant:
- Blocking I/O on a virtual thread suspends the virtual thread but releases the carrier (OS) thread β no thread pool starvation
- Enables the simple blocking-code style that developers prefer without the thread-per-request scalability limit
- Dramatically simplifies concurrent programming compared to reactive/CompletableFuture patterns
Q8. What is the Java Memory Model (JMM) and what is the happens-before relationship?
The JMM defines how threads interact through memory and what values a thread is guaranteed to see when reading a field written by another thread.
Happens-before is a guarantee that memory writes by one thread are visible to reads by another:
- Within a thread, each action happens-before the next
synchronizedunlock on a monitor happens-before subsequent lock on the same monitor- A
volatilewrite happens-before subsequent reads of the same variable - Thread
start()happens-before any actions in the started thread - Thread completion happens-before
join()returns
Without happens-before, threads may read stale cached values. Proper synchronization establishes happens-before chains.
Functional & Streams
Q9. Explain Java Streams and the difference between intermediate and terminal operations.
Streams process collections declaratively and lazily:
List<Employee> employees = getEmployees();
// Processing pipelineMap<String, Double> avgSalaryByDept = employees.stream() .filter(e -> e.getSalary() > 50_000) // Intermediate β lazy .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.averagingDouble(Employee::getSalary) // Terminal β triggers execution ));Intermediate operations (lazy, return a Stream):
filter, map, flatMap, distinct, sorted, limit, skip, peek
Terminal operations (trigger execution, return a result):
collect, forEach, count, findFirst, anyMatch, reduce, toList (Java 16+)
Streams donβt modify the source and are not reusable β a terminal operation βconsumesβ the stream.
Q10. What are the functional interfaces in the java.util.function package?
| Interface | Signature | Use case |
|---|---|---|
Predicate<T> | T β boolean | Filter, test conditions |
Function<T,R> | T β R | Transform/map values |
Consumer<T> | T β void | Side effects (forEach) |
Supplier<T> | () β T | Lazy value creation |
BiFunction<T,U,R> | (T,U) β R | Two-argument transform |
UnaryOperator<T> | T β T | Transform same type |
BinaryOperator<T> | (T,T) β T | Reduce two same-type values |
Predicate<String> isLong = s -> s.length() > 10;Function<String, Integer> length = String::length; // Method referenceConsumer<String> print = System.out::println;Supplier<List<String>> newList = ArrayList::new;
// CompositionPredicate<String> isShort = isLong.negate();Function<String, String> upperThenTrim = ((Function<String,String>)String::toUpperCase).andThen(String::trim);JVM & Modern Java
Q11. Explain the JVMβs garbage collection and the difference between G1 and ZGC.
The JVM heap is divided into generations (young, old) for most GCs:
- Young generation (Eden + Survivors): where new objects are born. Collected frequently (minor GC).
- Old generation: long-lived objects promoted from young. Collected less often (major/full GC).
| GC | Pause | Throughput | Best for |
|---|---|---|---|
| G1 GC | Low (region-based) | High | Default since Java 9; most apps |
| ZGC | Ultra-low (< 1ms) | Moderate | Latency-critical, huge heaps (TB) |
| Shenandoah | Ultra-low | Moderate | Similar to ZGC, Red Hat |
| Serial GC | High | Low | Single-core, embedded |
ZGC (now production-ready since Java 15, improved in 21) performs most GC work concurrently without stopping application threads, achieving sub-millisecond pause times regardless of heap size.
Q12. What are records in Java and when would you use them?
Records (Java 16+) are immutable data carriers with automatic boilerplate:
// Traditional class β 30+ lines for the same thingrecord Point(double x, double y) {}
// Auto-generated: constructor, getters, equals, hashCode, toStringPoint p = new Point(3.0, 4.0);System.out.println(p.x()); // 3.0System.out.println(p); // Point[x=3.0, y=4.0]
// Custom compact constructor for validationrecord Temperature(double celsius) { Temperature { if (celsius < -273.15) throw new IllegalArgumentException("Below absolute zero"); } double toFahrenheit() { return celsius * 9/5 + 32; }}Use records for DTOs, command objects, value objects β anywhere you want immutable, transparent data containers. They cannot extend other classes and fields are implicitly final.
Q13. What are sealed classes in Java and what problem do they solve?
Sealed classes (Java 17+) restrict which classes can extend or implement them:
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}record Rectangle(double width, double height) implements Shape {}record Triangle(double base, double height) implements Shape {}
// Pattern matching with switch (Java 21 β stable)double area = switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rectangle r -> r.width() * r.height(); case Triangle t -> 0.5 * t.base() * t.height();};// Compiler verifies exhaustiveness β no default needed!Sealed classes enable exhaustive pattern matching β the compiler guarantees all cases are handled, making refactoring safer.
Q14. Explain Javaβs Optional and when to use (and not use) it.
Optional<T> wraps a value that might be null, making the null case explicit in the type system:
// GOOD: return type from methods that might not return a valuepublic Optional<User> findById(Long id) { return userRepository.findById(id);}
// Consuming OptionalfindById(42L) .map(User::getEmail) .filter(email -> email.endsWith("@company.com")) .ifPresent(email -> sendWelcomeEmail(email));
// With default valueString name = findById(42L) .map(User::getName) .orElse("Guest");Donβt use Optional for:
- Field types (poor serialization, no performance benefit)
- Method parameters (just check for null)
- Collections (return empty collection instead of
Optional<List>) - Performance-critical code (Optional is an object, adds allocation overhead)
Q15. What is the difference between Comparable and Comparator?
// Comparable β natural ordering defined in the class itselfclass Employee implements Comparable<Employee> { String name; int salary;
@Override public int compareTo(Employee other) { return Integer.compare(this.salary, other.salary); // Natural: by salary }}
// Comparator β external, flexible orderingComparator<Employee> byName = Comparator.comparing(Employee::getName);Comparator<Employee> bySalaryDesc = Comparator.comparingInt(Employee::getSalary).reversed();Comparator<Employee> compound = byName.thenComparingInt(Employee::getSalary);
List<Employee> employees = getEmployees();employees.sort(bySalaryDesc);TreeMap<Employee, String> sorted = new TreeMap<>(byName); // Use as key comparatorUse Comparable for the default natural ordering (makes sense as a property of the class). Use Comparator when you need multiple ordering strategies or ordering on classes you donβt own.