As a Java developer preparing for your next big interview, you need to be ready for Java scenario based interview questions that test your problem-solving skills and practical knowledge. In this blog post, we’ll dive deep into 20 essential Java scenario-based interview questions, complete with detailed answers and explanations. Let’s get started.
Java Scenario Based Interview Questions:
Singleton Class
Question: Explain how you’d implement a thread-safe singleton class in Java.
Answer:
Here’s an example of a thread-safe singleton implementation using the double-checked locking 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;
}
}
Explanation:
- We use the
volatile
keyword to ensure that changes to the instance variable are immediately visible to other threads. - The double-checked locking pattern minimizes the use of synchronization, improving performance.
- The private constructor prevents direct instantiation.
- The
getInstance()
method first checks if an instance exists before entering the synchronized block, reducing overhead.
Handling Exceptions in File Reading
Question: How would you handle exceptions in a method that reads from a file?
Answer:
Here’s an example of how to handle exceptions when reading from a file:
public static String readFile(String fileName) throws IOException {
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
} catch (FileNotFoundException e) {
System.err.println("File not found: " + fileName);
throw e;
} catch (IOException e) {
System.err.println("Error reading file: " + fileName);
throw e;
}
return content.toString();
}
Explanation:
- We use a try-with-resources statement to ensure the BufferedReader is closed automatically.
- We catch specific exceptions (FileNotFoundException and IOException) to provide more detailed error messages.
- We re-throw the exceptions to allow the calling method to handle them if necessary.
- This approach follows the principle of “fail fast” by not suppressing exceptions.
Thread-Safe Counter
Question: You need to implement a thread-safe counter. How would you do it?
Answer:
Here’s an implementation of a thread-safe counter using AtomicInteger:
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadSafeCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public void decrement() {
count.decrementAndGet();
}
public int getValue() {
return count.get();
}
}
Explanation:
- We use AtomicInteger from the java.util.concurrent.atomic package, which provides atomic operations for integers.
- The incrementAndGet() and decrementAndGet() methods perform atomic increment and decrement operations, ensuring thread safety.
- This implementation is lock-free, offering better performance than using synchronized methods.
Observer Pattern
Question: Explain how you’d implement the Observer pattern in a weather monitoring application.
Answer:
Here’s a basic implementation of the Observer pattern for a weather monitoring application:
import java.util.ArrayList;
import java.util.List;
interface Observer {
void update(float temperature, float humidity, float pressure);
}
class WeatherStation {
private List<Observer> observers = new ArrayList<>();
private float temperature;
private float humidity;
private float pressure;
public void registerObserver(Observer o) {
observers.add(o);
}
public void removeObserver(Observer o) {
observers.remove(o);
}
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
notifyObservers();
}
}
class DisplayDevice implements Observer {
@Override
public void update(float temperature, float humidity, float pressure) {
System.out.println("Temperature: " + temperature + "°C");
System.out.println("Humidity: " + humidity + "%");
System.out.println("Pressure: " + pressure + " hPa");
}
}
Explanation:
- We define an Observer interface with an update method that receives weather data.
- The WeatherStation class maintains a list of observers and notifies them when weather data changes.
- The DisplayDevice class implements the Observer interface and updates its display when notified.
- This pattern allows for loose coupling between the weather station and display devices.
Custom Exception
Question: How would you implement a custom exception class?
Answer:
Here’s an example of implementing a custom exception class:
public class InsufficientFundsException extends Exception {
private double amount;
public InsufficientFundsException(double amount) {
super("Insufficient funds: Attempted to withdraw " + amount);
this.amount = amount;
}
public double getAmount() {
return amount;
}
}
Explanation:
- We extend the Exception class to create our custom exception.
- We include a constructor that takes the withdrawal amount as a parameter and passes a descriptive message to the superclass constructor.
- We provide a getter method for the amount, allowing the caller to access this information if needed.
- This custom exception can be used in a banking application to handle insufficient funds scenarios.
Producer-Consumer Pattern
Question: Explain how you’d implement a producer-consumer pattern using BlockingQueue.
Answer:
Here’s an implementation of the producer-consumer pattern using BlockingQueue:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class Producer implements Runnable {
private BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
queue.put(i);
System.out.println("Produced: " + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Consumer implements Runnable {
private BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
Integer item = queue.take();
System.out.println("Consumed: " + item);
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
Thread producerThread = new Thread(new Producer(queue));
Thread consumerThread = new Thread(new Consumer(queue));
producerThread.start();
consumerThread.start();
}
}
Explanation:
- We use a BlockingQueue (specifically, LinkedBlockingQueue) to safely pass items between the producer and consumer.
- The producer adds items to the queue using the put() method, which blocks if the queue is full.
- The consumer removes items from the queue using the take() method, which blocks if the queue is empty.
- This implementation ensures thread-safety and proper coordination between the producer and consumer.
Optimizing Database Queries
Question: Explain how you’d optimize database queries in a Java application.
Answer:
Here are several strategies to optimize database queries in a Java application:
Use prepared statements:
String sql = "SELECT * FROM users WHERE id = ?";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
pstmt.setInt(1, userId);
ResultSet rs = pstmt.executeQuery();
// Process results
}
Implement connection pooling:
ComboPooledDataSource cpds = new ComboPooledDataSource();
cpds.setDriverClass("com.mysql.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql://localhost/mydb");
cpds.setUser("username");
cpds.setPassword("password");
cpds.setMinPoolSize(5);
cpds.setAcquireIncrement(5);
cpds.setMaxPoolSize(20);
Use batch processing for multiple inserts:
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
for (User user : users) {
pstmt.setString(1, user.getName());
pstmt.setString(2, user.getEmail());
pstmt.addBatch();
}
pstmt.executeBatch();
}
Implement proper indexing in the database:
CREATE INDEX idx_user_email ON users (email);
Explanation:
- Prepared statements improve performance by allowing the database to reuse the query plan.
- Connection pooling reduces the overhead of creating new database connections for each query.
- Batch processing reduces the number of round trips to the database for multiple inserts.
- Proper indexing in the database can significantly improve query performance, especially for large tables.
Implementing a Custom Thread Pool
Question: You’re tasked with creating a thread pool for handling incoming network connections. How would you do this?
Answer:
Here’s an implementation of a custom thread pool for handling network connections:
import java.util.concurrent.*;
public class NetworkConnectionPool {
private final ExecutorService executorService;
public NetworkConnectionPool(int nThreads) {
this.executorService = new ThreadPoolExecutor(
nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
}
);
}
public void processConnection(Runnable task) {
executorService.execute(task);
}
public void shutdown() {
executorService.shutdown();
try {
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException ex) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
Usage:
NetworkConnectionPool pool = new NetworkConnectionPool(10);
// Process a connection
pool.processConnection(() -> {
// Handle network connection
});
// Shutdown the pool when done
pool.shutdown();
Explanation:
- We use ThreadPoolExecutor to create a fixed-size thread pool.
- The custom ThreadFactory creates daemon threads, which allows the JVM to exit if the main thread completes.
- The processConnection method submits tasks to the thread pool.
- The shutdown method ensures a graceful shutdown of the thread pool.
Caching Mechanism
Question: Describe how you’d implement a caching mechanism using the Proxy pattern.
Answer:
Here’s an implementation of a caching mechanism using the Proxy pattern:
interface Image {
void display();
}
class RealImage implements Image {
private String fileName;
public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading " + fileName);
}
@Override
public void display() {
System.out.println("Displaying " + fileName);
}
}
class ProxyImage implements Image {
private RealImage realImage;
private String fileName;
public ProxyImage(String fileName) {
this.fileName = fileName;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(fileName);
}
realImage.display();
}
}
public class ImageViewer {
public static void main(String[] args) {
Image image1 = new ProxyImage("image1.jpg");
Image image2 = new ProxyImage("image2.jpg");
image1.display(); // Loading and displaying image1
image1.display(); // Only displaying image1 (already loaded)
image2.display(); // Loading and displaying image2
}
}
Explanation:
- The Image interface defines the common interface for RealImage and ProxyImage.
- RealImage represents the actual image object, which is expensive to create.
- ProxyImage acts as a surrogate for RealImage, implementing the same interface.
- ProxyImage creates the RealImage object only when it’s first requested, implementing lazy loading.
- Subsequent calls to display() on the same ProxyImage object reuse the cached RealImage object.
Custom Lock with Timeout
Question: You need to implement a custom lock with timeout capabilities. How would you approach this?
Answer:
Here’s an implementation of a custom lock with timeout capabilities:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class TimeoutLock {
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
return compareAndSetState(0, 1);
}
@Override
protected boolean tryRelease(int arg) {
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
private final Sync sync = new Sync();
public boolean lock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
}
Usage:
TimeoutLock lock = new TimeoutLock();
if (lock.lock(5, TimeUnit.SECONDS)) {
try {
// Critical section
} finally {
lock.unlock();
}
} else {
System.out.println("Failed to acquire lock within timeout");
}
Explanation:
- We extend AbstractQueuedSynchronizer to implement the core locking mechanism.
- The tryAcquire method attempts to set the state from 0 to 1 atomically, indicating lock acquisition.
- The tryRelease method resets the state to 0, releasing the lock.
- The lock method uses tryAcquireNanos to attempt lock acquisition with a timeout.
- This implementation provides a reusable, efficient custom lock with timeout capabilities.
Custom Annotation
Question: Explain how you’d implement a custom annotation and where you might use it.
Answer:
Here’s an example of implementing a custom annotation for method execution timing:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TimeExecution {
}
And here’s how you might use it with an aspect:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class TimingAspect {
@Around("@annotation(TimeExecution)")
public Object timeMethod(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println(joinPoint.getSignature() + " took " + (end - start) + " ms");
return result;
}
}
Usage:
public class MyService {
@TimeExecution
public void doSomething() {
// Method implementation
}
}
Explanation:
- We define a custom annotation
TimeExecution
with runtime retention and method target. - We implement an aspect that intercepts methods annotated with
@TimeExecution
. - The aspect measures the execution time of the method and logs it.
- This annotation can be used to easily add performance logging to specific methods without modifying their code.
Factory Method Pattern
Question: How would you implement a factory method pattern in Java?
Answer:
Here’s an implementation of the factory method pattern for creating different types of vehicles:
interface Vehicle {
void drive();
}
class Car implements Vehicle {
@Override
public void drive() {
System.out.println("Driving a car");
}
}
class Motorcycle implements Vehicle {
@Override
public void drive() {
System.out.println("Riding a motorcycle");
}
}
abstract class VehicleFactory {
abstract Vehicle createVehicle();
public void deliverVehicle() {
Vehicle vehicle = createVehicle();
vehicle.drive();
}
}
class CarFactory extends VehicleFactory {
@Override
Vehicle createVehicle() {
return new Car();
}
}
class MotorcycleFactory extends VehicleFactory {
@Override
Vehicle createVehicle() {
return new Motorcycle();
}
}
Usage:
VehicleFactory carFactory = new CarFactory();
carFactory.deliverVehicle(); // Output: Driving a car
VehicleFactory motorcycleFactory = new MotorcycleFactory();
motorcycleFactory.deliverVehicle(); // Output: Riding a motorcycle
Explanation:
- We define a
Vehicle
interface and concrete implementations (Car
andMotorcycle
). - The abstract
VehicleFactory
class declares the factory methodcreateVehicle()
. - Concrete factory classes (
CarFactory
andMotorcycleFactory
) implement the factory method. - The
deliverVehicle()
method inVehicleFactory
uses the factory method to create and use a vehicle. - This pattern allows for easy extension of vehicle types without modifying existing code.
Custom Thread-Safe Data Structure
Question: You need to implement a custom thread-safe data structure. What considerations would you keep in mind?
Answer:
Here’s an example of a thread-safe bounded buffer implementation:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer<E> {
private final E[] items;
private int putIndex, takeIndex, count;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
@SuppressWarnings("unchecked")
public BoundedBuffer(int capacity) {
items = (E[]) new Object[capacity];
}
public void put(E item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
E item = items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
--count;
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
Explanation:
- We use a
ReentrantLock
to ensure thread-safety. - Conditions (
notFull
andnotEmpty
) are used to coordinate between producers and consumers. - The
put()
method blocks when the buffer is full, and thetake()
method blocks when the buffer is empty. - We use a circular buffer to efficiently use the array space.
- The
lock.lock()
andlock.unlock()
calls are placed in a try-finally block to ensure the lock is always released.
Key considerations for thread-safe data structures:
- Synchronization: Use locks, atomic operations, or other synchronization mechanisms.
- Consistency: Ensure that the data structure remains in a valid state even under concurrent access.
- Performance: Balance thread-safety with performance, using techniques like lock-free algorithms where appropriate.
- Deadlock prevention: Be careful about the order of acquiring multiple locks.
- Fairness: Consider whether operations should be fair (e.g., first-come-first-served) or not.
- Implementing Secure Password Hashing
Question: Explain how you’d implement proper password hashing in a Java application.
Answer:
Here’s an example of implementing secure password hashing using bcrypt:
import org.mindrot.jbcrypt.BCrypt;
public class PasswordHasher {
private static final int LOG_ROUNDS = 12;
public static String hashPassword(String plainTextPassword) {
return BCrypt.hashpw(plainTextPassword, BCrypt.gensalt(LOG_ROUNDS));
}
public static boolean checkPassword(String plainTextPassword, String hashedPassword) {
return BCrypt.checkpw(plainTextPassword, hashedPassword);
}
}
Usage:
String password = "mySecurePassword123";
String hashedPassword = PasswordHasher.hashPassword(password);
// Store hashedPassword in the database
// Later, when verifying:
boolean isValid = PasswordHasher.checkPassword("mySecurePassword123", hashedPassword);
Explanation:
- We use the BCrypt algorithm, which is designed for password hashing and includes salt automatically.
- The
LOG_ROUNDS
parameter determines the computational cost of the hashing (higher is more secure but slower). hashPassword()
generates a salt and hashes the password.checkPassword()
verifies a plain text password against a hashed password.- This approach protects against rainbow table attacks and makes brute-force attacks computationally expensive.
Circuit Breaker Pattern
Question: You’re tasked with implementing a circuit breaker pattern for fault tolerance. How would you approach this?
Answer:
Here’s a basic implementation of the Circuit Breaker pattern:
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class CircuitBreaker {
private final long timeout;
private final int failureThreshold;
private final long resetTimeout;
private AtomicInteger failureCount;
private AtomicLong lastFailureTime;
private State state;
private enum State {
CLOSED, OPEN, HALF_OPEN
}
public CircuitBreaker(long timeout, int failureThreshold, long resetTimeout) {
this.timeout = timeout;
this.failureThreshold = failureThreshold;
this.resetTimeout = resetTimeout;
this.failureCount = new AtomicInteger(0);
this.lastFailureTime = new AtomicLong(0);
this.state = State.CLOSED;
}
public boolean allowRequest() {
if (state == State.OPEN) {
if (System.currentTimeMillis() - lastFailureTime.get() > resetTimeout) {
synchronized (this) {
if (state == State.OPEN) {
state = State.HALF_OPEN;
}
}
} else {
return false;
}
}
return true;
}
public void recordSuccess() {
failureCount.set(0);
state = State.CLOSED;
}
public void recordFailure() {
failureCount.incrementAndGet();
lastFailureTime.set(System.currentTimeMillis());
if (failureCount.get() >= failureThreshold) {
state = State.OPEN;
}
}
}
Usage:
CircuitBreaker breaker = new CircuitBreaker(1000, 5, 60000);
public void performOperation() {
if (breaker.allowRequest()) {
try {
// Perform the operation
breaker.recordSuccess();
} catch (Exception e) {
breaker.recordFailure();
// Handle the exception
}
} else {
// Handle circuit open (e.g., return cached data, default response, or error)
}
}
Explanation:
- The Circuit Breaker has three states: CLOSED (normal operation), OPEN (failing, rejecting requests), and HALF_OPEN (testing if the system has recovered).
allowRequest()
checks if a request should be allowed based on the current state.recordSuccess()
andrecordFailure()
update the circuit breaker’s state based on the operation’s outcome.- This pattern helps prevent cascading failures in distributed systems by failing fast and allowing time for recovery.
Custom Collection
Question: You need to implement a custom collection that maintains elements in insertion order and allows for efficient removal of the oldest element. How would you approach this?
Answer:
Here’s an implementation of a custom collection called AgeOrderedSet
that maintains elements in insertion order and allows for efficient removal of the oldest element:
import java.util.*;
public class AgeOrderedSet<E> implements Set<E> {
private final Map<E, Node<E>> map;
private Node<E> head;
private Node<E> tail;
private static class Node<E> {
E element;
Node<E> prev;
Node<E> next;
Node(E element) {
this.element = element;
}
}
public AgeOrderedSet() {
this.map = new HashMap<>();
}
@Override
public boolean add(E element) {
if (map.containsKey(element)) {
return false;
}
Node<E> newNode = new Node<>(element);
map.put(element, newNode);
if (tail == null) {
head = tail = newNode;
} else {
newNode.prev = tail;
tail.next = newNode;
tail = newNode;
}
return true;
}
@Override
public boolean remove(Object o) {
Node<E> node = map.remove(o);
if (node == null) {
return false;
}
removeNode(node);
return true;
}
public E removeOldest() {
if (head == null) {
return null;
}
E oldest = head.element;
removeNode(head);
map.remove(oldest);
return oldest;
}
private void removeNode(Node<E> node) {
if (node.prev != null) {
node.prev.next = node.next;
} else {
head = node.next;
}
if (node.next != null) {
node.next.prev = node.prev;
} else {
tail = node.prev;
}
}
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean contains(Object o) {
return map.containsKey(o);
}
// Other Set methods would be implemented here...
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
private Node<E> current = head;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public E next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
E element = current.element;
current = current.next;
return element;
}
};
}
}
Explanation:
- We use a combination of a
HashMap
and a doubly-linked list to achieve the desired functionality. - The
HashMap
allows for O(1) lookups and removals by element. - The doubly-linked list maintains the insertion order and allows for efficient removal of the oldest element.
add()
inserts elements at the tail of the list.removeOldest()
removes the head of the list in O(1) time.- The
iterator()
method returns an iterator that traverses elements in insertion order. - This implementation provides O(1) time complexity for add, remove, and removeOldest operations.
Rate Limiter
Question: You need to implement a rate limiter to restrict the number of requests a user can make within a given time window. How would you approach this?
Answer:
Here’s an implementation of a simple rate limiter using the token bucket algorithm:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class RateLimiter {
private final ConcurrentHashMap<String, TokenBucket> userBuckets;
private final int capacity;
private final int refillRate;
private final long refillPeriodMillis;
public RateLimiter(int capacity, int refillRate, long refillPeriodMillis) {
this.userBuckets = new ConcurrentHashMap<>();
this.capacity = capacity;
this.refillRate = refillRate;
this.refillPeriodMillis = refillPeriodMillis;
}
public boolean allowRequest(String userId) {
TokenBucket bucket = userBuckets.computeIfAbsent(userId, k -> new TokenBucket());
return bucket.consumeToken();
}
private class TokenBucket {
private final AtomicInteger tokens;
private long lastRefillTimestamp;
TokenBucket() {
this.tokens = new AtomicInteger(capacity);
this.lastRefillTimestamp = System.currentTimeMillis();
}
synchronized boolean consumeToken() {
refill();
if (tokens.get() > 0) {
tokens.decrementAndGet();
return true;
}
return false;
}
private void refill() {
long now = System.currentTimeMillis();
long timeElapsed = now - lastRefillTimestamp;
int tokensToAdd = (int) (timeElapsed / refillPeriodMillis * refillRate);
if (tokensToAdd > 0) {
tokens.updateAndGet(currentTokens -> Math.min(capacity, currentTokens + tokensToAdd));
lastRefillTimestamp = now;
}
}
}
}
Usage:
RateLimiter limiter = new RateLimiter(10, 1, 1000); // 10 tokens, refill 1 token per second
String userId = "user123";
for (int i = 0; i < 15; i++) {
if (limiter.allowRequest(userId)) {
System.out.println("Request " + i + " allowed");
} else {
System.out.println("Request " + i + " denied");
}
Thread.sleep(200); // Simulate some delay between requests
}
Explanation:
- We implement a token bucket algorithm, where each user has a bucket of tokens.
- The bucket has a maximum capacity and refills at a specified rate.
allowRequest()
checks if a token is available and consumes it if so.- The
refill()
method adds tokens based on the time elapsed since the last refill. - This implementation is thread-safe and can handle concurrent requests for multiple users.
- The rate limiter allows for bursts of traffic up to the bucket capacity, while still maintaining a long-term rate limit.
Simple Dependency Injection Container
Question: Describe how you’d implement a simple dependency injection container.
Answer:
Here’s a basic implementation of a simple dependency injection container:
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class DIContainer {
private Map<Class<?>, Object> instances = new HashMap<>();
public <T> void register(Class<T> klass, T instance) {
instances.put(klass, instance);
}
public <T> T resolve(Class<T> klass) throws Exception {
T instance = (T) instances.get(klass);
if (instance != null) {
return instance;
}
Constructor<T> constructor = klass.getDeclaredConstructor();
instance = constructor.newInstance();
instances.put(klass, instance);
return instance;
}
}
Usage:
interface MessageService {
String getMessage();
}
class EmailService implements MessageService {
@Override
public String getMessage() {
return "Email message";
}
}
class SMSService implements MessageService {
@Override
public String getMessage() {
return "SMS message";
}
}
class MessagePrinter {
private MessageService messageService;
public MessagePrinter(MessageService messageService) {
this.messageService = messageService;
}
public void printMessage() {
System.out.println(messageService.getMessage());
}
}
public class Main {
public static void main(String[] args) throws Exception {
DIContainer container = new DIContainer();
container.register(MessageService.class, new EmailService());
MessageService messageService = container.resolve(MessageService.class);
MessagePrinter printer = new MessagePrinter(messageService);
printer.printMessage(); // Outputs: Email message
}
}
Explanation:
- The
DIContainer
class maintains a map of class types to their instances. register()
allows manual registration of instances for interfaces or classes.resolve()
returns an instance of the requested class. If an instance doesn’t exist, it creates one using reflection.- This simple implementation supports constructor injection and singleton instances.
- In a real-world scenario, you’d want to add support for more complex dependency graphs, lifecycle management, and different scopes (e.g., prototype, request-scoped).
Simple Object-Relational Mapping (ORM)
Question: How would you implement a simple Object-Relational Mapping (ORM) system in Java?
Answer:
Here’s a basic implementation of a simple ORM system:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Column {
String name();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Table {
String name();
}
public class SimpleORM {
private Connection connection;
public SimpleORM(Connection connection) {
this.connection = connection;
}
public <T> List<T> findAll(Class<T> klass) throws Exception {
Table table = klass.getAnnotation(Table.class);
if (table == null) {
throw new Exception("No Table annotation found on class " + klass.getName());
}
String sql = "SELECT * FROM " + table.name();
try (Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
List<T> results = new ArrayList<>();
while (rs.next()) {
T instance = klass.getDeclaredConstructor().newInstance();
for (Field field : klass.getDeclaredFields()) {
Column column = field.getAnnotation(Column.class);
if (column != null) {
field.setAccessible(true);
field.set(instance, rs.getObject(column.name()));
}
}
results.add(instance);
}
return results;
}
}
public <T> void save(T object) throws Exception {
Class<?> klass = object.getClass();
Table table = klass.getAnnotation(Table.class);
if (table == null) {
throw new Exception("No Table annotation found on class " + klass.getName());
}
StringBuilder columns = new StringBuilder();
StringBuilder values = new StringBuilder();
List<Object> parameterValues = new ArrayList<>();
for (Field field : klass.getDeclaredFields()) {
Column column = field.getAnnotation(Column.class);
if (column != null) {
if (columns.length() > 0) {
columns.append(", ");
values.append(", ");
}
columns.append(column.name());
values.append("?");
field.setAccessible(true);
parameterValues.add(field.get(object));
}
}
String sql = "INSERT INTO " + table.name() + " (" + columns + ") VALUES (" + values + ")";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
for (int i = 0; i < parameterValues.size(); i++) {
pstmt.setObject(i + 1, parameterValues.get(i));
}
pstmt.executeUpdate();
}
}
}
Usage:
@Table(name = "users")
class User {
@Column(name = "id")
private int id;
@Column(name = "name")
private String name;
@Column(name = "email")
private String email;
// Getters and setters...
}
public class Main {
public static void main(String[] args) throws Exception {
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "username", "password");
SimpleORM orm = new SimpleORM(connection);
User user = new User();
user.setName("John Doe");
user.setEmail("john@example.com");
orm.save(user);
List<User> users = orm.findAll(User.class);
for (User u : users) {
System.out.println(u.getName() + " - " + u.getEmail());
}
}
}
Explanation:
- We use annotations (
@Table
and@Column
) to map Java classes and fields to database tables and columns. - The
SimpleORM
class provides basic CRUD operations using reflection to map between objects and database records. findAll()
retrieves all records from a table and maps them to objects of the specified class.save()
inserts a new record into the database based on the object’s fields.- This simple ORM supports basic mapping and CRUD operations, but lacks features like caching, lazy loading, and complex relationship mapping that you’d find in full-fledged ORM frameworks.
Simple Publish-Subscribe System
Question: How would you implement a simple publish-subscribe system in Java?
Answer:
Here’s a basic implementation of a publish-subscribe system:
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
public class PubSubSystem {
private final Map<String, List<Subscriber>> subscribers = new ConcurrentHashMap<>();
public void subscribe(String topic, Subscriber subscriber) {
subscribers.computeIfAbsent(topic, k -> new CopyOnWriteArrayList<>()).add(subscriber);
}
public void unsubscribe(String topic, Subscriber subscriber) {
List<Subscriber> topicSubscribers = subscribers.get(topic);
if (topicSubscribers != null) {
topicSubscribers.remove(subscriber);
}
}
public void publish(String topic, String message) {
List<Subscriber> topicSubscribers = subscribers.get(topic);
if (topicSubscribers != null) {
for (Subscriber subscriber : topicSubscribers) {
subscriber.receive(topic, message);
}
}
}
public interface Subscriber {
void receive(String topic, String message);
}
}
Usage:
PubSubSystem.Subscriber subscriber1 = (topic, message) ->
System.out.println("Subscriber 1 received: " + message + " on topic: " + topic);
PubSubSystem.Subscriber subscriber2 = (topic, message) ->
System.out.println("Subscriber 2 received: " + message + " on topic: " + topic);
pubSub.subscribe("tech", subscriber1);
pubSub.subscribe("tech", subscriber2);
pubSub.subscribe("sports", subscriber1);
pubSub.publish("tech", "New AI breakthrough!");
pubSub.publish("sports", "Team wins championship!");
pubSub.unsubscribe("tech", subscriber1);
pubSub.publish("tech", "Tech stock prices
Distributed Cache
Question: Design and implement a simple distributed cache system in Java.
Answer:
Here’s a basic implementation of a distributed cache using a client-server model:
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.concurrent.ConcurrentHashMap;
// Remote interface
interface DistributedCache extends Remote {
void put(String key, String value) throws RemoteException;
String get(String key) throws RemoteException;
}
// Server implementation
class DistributedCacheImpl extends UnicastRemoteObject implements DistributedCache {
private ConcurrentHashMap<String, String> cache;
public DistributedCacheImpl() throws RemoteException {
super();
this.cache = new ConcurrentHashMap<>();
}
@Override
public void put(String key, String value) throws RemoteException {
cache.put(key, value);
System.out.println("Put: " + key + " = " + value);
}
@Override
public String get(String key) throws RemoteException {
String value = cache.get(key);
System.out.println("Get: " + key + " = " + value);
return value;
}
}
// Server
class CacheServer {
public static void main(String[] args) {
try {
DistributedCache cache = new DistributedCacheImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("DistributedCache", cache);
System.out.println("Cache Server is running...");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
// Client
class CacheClient {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
DistributedCache cache = (DistributedCache) registry.lookup("DistributedCache");
cache.put("key1", "value1");
String value = cache.get("key1");
System.out.println("Retrieved value: " + value);
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}
Explanation:
- We use Java RMI (Remote Method Invocation) to implement a simple distributed cache.
- The
DistributedCache
interface defines the remote methods that can be called. DistributedCacheImpl
is the server-side implementation of the cache, using aConcurrentHashMap
for thread-safe storage.- The
CacheServer
class sets up the RMI registry and binds the cache implementation. - The
CacheClient
class demonstrates how to connect to the cache and perform operations.
This is a basic implementation and would need additional features for a production-ready distributed cache, such as:
- Data partitioning and replication
- Consistency protocols
- Failure detection and recovery
- Eviction policies
- Security measures
- Implementing a Job Scheduler
Question: Design a job scheduler that can schedule and execute tasks at specified times or intervals.
Answer:
Here’s an implementation of a simple job scheduler using Java’s ScheduledExecutorService:
import java.util.concurrent.*;
import java.time.*;
public class JobScheduler {
private final ScheduledExecutorService scheduler;
public JobScheduler(int poolSize) {
this.scheduler = Executors.newScheduledThreadPool(poolSize);
}
public void scheduleOneTime(Runnable task, LocalDateTime executionTime) {
long delay = Duration.between(LocalDateTime.now(), executionTime).toMillis();
scheduler.schedule(task, delay, TimeUnit.MILLISECONDS);
}
public void scheduleRecurring(Runnable task, Duration interval) {
scheduler.scheduleAtFixedRate(task, 0, interval.toMillis(), TimeUnit.MILLISECONDS);
}
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
}
}
public static void main(String[] args) {
JobScheduler scheduler = new JobScheduler(4);
// Schedule a one-time task
scheduler.scheduleOneTime(() -> System.out.println("One-time task executed"),
LocalDateTime.now().plusSeconds(5));
// Schedule a recurring task
scheduler.scheduleRecurring(() -> System.out.println("Recurring task executed"),
Duration.ofSeconds(2));
// Let the scheduler run for a while
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
scheduler.shutdown();
}
}
Explanation:
- We use
ScheduledExecutorService
to manage the scheduling of tasks. scheduleOneTime
allows scheduling a task to run once at a specified time.scheduleRecurring
schedules a task to run repeatedly at fixed intervals.- The
shutdown
method provides a graceful shutdown of the scheduler. - In the
main
method, we demonstrate scheduling both one-time and recurring tasks.
This implementation provides basic job scheduling functionality. For a more robust job scheduler, you might consider adding:
- Persistence of job schedules
- Job prioritization
- Error handling and retries
- Distributed execution
- Job dependencies and workflows
- Implementing a Simple HTTP Server
Question: Implement a basic HTTP server in Java that can handle GET requests.
Answer:
Here’s a simple implementation of an HTTP server using Java’s built-in com.sun.net.httpserver
package:
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
public class SimpleHTTPServer {
public static void main(String[] args) throws IOException {
int port = 8000;
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/", new RootHandler());
server.createContext("/hello", new HelloHandler());
server.setExecutor(null); // Use the default executor
server.start();
System.out.println("Server is listening on port " + port);
}
static class RootHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
String response = "Welcome to the Simple HTTP Server!";
sendResponse(exchange, response);
}
}
static class HelloHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
String response = "Hello, World!";
sendResponse(exchange, response);
}
}
private static void sendResponse(HttpExchange exchange, String response) throws IOException {
exchange.sendResponseHeaders(200, response.length());
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
Explanation:
- We use
HttpServer
from thecom.sun.net.httpserver
package to create a simple HTTP server. - The server listens on port 8000 and can handle requests to two paths: “/” and “/hello”.
- Each path is associated with a handler that implements the
HttpHandler
interface. - The
RootHandler
responds to requests at the root path (“/”). - The
HelloHandler
responds to requests at the “/hello” path. - The
sendResponse
method is a utility to send the HTTP response back to the client.
To test this server:
- Run the
SimpleHTTPServer
class. - Open a web browser and navigate to
http://localhost:8000/
andhttp://localhost:8000/hello
.
This is a basic implementation and lacks many features of a production-ready HTTP server, such as:
- Request method handling (POST, PUT, DELETE, etc.)
- Request body parsing
- Header handling
- Static file serving
- Security features
- Logging and monitoring
These examples demonstrate how to approach more complex, real-world scenarios in Java. Remember, in an interview setting, you might not need to implement every detail, but you should be able to discuss these aspects and how you would approach them in a full implementation.
As you prepare for your interview, practice implementing these solutions and variations of them. Also, be ready to discuss trade-offs and alternative approaches for each scenario. Good luck with your interview preparation!
Certainly! Here’s a call-to-action (CTA) for the link you provided:
👉 Explore 100+ Java Interview Questions Now