Java Interview Questions & Answers - Core Concepts
OOP Principles (SOLID, DRY, KISS)
Q1: Explain the SOLID principles with examples.
Answer:
S - Single Responsibility Principle (SRP): A class should have only one reason to change, meaning it
should have only one responsibility.
java
// Bad - violates SRP
class User {
private String name;
private String email;
public void save() { /* database logic */ }
public void sendEmail() { /* email logic */ }
public String generateReport() { /* reporting logic */ }
}
// Good - follows SRP
class User {
private String name;
private String email;
// getters and setters
}
class UserRepository {
public void save(User user) { /* database logic */ }
}
class EmailService {
public void sendEmail(User user) { /* email logic */ }
}
class ReportGenerator {
public String generateUserReport(User user) { /* reporting logic */ }
}
O - Open/Closed Principle (OCP): Classes should be open for extension but closed for modification.
java
// Good - follows OCP using Strategy pattern
interface PaymentProcessor {
void processPayment(double amount);
}
class CreditCardProcessor implements PaymentProcessor {
public void processPayment(double amount) {
// Credit card processing logic
}
}
class PayPalProcessor implements PaymentProcessor {
public void processPayment(double amount) {
// PayPal processing logic
}
}
class PaymentService {
private PaymentProcessor processor;
public PaymentService(PaymentProcessor processor) {
this.processor = processor;
}
public void processPayment(double amount) {
processor.processPayment(amount);
}
}
L - Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of
subclasses without breaking functionality.
java
// Good - follows LSP
abstract class Bird {
abstract void move();
}
class Sparrow extends Bird {
void move() {
fly();
}
private void fly() { /* flying logic */ }
}
class Penguin extends Bird {
void move() {
walk();
}
private void walk() { /* walking logic */ }
}
I - Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they
don't use.
java
// Bad - violates ISP
interface Worker {
void work();
void eat();
void sleep();
}
// Good - follows ISP
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
class Human implements Workable, Eatable, Sleepable {
public void work() { /* work logic */ }
public void eat() { /* eat logic */ }
public void sleep() { /* sleep logic */ }
}
class Robot implements Workable {
public void work() { /* work logic */ }
}
D - Dependency Inversion Principle (DIP): High-level modules should not depend on low-level
modules. Both should depend on abstractions.
java
// Good - follows DIP
interface NotificationService {
void send(String message);
}
class EmailNotification implements NotificationService {
public void send(String message) {
// Email sending logic
}
}
class OrderService {
private NotificationService notificationService;
public OrderService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void processOrder() {
// Order processing logic
notificationService.send("Order processed");
}
}
Q2: What are DRY and KISS principles?
Answer:
DRY (Don't Repeat Yourself): Avoid code duplication by extracting common functionality.
java
// Bad - violates DRY
class Calculator {
public double calculateCircleArea(double radius) {
return 3.14159 * radius * radius;
}
public double calculateCircleCircumference(double radius) {
return 2 * 3.14159 * radius;
}
}
// Good - follows DRY
class Calculator {
private static final double PI = 3.14159;
public double calculateCircleArea(double radius) {
return PI * radius * radius;
}
public double calculateCircleCircumference(double radius) {
return 2 * PI * radius;
}
}
KISS (Keep It Simple, Stupid): Keep code simple and readable.
java
// Bad - overly complex
class StringProcessor {
public String processString(String input) {
return Optional.ofNullable(input)
.filter(s -> !s.isEmpty())
.map(String::trim)
.map(String::toLowerCase)
.orElse("");
}
}
// Good - simple and clear
class StringProcessor {
public String processString(String input) {
if (input == null || input.isEmpty()) {
return "";
}
return input.trim().toLowerCase();
}
}
Generics, Lambda Expressions, Functional Interfaces
Q3: Explain Java Generics with examples and their benefits.
Answer:
Generics provide type safety at compile time and eliminate the need for casting.
java
// Without generics
List list = new ArrayList();
list.add("Hello");
list.add(123); // Runtime error potential
String str = (String) list.get(0); // Explicit casting required
// With generics
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// stringList.add(123); // Compile-time error
String str = stringList.get(0); // No casting needed
// Generic class example
class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
// Generic method example
class Utility {
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
// Bounded generics
class NumberBox<T extends Number> {
private T number;
public void setNumber(T number) {
this.number = number;
}
public double getDoubleValue() {
return number.doubleValue();
}
}
Benefits:
Type safety at compile time
Elimination of casts
Enabling generic algorithms
Better code readability
Q4: What are Lambda expressions and how do they work?
Answer:
Lambda expressions are anonymous functions that provide a concise way to represent functional
interfaces.
java
// Traditional anonymous class
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Running...");
}
};
// Lambda expression
Runnable r2 = () -> System.out.println("Running...");
// Lambda with parameters
Comparator<String> comp1 = (s1, s2) -> s1.compareTo(s2);
// Lambda with multiple statements
Function<String, String> processor = (input) -> {
String trimmed = input.trim();
String upperCase = trimmed.toUpperCase();
return upperCase;
};
// Method reference examples
List<String> names = Arrays.asList("John", "Jane", "Bob");
// Lambda
names.forEach(name -> System.out.println(name));
// Method reference
names.forEach(System.out::println);
// Constructor reference
Function<String, StringBuilder> sbCreator = StringBuilder::new;
Q5: Explain Functional Interfaces with examples.
Answer:
Functional interfaces have exactly one abstract method and can be used with lambda expressions.
java
// Built-in functional interfaces
@FunctionalInterface
interface Function<T, R> {
R apply(T t);
}
@FunctionalInterface
interface Predicate<T> {
boolean test(T t);
}
@FunctionalInterface
interface Consumer<T> {
void accept(T t);
}
@FunctionalInterface
interface Supplier<T> {
T get();
}
// Custom functional interface
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
// Usage examples
public class FunctionalInterfaceExample {
public static void main(String[] args) {
// Function example
Function<String, Integer> stringLength = s -> s.length();
System.out.println(stringLength.apply("Hello")); // 5
// Predicate example
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true
// Consumer example
Consumer<String> printer = s -> System.out.println(s);
printer.accept("Hello World");
// Supplier example
Supplier<Double> randomValue = () -> Math.random();
System.out.println(randomValue.get());
// Custom functional interface
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
System.out.println(add.calculate(5, 3)); // 8
System.out.println(multiply.calculate(5, 3)); // 15
}
}
Java Streams API
Q6: Explain Java Streams with map/reduce operations and provide examples.
Answer:
Streams provide a functional approach to processing collections of data.
java
import java.util.*;
import java.util.stream.*;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Jane", "Bob", "Alice", "Charlie");
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Map operations - transform elements
List<String> upperCaseNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseNames); // [JOHN, JANE, BOB, ALICE, CHARLIE]
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(squares); // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
// Filter operations
List<String> longNames = names.stream()
.filter(name -> name.length() > 4)
.collect(Collectors.toList());
System.out.println(longNames); // [Alice, Charlie]
// Reduce operations - aggregate elements
Optional<Integer> sum = numbers.stream()
.reduce((a, b) -> a + b);
System.out.println(sum.get()); // 55
// Reduce with identity
Integer product = numbers.stream()
.reduce(1, (a, b) -> a * b);
System.out.println(product); // 3628800
// Complex example with chaining
OptionalDouble average = numbers.stream()
.filter(n -> n % 2 == 0) // even numbers
.mapToInt(Integer::intValue)
.average();
System.out.println(average.getAsDouble()); // 6.0
// Parallel streams
long count = numbers.parallelStream()
.filter(n -> n > 5)
.count();
System.out.println(count); // 5
}
}
Q7: Explain Collectors and provide practical examples.
Answer:
Collectors are used to accumulate stream elements into collections or other summary forms.
java
import java.util.*;
import java.util.stream.*;
class Employee {
private String name;
private String department;
private double salary;
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
// getters
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
@Override
public String toString() {
return name + "(" + department + ", $" + salary + ")";
}
}
public class CollectorsExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("John", "IT", 70000),
new Employee("Jane", "HR", 65000),
new Employee("Bob", "IT", 75000),
new Employee("Alice", "Finance", 80000),
new Employee("Charlie", "IT", 72000)
);
// Collecting to List
List<String> names = employees.stream()
.map(Employee::getName)
.collect(Collectors.toList());
// Collecting to Set
Set<String> departments = employees.stream()
.map(Employee::getDepartment)
.collect(Collectors.toSet());
// Grouping by department
Map<String, List<Employee>> byDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
System.out.println(byDepartment);
// Partitioning by salary
Map<Boolean, List<Employee>> partitioned = employees.stream()
.collect(Collectors.partitioningBy(emp -> emp.getSalary() > 70000));
System.out.println("High salary: " + partitioned.get(true));
// Calculating statistics
DoubleSummaryStatistics salaryStats = employees.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println("Average salary: " + salaryStats.getAverage());
System.out.println("Max salary: " + salaryStats.getMax());
// Joining strings
String allNames = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(allNames); // [John, Jane, Bob, Alice, Charlie]
// Custom collector
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
System.out.println(avgSalaryByDept);
}
}
Java Collections Framework
Q8: Explain the Java Collections hierarchy and when to use each collection type.
Answer:
java
import java.util.*;
import java.util.concurrent.*;
public class CollectionsExample {
public static void main(String[] args) {
// ArrayList - Resizable array, good for random access
List<String> arrayList = new ArrayList<>();
arrayList.add("First");
arrayList.add("Second");
arrayList.add(1, "Middle"); // Insert at index
System.out.println("ArrayList: " + arrayList);
// LinkedList - Doubly-linked list, good for frequent insertions/deletions
List<String> linkedList = new LinkedList<>();
linkedList.add("A");
linkedList.add("B");
((LinkedList<String>) linkedList).addFirst("Start");
System.out.println("LinkedList: " + linkedList);
// HashSet - No duplicates, no ordering, O(1) average time complexity
Set<String> hashSet = new HashSet<>();
hashSet.add("Apple");
hashSet.add("Banana");
hashSet.add("Apple"); // Duplicate ignored
System.out.println("HashSet: " + hashSet);
// LinkedHashSet - Maintains insertion order
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("First");
linkedHashSet.add("Second");
linkedHashSet.add("Third");
System.out.println("LinkedHashSet: " + linkedHashSet);
// TreeSet - Sorted set, implements NavigableSet
Set<Integer> treeSet = new TreeSet<>();
treeSet.addAll(Arrays.asList(5, 2, 8, 1, 9, 3));
System.out.println("TreeSet: " + treeSet); // [1, 2, 3, 5, 8, 9]
// HashMap - Key-value pairs, no ordering
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("Apple", 10);
hashMap.put("Banana", 20);
hashMap.put("Cherry", 15);
System.out.println("HashMap: " + hashMap);
// LinkedHashMap - Maintains insertion order
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("First", 1);
linkedHashMap.put("Second", 2);
linkedHashMap.put("Third", 3);
System.out.println("LinkedHashMap: " + linkedHashMap);
// TreeMap - Sorted map
Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("Charlie", 3);
treeMap.put("Alice", 1);
treeMap.put("Bob", 2);
System.out.println("TreeMap: " + treeMap); // Sorted by key
// PriorityQueue - Heap-based priority queue
Queue<Integer> priorityQueue = new PriorityQueue<>();
priorityQueue.addAll(Arrays.asList(5, 2, 8, 1, 9));
System.out.println("PriorityQueue poll: " + priorityQueue.poll()); // 1 (smallest)
// ArrayDeque - Resizable array implementation of Deque
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("First");
deque.addLast("Last");
deque.addFirst("New First");
System.out.println("Deque: " + deque);
// Thread-safe collections
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// Utility methods
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Collections.reverse(numbers);
System.out.println("Reversed: " + numbers);
Collections.shuffle(numbers);
System.out.println("Shuffled: " + numbers);
System.out.println("Max: " + Collections.max(numbers));
System.out.println("Min: " + Collections.min(numbers));
}
}
Q9: What are the differences between HashMap, LinkedHashMap, and TreeMap?
Answer:
Feature HashMap LinkedHashMap TreeMap
Ordering No ordering Insertion order Sorted by keys
Time Complexity O(1) average O(1) average O(log n)
Null Keys One null key allowed One null key allowed No null keys
Interface Map Map NavigableMap, SortedMap
Memory Less memory More memory (doubly-linked list) More memory (Red-Black tree)
java
// Performance comparison example
public class MapComparison {
public static void main(String[] args) {
Map<Integer, String> hashMap = new HashMap<>();
Map<Integer, String> linkedHashMap = new LinkedHashMap<>();
Map<Integer, String> treeMap = new TreeMap<>();
// Adding elements
for (int i = 5; i >= 1; i--) {
hashMap.put(i, "Value" + i);
linkedHashMap.put(i, "Value" + i);
treeMap.put(i, "Value" + i);
}
System.out.println("HashMap: " + hashMap); // Random order
System.out.println("LinkedHashMap: " + linkedHashMap); // Insertion order (5,4,3,2,1)
System.out.println("TreeMap: " + treeMap); // Sorted order (1,2,3,4,5)
// TreeMap specific operations
System.out.println("First key: " + treeMap.firstKey());
System.out.println("Last key: " + treeMap.lastKey());
System.out.println("Keys < 3: " + treeMap.headMap(3));
System.out.println("Keys >= 3: " + treeMap.tailMap(3));
}
}
Java Reflection API
Q10: Explain Java Reflection with practical examples.
Answer:
Reflection allows inspection and manipulation of classes, methods, fields, and constructors at runtime.
java
import java.lang.reflect.*;
class Person {
private String name;
protected int age;
public String email;
public Person() {}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private void privateMethod() {
System.out.println("Private method called");
}
public void publicMethod(String message) {
System.out.println("Public method: " + message);
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
public class ReflectionExample {
public static void main(String[] args) throws Exception {
// Getting Class object
Class<?> personClass = Person.class;
// Alternative ways:
// Class<?> personClass = Class.forName("Person");
// Class<?> personClass = new Person().getClass();
System.out.println("Class name: " + personClass.getName());
System.out.println("Simple name: " + personClass.getSimpleName());
System.out.println("Package: " + personClass.getPackage().getName());
// Getting constructors
Constructor<?>[] constructors = personClass.getConstructors();
System.out.println("\nConstructors:");
for (Constructor<?> constructor : constructors) {
System.out.println(constructor);
}
// Creating instance using reflection
Constructor<?> constructor = personClass.getConstructor(String.class, int.class);
Object person = constructor.newInstance("John", 25);
// Getting and manipulating fields
System.out.println("\nFields:");
Field[] fields = personClass.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName() + " - " + field.getType());
// Access private field
if (field.getName().equals("name")) {
field.setAccessible(true); // Bypass private access
System.out.println("Private field value: " + field.get(person));
field.set(person, "Jane"); // Modify private field
System.out.println("Modified private field value: " + field.get(person));
}
}
// Getting and invoking methods
System.out.println("\nMethods:");
Method[] methods = personClass.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName() + " - Parameters: " + method.getParameterCount());
}
// Invoke public method
Method publicMethod = personClass.getMethod("publicMethod", String.class);
publicMethod.invoke(person, "Hello from reflection!");
// Invoke private method
Method privateMethod = personClass.getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true);
privateMethod.invoke(person);
// Getting annotations (if any exist)
Annotation[] annotations = personClass.getAnnotations();
System.out.println("\nAnnotations: " + annotations.length);
// Checking modifiers
int modifiers = personClass.getModifiers();
System.out.println("Is public: " + Modifier.isPublic(modifiers));
System.out.println("Is final: " + Modifier.isFinal(modifiers));
System.out.println("Is abstract: " + Modifier.isAbstract(modifiers));
}
}
Q11: What are the practical use cases of Reflection?
Answer:
java
// 1. Framework Development (like Spring, Hibernate)
class SimpleFramework {
public static void injectDependencies(Object obj) throws Exception {
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Inject.class)) {
field.setAccessible(true);
// Simplified dependency injection
Object dependency = createInstance(field.getType());
field.set(obj, dependency);
}
}
}
private static Object createInstance(Class<?> type) throws Exception {
return type.getDeclaredConstructor().newInstance();
}
}
@interface Inject {}
class DatabaseService {
public void connect() {
System.out.println("Database connected");
}
}
class UserService {
@Inject
private DatabaseService dbService;
public void performOperation() {
dbService.connect();
System.out.println("User operation performed");
}
}
// 2. Serialization/Deserialization
class SimpleSerializer {
public static String serialize(Object obj) throws Exception {
Class<?> clazz = obj.getClass();
StringBuilder sb = new StringBuilder();
sb.append(clazz.getSimpleName()).append(":{");
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
sb.append(field.getName()).append("=").append(field.get(obj)).append(",");
}
sb.append("}");
return sb.toString();
}
}
// 3. Testing frameworks
class TestRunner {
public static void runTests(Class<?> testClass) throws Exception {
Object testInstance = testClass.getDeclaredConstructor().newInstance();
Method[] methods = testClass.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Test.class)) {
try {
method.invoke(testInstance);
System.out.println("Test " + method.getName() + " passed");
} catch (Exception e) {
System.out.println("Test " + method.getName() + " failed: " + e.getMessage());
}
}
}
}
}
@interface Test {}
class MyTests {
@Test
public void testAddition() {
assert 2 + 2 == 4;
}
@Test
public void testSubtraction() {
assert 5 - 3 == 2;
}
}
Exception Handling
Q12: Explain exception handling hierarchy and best practices with examples.
Answer:
java
// Exception hierarchy:
// Throwable
// ├── Error (JVM errors, not typically handled)
// └── Exception
// ├── RuntimeException (unchecked)
// │ ├── NullPointerException
// │ ├── ArrayIndexOutOfBoundsException
// │ └── IllegalArgumentException
// └── Checked Exceptions
// ├── IOException
// ├── SQLException
// └── ClassNotFoundException
import java.io.*;
import java.sql.*;
// Custom exception examples
class InsufficientFundsException extends Exception {
private double amount;
public InsufficientFundsException(double amount) {
super("Insufficient funds: " + amount);
this.amount = amount;
}
public double getAmount() { return amount; }
}
class InvalidAccountException extends RuntimeException {
public InvalidAccountException(String message) {
super(message);
}
}
class BankAccount {
private double balance;
private String accountNumber;
public BankAccount(String accountNumber, double balance) {
if (accountNumber == null || accountNumber.trim().isEmpty()) {
throw new InvalidAccountException("Account number cannot be null or empty");
}
this.accountNumber = accountNumber;
this.balance = balance;
}
public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (amount > balance) {
throw new InsufficientFundsException(amount - balance);
}
balance -= amount;
}
public double getBalance() { return balance; }
}
public class ExceptionHandlingExample {
// Method that handles multiple exceptions
public static void performBankOperation(String accountNumber, double withdrawAmount) {
try {
BankAccount account = new BankAccount(accountNumber, 1000.0);
account.withdraw(withdrawAmount);
System.out.println("Withdrawal successful. Remaining balance: " + account.getBalance());
} catch (InvalidAccountException e) {
System.err.println("Invalid account: " + e.getMessage());
} catch (InsufficientFundsException e) {
System.err.println("Transaction failed: " + e.getMessage());
System.err.println("Short by: $" + e.getAmount());
} catch (IllegalArgumentException e) {
System.err.println("Invalid amount: " + e.getMessage());
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
e.printStackTrace();
}
}
// Try-with-resources example
public static void readFileExample(String filename) {
// Automatic resource management
try (BufferedReader reader = Files.newBufferedReader(Paths.get(filename));
PrintWriter writer = new PrintWriter("output.txt")) {
String line;
while ((line = reader.readLine()) != null) {
writer.println(line.toUpperCase());
}
} catch (IOException e) {
System.err.println("File operation failed: " + e.getMessage());
}
// Resources automatically closed here
}
// Finally block example
public static void databaseExample() {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://localhost/test", "user", "pass");
stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, 1);
rs = stmt.executeQuery();
while (rs.next()) {
System.out.println("User: " + rs.getString("name"));
}
} catch (SQLException e) {
System.err.println("Database error: " + e.getMessage());