Unit I: Java Fundamentals
1. Define JDK, JRE, and JVM. Explain their relationship.
JVM (Java Virtual Machine):
- Abstract machine that provides runtime environment to execute Java bytecode
- Platform-dependent (different JVM for Windows, Linux, Mac)
- Performs memory management, garbage collection, security
- Components: Class Loader, Bytecode Verifier, Execution Engine
JRE (Java Runtime Environment):
- JVM + Java class libraries + supporting files
- Provides runtime environment to run Java applications
- Cannot compile Java programs (no development tools)
- End users need JRE to run Java applications
JDK (Java Development Kit):
- JRE + Development tools (javac compiler, debugger, javadoc)
- Required by developers to write and compile Java programs
- Includes tools like jar, jdb, javap
Relationship: JDK ⊃ JRE ⊃ JVM
JDK = JRE + Development Tools
JRE = JVM + Library Classes
JVM = Runtime Engine
2. Why is Java called platform-independent? Explain with diagram.
Java achieves platform independence through its "Write Once, Run Anywhere" (WORA) philosophy:
How it works:
- Java source code (.java) is compiled by javac compiler
- Compiler produces bytecode (.class files), not machine code
- Bytecode is platform-independent intermediate code
- JVM on any platform interprets/executes this bytecode
- JVM converts bytecode to machine-specific instructions
Source Code (.java)
↓
[javac compiler]
↓
Bytecode (.class) ← Platform Independent
↓
[JVM - Windows] [JVM - Linux] [JVM - Mac]
↓ ↓ ↓
Windows Code Linux Code Mac Code
Key Points:
- Java is platform-independent, but JVM is platform-dependent
- Same bytecode runs on any OS with compatible JVM
- This differs from C/C++ which compiles directly to machine code
3. Explain the four pillars of OOP with examples.
1. Encapsulation:
Bundling data (variables) and methods that operate on data into a single unit (class), hiding internal state.
class BankAccount {
private double balance; // Hidden data
public void deposit(double amount) { // Controlled access
if (amount > 0) balance += amount;
}
public double getBalance() {
return balance;
}
}
2. Inheritance:
Creating new classes from existing ones, inheriting properties and behaviors. Promotes code reuse.
class Animal {
void eat() { System.out.println("Eating..."); }
}
class Dog extends Animal { // Dog inherits from Animal
void bark() { System.out.println("Barking..."); }
}
// Dog can use eat() from Animal
3. Polymorphism:
One interface, multiple implementations. Same method behaves differently based on object.
// Compile-time (Overloading)
void print(int x) { }
void print(String s) { }
// Runtime (Overriding)
class Shape { void draw() { } }
class Circle extends Shape {
void draw() { System.out.println("Drawing Circle"); }
}
class Rectangle extends Shape {
void draw() { System.out.println("Drawing Rectangle"); }
}
Shape s = new Circle();
s.draw(); // "Drawing Circle" - decided at runtime
4. Abstraction:
Hiding complex implementation details, showing only essential features.
abstract class Vehicle {
abstract void start(); // What to do, not how
}
class Car extends Vehicle {
void start() {
// Complex implementation hidden
System.out.println("Car started");
}
}
4. Compare and contrast method overloading and method overriding.
| Aspect | Method Overloading | Method Overriding |
|---|---|---|
| Definition | Same method name, different parameters | Same method signature in parent and child |
| Polymorphism Type | Compile-time (Static) | Runtime (Dynamic) |
| Classes Involved | Within same class (or inherited) | Between parent and child classes |
| Parameters | Must be different | Must be same |
| Return Type | Can be different | Must be same or covariant |
| Access Modifier | Can be any | Cannot be more restrictive |
| static/private/final | Can be overloaded | Cannot be overridden |
| Binding | Early binding | Late binding |
Example of Overloading:
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
}
Example of Overriding:
class Animal {
void sound() { System.out.println("Some sound"); }
}
class Dog extends Animal {
@Override
void sound() { System.out.println("Bark"); }
}
5. Differentiate between abstract class and interface.
| Aspect | Abstract Class | Interface |
|---|---|---|
| Methods | Abstract and concrete methods | Abstract methods (default/static since Java 8) |
| Variables | Any type (instance, static, final) | Only public static final (constants) |
| Constructors | Can have constructors | Cannot have constructors |
| Inheritance | Single inheritance (extends one) | Multiple inheritance (implements many) |
| Access Modifiers | Any access modifier | Methods public by default |
| Keyword | extends | implements |
| When to Use | Share code among related classes | Define a contract for unrelated classes |
Example:
// Abstract Class
abstract class Animal {
String name; // Instance variable
Animal(String name) { this.name = name; } // Constructor
abstract void sound(); // Abstract method
void sleep() { System.out.println("Sleeping"); } // Concrete method
}
// Interface
interface Drawable {
int MAX_SIZE = 100; // public static final
void draw(); // public abstract
default void print() { // Default method (Java 8+)
System.out.println("Printing...");
}
}
6. Explain all access modifiers in Java with accessibility table.
Java provides four access modifiers to control visibility of classes, methods, and variables:
| Modifier | Same Class | Same Package | Subclass (Different Package) | Different Package |
|---|---|---|---|---|
| public | ✓ | ✓ | ✓ | ✓ |
| protected | ✓ | ✓ | ✓ | ✗ |
| default | ✓ | ✓ | ✗ | ✗ |
| private | ✓ | ✗ | ✗ | ✗ |
Usage Guidelines:
- private: For internal implementation details, helper methods
- default: For package-level utilities
- protected: For methods meant to be overridden by subclasses
- public: For API methods that other classes should use
public class Example {
public int publicVar; // Accessible everywhere
protected int protectedVar; // Same package + subclasses
int defaultVar; // Same package only
private int privateVar; // This class only
}
7. Explain == vs equals() with examples.
== Operator:
- Compares references (memory addresses) for objects
- Compares actual values for primitives
- Returns true if both references point to same object
equals() Method:
- Defined in Object class, can be overridden
- Default implementation uses == (reference comparison)
- String and wrapper classes override it for content comparison
// Example with Strings
String s1 = new String("Hello");
String s2 = new String("Hello");
String s3 = "Hello";
String s4 = "Hello";
System.out.println(s1 == s2); // false (different objects)
System.out.println(s1.equals(s2)); // true (same content)
System.out.println(s3 == s4); // true (String pool - same reference)
System.out.println(s3.equals(s4)); // true (same content)
// Example with primitives
int a = 5, b = 5;
System.out.println(a == b); // true (value comparison)
// Example with custom class
class Person {
String name;
Person(String name) { this.name = name; }
@Override
public boolean equals(Object obj) {
if (obj instanceof Person) {
return this.name.equals(((Person) obj).name);
}
return false;
}
}
Person p1 = new Person("John");
Person p2 = new Person("John");
System.out.println(p1 == p2); // false
System.out.println(p1.equals(p2)); // true (overridden)
8. Explain String pool and String immutability.
String Pool (String Intern Pool):
- Special memory area in heap for storing String literals
- When String literal is created, JVM checks pool first
- If exists, returns existing reference; else creates new entry
- Saves memory by reusing identical strings
String s1 = "Hello"; // Creates in pool
String s2 = "Hello"; // Returns same reference from pool
String s3 = new String("Hello"); // Creates new object in heap
System.out.println(s1 == s2); // true (same pool reference)
System.out.println(s1 == s3); // false (different objects)
// intern() moves string to pool
String s4 = s3.intern();
System.out.println(s1 == s4); // true
String Immutability:
- Once created, String content cannot be changed
- Any modification creates a new String object
- String class is final (cannot be extended)
Why Strings are Immutable:
- Security: Strings used in network connections, database URLs, file paths
- String Pool: Multiple references can safely share same String
- Thread Safety: Immutable objects are inherently thread-safe
- Hashcode Caching: Hash can be cached for HashMap keys
String s = "Hello";
s.concat(" World"); // Creates new String, s unchanged
System.out.println(s); // "Hello"
s = s.concat(" World"); // s now points to new String
System.out.println(s); // "Hello World"
9. Compare String, StringBuilder, and StringBuffer.
| Feature | String | StringBuilder | StringBuffer |
|---|---|---|---|
| Mutability | Immutable | Mutable | Mutable |
| Thread Safety | Yes (immutable) | No | Yes (synchronized) |
| Performance | Slow for modifications | Fastest | Slower than StringBuilder |
| Memory | Creates new objects | Modifies in place | Modifies in place |
| Storage | String Pool + Heap | Heap only | Heap only |
| When to Use | Few modifications | Single-threaded, many modifications | Multi-threaded, many modifications |
// String - inefficient for concatenation
String s = "";
for (int i = 0; i < 1000; i++) {
s += i; // Creates 1000 new String objects!
}
// StringBuilder - efficient
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i); // Modifies same object
}
String result = sb.toString();
// StringBuffer - thread-safe
StringBuffer sbuf = new StringBuffer();
sbuf.append("Hello");
sbuf.append(" World");
// Safe to use in multiple threads
10. What is a constructor? Explain types and constructor overloading.
Constructor:
- Special method called automatically when object is created
- Same name as class, no return type (not even void)
- Used to initialize object state
- Cannot be static, final, or abstract
Types of Constructors:
1. Default Constructor:
class Student {
String name;
// Compiler provides: Student() { }
}
Student s = new Student(); // Uses default constructor
2. Parameterized Constructor:
class Student {
String name;
int age;
Student(String name, int age) {
this.name = name;
this.age = age;
}
}
Student s = new Student("John", 20);
3. Copy Constructor (User-defined):
class Student {
String name;
Student(Student other) {
this.name = other.name;
}
}
Student s1 = new Student("John");
Student s2 = new Student(s1); // Copy of s1
Constructor Overloading:
class Rectangle {
int length, width;
Rectangle() { // Default
length = width = 1;
}
Rectangle(int side) { // Square
length = width = side;
}
Rectangle(int l, int w) { // Custom
length = l;
width = w;
}
}
Rectangle r1 = new Rectangle(); // 1x1
Rectangle r2 = new Rectangle(5); // 5x5
Rectangle r3 = new Rectangle(4, 6); // 4x6
11. Explain this and super keywords with examples.
this Keyword:
- Refers to current object instance
- Used to distinguish instance variables from parameters
- Can call current class constructor: this()
- Can pass current object as argument
class Student {
String name;
int age;
Student(String name, int age) {
this.name = name; // this.name = instance variable
this.age = age;
}
Student(String name) {
this(name, 18); // Calls parameterized constructor
}
void display() {
print(this); // Pass current object
}
}
super Keyword:
- Refers to immediate parent class object
- Used to access parent class members (hidden by child)
- Can call parent class constructor: super()
- Must be first statement in constructor
class Animal {
String name = "Animal";
Animal(String name) {
this.name = name;
}
void display() {
System.out.println("Animal display");
}
}
class Dog extends Animal {
String name = "Dog";
Dog(String name) {
super(name); // Call parent constructor
}
void display() {
System.out.println(name); // Dog
System.out.println(super.name); // Animal
super.display(); // Call parent method
}
}
12. Explain static keyword - variables, methods, blocks, and nested classes.
Static members belong to class, not instances:
1. Static Variables:
- One copy shared by all instances
- Memory allocated when class loads
- Also called class variables
class Counter {
static int count = 0; // Shared
int id; // Unique per object
Counter() {
count++;
id = count;
}
}
Counter c1 = new Counter(); // count=1, c1.id=1
Counter c2 = new Counter(); // count=2, c2.id=2
System.out.println(Counter.count); // 2
2. Static Methods:
- Called without creating object
- Cannot access instance variables/methods directly
- Cannot use this or super
class MathUtils {
static int square(int n) {
return n * n;
}
}
int result = MathUtils.square(5); // 25
3. Static Blocks:
- Executed once when class is loaded
- Used for static initialization
class Database {
static Connection conn;
static {
System.out.println("Class loading...");
conn = createConnection();
}
}
4. Static Nested Classes:
class Outer {
static class Nested {
void display() {
System.out.println("Static nested class");
}
}
}
Outer.Nested obj = new Outer.Nested();
13. Explain final keyword with all use cases.
1. Final Variable (Constant):
- Value cannot be changed once assigned
- Must be initialized (at declaration, constructor, or static block)
- Convention: UPPER_CASE names
class Circle {
final double PI = 3.14159; // Initialized at declaration
final int radius;
Circle(int r) {
radius = r; // Initialized in constructor
}
}
final int MAX_VALUE = 100;
MAX_VALUE = 200; // ERROR: cannot reassign
2. Final Method:
- Cannot be overridden by subclasses
- Used for security and design constraints
class Parent {
final void display() {
System.out.println("Cannot override this");
}
}
class Child extends Parent {
// void display() { } // ERROR: cannot override final method
}
3. Final Class:
- Cannot be extended (no inheritance)
- Example: String, Integer, Math classes
final class ImmutableClass {
// Class content
}
// class ExtendedClass extends ImmutableClass { } // ERROR
4. Final Parameters:
void process(final int value) {
// value = 10; // ERROR: cannot modify final parameter
}
Final vs Immutability:
final List<String> list = new ArrayList<>();
list.add("Hello"); // OK - modifying contents
// list = new ArrayList<>(); // ERROR - reassigning reference
14. Explain wrapper classes, autoboxing, and unboxing.
Wrapper Classes:
Classes that wrap primitive types as objects. Located in java.lang package.
| Primitive | Wrapper Class | Example |
|---|---|---|
| byte | Byte | Byte.parseByte("10") |
| short | Short | Short.valueOf(100) |
| int | Integer | Integer.parseInt("123") |
| long | Long | Long.valueOf(1000L) |
| float | Float | Float.parseFloat("3.14") |
| double | Double | Double.valueOf(3.14) |
| char | Character | Character.valueOf('A') |
| boolean | Boolean | Boolean.TRUE |
Why Wrapper Classes:
- Collections only work with objects, not primitives
- Provide utility methods (parsing, conversion)
- Allow null values
- Required for generics
Autoboxing (Primitive → Wrapper):
Integer num = 100; // Autoboxing: int → Integer
List<Integer> list = new ArrayList<>();
list.add(10); // Autoboxing
Unboxing (Wrapper → Primitive):
Integer obj = Integer.valueOf(100);
int num = obj; // Unboxing: Integer → int
int sum = obj + 50; // Automatic unboxing for arithmetic
Common Methods:
// Parsing strings
int i = Integer.parseInt("123");
double d = Double.parseDouble("3.14");
// Converting to strings
String s = Integer.toString(123);
// Comparing
Integer.compare(10, 20); // -1
// Min/Max values
Integer.MAX_VALUE; // 2147483647
Integer.MIN_VALUE; // -2147483648
15. Explain garbage collection and memory management in Java.
Garbage Collection:
- Automatic memory management by JVM
- Reclaims memory from objects no longer in use
- Runs in background as a daemon thread
- Cannot be forced, only requested via System.gc()
When an Object Becomes Eligible for GC:
- Reference set to null:
obj = null; - Reference reassigned:
obj = new Object(); - Object created inside method (after method returns)
- Island of isolation (objects referencing each other, but unreachable)
Memory Areas:
┌─────────────────────────────┐
│ Method Area │ ← Class metadata, static vars
├─────────────────────────────┤
│ Heap │ ← Objects, instance variables
│ ┌────────┬───────────────┐ │
│ │ Young │ Old │ │
│ │ Gen │ Generation │ │
│ └────────┴───────────────┘ │
├─────────────────────────────┤
│ Stack (per thread) │ ← Local variables, method calls
├─────────────────────────────┤
│ PC Registers │ ← Current instruction
├─────────────────────────────┤
│ Native Method Stack │ ← Native method info
└─────────────────────────────┘
finalize() Method:
class Resource {
@Override
protected void finalize() throws Throwable {
System.out.println("Cleanup before GC");
// Release resources
super.finalize();
}
}
Best Practices:
- Nullify references when done with large objects
- Use try-with-resources for closeable resources
- Avoid creating unnecessary objects in loops
- Use StringBuilder for string concatenation
16. Why is multiple inheritance not supported in Java? How to achieve it?
The Diamond Problem:
If Java allowed multiple inheritance with classes:
class A {
void display() { System.out.println("A"); }
}
class B extends A {
void display() { System.out.println("B"); }
}
class C extends A {
void display() { System.out.println("C"); }
}
class D extends B, C { } // NOT ALLOWED!
// Which display() would D inherit? Ambiguity!
Solution: Interfaces
Java supports multiple inheritance through interfaces:
interface Printable {
void print();
}
interface Showable {
void show();
}
class Document implements Printable, Showable {
public void print() {
System.out.println("Printing document");
}
public void show() {
System.out.println("Showing document");
}
}
Default Methods (Java 8+) Conflict:
interface A {
default void display() { System.out.println("A"); }
}
interface B {
default void display() { System.out.println("B"); }
}
class C implements A, B {
// Must override to resolve conflict
public void display() {
A.super.display(); // Choose A's implementation
// or B.super.display();
// or provide own implementation
}
}
17. Explain Java source file structure and naming conventions.
Source File Structure:
// 1. Package declaration (optional, must be first)
package com.example.myapp;
// 2. Import statements (optional)
import java.util.List;
import java.util.ArrayList;
// import java.util.*; // Wildcard import
// 3. Class/Interface declarations
public class MyClass {
// Class body
}
class HelperClass {
// Only one public class per file
}
Naming Conventions:
| Element | Convention | Example |
|---|---|---|
| Package | lowercase, dot-separated | com.company.project |
| Class/Interface | PascalCase (UpperCamelCase) | StudentRecord, Runnable |
| Method | camelCase | getName(), calculateTotal() |
| Variable | camelCase | studentName, totalAmount |
| Constant | UPPER_SNAKE_CASE | MAX_VALUE, PI |
File Naming Rules:
- File name must match public class name
- Extension must be .java
- One public class per source file
- Multiple non-public classes allowed
18. Explain data types in Java with memory allocation.
Primitive Data Types:
| Type | Size | Range | Default |
|---|---|---|---|
| byte | 1 byte | -128 to 127 | 0 |
| short | 2 bytes | -32,768 to 32,767 | 0 |
| int | 4 bytes | -2³¹ to 2³¹-1 | 0 |
| long | 8 bytes | -2⁶³ to 2⁶³-1 | 0L |
| float | 4 bytes | ±3.4e38 | 0.0f |
| double | 8 bytes | ±1.7e308 | 0.0d |
| char | 2 bytes | 0 to 65,535 (Unicode) | '\u0000' |
| boolean | ~1 bit | true / false | false |
Reference Data Types:
- Classes, Interfaces, Arrays
- Store reference (memory address) to actual object
- Default value is null
- Objects stored in Heap memory
Type Conversion:
// Implicit (Widening) - Automatic
int i = 100;
long l = i; // int → long
double d = l; // long → double
// Explicit (Narrowing) - Casting required
double d = 100.99;
int i = (int) d; // 100 (decimal lost)
// byte → short → int → long → float → double
19. Explain types of inheritance in Java.
1. Single Inheritance:
class Animal { }
class Dog extends Animal { }
// One parent, one child
2. Multilevel Inheritance:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
// Chain of inheritance
3. Hierarchical Inheritance:
class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
// Multiple children, one parent
4. Multiple Inheritance (via Interfaces):
interface Flyable { void fly(); }
interface Swimmable { void swim(); }
class Duck implements Flyable, Swimmable {
public void fly() { }
public void swim() { }
}
5. Hybrid Inheritance (via Interfaces):
interface A { }
interface B extends A { }
interface C extends A { }
class D implements B, C { }
// Combination of multiple types
Not Supported: Multiple inheritance with classes (Diamond Problem)
20. What are arrays in Java? Explain with examples.
Array Definition:
- Container holding fixed number of values of same type
- Zero-indexed (first element at index 0)
- Size fixed at creation, cannot be changed
- Stored in contiguous memory locations
Declaration and Initialization:
// Declaration
int[] numbers; // Preferred
int numbers[]; // Also valid
// Instantiation
numbers = new int[5]; // Creates array of 5 integers
// Declaration + Instantiation
int[] arr = new int[5];
// Declaration + Initialization
int[] arr = {1, 2, 3, 4, 5};
int[] arr = new int[]{1, 2, 3, 4, 5};
Multi-dimensional Arrays:
// 2D Array
int[][] matrix = new int[3][4]; // 3 rows, 4 columns
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// Jagged Array (different column sizes)
int[][] jagged = new int[3][];
jagged[0] = new int[2];
jagged[1] = new int[4];
jagged[2] = new int[3];
Common Operations:
int[] arr = {10, 20, 30, 40, 50};
// Access element
int first = arr[0]; // 10
// Length property
int size = arr.length; // 5
// Iterate
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
// Enhanced for loop
for (int num : arr) {
System.out.println(num);
}
// Arrays utility class
Arrays.sort(arr);
Arrays.fill(arr, 0);
int index = Arrays.binarySearch(arr, 30);
16. What is the difference between String, StringBuilder, and StringBuffer?
String: Immutable. Every modification creates a new object. Thread-safe by immutability.
StringBuilder: Mutable. More efficient for concatenation. Not thread-safe.
StringBuffer: Mutable and synchronized. Thread-safe but slower than StringBuilder.
Use case: String for fixed values, StringBuilder for single-threaded string manipulation, StringBuffer for multi-threaded scenarios.
17. What is method overloading and what are its rules?
Method overloading: Same method name, different parameters in same class. Compile-time polymorphism.
Rules:
- Must have different parameter list (number or types)
- Return type can be different but doesn't contribute to overloading
- Access modifiers can be different
- Can throw different exceptions
18. What is method overriding and what are its rules?
Method overriding: Subclass provides specific implementation of parent class method. Runtime polymorphism.
Rules:
- Must have same signature as parent method
- Return type must be same or covariant
- Access modifier cannot be more restrictive
- Cannot override static, final, or private methods
- @Override annotation recommended
19. What is the difference between == and equals() method?
== operator: Compares references (memory addresses) for objects. For primitives, compares values.
equals() method: Compares content/values of objects. Defined in Object class, can be overridden.
Example: String s1 = "Hello"; String s2 = new String("Hello");
s1 == s2 returns false (different objects)
s1.equals(s2) returns true (same content)
20. What is the purpose of the finalize() method?
Called by garbage collector before destroying object. Used for cleanup activities (close files, release resources).
Note: Not guaranteed to be called. Deprecated in Java 9. Use try-with-resources or cleaner API instead.
21. What are access modifiers and their scope?
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
| public | Yes | Yes | Yes | Yes |
| protected | Yes | Yes | Yes | No |
| default | Yes | Yes | No | No |
| private | Yes | No | No | No |
22. What is constructor chaining?
Process of calling one constructor from another constructor using this() or super().
this(): Calls another constructor in same class. Must be first statement.
super(): Calls parent class constructor. Must be first statement.
Use case: Avoid code duplication, initialize object in different ways.
23. What is the difference between break and continue?
break: Exits the loop completely. Control moves to statement after loop.
continue: Skips current iteration. Control moves to next iteration.
Both can be used with labels to break/continue outer loops in nested structures.
24. What is instanceof operator?
Tests whether an object is an instance of a specific class or its subclasses.
Syntax: object instanceof Class
Returns: true if object is instance of Class, false otherwise
Use case: Type checking before casting, avoiding ClassCastException
25. What is the difference between Array and ArrayList?
| Array | ArrayList |
|---|---|
| Fixed size | Dynamic size |
| Can store primitives and objects | Stores only objects (uses wrapper for primitives) |
| Better performance | Slightly slower due to resizing |
| length property | size() method |
| Cannot add/remove elements | Can add/remove elements |
Unit II: Exception Handling, I/O, Multithreading
1. What is an exception? Explain exception hierarchy in Java.
Exception: An event that disrupts the normal flow of program execution. It's an object that represents an error or unexpected condition.
Exception Hierarchy:
java.lang.Object
└── java.lang.Throwable
├── java.lang.Error (Unchecked)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── VirtualMachineError
│
└── java.lang.Exception
├── RuntimeException (Unchecked)
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ ├── ArithmeticException
│ ├── ClassCastException
│ └── IllegalArgumentException
│
└── Checked Exceptions
├── IOException
├── SQLException
├── FileNotFoundException
└── ClassNotFoundException
Error vs Exception:
- Error: Serious problems that application shouldn't try to handle (OutOfMemoryError)
- Exception: Conditions that application might want to catch and handle
2. Differentiate between checked and unchecked exceptions.
| Aspect | Checked Exceptions | Unchecked Exceptions |
|---|---|---|
| Checked at | Compile time | Runtime |
| Handling | Must handle (try-catch) or declare (throws) | Optional to handle |
| Inheritance | Extends Exception (not RuntimeException) | Extends RuntimeException |
| Examples | IOException, SQLException, FileNotFoundException | NullPointerException, ArithmeticException |
| Cause | External resources (file, network, DB) | Programming errors (logic bugs) |
| Recovery | Usually recoverable | Usually indicates bugs, fix the code |
// Checked Exception - must handle
public void readFile() throws IOException { // or try-catch
FileReader fr = new FileReader("file.txt");
}
// Unchecked Exception - optional handling
public void divide(int a, int b) {
int result = a / b; // May throw ArithmeticException
}
3. Explain throw vs throws with examples.
| Aspect | throw | throws |
|---|---|---|
| Purpose | Actually throws an exception | Declares exceptions method might throw |
| Location | Inside method body | In method signature |
| Syntax | throw new Exception() | method() throws Exception |
| Number | Can throw one exception at a time | Can declare multiple exceptions |
| Followed by | Exception instance | Exception class name(s) |
// throw - explicitly throwing exception
public void validateAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
if (age < 18) {
throw new RuntimeException("Must be 18 or older");
}
}
// throws - declaring exceptions
public void readFile(String filename) throws FileNotFoundException, IOException {
FileReader fr = new FileReader(filename);
BufferedReader br = new BufferedReader(fr);
String line = br.readLine();
}
// Combined usage
public void processFile(String path) throws CustomException {
if (path == null) {
throw new CustomException("Path cannot be null");
}
// Process file...
}
4. Explain try-catch-finally with all scenarios.
try: Contains code that might throw exceptions
catch: Handles specific exception types
finally: Always executes (cleanup code)
// Basic structure
try {
// Risky code
int result = 10 / 0;
} catch (ArithmeticException e) {
// Handle arithmetic exception
System.out.println("Cannot divide by zero");
} catch (Exception e) {
// Handle other exceptions (more general - must come last)
System.out.println("Error: " + e.getMessage());
} finally {
// Always executes
System.out.println("Cleanup code");
}
Multi-catch (Java 7+):
try {
// Code
} catch (IOException | SQLException e) {
// Handle both with same code
System.out.println("Error: " + e.getMessage());
}
Finally Execution Scenarios:
- Normal execution: try completes normally → finally runs
- Exception caught: exception handled → finally runs
- Exception not caught: exception propagates → finally runs first
- Return in try: finally runs before return
- System.exit(): finally does NOT run
public int testFinally() {
try {
return 1;
} finally {
System.out.println("Finally runs before return");
// return 2; // Would override try's return - bad practice!
}
}
5. What is try-with-resources? Explain with example.
Automatic resource management introduced in Java 7. Resources are automatically closed after try block.
Requirements:
- Resource must implement AutoCloseable interface
- Resources declared in try parentheses
- Multiple resources separated by semicolons
- Resources closed in reverse order of declaration
// Traditional way (verbose)
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("file.txt"));
String line = br.readLine();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Try-with-resources (clean)
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
}
// br is automatically closed here
// Multiple resources
try (FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt")) {
// Copy file
int data;
while ((data = fis.read()) != -1) {
fos.write(data);
}
} // Both streams auto-closed (fos closed first, then fis)
Java 9 Enhancement:
// Resource can be effectively final variable
BufferedReader br = new BufferedReader(new FileReader("file.txt"));
try (br) {
// Use br
} // br is closed
6. How to create user-defined exceptions?
Steps to create custom exception:
- Extend Exception (checked) or RuntimeException (unchecked)
- Provide constructors (typically matching parent constructors)
- Add custom fields/methods if needed
// Custom Checked Exception
public class InsufficientFundsException extends Exception {
private double amount;
public InsufficientFundsException(String message) {
super(message);
}
public InsufficientFundsException(String message, double amount) {
super(message);
this.amount = amount;
}
public double getAmount() {
return amount;
}
}
// Custom Unchecked Exception
public class InvalidUserException extends RuntimeException {
public InvalidUserException(String message) {
super(message);
}
public InvalidUserException(String message, Throwable cause) {
super(message, cause);
}
}
// Usage
public class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(
"Cannot withdraw " + amount, amount - balance);
}
balance -= amount;
}
}
// Handling
try {
account.withdraw(1000);
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage());
System.out.println("Shortage: " + e.getAmount());
}
7. Explain byte streams vs character streams.
| Aspect | Byte Streams | Character Streams |
|---|---|---|
| Data Unit | 8-bit bytes | 16-bit Unicode characters |
| Base Classes | InputStream, OutputStream | Reader, Writer |
| Use For | Binary data (images, audio, video) | Text data |
| File Classes | FileInputStream, FileOutputStream | FileReader, FileWriter |
| Buffered Classes | BufferedInputStream, BufferedOutputStream | BufferedReader, BufferedWriter |
// Byte Stream Example - Reading binary file
try (FileInputStream fis = new FileInputStream("image.jpg");
FileOutputStream fos = new FileOutputStream("copy.jpg")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
// Character Stream Example - Reading text file
try (BufferedReader br = new BufferedReader(new FileReader("text.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("copy.txt"))) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
}
}
Common Stream Classes:
// Byte Streams
FileInputStream / FileOutputStream // Files
ByteArrayInputStream / ByteArrayOutputStream // Byte arrays
DataInputStream / DataOutputStream // Primitive types
ObjectInputStream / ObjectOutputStream // Objects
// Character Streams
FileReader / FileWriter // Files
StringReader / StringWriter // Strings
BufferedReader / BufferedWriter // Buffered access
PrintWriter // Formatted output
8. What is serialization and deserialization?
Serialization: Converting object state to byte stream for storage or transmission.
Deserialization: Reconstructing object from byte stream.
Requirements:
- Class must implement java.io.Serializable (marker interface)
- All instance variables must be serializable or marked transient
- serialVersionUID recommended for version control
import java.io.*;
class Student implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String password; // Not serialized
public Student(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age +
", password='" + password + "'}";
}
}
// Serialization
Student student = new Student("John", 20, "secret123");
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("student.ser"))) {
oos.writeObject(student);
System.out.println("Object serialized");
}
// Deserialization
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("student.ser"))) {
Student s = (Student) ois.readObject();
System.out.println("Deserialized: " + s);
// password will be null (transient)
}
transient Keyword:
- Fields marked transient are not serialized
- Use for sensitive data (passwords), calculated fields, or non-serializable objects
9. Differentiate between Process and Thread.
| Aspect | Process | Thread |
|---|---|---|
| Definition | Independent program in execution | Lightweight subprocess within a process |
| Memory | Own memory space (heap, stack) | Shares memory with parent process |
| Communication | Inter-process communication (IPC) - complex | Direct via shared memory - easy but risky |
| Overhead | Heavy - more resources | Light - less resources |
| Creation Time | Slower | Faster |
| Context Switch | Expensive | Cheaper |
| Failure Impact | One process crash doesn't affect others | Thread crash can affect entire process |
| Example | Chrome browser windows | Tabs within a browser window |
// Process example
ProcessBuilder pb = new ProcessBuilder("notepad.exe");
Process p = pb.start();
// Thread example
Thread t = new Thread(() -> {
System.out.println("Running in thread");
});
t.start();
10. Explain the complete thread lifecycle with diagram.
Thread States (Thread.State enum):
┌─────────────────┐
│ NEW │
│ (Thread created)│
└────────┬────────┘
│ start()
▼
┌──────────────────────────────────────────┐
│ RUNNABLE │
│ (Ready to run / Running on CPU) │
└──────────────────────────────────────────┘
│ │ │
│ wait() │ sleep(), │ I/O
│ join() │ join(t) │ blocked
▼ ▼ ▼
┌────────────┐ ┌─────────────┐ ┌────────────┐
│ WAITING │ │TIMED_WAITING│ │ BLOCKED │
│ │ │ │ │(waiting for│
│(indefinite)│ │(with timeout│ │ lock) │
└─────┬──────┘ └──────┬──────┘ └─────┬──────┘
│ notify() │ timeout │ lock
│ notifyAll() │ complete │ acquired
└────────────────────┴──────────────┘
│
▼
┌─────────────────┐
│ TERMINATED │
│ (run() complete)│
└─────────────────┘
State Descriptions:
- NEW: Thread created but start() not called
- RUNNABLE: Thread executing or ready to execute
- BLOCKED: Waiting to acquire a monitor lock
- WAITING: Waiting indefinitely for another thread (wait(), join())
- TIMED_WAITING: Waiting for specified time (sleep(), wait(timeout))
- TERMINATED: Thread execution completed
Thread t = new Thread(() -> {
System.out.println("Running");
});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE
t.join();
System.out.println(t.getState()); // TERMINATED
11. How to create threads in Java? Compare the approaches.
Method 1: Extending Thread class
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + getName());
}
}
MyThread t = new MyThread();
t.start();
Method 2: Implementing Runnable interface
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable running");
}
}
Thread t = new Thread(new MyRunnable());
t.start();
Method 3: Lambda expression (Java 8+)
Thread t = new Thread(() -> {
System.out.println("Lambda thread");
});
t.start();
Method 4: Implementing Callable (returns value)
Callable<Integer> task = () -> {
return 42;
};
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);
Integer result = future.get(); // 42
executor.shutdown();
Comparison:
| Aspect | extends Thread | implements Runnable |
|---|---|---|
| Inheritance | Cannot extend another class | Can extend another class |
| Reusability | Less reusable | More reusable |
| Thread Pools | Cannot use directly | Works with ExecutorService |
| Overhead | Each object is a thread | Separates task from thread |
Recommendation: Use Runnable/Callable with ExecutorService for better resource management.
12. Explain synchronization in Java.
Purpose: Control access to shared resources in multithreaded environment to prevent race conditions.
1. Synchronized Method:
class Counter {
private int count = 0;
public synchronized void increment() {
count++; // Only one thread at a time
}
public synchronized int getCount() {
return count;
}
}
2. Synchronized Block:
class Counter {
private int count = 0;
private Object lock = new Object();
public void increment() {
synchronized (lock) { // Only sync critical section
count++;
}
}
public void decrement() {
synchronized (this) { // Using 'this' as lock
count--;
}
}
}
3. Static Synchronization:
class Counter {
private static int count = 0;
public static synchronized void increment() {
// Lock on Counter.class
count++;
}
}
Key Concepts:
- Monitor/Lock: Every object has an intrinsic lock
- Mutex: Only one thread can hold the lock at a time
- Reentrant: Same thread can acquire same lock multiple times
Race Condition Example:
// Without synchronization - WRONG
count++; // Read-modify-write is not atomic!
// Thread 1: read count (0)
// Thread 2: read count (0)
// Thread 1: write count (1)
// Thread 2: write count (1) // Lost update!
13. Explain wait(), notify(), and notifyAll() methods.
Inter-thread Communication Methods (Object class):
- wait(): Releases lock and waits until notified
- notify(): Wakes up one waiting thread
- notifyAll(): Wakes up all waiting threads
Rules:
- Must be called from synchronized context
- Must own the object's monitor (lock)
- IllegalMonitorStateException if rules violated
class SharedResource {
private int data;
private boolean hasData = false;
public synchronized void produce(int value) throws InterruptedException {
while (hasData) {
wait(); // Wait until consumed
}
data = value;
System.out.println("Produced: " + value);
hasData = true;
notify(); // Notify consumer
}
public synchronized int consume() throws InterruptedException {
while (!hasData) {
wait(); // Wait until produced
}
hasData = false;
notify(); // Notify producer
System.out.println("Consumed: " + data);
return data;
}
}
| Aspect | wait() | sleep() |
|---|---|---|
| Class | Object | Thread |
| Lock | Releases lock | Holds lock |
| Context | Must be in synchronized block | Can be anywhere |
| Wake up | notify()/notifyAll() or timeout | After time elapsed |
| Purpose | Inter-thread communication | Pause execution |
14. What is deadlock? How to prevent it?
Deadlock: Situation where two or more threads are blocked forever, each waiting for the other's lock.
Conditions for Deadlock:
- Mutual Exclusion: Resource can only be held by one thread
- Hold and Wait: Thread holds resource while waiting for another
- No Preemption: Resources cannot be forcibly taken
- Circular Wait: Circular chain of threads waiting for resources
Deadlock Example:
Object lock1 = new Object();
Object lock2 = new Object();
// Thread 1
new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock1");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock2) { // Waiting for lock2
System.out.println("Thread 1: Holding lock1 and lock2");
}
}
}).start();
// Thread 2
new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock2");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock1) { // Waiting for lock1 - DEADLOCK!
System.out.println("Thread 2: Holding lock2 and lock1");
}
}
}).start();
Prevention Strategies:
- Lock Ordering: Always acquire locks in same order
// Thread 1 and Thread 2 both do: synchronized (lock1) { synchronized (lock2) { // No deadlock } } - Lock Timeout: Use tryLock() with timeout
if (lock.tryLock(1, TimeUnit.SECONDS)) { try { /* work */ } finally { lock.unlock(); } } - Avoid Nested Locks: Minimize synchronized blocks
- Use Higher-level Concurrency: java.util.concurrent utilities
15. What is volatile keyword?
volatile: Ensures variable is always read from and written to main memory, not thread's local cache.
Purpose:
- Guarantees visibility of changes across threads
- Prevents compiler optimizations that cache values
- Establishes happens-before relationship
class VolatileExample {
private volatile boolean running = true;
public void stop() {
running = false; // Visible to all threads immediately
}
public void run() {
while (running) {
// Without volatile, thread might cache 'running'
// and never see the change from stop()
}
}
}
volatile vs synchronized:
| volatile | synchronized |
|---|---|
| Only visibility guarantee | Visibility + atomicity |
| No blocking | Blocking (acquires lock) |
| For single variables | For any code block |
| Faster | Slower |
When to use volatile:
- Simple flags (stop signals)
- Variables written by one thread, read by many
- When atomicity not required (single read/write)
When NOT to use volatile:
volatile int count = 0;
count++; // NOT ATOMIC! Use AtomicInteger or synchronized
16. What is BufferedReader and BufferedWriter?
Purpose: Provide buffering for efficient reading/writing by reducing I/O operations.
How Buffering Works:
- Data is read/written in chunks to buffer
- Reduces disk/network access
- Default buffer size: 8192 characters
// BufferedReader - efficient reading
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = br.readLine()) != null) { // Read line by line
System.out.println(line);
}
}
// BufferedWriter - efficient writing
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
bw.write("Hello World");
bw.newLine(); // Platform-independent newline
bw.write("Second line");
bw.flush(); // Ensure all data is written
}
// Reading from console
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Enter name: ");
String name = br.readLine();
Key Methods:
| BufferedReader | BufferedWriter |
|---|---|
| read() - single character | write(String) - write string |
| readLine() - entire line | newLine() - write line separator |
| skip(n) - skip characters | flush() - flush buffer |
| ready() - check if ready | close() - close stream |
17. Explain File class and file operations.
File class: Represents file or directory path. Used for file/directory manipulation (not reading/writing content).
import java.io.File;
// Creating File object
File file = new File("test.txt");
File dir = new File("C:\\Users\\data");
File file2 = new File(dir, "document.txt");
// File Information
System.out.println("Name: " + file.getName());
System.out.println("Path: " + file.getPath());
System.out.println("Absolute: " + file.getAbsolutePath());
System.out.println("Parent: " + file.getParent());
System.out.println("Exists: " + file.exists());
System.out.println("Is File: " + file.isFile());
System.out.println("Is Directory: " + file.isDirectory());
System.out.println("Can Read: " + file.canRead());
System.out.println("Can Write: " + file.canWrite());
System.out.println("Size: " + file.length() + " bytes");
System.out.println("Last Modified: " + new Date(file.lastModified()));
// File Operations
file.createNewFile(); // Create file
file.delete(); // Delete file
file.renameTo(new File("new.txt")); // Rename
// Directory Operations
dir.mkdir(); // Create single directory
dir.mkdirs(); // Create including parent dirs
String[] files = dir.list(); // List file names
File[] fileList = dir.listFiles(); // List File objects
// List with filter
File[] txtFiles = dir.listFiles((d, name) -> name.endsWith(".txt"));
// Delete directory
for (File f : dir.listFiles()) {
f.delete();
}
dir.delete();
18. What are daemon threads?
Daemon Thread: Background service thread that runs with low priority. JVM terminates when only daemon threads remain.
Characteristics:
- Runs in background
- JVM doesn't wait for daemon threads to complete
- Child of daemon thread is also daemon
- Used for background tasks (garbage collection, etc.)
// Creating daemon thread
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("Daemon running...");
try { Thread.sleep(1000); } catch (Exception e) {}
}
});
daemonThread.setDaemon(true); // Must be set before start()
daemonThread.start();
System.out.println("Is daemon: " + daemonThread.isDaemon());
// Main thread does some work
Thread.sleep(3000);
System.out.println("Main ending - daemon will be terminated");
// JVM exits, daemon thread is killed
User Thread vs Daemon Thread:
| User Thread | Daemon Thread |
|---|---|
| JVM waits for completion | JVM doesn't wait |
| High priority (foreground) | Low priority (background) |
| Created by user | Usually created by JVM |
| Example: main thread | Example: GC thread |
19. What is thread priority?
Thread Priority: Hint to thread scheduler about relative importance. Higher priority threads get more CPU time (not guaranteed).
Priority Constants:
- Thread.MIN_PRIORITY = 1
- Thread.NORM_PRIORITY = 5 (default)
- Thread.MAX_PRIORITY = 10
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Low priority: " + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("High priority: " + i);
}
});
t1.setPriority(Thread.MIN_PRIORITY); // 1
t2.setPriority(Thread.MAX_PRIORITY); // 10
System.out.println("t1 priority: " + t1.getPriority());
System.out.println("t2 priority: " + t2.getPriority());
t1.start();
t2.start();
Important Notes:
- Priority is only a suggestion to OS scheduler
- Actual behavior depends on OS and JVM implementation
- Don't rely on priority for correctness
- Child thread inherits parent's priority
20. Explain join() method with example.
join(): Makes current thread wait for another thread to complete.
Variants:
join()- Wait indefinitelyjoin(long millis)- Wait for millisecondsjoin(long millis, int nanos)- Wait with nanoseconds
Thread t1 = new Thread(() -> {
System.out.println("Thread 1 started");
try { Thread.sleep(2000); } catch (Exception e) {}
System.out.println("Thread 1 finished");
});
Thread t2 = new Thread(() -> {
System.out.println("Thread 2 started");
try { Thread.sleep(1000); } catch (Exception e) {}
System.out.println("Thread 2 finished");
});
t1.start();
t2.start();
t1.join(); // Main waits for t1
System.out.println("Thread 1 has completed");
t2.join(); // Main waits for t2
System.out.println("Thread 2 has completed");
System.out.println("All threads completed");
// Output:
// Thread 1 started
// Thread 2 started
// Thread 2 finished
// Thread 1 finished
// Thread 1 has completed
// Thread 2 has completed
// All threads completed
Use Cases:
- Wait for worker threads to complete
- Ensure sequential execution when needed
- Collect results from multiple threads
Unit III: Modern Java Features
1. What is a functional interface? Explain with examples.
Functional Interface: An interface with exactly one abstract method. Can have multiple default and static methods.
Characteristics:
- Contains exactly one abstract method (SAM - Single Abstract Method)
- Can be annotated with @FunctionalInterface (optional but recommended)
- Can be implemented using lambda expressions
- Can have default and static methods
// Custom Functional Interface
@FunctionalInterface
interface Calculator {
int calculate(int a, int b); // Single abstract method
// Default method allowed
default void print(int result) {
System.out.println("Result: " + result);
}
// Static method allowed
static int square(int n) {
return n * n;
}
}
// Implementation using Lambda
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
System.out.println(add.calculate(10, 5)); // 15
System.out.println(multiply.calculate(10, 5)); // 50
add.print(15); // Result: 15
System.out.println(Calculator.square(4)); // 16
Built-in Functional Interfaces (java.util.function):
| Interface | Method | Description |
|---|---|---|
| Predicate<T> | test(T t) | T → boolean |
| Function<T,R> | apply(T t) | T → R |
| Consumer<T> | accept(T t) | T → void |
| Supplier<T> | get() | () → T |
| BiFunction<T,U,R> | apply(T t, U u) | (T, U) → R |
| UnaryOperator<T> | apply(T t) | T → T |
2. What is a lambda expression? Explain syntax and examples.
Lambda Expression: Anonymous function that provides concise way to implement functional interfaces.
Syntax:
(parameters) -> expression
(parameters) -> { statements; }
// Examples:
() -> 42 // No parameters
x -> x * 2 // Single parameter (parentheses optional)
(x, y) -> x + y // Multiple parameters
(int x, int y) -> x + y // Explicit types
(x, y) -> { return x + y; } // Block with return
Practical Examples:
// Before Lambda
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// With Lambda
Runnable r2 = () -> System.out.println("Hello");
// Comparator
Comparator<String> comp1 = new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
};
Comparator<String> comp2 = (s1, s2) -> s1.length() - s2.length();
// Using with Collections
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));
names.sort((a, b) -> a.compareTo(b));
names.removeIf(name -> name.length() < 4);
Variable Capture:
int factor = 2; // Effectively final
Function<Integer, Integer> multiplier = n -> n * factor;
// factor = 3; // ERROR: must be effectively final
3. Explain method references with all types.
Method Reference: Shorthand notation for lambda expressions that call existing methods. Uses :: operator.
Four Types:
1. Reference to Static Method
// ClassName::staticMethodName
Function<String, Integer> parser = Integer::parseInt;
int num = parser.apply("123"); // 123
// Equivalent lambda
Function<String, Integer> parser2 = s -> Integer.parseInt(s);
2. Reference to Instance Method of Particular Object
// object::instanceMethodName
String str = "Hello World";
Supplier<Integer> lengthSupplier = str::length;
System.out.println(lengthSupplier.get()); // 11
// Equivalent lambda
Supplier<Integer> lengthSupplier2 = () -> str.length();
3. Reference to Instance Method of Arbitrary Object
// ClassName::instanceMethodName
Function<String, String> upper = String::toUpperCase;
System.out.println(upper.apply("hello")); // HELLO
// Used with streams
List<String> names = Arrays.asList("alice", "bob");
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
4. Reference to Constructor
// ClassName::new
Supplier<ArrayList<String>> listSupplier = ArrayList::new;
ArrayList<String> list = listSupplier.get();
Function<String, StringBuilder> sbCreator = StringBuilder::new;
StringBuilder sb = sbCreator.apply("Hello");
// Array constructor reference
Function<Integer, String[]> arrayCreator = String[]::new;
String[] arr = arrayCreator.apply(5); // Creates String[5]
4. Explain Stream API with operations.
Stream: Sequence of elements supporting sequential and parallel aggregate operations. Does not store data, processes on-demand.
Creating Streams:
// From Collection
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream1 = list.stream();
// From Array
String[] arr = {"a", "b", "c"};
Stream<String> stream2 = Arrays.stream(arr);
// Using Stream.of()
Stream<String> stream3 = Stream.of("a", "b", "c");
// Infinite Stream
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1);
Stream<Double> randomStream = Stream.generate(Math::random);
Intermediate Operations (Lazy):
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// filter - select elements matching predicate
numbers.stream().filter(n -> n % 2 == 0); // Even numbers
// map - transform elements
numbers.stream().map(n -> n * 2); // Double each
// flatMap - flatten nested structures
List<List<Integer>> nested = Arrays.asList(
Arrays.asList(1, 2), Arrays.asList(3, 4));
nested.stream().flatMap(List::stream); // [1, 2, 3, 4]
// distinct - remove duplicates
// sorted - sort elements
// peek - perform action without modifying
// limit - limit to first n elements
// skip - skip first n elements
Terminal Operations:
// forEach - perform action on each element
numbers.stream().forEach(System.out::println);
// collect - collect to collection
List<Integer> result = numbers.stream()
.filter(n -> n > 5)
.collect(Collectors.toList());
// reduce - reduce to single value
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
// count, min, max, findFirst, findAny
long count = numbers.stream().count();
Optional<Integer> max = numbers.stream().max(Integer::compare);
// anyMatch, allMatch, noneMatch
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);
Complete Example:
List<String> names = Arrays.asList("John", "Jane", "Adam", "Eve", "Bob");
// Names starting with 'J', uppercase, sorted
List<String> result = names.stream()
.filter(name -> name.startsWith("J"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
// ["JANE", "JOHN"]
5. Explain default and static methods in interfaces.
Default Methods (Java 8+):
- Methods with implementation in interfaces
- Use
defaultkeyword - Allow adding methods without breaking implementations
- Can be overridden by implementing classes
interface Vehicle {
void start(); // Abstract method
default void honk() { // Default method
System.out.println("Honking!");
}
default void stop() {
System.out.println("Stopping...");
}
}
class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car starting");
}
// Can use default honk() as-is
// Can override stop() if needed
@Override
public void stop() {
System.out.println("Car stopping with brake");
}
}
Static Methods in Interfaces:
interface MathOperations {
static int add(int a, int b) {
return a + b;
}
static int multiply(int a, int b) {
return a * b;
}
}
// Called using interface name
int sum = MathOperations.add(5, 3); // 8
Diamond Problem Resolution:
interface A {
default void show() { System.out.println("A"); }
}
interface B {
default void show() { System.out.println("B"); }
}
class C implements A, B {
// Must override to resolve conflict
@Override
public void show() {
A.super.show(); // Call A's version
// or B.super.show();
// or provide own implementation
}
}
6. What is Optional class? How to use it?
Optional: Container object that may or may not contain a non-null value. Helps avoid NullPointerException.
Creating Optional:
// Empty Optional
Optional<String> empty = Optional.empty();
// Of non-null value (throws NPE if null)
Optional<String> opt1 = Optional.of("Hello");
// Of nullable value (allows null)
Optional<String> opt2 = Optional.ofNullable(mayBeNull);
Checking and Getting Value:
Optional<String> opt = Optional.of("Hello");
// Check if value present
if (opt.isPresent()) {
System.out.println(opt.get());
}
// ifPresent with Consumer
opt.ifPresent(value -> System.out.println(value));
opt.ifPresent(System.out::println);
// ifPresentOrElse (Java 9+)
opt.ifPresentOrElse(
value -> System.out.println("Value: " + value),
() -> System.out.println("Empty")
);
Getting with Fallback:
Optional<String> opt = Optional.empty();
// orElse - default value
String value1 = opt.orElse("Default");
// orElseGet - supplier (lazy evaluation)
String value2 = opt.orElseGet(() -> computeDefault());
// orElseThrow - throw exception
String value3 = opt.orElseThrow(() -> new RuntimeException("No value"));
// orElseThrow() - Java 10+ throws NoSuchElementException
String value4 = opt.orElseThrow();
Transforming Optional:
Optional<String> opt = Optional.of("hello");
// map - transform value
Optional<Integer> length = opt.map(String::length); // Optional[5]
// flatMap - when transformation returns Optional
Optional<String> upper = opt.flatMap(s -> Optional.of(s.toUpperCase()));
// filter - keep if predicate matches
Optional<String> filtered = opt.filter(s -> s.length() > 3);
// Chaining
String result = Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.orElse("Unknown");
7. Explain built-in functional interfaces with examples.
1. Predicate<T> - Test condition
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isEmpty = String::isEmpty;
System.out.println(isEven.test(4)); // true
System.out.println(isEmpty.test("")); // true
// Combining predicates
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isPositiveEven = isEven.and(isPositive);
Predicate<Integer> isEvenOrPositive = isEven.or(isPositive);
Predicate<Integer> isOdd = isEven.negate();
2. Function<T, R> - Transform
Function<String, Integer> length = String::length;
Function<Integer, Integer> square = n -> n * n;
System.out.println(length.apply("Hello")); // 5
System.out.println(square.apply(4)); // 16
// Composing functions
Function<String, Integer> composed = length.andThen(square);
System.out.println(composed.apply("Hi")); // 4 (length 2, squared)
3. Consumer<T> - Consume without return
Consumer<String> printer = System.out::println;
Consumer<List<String>> addItem = list -> list.add("new");
printer.accept("Hello"); // Prints: Hello
// Chaining
Consumer<String> print = s -> System.out.print(s);
Consumer<String> printLn = s -> System.out.println();
Consumer<String> printWithNewLine = print.andThen(printLn);
4. Supplier<T> - Supply value
Supplier<Double> random = Math::random;
Supplier<LocalDate> today = LocalDate::now;
Supplier<List<String>> listFactory = ArrayList::new;
System.out.println(random.get());
System.out.println(today.get());
List<String> list = listFactory.get();
5. BiFunction<T, U, R> - Two inputs
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
BiFunction<String, String, String> concat = String::concat;
System.out.println(add.apply(5, 3)); // 8
System.out.println(concat.apply("Hi", "!")); // Hi!
6. UnaryOperator<T> - Same type in/out
UnaryOperator<Integer> increment = n -> n + 1;
UnaryOperator<String> toUpper = String::toUpperCase;
System.out.println(increment.apply(5)); // 6
System.out.println(toUpper.apply("hello")); // HELLO
8. What is the difference between intermediate and terminal operations?
| Aspect | Intermediate Operations | Terminal Operations |
|---|---|---|
| Return Type | Returns Stream | Returns non-Stream (void, Optional, Collection, etc.) |
| Execution | Lazy - not executed until terminal called | Eager - triggers pipeline execution |
| Chaining | Can be chained | Ends the pipeline |
| Examples | filter, map, flatMap, sorted, distinct, peek, limit, skip | forEach, collect, reduce, count, min, max, findFirst, anyMatch |
List<String> names = Arrays.asList("John", "Jane", "Bob");
// Intermediate operations - just building pipeline
Stream<String> stream = names.stream()
.filter(n -> n.startsWith("J")) // Intermediate
.map(String::toUpperCase) // Intermediate
.sorted(); // Intermediate
// Nothing executed yet!
// Terminal operation - triggers execution
List<String> result = stream.collect(Collectors.toList());
// Now filter, map, sorted all execute
Short-circuiting Operations:
// Intermediate (limit, skip)
Stream.iterate(1, n -> n + 1)
.limit(5) // Short-circuit - don't process infinite stream
.forEach(System.out::println); // 1, 2, 3, 4, 5
// Terminal (findFirst, findAny, anyMatch, allMatch, noneMatch)
boolean hasJohn = names.stream()
.anyMatch(n -> n.equals("John")); // Stops after finding match
9. Explain Collectors in Stream API.
Collectors: Utility class providing reduction operations to collect stream elements.
List<String> names = Arrays.asList("John", "Jane", "Bob", "Jane");
// toList, toSet
List<String> list = names.stream().collect(Collectors.toList());
Set<String> set = names.stream().collect(Collectors.toSet());
// toCollection (specific collection)
TreeSet<String> treeSet = names.stream()
.collect(Collectors.toCollection(TreeSet::new));
// joining
String joined = names.stream()
.collect(Collectors.joining(", ")); // "John, Jane, Bob, Jane"
String joinedWith = names.stream()
.collect(Collectors.joining(", ", "[", "]")); // "[John, Jane, Bob, Jane]"
// counting
long count = names.stream().collect(Collectors.counting()); // 4
// summarizing (for numeric streams)
IntSummaryStatistics stats = names.stream()
.collect(Collectors.summarizingInt(String::length));
// count, sum, min, average, max
// groupingBy
Map<Integer, List<String>> byLength = names.stream()
.collect(Collectors.groupingBy(String::length));
// {3=[Bob], 4=[John, Jane, Jane]}
// partitioningBy (boolean grouping)
Map<Boolean, List<String>> partitioned = names.stream()
.collect(Collectors.partitioningBy(n -> n.length() > 3));
// {false=[Bob], true=[John, Jane, Jane]}
// toMap
Map<String, Integer> nameToLength = names.stream()
.distinct()
.collect(Collectors.toMap(
name -> name,
String::length
));
10. What are Records in Java?
Record (Java 16+): Compact syntax for immutable data classes. Automatically generates constructor, getters, equals(), hashCode(), toString().
// Traditional class
public class PersonOld {
private final String name;
private final int age;
public PersonOld(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
// Record - single line!
public record Person(String name, int age) { }
// Usage
Person p = new Person("John", 25);
System.out.println(p.name()); // John (accessor, not getName)
System.out.println(p.age()); // 25
System.out.println(p); // Person[name=John, age=25]
Record Features:
// Custom constructor (compact)
public record Person(String name, int age) {
public Person { // Compact constructor - no parameters
if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
name = name.toUpperCase(); // Can modify parameters
}
}
// Additional methods
public record Point(int x, int y) {
public double distance() {
return Math.sqrt(x * x + y * y);
}
public static Point origin() {
return new Point(0, 0);
}
}
// Records can implement interfaces
public record Employee(String name, int id) implements Comparable<Employee> {
@Override
public int compareTo(Employee other) {
return this.name.compareTo(other.name);
}
}
Limitations:
- Cannot extend other classes (implicitly extends Record)
- All fields are final (immutable)
- Cannot declare instance fields
- Cannot be abstract
11. Explain var keyword (Local Variable Type Inference).
var (Java 10+): Compiler infers variable type from initialization expression.
// Instead of
ArrayList<String> list1 = new ArrayList<String>();
HashMap<String, List<Integer>> map1 = new HashMap<String, List<Integer>>();
// Use var
var list2 = new ArrayList<String>();
var map2 = new HashMap<String, List<Integer>>();
// Common usages
var name = "John"; // String
var age = 25; // int
var price = 19.99; // double
var stream = list.stream(); // Stream<String>
// In loops
for (var item : list) {
System.out.println(item);
}
for (var i = 0; i < 10; i++) {
System.out.println(i);
}
Where var can be used:
- Local variables with initializers
- For-loop indexes
- Enhanced for-loop variables
- Try-with-resources (Java 11+)
Where var cannot be used:
// Method parameters - NO
// void process(var data) { }
// Return type - NO
// var getData() { return "data"; }
// Fields - NO
// class MyClass { var field = 10; }
// Without initializer - NO
// var x;
// With null - NO
// var nothing = null;
// Lambda expressions - NO
// var fn = (x) -> x * 2; // Cannot infer functional interface
Best Practices:
- Use when type is obvious from right side
- Don't sacrifice readability
- Good for complex generic types
12. What is enhanced switch expression?
Switch Expression (Java 14+): Enhanced switch that can return values and uses arrow syntax.
// Traditional switch (statement)
String dayType;
switch (day) {
case "MONDAY":
case "TUESDAY":
case "WEDNESDAY":
case "THURSDAY":
case "FRIDAY":
dayType = "Weekday";
break;
case "SATURDAY":
case "SUNDAY":
dayType = "Weekend";
break;
default:
dayType = "Unknown";
}
// Switch expression with arrow syntax
String dayType = switch (day) {
case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" -> "Weekday";
case "SATURDAY", "SUNDAY" -> "Weekend";
default -> "Unknown";
};
// With blocks and yield
String result = switch (status) {
case "ACTIVE" -> {
log("Processing active user");
yield "User is active";
}
case "INACTIVE" -> {
log("Processing inactive user");
yield "User is inactive";
}
default -> throw new IllegalArgumentException("Unknown status");
};
Features:
- No fall-through (no break needed)
- Multiple case labels separated by comma
- Can return values (use yield in blocks)
- Must be exhaustive when returning value
Pattern Matching (Java 17+ Preview):
Object obj = "Hello";
String result = switch (obj) {
case String s -> "String of length " + s.length();
case Integer i -> "Integer: " + i;
case null -> "Null value";
default -> "Unknown type";
};
13. What are Text Blocks?
Text Blocks (Java 15+): Multi-line string literals using triple quotes.
// Traditional
String html = "<html>\n" +
" <body>\n" +
" <p>Hello World</p>\n" +
" </body>\n" +
"</html>";
// Text Block
String html = """
<html>
<body>
<p>Hello World</p>
</body>
</html>
""";
// JSON
String json = """
{
"name": "John",
"age": 30,
"city": "New York"
}
""";
// SQL
String sql = """
SELECT id, name, email
FROM users
WHERE status = 'ACTIVE'
ORDER BY name
""";
Features:
- Preserves line breaks
- No need to escape quotes
- Automatic indentation management
- Trailing whitespace stripped by default
Escape Sequences:
// \ at end of line - no newline
String singleLine = """
This is \
one line\
"""; // "This is one line"
// \s - preserve trailing space
String withSpaces = """
Line with trailing space\s
Next line
""";
14. What is pattern matching for instanceof?
Pattern Matching (Java 16+): Combines instanceof check with variable declaration and casting.
// Traditional approach
Object obj = "Hello";
if (obj instanceof String) {
String s = (String) obj; // Explicit cast needed
System.out.println(s.length());
}
// With pattern matching
if (obj instanceof String s) {
// s is automatically cast to String
System.out.println(s.length());
}
// In complex conditions
if (obj instanceof String s && s.length() > 5) {
System.out.println("Long string: " + s);
}
// Negation
if (!(obj instanceof String s)) {
return;
}
// s is in scope here
System.out.println(s.toUpperCase());
Scope Rules:
// Variable scope limited to where pattern definitely matches
if (obj instanceof String s || obj instanceof Integer i) {
// s and i NOT in scope - either could match
}
if (obj instanceof String s && s.length() > 0) {
// s is in scope
}
// In ternary
String result = obj instanceof String s ? s.toUpperCase() : "not string";
15. What are Sealed Classes?
Sealed Classes (Java 17+): Classes that restrict which classes can extend them.
// Sealed class with permits clause
public sealed class Shape permits Circle, Rectangle, Square {
// Common shape properties
}
// Permitted subclass - must be final, sealed, or non-sealed
public final class Circle extends Shape {
private double radius;
}
public final class Rectangle extends Shape {
private double width, height;
}
public sealed class Square extends Shape permits ColoredSquare {
private double side;
}
// Further subclass of sealed class
public final class ColoredSquare extends Square {
private String color;
}
// non-sealed - opens up hierarchy again
public non-sealed class Triangle extends Shape {
// Any class can extend Triangle
}
Benefits:
- Controlled inheritance hierarchy
- Exhaustive pattern matching in switch
- Better maintainability and security
// Exhaustive switch (no default needed)
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Square s -> s.side() * s.side();
// Compiler knows all possibilities
};
Unit IV: Collections Framework
1. What is the Collections Framework? Explain its hierarchy.
Collections Framework: A unified architecture for representing and manipulating collections of objects.
Components:
- Interfaces: Abstract data types (Collection, List, Set, Map)
- Implementations: Concrete classes (ArrayList, HashSet, HashMap)
- Algorithms: Static methods for operations (sort, search)
Iterable
│
Collection
┌─────────────┼─────────────┐
│ │ │
List Set Queue
│ │ │
┌─────┼─────┐ ┌────┼────┐ ┌────┼────┐
ArrayList LinkedList HashSet TreeSet PriorityQueue
Vector LinkedHashSet Deque
│ │
SortedSet ArrayDeque
LinkedList
Map (separate hierarchy)
┌─────────────┼─────────────┐
HashMap TreeMap LinkedHashMap
Hashtable SortedMap
ConcurrentHashMap
Key Interfaces:
- Collection: Root interface, basic operations (add, remove, size)
- List: Ordered, allows duplicates, index-based access
- Set: No duplicates, unordered (except TreeSet)
- Queue: FIFO ordering, special insertion/removal
- Map: Key-value pairs, unique keys
2. Compare ArrayList and LinkedList in detail.
| Aspect | ArrayList | LinkedList |
|---|---|---|
| Internal Structure | Dynamic array | Doubly linked list |
| Random Access | O(1) - excellent | O(n) - poor |
| Insert at beginning | O(n) - shifts elements | O(1) - just update links |
| Insert at end | O(1) amortized | O(1) |
| Insert in middle | O(n) - shifts elements | O(1) if position known, O(n) to find |
| Delete | O(n) - shifts elements | O(1) if position known |
| Memory | Contiguous, less overhead | Non-contiguous, more overhead (prev/next pointers) |
| Cache Locality | Good (contiguous memory) | Poor (scattered memory) |
| Implements | List, RandomAccess | List, Deque, Queue |
// ArrayList - good for random access and iteration
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("A");
System.out.println(arrayList.get(0)); // O(1)
// LinkedList - good for frequent insertions/deletions
LinkedList<String> linkedList = new LinkedList<>();
linkedList.addFirst("A"); // O(1)
linkedList.addLast("B"); // O(1)
linkedList.removeFirst(); // O(1)
// LinkedList as Deque/Queue
linkedList.offer("item"); // Add to end
linkedList.poll(); // Remove from front
linkedList.push("item"); // Stack: add to front
linkedList.pop(); // Stack: remove from front
When to Use:
- ArrayList: Most common choice, frequent reads, rare insertions
- LinkedList: Frequent add/remove at ends, implementing queue/stack
3. Compare HashSet, LinkedHashSet, and TreeSet.
| Aspect | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| Internal Structure | Hash table | Hash table + Linked list | Red-Black tree |
| Ordering | No order | Insertion order | Sorted order |
| Performance (add/remove/contains) | O(1) | O(1) | O(log n) |
| Null Elements | One null allowed | One null allowed | No null (comparison fails) |
| Memory | Less | More (linked list overhead) | More (tree structure) |
| When to Use | Fast lookup, order not needed | Fast lookup + insertion order | Sorted data needed |
// HashSet - no order
Set<String> hashSet = new HashSet<>();
hashSet.addAll(Arrays.asList("C", "A", "B"));
System.out.println(hashSet); // [A, B, C] or any order
// LinkedHashSet - insertion order
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.addAll(Arrays.asList("C", "A", "B"));
System.out.println(linkedHashSet); // [C, A, B]
// TreeSet - sorted order
Set<String> treeSet = new TreeSet<>();
treeSet.addAll(Arrays.asList("C", "A", "B"));
System.out.println(treeSet); // [A, B, C] - alphabetically sorted
// TreeSet with custom Comparator
TreeSet<String> descending = new TreeSet<>(Comparator.reverseOrder());
descending.addAll(Arrays.asList("C", "A", "B"));
System.out.println(descending); // [C, B, A]
// TreeSet navigation methods
TreeSet<Integer> nums = new TreeSet<>(Arrays.asList(10, 20, 30, 40, 50));
System.out.println(nums.ceiling(25)); // 30 (smallest >= 25)
System.out.println(nums.floor(25)); // 20 (largest <= 25)
System.out.println(nums.first()); // 10
System.out.println(nums.last()); // 50
4. Compare HashMap, LinkedHashMap, and TreeMap.
| Aspect | HashMap | LinkedHashMap | TreeMap |
|---|---|---|---|
| Internal Structure | Hash table | Hash table + Doubly linked list | Red-Black tree |
| Ordering | No order | Insertion order (or access order) | Sorted by keys |
| Performance | O(1) | O(1) | O(log n) |
| Null Keys | One null key allowed | One null key allowed | No null keys |
| Null Values | Multiple allowed | Multiple allowed | Multiple allowed |
| Thread-safe | No | No | No |
// HashMap
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("B", 2);
hashMap.put("A", 1);
hashMap.put("C", 3);
System.out.println(hashMap); // Order not guaranteed
// LinkedHashMap - maintains insertion order
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("B", 2);
linkedHashMap.put("A", 1);
linkedHashMap.put("C", 3);
System.out.println(linkedHashMap); // {B=2, A=1, C=3}
// TreeMap - sorted by keys
Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("B", 2);
treeMap.put("A", 1);
treeMap.put("C", 3);
System.out.println(treeMap); // {A=1, B=2, C=3}
// LRU Cache using LinkedHashMap
LinkedHashMap<String, Integer> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 3; // Keep max 3 entries
}
};
5. Explain Comparable vs Comparator with examples.
| Aspect | Comparable | Comparator |
|---|---|---|
| Package | java.lang | java.util |
| Method | compareTo(Object o) | compare(Object o1, Object o2) |
| Implementation | Class itself implements | Separate class or lambda |
| Sort Sequences | Single natural ordering | Multiple custom orderings |
| Modification | Requires modifying original class | No modification needed |
| Usage | Collections.sort(list) | Collections.sort(list, comparator) |
// Comparable - Natural ordering
class Student implements Comparable<Student> {
String name;
int age;
double gpa;
@Override
public int compareTo(Student other) {
return this.name.compareTo(other.name); // Natural: by name
}
}
List<Student> students = new ArrayList<>();
Collections.sort(students); // Uses compareTo
// Comparator - Custom orderings
Comparator<Student> byAge = (s1, s2) -> Integer.compare(s1.age, s2.age);
Comparator<Student> byGpa = (s1, s2) -> Double.compare(s2.gpa, s1.gpa); // Descending
Collections.sort(students, byAge);
students.sort(byGpa);
// Comparator utility methods
Comparator<Student> byName = Comparator.comparing(s -> s.name);
Comparator<Student> byAgeDesc = Comparator.comparingInt((Student s) -> s.age).reversed();
Comparator<Student> byNameThenAge = Comparator
.comparing((Student s) -> s.name)
.thenComparingInt(s -> s.age);
// Null-safe comparator
Comparator<Student> nullSafe = Comparator.nullsFirst(Comparator.comparing(s -> s.name));
6. Explain Iterator and ListIterator.
Iterator: Universal cursor for traversing any collection.
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
// Using Iterator
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("B")) {
iterator.remove(); // Safe removal during iteration
}
}
System.out.println(list); // [A, C, D]
ListIterator: Bidirectional iterator for List implementations.
| Iterator | ListIterator |
|---|---|
| Forward traversal only | Forward and backward |
| hasNext(), next() | hasPrevious(), previous() also |
| remove() only | add(), set() also |
| No index access | nextIndex(), previousIndex() |
| Works with any Collection | Only List implementations |
// Using ListIterator
ListIterator<String> listIterator = list.listIterator();
// Forward
while (listIterator.hasNext()) {
int index = listIterator.nextIndex();
String item = listIterator.next();
System.out.println(index + ": " + item);
}
// Backward
while (listIterator.hasPrevious()) {
String item = listIterator.previous();
if (item.equals("A")) {
listIterator.set("Z"); // Replace current element
}
}
// Add element at current position
listIterator.add("NEW");
Fail-fast vs Fail-safe:
// Fail-fast (ArrayList, HashMap)
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
list.add("D"); // ConcurrentModificationException!
}
// Fail-safe (CopyOnWriteArrayList, ConcurrentHashMap)
List<String> safeList = new CopyOnWriteArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : safeList) {
safeList.add("D"); // Works - iterates over copy
}
7. Explain the internal working of HashMap.
Structure: Array of buckets, each bucket is linked list (or tree for Java 8+).
// Internal structure (simplified)
class HashMap<K, V> {
Node<K,V>[] table; // Array of buckets
int size;
float loadFactor; // Default 0.75
int threshold; // capacity * loadFactor
static class Node<K,V> {
int hash;
K key;
V value;
Node<K,V> next;
}
}
put() Operation:
- Calculate hash:
hash = key.hashCode() ^ (hash >>> 16) - Find bucket index:
index = hash & (capacity - 1) - If bucket empty, create new node
- If key exists (equals check), update value
- Else, add to linked list (end) or tree
- If size > threshold, resize (double capacity)
get() Operation:
- Calculate hash and find bucket
- Search bucket (linked list or tree) using equals()
- Return value if found, null otherwise
// Hash collision handling
// Java 8+: When bucket has > 8 nodes, converts to tree (O(log n))
// When nodes < 6, converts back to linked list
// Rehashing when capacity doubles
// All entries are re-hashed to new buckets
Performance:
- Best case: O(1) - no collisions
- Worst case (before Java 8): O(n) - all in one bucket
- Worst case (Java 8+): O(log n) - tree bucket
8. Explain hashCode() and equals() contract.
The Contract:
- If
a.equals(b) == true, thena.hashCode() == b.hashCode() - If
a.hashCode() != b.hashCode(), thena.equals(b) == false - If
a.hashCode() == b.hashCode(), a.equals(b) may or may not be true (collision) - hashCode() must return same value for same object across invocations
class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
// Problems without proper implementation
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("John", 25);
map.put(p1, "Engineer");
Person p2 = new Person("John", 25); // Same data as p1
String job = map.get(p2); // null if hashCode/equals not overridden!
// "Engineer" if properly overridden
Why Override Both:
- HashMap uses hashCode() to find bucket
- Then uses equals() to find exact match in bucket
- If only equals() overridden: keys with same data go to different buckets
- If only hashCode() overridden: same hash but never equal
9. Explain List, Set, and Map interfaces.
List Interface:
- Ordered collection (maintains insertion order)
- Allows duplicate elements
- Allows null elements
- Index-based access (get(i), set(i, e))
- Implementations: ArrayList, LinkedList, Vector
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("A"); // Duplicate allowed
list.add(1, "C"); // Insert at index
String s = list.get(0);
list.set(0, "Z");
list.remove(0);
Set Interface:
- No duplicate elements (uses equals())
- At most one null element
- No index-based access
- Implementations: HashSet, LinkedHashSet, TreeSet
Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("A"); // Ignored - duplicate
boolean exists = set.contains("A");
set.remove("A");
Map Interface:
- Key-value pairs
- No duplicate keys (duplicate values allowed)
- Not part of Collection interface
- Implementations: HashMap, LinkedHashMap, TreeMap, Hashtable
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("A", 3); // Updates value for key "A"
int value = map.get("A"); // 3
boolean hasKey = map.containsKey("A");
boolean hasValue = map.containsValue(3);
Set<String> keys = map.keySet();
Collection<Integer> values = map.values();
Set<Map.Entry<String, Integer>> entries = map.entrySet();
// Iterate Map
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
map.forEach((k, v) -> System.out.println(k + ": " + v));
10. What is ConcurrentHashMap? How is it different from Hashtable?
| Aspect | Hashtable | ConcurrentHashMap |
|---|---|---|
| Locking | Entire map locked | Segment/node-level locking |
| Concurrency | Single thread at a time | Multiple threads concurrently |
| Performance | Poor under contention | Better scalability |
| Null Keys/Values | Not allowed | Not allowed |
| Iteration | Fail-fast | Fail-safe (weakly consistent) |
| Legacy | Yes (Java 1.0) | No (Java 5) |
// ConcurrentHashMap usage
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Atomic operations
map.putIfAbsent("key", 1);
map.computeIfAbsent("key", k -> expensiveComputation());
map.computeIfPresent("key", (k, v) -> v + 1);
map.merge("key", 1, Integer::sum);
// Safe iteration
for (Map.Entry<String, Integer> entry : map.entrySet()) {
// No ConcurrentModificationException
map.put("newKey", 100); // Safe
}
// Parallel operations
map.forEach(4, (k, v) -> System.out.println(k + ": " + v));
long count = map.reduceValues(4, v -> 1L, Long::sum);
11. Explain Queue and Deque interfaces.
Queue Interface: FIFO (First-In-First-Out) collection.
Queue<String> queue = new LinkedList<>();
// Throws exception on failure
queue.add("A"); // Add to tail
queue.remove(); // Remove from head
queue.element(); // Peek head
// Returns null/false on failure
queue.offer("A"); // Add to tail
queue.poll(); // Remove from head
queue.peek(); // Peek head
// PriorityQueue - elements sorted by priority
Queue<Integer> pq = new PriorityQueue<>(); // Min-heap
pq.addAll(Arrays.asList(3, 1, 4, 1, 5));
while (!pq.isEmpty()) {
System.out.println(pq.poll()); // 1, 1, 3, 4, 5
}
// Max-heap
Queue<Integer> maxPq = new PriorityQueue<>(Comparator.reverseOrder());
Deque Interface: Double-ended queue - insert/remove from both ends.
Deque<String> deque = new ArrayDeque<>();
// Add operations
deque.addFirst("A"); // Add to front
deque.addLast("B"); // Add to back
deque.offerFirst("C");
deque.offerLast("D");
// Remove operations
deque.removeFirst(); // Remove from front
deque.removeLast(); // Remove from back
deque.pollFirst();
deque.pollLast();
// Peek operations
deque.getFirst();
deque.getLast();
deque.peekFirst();
deque.peekLast();
// As Stack (LIFO)
deque.push("item"); // addFirst
deque.pop(); // removeFirst
deque.peek(); // peekFirst
// As Queue (FIFO)
deque.offer("item"); // addLast
deque.poll(); // removeFirst
12. How to make collections thread-safe?
1. Using Collections.synchronizedXXX():
// Synchronized collections
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// Must synchronize iteration manually
synchronized (syncList) {
for (String item : syncList) {
System.out.println(item);
}
}
2. Using Concurrent Collections:
// Better performance, no external synchronization needed
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
CopyOnWriteArraySet<String> cowSet = new CopyOnWriteArraySet<>();
ConcurrentLinkedQueue<String> concurrentQueue = new ConcurrentLinkedQueue<>();
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(100);
3. Using Immutable Collections:
// Immutable = inherently thread-safe
List<String> immutableList = List.of("A", "B", "C");
Set<String> immutableSet = Set.of("A", "B", "C");
Map<String, Integer> immutableMap = Map.of("A", 1, "B", 2);
// From existing collection
List<String> unmodifiable = Collections.unmodifiableList(existingList);
| Approach | When to Use |
|---|---|
| synchronizedXXX() | Simple thread-safety, moderate contention |
| ConcurrentHashMap | High concurrency reads/writes |
| CopyOnWriteArrayList | Many reads, few writes |
| Immutable collections | Read-only data, no modification needed |
13. What are the differences between HashMap and Hashtable?
| Aspect | HashMap | Hashtable |
|---|---|---|
| Thread Safety | Not synchronized | Synchronized (thread-safe) |
| Null Keys | One null key allowed | Not allowed (NullPointerException) |
| Null Values | Multiple nulls allowed | Not allowed |
| Performance | Faster (no sync overhead) | Slower |
| Inheritance | Extends AbstractMap | Extends Dictionary (legacy) |
| Iterator | Fail-fast | Fail-fast (Enumerator is fail-safe) |
| Introduced | Java 1.2 | Java 1.0 (legacy) |
| Recommendation | Preferred in modern code | Use ConcurrentHashMap instead |
// HashMap
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put(null, 1); // OK
hashMap.put("A", null); // OK
// Hashtable
Hashtable<String, Integer> hashtable = new Hashtable<>();
// hashtable.put(null, 1); // NullPointerException
// hashtable.put("A", null); // NullPointerException
// For thread-safety, use:
Map<String, Integer> safeMap = new ConcurrentHashMap<>();
// or
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
14. What is Vector? How is it different from ArrayList?
| Aspect | ArrayList | Vector |
|---|---|---|
| Thread Safety | Not synchronized | Synchronized |
| Performance | Faster | Slower (sync overhead) |
| Growth | 50% increase (n + n/2) | 100% increase (2n) |
| Iteration | Iterator (fail-fast) | Enumeration + Iterator |
| Legacy | Part of Collections Framework | Legacy class (pre-Collections) |
| Introduced | Java 1.2 | Java 1.0 |
// Vector usage (legacy)
Vector<String> vector = new Vector<>();
vector.addElement("A"); // Legacy method
vector.add("B"); // Collections method
// Enumeration (legacy iteration)
Enumeration<String> e = vector.elements();
while (e.hasMoreElements()) {
System.out.println(e.nextElement());
}
// Modern alternative for thread-safe list
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
// or
List<String> cowList = new CopyOnWriteArrayList<>();
Recommendation: Avoid Vector and Hashtable. Use ArrayList/HashMap with explicit synchronization or concurrent collections.
15. Explain Collections utility class methods.
Collections: Utility class with static methods for collection operations.
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9));
// Sorting
Collections.sort(list); // Natural order
Collections.sort(list, Collections.reverseOrder()); // Reverse
Collections.sort(list, Comparator.reverseOrder()); // Same
// Searching (list must be sorted)
int index = Collections.binarySearch(list, 4);
// Shuffling
Collections.shuffle(list);
Collections.shuffle(list, new Random(42)); // With seed
// Reversing
Collections.reverse(list);
// Min/Max
int min = Collections.min(list);
int max = Collections.max(list);
// Fill/Copy
Collections.fill(list, 0); // Fill with 0s
List<Integer> dest = new ArrayList<>(list);
Collections.copy(dest, list);
// Frequency/disjoint
int freq = Collections.frequency(list, 1); // Count occurrences
boolean disjoint = Collections.disjoint(list1, list2); // No common elements?
// Rotate/Swap
Collections.rotate(list, 2); // Rotate elements
Collections.swap(list, 0, 1); // Swap positions
// Singleton/Empty
List<String> single = Collections.singletonList("only");
List<String> empty = Collections.emptyList();
// Unmodifiable wrappers
List<Integer> unmodifiable = Collections.unmodifiableList(list);
// Synchronized wrappers
List<Integer> syncList = Collections.synchronizedList(list);
// Checked wrapper (type-safe)
List<String> checkedList = Collections.checkedList(new ArrayList<>(), String.class);
Unit V: Spring Framework
1. What is Spring Framework? Explain its key features.
Spring Framework: A comprehensive, modular framework for building enterprise Java applications. It provides infrastructure support and promotes loose coupling.
Key Features:
- IoC/DI: Dependency Injection and Inversion of Control
- AOP: Aspect-Oriented Programming for cross-cutting concerns
- Data Access: Integration with JDBC, ORM (Hibernate, JPA)
- Transaction Management: Declarative transaction support
- MVC: Web application framework
- Security: Spring Security for authentication/authorization
- Testing: Comprehensive testing support
Spring Modules:
┌─────────────────────────────────────────────────────────┐
│ Spring Framework │
├─────────────┬─────────────┬─────────────┬───────────────┤
│ Spring Core │ Spring AOP │ Spring Data │ Spring MVC │
│ (IoC/DI) │ (Aspects) │ (DB Access) │ (Web Layer) │
├─────────────┴─────────────┴─────────────┴───────────────┤
│ Spring Boot (Simplified Config) │
└─────────────────────────────────────────────────────────┘
Advantages:
- Lightweight and non-invasive
- Promotes POJO-based programming
- Modular architecture - use what you need
- Easy testing with dependency injection
- Rich ecosystem and community support
2. Explain Inversion of Control (IoC) and Dependency Injection (DI).
Inversion of Control (IoC): Design principle where the control of object creation and lifecycle is transferred from the application to a container/framework.
Dependency Injection (DI): A pattern implementing IoC where dependencies are "injected" into objects rather than objects creating their own dependencies.
Without DI (Tight Coupling):
class UserService {
private UserRepository repository;
public UserService() {
// Creating dependency - tight coupling
this.repository = new MySQLUserRepository();
}
}
// Problems:
// - Cannot easily switch to PostgreSQL
// - Hard to unit test
// - UserService responsible for creating repository
With DI (Loose Coupling):
class UserService {
private UserRepository repository;
// Dependency injected by container
public UserService(UserRepository repository) {
this.repository = repository;
}
}
// In Spring
@Service
public class UserService {
private final UserRepository repository;
@Autowired // Spring injects the dependency
public UserService(UserRepository repository) {
this.repository = repository;
}
}
// Benefits:
// - Easy to switch implementations
// - Easy to test with mocks
// - Loose coupling
IoC Container: Spring provides two main containers:
- BeanFactory: Basic container, lazy initialization
- ApplicationContext: Advanced container, eager initialization, AOP, i18n
3. What are the types of Dependency Injection in Spring?
1. Constructor Injection (Recommended):
@Service
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
@Autowired // Optional for single constructor
public OrderService(PaymentService paymentService,
InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
}
// Advantages:
// - Dependencies are final (immutable)
// - Required dependencies clearly visible
// - Easy to test, no reflection needed
// - Prevents circular dependencies
2. Setter Injection:
@Service
public class OrderService {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
// Use case:
// - Optional dependencies
// - Re-configurable dependencies
3. Field Injection (Not Recommended):
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
}
// Disadvantages:
// - Cannot make fields final
// - Hard to test without reflection
// - Hides dependencies
// - Allows many dependencies (code smell)
| Type | Recommended | Use Case |
|---|---|---|
| Constructor | Yes | Required dependencies |
| Setter | Sometimes | Optional dependencies |
| Field | No | Avoid - only for tests |
4. What is Aspect-Oriented Programming (AOP)? Explain its concepts.
AOP: Programming paradigm that allows separating cross-cutting concerns (logging, security, transactions) from business logic.
Key Concepts:
- Aspect: Module encapsulating cross-cutting concern (e.g., LoggingAspect)
- Join Point: Point in program execution (method call, exception)
- Pointcut: Expression selecting join points
- Advice: Action taken at join point
- Weaving: Linking aspects with application code
@Aspect
@Component
public class LoggingAspect {
// Pointcut expression
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
// Before advice
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Calling: " + joinPoint.getSignature().getName());
}
// After advice
@After("serviceMethods()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("Completed: " + joinPoint.getSignature().getName());
}
// Around advice
@Around("serviceMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // Execute method
long time = System.currentTimeMillis() - start;
System.out.println("Execution time: " + time + "ms");
return result;
}
// After returning
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logResult(JoinPoint joinPoint, Object result) {
System.out.println("Result: " + result);
}
// After throwing
@AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
System.out.println("Exception: " + ex.getMessage());
}
}
5. What are the types of Advice in AOP?
| Advice Type | Annotation | When Executed |
|---|---|---|
| Before | @Before | Before method execution |
| After | @After | After method (regardless of outcome) |
| After Returning | @AfterReturning | After successful completion |
| After Throwing | @AfterThrowing | After exception is thrown |
| Around | @Around | Wraps method - controls execution |
Common Pointcut Expressions:
// All methods in service package
execution(* com.example.service.*.*(..))
// All public methods
execution(public * *(..))
// Methods starting with "get"
execution(* get*(..))
// Methods with specific parameter
execution(* *..UserService.findById(Long))
// Within specific class
within(com.example.service.UserService)
// Bean with specific name
bean(userService)
// Combining expressions
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* get*(..))")
public void serviceMethodsExceptGetters() {}
6. What are Spring Bean Scopes?
| Scope | Description | When to Use |
|---|---|---|
| singleton (default) | Single instance per container | Stateless services, DAO |
| prototype | New instance for each request | Stateful beans |
| request | One per HTTP request | Request-specific data |
| session | One per HTTP session | User session data |
| application | One per ServletContext | Application-wide data |
| websocket | One per WebSocket session | WebSocket session data |
// Singleton (default)
@Service
public class UserService { } // Same instance everywhere
// Prototype
@Component
@Scope("prototype")
public class ShoppingCart { } // New instance each time
// Request scope
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestInfo { }
// Session scope
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession { }
// Using @RequestScope and @SessionScope shortcuts
@RequestScope
@Component
public class CurrentRequest { }
@SessionScope
@Component
public class UserPreferences { }
7. What is @Autowired? Explain with @Qualifier.
@Autowired: Tells Spring to automatically inject dependencies. Spring finds matching bean by type.
// Basic usage
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
}
// Constructor injection (recommended)
@Service
public class OrderService {
private final PaymentService paymentService;
@Autowired // Optional for single constructor
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
// Optional dependency
@Autowired(required = false)
private NotificationService notificationService;
@Qualifier: Used when multiple beans of same type exist.
// Multiple implementations
interface PaymentService { }
@Service("creditCard")
class CreditCardPayment implements PaymentService { }
@Service("paypal")
class PayPalPayment implements PaymentService { }
// Injecting specific implementation
@Service
public class OrderService {
@Autowired
@Qualifier("creditCard")
private PaymentService paymentService;
}
// With constructor injection
@Service
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(@Qualifier("paypal") PaymentService paymentService) {
this.paymentService = paymentService;
}
}
@Primary: Mark default bean when multiple candidates exist.
@Service
@Primary // This will be injected by default
class CreditCardPayment implements PaymentService { }
8. What is Spring Boot? How is it different from Spring Framework?
Spring Boot: Framework built on top of Spring to simplify application development with auto-configuration and embedded servers.
| Aspect | Spring Framework | Spring Boot |
|---|---|---|
| Configuration | Manual (XML or Java config) | Auto-configuration |
| Server | External (Tomcat, Jetty) | Embedded server |
| Dependencies | Manage individually | Starter dependencies |
| Setup Time | Longer | Minimal |
| Deployment | WAR to external server | JAR with embedded server |
| Properties | Multiple config files | application.properties/yml |
Spring Boot Features:
// Main application - that's it!
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
// application.properties
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.jpa.hibernate.ddl-auto=update
// Starter dependencies (pom.xml)
spring-boot-starter-web // Web application
spring-boot-starter-data-jpa // JPA/Hibernate
spring-boot-starter-security // Security
spring-boot-starter-test // Testing
Spring Boot Auto-configuration:
- Automatically configures beans based on classpath
- DataSource if database driver present
- JPA if spring-data-jpa present
- Security if spring-security present
- Can be overridden with custom configuration
9. Explain @Component, @Service, @Repository, @Controller annotations.
These are stereotype annotations that mark classes as Spring-managed components.
// @Component - Generic stereotype
@Component
public class EmailHelper {
public void sendEmail(String to, String message) { }
}
// @Service - Business logic layer
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
// @Repository - Data access layer
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByEmail(String email);
}
// @Controller - Web layer (returns views)
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("message", "Welcome");
return "home"; // Returns view name
}
}
// @RestController - REST API (returns data)
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // Returns JSON
}
}
| Annotation | Layer | Special Behavior |
|---|---|---|
| @Component | Any | Base annotation |
| @Service | Service | Semantic clarity |
| @Repository | DAO | Exception translation |
| @Controller | Web | MVC controller |
| @RestController | REST | @Controller + @ResponseBody |
10. Explain @RequestMapping and HTTP method annotations.
@RequestMapping: Maps HTTP requests to handler methods.
// Class-level mapping
@RestController
@RequestMapping("/api/users")
public class UserController {
// GET /api/users
@RequestMapping(method = RequestMethod.GET)
public List<User> getAllUsers() { }
// GET /api/users/123
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User getUser(@PathVariable Long id) { }
// POST /api/users
@RequestMapping(method = RequestMethod.POST)
public User createUser(@RequestBody User user) { }
}
Shortcut Annotations:
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping // GET /api/users
public List<User> getAll() { }
@GetMapping("/{id}") // GET /api/users/{id}
public User getById(@PathVariable Long id) { }
@PostMapping // POST /api/users
public User create(@RequestBody User user) { }
@PutMapping("/{id}") // PUT /api/users/{id}
public User update(@PathVariable Long id, @RequestBody User user) { }
@PatchMapping("/{id}") // PATCH /api/users/{id}
public User partialUpdate(@PathVariable Long id, @RequestBody Map<String, Object> updates) { }
@DeleteMapping("/{id}") // DELETE /api/users/{id}
public void delete(@PathVariable Long id) { }
}
Common Attributes:
@GetMapping(
value = "/search",
params = "name", // Required parameter
headers = "X-API-VERSION=1", // Required header
consumes = "application/json", // Request content type
produces = "application/json" // Response content type
)
public List<User> search(@RequestParam String name) { }
11. Explain the Spring Bean Lifecycle.
Bean Lifecycle Phases:
1. Instantiation → Bean object created
2. Populate Properties → Dependencies injected
3. BeanNameAware → setBeanName() called
4. BeanFactoryAware → setBeanFactory() called
5. ApplicationContextAware → setApplicationContext() called
6. Pre-Initialization → BeanPostProcessor.postProcessBeforeInitialization()
7. InitializingBean → afterPropertiesSet() called
8. Custom init-method → @PostConstruct or init-method
9. Post-Initialization → BeanPostProcessor.postProcessAfterInitialization()
10. Bean Ready to Use
11. DisposableBean → destroy() called
12. Custom destroy → @PreDestroy or destroy-method
Lifecycle Callbacks:
@Component
public class MyBean implements InitializingBean, DisposableBean {
// Post-construction (preferred)
@PostConstruct
public void init() {
System.out.println("1. @PostConstruct");
}
// InitializingBean interface
@Override
public void afterPropertiesSet() {
System.out.println("2. afterPropertiesSet");
}
// Pre-destruction (preferred)
@PreDestroy
public void cleanup() {
System.out.println("3. @PreDestroy");
}
// DisposableBean interface
@Override
public void destroy() {
System.out.println("4. destroy");
}
}
// Configuration-based callbacks
@Bean(initMethod = "init", destroyMethod = "cleanup")
public MyService myService() {
return new MyService();
}
Recommended Approach:
- Use @PostConstruct and @PreDestroy (JSR-250)
- Avoid implementing interfaces (couples to Spring)
- Constructor injection for required dependencies
12. What is @Transactional annotation?
@Transactional: Declares transaction boundaries declaratively. Spring handles begin, commit, rollback.
@Service
public class AccountService {
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).get();
Account to = accountRepository.findById(toId).get();
from.debit(amount);
to.credit(amount);
accountRepository.save(from);
accountRepository.save(to);
// If any exception occurs, entire transaction rolls back
}
}
Transaction Attributes:
@Transactional(
propagation = Propagation.REQUIRED, // Default
isolation = Isolation.READ_COMMITTED, // Default
timeout = 30, // Seconds
readOnly = false, // Optimization hint
rollbackFor = Exception.class, // Rollback for
noRollbackFor = CustomException.class // Don't rollback for
)
public void process() { }
Propagation Types:
| Type | Behavior |
|---|---|
| REQUIRED (default) | Join existing or create new |
| REQUIRES_NEW | Always create new, suspend existing |
| SUPPORTS | Join if exists, else non-transactional |
| NOT_SUPPORTED | Execute non-transactionally |
| MANDATORY | Must have existing transaction |
| NEVER | Must not have transaction |
| NESTED | Nested transaction with savepoint |
13. Explain ApplicationContext vs BeanFactory.
| Aspect | BeanFactory | ApplicationContext |
|---|---|---|
| Bean Loading | Lazy (on request) | Eager (at startup) |
| AOP Support | No | Yes |
| i18n | No | Yes (MessageSource) |
| Event Publishing | No | Yes |
| Web Support | No | Yes (WebApplicationContext) |
| Resource Loading | Basic | Rich (ResourceLoader) |
| Memory | Lower | Higher |
| Use Case | Simple apps, memory constrained | Enterprise applications |
// BeanFactory (rarely used directly)
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
MyBean bean = factory.getBean("myBean", MyBean.class);
// ApplicationContext (commonly used)
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// or
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// or in Spring Boot
@SpringBootApplication
public class App {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(App.class, args);
MyBean bean = context.getBean(MyBean.class);
}
}
14. What is @RestController? Explain @PathVariable and @RequestParam.
@RestController: Combines @Controller + @ResponseBody. All methods return data (JSON), not views.
@RestController // Every method returns data, not view name
@RequestMapping("/api")
public class ApiController {
@GetMapping("/hello")
public String hello() {
return "Hello World"; // Returns as JSON string
}
@GetMapping("/user")
public User getUser() {
return new User("John", 25); // Returns as JSON object
}
}
@PathVariable: Extracts values from URL path.
// URL: /api/users/123
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // id = 123
}
// Multiple path variables: /api/users/123/orders/456
@GetMapping("/users/{userId}/orders/{orderId}")
public Order getOrder(
@PathVariable Long userId,
@PathVariable Long orderId) {
return orderService.findByUserAndOrderId(userId, orderId);
}
// With different name
@GetMapping("/users/{user_id}")
public User getUser(@PathVariable("user_id") Long id) {
return userService.findById(id);
}
@RequestParam: Extracts query parameters from URL.
// URL: /api/users?name=John&age=25
@GetMapping("/users")
public List<User> searchUsers(
@RequestParam String name,
@RequestParam Integer age) {
return userService.search(name, age);
}
// Optional parameter with default
@GetMapping("/users")
public List<User> getUsers(
@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return userService.findAll(page, size);
}
// Multiple values: /api/users?ids=1,2,3
@GetMapping("/users")
public List<User> getUsers(@RequestParam List<Long> ids) {
return userService.findByIds(ids);
}
15. Explain @RequestBody, @ResponseBody, and ResponseEntity.
@RequestBody: Binds HTTP request body to method parameter. Deserializes JSON/XML to object.
@PostMapping("/users")
public User createUser(@RequestBody User user) {
// Request body JSON: {"name": "John", "email": "john@email.com"}
// Automatically converted to User object
return userService.save(user);
}
// With validation
@PostMapping("/users")
public User createUser(@Valid @RequestBody UserDTO user) {
return userService.save(user);
}
@ResponseBody: Returns method result as response body (JSON/XML). Included in @RestController.
@Controller // Regular controller
public class MyController {
@GetMapping("/user")
@ResponseBody // Return JSON instead of view
public User getUser() {
return new User("John", 25);
}
}
ResponseEntity: Full control over HTTP response (status, headers, body).
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user == null) {
return ResponseEntity.notFound().build(); // 404
}
return ResponseEntity.ok(user); // 200 with body
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User saved = userService.save(user);
URI location = URI.create("/api/users/" + saved.getId());
return ResponseEntity
.created(location) // 201 Created
.header("X-Custom-Header", "value")
.body(saved);
}
@DeleteMapping("/users/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build(); // 204 No Content
}
// Error handling
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("Invalid input"));
return ResponseEntity
.badRequest()
.body(errors);