Java Interview Questions: Core Concepts, Concurrency, and JVM (2025)

Java remains the dominant language at Amazon, Google, Goldman Sachs, and most enterprise tech companies. This guide covers the Java interview questions asked at these companies — from core OOP to JVM internals and modern Java features.

Core OOP and Language Features

1. Abstract class vs Interface — when to use each

// Abstract class: partial implementation, shared state, one inheritance
abstract class Shape {
    protected String color;  // Shared state

    public Shape(String color) { this.color = color; }

    public abstract double area();  // Must implement

    public String describe() {  // Default behavior
        return color + " shape with area " + area();
    }
}

// Interface: contract, multiple implementation, default methods (Java 8+)
interface Drawable {
    void draw();  // Abstract by default

    default void drawWithBorder() {  // Default method (Java 8+)
        System.out.println("Border");
        draw();
    }

    static Drawable noOp() {  // Static factory method
        return () -> {};  // Lambda implements Drawable
    }
}

// Use abstract class when: shared state/code, IS-A relationship, one parent
// Use interface when: multiple inheritance, capabilities/roles, looser coupling

class Circle extends Shape implements Drawable {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override public double area() { return Math.PI * radius * radius; }
    @Override public void draw()   { System.out.println("Drawing circle r=" + radius); }
}

2. Generics — bounded type parameters and wildcards

import java.util.*;
import java.util.function.*;

// Bounded type parameters
public class MathUtils {
    // T must implement Comparable
    public static <T extends Comparable> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }

    // Multiple bounds
    public static <T extends Comparable & Cloneable> T cloneIfGreater(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
}

// Wildcards: ? extends T (producer/upper bound) vs ? super T (consumer/lower bound)
// PECS: Producer Extends, Consumer Super

class Stack {
    private List elements = new ArrayList();

    public void push(T item) { elements.add(item); }
    public T pop() { return elements.remove(elements.size() - 1); }

    // Producer: we produce T, caller consumes; use ? extends T
    public void pushAll(Iterable src) {
        for (T t : src) push(t);
    }

    // Consumer: destination consumes our T; use ? super T
    public void popAll(Collection dst) {
        while (!elements.isEmpty()) dst.add(pop());
    }
}

// Type erasure: generics are compile-time only; List == List at runtime
// Cannot: new T(), new T[10], instanceof List, T.class

3. equals() and hashCode() contract

import java.util.Objects;

public class Employee {
    private final String id;
    private final String name;
    private int age;  // Not part of identity

    public Employee(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Employee)) return false;
        Employee emp = (Employee) o;
        return Objects.equals(id, emp.id) && Objects.equals(name, emp.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);  // Must include same fields as equals!
    }
}

// Contract:
// 1. equals is reflexive, symmetric, transitive, consistent
// 2. If a.equals(b), then a.hashCode() == b.hashCode()  (required)
// 3. If !a.equals(b), hashCodes MAY differ (ideally do for HashMap performance)
// Violation: break HashMap/HashSet — objects get "lost"

Java Concurrency

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.*;

// AtomicInteger: lock-free compare-and-swap operations
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();           // Atomic increment
counter.compareAndSet(5, 10);        // CAS: if value is 5, set to 10

// ReentrantLock: more control than synchronized
ReentrantLock lock = new ReentrantLock(true);  // true = fair lock
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

void read() throws InterruptedException {
    rwLock.readLock().lock();   // Multiple readers allowed simultaneously
    try { /* read */ } finally { rwLock.readLock().unlock(); }
}

void write() {
    rwLock.writeLock().lock();  // Exclusive writer
    try { /* write */ } finally { rwLock.writeLock().unlock(); }
}

// ExecutorService: thread pool management
ExecutorService pool = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors()
);

// CompletableFuture: async pipelines (Java 8+)
CompletableFuture future = CompletableFuture
    .supplyAsync(() -> fetchUser(1))           // Run async
    .thenApplyAsync(user -> transform(user))   // Chain async step
    .exceptionally(ex -> "fallback")           // Handle error
    .thenCombine(                              // Combine two futures
        CompletableFuture.supplyAsync(() -> fetchOrders(1)),
        (user, orders) -> user + " has " + orders.size() + " orders"
    );

String result = future.get(5, TimeUnit.SECONDS);

// Semaphore: limit concurrent access
Semaphore semaphore = new Semaphore(10);  // Allow 10 concurrent threads
semaphore.acquire();
try { /* access limited resource */ } finally { semaphore.release(); }

// CountDownLatch: wait for N events
CountDownLatch latch = new CountDownLatch(3);
// 3 worker threads each call latch.countDown() when done
latch.await();  // Blocks until count reaches 0

Java Streams and Functional Programming

import java.util.*;
import java.util.stream.*;
import java.util.function.*;

List employees = getEmployees();

// Grouping and aggregation
Map avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

// Flat map: flatten nested collections
List allSkills = employees.stream()
    .flatMap(e -> e.getSkills().stream())
    .distinct()
    .sorted()
    .collect(Collectors.toList());

// Custom collector: top-N per group
Map<String, Optional> topEarnerPerDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.maxBy(Comparator.comparingDouble(Employee::getSalary))
    ));

// Parallel streams: use for CPU-bound work on large datasets
long count = IntStream.range(0, 1_000_000)
    .parallel()
    .filter(n -> isPrime(n))
    .count();

// Method references
List names = employees.stream()
    .map(Employee::getName)          // Instance method ref
    .map(String::toUpperCase)        // Instance method on stream element
    .filter(Objects::nonNull)        // Static method ref
    .collect(Collectors.toList());

// Optional: avoid null pointer exceptions
Optional topEarner = employees.stream()
    .max(Comparator.comparingDouble(Employee::getSalary));

topEarner
    .map(Employee::getName)
    .ifPresentOrElse(
        name -> System.out.println("Top earner: " + name),
        () -> System.out.println("No employees")
    );

JVM Internals

Area Details
Class loading Bootstrap → Extension → Application class loaders (delegation model)
Memory areas Heap (young/old gen), Metaspace (class metadata), Stack (per thread), PC register
GC algorithms G1 (default Java 9+), ZGC (sub-millisecond pauses), Shenandoah, Serial/Parallel
JIT compilation Interpreter → C1 (client) → C2 (server) as method gets hotter
String pool Interned strings in heap (Java 7+); intern() to add manually
volatile keyword Visibility guarantee (CPU cache flush); not atomic for compound operations
happens-before Ordering guarantee: monitor release HB monitor acquire; volatile write HB read

Frequently Asked Questions

What is the difference between an interface and an abstract class in Java?

An interface defines a contract with abstract methods (and default/static methods since Java 8) — a class can implement multiple interfaces. An abstract class can have state (fields), constructors, and both abstract and concrete methods — a class can only extend one abstract class. Use interfaces to define capabilities (Runnable, Comparable); use abstract classes when sharing implementation across related classes.

What is the Java Memory Model and how does volatile work?

The Java Memory Model (JMM) defines how threads interact through memory. Without synchronization, threads may see stale cached values. volatile guarantees visibility: reads always fetch from main memory, writes flush immediately. volatile does NOT provide atomicity for compound operations (check-then-act, increment). For atomicity, use AtomicInteger or synchronized. volatile is correct for single-writer, multiple-reader flags.

What is the difference between HashMap and ConcurrentHashMap?

HashMap is not thread-safe — concurrent modifications cause ConcurrentModificationException or data corruption. ConcurrentHashMap uses segment-level locking (Java 7) or CAS + synchronized on individual buckets (Java 8+), allowing concurrent reads and localized writes with no full-table lock. ConcurrentHashMap does not allow null keys or values. For read-heavy scenarios, ConcurrentHashMap is far superior to a synchronized HashMap.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is the difference between an interface and an abstract class in Java?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “An interface defines a contract with abstract methods (and default/static methods since Java 8) — a class can implement multiple interfaces. An abstract class can have state (fields), constructors, and both abstract and concrete methods — a class can only extend one abstract class. Use interfaces to define capabilities (Runnable, Comparable); use abstract classes when sharing implementation across related classes.”
}
},
{
“@type”: “Question”,
“name”: “What is the Java Memory Model and how does volatile work?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The Java Memory Model (JMM) defines how threads interact through memory. Without synchronization, threads may see stale cached values. volatile guarantees visibility: reads always fetch from main memory, writes flush immediately. volatile does NOT provide atomicity for compound operations (check-then-act, increment). For atomicity, use AtomicInteger or synchronized. volatile is correct for single-writer, multiple-reader flags.”
}
},
{
“@type”: “Question”,
“name”: “What is the difference between HashMap and ConcurrentHashMap?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “HashMap is not thread-safe — concurrent modifications cause ConcurrentModificationException or data corruption. ConcurrentHashMap uses segment-level locking (Java 7) or CAS + synchronized on individual buckets (Java 8+), allowing concurrent reads and localized writes with no full-table lock. ConcurrentHashMap does not allow null keys or values. For read-heavy scenarios, ConcurrentHashMap is far superior to a synchronized HashMap.”
}
}
]
}

Scroll to Top