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:
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!
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.
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:
.java
files) is compiled into platform-independent bytecode (.class
files) by the Java Compiler (javac
)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.
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:
==
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.
Access modifiers define the scope and accessibility of classes, methods, and properties. Java provides four main access modifiers:
public
: Accessible from any classprotected
: Accessible within the same package and by subclasses outside the packagedefault
(package-private): Accessible only within the same package (no keyword needed)private
: Accessible only within the same classFor 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.
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.
For example
final int maxValue = 100;
maxValue = 200; // Error: cannot assign a value to final variable
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.
Both abstract classes and interfaces are used to achieve abstraction in Java, but they differ in design and use cases.
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");
}
}
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 modularitypublic static final
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.
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
. Constructor
s 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
, method
s must have a return type or void
if no value is returned. Method
s can also be static, abstract, or final, while constructor
s 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:
new ClassName()
syntax when creating an object, while methods must be explicitly called on an object or class.static
, abstract
, or final
, whereas methods can use these modifiersWhy 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.
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:
String
for immutable text or when thread safety is required by defaultStringBuilder
for efficient, single-threaded text manipulationsStringBuffer
for thread-safe text manipulations in multi-threaded environmentsWhy 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.
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);
this
or super
keywords because they are not tied to an instanceMath.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.
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:
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.
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.
Arrays and ArrayLists are both used to store collections of data in Java, but they differ in their structure and functionality.
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
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:
add()
, remove()
, and contains()
, making them easier to work withWhy 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.
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.
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)
HashMap
: No order guaranteedLinkedHashMap
: Maintains insertion orderTreeMap
: Maintains sorted orderHashMap
: O(1) for most operationsLinkedHashMap
: Slightly slower than HashMap
TreeMap
: O(log n) due to the underlying Red-Black TreeHashMap
and LinkedHashMap
: Allow one null
keyTreeMap
: Does not allow null
keysWhy 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.
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.
IOException
, SQLException
), requiring you to handle or declare them using the throws
keywordNullPointerException
, 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
blockA 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
blockThe 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 exceptionthrows
Declares exceptions that a method may throwFor 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.
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.
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);
}
}
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);
}
}
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.
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.
Threads can be created in two primary ways:
Thread
classRunnable
interfaceFor 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
}
}
The java.util.concurrent
package provides high-level abstractions like:
ExecutorService
)ReentrantLock
)ConcurrentHashMap
)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.
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.
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;
}
}
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;
}
}
this
, a custom lock object, etc.)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.
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.
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();
}
}
}
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.
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.
volatile
worksIn 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
}
}
}
volatile
volatile
ensures visibility of changes to a variable across threads but does not guarantee atomicitysynchronized
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.
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.
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();
}
}
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();
}
}
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();
}
}
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.
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.
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
.
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.
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
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.
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.
Java’s memory is divided into key areas:
The Heap, which stores dynamically allocated objects, is further divided into:
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:
Java offers multiple garbage collectors tailored to different use cases:
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.
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.
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.
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.
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.
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 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.
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.
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 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:
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:
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.
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.
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.
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.
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;
}
}
volatile
or synchronized blocksWhy 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.
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.
These deal with object creation, ensuring flexibility and reuse.
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;
}
}
Defines an interface for creating objects but lets subclasses decide which object to create. For example, when generating different shapes in a graphics application.
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 focus on composing classes and objects into larger structures.
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.
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_", "");
}
}
Provides a placeholder or surrogate to control access to another object. For example, when managing access to a remote service.
Behavioral patterns focus on communication between objects and managing object responsibilities.
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!");
}
}
Encapsulates algorithms and allows them to be swapped dynamically. For example, when switching between different sorting algorithms.
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.
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.
ExecutorService
worksThe 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.
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.
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.
CompletableFuture
worksAt 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.
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.
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.
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.
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.
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.
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.
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:
java.sql
modulecom.example.myapp.services
package, making it accessible to other modulesjlink
tool, which creates a lightweight, optimized Java runtime.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.
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.
Optional
worksAn Optional
object can either:
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);
Optional
classThe 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 isorElse()
and orElseGet()
: Return a default value if the Optional
is emptymap()
and flatMap()
: Transform the value if it is presentFor 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!"
Optional
Optional
reduces boilerplate code and ensures that null handling is explicit and intentionalmap()
and flatMap()
, you can process values in a functional and expressive styleWhy 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.
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.
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.
filter()
and map()
, transform or filter elements in a lazy manner, meaning they execute only when a terminal operation is invokedcollect()
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.
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.
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<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.
Location of sorting logic:
Comparable
embeds the sorting logic in the class itself, defining the natural orderComparator
separates the sorting logic, allowing flexibility and multiple sorting optionsUse case:
Comparable
when there is a single, natural order for the objectComparator
when you need multiple sorting strategies or want to keep sorting logic externalImplementation:
Comparable
requires the compareTo()
methodComparator
uses the compare()
method or can be implemented using lambda expressionsWhy 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.
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:
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!