Back to Blog
Top Java Interview Questions for 2025: Quick Prep Guide
Career & Interview Prep

Top Java Interview Questions for 2025: Quick Prep Guide

Currently interviewing for Java roles? Get the essential questions and answers I'm seeing in 2025 interviews. Covers Virtual Threads, modern concurrency, memory management, and Spring Boot internals with practical examples.

22 viewsJuly 3, 202514 min readRabi
javainterviewcareerconcurrencyspring-bootvirtual-threadsjob-preparation

I'm currently in the middle of Java interviews for 2025-level roles, and I've compiled the essential questions I'm seeing—along with concise answers—so you can level up too.

Whether you're facing senior-level concurrency challenges or junior baseline queries, this guide cuts to the chase. Here's what matters right now.

After sitting through about a dozen technical rounds (both as a candidate and helping friends prep), I noticed the same patterns emerging. Interviewers care less about memorizing syntax and more about understanding concepts deeply enough to explain trade-offs and make real-world decisions.

So here's my battle-tested list of questions that actually came up, with the answers that got me through to the next round.

Core Java Basics (Still Matter in 2025)

What is Java, and why is it still relevant?

TL;DR: Platform-independent, compiled to bytecode, runs on JVM.

Java's "write once, run anywhere" principle comes from its compilation process. When you compile Java code, it becomes bytecode that runs on the Java Virtual Machine, not directly on the operating system. This means the same .class file works on Windows, Linux, and macOS without recompilation.

// This code compiles to bytecode that runs anywhere with a JVM
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Same bytecode, any platform");
    }
}

Why it still matters in 2025: Enterprise systems need stability and cross-platform compatibility. Java delivers both, plus it keeps evolving (Virtual Threads, Pattern Matching, Records) while maintaining backward compatibility.

Real-world connection: In my current role, we deploy the same JAR file to Linux production servers and Windows dev machines. No platform-specific builds needed.

JVM vs JRE vs JDK - What's the Difference?

TL;DR: JDK contains tools to develop, JRE contains runtime to execute, JVM actually runs the bytecode.

This confusion trips up even experienced developers:

  • JDK (Java Development Kit): Contains everything - compiler (javac), debugger, JRE, documentation, etc.
  • JRE (Java Runtime Environment): Just what you need to run Java apps - JVM plus libraries
  • JVM (Java Virtual Machine): The actual engine that executes bytecode
# Development machine needs JDK
javac MyApp.java    # Compiler is in JDK
java MyApp          # Runtime is in JRE (which is in JDK)
 
# Production server only needs JRE
java -jar myapp.jar # Just needs the runtime

Interview tip: I got asked this as a follow-up to "How would you containerize a Java app?" The answer involves using a minimal JRE-based image for production.

Memory & Object Creation

What happens when you type new?

TL;DR: Allocates memory in heap, calls constructor, returns reference.

Here's the step-by-step process:

User user = new User("John", 25);
  1. Memory allocation: JVM allocates space in heap for the User object
  2. Zero initialization: All instance variables set to default values (null, 0, false)
  3. Constructor execution: Runs the constructor code with provided arguments
  4. Reference assignment: Variable user gets the memory address
public class User {
    private String name;    // Step 2: initialized to null
    private int age;        // Step 2: initialized to 0
 
    public User(String name, int age) {  // Step 3: constructor runs
        this.name = name;
        this.age = age;
    }
}

Garbage Collection tie-in: The object stays in heap until no references point to it, then becomes eligible for GC.

What interviewers want to hear: Understanding that objects live in heap, constructors initialize state, and GC manages cleanup automatically.

Heap vs Stack - Where Does What Go?

TL;DR: Stack holds method calls and local variables, Heap holds objects and instance variables.

This visual helped me explain it clearly:

public void processOrder(Order order) {  // 'order' reference goes on Stack
    String status = "PENDING";           // 'status' reference goes on Stack
 
    PaymentInfo payment = new PaymentInfo();  // PaymentInfo object goes on Heap
                                              // 'payment' reference goes on Stack
 
    order.setStatus(status);  // order object lives on Heap
}

Stack characteristics:

  • Fast allocation/deallocation
  • Thread-specific (each thread has its own stack)
  • Limited size (can cause StackOverflowError)
  • Stores method parameters, local variables, return addresses

Heap characteristics:

  • Shared across all threads
  • Garbage collected
  • Larger but slower allocation
  • Stores all objects and instance variables

Real interview follow-up: "What happens if you have a recursive method without a base case?" Answer: StackOverflowError because each recursive call adds a stack frame.

Concurrency & Modern Java Features

Virtual Threads vs Platform Threads (The 2025 Question)

TL;DR: Virtual Threads are lightweight, managed by JVM, perfect for I/O-bound tasks. Platform Threads map 1:1 with OS threads.

This is the question I've been asked in every senior-level interview:

// Platform Thread approach (old way)
ExecutorService executor = Executors.newFixedThreadPool(200);
executor.submit(() -> {
    // This blocks a precious OS thread during I/O
    String result = httpClient.get("https://api.example.com/data");
    processResult(result);
});
 
// Virtual Thread approach (new way)
ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
virtualExecutor.submit(() -> {
    // This parks the virtual thread, freeing the carrier thread
    String result = httpClient.get("https://api.example.com/data");
    processResult(result);
});

Key differences:

| Platform Threads | Virtual Threads | |------------------|-----------------| | Limited (thousands) | Abundant (millions) | | 1:1 with OS threads | Many:few with carrier threads | | Heavy memory overhead | Lightweight | | Good for CPU-bound | Perfect for I/O-bound |

When to use what:

  • CPU-intensive: Platform threads
  • I/O-heavy (database calls, HTTP requests): Virtual threads
  • Mixed workload: Virtual threads with CPU work offloaded to platform thread pool

synchronized vs ReentrantLock - When and Why?

TL;DR: synchronized is simpler, ReentrantLock offers more features like timeouts and fair locking.

// synchronized approach
private final Object lock = new Object();
 
public void synchronizedMethod() {
    synchronized(lock) {
        // Critical section
        sharedResource.update();
    } // automatically releases lock
}
 
// ReentrantLock approach
private final ReentrantLock lock = new ReentrantLock();
 
public void lockMethod() {
    if (lock.tryLock(5, TimeUnit.SECONDS)) {
        try {
            // Critical section
            sharedResource.update();
        } finally {
            lock.unlock(); // must manually release
        }
    } else {
        throw new TimeoutException("Could not acquire lock");
    }
}

Use synchronized when:

  • Simple mutual exclusion
  • Don't need timeouts or interruption
  • Want automatic lock release

Use ReentrantLock when:

  • Need tryLock with timeout
  • Want fair locking (longest-waiting thread gets lock first)
  • Need to interrupt threads waiting for locks
  • Complex locking patterns

Virtual Thread gotcha: synchronized pins virtual threads to carrier threads, defeating the purpose. Use ReentrantLock with virtual threads.

Volatile, ThreadLocal, and Memory Model Basics

TL;DR: volatile ensures visibility across threads, ThreadLocal gives each thread its own copy, JMM defines ordering guarantees.

public class ConcurrencyExample {
    private volatile boolean running = true;  // All threads see updates immediately
    private static ThreadLocal<User> currentUser = new ThreadLocal<>();  // Per-thread storage
 
    public void stopExecution() {
        running = false;  // Without volatile, other threads might not see this change
    }
 
    public void setCurrentUser(User user) {
        currentUser.set(user);  // Each thread has its own User instance
    }
 
    public User getCurrentUser() {
        return currentUser.get();  // Returns this thread's User
    }
}

Volatile use cases:

  • Flags that control loop execution
  • Status variables read by multiple threads
  • Double-checked locking pattern

ThreadLocal use cases:

  • User session information in web applications
  • Database connections per thread
  • SimpleDateFormat (not thread-safe, so wrap in ThreadLocal)

Memory model essentials:

  • Operations within a thread appear sequential (as-if-serial semantics)
  • Cross-thread visibility isn't guaranteed without synchronization
  • happens-before relationship ensures ordering

Advanced Java Concepts

Records and Optional - Modern Java Done Right

TL;DR: Records for immutable data, Optional to avoid null pointer exceptions.

Records eliminate boilerplate for data classes:

// Old way (before Records)
public class UserOld {
    private final String name;
    private final int age;
 
    public UserOld(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public String getName() { return name; }
    public int getAge() { return age; }
 
    @Override
    public boolean equals(Object obj) { /* boilerplate */ }
    @Override
    public int hashCode() { /* boilerplate */ }
    @Override
    public String toString() { /* boilerplate */ }
}
 
// Modern way (Records)
public record User(String name, int age) {
    // Compact constructor for validation
    public User {
        if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
    }
 
    // Custom methods still allowed
    public boolean isAdult() {
        return age >= 18;
    }
}

Optional prevents null pointer exceptions:

// Dangerous (can throw NPE)
public String getUserEmail(Long userId) {
    User user = userRepository.findById(userId);
    return user.getEmail().toLowerCase();  // NPE if user is null
}
 
// Safe with Optional
public Optional<String> getUserEmail(Long userId) {
    return userRepository.findById(userId)
        .map(User::getEmail)
        .map(String::toLowerCase);
}
 
// Usage
getUserEmail(123L)
    .ifPresentOrElse(
        email -> sendEmail(email),
        () -> log.warn("User not found")
    );

Interview tip: Show you understand when NOT to use Optional (method parameters, fields in classes, collections).

Spring Boot Internals - How Annotations Really Work

TL;DR: Spring uses reflection and proxy pattern to implement dependency injection and AOP.

@RestController
@RequestMapping("/api/users")
public class UserController {
 
    @Autowired  // How does this work?
    private UserService userService;
 
    @GetMapping("/{id}")  // How does Spring know this is a GET endpoint?
    @Transactional       // How does this create a transaction?
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }
}

Behind the scenes:

  1. Component Scanning: Spring scans classpath for @Component, @Service, @Controller annotations
  2. Bean Creation: Creates instances and stores them in ApplicationContext
  3. Dependency Injection: Uses reflection to inject dependencies into @Autowired fields/constructors
  4. Proxy Creation: For @Transactional, Spring creates a proxy that wraps your method with transaction logic
// Simplified version of what Spring does
public class SpringMagicSimplified {
 
    public Object createTransactionalProxy(Object target) {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            (proxy, method, args) -> {
                // Start transaction
                Transaction tx = transactionManager.begin();
                try {
                    Object result = method.invoke(target, args);
                    tx.commit();
                    return result;
                } catch (Exception e) {
                    tx.rollback();
                    throw e;
                }
            }
        );
    }
}

Common follow-up: "Why doesn't @Transactional work when called from the same class?" Answer: Because Spring's proxy only intercepts external calls, not internal method calls.

Coding & Problem-Solving

Common Puzzles That Actually Come Up

Reverse a String (with a twist):

// Basic version
public String reverse(String str) {
    return new StringBuilder(str).reverse().toString();
}
 
// Interview twist: "Do it without StringBuilder"
public String reverseManually(String str) {
    char[] chars = str.toCharArray();
    int left = 0, right = chars.length - 1;
 
    while (left < right) {
        char temp = chars[left];
        chars[left] = chars[right];
        chars[right] = temp;
        left++;
        right--;
    }
 
    return new String(chars);
}
 
// Senior-level twist: "Handle Unicode properly"
public String reverseUnicode(String str) {
    return new StringBuilder(str).reverse().toString(); // StringBuilder handles surrogates correctly
}

Detect and Prevent Deadlocks:

// Deadlock scenario
public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
 
    public void method1() {
        synchronized(lock1) {
            synchronized(lock2) {  // Potential deadlock if another thread locks in reverse order
                // Critical section
            }
        }
    }
 
    public void method2() {
        synchronized(lock2) {
            synchronized(lock1) {  // Deadlock with method1!
                // Critical section
            }
        }
    }
}
 
// Solution: Always acquire locks in the same order
public class DeadlockSafe {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
 
    public void method1() {
        synchronized(lock1) {    // Always lock1 first
            synchronized(lock2) {
                // Critical section
            }
        }
    }
 
    public void method2() {
        synchronized(lock1) {    // Always lock1 first
            synchronized(lock2) {
                // Critical section
            }
        }
    }
}

Implement a Basic LRU Cache:

public class LRUCache<K, V> {
    private final int capacity;
    private final Map<K, Node<K, V>> cache;
    private final Node<K, V> head;
    private final Node<K, V> tail;
 
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>();
        this.head = new Node<>(null, null);
        this.tail = new Node<>(null, null);
        head.next = tail;
        tail.prev = head;
    }
 
    public V get(K key) {
        Node<K, V> node = cache.get(key);
        if (node == null) return null;
 
        moveToHead(node);  // Mark as recently used
        return node.value;
    }
 
    public void put(K key, V value) {
        Node<K, V> existing = cache.get(key);
 
        if (existing != null) {
            existing.value = value;
            moveToHead(existing);
        } else {
            Node<K, V> newNode = new Node<>(key, value);
            cache.put(key, newNode);
            addToHead(newNode);
 
            if (cache.size() > capacity) {
                Node<K, V> tail = removeTail();
                cache.remove(tail.key);
            }
        }
    }
 
    // Helper methods for doubly linked list operations...
}

Performance & Security Awareness

Memory Leaks & Garbage Collection

TL;DR: Know common leak sources and how to detect them.

Common memory leaks in Java:

// 1. Static collections that grow indefinitely
public class LeakyCache {
    private static Map<String, Object> cache = new HashMap<>();  // Never cleared!
 
    public static void addToCache(String key, Object value) {
        cache.put(key, value);  // Grows forever
    }
}
 
// 2. Listeners not removed
public class LeakyListener {
    public void registerListener() {
        SomeGlobalService.addListener(event -> {
            // This creates a reference to the outer class
            processEvent(event);
        });
        // Forgot to remove listener - object can't be GC'd
    }
}
 
// 3. ThreadLocal not cleaned up
public class LeakyThreadLocal {
    private static ThreadLocal<ExpensiveObject> threadLocal = new ThreadLocal<>();
 
    public void doWork() {
        threadLocal.set(new ExpensiveObject());
        // Forgot to call threadLocal.remove() - memory leak in thread pools
    }
}

Detection strategies:

  • Heap dumps (jmap, VisualVM, Eclipse MAT)
  • GC logs (-XX:+PrintGCDetails)
  • Application monitoring (Micrometer, AppDynamics)

GC tuning basics:

# G1GC (good default for most applications)
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 MyApp
 
# ZGC (ultra-low latency)
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC MyApp

Deserialization Vulnerabilities

TL;DR: Never deserialize untrusted data without validation.

// Dangerous - can lead to RCE
public User deserializeUser(byte[] data) {
    try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
        return (User) ois.readObject();  // Attacker can execute arbitrary code!
    }
}
 
// Safer approach - validate before deserializing
public User deserializeUserSafely(String json) {
    ObjectMapper mapper = new ObjectMapper();
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
 
    try {
        User user = mapper.readValue(json, User.class);
        validateUser(user);  // Business logic validation
        return user;
    } catch (JsonProcessingException e) {
        throw new InvalidDataException("Invalid user data", e);
    }
}
 
// Best approach - use Records with validation
public record ValidatedUser(
    @NotBlank String username,
    @Email String email,
    @Min(13) int age
) {
    public ValidatedUser {
        // Compact constructor validation
        Objects.requireNonNull(username, "Username required");
        Objects.requireNonNull(email, "Email required");
    }
}

Final Tips for Crushing Java Interviews

How to Structure Your Answers

The winning formula:

  1. TL;DR - Give the short answer first
  2. Explain the concept - Show you understand the underlying principles
  3. Provide an example - Code snippet or real-world scenario
  4. Mention trade-offs - Show you think about pros/cons
  5. Connect to experience - "In my last project, we used this because..."

Before You Answer, Clarify Assumptions

Good questions to ask:

  • "Are we talking about single-threaded or multi-threaded environment?"
  • "Should I optimize for memory or speed?"
  • "What's the expected scale - thousands or millions of operations?"
  • "Are there any constraints on external libraries?"

Connect Everything to Real-World Experience

Instead of: "HashMap uses array of buckets with linked lists."

Try: "HashMap uses array of buckets with linked lists. In our user session cache, we chose HashMap over TreeMap because we needed O(1) lookups and didn't care about ordering. But we had to be careful about thread safety since multiple requests could access the same session data."

Red Flags to Avoid

"I don't know" (without trying to reason through it) ✅ "I'm not certain, but here's how I'd approach it..."

Memorized answers without understandingExplaining the reasoning behind design decisions

Only theoretical knowledgeConnecting concepts to practical experience


The Questions You Should Ask Them

Turn the interview around with smart questions:

  • "What's your approach to handling technical debt?"
  • "How do you balance feature delivery with code quality?"
  • "What's the most interesting technical challenge the team solved recently?"
  • "How do you stay current with Java's rapid release cycle?"

Remember, you're interviewing them too. A company that can't answer these thoughtfully might not be the right fit.


Quick confession: I bombed my first few 2025 interviews because I focused too much on memorizing APIs instead of understanding concepts. Once I shifted to explaining trade-offs and connecting everything to real experience, the conversations became much more natural.

The Java ecosystem keeps evolving, but the fundamentals—understanding memory, concurrency, and writing maintainable code—never go out of style. Master these concepts, practice explaining them clearly, and you'll do great.

Good luck with your interviews! 🚀

Liked the blog?

Share it with your friends and help them learn something new!