Last chance! 50% off DataCamp Premium
Sale ends in
According to TIOBE index, which has been ranking programming languages for over two decades, Java has always been one of the four most popular languages in the world. Many languages have been pitched as Java replacements over the years, but Java is still going strong in 2026. If you search for jobs with the “Java” keyword in the title, you are going to get thousands of hits in the US alone. Before you apply to any of them, you need to practice for interview questions on Java.
The questions and answers listed in this article will help developers of all levels. Even seniors may benefit from reading answers to beginner-level questions because the ability to write stellar software is completely different from the ability to explain it to others.
The questions are grouped by experience level and can apply to developers in the following roles:
So, let’s get started without further ado!
Note: If you are just starting out on your Java journey, it is worth solidifying your skills with our Introduction to Java course before tackling interview questions.
Java remains one of the world's top 4 programming languages and is in high demand for backend, full-stack, and enterprise roles in 2026
Interview questions are grouped by level: beginner (Q1–Q22, 0–3 years), intermediate (Q23–Q44, ~5 years), and senior (Q45–Q56, 10+ years)
Core beginner topics: OOP pillars (inheritance, encapsulation, polymorphism, abstraction), primitives vs. objects, exception handling, collections
Core intermediate topics: concurrency (synchronized, volatile, ExecutorService), Java 8 features (streams, lambdas, functional interfaces), design patterns
Core senior topics: JVM internals, garbage collection tuning, lock-free programming, reactive programming, distributed systems design
Modern Java (17–21) topics increasingly appear in 2026 interviews: records, sealed classes, pattern matching, and virtual threads
These are some of the interview questions you might face if you’re relatively new to working with Java, suitable for those with 0-3 years of experience.
This is one of the most commonly asked Java interview questions at all levels. Understanding these three components is fundamental to understanding how Java programs run:
JVM (Java Virtual Machine): The abstract machine that executes Java bytecode. It is platform-specific (Windows, Linux, macOS each have their own JVM implementation) but executes the same bytecode, giving Java its platform independence.
JRE (Java Runtime Environment): The runtime environment that includes the JVM plus the core Java class libraries needed to run Java programs. You need the JRE to run Java applications, but not to develop them.
JDK (Java Development Kit): The full development kit that includes the JRE plus development tools like the compiler (javac), debugger, and documentation generator (javadoc). You need the JDK to write and compile Java programs.
A useful memory aid: JDK ⊃ JRE ⊃ JVM. Every JDK contains a JRE, and every JRE contains a JVM.
// JDK: compiles .java files to .class files
javac HelloWorld.java
// JRE/JVM: executes the .class bytecode
java HelloWorld
Java is a platform-independent (write once, run anywhere) object-oriented language with automatic memory management (garbage collection), strong typing, and a rich standard library.
Primitives and objects represent two fundamentally different ways Java handles data.
In terms of storage, primitives store actual values while objects store references. Primitives take up less memory, objects more. Primitives have limited built-in operations while you can implement as many methods as you want for objects.
Also, primitives can’t be null, limiting their flexibility (depends on the context) while objects can. For their simplicity, primitives are generally faster to access and manipulate.
In Java, there are 8 primitive types (int, boolean, etc.) while you can create unlimited object types.
If your string is not going to change, use a String class as a String object is immutable. If your string should be modified and will be accessed only by a single thread, StringBuilder is good enough. In other scenarios (string can be changed, using multiple threads), use StringBuffer because it is synchronized and thread-safe.
// String (immutable)
String s = "Hello";
s += " World"; // Creates a new String object
// StringBuilder (mutable, not thread-safe)
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // Modifies the same object
// StringBuffer (mutable, thread-safe)
StringBuffer sbf = new StringBuffer("Hello");
sbf.append(" World"); // Thread-safe modification
Exceptions in Java can be gracefully handled using try-catch blocks. In the try block, we write the code that might throw an exception, and the catch block specifies what the code must do if the exception occurs.
A finally block can be used for cleanup operations if try-catch blocks deal with external resources like file managers, database connections, etc.
Here is a code example demonstrating exception handling in Java:
import java.io.FileReader;
import java.io.IOException;
public class ExceptionHandlingExample {
public static void main(String[] args) {
FileReader reader = null;
try {
reader = new FileReader("nonexistent.txt");
// Code that might throw an exception
int character = reader.read();
System.out.println((char) character);
} catch (IOException e) {
// Handling the specific exception
System.out.println("An error occurred while reading the file: " + e.getMessage());
} finally {
// Cleanup code that always executes
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("Error closing file: " + e.getMessage());
}
}
}
}
}
The static keyword in Java is used to declare members (variables, methods, nested classes) that belong to the class itself rather than instances of the class. Declaring static members allows sharing data across all instances of a class, creating utility methods that don't require object instantiation or defining constraints.
For example, in the following BankAccount class, totalAccounts and INTEREST_RATE are static, so they are shared across all instances of the class.
public class BankAccount {
private String accountHolder;
private double balance;
private static int totalAccounts = 0;
private static final double INTEREST_RATE = 0.05;
// The rest of the code here ...
}
Inheritance is one of the core pillars of object-oriented programming in Java. It allows one class to inherit properties and methods from another class, promoting code reuse and establishing a parent-child relationship between classes.
For example, Car class may inherit from a general Vehicle class. When doing so, Car can behave just like Vehicle in terms of attributes and methods like:
Vehicle class has members like year and make while Car has an additional member transmission.
Vehicle class has a move method while Car overrides move with additional behavior suited to cars.
== compares object references (memory addresses), while .equals() compares the content of strings. For string comparison, always use .equals().
Here’s an example to illustrate the difference:
String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");
System.out.println(str1 == str2); // true (same object reference)
System.out.println(str1 == str3); // false (different object references)
System.out.println(str1.equals(str2)); // true (same content)
System.out.println(str1.equals(str3)); // true (same content)
Arrays are critical objects that store multiple values of the same type in Java. They are created using square brackets and can be initialized in several ways. Here are a couple of common patterns:
int[] numbers = new int[5]; // Creates an array of 5 integers
int[] numbers = {1, 2, 3, 4, 5}; // Creates and initializes an array
If we compare Java arrays to Python lists, there are many differences. Here are a couple:
Constructors are special and very important methods used to initialize instances of a class. They have the same name as the class and are called when a new object is created using the new keyword.
break and continue are important loop flow control keywords in Java. break statement is used to stop the entire loop immediately and ignore the rest of the loop. It is useful to terminate the loop early based on a condition.
For example, I use break for debugging purposes like running only a single iteration of a long loop.
continue is used to skip the rest of the current iteration and immediately jumps to the next iteration. It is useful when looping over sequences and want to skip certain elements based on a condition.
Here is a code example:
for (int i = 0; i < 5; i++) {
if (i == 2) {
continue; // Skip iteration when i is 2
}
if (i == 4) {
break; // Exit loop when i is 4
}
System.out.println(i);
}
Method overloading is a powerful technique that allows a class to have multiple methods with the same name but different parameters. This lets objects of the class handle closely related tasks with different input types.
Here’s a super short example demonstrating method overloading:
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
In this example, the Calculator class has two add methods: One that takes two integers and returns an integer sum, and another that takes two doubles and returns a double sum. The method name is the same, but the parameters differ, allowing the appropriate method to be called based on the argument types.
To accept user input from the user, one can use the Scanner class:
Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine();
Here’s what each line does:
Scanner scanner = new Scanner(System.in); This line creates a new Scanner object that reads input from the console (System.in).
String input = scanner.nextLine(); This line reads a full line of text entered by the user and stores it in the input variable.
The Scanner class is used to parse primitive types and strings from various input sources. In this case, it’s reading from the standard input (keyboard).
The nextLine() method reads the entire line of text, including spaces, and returns it as a String.
One limitation of Java’s built-in array objects is that their size can’t be changed after initialization. ArrayList solves this problem and offers more methods for manipulation. However, this comes at the cost of not being able to store primitives directly. ArrayList only stores objects.
A collection in Java is an object that groups multiple elements into a single unit and part of the Java Collections Framework. It is often used to store, retrieve, manipulate and communicate aggregate data.
To iterate through a collection, you can use a for-each loop, iterator, or a traditional for loop. The code example below shows the use of a for-each and a regular for loop:
// Example using for-each loop:
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange");
for (String fruit : fruits) {
System.out.println(fruit);
}
// Example using regular for loop:
for (int i = 0; i < fruits.size(); i++) {
System.out.println(fruits.get(i));
}
The final keyword in Java is a modifier that can be applied to variables, methods and classes. When used with a variable, the keyword makes it immutable or in other words, constant. For example, PI is declared as a final variable in the class below:
public class CircleCalculator {
private final double PI = 3.14159;
public double calculateArea(double radius) {
return PI * radius * radius;
}
}
If any code attempts to modify PI, it will result in a compile-time error.
Java controls the visibility of class variables and methods using different levels of access modifiers — public, private, and protected. Here are their differences:
public: Accessible from any other class.
private: Only accessible within the same class.
protected: Accessible within the same package and by subclasses.
Default (no modifier): Accessible within the same package only.
Access modifiers are key in implementing encapsulation, one of the fundamental principles of object-oriented programming. With them, developers improve security and reduce the complexity of the codebase.
I’d like to think of this as a kind of placeholder for a future instance of my class. It is a requirement to use if you want to differentiate between instance variables and parameters with the same name.
The Integer class exposes a parseInt() method that converts strings with numeric value into integers:
String str = "123";
int num = Integer.parseInt(str);
There are similar methods for doubles and floats in Java. Most often, you use one of these parsing methods after accepting a user input, which is always received as a string.
&& is the logical AND operator with short-circuit evaluation. & is the bitwise AND operator, which also works as a logical AND but evaluates both sides always.
Consider this code example:
int a = 5;
int b = 10;
boolean result;
result = (a > 10) && (b++ > 5);
System.out.println("a = " + a + ", b = " + b + ", result = " + result);
result = (a > 10) & (b++ > 5);
System.out.println("a = " + a + ", b = " + b + ", result = " + result);
Output:
a = 5, b = 10, result = false
a = 5, b = 11, result = false
In the first case (&&), b isn't incremented because the left side is false, so evaluation short-circuits. In the second case (&), b is incremented despite the left side being false, as both sides are always evaluated.
An enum is a special type used to define collections of constants. Example:
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
Day today = Day.MONDAY;
An abstract class can have both abstract and concrete methods, while an interface in Java (before Java 8) could only contain abstract methods. Starting from Java 8, interfaces can also have default and static methods.
Use cases:
Autoboxing is the automatic conversion of primitive types to their corresponding wrapper classes (e.g., converting int to Integer). Unboxing is the reverse process. For example:
int primitive = 5;
Integer wrapped = primitive; // Autoboxing
int unboxed = wrapped; // Unboxing
This feature allows for integration between primitives and objects.
New to Java? Solidify your foundations with the Java Fundamentals skill track on DataCamp, which covers variables, data types, OOP, and data structures — all concepts tested in junior-level interviews.
These questions are suitable for those with around 5 years of experience in using Java and are more of an intermediate-level of Java practitioner.
Both are used for defining tasks that can be executed by threads. Runnable doesn't return a result, cannot throw checked exceptions, and thus, is simpler to use for basic tasks. Callable, on the other hand, can return a result and can throw checked exceptions. Callable is typically used with ExecutorService for asynchronous task execution.
Think of Runnable as a worker who silently does the job but doesn't report back with any information other than success or failure. Callable is a more professional worker who not only performs the task but puts together a detailed report on task results with any or all problems encountered during task execution.
The choice of ArrayList and LinkedList comes down to the tradeoff between array access and array modification. Since ArrayList uses a dynamic array internally, it provides fast random access but slower insertions/deletions. LinkedList, on the other hand, uses a doubly-linked list which makes insertions/deletions faster at the cost of slower random access.
In Java, Singleton pattern is often used to ensure a class has only one instance. This can be useful when managing global state or resources and in scenarios involving centralized control, like logging or database connections.
Here is a thread-safe implementation of the Singleton pattern:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Fail-fast iterators throw ConcurrentModificationException if the collection is modified while iterating. Examples include iterators of ArrayList and HashMap. Fail-safe iterators don't throw exceptions if the collection is modified; they work on a clone of the collection. Examples include iterators of ConcurrentHashMap and CopyOnWriteArrayList.
We use fail-fast and fail-safe iterators for different purposes in concurrent Java programming:
Fail-fast iterators:
Fail-safe iterators:
The Java Memory Model (JMM) sets the rules for how Java programs interact with computer memory, especially in multi-threaded environments.
The first rule is ensuring changes to shared data are seen by all threads. Another rule guarantees certain operations are indivisible (atomicity). Also, the model defines the sequence of memory operations (order).
With these rules in place, you can expect safe data sharing between threads and proper synchronization.
In Java 8 and later, abstract classes are used as partially implemented classes that cannot be instantiated. Rather, they are used to define a common base for related classes in a hierarchy.
Interfaces, on the other hand, are contracts that specify a set of abstract methods that can be applied to non-related classes to give them the same abilities.
For example, use an abstract class Animal to define common properties and behaviors for different types of animals. Other classes like Bear or Wolf can extend from it.
Use an interface Swimmable to define the ability to swim, which can be implemented by both related (e.g., different species of fish) and unrelated (e.g., duck and human) classes.
When you call .equals() on objects, Java compares their hash codes to perform the check. If you override this method in your classes, you should also override hashCode() method as equal objects must have equal hash codes. Breaking this contract can cause issues with hash-based collections.
In several scenarios like stream operations, functional interface implementations, or event handling, you want to pass behavior (typically in the form of a function) as an argument. This is called method referencing and they can be more readable than certain lambda expressions. They come in four types:
Here is an example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Static method reference
names.forEach(System.out::println);
// Instance method reference
names.sort(String::compareToIgnoreCase);
// Constructor reference
Supplier<List<String>> listSupplier = ArrayList::new;
The volatile keyword in Java ensures that changes to a variable are always immediately visible to other threads. It establishes a happens-before relationship, meaning that any write to a volatile variable is guaranteed to be visible to any subsequent read of the same variable by any thread.
Here’s a concrete example:
public class FlagExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true;
}
public void checkFlag() {
while (!flag) {
// Wait for flag to be set
}
System.out.println("Flag was set!");
}
}
So, without volatile, the checkFlag method might never see the update made by setFlag due to CPU caching or compiler optimizations, potentially resulting in an infinite loop. But with volatile, Java guarantees that:
The write to flag in setFlag is immediately visible to all threads.
The read of flag in checkFlag always gets the most up-to-date value.
The observer pattern lets objects automatically get notified when another object changes. It’s great for loose coupling in systems like GUIs or event handling.
In Java, we now use PropertyChangeListener instead of the deprecated Observable. A quick example I can think of is a WeatherStation class, which uses PropertyChangeSupport. You’d do something like:
WeatherStation station = new WeatherStation();
station.addObserver(evt -> System.out.println("New temp: " + evt.getNewValue()));
station.setTemperature(25);
This way, observers automatically update when the temperature changes, creating a flexible decoupled design.
Comparable and Comparator are both used for sorting objects but with different purposes.
You can implement a Comparable in the class itself to define its natural ordering. You’d use it when there’s a clear, default way to compare objects of that class. For example:
public class Person implements Comparable<Person> {
private String name;
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
}
/*
This code defines a Person class that implements the Comparable interface.
It allows Person objects to be compared based on their names.
The compareTo method compares the name of the current Person object with another Person object.
This enables natural ordering of Person objects, typically used for sorting or in sorted collections.
*/
Comparator, on the other hand, is a separate class used to define custom orderings. It’s great when you need multiple ways to sort objects or can’t modify the original class. Here is a quick example:
Comparator<Person> ageComparator = (p1, p2) -> Integer.compare(p1.getAge(), p2.getAge());
Collections.sort(personList, ageComparator);
/*
This code creates a Comparator for Person objects that compares them based on age.
It then uses this Comparator to sort a list of Person objects.
The lambda expression defines how to compare two Person objects by their age.
The Collections.sort method uses this Comparator to determine the order of elements in personList.
*/
So, use Comparable for the default sorting behavior, and Comparator when you need flexibility in how you sort.
Try-with-resources is a language construct introduced in Java 7 that automatically closes resources that implement AutoCloseable. It simplifies resource management and helps prevent resource leaks. Example:
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
// Use the resource
} catch (IOException e) {
// Handle exceptions
}
// br is automatically closed
The synchronized keyword can be great if you know how to use it. It can control access to a block of code or method in multi-threaded environments, ensuring that only one thread can execute the synchronized code at a time.
For example, you might use it like this:
public synchronized void updateBalance(int amount) {
balance += amount;
}
/*
* We need a synchronized block for this method because it modifies a shared
* resource (balance) that could be accessed by multiple threads simultaneously.
* Synchronization ensures that only one thread can execute this method at a time,
* preventing race conditions and maintaining data consistency.
*/
However, its overuse can lead to performance issues due to thread contention (threads competing for resources). It can also cause deadlocks if not used carefully.
Here's a summary of the terms:
HashMap: Unordered, allows null keys and values, O(1) average time complexity for basic operations.
LinkedHashMap: Maintains insertion order (or access order), allows null keys and values, slightly slower than HashMap.
TreeMap: Sorted by keys, doesn't allow null keys, O(log n) time complexity for basic operations.
I’d like to answer this question with an example. Consider this class:
public class User implements Serializable {
private String username;
private transient String password;
}
In this case, when you serialize a User object, the password won’t be included in the serialized data because it is transient. So, this keyword is used to indicate fields that shouldn't be serialized when the object is converted to a byte stream.
The keyword can also be useful for fields that contain temporary data or derived values that can be calculated. Also, it can be a lifesaver if the field includes data that doesn’t support serialization.
Method overloading and overriding are two forms of polymorphism in Java.
When multiple methods in the same class have the same name but different parameters, we call it method overloading. For example, consider a calculator class:
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
Here, we’ve overloaded the add method to work with both integers and doubles.
On the other hand, method overriding happens when a subclass provides a different implementation for a method defined in its superclass — aka, runtime polymorphism. For instance:
class Animal {
void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof");
}
}
In this case, Dog overrides the makeSound method from Animal.
In the past, if you wanted to add a method to an interface, you’d have to implement it in every class that uses the interface. That could be a massive pain, especially in large codebases.
With default methods introduced in Java 8, you can provide a default implementation of a method right in the interface and not break any existing code. For example:
interface Vehicle {
void start();
default void honk() {
System.out.println("Beep beep!");
}
}
The Vehicle interface now has a default honk() method and any class implementing the interface gets it, but can still override it if needed.
Checked and unchecked exceptions indicate the method of handling them.
Checked exceptions are checked at compile-time. You must either catch them or declare them in the method signature. They are typically used for recoverable conditions that you expect might happen. For example:
try {
FileReader file = new FileReader("example.txt");
} catch (FileNotFoundException e) {
// Handle the exception
}
Unchecked exceptions, also called runtime exceptions, aren’t checked at compile-time. You don’t have to do anything, no checks or declarations. Just face them and fix them as they usually indicate programming errors and bugs such as null pointer references or illegal arguments. For instance:
int[] arr = new int[5];
arr[10] = 50; // Throws ArrayIndexOutOfBoundsException; you don't have to catch it
java.util.concurrent is a dedicated package for concurrent programming in Java. It offers thread-safe, high-performance alternatives to traditional synchronization.
Key classes in the package include:
1. ExecutorService: For managing and executing tasks asynchronously.
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> System.out.println("Task executed"));
2. ConcurrentHashMap: A thread-safe version of HashMap.
Map<String, Integer> map = new ConcurrentHashMap<>();
3. CountDownLatch: For synchronizing threads waiting for operations to complete.
CountDownLatch latch = new CountDownLatch(3);
latch.countDown(); // in worker threads
latch.await(); // in waiting thread
4. AtomicInteger: For lock-free atomic operations on integers.
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
It is better to explain a functional interface with a simple analogy. Think of it as a simple job description with just one task. For example, imagine a ‘Chef’ job where the only requirement is to ‘cook a meal’.
Now, lambda expressions are like quick, on-the-spot hires for these simple jobs. Instead of going through a lengthy hiring process and creating a full employee contract, you can just tell someone, ‘Hey, here is how to cook the meal’, and they do it.
For example:
interface Chef {
String cookMeal(String ingredient);
}
// Traditional way (like a formal hire)
Chef traditionalChef = new Chef() {
public String cookMeal(String ingredient) {
return "Cooked " + ingredient;
}
};
// Lambda way (like a quick, on-the-spot hire)
Chef lambdaChef = ingredient -> "Cooked " + ingredient;
System.out.println(lambdaChef.cookMeal("chicken")); // Outputs: Cooked chicken
In this analogy, the Chef interface is our functional interface with one job (cookMeal), and the lambda expression is our quick way to fill that job without the formalities.
Synchronized collections: Found in java.util.Collections (e.g., Collections.synchronizedList). They are thread-safe but achieve this by synchronizing all methods, which can cause contention in multi-threaded environments.
Concurrent collections: Found in java.util.concurrent (e.g., ConcurrentHashMap). They allow concurrent access by multiple threads using more fine-grained locks or lock-free algorithms, improving performance in concurrent environments.
The Fork/Join framework, introduced in Java 7, is designed for parallel processing of tasks. It splits tasks into smaller subtasks (fork), executes them concurrently, and combines the results (join). It can be used for processing large datasets or recursive algorithms like quicksort. For example:
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(new RecursiveTaskExample());
Want to go deeper? DataCamp's Intermediate Java course covers methods, control flow, and loops, while Data Types and Exceptions in Java covers the Collections Framework and exception handling covered in this section.
If you’re a senior developer with up to 10 years of experience in Java, you may encounter questions such as these:
Lock-free programming, as the name suggests, is used to achieve thread-safety in multi-threaded environments without using locks. This is done by leveraging atomic operations and compare-and-swap (CAS) mechanisms.
This programming paradigm offers better scalability, no deadlocks and often better performance. Here is a quick example:
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // Thread-safe, lock-free increment
However, it comes with the following challenges:
Java’s Garbage Collection (GC) is an automatic cleaning system that finds and removes unused objects to free up memory. For apps that need to handle lots of data quickly without delays, you can make the following adjustments to this underlying mechanism:
For example, you might run your app like this:
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xmx4g YourApp
This tells Java to use G1GC, try not to pause for more than 200 milliseconds, and use up to 4 GB of memory.
JMM sets the rules for how Java programs interact with computer memory, especially in multi-threaded environments. The happens-before relationship is a key part of this model.
‘Happens-before’ ensures that memory operations in one thread are correctly ordered with respect to operations in another thread. This is crucial for writing correct concurrent code.
For example, consider the volatile keyword:
public class SharedData {
private volatile boolean flag = false;
private int data = 0;
void writeData() {
data = 42;
flag = true;
}
void readData() {
if (flag) {
// This is guaranteed to see data = 42
System.out.println(data);
}
}
}
Here, the JMM guarantees that if flag is true in readData(), it will see the updated value of data. This is because the write to flag happens-before the read of flag, establishing an ordering between the threads.
Java agents are special utilities that can watch and change how a Java program runs, without needing to change the original code. One way to think about them is as invisible assistants that can:
To use a Java agent, you’d typically write special code that tells the agent what to look for, package this code into a file (called a JAR), and tell Java to use this agent when running your program.
Class loading in Java involves three main steps:
To implement a custom class loader, you’d extend the ClassLoader class and override the findClass method. Here’s a simple example:
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassFromFile(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassFromFile(String fileName) {
// Load the class file as bytes
// Implementation details omitted for brevity
}
}
Use cases for custom class loaders include:
A scalable, distributed caching system could use technologies like Hazelcast or Apache Ignite. Key considerations include:
Reactive programming is a declarative programming paradigm focused on data streams and the propagation of change. In Java, it’s often implemented using libraries like RxJava or Project Reactor. It differs from imperative programming by emphasizing the flow of data and reactions to events, rather than sequential execution of commands. This can lead to more scalable and responsive systems, especially for I/O-bound applications.
There are several approaches:
Each has trade-offs in terms of thread-safety, lazy loading, and serialization behavior. The enum approach is often considered the best as it’s concise, provides serialization for free, and is inherently thread-safe.
ConcurrentHashMap uses a segmented structure, dividing the map into segments that can be locked independently. It uses CAS operations for updates when possible, reducing contention. For read operations, it doesn’t use locks at all, allowing for high concurrency. The internal structure has evolved over different Java versions to improve performance and reduce memory footprint.
Custom annotation processors can be created by implementing the javax.annotation.processing.Processor interface. Key steps include:
CompletableFuture provides a more flexible and functional approach to asynchronous programming compared to Future. It allows chaining tasks with methods like .thenApply(), .thenAccept(), and .thenCompose().
CompletableFuture.supplyAsync(() -> fetchData())
.thenApply(data -> processData(data))
.thenAccept(result -> System.out.println("Result: " + result));
Weak reference: Allows objects to be garbage collected when no strong references exist. Used in WeakHashMap.
Soft reference: Retains objects until memory is low. Commonly used for caching.
Java 17 (LTS) and Java 21 (LTS) introduced several features that now appear regularly in 2026 interviews, especially for roles at companies using modern Java stacks. Here are the key topics to know:
Records are a concise way to declare immutable data classes. They automatically generate constructors, getters, equals(), hashCode(), and toString().
// Traditional POJO (verbose)
public class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
// equals, hashCode, toString omitted for brevity
}
// Record (concise)
public record Point(int x, int y) {}
Sealed classes restrict which classes can extend or implement them, enabling exhaustive pattern matching and better modeling of closed hierarchies.
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
Pattern matching extends the switch expression to work with type patterns, making it easier to work with sealed hierarchies and eliminating verbose instanceof checks.
Shape shape = new Circle(5.0);
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
};
Virtual threads are lightweight threads managed by the JVM (not the OS), enabling millions of concurrent threads without the overhead of platform threads. They are a key feature of Project Loom and dramatically simplify writing high-throughput concurrent applications.
// Platform thread (OS-managed, expensive)
Thread platformThread = new Thread(() -> System.out.println("Platform thread"));
platformThread.start();
// Virtual thread (JVM-managed, lightweight)
Thread virtualThread = Thread.ofVirtual()
.start(() -> System.out.println("Virtual thread"));
// Or via ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> System.out.println("Virtual task"));
}
Virtual threads use the same Thread API, so existing code can migrate with minimal changes. They are ideal for I/O-bound workloads like HTTP servers and database queries.
Preparing for senior interviews? Our Optimizing Code in Java course covers GC tuning, Big-O complexity, JVM internals, multi-threading, and caching . You can also explore Python vs Java: Differences and Similarities to understand where Java excels in modern software engineering.
After 61 questions, we are finally done! While this article covered questions spanning many aspects of Java, there are still so many concepts to learn about the language.
Ready to strengthen your Java skills beyond the interview? Start with the Java Fundamentals skill track on DataCamp to build a solid foundation, or dive deeper with Optimizing Code in Java to master performance tuning — a critical skill for senior roles. For object-oriented design and patterns, the Introduction to Object-Oriented Programming in Java course is a great next step.
Bex Tuychiev builds voice agents and LLM systems, and most of his work goes into making them reliable enough to run in production, preferably in healthcare.
{count, plural, one {blog} other {blogs}}
Elena Kosourova
15 min
{count, plural, one {blog} other {blogs}}
Elena Kosourova
15 min
{count, plural, one {blog} other {blogs}}
Abid Ali Awan
15 min
{count, plural, one {blog} other {blogs}}
Josep Ferrer
15 min
{count, plural, one {blog} other {blogs}}
Kevin Babitz
14 min
{count, plural, one {blog} other {blogs}}
Abid Ali Awan
15 min