Java Coding Interview Questions + Answers (With Code Examples)

Maaike van Putten
Maaike van Putten
hero image

Preparing for a Java developer interview can feel overwhelming, especially when you're unsure what to expect. The good news? Java is one of the most widely used programming languages, and many of the questions you'll encounter are based on foundational principles and real-world applications.

This guide breaks down commonly asked Java interview questions into three levels: beginner, intermediate, and advanced. So whether you're brushing up on basics like OOP principles or diving deep into topics like multithreading and JVM internals, we've got you covered. Each question includes a detailed answer to help you not only understand the concept but also confidently explain it during your interview.

Ready to ace your Java coding interview? Let’s dive in.

Sidenote: If you find that you’re struggling with the questions in this guide, or perhaps feel that you could use some more training, or simply want to build some more impressive projects for your portfolio, then check out my Java programming bootcamp:

Learn Java coding

Updated for 2025, you'll learn Java programming fundamentals all the way from complete beginner to advanced skills.

Better still, you’ll be able to reinforce your learning with over 80 exercises and 18 quizzes. This is the only course you need to go from complete programming beginner to being able to get hired as a Backend Developer!

With that out of the way, let’s get into these interview questions!

Beginner Java coding interview questions

These questions focus on the fundamentals of Java, covering essential concepts that every Java developer should know.

Mastering these basics not only sets the stage for more advanced topics but also demonstrates to interviewers that you have a solid grasp of the language.

#1. What is the Java Virtual Machine (JVM), and how does it work?

The Java Virtual Machine (JVM) is a runtime engine that allows Java programs to execute on any device with a JVM implementation. It is central to Java’s "Write Once, Run Anywhere" (WORA) promise by abstracting platform-specific details and providing a consistent execution environment.

Here’s how the JVM operates:

  1. Compilation: Java source code (.java files) is compiled into platform-independent bytecode (.class files) by the Java Compiler (javac)
  2. Class Loading: The JVM dynamically loads the required bytecode into memory during execution
  3. Bytecode Verification: The verifier checks the bytecode for memory safety and security, ensuring it adheres to Java’s strict standards
  4. Execution: The bytecode is executed by either interpreting it line by line or compiling it to native machine code using the Just-In-Time (JIT) compiler for performance optimization

With updates like Project Loom in 2025, Java has introduced lightweight virtual threads, significantly improving its ability to handle concurrent workloads efficiently without the heavy overhead of traditional threads.

Why it matters in practice:

Understanding the JVM helps developers debug performance bottlenecks, write platform-agnostic code, and optimize applications for scalability. Interviewers often ask about the JVM to evaluate how well candidates grasp the underlying mechanics of Java.

#2. What are the main features of Java?

Java remains one of the most popular programming languages, thanks to its powerful features and consistent updates that keep it relevant. These key features include:

  1. Platform Independence: Java's "Write Once, Run Anywhere" (WORA) principle ensures that bytecode compiled on one platform can run on any other platform with a Java Virtual Machine (JVM)
  2. Object-Oriented Programming (OOP): Java adheres to OOP principles such as encapsulation, inheritance, polymorphism, and abstraction. This design makes code modular, reusable, and easy to maintain
  3. Robustness: Java minimizes runtime errors through features like automatic memory management, garbage collection, and strong exception handling
  4. Security: Java’s security manager and classloader provide mechanisms to restrict untrusted code, protecting sensitive resources
  5. Multithreading: Java’s built-in multithreading support allows simultaneous execution of multiple tasks, improving performance in applications requiring parallel processing. With Project Loom’s virtual threads (introduced in recent Java versions), multithreading is now lighter and more efficient, reducing the overhead of traditional threads
  6. Rich Standard Library: Java’s extensive standard library supports operations ranging from data structure manipulation to advanced networking and concurrency
  7. High Performance: The Just-In-Time (JIT) compiler improves runtime performance by optimizing bytecode execution and applying techniques like inlining and loop unrolling.
  8. Scalability: Java scales seamlessly from small applications to large enterprise systems. Recent enhancements, like the Java Foreign Function & Memory API, improve integration with native code, making it a strong choice for high-performance systems

#3. What is the difference between == and .equals() in Java?

In Java, == and .equals() are both used for comparison but serve distinct purposes:

== compares the memory addresses of objects (reference comparison). For primitives, it compares values.

For example

int a = 5;
int b = 5;
System.out.println(a == b); // true (values are the same)

String str1 = new String("Java");
String str2 = new String("Java");
System.out.println(str1 == str2); // false (different memory addresses)

While .equals() compares the logical equality or content of two objects. By default, the .equals() method in the Object class behaves like ==, but many classes (e.g., String) override it to compare content.

For example

System.out.println(str1.equals(str2)); // true (contents are the same)

Why it matters in practice:

Misunderstanding the difference between == and .equals() can lead to subtle bugs, especially when comparing objects. For instance, using == to compare String values fetched from a database or API often yields unexpected results due to different memory locations.

This is a popular interview question because it tests your understanding of Java’s object model and equality principles.

#4. What are Java’s access modifiers, and how are they used?

Access modifiers define the scope and accessibility of classes, methods, and properties. Java provides four main access modifiers:

  1. public: Accessible from any class
  2. protected: Accessible within the same package and by subclasses outside the package
  3. default (package-private): Accessible only within the same package (no keyword needed)
  4. private: Accessible only within the same class

For example

package accessmodifiers;

public class Example {
	public int publicVar = 1;
	protected int protectedVar = 2;
	int defaultVar = 3; // No modifier specified
	private int privateVar = 4;

	public void show() {
    	System.out.println(privateVar); // Accessible within the same class
	}
}

Access modifiers enforce encapsulation, helping to protect sensitive data and control access to critical methods.

Why it matters in practice:

Mastering access modifiers ensures secure and maintainable code. Interviewers may test your ability to use them effectively, especially when designing APIs or structuring large applications.

#5. What is the difference between final, finally, and finalize in Java?

These three terms sound similar but have distinct purposes in Java.

Final

Final is a keyword used to declare constants, prevent method overriding, or prevent inheritance of a class.

  • Final variable: The value cannot be changed once assigned

For example

final int maxValue = 100;
maxValue = 200; // Error: cannot assign a value to final variable
  • Final method: The method cannot be overridden by subclasses
  • Final class: The class cannot be extended by other classes (e.g., java.lang.String)

Finally

Finally is a block in exception handling that executes after the try-catch block, regardless of whether an exception was thrown.

For example

try {
	int result = 10 / 0;
} catch (ArithmeticException e) {
	System.out.println("Caught an exception");
} finally {
	System.out.println("This will always execute");
}

Finalize

Finalize is a method called by the Garbage Collector before an object is destroyed. It is rarely used in modern Java due to its unpredictability and has been deprecated since Java 9.

(Developers are encouraged to use alternative cleanup mechanisms like try-with-resources or explicit resource management).

Why it matters in practice:

These keywords demonstrate Java’s ability to handle code structure (final), resource management (finally), and memory cleanup (finalize). Interviewers often test this to assess your understanding of Java’s lifecycle and exception handling.

#6. What is the difference between an abstract class and an interface?

Both abstract classes and interfaces are used to achieve abstraction in Java, but they differ in design and use cases.

Abstract Class

  • Can have both abstract methods (without implementation) and concrete methods (with implementation)
  • Supports fields with any access modifier
  • A class can only extend one abstract class (single inheritance)
  • Use abstract classes when you need a base class with shared state or behavior

For example

abstract class Animal {
	abstract void sound(); // Abstract method
	void sleep() { // Concrete method
    	System.out.println("Sleeping...");
	}
}
class Dog extends Animal {
	void sound() {
    	System.out.println("Bark");
	}
}

Interface

  • All methods in an interface are public and abstract by default (prior to Java 8). Starting with Java 8, interfaces can include default methods (with implementation) and static methods. From Java 9, interfaces can also include private methods to enhance code organization and modularity
  • Fields are implicitly public static final
  • A class can implement multiple interfaces (multiple inheritance)
  • Use interfaces for defining a contract that multiple unrelated classes can implement

For example

interface Pet {
	void play();
	default void info() {
    	System.out.println("This is a pet.");
	}
}
class Cat implements Pet {
	public void play() {
    	System.out.println("The cat is playing.");
	}
}

Why it matters in practice

Understanding when to use abstract classes versus interfaces is key to designing scalable and maintainable object-oriented systems. With Java’s evolving capabilities, such as default methods in interfaces (Java 8+) and sealed classes (Java 17+), interviewers may explore how you would approach real-world scenarios requiring abstraction.

#7. What is the difference between a constructor and a method in Java?

A constructor and a method may look similar, but they have distinct roles in Java.

Constructor

A constructor is a special block of code that initializes a newly created object. It has the same name as the class and does not have a return type, not even void. Constructors can be overloaded to provide multiple ways to initialize an object.

For example

class Person {
	String name;

	// Constructor
	Person(String name) {
    	this.name = name;
	}
}

Method

A method is a block of code designed to perform specific actions. Unlike a constructor, methods must have a return type or void if no value is returned. Methods can also be static, abstract, or final, while constructors cannot.

For example

class Person {
	String name;

	Person(String name) {
    	this.name = name;
	}

	// Method
	void greet() {
    	System.out.println("Hello, my name is " + name);
	}
}

Key differences:

  • A constructor is invoked using the new ClassName() syntax when creating an object, while methods must be explicitly called on an object or class.
  • Constructors cannot be static, abstract, or final, whereas methods can use these modifiers

Why it matters in practice

Understanding constructors and methods is crucial for creating well-structured, reusable, and maintainable code. Interviewers often test your understanding of constructors, particularly when dealing with object initialization in frameworks like Spring or Hibernate.

#8. What is the difference between String, StringBuilder, and StringBuffer in Java?

String, StringBuilder, and StringBuffer are all used for handling and manipulating text in Java, but they differ in mutability and thread-safety.

String

A String in Java is immutable, meaning its value cannot be changed once created. Any operation that modifies a String creates a new object. This immutability makes String thread-safe but can lead to performance overhead when performing many concatenations.

For example

String str = "Hello";
str = str + " World"; // A new String object is created

StringBuilder

StringBuilder is mutable, allowing its value to be changed without creating new objects. It is faster than String for string manipulations because it doesn’t create new objects. However, it is not thread-safe, meaning it should not be used in multi-threaded environments.

For example

StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // Modifies the existing object

StringBuffer

StringBuffer is also mutable but is thread-safe. It uses synchronized methods to ensure safe operation in multi-threaded environments. However, this synchronization introduces overhead, making it slower than StringBuilder for single-threaded applications.

For example

StringBuffer sb = new StringBuffer("Hello");
sb.append(" World"); // Modifies the existing object

Key differences:

  • Use String for immutable text or when thread safety is required by default
  • Use StringBuilder for efficient, single-threaded text manipulations
  • Use StringBuffer for thread-safe text manipulations in multi-threaded environments

Why it matters in practice

Understanding these distinctions helps you choose the right tool for tasks involving text manipulation. Interviewers often ask this question to gauge your knowledge of performance trade-offs and thread-safety concerns in Java.

#9. What is a static method in Java, and when would you use it?

A static method in Java belongs to the class rather than any instance of the class. This means it can be called without creating an object of the class.

Static methods are declared using the static keyword and are often used for utility or helper functions that do not depend on object state.

For example

class MathUtils {
	public static int add(int a, int b) {
    	return a + b;
	}
}

// Call the method without creating an object
int sum = MathUtils.add(5, 10);

Characteristics of static methods

  • They can only access static fields and methods directly
  • They cannot use this or super keywords because they are not tied to an instance
  • They are commonly used for utility operations (e.g., Math.max() or Math.sqrt())

Static methods are ideal when functionality is independent of object state. For instance, utility classes like Math or logging frameworks use static methods extensively for operations that don’t need instance-specific data.

Why it matters in practice

Static methods are crucial for designing reusable and efficient code, especially for tasks that don’t require object state. Interviewers may explore scenarios where static methods are appropriate or ask about their limitations, such as their inability to participate in polymorphism.

#10. What is method overloading in Java, and how does it differ from method overriding?

Method overloading occurs when two or more methods in the same class have the same name but different parameter lists (either in the number or type of parameters). It allows methods to perform similar but slightly different tasks.

For example

class Calculator {
	public int add(int a, int b) {
    	return a + b;
	}

	public double add(double a, double b) {
    	return a + b;
	}
}

Method overriding, on the other hand, occurs when a subclass provides a specific implementation for a method already defined in its superclass. Overriding requires the method signature (name and parameters) to remain identical but can include changes in functionality.

For example

class Animal {
	void sound() {
    	System.out.println("Animal makes a sound");
	}
}

class Dog extends Animal {
	@Override
	void sound() {
    	System.out.println("Dog barks");
	}
}

Key differences:

  • Method overloading happens within the same class, while overriding occurs between a superclass and its subclass
  • Overloading involves different parameter lists, while overriding keeps the same method signature
  • Overriding methods can take advantage of polymorphism, whereas overloading cannot

Why it matters in practice

Method overloading provides flexibility within a class, while method overriding allows for polymorphism, a cornerstone of object-oriented programming. Interviewers often explore these concepts to evaluate your understanding of Java’s OOP principles and real-world application.

Intermediate Java coding interview questions

Now that we’ve covered the fundamentals, it’s time to dive into the intermediate-level concepts that test your ability to work with more complex data structures, concurrency, and other essential Java features.

These questions are designed to assess how well you can handle real-world scenarios, optimize your code, and leverage Java’s advanced features effectively.

#11. What are the key differences between Array and ArrayList in Java?

Arrays and ArrayLists are both used to store collections of data in Java, but they differ in their structure and functionality.

Array

An array is a fixed-size data structure that can store elements of a specific data type. It is part of Java’s core language and has better performance because of its simplicity.

For example

int[] numbers = new int[5]; // Array of fixed size
numbers[0] = 10;       	// Adding an element
System.out.println(numbers[0]); // Accessing an element

ArrayList

An ArrayList is a part of the Java Collections framework and provides a resizable array implementation. It offers more functionality, such as dynamic resizing and built-in methods for adding, removing, and searching elements.

For example

import java.util.ArrayList;

ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(10);         	// Adding an element
System.out.println(numbers.get(0)); // Accessing an element

Key differences:

  1. Size: Arrays have a fixed size, while ArrayLists are dynamic and can grow or shrink as needed
  2. Type safety: Arrays can hold primitives and objects, but ArrayLists only hold objects. Generics in ArrayLists ensure type safety
  3. Performance: Arrays are faster because they have less overhead compared to ArrayLists
  4. Features: ArrayLists offer built-in methods like add(), remove(), and contains(), making them easier to work with

Why it matters in practice

Understanding these differences helps you choose the right data structure for the task. For example, arrays are ideal for scenarios where performance is critical and the size is known upfront, while ArrayLists are more flexible for dynamic data.

#12. How does the Collections framework work, and what are its key components?

The Java Collections framework is a set of classes and interfaces designed to simplify working with groups of objects. It provides data structures (e.g., lists, sets, maps) and algorithms (e.g., sorting, searching) to manage, store, and manipulate collections efficiently.

List

The List interface represents an ordered collection that allows duplicate elements. Common implementations include ArrayList, LinkedList, and Vector.

For example

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
System.out.println(names); // Output: [Alice, Bob]

Set

The Set interface represents an unordered collection that does not allow duplicate elements. Common implementations include HashSet, LinkedHashSet, and TreeSet.

For example

Set<Integer> uniqueNumbers = new HashSet<>();
uniqueNumbers.add(1);
uniqueNumbers.add(1);
System.out.println(uniqueNumbers); // Output: [1]

Map

The Map interface represents key-value pairs. Common implementations include HashMap, LinkedHashMap, and TreeMap.

For example

Map<String, Integer> ageMap = new HashMap<>();
ageMap.put("Alice", 30);
ageMap.put("Bob", 25);
System.out.println(ageMap); // Output: {Alice=30, Bob=25}

(We’ll cover these more in a second).

Queue and Deque

The Queue interface represents a collection designed for holding elements prior to processing (FIFO), while Deque supports both FIFO and LIFO operations. Common implementations include LinkedList and ArrayDeque.

Why it matters in practice

The Collections framework provides the foundation for handling data in most Java applications. Interviewers often test your understanding of its components, their differences, and when to use each. Knowing these concepts helps you write cleaner, more efficient code.

#13. What is the difference between HashMap, LinkedHashMap, and TreeMap?

HashMap, LinkedHashMap, and TreeMap are all implementations of the Map interface in Java, but they differ in how they store and manage key-value pairs.

HashMap

HashMap stores key-value pairs in a hash table. It allows one null key and multiple null values. The order of elements is not guaranteed. It is the fastest of the three for most operations like insertion and retrieval.

For example

Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("Alice", 30);
hashMap.put("Bob", 25);
System.out.println(hashMap); // Output: {Bob=25, Alice=30} (order is not guaranteed)

LinkedHashMap

LinkedHashMap extends HashMap and maintains a linked list of entries, preserving the insertion order. It is slightly slower than HashMap due to the additional overhead of maintaining the linked list.

For example

Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("Alice", 30);
linkedHashMap.put("Bob", 25);
System.out.println(linkedHashMap); // Output: {Alice=30, Bob=25} (insertion order maintained)

TreeMap

TreeMap implements the NavigableMap interface and stores key-value pairs in a sorted order based on the natural ordering of keys or a custom comparator. It does not allow null keys.

For example

Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("Alice", 30);
treeMap.put("Bob", 25);
System.out.println(treeMap); // Output: {Alice=30, Bob=25} (keys sorted)

Key differences

  • Order:
    • HashMap: No order guaranteed
    • LinkedHashMap: Maintains insertion order
    • TreeMap: Maintains sorted order
  • Performance:
    • HashMap: O(1) for most operations
    • LinkedHashMap: Slightly slower than HashMap
    • TreeMap: O(log n) due to the underlying Red-Black Tree
  • Null keys:
    • HashMap and LinkedHashMap: Allow one null key
    • TreeMap: Does not allow null keys

Why it matters in practice

Choosing the right Map implementation can drastically impact performance and functionality. Interviewers frequently test this topic to assess your ability to select the best data structure for a given scenario, such as ensuring order or optimizing performance.

#14. How does exception handling work in Java?

Exception handling in Java is a mechanism to manage runtime errors, ensuring the normal flow of a program even when unexpected situations arise. It uses the try, catch, finally, and throw/throws constructs to handle exceptions effectively.

Here are the key concepts of exception handling.

Checked and unchecked exceptions

  • Checked exceptions are checked at compile time (e.g., IOException, SQLException), requiring you to handle or declare them using the throws keyword
  • Unchecked exceptions are not checked at compile time and typically result from programming errors (e.g., NullPointerException, ArithmeticException)

For example

import java.io.*;

class Example {
	void readFile(String fileName) throws IOException {
    	BufferedReader reader = new BufferedReader(new FileReader(fileName));
    	System.out.println(reader.readLine());
    	reader.close();
	}
}

Try-catch block

A try block contains code that may throw an exception, and the catch block handles the exception.

For example

try {
	int result = 10 / 0;
} catch (ArithmeticException e) {
	System.out.println("Cannot divide by zero");
}

Finally block

The finally block always executes, regardless of whether an exception is thrown or caught, and is typically used for cleanup operations.

For example

try {
	BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
} catch (IOException e) {
	System.out.println("File not found");
} finally {
	System.out.println("Cleanup code here");
}

Throw and throws

  • throw Used to explicitly throw an exception
  • throws Declares exceptions that a method may throw

For example

void divide(int a, int b) throws ArithmeticException {
	if (b == 0) throw new ArithmeticException("Cannot divide by zero");
	System.out.println(a / b);
}

Why it matters in practice

Exception handling is critical for building robust, fault-tolerant applications. Interviewers often ask this question to assess your ability to write clean error-handling logic and manage resources effectively.

#15. What are generics in Java, and why are they used?

Generics in Java provide a way to define classes, interfaces, and methods with type parameters. This allows developers to create reusable, type-safe code while avoiding unnecessary casting and runtime errors.

How generics work

Generics enable type parameters that can be specified when defining a class or method. For example, List<T> can hold elements of any type, but the type is fixed at the time of instantiation.

For example

import java.util.ArrayList;

class Example {
	public static void main(String[] args) {
    	// Using generics with ArrayList
    	ArrayList<String> list = new ArrayList<>();
    	list.add("Java");
    	list.add("Generics");

    	// No need to cast during retrieval
    	String firstItem = list.get(0);
    	System.out.println(firstItem);
	}
}

Benefits of generics

  1. Type safety: Ensures that only compatible types are added to a collection, catching errors at compile time
  2. Eliminates casts: Avoids explicit casting when retrieving elements
  3. Code reusability: Allows methods and classes to work with any object type

Generics with methods

Generics can also be applied to methods, making them more flexible.

For example

public <T> void printArray(T[] array) {
	for (T element : array) {
    	System.out.println(element);
	}
}

Bounded type parameters

Generics allow constraints using bounds, such as extends for upper bounds.

For example

public <T extends Number> void printNumber(T number) {
	System.out.println("Number: " + number);
}

Why it matters in practice

Generics reduce runtime errors and make code easier to read and maintain. They are integral to Java’s type-safe Collections framework, which is widely used in real-world applications.

Interviewers often ask about generics to assess your understanding of Java’s type system and how it helps write robust, reusable code.

#16. How does Java implement threads and concurrency?

Java implements threads and concurrency using the Thread class, the Runnable interface, and high-level constructs from the java.util.concurrent package. These tools allow developers to write parallel and concurrent programs to improve performance and efficiency.

Creating threads

Threads can be created in two primary ways:

  • Extending the Thread class
  • Implementing the Runnable interface

For example (Thread class):

class MyThread extends Thread {
	public void run() {
    	System.out.println("Thread is running");
	}
}

public class Main {
	public static void main(String[] args) {
    	MyThread thread = new MyThread();
    	thread.start(); // Start the thread
	}
}

For example (Runnable interface):

class MyTask implements Runnable {
	public void run() {
    	System.out.println("Task is running");
	}
}

public class Main {
	public static void main(String[] args) {
    	Thread thread = new Thread(new MyTask());
    	thread.start(); // Start the thread
	}
}

Concurrency utilities

The java.util.concurrent package provides high-level abstractions like:

  • Executor framework: Manages thread pools (ExecutorService)
  • Locks: Provides explicit locking mechanisms (ReentrantLock)
  • Concurrent collections: Thread-safe versions of collections (ConcurrentHashMap)
  • Synchronization helpers: Classes like CountDownLatch, CyclicBarrier, and Semaphore

For example (ExecutorService):

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
	public static void main(String[] args) {
    	ExecutorService executor = Executors.newFixedThreadPool(2);

    	executor.execute(() -> System.out.println("Task 1 running"));
    	executor.execute(() -> System.out.println("Task 2 running"));

    	executor.shutdown(); // Gracefully shuts down the executor
	}
}

Why it matters in practice

Threads and concurrency are crucial for building efficient, high-performance applications, especially in multi-core environments. Interviewers frequently test this topic to evaluate your understanding of parallel execution and thread safety.

#17. What is the difference between synchronized blocks and methods in Java?

In Java, synchronization is used to prevent multiple threads from accessing shared resources simultaneously, ensuring thread safety. Synchronization can be applied to both methods and code blocks, but they differ in scope and flexibility.

Synchronized methods

A synchronized method locks the entire method, preventing other threads from accessing it until the lock is released. This ensures that only one thread can execute the method at a time.

For example

class Counter {
	private int count = 0;

	public synchronized void increment() {
    	count++;
	}

	public synchronized int getCount() {
    	return count;
	}
}

Synchronized blocks

A synchronized block locks only the specified part of the code, offering finer control over synchronization. This approach allows other threads to execute non-synchronized parts of the method.

For example

class Counter {
	private int count = 0;

	public void increment() {
    	synchronized (this) {
        	count++;
    	}
	}

	public int getCount() {
    	return count;
	}
}

Key differences

  • Scope:
    • Synchronized methods lock the entire method
    • Synchronized blocks lock only the code inside the block
  • Performance:
    • Synchronized methods can be less efficient because they lock the entire method, including unnecessary code
    • Synchronized blocks are more efficient as they minimize the locked section of code
  • Flexibility:
    • Synchronized blocks allow locking on a specific object (this, a custom lock object, etc.)
    • Synchronized methods always lock the instance or class they belong to

For example (custom lock object):

class Counter {
	private int count = 0;
	private final Object lock = new Object();

	public void increment() {
    	synchronized (lock) {
        	count++;
    	}
	}

	public int getCount() {
    	return count;
	}
}

Why it matters in practice

Understanding the differences between synchronized methods and blocks is crucial for writing efficient multi-threaded programs. Interviewers often ask this question to assess your ability to manage shared resources effectively and optimize synchronization for better performance.

#18. How does Java handle file I/O?

Java provides a rich set of APIs for file input and output (I/O) operations, enabling developers to read, write, and manipulate files efficiently. These APIs are primarily found in the java.io and java.nio packages.

File I/O using java.io

The java.io package offers classes for basic file operations, such as File, FileReader, FileWriter, BufferedReader, and BufferedWriter.

For example (reading a file using BufferedReader):

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class FileReadExample {
	public static void main(String[] args) {
    	try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
        	String line;
        	while ((line = reader.readLine()) != null) {
            	System.out.println(line);
        	}
    	} catch (IOException e) {
        	e.printStackTrace();
    	}
	}
}

For example (writing to a file using BufferedWriter):

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class FileWriteExample {
	public static void main(String[] args) {
    	try (BufferedWriter writer = new BufferedWriter(new FileWriter("example.txt"))) {
        	writer.write("Hello, Java I/O!");
    	} catch (IOException e) {
        	e.printStackTrace();
    	}
	}
}

File I/O using java.nio

The java.nio package provides more efficient, non-blocking I/O capabilities with classes like Files and Paths. These classes simplify file operations and improve performance, especially for large files.

For example (reading all lines using Files):

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class NIOReadExample {
	public static void main(String[] args) {
    	try {
        	List<String> lines = Files.readAllLines(Paths.get("example.txt"));
        	lines.forEach(System.out::println);
    	} catch (IOException e) {
        	e.printStackTrace();
    	}
	}
}

For example (writing a file using Files):

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;

public class NIOWriteExample {
	public static void main(String[] args) {
    	try {
        	Files.write(Paths.get("example.txt"), Arrays.asList("Hello, Java NIO!"));
    	} catch (IOException e) {
        	e.printStackTrace();
    	}
	}
}

Why it matters in practice

File I/O is a fundamental aspect of many applications, such as reading configuration files or processing user data. Interviewers may ask about both java.io and java.nio to test your understanding of how to efficiently handle file operations in Java.

#19. What is the purpose of the volatile keyword in Java?

The volatile keyword in Java ensures that changes to a variable are visible to all threads immediately. It prevents threads from caching a variable’s value locally, ensuring that every thread reads the most recent value directly from main memory.

How volatile works

In multi-threaded environments, threads can maintain a local copy of variables for performance reasons. The volatile keyword forces threads to read the variable's value from main memory every time it is accessed, preventing stale or inconsistent data.

For example (without volatile):

class Counter {
	private boolean running = true;

	public void stop() {
    	running = false;
	}

	public void run() {
    	while (running) {
        	// Loop will not stop due to caching in some threads
    	}
	}
}

For example (with volatile):

class Counter {
	private volatile boolean running = true;

	public void stop() {
    	running = false; // Update immediately visible to all threads
	}

	public void run() {
    	while (running) {
        	// Loop will stop as soon as running is set to false
    	}
	}
}

Key points about volatile

  • volatile ensures visibility of changes to a variable across threads but does not guarantee atomicity
  • It is ideal for flags or status variables that multiple threads read and write but not for compound actions like incrementing a variable (use synchronized or AtomicInteger for atomicity)

For example (atomic operations):

import java.util.concurrent.atomic.AtomicInteger;

class Counter {
	private AtomicInteger count = new AtomicInteger(0);

	public void increment() {
    	count.incrementAndGet(); // Atomic operation
	}

	public int getCount() {
    	return count.get();
	}
}

Why it matters in practice

The volatile keyword is critical for ensuring visibility in simple scenarios where synchronization is not necessary. Interviewers may ask this to test your understanding of thread safety, visibility issues, and when volatile is sufficient compared to alternatives like synchronized or atomic classes.

#20. What is an inner class, and what are the types of inner classes in Java?

An inner class in Java is a class declared inside another class or interface. Inner classes are associated with their enclosing class and can access its private members directly. They are often used to logically group classes that work together or to provide more readable and maintainable code.

Nested Static Class

A nested static class is a static member of its enclosing class. It can be accessed without an instance of the enclosing class and cannot access the enclosing class's non-static members.

For example

class Outer {
	static class StaticNested {
    	void display() {
        	System.out.println("Inside static nested class");
    	}
	}
}

public class Main {
	public static void main(String[] args) {
    	Outer.StaticNested nested = new Outer.StaticNested();
    	nested.display();
	}
}

Non-Static Inner Class

A non-static inner class is tied to an instance of its enclosing class and can access its non-static members directly.

For example

class Outer {
	class Inner {
    	void display() {
        	System.out.println("Inside non-static inner class");
    	}
	}
}

public class Main {
	public static void main(String[] args) {
    	Outer outer = new Outer();
    	Outer.Inner inner = outer.new Inner();
    	inner.display();
	}
}

Local Inner Class

A local inner class is defined within a block, such as a method, and is only accessible within that block.

For example

class Outer {
	void method() {
    	class LocalInner {
        	void display() {
            	System.out.println("Inside local inner class");
        	}
    	}
    	LocalInner local = new LocalInner();
    	local.display();
	}
}

Anonymous Inner Class

An anonymous inner class is a subclass or implementation of an interface that is defined and instantiated in a single statement.

For example

interface Greeting {
	void sayHello();
}

public class Main {
	public static void main(String[] args) {
    	Greeting greeting = new Greeting() {
        	@Override
        	public void sayHello() {
            	System.out.println("Hello from anonymous inner class");
        	}
    	};
    	greeting.sayHello();
	}
}

Why it matters in practice

Inner classes are useful for organizing code and reducing clutter, especially when one class is closely associated with another. Interviewers often test this topic to evaluate your understanding of encapsulation and your ability to use inner classes effectively in real-world scenarios.

Advanced Java coding interview questions

These questions focus on performance optimization, JVM internals, garbage collection, and other high-level topics.

Mastering these concepts not only helps you excel in interviews but also equips you to handle complex challenges in real-world applications.

#21. What are the key differences between the JVM, JRE, and JDK?

Java Virtual Machine (JVM)

The JVM is the runtime engine that executes Java bytecode. It is platform-dependent and ensures Java's "Write Once, Run Anywhere" promise by abstracting system-specific details. The JVM handles tasks like memory management, garbage collection, and bytecode interpretation.

For example

When you run a Java program using java MyProgram, the JVM interprets or compiles the bytecode in MyProgram.class.

Java Runtime Environment (JRE)

The JRE provides the runtime environment for Java programs, including the JVM and essential libraries. It does not include development tools like the compiler. The JRE is ideal for running Java applications but not for building them.

For example

When installing Java to run applications, end-users typically install the JRE.

Java Development Kit (JDK)

The JDK is a complete toolkit for Java development, including the JRE, the Java compiler (javac), and other tools like the debugger (jdb) and archiver (jar). Developers use the JDK to write, compile, and debug Java applications.

For example

To compile a Java program, you would use the JDK command:

javac MyProgram.java

Key differences

  1. JVM: Executes bytecode; part of the JRE
  2. JRE: Provides the runtime environment; includes the JVM and core libraries
  3. JDK: Development toolkit; includes the JRE, compiler, and development tools

Why it matters in practice

Understanding these components is crucial for configuring Java environments and troubleshooting issues. Interviewers may ask this question to test your ability to differentiate these tools and explain their roles in Java development.

#22. How does garbage collection work in Java?

Garbage collection (GC) in Java is an automated process where the JVM reclaims memory by removing objects that are no longer in use. This approach reduces the risk of memory leaks and simplifies application development by managing memory allocation and deallocation behind the scenes.

How garbage collection works

Java’s memory is divided into key areas:

The Heap, which stores dynamically allocated objects, is further divided into:

  • Young Generation: This is where new objects are initially allocated. Objects that survive garbage collection in this area are moved to the Survivor Spaces and eventually to the Old Generation if they remain in use for an extended time
  • Old Generation: This area contains long-lived objects and is collected less frequently than the Young Generation
  • Metaspace: This holds metadata about classes and is separate from the heap

And the Stack, which stores method calls and local variables, is not managed by garbage collection.

Garbage collection works by identifying unreachable objects (objects no longer referenced by any thread) and reclaiming their memory. The process typically involves Minor GC for the Young Generation and Major GC or Full GC for the Old Generation.

For example

Consider an object created in the Eden Space:

  1. It is first allocated in the Young Generation (Eden Space)
  2. After surviving a Minor GC, it moves to the Survivor Space
  3. If it continues to be used, it is eventually promoted to the Old Generation

Types of garbage collectors

Java offers multiple garbage collectors tailored to different use cases:

  • Serial GC: A simple, single-threaded collector suitable for small applications
  • Parallel GC: A throughput-focused collector that uses multiple threads for garbage collection
  • G1 GC (Garbage-First): A balanced collector that minimizes pause times while maintaining high throughput, making it the default choice for most applications
  • ZGC (Z Garbage Collector): Designed for ultra-low latency and large heaps, introduced in Java 11
  • Shenandoah GC: Focused on reducing pause times, introduced in Java 12

Why it matters in practice

Understanding garbage collection helps developers write efficient, memory-optimized applications and troubleshoot issues like long GC pauses or OutOfMemoryError.

Interviewers often ask about GC mechanisms and collector types to evaluate your ability to manage memory effectively, especially in high-performance environments.

#23. What are the differences between the various garbage collectors in Java?

Java provides several garbage collectors, each optimized for different use cases and workloads. Understanding these differences is crucial for selecting the right collector based on application needs.

Serial Garbage Collector

The Serial GC is a single-threaded collector designed for simplicity and small applications. It performs garbage collection in a stop-the-world fashion, pausing all application threads during its operation.

Best for: Applications with low memory requirements and single-threaded workloads.

Parallel Garbage Collector

The Parallel GC, also known as the throughput collector, uses multiple threads for garbage collection. It prioritizes application throughput over low pause times by maximizing the time spent on application execution.

Best for: Applications with high throughput requirements and large data processing tasks.

G1 Garbage Collector (Garbage-First)

The G1 GC divides the heap into regions and prioritizes collecting regions with the most garbage. It balances throughput and low latency, making it the default collector for most applications starting from Java 9.

Best for: Applications requiring predictable pause times and balanced performance.

Z Garbage Collector (ZGC)

ZGC focuses on ultra-low latency, maintaining pause times of less than 10ms even for heaps up to terabytes in size. It achieves this by performing most of its work concurrently with the application threads.

Best for: Applications with large heaps and strict low-latency requirements.

Shenandoah Garbage Collector

Shenandoah GC minimizes pause times by performing concurrent compaction, reducing the time spent in stop-the-world events. It is similar to ZGC in its low-latency goals but designed for medium-sized heaps.

Best for: Applications requiring low-latency garbage collection but with more moderate heap sizes than ZGC.

Key differences between garbage collectors

Collector Multi-threaded Focus Best Use Case
Serial GC No Simplicity Small, single-threaded applications
Parallel GC Yes Throughput High-throughput applications
G1 GC Yes Balanced performance General-purpose, low-latency needs
ZGC Yes Ultra-low latency Large heaps, real-time systems
Shenandoah Yes Low latency Medium-sized heaps, low-pause needs

Why it matters in practice

Choosing the right garbage collector directly impacts application performance, especially for large-scale or real-time systems. Interviewers often test your understanding of these collectors to gauge your ability to tune JVM settings and optimize application behavior under varying workloads.

#24. What is the difference between parallelism and concurrency?

Parallelism and concurrency are often used interchangeably, but they represent different concepts in programming and system design. Understanding these differences is crucial when building efficient, scalable applications.

Concurrency

Concurrency refers to the ability of a system to handle multiple tasks at once by interleaving their execution. Tasks may not run simultaneously but are managed to give the appearance of simultaneous execution.

For example

In a single-core CPU, multiple threads may share the processor by rapidly switching between them (context switching).

Key points about concurrency:

  • Achieved through multitasking or multithreading
  • Focuses on managing multiple tasks efficiently
  • Common in I/O-bound tasks like reading files or handling network requests

Parallelism

Parallelism involves executing multiple tasks simultaneously, typically on multi-core processors. It requires hardware support to truly perform operations at the same time.

For example

In a multi-core CPU, two threads can run on separate cores simultaneously.

Key points about parallelism:

  • Involves simultaneous execution of tasks
  • Focuses on improving performance by dividing tasks
  • Common in compute-intensive tasks like matrix multiplication or image processing

Why it matters in practice

Understanding concurrency and parallelism helps developers choose the right approach for different scenarios. Concurrency is ideal for applications with high I/O demands, like web servers, while parallelism excels in CPU-intensive computations. Interviewers may ask this to evaluate your knowledge of system design and multi-threaded programming.

#25. How does Java’s memory model work?

The Java Memory Model (JMM) defines how threads interact with memory, ensuring consistent behavior across different platforms and processors. It specifies rules for reading and writing shared variables and synchronizing access to ensure thread safety.

Main memory and thread-local memory

Each thread has its own working memory (thread-local cache), where it stores copies of variables from main memory. Threads update main memory only when necessary, which can cause visibility issues.

For example

If one thread updates a variable in its working memory, another thread might not immediately see the updated value unless proper synchronization is used.

Happens-before relationship

The JMM defines a happens-before relationship, which specifies the order in which actions (like reads and writes) must appear to be executed across threads. If one action happens-before another, the first is visible and ordered before the second.

Synchronization and visibility guarantees

The JMM ensures visibility and ordering of variables using constructs like synchronized, volatile, and locks. These mechanisms prevent issues like race conditions and stale reads.

For example (synchronization):

class Counter {
	private int count = 0;

	public synchronized void increment() {
    	count++;
	}

	public synchronized int getCount() {
    	return count;
	}
}

Common issues resolved by the Java Memory Model

  1. Visibility: Ensures that changes made by one thread are visible to others, typically using volatile or synchronized blocks
  2. Atomicity: Guarantees that compound actions like increments are executed as a single, indivisible operation
  3. Ordering: Prevents reordering of instructions that could lead to inconsistent behavior across threads

Why it matters in practice

Understanding the Java Memory Model is essential for writing thread-safe code. Interviewers often ask about it to test your ability to manage concurrency effectively and avoid pitfalls like race conditions or visibility issues.

#26. What are Java’s design patterns, and when would you use them?

Design patterns provide standardized solutions to common software design problems. They help improve code organization, maintainability, and scalability. Java's object-oriented features make it ideal for implementing these patterns.

Creational patterns

These deal with object creation, ensuring flexibility and reuse.

Singleton

Ensures a class has only one instance and provides a global access point.

For example

Managing a single database connection pool.

class Singleton {
	private static Singleton instance;

	private Singleton() {}

	public static Singleton getInstance() {
    	if (instance == null) {
        	instance = new Singleton();
    	}
    	return instance;
	}
}
Factory Method

Defines an interface for creating objects but lets subclasses decide which object to create. For example, when generating different shapes in a graphics application.

Builder

Constructs complex objects step-by-step, allowing customization without creating subclasses. For example, when building an HTTP request object with headers, parameters, and body.

Structural patterns

Structural patterns focus on composing classes and objects into larger structures.

Adapter

Bridges incompatible interfaces to enable them to work together. For example, when adapting a legacy payment gateway to work with a new e-commerce platform.

Decorator

Dynamically adds responsibilities to objects without modifying their structure. For example, when adding encryption and compression to a data stream.

interface DataSource {
	void writeData(String data);
	String readData();
}

class FileDataSource implements DataSource {
	// Base implementation for reading/writing data
}

class EncryptionDecorator implements DataSource {
	private DataSource wrapped;

	public EncryptionDecorator(DataSource source) {
    	this.wrapped = source;
	}

	public void writeData(String data) {
    	wrapped.writeData(encrypt(data));
	}

	public String readData() {
    	return decrypt(wrapped.readData());
	}

	private String encrypt(String data) {
    	return "encrypted_" + data;
	}

	private String decrypt(String data) {
    	return data.replace("encrypted_", "");
	}
}
Proxy

Provides a placeholder or surrogate to control access to another object. For example, when managing access to a remote service.

Behavioral patterns

Behavioral patterns focus on communication between objects and managing object responsibilities.

Observer

Defines a one-to-many dependency, notifying all dependents when an object changes state. For example, when using eEvent listeners in GUI applications.

import java.util.ArrayList;
import java.util.List;

class Subject {
	private List<Observer> observers = new ArrayList<>();

	public void addObserver(Observer observer) {
    	observers.add(observer);
	}

	public void notifyObservers() {
    	for (Observer observer : observers) {
        	observer.update();
    	}
	}
}

interface Observer {
	void update();
}

class ConcreteObserver implements Observer {
	public void update() {
    	System.out.println("State updated!");
	}
}

Strategy

Encapsulates algorithms and allows them to be swapped dynamically. For example, when switching between different sorting algorithms.

Command

Encapsulates a request as an object, allowing parameterization and queuing of requests. For example, when implementing undo functionality in a text editor.

Why it matters in practice

Design patterns provide reusable solutions for common challenges in software design. They simplify communication among developers and ensure best practices. Interviewers may test your understanding of these patterns to assess your ability to write clean, scalable, and maintainable code.

27. How does Java handle multithreading using the ExecutorService?

Java’s ExecutorService, part of the java.util.concurrent package, simplifies thread management by abstracting the complexities of thread creation, scheduling, and lifecycle management.

It enables developers to execute tasks asynchronously using a pool of reusable threads, making applications more scalable and efficient.

How ExecutorService works

The ExecutorService framework manages a pool of threads to execute submitted tasks. Developers can define tasks using Runnable or Callable interfaces, which the executor runs concurrently. When a task is submitted, the executor assigns it to an available thread from the pool. If all threads are busy, the task waits in a queue until a thread becomes available.

The framework also supports shutting down the thread pool gracefully. By calling the shutdown() method, the executor completes all ongoing tasks before terminating, ensuring clean resource management.

Benefits of using ExecutorService

The key advantage of ExecutorService is its efficiency. Instead of creating new threads for each task, the framework reuses threads from the pool, reducing the overhead of thread creation and destruction. This makes it particularly effective for applications handling numerous short-lived tasks, such as server request processing.

ExecutorService also provides greater control over task execution. It allows you to monitor task status, retrieve results using Future, and handle exceptions robustly. This makes it easier to write maintainable and error-tolerant concurrent applications.

For example

Here’s an example of using a fixed thread pool to execute multiple tasks concurrently:

ExecutorService executor = Executors.newFixedThreadPool(2);

executor.execute(() -> System.out.println("Task 1 executed by: " + Thread.currentThread().getName()));
executor.execute(() -> System.out.println("Task 2 executed by: " + Thread.currentThread().getName()));

executor.shutdown();

In this example, two tasks are submitted to the executor, which assigns them to threads in the pool for execution.

Why it matters in practice

The ExecutorService framework is essential for building scalable and maintainable multi-threaded applications in Java. By reducing manual thread management, it simplifies concurrency and improves application performance. Interviewers often test your knowledge of ExecutorService to assess your ability to design efficient, thread-safe systems.

#28. What is the purpose of the CompletableFuture class?

The CompletableFuture class, introduced in Java 8, is part of the java.util.concurrent package and enables developers to handle asynchronous programming more efficiently.

Unlike the older Future interface, CompletableFuture allows you to write non-blocking, event-driven code with clearer workflows and better error handling.

How CompletableFuture works

At its core, CompletableFuture represents a future result of an asynchronous computation. It lets developers run tasks on separate threads while continuing other operations in the main thread. For example, you can perform computationally heavy tasks like fetching data from an external API or processing large datasets without blocking the main program.

To execute tasks asynchronously, methods like supplyAsync or runAsync are used. These allow tasks to be submitted to an executor service and run in the background. Once a task completes, additional actions can be chained using methods like thenApply (for transforming results) or thenCompose (for creating dependent tasks). This chaining makes workflows more expressive and easier to maintain.

Combining tasks and handling errors

Another strength of CompletableFuture is its ability to combine multiple tasks. Methods like thenCombine allow you to process the results of two or more computations together, while allOf lets you wait for all tasks in a group to complete. This is particularly useful when coordinating operations like aggregating responses from different services.

Error handling is seamlessly integrated into CompletableFuture via methods like exceptionally and handle. These provide recovery mechanisms when tasks fail, ensuring your application remains resilient.

For example

Here’s how a CompletableFuture can compute a result, transform it, and handle any potential errors:

CompletableFuture.supplyAsync(() -> "Hello")
             	.thenApply(greeting -> greeting + ", World!")
             	.thenAccept(System.out::println)
             	.exceptionally(ex -> {
                 	System.out.println("Error: " + ex.getMessage());
                 	return null;
             	});

This approach avoids blocking the main thread and demonstrates how CompletableFuture simplifies error handling and chaining tasks.

Why it matters in practice

CompletableFuture is essential for modern Java applications that rely on asynchronous workflows or event-driven programming. By reducing the need for manual thread management, it improves readability, maintainability, and efficiency.

Interviewers may ask about it to evaluate your understanding of concurrency and your ability to design scalable, non-blocking systems.

#29. What are the key improvements introduced in Java 17 and beyond?

Java 17, as a long-term support (LTS) release, introduced several impactful updates that modernize the language and improve performance. These enhancements, along with updates in earlier releases like Java 15 and 16, reflect Java’s ongoing evolution to meet contemporary development needs.

Language features

One of the standout additions in Java 17 is sealed classes, which allow developers to control inheritance hierarchies explicitly. By specifying which classes can extend a sealed class, you can enforce stricter design constraints. For instance, a Shape class can be sealed to allow only Circle and Square as its subclasses, ensuring clearer domain models and preventing misuse.

Java 17 also introduced pattern matching for switch (in preview). This feature enhances the traditional switch statement by enabling type checking and extraction directly within cases. Instead of multiple if-else blocks or type casts, developers can now simplify workflows with concise and readable code.

Text blocks, standardized in Java 15, have become a staple for handling multi-line strings. They eliminate the need for concatenation or escape characters, making it easier to write and read structured content such as HTML or JSON directly in Java code.

Performance and runtime improvements

Java 17 included updates to garbage collectors like ZGC and Shenandoah, both of which are designed for ultra-low latency and efficient memory management. These enhancements reduce pause times significantly, making Java a better fit for applications requiring real-time or near-real-time performance.

Another key addition is the Foreign Function & Memory API (preview), which simplifies interoperability between Java and native code. This API allows developers to call native libraries directly and manage memory outside the JVM, providing more flexibility and performance for systems-level programming.

Developer tooling

The release of the jpackage tool in Java 16 was a game-changer for packaging Java applications. With jpackage, developers can easily create native installers for platforms like Windows, macOS, and Linux, streamlining application deployment.

In Java 17, the removal of legacy features like the RMI Activation System reflects the platform’s effort to modernize and reduce technical debt. By focusing on active, widely-used features, Java continues to align with best practices and contemporary development standards.

Why it matters in practice

Java 17’s improvements enhance both developer productivity and application performance. From language updates like sealed classes and pattern matching to runtime optimizations and modern tooling, these changes make Java more versatile and competitive. Interviewers may ask about these features to assess whether you can leverage modern Java effectively in real-world scenarios.

#30. How does Java support modular programming?

Java introduced modular programming with the release of Java 9, providing developers with a structured way to manage codebases and dependencies. The module system, also known as Project Jigsaw, helps improve application performance, maintainability, and security by allowing better control over code visibility and packaging.

What is the Java module system?

The module system in Java organizes code into modules, which are self-contained units that group related packages and resources. Each module declares its dependencies and explicitly states which parts of its code can be accessed by other modules.

A module is defined using a module-info.java file, located at the root of the module directory. This file specifies the module’s name, dependencies, and the packages it exports.

For example

module com.example.myapp {
	requires java.sql;
	exports com.example.myapp.services;
}

In this example, the com.example.myapp module:

  • Declares a dependency on the java.sql module
  • Exports the com.example.myapp.services package, making it accessible to other modules

Key features of the module system

  • Encapsulation: Modules can restrict access to their internal packages, ensuring that only explicitly exported packages are accessible. This improves code security and reduces the risk of unintentional usage of internal APIs
  • Dependency management: Modules explicitly declare their dependencies, which the JVM verifies at runtime. This eliminates classpath issues like missing or conflicting libraries
  • Smaller runtime footprint: The module system allows you to build custom runtime images with only the modules your application needs. This is done using the jlink tool, which creates a lightweight, optimized Java runtime.

Advantages of modular programming

Modular programming improves application maintainability by organizing code into logical units, making it easier to navigate and understand. It also enhances performance by allowing you to ship minimal runtime images tailored to your application’s requirements. For large projects with multiple dependencies, the module system reduces errors by enforcing clear boundaries between modules.

Why it matters in practice

The Java module system addresses long-standing issues with the classpath and dependency management. By enabling developers to write modular, secure, and scalable applications, it is a key feature in modern Java development.

Interviewers often ask about it to assess your understanding of Java’s ecosystem and your ability to design maintainable, modular software.

#31. What is the Optional class in Java, and how is it used?

The Optional class, introduced in Java 8, is a container object used to represent the presence or absence of a value. It helps avoid NullPointerException (NPE) by providing a structured approach to handle potentially null values. Instead of checking for null explicitly, Optional enables more readable and functional-style programming.

How Optional works

An Optional object can either:

  • Contain a non-null value
  • Be empty, indicating the absence of a value

You can create an Optional using the static methods Optional.of(), Optional.ofNullable(), or Optional.empty().

For example

Optional<String> nonEmpty = Optional.of("Hello");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(null);

Key methods of the Optional class

The Optional class provides several methods to work with values effectively:

  • isPresent() and ifPresent(): Check whether a value is present and perform an action if it is
  • orElse() and orElseGet(): Return a default value if the Optional is empty
  • map() and flatMap(): Transform the value if it is present

For example

Optional<String> optional = Optional.ofNullable("Hello, World!");
optional.ifPresent(value -> System.out.println(value)); // Prints "Hello, World!"
String result = optional.orElse("Default Value");
System.out.println(result); // Prints "Hello, World!"

Advantages of using Optional

  • Eliminates null checks: Using Optional reduces boilerplate code and ensures that null handling is explicit and intentional
  • Improves readability: By chaining methods like map() and flatMap(), you can process values in a functional and expressive style
  • Encourages defensive programming: Explicitly handling absence of values helps avoid runtime exceptions and makes code more robust

Why it matters in practice

The Optional class is widely used in modern Java applications to handle nullable values in a structured and safe manner. Interviewers often ask about it to evaluate your understanding of functional programming concepts in Java and your ability to write error-resistant code.

#32. How does the Stream API work in Java, and why is it useful?

The Stream API, introduced in Java 8, provides a functional and declarative way to process collections of data.

Unlike traditional iteration techniques, it enables developers to write cleaner, more expressive code by focusing on the desired outcome rather than the step-by-step implementation. With features like lazy evaluation and parallel processing, the Stream API also helps optimize performance.

How Stream works

A stream represents a sequence of elements from a data source, such as a collection, array, or file. It operates in a pipeline structure, which consists of three key stages.

  • First, a source initializes the stream with data
  • Then, intermediate operations, such as filter() and map(), transform or filter elements in a lazy manner, meaning they execute only when a terminal operation is invoked
  • Finally, terminal operations, such as collect() or forEach(), consume the stream and produce a result or side effect.

For example

Consider a list of names that you want to filter and transform. Using the Stream API, this process becomes concise and readable:

List<String> names = List.of("Alice", "Bob", "Charlie", "David");
List<String> filteredNames = names.stream()
                               	.filter(name -> name.startsWith("A"))
                               	.map(String::toUpperCase)
                               	.toList();
System.out.println(filteredNames); // Output: [ALICE]

Here, the stream filters names starting with "A" and converts them to uppercase, all in a single pipeline.

Key features and use cases

The Stream API excels at simplifying complex operations on collections. One of its most important features is lazy evaluation, where intermediate operations are not executed until a terminal operation is called. This improves performance by processing data only as needed.

Additionally, streams enable parallel processing through parallelStream(), which distributes tasks across multiple CPU cores with minimal effort.

While in practical use, streams are invaluable for tasks like filtering and transforming data, aggregating results with methods like reduce(), and grouping or partitioning data with collectors.

For example

You can group names based on their length using a collector:

Map<Boolean, List<String>> grouped = names.stream()
                                      	.collect(Collectors.partitioningBy(name -> name.length() > 3));
System.out.println(grouped);
// Output: {false=[Bob], true=[Alice, Charlie, David]}

Why it matters in practice

The Stream API modernized data processing in Java by introducing a functional programming style. Its ability to simplify workflows and handle data efficiently makes it an essential tool for writing robust, maintainable applications.

Interviewers often test familiarity with streams to assess a candidate's understanding of Java 8 features and their ability to write concise, optimized code.

#33. What is the difference between Comparable and Comparator in Java?

Comparable and Comparator are two interfaces in Java used to define the sorting logic for objects. While they serve a similar purpose, they differ in how and where the sorting logic is defined, offering flexibility for different use cases.

Comparable

The Comparable interface is used when an object has a natural ordering. A class implementing this interface must override the compareTo() method, which defines the default sorting logic. The natural order is typically used when sorting collections like TreeSet or arrays with Arrays.sort().

For example

Consider a Student class where students are sorted by their ID:

class Student implements Comparable<Student> {
	private int id;
	private String name;

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

	public int compareTo(Student other) {
    	return Integer.compare(this.id, other.id);
	}

	// Getters and toString()
}

In this case, the compareTo() method defines a natural order based on student IDs. Using Collections.sort() on a list of students will automatically sort them by ID.

Comparator

The Comparator interface, on the other hand, is used when you want to define multiple or custom sorting orders. Instead of embedding the sorting logic within the object itself, the Comparator interface separates it, making the code more flexible and reusable.

For example, you could sort the Student class by name instead of ID using a custom comparator:

Comparator&lt;Student> nameComparator = (s1, s2) -> s1.getName().compareTo(s2.getName());
Collections.sort(studentList, nameComparator);

This approach allows you to define multiple comparators for different sorting criteria, such as by name, age, or GPA, without altering the Student class.

Key differences

Location of sorting logic:

  • Comparable embeds the sorting logic in the class itself, defining the natural order
  • Comparator separates the sorting logic, allowing flexibility and multiple sorting options

Use case:

  • Use Comparable when there is a single, natural order for the object
  • Use Comparator when you need multiple sorting strategies or want to keep sorting logic external

Implementation:

  • Comparable requires the compareTo() method
  • Comparator uses the compare() method or can be implemented using lambda expressions

Why it matters in practice

Understanding Comparable and Comparator is essential for handling custom sorting in Java. These interfaces are frequently used in applications where objects need to be ordered, such as sorting data in collections.

Interviewers may ask about them to assess your ability to write efficient, reusable code and apply the right sorting approach for different scenarios.

How did you do?

So there you have it - 33 of the most common Java coding interview questions and answers that you might encounter.

What did you score? Did you nail all 33 questions? If so, it might be time to move from studying to actively interviewing!

Didn't get them all? Got tripped up on a few? Don't worry; I'm here to help.

If you want to fast-track your Java knowledge and interview prep, and get as much hands-on practice as possible, then check out my Java programming bootcamp:

Learn Java coding

Updated for 2025, you'll learn Java programming fundamentals all the way from complete beginner to advanced skills.

Better still, you’ll be able to reinforce your learning with over 80 exercises and 18 quizzes. This is the only course you need to go from complete programming beginner to being able to get hired as a Backend Developer!

Plus, once you join, you'll have the opportunity to ask questions in our private Discord community from me, other students and working backend developers.


If you join or not, I just want to wish you the best of luck with your interview!

More from Zero To Mastery

How To Ace The Coding Interview preview
How To Ace The Coding Interview

Are you ready to apply for & land a coding job but not sure how? This coding interview guide will show you how it works, how to study, what to learn & more!

How To Get A Job In Tech & Succeed When You’re There! preview
Popular
How To Get A Job In Tech & Succeed When You’re There!

Are you looking to get a job in tech? These are the steps (+ tips, tricks, and resources) from a Senior Developer to get hired in tech with zero experience!

How to Get More Interviews, More Job Offers, and Even a Raise preview
How to Get More Interviews, More Job Offers, and Even a Raise

This is Part 3 of a 3 part series on how I taught myself programming, and ended up with multiple job offers in less than 6 months!