Introduction to Object-Oriented Programming
๐ What is OOP and Why Was It Introduced?
Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around data (objects) rather than around functions and logic. Before OOP, developers used Procedural / Structured Programming โ writing code as a sequence of instructions executed top to bottom.
The Problem with Procedural Programming
Procedural programming worked fine for small programs in the 1970s. As software grew larger, serious problems emerged:
OOP was introduced to solve all five of these problems by organizing code around objects that bundle both data and behavior together.
๐๏ธ The Four Pillars of OOP
Every OOP language is built on four fundamental concepts. Java implements all four:
private + getters/setters.extends.๐ฆ Object โ The Core Concept
An object is an instance of a class. Every real-world entity can be modeled as an object. An object has two things:
โ๏ธ OOP vs Procedural โ Full Comparison
| Feature | Procedural | OOP (Java) |
|---|---|---|
| Focus | Functions / procedures | Objects & classes |
| Data | Global, shared freely | Encapsulated inside objects |
| Code Reuse | Through function calls | Through inheritance |
| Security | No data hiding | Data hiding via private |
| Complexity | Harder to manage large apps | Scales well with size |
| Real-world mapping | Poor | Natural โ everything is an object |
| Examples | C, Pascal, BASIC | Java, Python, C++, C# |
โ Why Java for OOP?
Java was designed from the ground up as an OOP language by James Gosling at Sun Microsystems (1995). Key reasons Java is ideal for OOP:
- Pure OOP enforcement โ every piece of code lives inside a class
- Platform independence โ Write Once, Run Anywhere via JVM
- Strong type system โ catches errors at compile time
- Rich standard library โ entire Java API is built with OOP
- Industry adoption โ Android, Spring Boot, enterprise backends
int, double, boolean, etc.) that are NOT objects. Languages like Smalltalk are truly 100% OOP. Java is often called a "strongly OOP" language, not a "pure OOP" language. Wrapper classes (Integer, Double) exist to bridge this gap.
๐ How Java Code Runs โ JVM Overview
๐ฌ Demonstrating OOP Thinking in Java
Let's contrast how you'd solve the same problem in a procedural style vs an OOP style in Java.
โ Procedural Approach (Not Recommended)
// โ Procedural style โ data is scattered as loose variables public class ProceduralStyle { // Data floating around โ no protection, any method can touch it static String studentName = "Rahul"; static int studentAge = 20; static double studentMarks = 88.5; static void printStudent() { System.out.println("Name: " + studentName); System.out.println("Age: " + studentAge); System.out.println("Marks: " + studentMarks); } public static void main(String[] args) { printStudent(); // Problem: What if we need 2 students? We'd need 6 more variables! // This does NOT scale at all. } }
โ OOP Approach (Recommended)
// โ OOP style โ data and behavior bundled inside a class public class Student { // State โ instance variables String name; int age; double marks; // Behavior โ method void printInfo() { System.out.println("Name: " + name + ", Age: " + age + ", Marks: " + marks); } } class Main { public static void main(String[] args) { // Create first student object Student s1 = new Student(); s1.name = "Rahul"; s1.age = 20; s1.marks = 88.5; // Create second student object โ completely independent! Student s2 = new Student(); s2.name = "Priya"; s2.age = 22; s2.marks = 94.0; s1.printInfo(); s2.printInfo(); // Scales to 1000 students with just 1 more line each! } }
โ Demonstrating All 4 Pillars in One Program
// Demonstrates all 4 OOP pillars conceptually // ABSTRACTION โ hide implementation via abstract class abstract class Shape { abstract double area(); // contract โ every Shape MUST have area() void describe() { System.out.println("I am a shape with area: " + area()); } } // INHERITANCE โ Circle inherits from Shape class Circle extends Shape { // ENCAPSULATION โ private field + getter private double radius; Circle(double radius) { this.radius = radius; } public double getRadius() { return radius; } // POLYMORPHISM โ overrides parent's area() @Override double area() { return Math.PI * radius * radius; } } class Rectangle extends Shape { private double width, height; Rectangle(double w, double h) { width = w; height = h; } @Override double area() { return width * height; } } public class FourPillarsDemo { public static void main(String[] args) { // POLYMORPHISM โ same type (Shape), different actual objects Shape[] shapes = { new Circle(5), new Rectangle(4, 6) }; for (Shape s : shapes) { s.describe(); // area() called depends on actual type at runtime } } }
Abstraction โ Hiding complex implementation details and exposing only necessary functionality (abstract class, interface).
Inheritance โ A child class acquiring the properties and behaviors of a parent class (
extends keyword).Polymorphism โ One interface, many implementations โ same method name behaves differently (overloading + overriding).
int, long, double, float, char, boolean, byte, short) that are not objects โ they're stored directly in memory without any class wrapper. In a truly pure OOP language (like Smalltalk), everything is an object. Java provides wrapper classes (Integer, Double, etc.) to treat primitives as objects when needed, but the primitives themselves are not objects. Additionally, Java has static methods and variables that belong to the class rather than to any object.
An Object is a concrete instance created from the class. It exists at runtime in the Heap memory and has its own copy of instance variables.
Analogy: Class = cookie cutter mold. Object = the actual cookie.
Encapsulation = How you protect data โ hiding the internal state of an object and exposing it only through controlled getters/setters. It's about data hiding. Example: A bank account class has a private
balance field โ you can't directly modify it, only through deposit() or withdraw().Abstraction = What you hide โ hiding complex implementation logic and showing only the essential interface. It's about implementation hiding. Example: You call
list.sort() โ you don't know whether it uses merge sort or quicksort internally. That detail is abstracted away.Simple summary: Encapsulation hides data. Abstraction hides complexity.
2. Code Reusability โ Inheritance lets you reuse existing code without rewriting.
3. Maintainability โ Changes in one class don't break unrelated classes (if designed well).
4. Scalability โ Large applications are easier to manage with well-defined classes.
5. Real-world modeling โ Objects mirror real-world entities naturally.
6. Flexibility โ Polymorphism allows writing flexible and extensible code.
๐ฑ How OOP Concepts Drive Spring Boot
Spring Boot is built entirely on OOP principles. Understanding OOP is a prerequisite to understanding Spring. Here's how each pillar maps to Spring Boot:
Abstraction โ You write to interfaces (
UserRepository), not implementations. Spring injects the concrete class at runtime.Inheritance โ Spring controllers often extend base controllers for shared error handling, CORS config, etc.
Polymorphism โ Spring's
@Qualifier and profiles let you swap implementations without changing calling code.
Spring Bean โ OOP in Action
// ABSTRACTION โ program to interface, not implementation public interface UserService { User findById(Long id); List<User> findAll(); } // ENCAPSULATION โ implementation hidden inside service class @Service // Spring annotation โ marks this as a managed Bean public class UserServiceImpl implements UserService { // ENCAPSULATION โ private dependency, injected by Spring private final UserRepository userRepository; // Constructor injection โ recommended OOP approach public UserServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override public User findById(Long id) { return userRepository.findById(id).orElseThrow(); } @Override public List<User> findAll() { return userRepository.findAll(); } } // POLYMORPHISM โ Controller uses UserService interface // Spring decides at runtime which implementation to inject @RestController public class UserController { private final UserService userService; // Interface reference! public UserController(UserService userService) { this.userService = userService; } @GetMapping("/users/{id}") public User getUser(@PathVariable Long id) { return userService.findById(id); } }
Notice: UserController depends on the interface UserService, not the implementation UserServiceImpl. This is Abstraction + Polymorphism in action. You can swap the entire implementation (e.g., from SQL to NoSQL) without touching the controller.
Key Spring annotations to know: @Service @Repository @Controller @Component @Autowired
- OOP was introduced to overcome limitations of procedural programming
- The 4 pillars: Encapsulation, Abstraction, Inheritance, Polymorphism (mnemonic: APIE)
- Java is NOT 100% pure OOP (8 primitive types are not objects)
- Smalltalk is considered a 100% pure OOP language
- Java was created by James Gosling at Sun Microsystems in 1995
- JVM = Java Virtual Machine โ enables platform independence
- Class takes no memory; Object takes memory in Heap
๐ Quick MCQ Traps
| Question | Answer | Why (Trap) |
|---|---|---|
| Which is NOT a pillar of OOP? | Compilation | The 4 pillars are Encapsulation, Abstraction, Inheritance, Polymorphism only |
| Is Java 100% OOP? | No | Primitive types (int, char etc.) are not objects |
| What does a class represent? | Blueprint / Template | Not a real entity โ it's a design. Object is the real entity |
| Which OOP pillar prevents direct data access? | Encapsulation | Common confusion โ Abstraction hides complexity, Encapsulation hides data |
| Which pillar allows one class to use methods of another? | Inheritance | Not to be confused with Composition (has-a relationship) |
| In which memory area are objects stored in Java? | Heap | Stack stores references and primitive locals. Heap stores objects |
๐ One-Liner Definitions (for quick revision)
- OOP โ A programming paradigm organizing code around objects that bundle data and behavior.
- Class โ A blueprint that defines the structure and behavior of objects.
- Object โ A runtime instance of a class with its own state in Heap memory.
- Encapsulation โ Bundling data and methods; hiding data via access modifiers.
- Abstraction โ Hiding implementation details; exposing only the interface.
- Inheritance โ A subclass acquiring properties and methods of a superclass.
- Polymorphism โ Same method name behaving differently based on context or object type.
- JVM โ Java Virtual Machine that interprets bytecode and enables cross-platform execution.
Classes & Objects
๐งฑ What is a Class?
A class is the fundamental building block of OOP in Java. It is a logical construct โ a blueprint or template that describes what properties (data) and behaviors (methods) objects of that type will have.
Think of a class as an architectural blueprint for a house. The blueprint itself is not a house โ it doesn't consume land or materials. But from one blueprint you can construct many houses, each with its own address, color, and residents.
new keyword. The class is purely a compile-time concept.
Anatomy of a Class
๐ฆ What is an Object?
An object is a concrete, real instance of a class created at runtime. When you use the new keyword, the JVM:
- Allocates memory in the Heap for the object's fields
- Runs the constructor to initialize the object
- Returns a reference (memory address) stored in the Stack variable
๐ท๏ธ Types of Classes in Java
| Type | Description | Example |
|---|---|---|
| Concrete Class | Normal, fully implemented class. Can be instantiated. | class Student { } |
| Abstract Class | Has abstract keyword. Cannot be instantiated directly. May have abstract methods. | abstract class Shape { } |
| Final Class | Cannot be subclassed. Used to prevent inheritance. | final class String { } |
| Inner Class | Defined inside another class. | class Outer { class Inner { } } |
| Anonymous Class | Class with no name, defined inline for one-time use. | new Runnable() { ... } |
| Pre-defined Class | Provided by the Java standard library (JDK). | String, Math, ArrayList |
| User-defined Class | Created by the programmer for the application. | class Employee { } |
๐ Accessing Members via Object
Once an object is created, you access its fields and methods using the dot operator (.):
๐ Multiple Objects โ Independent State
Each object created from the same class has its own independent copy of instance variables. Changing one object's state does not affect another (unless they share a reference).
Student s2 = s1; does NOT create a new object. It creates a second reference pointing to the same object in Heap. Any changes via s2 will be visible via s1. To create a truly independent copy, use a Copy Constructor (covered in Topic 10).
๐ง Default Values of Instance Variables
| Type | Default Value |
|---|---|
int, long, short, byte | 0 |
float, double | 0.0 |
boolean | false |
char | '\u0000' (null char) |
| Any object reference (String, arrays, etc.) | null |
Local variables (declared inside a method) are NOT given defaults โ the compiler will throw an error if you use them without initializing.
๐ป Program 1 โ Basic Class and Object
public class Student { // Instance variables โ STATE String name; int age; double marks; // Method โ BEHAVIOR void printInfo() { System.out.println("--- Student Info ---"); System.out.println("Name : " + name); System.out.println("Age : " + age); System.out.println("Marks : " + marks); } public static void main(String[] args) { // Create object โ memory allocated in Heap Student s1 = new Student(); // Assign values via dot operator s1.name = "Rahul"; s1.age = 20; s1.marks = 88.5; // Create second independent object Student s2 = new Student(); s2.name = "Priya"; s2.age = 22; s2.marks = 94.0; s1.printInfo(); s2.printInfo(); } }
๐ป Program 2 โ Default Values Before Assignment
public class DefaultValues { int count; // default: 0 double price; // default: 0.0 boolean active; // default: false String label; // default: null char grade; // default: '\u0000' void showDefaults() { System.out.println("int : " + count); System.out.println("double : " + price); System.out.println("boolean : " + active); System.out.println("String : " + label); System.out.println("char : '" + grade + "' (unicode: " + (int) grade + ")"); } public static void main(String[] args) { new DefaultValues().showDefaults(); } }
๐ป Program 3 โ Reference Copy Trap
public class ReferenceTrap { String name; int age; public static void main(String[] args) { ReferenceTrap obj1 = new ReferenceTrap(); obj1.name = "Arjun"; obj1.age = 25; // โ ๏ธ This does NOT create a new object! // obj2 is just another reference to the SAME Heap object ReferenceTrap obj2 = obj1; obj2.name = "Changed!"; // modifies the shared object // Both reflect the change โ same object! System.out.println("obj1.name = " + obj1.name); System.out.println("obj2.name = " + obj2.name); System.out.println("Same object? " + (obj1 == obj2)); } }
๐ป Program 4 โ Class as a Type (Array of Objects)
public class Car { String brand; int year; double price; void display() { System.out.printf("%-10s | %d | โน%.2f%n", brand, year, price); } public static void main(String[] args) { // Array of Car objects โ initially all null Car[] fleet = new Car[3]; fleet[0] = new Car(); fleet[0].brand = "Toyota"; fleet[0].year = 2022; fleet[0].price = 1500000; fleet[1] = new Car(); fleet[1].brand = "Honda"; fleet[1].year = 2023; fleet[1].price = 1200000; fleet[2] = new Car(); fleet[2].brand = "Maruti"; fleet[2].year = 2021; fleet[2].price = 700000; System.out.println("Brand | Year | Price"); System.out.println("-----------|------|------------"); for (Car c : fleet) c.display(); } }
public class, and the filename must match that class name exactly (case-sensitive). You can have multiple non-public classes in one file, but keep it to one class per file for clean, maintainable code.
new, calling any method on it will throw a NullPointerException at runtime:Student s; โ declared but nulls.printInfo(); โ ๐ฅ NullPointerException!Always ensure you've used
new before calling methods on an object reference.
An Object is a runtime instance of a class โ allocated on the Heap, with its own copy of instance variables. You create objects using the
new keyword.Analogy: Class = blueprint of a house. Object = the actual house built from that blueprint. You can build many houses from one blueprint, each with different paint colors (state).
int/long/short/byte โ 0float/double โ 0.0boolean โ falsechar โ '\u0000' (null character)Object references (String, arrays, custom classes) โ nullNote: local variables are NOT initialized โ the compiler throws an error if you use an uninitialized local variable.
a1 and a2 now point to the same object in Heap. Modifying the object via a2 will also reflect when accessed via a1. To get a true independent copy, you need a Copy Constructor or implement Cloneable.
javac) uses the filename to locate the public class definition. When the JVM needs to load a class, it looks for a .class file with the matching name. If the filename doesn't match the public class name, the compiler throws a compile-time error. This convention also makes it easy for developers and tools to quickly find where a class is defined โ you always know UserService.java contains the UserService class.
.java file can have multiple class definitions, but with strict rules:1. Only one class can be
public in a file.2. The filename must exactly match the
public class name.3. Non-public classes in the same file get their own
.class file after compilation but can't be accessed from outside the package.Best practice: one class per file for clarity and maintainability.
NullPointerException (NPE) is thrown when you try to call a method or access a field on an object reference that is null (points to no object).Common causes: forgetting
new, a method returning null and not checking, uninitialized array elements.Prevention: Always initialize references before use, use
Optional<T> in modern Java for nullable returns, add null checks before method calls, use Objects.requireNonNull() in constructors.
๐ฑ Classes & Objects in Spring Boot
In Spring Boot, every component you write is a class. Spring manages the lifecycle of these objects (called Beans) for you โ you don't call new manually. Spring's IoC (Inversion of Control) container creates, wires, and destroys objects.
new ClassName() for you. This is called Dependency Injection (DI).
import jakarta.persistence.*; // @Entity maps this Java class to a database table @Entity @Table(name = "products") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private Double price; private Integer stock; // Getters and setters (Spring/JPA requires these) public Long getId() { return id; } public String getName() { return name; } public Double getPrice() { return price; } public Integer getStock() { return stock; } public void setName(String name) { this.name = name; } public void setPrice(Double price) { this.price = price; } public void setStock(Integer stock) { this.stock = stock; } }
Key Spring class-related annotations: @Entity @Component @Service @Repository @RestController @Bean
Student s = new Student();. In Spring Boot you write @Autowired Student s; and Spring injects a managed instance. This is the IoC (Inversion of Control) principle in action โ the framework controls object creation, not you.
- A class is a logical entity โ takes no memory at runtime
- An object is a physical entity โ allocated in Heap
- Objects are created using the
newkeyword - Default value of object references is
null - Default value of
intis0,booleanisfalse Student s2 = s1โ reference copy, NOT object copy- One
publicclass per.javafile; filename must match - Dot operator (
.) used to access members of an object
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Where is an object stored in Java? | Heap | Stack stores local vars & references, not objects themselves |
| What is the default value of a String field? | null | Not "" โ it's null until explicitly assigned |
| Does a class take memory? | No | Only objects take memory. Class is a template. |
Dog d2 = d1; โ how many objects? | 1 object | Only one object in Heap; two references pointing to it |
| Can a .java file have 2 public classes? | No | Compiler error โ only 1 public class allowed per file |
Default value of char? | '\u0000' | Not '0' โ it's the null Unicode character (value 0) |
๐ One-Liner Definitions
- Class โ A logical blueprint defining the structure and behavior of objects; takes no runtime memory.
- Object โ A runtime instance of a class stored in the Heap with its own state.
- Instance Variable โ A field declared inside a class but outside methods; each object gets its own copy.
- Reference Variable โ A variable that holds the memory address (reference) of an object in Heap.
- new keyword โ Allocates Heap memory and calls the constructor to create an object.
- Dot Operator (.) โ Used to access fields and methods of an object via its reference.
- NullPointerException โ Runtime exception thrown when a null reference is used to access members.
Methods in Java
โ๏ธ What is a Method?
A method is a named block of code inside a class that performs a specific task. Methods define the behavior of an object. Calling a method executes that block of code without rewriting it every time โ this is the essence of code reuse.
Anatomy of a Method
๐ Return Types
Every method must declare what it returns. Java enforces this at compile time:
voidโ method returns nothing. Noreturnstatement needed (or barereturn;).- Primitive type (
int,double,boolean, etc.) โ method must return that exact type. - Object/reference type (
String,Student,List<T>, etc.) โ method must return that object. Objectโ can return any object (least specific, avoid if possible).
๐จ Parameters vs Arguments
๐ How Java Passes Arguments โ Pass by Value
Java is always pass-by-value. This is one of the most misunderstood concepts in Java:
- For primitives: a copy of the value is passed. The original is never modified.
- For objects: a copy of the reference (address) is passed. The method can modify the object's fields via this reference, but cannot reassign the original variable.
๐ Method Overloading
Method Overloading means defining multiple methods in the same class with the same name but different parameter lists. Java resolves which one to call at compile time based on the number, types, and order of arguments โ making it compile-time (static) polymorphism.
Three valid ways to overload:
- Different number of parameters โ
add(int a, int b)vsadd(int a, int b, int c) - Different types of parameters โ
add(int a, int b)vsadd(double a, double b) - Different order of parameter types โ
show(int a, String b)vsshow(String a, int b)
int getValue() and double getValue() in the same class = compile error. The compiler resolves overloading based on the call's argument list. Since return type is not part of the call, it cannot disambiguate. This will always fail.
๐ Method Chaining
When a method returns this (the current object), you can chain multiple calls together. This is widely used in the Builder pattern and StringBuilder:
๐ฆ Types of Methods in Java
| Type | Description | Example |
|---|---|---|
| Instance Method | Belongs to an object. Needs object to call. Can access instance variables. | obj.getName() |
| Static Method | Belongs to the class. Called without object. Cannot access instance variables directly. | Math.sqrt() |
| Abstract Method | Declared without body. Must be overridden by subclass. | abstract void draw(); |
| Final Method | Cannot be overridden in subclasses. | final void lock() |
| Synchronized Method | Thread-safe. Only one thread can execute at a time. | synchronized void add() |
| Native Method | Implemented in another language (C/C++) via JNI. | native int compute(); |
๐ป Program 1 โ void Method and Return Type Method
public class Calculator { // void method โ performs action, returns nothing void printSum(int a, int b) { System.out.println("Sum (void): " + (a + b)); } // int return type โ returns computed value to caller int getSum(int a, int b) { return a + b; } // double return type double getAverage(int a, int b, int c) { return (a + b + c) / 3.0; } // boolean return type โ useful for validation boolean isEven(int n) { return n % 2 == 0; } public static void main(String[] args) { Calculator calc = new Calculator(); calc.printSum(5, 3); int result = calc.getSum(5, 3); System.out.println("Sum (return): " + result); System.out.println("Average: " + calc.getAverage(10, 20, 30)); System.out.println("Is 8 even? " + calc.isEven(8)); System.out.println("Is 7 even? " + calc.isEven(7)); } }
๐ป Program 2 โ Method Overloading (all 3 ways)
public class OverloadDemo { // Overload 1 โ different NUMBER of parameters int add(int a, int b) { System.out.print("add(int,int) โ "); return a + b; } int add(int a, int b, int c) { System.out.print("add(int,int,int) โ "); return a + b + c; } // Overload 2 โ different TYPES of parameters double add(double a, double b) { System.out.print("add(double,double) โ "); return a + b; } // Overload 3 โ different ORDER of parameter types String display(int id, String name) { System.out.print("display(int,String) โ "); return id + ": " + name; } String display(String name, int id) { System.out.print("display(String,int) โ "); return name + " (ID: " + id + ")"; } public static void main(String[] args) { OverloadDemo obj = new OverloadDemo(); System.out.println(obj.add(2, 3)); System.out.println(obj.add(2, 3, 4)); System.out.println(obj.add(2.5, 3.5)); System.out.println(obj.display(101, "Rahul")); System.out.println(obj.display("Priya", 102)); } }
๐ป Program 3 โ Pass by Value: Primitive vs Object
class Box { int value; Box(int v) { value = v; } } public class PassByValue { // Primitive โ original NOT modified static void tryDoubleInt(int x) { x = x * 2; // only modifies local copy } // Object โ field CAN be modified via reference copy static void tryDoubleBox(Box b) { b.value = b.value * 2; // modifies actual Heap object } // Object reassignment โ does NOT affect original reference static void tryReplaceBox(Box b) { b = new Box(999); // only changes local copy of reference } public static void main(String[] args) { int num = 10; tryDoubleInt(num); System.out.println("After tryDoubleInt: " + num); // still 10 Box box = new Box(10); tryDoubleBox(box); System.out.println("After tryDoubleBox: " + box.value); // 20 โ changed! Box box2 = new Box(10); tryReplaceBox(box2); System.out.println("After tryReplaceBox: " + box2.value); // still 10 } }
๐ป Program 4 โ Method Chaining with Builder Pattern
public class PersonBuilder { String name; int age; String city; // Each setter returns 'this' โ enables chaining PersonBuilder setName(String name) { this.name = name; return this; } PersonBuilder setAge(int age) { this.age = age; return this; } PersonBuilder setCity(String city) { this.city = city; return this; } void print() { System.out.println(name + " | Age: " + age + " | City: " + city); } public static void main(String[] args) { // Fluent method chaining new PersonBuilder() .setName("Rahul") .setAge(25) .setCity("Mumbai") .print(); } }
validateAndSave()), it's a sign it should be split into two separate methods.
Example: for
public int add(int a, double b), the signature is add(int, double).The signature is what the compiler uses to resolve method calls โ especially for overloading.
Valid overloading requires one of: different number of parameters, different types of parameters, or different order of parameter types.
Invalid: changing only the return type โ the compiler can't tell which version to call since the return type is not visible at the call site.
Overloading is resolved at compile time โ it is compile-time / static polymorphism.
For primitives: the actual value is copied into the method parameter. Changes inside the method don't affect the original.
For object references: the value of the reference (i.e., the memory address) is copied. The method gets a copy of the pointer. It can modify the object's fields via that pointer, but it cannot make the original variable point to a different object.
This is why people get confused โ modifying an object's field inside a method does persist, but reassigning the reference does not.
A function is a standalone block of code not tied to any class (common in C, Python, JavaScript). Java doesn't support this at the language level โ though lambda expressions in Java 8+ bring functional-style programming, they are still backed by functional interfaces (which are classes).
main() by giving it different parameter lists. The JVM will always call public static void main(String[] args) as the entry point โ the overloaded versions are just regular methods that you'd have to call manually. Overloading main() is valid Java but rarely useful in practice.
Runtime polymorphism (dynamic binding): resolved by the JVM while the program is running. Achieved through Method Overriding. The JVM picks the right method based on the actual object type at runtime, even if the reference type is a parent class.
int literals 1 and 2 exactly match add(int, int), so that version is called. If only add(long, long) existed, Java would automatically widen the int arguments to long and call it. Java's overload resolution order is: exact match โ widening โ autoboxing โ varargs.
๐ฑ Methods in Spring Boot
In Spring Boot, methods are at the core of every layer โ controllers handle HTTP requests, services contain business logic, and repositories define data access. Method design directly impacts your API's clarity and maintainability.
findUserByIdOrEmail().
@Service public class OrderService { private final OrderRepository repo; public OrderService(OrderRepository repo) { this.repo = repo; } // Overloaded methods โ same intent, different filters public List<Order> findOrders() { return repo.findAll(); } public List<Order> findOrders(String status) { return repo.findByStatus(status); } public List<Order> findOrders(String status, Long userId) { return repo.findByStatusAndUserId(status, userId); } // void method โ side effect only (send email, log, etc.) public void cancelOrder(Long orderId) { Order order = repo.findById(orderId).orElseThrow(); order.setStatus("CANCELLED"); repo.save(order); } }
getX() / findX() for reads, createX() / saveX() for inserts, updateX() for modifications, deleteX() / removeX() for deletes. Spring Data JPA also uses method names to auto-generate queries โ findByEmailAndActive(String email, boolean active) generates the SQL automatically.
- Method signature = name + parameter types (return type NOT included)
- Method overloading = same name, different parameter list = compile-time polymorphism
- Cannot overload by return type alone โ compile error
- Java is always pass-by-value โ even for objects (reference copy, not object copy)
voidmethods do not return any value- In Java there are no standalone functions โ only methods inside classes
- Overloading resolution order: exact match โ widening โ autoboxing โ varargs
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Can two methods differ only in return type? | No โ compile error | Return type is not part of the signature. Compiler can't distinguish. |
| Java is pass-by-__? | Value | Even for objects โ the reference value is copied, not the object itself |
| Method overloading is resolved at __? | Compile time | Runtime resolution = overriding. Compile-time = overloading. |
| Can main() be overloaded? | Yes | Valid, but JVM only calls main(String[]) as entry point |
| What is the return type of a constructor? | None (not even void) | Constructors have no return type โ not even void. This is a defining rule. |
| Which type of method cannot access instance variables? | Static method | Static methods belong to the class, not to any object, so no this |
๐ One-Liner Definitions
- Method โ A named block of code inside a class that defines the behavior of an object.
- Method Signature โ Method name + parameter types. Used by compiler to uniquely identify a method.
- Method Overloading โ Multiple methods with the same name but different parameter lists in one class.
- void โ Return type keyword meaning the method returns no value.
- Pass by Value โ Java always passes a copy of the value (primitive value or reference address) to methods.
- Static Method โ A method belonging to the class, callable without creating an object.
- Method Chaining โ Calling multiple methods on the same object in a single expression by returning
this.
Memory Management โ Stack & Heap
๐ง Why Does Memory Management Matter?
Every variable, object, and method call in a Java program occupies memory at runtime. The JVM divides this memory into distinct regions, each with a specific purpose. The two most important regions for a Java developer are the Stack and the Heap.
๐ The Stack
The Stack is a Last-In-First-Out (LIFO) memory region. Every method call pushes a new stack frame; when the method returns, that frame is popped and all its data is gone.
Each stack frame holds: local variables, reference variables, method parameters, and the return address.
๐๏ธ The Heap
The Heap is a large shared pool where all objects and instance variables live. Every new ClassName() allocates Heap memory. The Garbage Collector automatically reclaims memory from unreachable objects.
๐จ Full Memory Diagram
๐ Stack Frame Lifecycle
โป๏ธ Garbage Collection
When no reference points to a Heap object, it becomes unreachable and is eligible for GC. You can suggest GC with System.gc() but the JVM may ignore it. Never rely on GC timing โ use try-with-resources for IO/DB cleanup.
๐ฅ Common Memory Errors
| Error | Cause | Area |
|---|---|---|
StackOverflowError | Infinite recursion โ frames pile up until Stack exhausted | Stack |
OutOfMemoryError: Java heap space | Too many objects, none becoming unreachable โ memory leak | Heap |
NullPointerException | Dereferencing a null reference โ points to no object | Stack (null ref) |
OutOfMemoryError: Metaspace | Too many classes loaded dynamically | Metaspace |
๐บ๏ธ Where Does Each Variable Live?
| Variable Type | Memory Location | Example |
|---|---|---|
| Local primitive | Stack | int x = 5; inside a method |
| Local reference variable | Stack (address only) | Student s = ... โ s itself on Stack |
| Object itself | Heap | The actual Student object fields |
| Instance variable (field) | Heap (inside object) | String name; in class body |
| Static variable | Metaspace | static int count; |
| String literal | String Pool (Heap) | String s = "hello"; |
String a = "hello" checks the pool first โ if "hello" exists, reuses it. That's why "hello" == "hello" is true. But new String("hello") == new String("hello") is false โ new always bypasses the pool. Use .intern() to force a string into the pool.
๐ป Program 1 โ Stack vs Heap: References & GC Eligibility
class Student { String name; // instance var โ HEAP (inside object) int age; // instance var โ HEAP (inside object) Student(String name, int age) { this.name=name; this.age=age; } } public class MemoryDemo { static void printStudent(Student s) { // 's' = local reference on THIS frame โ points to same Heap object int localVar = 42; // on THIS stack frame only, gone after return System.out.println(s.name + " | Age: " + s.age); } public static void main(String[] args) { Student s1 = new Student("Rahul", 20); // s1 ref on Stack, object on Heap Student s2 = new Student("Priya", 22); // s2 ref on Stack, new object on Heap Student s3 = s1; // s3 ref on Stack, SAME Heap object as s1 printStudent(s1); printStudent(s2); s3.name = "Changed via s3"; System.out.println("s1.name = " + s1.name); // reflects s3's change! s1 = null; // object still alive โ s3 still points to it System.out.println("s3.name = " + s3.name); s3 = null; // NOW โ 0 references โ GC eligible } }
๐ป Program 2 โ StackOverflowError via Infinite Recursion
public class StackOverflowDemo { static void recurse(int n) { System.out.println("Call #" + n); recurse(n + 1); // no base case โ infinite stack frames } public static void main(String[] args) { try { recurse(1); } catch (StackOverflowError e) { System.out.println("๐ฅ StackOverflowError! Stack memory exhausted."); } } }
๐ป Program 3 โ String Pool: == vs .equals()
public class StringPoolDemo { public static void main(String[] args) { String a = "hello"; // goes to String Pool String b = "hello"; // reuses same pooled object String c = new String("hello"); // new Heap object, bypasses pool String d = new String("hello"); // another new Heap object // == compares REFERENCES (addresses) System.out.println("a == b : " + (a == b)); // true โ same pool obj System.out.println("a == c : " + (a == c)); // false โ different objects System.out.println("c == d : " + (c == d)); // false โ two new objects // .equals() compares CONTENT โ use this always for Strings! System.out.println("a.equals(c) : " + a.equals(c)); // true โ same content // intern() forces into the pool String e = c.intern(); System.out.println("a == e interned: " + (a == e)); // true } }
== on Strings compares memory addresses, not content. It may accidentally work for literals (same pool object) but will fail for new String(), user input, or database values. Best practice: "literal".equals(variable) โ literal first to avoid NPE if variable is null.
java -Xms256m -Xmx1g -jar app.jar-Xms = initial heap size | -Xmx = max heap size. Start with these flags and profile with tools like VisualVM or Java Flight Recorder if you see OutOfMemoryError.
StackOverflowError if full.Heap: Shared across threads, stores all objects + instance vars. GC-managed. Slower allocation. Configurable size โ
OutOfMemoryError if full.
Instance variables (class fields): on the Heap, inside the object. Alive as long as the object is reachable.
Note: a local reference variable is on the Stack, but the object it points to is on the Heap.
System.gc() suggests GC but is not guaranteed. Never rely on GC timing for resource cleanup โ use try-with-resources instead.
StackOverflowError: Stack exhausted โ almost always caused by infinite recursion (no base case). Each call pushes a frame until no space remains.OutOfMemoryError: Java heap space: Heap exhausted โ caused by creating objects faster than GC can collect them, or by memory leaks (objects kept alive in collections/listeners never cleaned up).
"hello", Java checks if it's in the pool โ if yes, reuses that object; if no, creates and adds it. This saves memory because Strings are the most-used objects and many have identical values.new String("hello") bypasses the pool and creates a fresh Heap object. .intern() manually adds a String to the pool.
== checks if two references point to the same object in memory (same address)..equals() checks if two Strings have the same content.For literals,
== may accidentally return true (same pooled object). But for new String(), user input, or values from a database, identical content may live in different objects โ == would return false. Always use .equals() for content comparison. Use "literal".equals(var) to prevent NPE.
OutOfMemoryError: Metaspace error when exhausted.
๐ฑ Memory Management in Spring Boot
Spring Boot runs inside a JVM โ all Stack/Heap rules apply. Bean scope directly controls how many objects Spring creates in the Heap.
Prototype: NEW Heap object every injection request โ use sparingly, watch memory.
Request: New object per HTTP request, GC-eligible after the request ends.
// SINGLETON โ one Heap object for entire app lifetime (default) @Service public class UserService { } // PROTOTYPE โ new Heap object every time it's requested @Component @Scope("prototype") public class ReportGenerator { } // JVM Heap tuning flags for Spring Boot production: // java -Xms256m -Xmx1g -jar myapp.jar // -Xms = initial heap size (avoid expensive resizing at startup) // -Xmx = max heap size (cap it based on your server's RAM)
- Stack = LIFO, per-thread, holds local vars & frames, auto-cleaned
- Heap = shared, holds all objects, GC-managed
- Instance variables โ Heap (inside object)
- Local variables โ Stack (including reference vars)
- Static variables โ Metaspace (Method Area)
StackOverflowErrorโ infinite recursion โ Stack fullOutOfMemoryErrorโ too many objects โ Heap full- String literals โ String Pool (inside Heap) โ shared/reused
new String("x")โ always new Heap object, bypasses pool- Always use
.equals()for String content comparison, never==
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Where do objects live in Java? | Heap | Stack only holds references and primitives, not objects themselves |
| Where do instance variables live? | Heap (inside object) | They travel with the object โ not on any method's stack frame |
| Where do static variables live? | Metaspace / Method Area | Not Stack, not Heap โ separate class-level memory |
String a="x"; String b="x"; a==b? | true | Both reference the same pooled String object |
new String("x") == new String("x")? | false | new always creates fresh Heap objects, bypasses pool |
| Infinite recursion causes what error? | StackOverflowError | Not OutOfMemoryError โ specifically the Stack overflows |
| Is Stack shared between threads? | No | Each thread has its own Stack. Heap IS shared. |
๐ One-Liner Definitions
- Stack โ LIFO per-thread memory holding method frames, local variables, and references.
- Heap โ Shared memory where all Java objects and instance variables live.
- Stack Frame โ A block of Stack memory for a single method call; destroyed on return.
- Garbage Collection โ Automatic JVM process reclaiming Heap memory from unreachable objects.
- String Pool โ A cache of String literals inside the Heap enabling reuse of identical strings.
- Metaspace โ Native memory (Java 8+) storing class metadata and static variables.
- StackOverflowError โ Error thrown when Stack memory is exhausted, typically via infinite recursion.
Arrays (Primitives & Objects)
๐ฆ What is an Array?
An array is a fixed-size, ordered collection of elements of the same data type, stored in contiguous memory locations in the Heap. Arrays are the simplest data structure in Java and form the foundation of many other collections like ArrayList.
- Fixed size โ once created, the length cannot change
- Zero-indexed โ first element is at index
0, last atlength - 1 - Same type โ all elements must be the same data type
- Heap-allocated โ arrays are objects in Java, stored in Heap
- Default values โ elements initialized to type defaults (
0,false,null)
๐ง Declaring and Creating Arrays
There are three ways to create an array in Java:
๐ง Arrays in Memory
Student[] students = new Student[3], you get an array of 3 null references โ not 3 Student objects. You must separately create each object with new Student() and assign it to each slot. Forgetting this causes NullPointerException when accessing elements.
๐ Iterating Arrays
Java provides two ways to loop through an array:
๐ Multi-Dimensional Arrays
Java supports arrays of arrays โ commonly used for matrices and grids:
๐ ๏ธ Useful Array Utilities โ java.util.Arrays
| Method | Purpose | Example |
|---|---|---|
Arrays.sort(arr) | Sort in ascending order | Arrays.sort(marks) |
Arrays.toString(arr) | Print array as string | "[85, 90, 78]" |
Arrays.fill(arr, val) | Fill all elements with value | Arrays.fill(arr, 0) |
Arrays.copyOf(arr, len) | Copy with new length | Arrays.copyOf(arr, 3) |
Arrays.equals(a, b) | Compare two arrays by content | Arrays.equals(a, b) |
Arrays.binarySearch(arr, key) | Search sorted array | Returns index or negative |
๐ป Program 1 โ Primitive Array: Declare, Fill, Iterate
import java.util.Arrays; public class PrimitiveArray { public static void main(String[] args) { // Create and initialise int[] marks = {85, 92, 78, 96, 88}; // length property โ NOT a method, no parentheses System.out.println("Length: " + marks.length); // Standard for loop โ with index System.out.print("Marks: "); for (int i = 0; i < marks.length; i++) System.out.print(marks[i] + " "); System.out.println(); // Enhanced for-each โ cleaner when index not needed int sum = 0; for (int m : marks) sum += m; System.out.println("Sum: " + sum); System.out.println("Average: " + (double) sum / marks.length); // Sort and print Arrays.sort(marks); System.out.println("Sorted: " + Arrays.toString(marks)); } }
๐ป Program 2 โ Array of Objects (Student)
class Student { String name; int marks; Student(String name, int marks) { this.name = name; this.marks = marks; } } public class ObjectArray { public static void main(String[] args) { // Step 1: create array of 3 null references Student[] batch = new Student[3]; // Step 2: create each object and assign to its slot batch[0] = new Student("Rahul", 88); batch[1] = new Student("Priya", 94); batch[2] = new Student("Arjun", 79); // Iterate and print System.out.println("Student Results:"); for (Student s : batch) System.out.printf(" %-10s โ %d%n", s.name, s.marks); // Find topper Student topper = batch[0]; for (Student s : batch) if (s.marks > topper.marks) topper = s; System.out.println("Topper: " + topper.name + " (" + topper.marks + ")"); } }
๐ป Program 3 โ ArrayIndexOutOfBoundsException Demo
public class ArrayBoundsDemo { public static void main(String[] args) { int[] nums = {10, 20, 30}; // valid indices: 0, 1, 2 // Safe access System.out.println(nums[2]); // 30 โ OK // Dangerous โ catching the exception try { System.out.println(nums[5]); // index 5 doesn't exist! } catch (ArrayIndexOutOfBoundsException e) { System.out.println("๐ฅ Caught: " + e.getMessage()); } // Common off-by-one mistake โ use < not <= for (int i = 0; i < nums.length; i++) // correct: i < length System.out.print(nums[i] + " "); } }
๐ป Program 4 โ 2D Array: Matrix Operations
public class MatrixDemo { public static void main(String[] args) { int[][] matrix = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; // Print as grid System.out.println("Matrix:"); for (int[] row : matrix) { for (int val : row) System.out.printf("%3d", val); System.out.println(); } // Sum of diagonal int diag = 0; for (int i = 0; i < matrix.length; i++) diag += matrix[i][i]; System.out.println("Diagonal sum: " + diag); } }
System.out.println(marks) prints something like [I@6d06d69c โ the type code and hash, NOT the values. Always use Arrays.toString(marks) for 1D arrays and Arrays.deepToString(matrix) for 2D arrays.
array when the size is fixed and performance matters (no overhead). Use ArrayList<T> when the size is dynamic, you need to add/remove elements, or you want built-in utility methods. In Spring Boot, you will almost always use List<T> and ArrayList.
.length field and inherits from Object).
array.length is a field (no parentheses) โ it's a property stored directly in the array object.string.length() is a method (with parentheses) โ it computes and returns the character count.This is a classic exam trap. Arrays use
.length (field). Strings use .length() (method). Collections like ArrayList use .size() (method).
RuntimeException thrown when you try to access an array element using an index that is either negative or โฅ array.length. Valid indices are always 0 to length - 1.Most common cause: the classic off-by-one error using
i <= arr.length instead of i < arr.length in a for loop.
1. Modify the array โ changes to the loop variable don't affect the array
2. Access the index โ no counter available
3. Iterate in reverse โ always forward only
4. Iterate two arrays simultaneously
Use the standard
for loop whenever you need any of the above.
| Array | ArrayList |
|---|---|
| Fixed size | Dynamic size โ grows/shrinks |
| Can hold primitives | Holds only objects (uses autoboxing) |
| Faster โ no overhead | Slightly slower โ resizing overhead |
arr.length | list.size() |
arr[i] for access | list.get(i) for access |
| No built-in add/remove | Built-in add(), remove() |
For an array of objects (e.g.,
Student[]), the array object on the Heap contains references โ each slot holds a reference pointing to its respective Student object elsewhere in the Heap. The array of references and the objects it points to are all in the Heap.
null. You must separately call new Student(...) for each slot and assign it. Accessing any element before assignment throws NullPointerException.
๐ฑ Arrays in Spring Boot
In Spring Boot you rarely use raw arrays โ List<T> and Collection<T> are preferred because they work natively with JPA, Jackson (JSON), and Spring's dependency injection. But arrays do appear in specific contexts:
REST API responses โ Jackson serializes both arrays and Lists to JSON arrays.
@RequestParam โ multiple values for same param come as a String array.
Batch processing โ fixed-size byte[] buffers for file/stream processing.
@RestController public class ProductController { // Inject comma-separated config into a String array @Value("${app.allowed-categories}") // application.properties: private String[] allowedCategories; // app.allowed-categories=books,food,electronics // Multiple query params โ String array @GetMapping("/products") public List<Product> getProducts( @RequestParam(required = false) String[] tags) { // GET /products?tags=java&tags=spring โ tags = {"java","spring"} if (tags == null) return productService.findAll(); return productService.findByTags(Arrays.asList(tags)); } }
Arrays.asList(arr) wraps an array in a fixed-size List view โ useful when a Spring method expects a List but you have an array. Note: the returned list is fixed-size โ you cannot add() or remove() from it. Use new ArrayList<>(Arrays.asList(arr)) for a fully mutable list.
- Arrays are objects in Java โ stored in Heap
- Arrays are zero-indexed โ valid range:
0tolength - 1 - Array size is fixed after creation โ cannot resize
- Default values:
int[]โ0,boolean[]โfalse,Object[]โnull array.lengthis a field, not a method (no parentheses)new Student[3]creates 3 null references, NOT 3 objects- For-each cannot modify array or access index
- Use
Arrays.toString()to print array contents โ notprintln(arr) - Accessing invalid index โ
ArrayIndexOutOfBoundsException(runtime)
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| How do you get array size? | arr.length | It's a field, not arr.length() โ no parentheses |
| First element of array is at index? | 0 | Zero-indexed โ a common off-by-one mistake |
new int[5] โ what are the values? | All 0 | Default init โ not garbage values like C/C++ |
new Student[3] โ how many Student objects? | 0 | Creates 3 null references, no Student objects yet |
| Can for-each modify array elements? | No | Loop variable is a copy โ assignment doesn't affect array |
System.out.println(arr) prints? | Hash/type code | Use Arrays.toString(arr) to print values |
| Can an array hold different types? | No | All elements must be the same declared type |
๐ One-Liner Definitions
- Array โ Fixed-size, same-type, zero-indexed collection of elements stored contiguously in Heap.
- array.length โ A field (not method) holding the fixed size of the array.
- ArrayIndexOutOfBoundsException โ Runtime exception for accessing index < 0 or โฅ length.
- for-each loop โ Enhanced loop for read-only iteration; cannot access index or modify elements.
- Arrays.sort() โ Sorts a primitive array in ascending order in-place using Dual-Pivot Quicksort.
- Arrays.toString() โ Returns a human-readable String representation of a 1D array.
- 2D Array โ An array of arrays; each row is a separate Heap object with its own length.
Static Keyword
โก What Does static Mean?
The static keyword in Java means a member belongs to the class itself, not to any particular object. It is loaded and initialized once when the class is first loaded by the JVM โ before any object is ever created.
Think of it this way: instance members are like personal belongings of each person (every object has their own copy). Static members are like shared facilities in an apartment building โ one copy shared by everyone.
Step 1 โ Class Loading: loads the .class bytecode, allocates Metaspace, runs static blocks, initializes static variables.
Step 2 โ Object Instantiation: only happens when you call
new ClassName() โ runs the constructor, allocates Heap memory for the object.Static members exist after Step 1 โ you never need Step 2 to access them.
๐ Static Variable (Class Variable)
A static variable is shared across all instances of a class. There is exactly one copy in Metaspace, and every object reads and writes the same location.
c1.count works but is misleading โ it looks like c1 owns it. Always write Counter.count to make it clear this is a class-level member. Most IDEs show a warning when you access static members via object reference.
๐งฑ Static Block
A static block (also called a static initializer) is a block of code that runs once when the class is loaded โ before any constructor, before main(), before any object is created. Used for complex static initialization that cannot be done in a single line.
๐ง Static Method
A static method belongs to the class and can be called without creating an object. It has these restrictions:
- Cannot access instance variables directly (no
this) - Cannot call instance methods directly
- Can only directly access other static members
- Can access instance members if given an object reference
๐๏ธ Static Class (Nested)
In Java you cannot make a top-level class static. However, a nested class can be declared static โ a static nested class does not need an instance of the outer class to be instantiated (covered in detail in Topic 24: Inner Classes).
๐ Summary โ What static Can Be Applied To
| Applied To | Effect | Example |
|---|---|---|
| Variable | One shared copy in Metaspace for all objects | static int count; |
| Method | Callable without object; no this access | static void help() |
| Block | Runs once at class load time before constructors | static { ... } |
| Nested class | Inner class with no dependency on outer class instance | static class Node { } |
| Import | Import static members directly โ use without class prefix | import static java.lang.Math.*; |
๐ป Program 1 โ Static Variable as Object Counter
public class Employee { // static โ ONE copy shared by all Employee objects static int count = 0; // instance โ each Employee has their own String name; int id; Employee(String name) { this.name = name; count++; // increments the SHARED counter this.id = count; // auto-assign unique id } void display() { System.out.printf("ID: %d | Name: %s%n", id, name); } public static void main(String[] args) { Employee e1 = new Employee("Rahul"); Employee e2 = new Employee("Priya"); Employee e3 = new Employee("Arjun"); e1.display(); e2.display(); e3.display(); // Access via class name โ correct way System.out.println("Total employees: " + Employee.count); } }
๐ป Program 2 โ Static Block: Execution Order Proof
public class StaticBlockOrder { static int x; static int y; // Static block 1 โ runs first static { x = 10; System.out.println("Static block 1: x = " + x); } // Static block 2 โ runs second (multiple blocks allowed) static { y = x * 2; // can use x โ block 1 already ran System.out.println("Static block 2: y = " + y); } StaticBlockOrder() { System.out.println("Constructor called"); } public static void main(String[] args) { System.out.println("main() started"); new StaticBlockOrder(); new StaticBlockOrder(); // static blocks do NOT run again } }
main(). Even main() itself lives in a class that must first be loaded.
๐ป Program 3 โ Static Method: Limitation Demonstration
public class StaticMethodDemo { String instanceVar = "I am instance"; static String staticVar = "I am static"; // Static method โ belongs to class static void staticMethod() { System.out.println(staticVar); // โ can access static // System.out.println(instanceVar); // โ compile error โ no 'this' // Workaround: pass object reference StaticMethodDemo obj = new StaticMethodDemo(); System.out.println(obj.instanceVar); // โ via explicit object } // Instance method โ has 'this', can access everything void instanceMethod() { System.out.println(instanceVar); // โ both accessible System.out.println(staticVar); // โ } public static void main(String[] args) { staticMethod(); // no object needed new StaticMethodDemo().instanceMethod(); } }
๐ป Program 4 โ Static Import
// Without static import: Math.sqrt(16), Math.PI // With static import: sqrt(16), PI โ no Math. prefix needed import static java.lang.Math.*; public class StaticImportDemo { public static void main(String[] args) { System.out.println(sqrt(144)); // 12.0 System.out.println(pow(2, 10)); // 1024.0 System.out.println(PI); // 3.141592653589793 System.out.println(abs(-42)); // 42 } }
static turns your OOP code into procedural code. Static state is shared globally, making it hard to test, hard to parallelize, and prone to bugs in multi-threaded apps. Use static only for: constants (static final), utility/helper methods that need no state (like Math), and factory methods. Avoid mutable static variables in production code.
static is declaring constants:public static final double PI = 3.14159;static = one copy shared. final = value never changes. Together they define a true constant. Name in ALL_CAPS by convention.
static. One copy stored in Metaspace, shared by all objects of the class. Created when the class loads, destroyed when class is unloaded. Accessed via class name: ClassName.variable.Instance variable: no
static. Each object gets its own copy in Heap. Created when object is created (new), destroyed when object is GC'd. Accessed via object reference: obj.variable.
main() to start the program โ at that moment, no objects exist yet. The JVM cannot call an instance method without an object. By making main() static, the JVM can invoke it directly on the class without creating any instance. The full signature public static void main(String[] args) is a JVM contract โ every part has a reason: public = JVM can access it, static = no object needed, void = no return value to JVM, String[] args = command-line arguments.
1. Cannot use
this or super โ these refer to object instances which don't exist in a static context2. Cannot directly access instance variables โ no implicit object
3. Cannot directly call instance methods
4. Can access instance members only via an explicit object reference
5. Can freely access other static variables and static methods
When a subclass defines a static method with the same signature as a parent's static method, it hides the parent's version. The method called depends on the reference type at compile time, not the actual object type at runtime. True overriding involves runtime dispatch (polymorphism) โ static methods are resolved at compile time, so no runtime dispatch occurs.
Proof:
@Override on a static method causes a compile error.
main() executes. It runs exactly once per class load, regardless of how many objects are created.Yes, a class can have multiple static blocks. They run in the order they appear in the source code, top to bottom. Each subsequent block can use values set by earlier blocks.
static = belongs to the class, one shared copy, no object needed.final = value/reference cannot be changed after assignment (for variables), method cannot be overridden, class cannot be subclassed.They are completely independent but often combined:
public static final int MAX = 100; โ this is the Java idiom for a constant: shared across all instances (static) and never reassignable (final).
Solutions: use
synchronized blocks/methods, use AtomicInteger / AtomicLong (from java.util.concurrent.atomic), or avoid mutable static state altogether. This is one of the reasons overusing static is a design smell in multi-threaded applications.
๐ฑ static in Spring Boot
Spring Boot generally discourages mutable static state because it breaks testability and thread safety. However, static final constants and specific patterns are common and correct.
public static final String ROLE_ADMIN = "ROLE_ADMIN";Logger โ
private static final Logger log = LoggerFactory.getLogger(MyClass.class);Utility methods โ stateless helper methods that don't need Spring-managed state.
@Value workaround โ injecting into static field via setter (shown below).
// โโ Constants class โ all static final โโโโโโโโโโโโโโโโโโโโโโ public class AppConstants { public static final String ROLE_ADMIN = "ROLE_ADMIN"; public static final String ROLE_USER = "ROLE_USER"; public static final int MAX_PAGE_SIZE = 50; private AppConstants() {} // prevent instantiation } // โโ Logger โ standard Spring Boot pattern โโโโโโโโโโโโโโโโโโโ @Service public class OrderService { // static final logger โ one per class, shared, immutable private static final Logger log = LoggerFactory.getLogger(OrderService.class); public void placeOrder(Order order) { log.info("Placing order for user: {}", order.getUserId()); // ... } } // โโ @Value into static field (requires setter trick) โโโโโโโโ @Component public class Config { public static String apiKey; @Value("${app.api-key}") public void setApiKey(String key) { Config.apiKey = key; // @Value on setter, assigns to static field } }
@Autowired static UserRepository repo; will always be null at runtime. Spring's dependency injection works on object instances โ it sets fields on the bean object after construction. Static fields belong to the class, not the object, so Spring never injects into them. Use the setter injection workaround shown above, or better yet, redesign to avoid static Spring beans entirely.
- static = belongs to class, not object โ one copy in Metaspace
- Static block runs once, at class load time, before constructor and before main()
- Multiple static blocks run in top-to-bottom order
- Static methods cannot use
this,super, or access instance members directly main()is static because JVM calls it before any object exists- Static methods cannot be overridden โ only hidden (method hiding)
- Static variables are not thread-safe by default
- Best use of static:
static finalconstants, utility methods, loggers
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Can a static method access instance variables? | Not directly | Only via an explicit object reference โ no implicit this |
| How many copies of a static variable exist? | One | Shared across all objects โ one copy in Metaspace |
| When does a static block run? | At class load time | Before constructor AND before main() โ surprises many |
| Can static methods be overridden? | No | They are hidden, not overridden โ compile-time resolution |
Can we use this in a static method? | No | Compile error โ this is undefined in static context |
| Where are static variables stored? | Metaspace | Not Heap, not Stack โ method area / Metaspace |
| Can an interface have static methods? | Yes (Java 8+) | Added in JDK 8 โ covered in Topic 26 |
๐ One-Liner Definitions
- static variable โ Class-level variable with one shared copy in Metaspace; exists without any object.
- static method โ Method callable without an object; cannot access instance members directly.
- static block โ Initializer block that runs exactly once when the class is first loaded, before any constructor.
- static final โ Java's idiom for a constant: one shared copy that can never be reassigned.
- Method hiding โ When a subclass defines a static method with the same signature as a parent's static method; resolved at compile time, not runtime.
- Static import โ Allows using static members without the class name prefix:
import static java.lang.Math.*;
Encapsulation
๐งฑ What is Encapsulation?
Encapsulation is the OOP principle of binding data (fields) and the methods that operate on that data together into a single unit (class), while restricting direct access to the internal state from outside the class.
Think of a medicine capsule: the active ingredients (data) are sealed inside the capsule shell (class). You consume the capsule โ you don't directly touch or mix the ingredients. The capsule controls how its contents are delivered.
private).2. Controlled Access โ expose fields only through well-defined
public getter and setter methods that can validate input and enforce rules.
๐ How to Achieve Encapsulation in Java
The recipe is always the same three steps:
๐ฆ Real-World Analogy โ Bank Account
A bank account is the classic encapsulation example. Your account balance is private โ you cannot walk into the bank and directly change it. All access goes through controlled methods: deposit(), withdraw(), getBalance(). The bank enforces rules (no negative balance, daily limits) inside those methods.
๐ Encapsulation vs Abstraction โ The Critical Difference
| Aspect | Encapsulation | Abstraction |
|---|---|---|
| Goal | Hide the data | Hide the implementation complexity |
| What is hidden | Internal state (fields) | Internal logic (how it works) |
| How achieved | private fields + getters/setters | Abstract classes, Interfaces |
| Focus | Data protection & control | Simplifying usage |
| Level | Class level | Design level |
| Analogy | ATM hides your balance as private | ATM hides the banking software internals |
โ Benefits of Encapsulation
- Data Integrity โ setters validate input before changing state, preventing invalid data
- Flexibility โ internal implementation can change without affecting external code
- Maintainability โ changes are localized inside the class
- Reusability โ well-encapsulated classes are easier to reuse safely
- Testability โ controlled access makes unit testing predictable
- Security โ sensitive data (passwords, PINs) stays hidden
๐ Getter and Setter Naming Convention
๐ป Program 1 โ Fully Encapsulated BankAccount
public class BankAccount { // Step 1: private fields โ no direct outside access private String owner; private double balance; private int pin; // Constructor public BankAccount(String owner, double initialDeposit, int pin) { this.owner = owner; this.balance = initialDeposit; this.pin = pin; } // Step 2: getter โ read-only for balance public String getOwner() { return owner; } public double getBalance() { return balance; } // No getter for pin โ completely hidden! // Step 3: controlled operations with validation public void deposit(double amount) { if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive"); balance += amount; System.out.printf("Deposited โน%.2f | New balance: โน%.2f%n", amount, balance); } public void withdraw(double amount, int enteredPin) { if (enteredPin != pin) { System.out.println("โ Wrong PIN!"); return; } if (amount > balance) { System.out.println("โ Insufficient funds!"); return; } balance -= amount; System.out.printf("Withdrew โน%.2f | New balance: โน%.2f%n", amount, balance); } public static void main(String[] args) { BankAccount acc = new BankAccount("Rahul", 5000.0, 1234); // acc.balance = 99999; // โ compile error โ private! // acc.pin = 0000; // โ compile error โ private! acc.deposit(1500.0); acc.withdraw(2000.0, 9999); // wrong PIN acc.withdraw(2000.0, 1234); // correct PIN acc.withdraw(9000.0, 1234); // insufficient funds System.out.println("Final balance: โน" + acc.getBalance()); } }
๐ป Program 2 โ Student with Validated Setters
public class Student { private String name; private int age; private double marks; private boolean passed; // derived โ no setter needed // Getters public String getName() { return name; } public int getAge() { return age; } public double getMarks() { return marks; } public boolean isPassed() { return passed; } // "is" for boolean // Setters with validation public void setName(String name) { if (name == null || name.isBlank()) throw new IllegalArgumentException("Name cannot be blank"); this.name = name.trim(); } public void setAge(int age) { if (age < 1 || age > 120) throw new IllegalArgumentException("Age out of range: " + age); this.age = age; } public void setMarks(double marks) { if (marks < 0 || marks > 100) throw new IllegalArgumentException("Marks must be 0-100"); this.marks = marks; this.passed = marks >= 35; // auto-derived field } public static void main(String[] args) { Student s = new Student(); s.setName("Rahul"); s.setAge(20); s.setMarks(72.5); System.out.printf("%s | Age: %d | Marks: %.1f | Passed: %b%n", s.getName(), s.getAge(), s.getMarks(), s.isPassed()); try { s.setAge(-5); // triggers validation } catch (IllegalArgumentException e) { System.out.println("โ Error: " + e.getMessage()); } } }
๐ป Program 3 โ Read-Only and Write-Only Fields
public class AccessControl { private final String id; // READ-ONLY: getter only, no setter private String password; // WRITE-ONLY: setter only, no getter private String email; // READ-WRITE: both getter and setter public AccessControl(String id, String password, String email) { this.id = id; this.password = password; this.email = email; } // READ-ONLY โ getter, no setter public String getId() { return id; } // WRITE-ONLY โ setter, no getter (password stays hidden) public void setPassword(String newPwd) { if (newPwd.length() < 8) throw new IllegalArgumentException("Password too short"); this.password = newPwd; // stored as-is (use BCrypt in real apps) } // READ-WRITE โ both public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public static void main(String[] args) { AccessControl user = new AccessControl("USR001", "secret99", "r@mail.com"); System.out.println("ID: " + user.getId()); System.out.println("Email: " + user.getEmail()); // user.getId() works; user.id = "X" โ compile error // user.password โ compile error (write-only) user.setPassword("newSecure123"); System.out.println("Password updated successfully"); } }
Alt+Insert)In Eclipse: Right-click โ Source โ Generate Getters and Setters
In VS Code: install the Java Extension Pack โ same right-click option appears.
Always review generated code โ IDEs don't add validation logic.
ArrayList), returning it directly from a getter exposes the internal reference โ callers can modify it without going through setters:public List<String> getTags() { return tags; } โ caller can call getTags().clear()!Fix: return a defensive copy:
return new ArrayList<>(tags); or return an unmodifiable view: return Collections.unmodifiableList(tags);
Real-world example: A TV remote. You press buttons (public methods) to change the channel or volume. You cannot directly access or modify the internal circuit board (private fields). The remote's internals are encapsulated โ you interact only through the defined interface (buttons).
In Java: make fields
private, expose via public getters and setters.
Abstraction hides implementation complexity โ it exposes only a clean interface, hiding how things work internally. It answers: "What should the user see?"
Simple memory trick: Encapsulation = data hiding (field level). Abstraction = implementation hiding (design level). A class can implement both simultaneously โ private fields (encapsulation) + interface-based design (abstraction).
public, any external code can set them to invalid values directly โ student.age = -500; student.marks = 999; โ and there is nothing the class can do to stop it. This breaks data integrity.Making fields
private forces all access through methods, which can validate input, enforce business rules, trigger side effects (like logging or updating derived fields), and change the internal implementation without breaking external code.
get + field name with first letter capitalized. E.g., firstName โ getFirstName()Boolean getter:
is + field name. E.g., active โ isActive()Setter:
set + field name with first letter capitalized. E.g., firstName โ setFirstName(String fn)This convention matters because Spring, Hibernate, and Jackson use reflection to automatically discover and call getters/setters based on these naming patterns. Breaking the convention breaks framework auto-wiring, JSON serialization, and JPA column mapping.
Read-only field: provide a getter but no setter. Use
private final + constructor initialization for true immutability. Example: auto-generated IDs, creation timestamps.Write-only field: provide a setter but no getter. Used for sensitive data like passwords, PINs, or API keys โ external code can set them but can never read them back (they are only used internally for comparison).
Problem:
public List<String> getItems() { return items; } โ caller gets the real internal list and can call getItems().clear(), bypassing all encapsulation.Fix:
return new ArrayList<>(items); โ caller gets a copy. Changes to the copy don't affect the original.This is especially important for arrays, collections, and
Date objects.
1. Declare class as
final (prevent subclassing)2. All fields
private final3. No setters โ only a constructor to set values
4. Return defensive copies from getters for mutable fields
5. Ensure no method modifies state
Java's
String, Integer, LocalDate are all immutable. Immutable objects are inherently thread-safe โ no synchronization needed.
๐ฑ Encapsulation in Spring Boot
Spring Boot relies heavily on encapsulation. Every layer โ Entity, DTO, Service, Controller โ is a well-encapsulated class. Jackson (JSON library), JPA (database), and Spring DI all depend on the JavaBean convention of private fields + public getters/setters.
DTO (Data Transfer Object) โ encapsulates only the fields needed for API request/response, hiding internal entity fields.
@ConfigurationProperties โ encapsulates config values as a typed, validated class.
Service layer โ encapsulates business logic, hides repository details from controllers.
// โโ Entity โ private fields, full getter/setter for JPA โโโโโ @Entity public class User { @Id @GeneratedValue private Long id; private String name; private String email; private String passwordHash; // never exposed to API public Long getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } public void setName(String n) { this.name = n; } public void setEmail(String e) { this.email = e; } // No getter for passwordHash โ encapsulated completely } // โโ DTO โ only expose what the API consumer should see โโโโโโ public class UserResponseDTO { private Long id; private String name; private String email; // passwordHash intentionally NOT included public UserResponseDTO(User user) { this.id = user.getId(); this.name = user.getName(); this.email = user.getEmail(); } public Long getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } }
@Getter @Setter โ generates all getters and setters@Data โ generates getters, setters, equals, hashCode, toString@Value โ generates an immutable class (all fields private final, no setters)@Builder โ generates a builder patternLombok does not skip encapsulation โ it just removes the verbosity of writing it manually.
- Encapsulation = data hiding via private fields + public getters/setters
- Achieved by:
privatefields +publicgetters/setters - Getter for boolean:
isFieldName()notgetFieldName() - Read-only = getter only | Write-only = setter only
- Encapsulation hides data; Abstraction hides implementation
- Encapsulation enables validation in setters โ impossible with public fields
- Immutable class = strongest form of encapsulation
- Frameworks (Spring, Jackson, JPA) rely on JavaBean naming convention
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Which modifier achieves data hiding? | private | Not protected โ protected still allows subclass and package access |
Getter for boolean active? | isActive() | Not getActive() โ boolean fields use is prefix by JavaBean convention |
| Which pillar does private fields + getters/setters implement? | Encapsulation | Not Abstraction โ encapsulation is about data hiding at field level |
| Can a class have a field with no getter AND no setter? | Yes | A completely hidden field used only internally by the class is valid |
| What is an immutable class? | No state change after creation | Requires private final fields, no setters, and defensive copies for mutable fields |
Is String encapsulated in Java? | Yes โ and immutable | All String internals are private; you can only use its public methods |
๐ One-Liner Definitions
- Encapsulation โ Bundling data and methods in a class while restricting direct field access using
private. - Getter โ A public method that returns the value of a private field; named
getField()orisField()for booleans. - Setter โ A public method that sets the value of a private field, optionally with validation; named
setField(). - Data Hiding โ Preventing external code from directly accessing or modifying internal state.
- Defensive Copy โ A fresh copy of a mutable object returned from a getter to prevent external mutation of internal state.
- Immutable Class โ A class whose instances cannot change state after construction; the strongest form of encapsulation.
- JavaBean Convention โ Standard naming rules for getters/setters used by frameworks:
get/set/is + CapitalizedFieldName.
this Keyword
๐ What is this?
this is a reference variable that refers to the current object โ the object whose method or constructor is currently executing. It is implicitly available in every instance method and constructor. It is not available in static methods (no current object exists there).
s1.printInfo(), the JVM secretly passes s1 as a hidden first argument into the method. Inside the method, that hidden reference is named this. This is how every instance method knows which object's data to operate on.
๐ Four Uses of this
Use 1 โ Resolve Name Conflict (Instance Variable vs Parameter)
The most common use. When a constructor or method parameter has the same name as an instance variable, this.varName refers to the instance variable, and plain varName refers to the parameter.
Use 2 โ Constructor Chaining with this()
You can call one constructor of the same class from another using this(args). This avoids duplicating initialization code across multiple constructors.
Use 3 โ Pass Current Object as Argument
You can pass this to another method or constructor that expects the current object type. Useful for event-handling, callbacks, and builder patterns.
Use 4 โ Return Current Object (Method Chaining / Builder)
When a method returns this, the caller can chain multiple method calls on the same object in a single expression โ this is the foundation of the Builder Pattern and fluent APIs like StringBuilder.
โ ๏ธ Where this Cannot Be Used
- Static methods โ compile error. No current object exists in a static context.
- Static blocks โ same reason. No object is being constructed.
- Outside a class โ meaningless without a class context.
๐ Summary of this Uses
| Use | Syntax | Purpose |
|---|---|---|
| Resolve name conflict | this.fieldName | Distinguish instance var from local/param with same name |
| Constructor chaining | this(args) | Call another constructor of same class โ must be first line |
| Pass current object | methodCall(this) | Send current object to another method as argument |
| Return current object | return this; | Enable method chaining / fluent API / Builder pattern |
๐ป Program 1 โ this to Resolve Name Conflict
public class Student { String name; int age; double marks; // Without this โ name conflict bug Student(String name, int age, double marks) { this.name = name; // instance var = parameter this.age = age; this.marks = marks; } void display() { // this. optional here (no conflict) but makes intent clear System.out.printf("Name: %s | Age: %d | Marks: %.1f%n", this.name, this.age, this.marks); } public static void main(String[] args) { new Student("Rahul", 20, 88.5).display(); new Student("Priya", 22, 94.0).display(); } }
๐ป Program 2 โ Constructor Chaining with this()
public class Box { int length, width, height; // Master constructor โ does all the real work Box(int length, int width, int height) { this.length = length; this.width = width; this.height = height; System.out.println("Box("+length+","+width+","+height+") created"); } // Cube shortcut โ delegates to master Box(int side) { this(side, side, side); // MUST be first line System.out.println("Cube shortcut used"); } // Default 1x1x1 box Box() { this(1); // chains to Box(int side) System.out.println("Default box used"); } int volume() { return length * width * height; } public static void main(String[] args) { System.out.println("--- Custom Box ---"); Box b1 = new Box(3, 4, 5); System.out.println("Volume: " + b1.volume()); System.out.println("--- Cube ---"); Box b2 = new Box(4); System.out.println("Volume: " + b2.volume()); System.out.println("--- Default ---"); Box b3 = new Box(); System.out.println("Volume: " + b3.volume()); } }
๐ป Program 3 โ Passing this as Argument
class Validator { boolean isValid(BankAccount acc) { return acc.balance >= 0 && acc.owner != null; } } class BankAccount { String owner; double balance; BankAccount(String owner, double balance) { this.owner = owner; this.balance = balance; } void validate() { Validator v = new Validator(); boolean ok = v.isValid(this); // pass current object System.out.println(owner + " account valid: " + ok); } } public class PassThisDemo { public static void main(String[] args) { new BankAccount("Rahul", 5000).validate(); new BankAccount(null, -100).validate(); } }
๐ป Program 4 โ Return this for Fluent Builder
public class QueryBuilder { String table, condition, orderBy; int limit = 10; QueryBuilder from(String table) { this.table = table; return this; } QueryBuilder where(String condition) { this.condition = condition; return this; } QueryBuilder orderBy(String col) { this.orderBy = col; return this; } QueryBuilder limit(int n) { this.limit = n; return this; } String build() { return "SELECT * FROM " + table + " WHERE " + condition + " ORDER BY " + orderBy + " LIMIT " + limit; } public static void main(String[] args) { String query = new QueryBuilder() .from("users") .where("active = true") .orderBy("name") .limit(5) .build(); System.out.println(query); } }
this(args) in a constructor, you get a compile error: "Constructor call must be the first statement in a constructor". The same rule applies to super(). And you cannot have both this() and super() as the first line โ only one is allowed.
this.field = field. This makes the mapping crystal clear, is the universal Java convention, and is what IDEs generate automatically. Never use misleading names like setName(String n) when you could use setName(String name).
this is a reference to the current object inside an instance method or constructor. Its four uses:1. Resolve name conflict:
this.name = name โ distinguish instance variable from parameter2. Constructor chaining:
this(args) โ call another constructor of same class (must be first line)3. Pass current object:
method(this) โ send current object as argument to another method4. Return current object:
return this โ enable method chaining / fluent APIs
this(args).Rules:
1.
this() must be the first statement in the constructor body2. You cannot have both
this() and super() as the first line โ only one3. Cannot create circular chains (A calls B, B calls A) โ infinite loop detected at compile time
4. The chain must eventually reach a constructor that does NOT call another constructor
this refers to the current object instance. Static methods belong to the class and execute without any object context โ there is no "current object" when a static method runs. Writing this inside a static method causes: "Cannot use this in a static context".
this(): calls another constructor of the same class. Used for constructor overloading / chaining within one class.super(): calls a constructor of the parent class. Used to initialize the parent part of the object. If you don't write it explicitly, the compiler inserts super() automatically as the first line of every constructor.Both must be the first line โ so they are mutually exclusive in any single constructor.
It works by having each setter-style method assign a value and
return this โ returning the same object each time so the next method call operates on the same instance. The chain ends with a build() or terminal method.This pattern is heavily used in Spring Boot โ
ResponseEntity.ok().header(...).body(...), MockMvcRequestBuilders.get(...).param(...).accept(...), and Lombok's @Builder annotation all use it.
name = name simply assigns the parameter back to itself โ the instance variable is never touched and remains null (for String) or 0 (for int). No error is thrown โ this is one of the most common beginner bugs in Java.
this never creates a new object. It is simply a reference (alias) pointing to the already-existing current object. When you write return this, you're returning a reference to the existing object โ not a copy, not a new object. This is what makes method chaining memory-efficient: the same Heap object is reused across the entire chain.
๐ฑ this in Spring Boot
The this keyword appears constantly in Spring Boot โ in constructors for dependency injection, in builder-style response construction, and in entity design. Understanding it well makes Spring code more natural to read and write.
this.repo = repo in every Spring service/controllerResponseEntity builder โ Spring's fluent HTTP response builder uses
return this internally@Builder (Lombok) โ auto-generates Builder pattern with
return this for entity classesthis() in entity constructors โ chain overloaded constructors to avoid duplicating JPA logic
// โโ Constructor injection โ this.field = field โโโโโโโโโโโโโโ @RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; // classic this use } @GetMapping("/users/{id}") public ResponseEntity<User> getUser(@PathVariable Long id) { // ResponseEntity uses return this internally โ fluent builder return ResponseEntity.ok(userService.findById(id)); } } // โโ Lombok @Builder โ auto-generates return this setters โโโโ @Entity @Builder // Lombok generates full Builder class @NoArgsConstructor @AllArgsConstructor public class User { @Id @GeneratedValue private Long id; private String name; private String email; } // Usage โ Lombok Builder (uses this internally): // User u = User.builder().name("Rahul").email("r@x.com").build();
this= reference to the current object inside instance methods and constructorsthisis not available in static methods โ compile errorthis(args)= constructor chaining within same class โ must be first linethis()andsuper()are mutually exclusive โ only one can be first linename = namewithoutthisis a silent bug โ parameter assigns to itselfreturn thisenables method chaining / Builder patternthisnever creates a new object โ it references the existing one
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Can this be used in a static method? | No โ compile error | No current object in static context |
| Where must this() appear in a constructor? | First line | Any statement before it causes compile error |
| Can both this() and super() be in one constructor? | No | Both must be first line โ mutually exclusive |
| What does return this; do? | Returns current object | Not a copy โ the same Heap object reference |
| What is name = name without this? | Silent bug | Parameter assigned to itself; instance var stays default |
| Does this() call the parent constructor? | No | this() = same class. super() = parent class constructor |
๐ One-Liner Definitions
- this โ A reference to the current object inside an instance method or constructor.
- this.field โ Accesses the instance variable of the current object, resolving shadowing by local variables.
- this(args) โ Calls another constructor of the same class; must be the first statement.
- Constructor chaining โ Calling one constructor from another to reuse initialization logic.
- Method chaining โ Calling multiple methods on the same object in sequence, enabled by each method returning
this. - Builder Pattern โ A design pattern using
return thisto build complex objects step by step via a fluent API.
Constructors โ Fundamentals & Types
๐๏ธ What is a Constructor?
A constructor is a special method-like block that is automatically called when an object is created using new. Its job is to initialise the object's state โ setting instance variables to meaningful starting values before the object is used.
| Constructor | Method |
|---|---|
| Same name as the class | Any valid identifier name |
| No return type โ not even void | Must have a return type (or void) |
Called automatically on new | Called explicitly by the programmer |
| Cannot be called again after object creation | Can be called any number of times |
| Cannot be inherited (but can be chained via super) | Can be inherited and overridden |
| Cannot be static, abstract, or final | Can be any modifier |
1๏ธโฃ Default Constructor
If you write no constructor at all in your class, the Java compiler automatically inserts a default constructor โ a no-argument constructor with an empty body. It simply calls super() and lets instance variables take their default values (0, false, null).
2๏ธโฃ Parameterized Constructor
A constructor that accepts arguments, allowing each object to be initialised with specific values at creation time. This is the most commonly used constructor type.
3๏ธโฃ Constructor Overloading
Just like method overloading, you can define multiple constructors in one class as long as each has a different parameter list (different number, types, or order of parameters). The JVM calls the matching one based on the arguments provided at the call site.
๐ Object Creation โ Full Sequence
Class.newInstance() will throw an exception. Best practice: always explicitly add a no-arg constructor when you also define parameterized ones.
๐ป Program 1 โ Default vs Parameterized Constructor
public class Car { String brand; String color; int year; // 1. No-arg constructor โ explicit (needed alongside parameterized) Car() { this.brand = "Unknown"; this.color = "White"; this.year = 2020; System.out.println("No-arg constructor called"); } // 2. Parameterized constructor Car(String brand, String color, int year) { this.brand = brand; this.color = color; this.year = year; System.out.println("Parameterized constructor called"); } void display() { System.out.printf(" %s | %s | %d%n", brand, color, year); } public static void main(String[] args) { Car c1 = new Car(); // no-arg c1.display(); Car c2 = new Car("Toyota", "Red", 2023); // parameterized c2.display(); } }
๐ป Program 2 โ Constructor Overloading with this() Chaining
public class Product { String name; double price; int stock; String category; // Master constructor โ all four fields Product(String name, double price, int stock, String category) { this.name = name; this.price = price; this.stock = stock; this.category = category; } // No category โ delegates, uses "General" as default Product(String name, double price, int stock) { this(name, price, stock, "General"); } // Name + price only โ stock defaults to 0 Product(String name, double price) { this(name, price, 0); } // Name only โ placeholder product Product(String name) { this(name, 0.0); } void display() { System.out.printf("%-12s | โน%-8.2f | stock:%-4d | %s%n", name, price, stock, category); } public static void main(String[] args) { new Product("Laptop", 75000, 10, "Electronics").display(); new Product("Pen", 15, 500).display(); new Product("Notebook", 120).display(); new Product("TBD").display(); } }
๐ป Program 3 โ Proving Constructor Has No Return Type
public class Demo { // TRUE constructor โ no return type, auto-called on new Demo() { System.out.println("Constructor: Demo()"); } // This looks like a constructor but has void โ it is a METHOD! // Java allows methods with same name as class (confusing but legal) void Demo() { // โ this is a regular method, NOT a constructor System.out.println("Method: void Demo() โ I am a method, not a constructor!"); } public static void main(String[] args) { Demo d = new Demo(); // only constructor runs d.Demo(); // explicit call runs the method } }
void Demo(), it is a regular method. Adding any return type (including void) strips it of constructor status โ it will never be called automatically on new.
this(args) with appropriate defaults. This way, if initialisation logic changes, you update only one place.
new.Key differences from a method:
1. Constructor has the same name as the class; methods can have any name
2. Constructor has no return type (not even void); methods must have one
3. Constructor is called automatically by the JVM on
new; methods are called explicitly4. Constructor cannot be called again after object creation; methods can be called any number of times
5. Constructor cannot be static, abstract, final, or synchronized
super() and lets all instance variables take their type defaults.The compiler does NOT provide it if you define any constructor โ even a parameterized one. This is the most common pitfall: adding a parameterized constructor silently removes the default constructor, breaking code that uses
new ClassName() and frameworks that rely on no-arg instantiation.
void.If you add any return type, Java treats it as a regular method (method naming is just a naming coincidence). The "constructor" with a return type will not be called on
new โ it becomes an ordinary method that must be called explicitly. This is a classic exam/interview trick question.
new ClassName() from outside the class itself.This is used in two main patterns:
1. Singleton Pattern โ only one object of the class should ever exist. A private constructor ensures no external code creates a second one. A static factory method controls instance creation. (Covered in detail in Topic 11.)
2. Utility classes โ classes with only static methods (like
Math, Collections) use a private constructor to prevent pointless instantiation.
super(args) as the first line.If a subclass constructor does not explicitly call
super(), the compiler automatically inserts a call to the parent's no-arg constructor. If the parent has no no-arg constructor (only parameterized), this causes a compile error in the subclass.
The JVM resolves which constructor to call at compile time, based on the number and types of arguments provided in the
new expression. This is static dispatch โ the same resolution mechanism as method overloading.
2. Memory allocation โ JVM allocates space in Heap for the object; all instance variables set to defaults (0, false, null)
3. Instance initialisers run (if any โ e.g.,
int x = 5; directly in the class body)4. Constructor executes โ
super() is called first (walking up to Object), then the constructor body runs5. Reference returned โ the address of the fully constructed object is assigned to the variable on the Stack
๐ฑ Constructors in Spring Boot
Constructors are central to Spring Boot โ the framework uses them extensively for dependency injection, and constructors on entity/DTO classes are critical for JPA and JSON serialisation.
No-arg constructor for JPA โ Hibernate needs it to instantiate entities via reflection
All-args constructor โ used with Lombok
@AllArgsConstructor for DTO creationLombok @RequiredArgsConstructor โ generates a constructor for all
final fields โ Spring injection without boilerplate
// โโ Constructor injection (recommended over @Autowired field) โโ @Service @RequiredArgsConstructor // Lombok: generates constructor for final fields public class OrderService { private final OrderRepository orderRepo; // injected via constructor private final PaymentService paymentSvc; // injected via constructor // Lombok generates this automatically: // OrderService(OrderRepository orderRepo, PaymentService paymentSvc) { // this.orderRepo = orderRepo; // this.paymentSvc = paymentSvc; // } } // โโ JPA Entity โ must have no-arg constructor โโโโโโโโโโโโโโโโโโ @Entity @NoArgsConstructor // Hibernate needs this @AllArgsConstructor // convenient for creating instances public class Order { @Id @GeneratedValue private Long id; private String product; private int quantity; } // Usage: // Order o = new Order(null, "Laptop", 2); โ @AllArgsConstructor // Order o = new Order(); โ @NoArgsConstructor (Hibernate)
@Autowired private UserRepo repo;) is convenient but has problems: fields are null during unit tests unless you use a Spring context, you can't mark them final, and circular dependencies are harder to spot. Constructor injection solves all three: fields can be final, NullPointerExceptions are impossible (Spring must provide them before creating the bean), and circular dependencies fail fast at startup.
- Constructor name = same as class name, no return type (not even void)
- Default constructor = compiler-provided no-arg constructor, only if NO constructor is written
- Defining any constructor removes the compiler-provided default constructor
- Constructor overloading = multiple constructors with different parameter lists
- Constructor cannot be
static,abstract,final, orsynchronized - Constructor cannot be inherited โ but can be called via
super() - A method with same name as class and a return type is a regular method, not a constructor
- Constructor can be private โ used for Singleton and utility classes
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| What is the return type of a constructor? | No return type | Not void โ no return type at all. void makes it a method. |
| When does compiler add default constructor? | Only if NO constructor defined | One parameterized constructor = compiler removes default |
| Can constructors be inherited? | No | Not inherited โ only called via super() |
| Can constructor be private? | Yes | Used in Singleton pattern and utility classes |
| Can constructor be static? | No | Constructors cannot be static, abstract, or final |
void Demo() inside class Demo is aโฆ? | Regular method | Has a return type (void) โ it's a method, not a constructor |
| Can a class have multiple constructors? | Yes โ overloading | Must differ in parameter count/types, resolved at compile time |
๐ One-Liner Definitions
- Constructor โ Special class member with same name as class, no return type, auto-called on
newto initialise object state. - Default constructor โ Compiler-generated no-arg constructor; only added when the class defines zero constructors.
- Parameterized constructor โ Constructor that accepts arguments to set instance variables at object creation time.
- Constructor overloading โ Multiple constructors in one class with different parameter lists, resolved at compile time.
- Private constructor โ Constructor accessible only within the class; prevents external instantiation; used in Singleton and utility classes.
- super() โ Calls the parent class constructor; implicitly inserted as first line if not written; must be first statement.
Copy Constructor
๐ What is a Copy Constructor?
A copy constructor is a constructor that creates a new object as a duplicate of an existing object of the same class. It takes a reference to an object of its own class as the only parameter and uses it to initialise the new object's fields.
In Java, there is no built-in copy constructor. Java has no pass-by-value for objects and no implicit copy construction. If you want a copy constructor, you must write it yourself. Java's alternative is the
clone() method (from Object), but copy constructors are generally preferred as they are simpler, more readable, and don't require implementing Cloneable.
๐ The Core Problem โ Reference Copy Trap
When you write Student s2 = s1;, you do not create a new object. Both s1 and s2 point to the same Heap object. Changing s2's fields changes s1's fields too โ they are aliases, not independent copies.
๐งช Shallow Copy vs Deep Copy
This distinction is critical when the class contains fields that are themselves objects (reference types).
String is immutable in Java โ once created, its value never changes. So sharing a String reference between two objects is safe: if you "change" a String field, you're actually pointing the reference to a new String object, not modifying the shared one. But mutable fields like int[], ArrayList, or other custom objects MUST be deep-copied; otherwise both objects share and can mutate the same data.
๐ When to Use a Copy Constructor
- When you need an independent duplicate of an object to modify without affecting the original
- When implementing immutable snapshots โ take a copy before passing to untrusted code
- As an alternative to
clone()โ clearer, no checked exception, no need forCloneable - In defensive copying โ return copies from getters to prevent external mutation of internal state
๐ป Program 1 โ Basic Copy Constructor
public class Student { String name; int age; // Regular parameterized constructor Student(String name, int age) { this.name = name; this.age = age; } // Copy constructor โ takes a Student, copies its fields Student(Student other) { this.name = other.name; // String is immutable โ safe this.age = other.age; // primitive โ direct copy } void display(String label) { System.out.printf("%s โ name=%-8s age=%d%n", label, name, age); } public static void main(String[] args) { Student s1 = new Student("Rahul", 20); // โ Reference copy โ same object Student ref = s1; // โ Copy constructor โ new independent object Student s2 = new Student(s1); // Modify s2 and ref s2.name = "Priya"; s2.age = 22; ref.name = "Changed!"; // also changes s1 s1.display("s1 "); // affected by ref change s2.display("s2 "); // independent โ unaffected ref.display("ref"); // same as s1 } }
๐ป Program 2 โ Shallow vs Deep Copy (Array Field)
import java.util.Arrays; public class Gradebook { String student; int[] scores; // mutable reference type โ needs deep copy Gradebook(String student, int[] scores) { this.student = student; this.scores = scores; } // SHALLOW copy constructor โ copies the array REFERENCE Gradebook(Gradebook other, boolean deep) { this.student = other.student; if (deep) { // DEEP copy โ new array with same values this.scores = Arrays.copyOf(other.scores, other.scores.length); } else { // SHALLOW copy โ copies the reference, shares array this.scores = other.scores; } } void display(String label) { System.out.println(label + " " + student + ": " + Arrays.toString(scores)); } public static void main(String[] args) { Gradebook original = new Gradebook("Rahul", new int[]{90,85,92}); Gradebook shallow = new Gradebook(original, false); Gradebook deep = new Gradebook(original, true); // Modify via shallow copy shallow.scores[0] = 99; System.out.println("After shallow.scores[0] = 99:"); original.display("original:"); // AFFECTED โ shared array! shallow.display("shallow: "); // Modify via deep copy deep.scores[0] = 55; System.out.println("After deep.scores[0] = 55:"); original.display("original:"); // UNAFFECTED โ independent array deep.display("deep: "); } }
๐ป Program 3 โ Copy Constructor vs clone()
public class Point { int x, y; Point(int x, int y) { this.x=x; this.y=y; } // โ Copy constructor โ clean, no exceptions, no Cloneable Point(Point other) { this.x = other.x; this.y = other.y; } public static void main(String[] args) { Point p1 = new Point(3, 4); Point p2 = new Point(p1); // copy constructor p2.x = 10; System.out.println("p1: ("+p1.x+","+p1.y+")"); // unchanged System.out.println("p2: ("+p2.x+","+p2.y+")"); // independent System.out.println("Same object? " + (p1 == p2)); // false } }
Cloneable, works well with inheritance, and is easy to add deep-copy logic. Use clone() only when required by a legacy API or when working with arrays (arrays have a built-in .clone() that does a shallow copy).
No โ Java does NOT provide a copy constructor automatically. Unlike C++, Java has no language-level copy constructor. You must write it manually if needed. Java's alternative is
Object.clone(), but copy constructors are generally preferred.
Student s2 = s1): No new object is created. Both s1 and s2 point to the same Heap object. Any change through either variable affects the same object.Copy constructor (
Student s2 = new Student(s1)): A new Heap object is created with field values copied from s1. s1 and s2 are independent โ modifying one does not affect the other (assuming proper deep copy for reference fields).
Deep copy: Recursively copies all reference-type fields too โ creates new independent instances of every nested object. Both original and copy are completely independent at all levels.
String is immutable in Java โ its value can never change after creation. When you "assign" a new value to a String field, you are actually creating a new String object and pointing the reference to it. The shared original String is untouched.So even if both original and copy share the same String reference, neither can corrupt the other's data. This is why shallow copy is safe for classes containing only primitives and Strings โ but unsafe the moment any mutable reference type (array, ArrayList, custom object) is involved.
clone():1. No interface required โ
clone() requires implementing Cloneable; forgetting it throws CloneNotSupportedException2. No checked exception โ
clone() throws CloneNotSupportedException; copy constructor throws nothing3. Type safe โ returns the correct type directly;
clone() returns Object and needs casting4. Explicit and readable โ clear what fields are copied and how
5. Deep copy is straightforward โ just initialise fields manually;
clone()'s deep copy is tricky with inheritance
Two places:
1. In constructors: if a constructor receives a mutable object (e.g.,
int[] or Date), copy it before storing: this.scores = Arrays.copyOf(scores, scores.length) โ otherwise the caller could modify the original array and affect your object.2. In getters: instead of returning the actual internal array, return a copy:
return Arrays.copyOf(this.scores, this.scores.length). This is key to building truly encapsulated or immutable classes.
If your copy constructor does
this.scores = other.scores for an array field, that is shallow โ shared array. If it does this.scores = Arrays.copyOf(other.scores, other.scores.length), that is deep for that level. For nested custom objects, you must recursively copy each one. The developer is fully responsible for the depth of copying.
๐ฑ Copy Constructors & Defensive Copying in Spring Boot
In Spring Boot, copy constructors and defensive copying appear in two key areas: DTO mapping (converting between Entity and Data Transfer Object) and immutable value objects to prevent shared mutable state between service layers.
DTO โ Entity โ copy fields from incoming request DTO into a managed entity before saving
Defensive copy in @Value objects โ prevent callers from mutating internal state of domain objects
ModelMapper / MapStruct โ library-level copy constructors that auto-map fields by name
// JPA Entity โ internal, has DB-specific fields @Entity public class User { @Id private Long id; private String name; private String email; private String passwordHash; // must NOT expose in API! } // DTO โ safe to expose, no sensitive fields public class UserDTO { private Long id; private String name; private String email; // "Copy constructor" from Entity โ DTO public UserDTO(User user) { this.id = user.getId(); this.name = user.getName(); this.email = user.getEmail(); // passwordHash intentionally excluded } } // In service layer: // User entity = repo.findById(id); // return new UserDTO(entity); โ safe copy for API response
@Value annotation creates an immutable class (all fields private final, no setters, all-args constructor). Combined with defensive copying in the constructor, it guarantees that the DTO's state can never change after creation โ ideal for read-only API responses and thread-safe value objects.
s2 = s1is a reference copy โ no new object, same Heap address- Copy constructor signature:
ClassName(ClassName other) - Java does NOT provide a copy constructor automatically (unlike C++)
- Shallow copy: primitives duplicated, reference fields shared
- Deep copy: all fields fully independent including nested objects/arrays
- String is safe in shallow copy โ immutable, cannot be mutated
- Arrays and mutable objects need deep copy โ use
Arrays.copyOf() - Copy constructor preferred over
clone()โ noCloneable, no checked exception
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Does Java auto-provide a copy constructor? | No | C++ does; Java does not โ must write manually |
Student s2 = s1; creates a new object? | No | Reference copy only โ same Heap object |
| Is String safe in a shallow copy? | Yes | String is immutable โ "changing" creates a new object |
| Is int[] safe in a shallow copy? | No | Array is mutable โ changes affect both objects |
Does new Student(s1) guarantee deep copy? | No | Depends on how you wrote the copy constructor |
| Advantage of copy constructor over clone()? | No checked exception, no Cloneable, type-safe | clone() requires implementing Cloneable and throws CloneNotSupportedException |
๐ One-Liner Definitions
- Copy constructor โ A constructor that takes an object of the same class and creates a new independent duplicate.
- Reference copy โ Assigning one object variable to another; both point to the same Heap object โ not a new object.
- Shallow copy โ A copy where primitives are duplicated but reference-type fields still point to the same nested objects.
- Deep copy โ A copy where all fields including nested objects and arrays are fully duplicated and independent.
- Defensive copy โ Creating a copy of a mutable object when storing it or returning it, to protect internal state from external mutation.
- Immutable class โ A class whose state cannot change after construction; String is the most common example in Java.
Private Constructor & Singleton Pattern
๐ Private Constructor
Marking a constructor private means no code outside the class can call new ClassName(). This gives the class complete control over how and when its instances are created. It is the foundation of two important patterns: the Singleton and utility classes.
๐๏ธ The Singleton Design Pattern
The Singleton is a creational design pattern that ensures a class has exactly one instance throughout the lifetime of the application, and provides a global access point to that instance.
Real-world analogies: a country has one government, an OS has one file system, an app has one configuration manager, a DB pool has one connection pool.
new from outside2. Private static instance field โ holds the one and only object
3. Public static getInstance() method โ creates the instance on first call, returns it on all subsequent calls
๐ Singleton Variants
Variant 1 โ Eager Initialisation
The instance is created when the class loads โ before anyone calls getInstance(). Simple and thread-safe (class loading is thread-safe in the JVM), but wastes resources if the singleton is never used.
Variant 2 โ Lazy Initialisation
The instance is created only on the first call to getInstance(). Saves resources if the singleton might never be needed, but the basic version is not thread-safe.
Variant 3 โ Thread-Safe Lazy (Double-Checked Locking)
The production-ready lazy singleton. Uses synchronized and volatile to guarantee exactly one instance even under concurrent access.
Variant 4 โ Bill Pugh (Best Practice)
The cleanest, most elegant solution. Uses a static inner helper class. The inner class is not loaded until getInstance() is first called (lazy), and class loading is thread-safe (no synchronization needed).
๐ซ Singleton Killers โ What Can Break It
| Threat | How It Breaks Singleton | Defence |
|---|---|---|
| Reflection | Constructor.setAccessible(true) bypasses private | Throw exception in constructor if instance already exists |
| Serialisation | Deserialising creates a new object | Implement readResolve() returning INSTANCE |
| Cloning | clone() creates a new object | Override clone() to throw CloneNotSupportedException |
| Multiple ClassLoaders | Each classloader has its own class โ separate instances | Use enum singleton (immune to all three above) |
Variant 5 โ Enum Singleton (Most Robust)
๐ป Program 1 โ Eager Singleton: Proving One Instance
class DatabaseConnection { // 1. Private static instance โ created at class load time private static final DatabaseConnection INSTANCE = new DatabaseConnection(); // 2. Private constructor โ no one outside can call new private DatabaseConnection() { System.out.println("DB connection created (once!)"); } // 3. Public static access point public static DatabaseConnection getInstance() { return INSTANCE; } public void query(String sql) { System.out.println("Executing: " + sql); } } public class EagerSingleton { public static void main(String[] args) { DatabaseConnection d1 = DatabaseConnection.getInstance(); DatabaseConnection d2 = DatabaseConnection.getInstance(); DatabaseConnection d3 = DatabaseConnection.getInstance(); // All three point to the same object System.out.println("d1 == d2: " + (d1 == d2)); System.out.println("d2 == d3: " + (d2 == d3)); System.out.println("hashCode d1: " + d1.hashCode()); System.out.println("hashCode d2: " + d2.hashCode()); d1.query("SELECT * FROM users"); } }
๐ป Program 2 โ Bill Pugh Singleton (Production Best Practice)
class AppConfig { private String dbUrl; private int maxConnections; // Private constructor โ loads config private AppConfig() { // Simulate loading from properties file this.dbUrl = "jdbc:mysql://localhost:3306/mydb"; this.maxConnections = 10; System.out.println("AppConfig loaded"); } // Inner Holder class โ loaded ONLY when getInstance() is first called private static class Holder { static final AppConfig INSTANCE = new AppConfig(); } public static AppConfig getInstance() { return Holder.INSTANCE; // triggers Holder class load on first call } public void printConfig() { System.out.println("DB: " + dbUrl + " | MaxConn: " + maxConnections); } } public class BillPughSingleton { public static void main(String[] args) { System.out.println("Before getInstance()"); AppConfig cfg1 = AppConfig.getInstance(); // triggers creation AppConfig cfg2 = AppConfig.getInstance(); // returns same cfg1.printConfig(); System.out.println("Same instance: " + (cfg1 == cfg2)); } }
๐ป Program 3 โ Utility Class with Private Constructor
public class MathUtils { // Private constructor โ prevents instantiation of utility class private MathUtils() { throw new UnsupportedOperationException( "MathUtils is a utility class โ do not instantiate!"); } public static int factorial(int n) { if (n <= 1) return 1; return n * factorial(n - 1); } public static boolean isPrime(int n) { if (n < 2) return false; for (int i = 2; i * i <= n; i++) if (n % i == 0) return false; return true; } public static void main(String[] args) { // Use directly via class name โ no object needed System.out.println("5! = " + MathUtils.factorial(5)); System.out.println("17 prime? " + MathUtils.isPrime(17)); System.out.println("18 prime? " + MathUtils.isPrime(18)); // new MathUtils(); โ compile-time accessible but throws at runtime } }
if (instance == null) instance = new X()) in multi-threaded code. Two threads can both see null simultaneously and each create an instance. Use the Bill Pugh idiom or eager initialisation for production code. Double-checked locking also works but requires both synchronized and volatile to be correct.
Bill Pugh โ best default choice: lazy, thread-safe, no overhead.
Enum โ use when you also need protection against reflection/serialisation attacks.
Double-checked locking โ valid but verbose; prefer Bill Pugh instead.
Three components:
1. Private constructor โ prevents
new ClassName() from outside2. Private static field of the same type โ holds the single instance
3. Public static
getInstance() โ creates on first call, returns the same instance on all subsequent calls
getInstance() is ever called. Simple and inherently thread-safe (JVM class loading is synchronised). Downside: creates the object even if it's never used.Lazy: Instance created only on the first call to
getInstance(). Saves resources if the singleton might never be needed. Downside: basic lazy implementation is not thread-safe โ needs extra care (volatile + synchronized or Bill Pugh idiom).
1. Eager initialisation โ thread-safe by default (class loading is atomic)
2. Bill Pugh (static inner class) โ lazy + thread-safe without any synchronisation overhead. Best choice for most cases.
3. Double-checked locking โ use
volatile on the field and synchronized on the creation block. The volatile keyword is mandatory โ without it, a partially-constructed object can be visible to another thread due to instruction reordering.
Constructor.setAccessible(true) can bypass the private constructor and create a second instance. Defence: throw an exception in the constructor if an instance already exists.Serialisation: Deserialising a singleton creates a new object. Defence: implement
readResolve() returning INSTANCE.Cloning:
clone() creates a new object. Defence: override clone() to throw CloneNotSupportedException.Enum singleton is the only approach immune to all three โ the JVM guarantees enum constants are never duplicated, even via reflection or serialisation.
enum Config { INSTANCE; }) is immune to all Singleton killers:โ Reflection cannot create a second enum constant (JVM enforces this)
โ Serialisation is handled correctly by default for enums
โ Cloning is not allowed for enums
โ Thread-safe by the JVM
โ Lazily initialised (enum class loaded only when first referenced)
Downside: enums cannot extend another class (Java's single-inheritance constraint), so if the singleton needs to extend a base class, you must use another variant.
Difference: a Java Singleton uses a private constructor to guarantee one instance per JVM. Spring's singleton scope means one instance per Spring ApplicationContext โ the class itself can still be instantiated with
new. If you create a second ApplicationContext, you get a second bean instance. Spring's singleton is a container-level concept; Java's Singleton is a language/class-level concept.
1. Unit testing: Singletons carry state between tests, making tests order-dependent and brittle. They cannot easily be mocked or replaced with a test double.
2. High concurrency: If the singleton holds mutable state, every thread contending on it creates a bottleneck.
3. Multiple independent contexts: If you need separate instances for different users, tenants, or subsystems, Singleton is the wrong pattern.
Prefer dependency injection (Spring's IoC container) over manual Singletons in production code โ it gives you singleton behaviour with testability.
๐ฑ Singleton in Spring Boot
Spring Boot is built around the Singleton concept. Every @Component, @Service, @Repository, and @Controller bean is a singleton by default within the ApplicationContext โ Spring manages the instance and injects it wherever needed.
Spring Singleton scope โ one instance per ApplicationContext; the class still has a public constructor (Spring uses it to create the one bean); multiple ApplicationContexts = multiple instances
// Spring manages ONE instance of this bean (singleton scope default) @Service public class ConfigService { private final String dbUrl; public ConfigService(@Value("${spring.datasource.url}") String dbUrl) { this.dbUrl = dbUrl; System.out.println("ConfigService created once by Spring"); } public String getDbUrl() { return dbUrl; } } // Manual Java Singleton โ needed when outside Spring context // e.g. in a non-Spring utility or library code public class LegacyConfig { private static class Holder { static final LegacyConfig INSTANCE = new LegacyConfig(); } private LegacyConfig() {} public static LegacyConfig getInstance() { return Holder.INSTANCE; } }
- Singleton = one instance per application + global access point
- Three components: private constructor + private static field + public static getInstance()
- Eager singleton โ created at class load time โ thread-safe, no lazy
- Lazy singleton (basic) โ NOT thread-safe without extra measures
- Bill Pugh โ best default: lazy + thread-safe via static inner class
- Enum singleton โ immune to reflection, serialisation, cloning
- Double-checked locking needs both
volatileANDsynchronized - Spring beans are singleton by default โ but it is container-managed, not class-level
- Private constructor also used in utility classes (all static methods)
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Is eager singleton thread-safe? | Yes | JVM class loading is atomic โ instance created safely |
| Is basic lazy singleton thread-safe? | No | Two threads can both see null and create separate instances |
| Can reflection break an eager singleton? | Yes | setAccessible(true) bypasses private โ must guard in constructor |
| Which singleton variant is immune to reflection? | Enum singleton | JVM prevents creating new enum instances via reflection |
| Is Spring @Service singleton the same as Java Singleton? | No | Spring = one per ApplicationContext; Java = one per JVM via private constructor |
| Why volatile in double-checked locking? | Prevents instruction reordering | Without volatile, partially-constructed object may be visible to other threads |
๐ One-Liner Definitions
- Singleton pattern โ Design pattern ensuring a class has exactly one instance with a global access point.
- Eager singleton โ Instance created at class load time; thread-safe but not lazy.
- Lazy singleton โ Instance created on first
getInstance()call; saves resources but needs thread-safety measures. - Bill Pugh singleton โ Uses a static inner holder class for lazy, thread-safe singleton without synchronisation overhead.
- Enum singleton โ Singleton implemented as an enum constant; immune to reflection, serialisation, and cloning.
- Double-checked locking โ Lazy singleton using two null checks and synchronized + volatile to safely create one instance in multithreaded code.
- Utility class โ Class with only static methods and a private constructor to prevent instantiation (e.g.,
Math,Collections).
Naming Conventions
๐ Why Naming Conventions Matter
Java naming conventions are not enforced by the compiler โ your code compiles fine if you break them. But they are universally followed in the Java ecosystem because they make code instantly readable to any Java developer, enable IDE tooling to work better, and are required for frameworks like Spring and Hibernate that rely on naming patterns via reflection.
A convention is a community agreement โ violating it compiles fine but makes you a bad teammate. Java's naming conventions come from Sun Microsystems' original Java Code Conventions document and are followed by virtually all Java teams worldwide.
๐ก The Four Casing Styles
๐ Complete Naming Rules by Identifier Type
| Identifier | Convention | Examples |
|---|---|---|
| Class | PascalCase โ noun or noun phrase | Student, OrderService, HttpClient |
| Interface | PascalCase โ adjective or noun | Runnable, Serializable, Comparable |
| Method | camelCase โ verb or verb phrase | getName(), calculateTotal(), isValid() |
| Variable (instance/local) | camelCase โ noun | firstName, totalAmount, studentList |
| Parameter | camelCase โ same as variable | userId, orderDate, maxRetries |
Constant (static final) | ALL_CAPS with underscores | MAX_SIZE, PI, DEFAULT_TIMEOUT |
| Package | all lowercase, no underscores | com.example.service, java.util |
| Enum type | PascalCase (like a class) | DayOfWeek, OrderStatus |
| Enum constant | ALL_CAPS | MONDAY, PENDING, HTTP_OK |
| Generic type parameter | Single uppercase letter | T, E, K, V, N |
| Annotation | PascalCase | @Override, @NotNull, @SpringBootTest |
๐ค Specific Naming Patterns
โ Common Naming Mistakes
i, j, k), generic type parameters (T, E), and lambda parameters in very short expressions. Everything else should be descriptive: totalOrderAmount is better than t, tot, or total.
๐ป Program 1 โ Conventions Applied: Well-Named Class
package com.example.banking; // lowercase package public class BankAccount { // PascalCase class // ALL_CAPS constants public static final double MIN_BALANCE = 500.0; public static final int MAX_TRANSACTIONS = 100; // camelCase instance variables private String accountNumber; private String holderName; private double currentBalance; private boolean isActive; // PascalCase constructor name (matches class) public BankAccount(String accountNumber, String holderName) { this.accountNumber = accountNumber; this.holderName = holderName; this.currentBalance = MIN_BALANCE; this.isActive = true; } // camelCase methods โ verb phrases public void depositAmount(double amount) { // camelCase param if (amount > 0) currentBalance += amount; } public boolean withdrawAmount(double amount) { if (currentBalance - amount >= MIN_BALANCE) { currentBalance -= amount; return true; } return false; } // Boolean getter uses "is" prefix public boolean isActive() { return isActive; } // Standard getters โ get + FieldName (PascalCase field name) public String getAccountNumber() { return accountNumber; } public String getHolderName() { return holderName; } public double getCurrentBalance() { return currentBalance; } public static void main(String[] args) { BankAccount account = new BankAccount("ACC001", "Rahul"); account.depositAmount(2000); boolean success = account.withdrawAmount(1000); System.out.println("Holder : " + account.getHolderName()); System.out.println("Balance : " + account.getCurrentBalance()); System.out.println("Active : " + account.isActive()); System.out.println("Withdraw: " + success); System.out.println("MIN_BAL : " + BankAccount.MIN_BALANCE); } }
๐ป Program 2 โ Enum Naming + Generic Type Parameter
// Enum type โ PascalCase | Enum constants โ ALL_CAPS enum OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED } // Generic class โ single uppercase letter T for type parameter class ApiResponse<T> { private T data; private String message; private boolean success; ApiResponse(T data, String message, boolean success) { this.data = data; this.message = message; this.success = success; } public boolean isSuccess() { return success; } // boolean โ is prefix public T getData() { return data; } public String getMessage() { return message; } } public class NamingShowcase { public static void main(String[] args) { OrderStatus currentStatus = OrderStatus.CONFIRMED; System.out.println("Order status: " + currentStatus); ApiResponse<String> response = new ApiResponse<>("user-123", "User found", true); System.out.println("Success : " + response.isSuccess()); System.out.println("Data : " + response.getData()); System.out.println("Message : " + response.getMessage()); } }
active, Spring/Jackson expect isActive() as the getter. Breaking the convention silently breaks serialisation and ORM mapping.
get/set/is methods following the JavaBean convention. Always use this โ never hand-write getters/setters, and definitely do not deviate from the naming pattern they produce.
StudentRecord, OrderServiceMethods & Variables: camelCase (first word lowercase) โ
calculateTotal(), firstNameConstants (
static final): ALL_CAPS with underscores โ MAX_SIZE, PIPackages: all lowercase, dot-separated โ
com.example.service
is as the prefix instead of get: isActive(), isValid(), hasPermission().It matters because frameworks like Jackson (JSON serialiser), Hibernate, and Spring use reflection to find getters. Jackson specifically looks for
isXxx() for boolean fields. If you name it getActive(), Jackson may serialise the field as active via the getter name, causing a mismatch. Also, isActive() reads naturally as a question โ "is this account active?" โ which is better English for a boolean.
Runnable, Comparable, Serializable, UserService.The distinction between class and interface is visible from the
implements keyword and from IDE tooling. The I prefix is considered a code smell in Java โ it pollutes the type name and provides no useful information that the language doesn't already express.
1. A public no-arg constructor
2. Private fields
3. Public getters/setters named
getFieldName() / setFieldName() (or isFieldName() for boolean)It matters because virtually every Java framework โ Spring MVC, Jackson, Hibernate, JSP EL โ uses reflection to discover properties by finding
get/set/is methods with this naming pattern. Deviating from it silently breaks property binding, JSON serialisation, and ORM mapping.
studentRecord or a constant maxSize. However:โ Tools enforce them: Checkstyle, PMD, SonarQube, and most IDE inspections flag convention violations
โ Frameworks rely on them: Spring, Hibernate, and Jackson use naming patterns to auto-wire and map fields
โ Team readability: any Java developer reads
isActive() as a boolean getter instantly โ getIsActive() or checkActive() creates confusionBreaking conventions is not a compiler error, but it is a professional error.
com.companyname.projectname.module.Reasons for lowercase:
โ File system compatibility: on case-sensitive file systems (Linux),
com.Example and com.example are different packages. Lowercase avoids ambiguity.โ Conflict avoidance: using your domain name reversed (
com.google, org.apache) guarantees uniqueness globally โ no two organisations share the same domain.Underscores are allowed but discouraged; hyphens are not allowed (not valid Java identifiers).
๐ฑ Naming Conventions in Spring Boot
Spring Boot has strong opinions on naming โ both at the Java code level and at the configuration level. Following them enables auto-configuration, reduces boilerplate, and makes the project structure immediately recognisable to any Spring developer.
UserController, OrderController@Service โ
UserService, OrderService (interface) + UserServiceImpl (impl)@Repository โ
UserRepository, OrderRepository@Entity โ
User, Order, Product (singular nouns)DTO classes โ
UserDTO, CreateOrderRequest, OrderResponseException classes โ
UserNotFoundException, InvalidOrderExceptionapplication.properties keys โ
kebab-case: spring.datasource.url, app.jwt-secret
// Package structure โ all lowercase // com.example.ecommerce.controller // com.example.ecommerce.service // com.example.ecommerce.repository // com.example.ecommerce.model // com.example.ecommerce.dto // com.example.ecommerce.exception @RestController @RequestMapping("/api/orders") public class OrderController { // PascalCase + "Controller" suffix private final OrderService orderService; // camelCase field public OrderController(OrderService orderService) { this.orderService = orderService; } @GetMapping("/{orderId}") public OrderResponse getOrderById( // camelCase method @PathVariable Long orderId) { // camelCase param return orderService.findById(orderId); } } // application.properties โ kebab-case for config keys // spring.datasource.url=jdbc:mysql://localhost/mydb // app.jwt-secret=mySecretKey // app.max-retry-count=3
firstName โ "firstName". If your API needs snake_case JSON ("first_name"), add this to application.properties:spring.jackson.property-naming-strategy=SNAKE_CASEOr annotate per-class:
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
- Class / Interface / Enum type / Annotation โ PascalCase
- Method / Variable / Parameter โ camelCase
- Constant (
static final) โ ALL_CAPS_WITH_UNDERSCORES - Package โ all lowercase, dot-separated
- Enum constant โ ALL_CAPS (same as constants)
- Boolean getter โ
isprefix, notget - Generic type parameter โ single uppercase letter: T, E, K, V
- Java does NOT use I prefix for interfaces (unlike C#)
- Conventions are not compiler-enforced โ but framework-critical
๐ MCQ Traps
| Identifier | Correct Convention | Common Wrong Answer |
|---|---|---|
class name | StudentRecord (PascalCase) | studentRecord or STUDENTRECORD |
| Method name | calculateTotal() (camelCase) | CalculateTotal() or calculate_total() |
static final variable | MAX_SIZE (ALL_CAPS) | maxSize or MaxSize |
Boolean getter for active | isActive() | getActive() (wrong prefix) |
| Package name | com.example.util (lowercase) | com.Example.Util |
| Interface convention | Runnable (no prefix) | IRunnable (C# style โ wrong in Java) |
| Enum constant | PENDING (ALL_CAPS) | Pending or pending |
๐ One-Liner Definitions
- PascalCase โ Every word starts with uppercase; used for classes, interfaces, enums, and annotations.
- camelCase โ First word lowercase, subsequent words capitalised; used for methods, variables, and parameters.
- ALL_CAPS โ All uppercase with underscores; used for
static finalconstants and enum constants. - JavaBean convention โ Standard of public no-arg constructor, private fields, and
get/set/isaccessors relied on by Spring, Jackson, and Hibernate. - Reverse domain package naming โ Using reverse domain as package root (
com.company.app) to guarantee global uniqueness.
Anonymous Objects
๐ป What is an Anonymous Object?
An anonymous object is an object that is created without assigning it to a reference variable. You call new ClassName() and immediately use the object โ usually by calling a method on it โ without storing any reference to it.
๐ง Memory Behaviour
โ When to Use Anonymous Objects
- Single-use operations โ when you only need to call one method and never need the object again
- Method arguments โ passing a newly created object directly into another method without naming it
- Method chaining โ chaining a sequence of calls starting from creation
- Reducing clutter โ avoids creating a named variable that is used exactly once
โ When NOT to Use Anonymous Objects
- When you need to call multiple separate methods on the object after creation
- When you need to store or retrieve state from the object later
- When you need to pass the object to multiple places
- When the object creation is expensive and you need to reuse it
Anonymous Object โ an instance of a named class, created without storing a reference. e.g.
new Student().display()Anonymous Class โ a class defined inline without a name, typically used to implement an interface or extend a class on the spot. e.g.
new Runnable() { public void run() { ... } }Topic 24 covers anonymous classes in detail. This topic covers anonymous objects of named classes.
๐ Common Patterns
๐ป Program 1 โ Anonymous Object: Basic Usage
class Greeter { String name; Greeter(String name) { this.name = name; } void greet() { System.out.println("Hello, " + name + "!"); } int nameLength() { return name.length(); } } public class AnonymousBasic { public static void main(String[] args) { // Named object โ reference stored, reusable Greeter g = new Greeter("Rahul"); g.greet(); System.out.println("Length: " + g.nameLength()); // can call again // Anonymous object โ used once, then unreachable new Greeter("Priya").greet(); // created and used in one line new Greeter("Arjun").greet(); // each is a fresh separate object // Cannot do this with anonymous โ no reference to call second method: // new Greeter("Test").greet(); โ greet() done // .nameLength(); โ compile error โ nothing to chain } }
๐ป Program 2 โ Anonymous Object as Method Argument
class Student { String name; int marks; Student(String name, int marks) { this.name = name; this.marks = marks; } } class ReportPrinter { void printResult(Student s) { String grade = s.marks >= 90 ? "A" : s.marks >= 75 ? "B" : s.marks >= 60 ? "C" : "F"; System.out.printf("%-10s | %3d | Grade: %s%n", s.name, s.marks, grade); } } public class AnonymousAsArgument { public static void main(String[] args) { ReportPrinter printer = new ReportPrinter(); // Anonymous Student objects passed directly as arguments printer.printResult(new Student("Rahul", 92)); printer.printResult(new Student("Priya", 78)); printer.printResult(new Student("Arjun", 55)); printer.printResult(new Student("Sneha", 88)); // Each Student is created anonymously โ no variable name needed // All are GC-eligible after printResult() returns } }
๐ป Program 3 โ GC Eligibility: Named vs Anonymous
class HeavyObject { static int liveCount = 0; HeavyObject() { liveCount++; System.out.println("Created | live: " + liveCount); } void process() { System.out.println("Processing..."); } protected void finalize() { // called by GC (not guaranteed, for demo only) liveCount--; System.out.println("Finalized| live: " + liveCount); } } public class GCEligibility { public static void main(String[] args) { System.out.println("--- Anonymous object ---"); new HeavyObject().process(); // GC eligible after this line System.out.println("--- Named object ---"); HeavyObject h = new HeavyObject(); h.process(); // still alive โ h holds reference // h goes out of scope at end of main() โ then GC eligible } }
new Student("Rahul").setAge(20).display() works only if setAge() returns this (Builder pattern). If setAge() returns void, the chain breaks โ you cannot call .display() after a void method. Anonymous objects only support chaining when each method returns this or another object.
System.out.println(new Calculator().add(5, 3)); is more readable than:Calculator c = new Calculator();System.out.println(c.add(5, 3));The named version is only worthwhile if
c is used more than once.
new ClassName() without assigning it to a reference variable. It can be used only at the point of creation โ typically to call one method or pass it as an argument.A named object has a reference variable stored on the Stack that points to it. It can be accessed multiple times as long as the reference is in scope.
Key difference: an anonymous object has zero references from the moment its single expression finishes โ making it immediately GC-eligible. A named object lives until its reference variable goes out of scope.
For example, in
new Student("Rahul").display(); โ after display() returns and the semicolon is reached, the Student object has no references and is eligible for GC. This is earlier than a named object, which lives until its reference variable goes out of scope.
new Logger().log("msg")2. Method argument โ pass a freshly created object into another method without naming it:
processor.handle(new Request("GET", "/api"))3. Method chaining โ start a fluent chain from creation:
new StringBuilder().append("a").append("b").toString()4. One-shot computation โ compute and immediately use a result:
System.out.println(new Calculator().factorial(5))
new Student().display()Anonymous class: a class defined inline with no name, typically to implement an interface or override methods of a class on the spot. It both defines the class and creates one instance simultaneously. e.g.:
Runnable r = new Runnable() { public void run() { ... } };These are fundamentally different concepts. Anonymous objects are about object lifetime. Anonymous classes are about class definition โ covered in Topic 24.
You cannot do:
new Student("Rahul").greet();// and then separately: .display(); โ there is no reference to chain off.You can do chaining if each method returns
this:new Builder().setName("Rahul").setAge(20).build();But if any method returns
void, the chain ends there and no further calls are possible on that object.
Internally, the JVM may use a temporary implicit reference during expression evaluation, but that reference is never stored in any named variable and is released as soon as the expression completes.
๐ฑ Anonymous Objects in Spring Boot
Anonymous objects appear frequently in Spring Boot in three contexts: building request/response objects inline, writing test assertions, and constructing entities/DTOs in service layer code.
Test assertions โ create expected object anonymously for comparison
Exception throwing โ
throw new ResourceNotFoundException("User not found")DTO construction โ build and immediately pass a DTO to a service method
@RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @PostMapping public ResponseEntity<UserDTO> createUser( @RequestBody CreateUserRequest request) { // Anonymous UserDTO created inline and returned immediately return ResponseEntity.ok( new UserDTO(userService.createUser(request))); } @GetMapping("/{id}") public User getUser(@PathVariable Long id) { return userService.findById(id) // Anonymous exception โ most common anonymous object in Spring! .orElseThrow(() -> new RuntimeException("User not found: " + id)); } }
throw new ResourceNotFoundException("msg"), you are creating an anonymous object โ a new exception instance with no reference variable. It is passed directly to the JVM's exception handling machinery. This is the single most common anonymous object pattern in all Java/Spring Boot code.
- Anonymous object = object created without a reference variable
- Anonymous object is on the Heap โ all objects are; it just has no Stack reference
- GC eligible immediately after the statement finishes โ zero references
- Can only be used once โ at the point of creation
- Can chain methods only if each returns
thisor another object โ not aftervoid - Best for: single method calls, method arguments, one-shot computations
- Anonymous object โ Anonymous class โ completely different concepts
throw new Exception("msg")is the most common anonymous object in Java
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Where does an anonymous object live? | Heap | All objects live in Heap โ anonymous or not; only the Stack reference is missing |
| When is anonymous object GC eligible? | After the statement ends | Not "immediately" in a vague sense โ specifically after the expression/statement that created it completes |
| Can you call two separate methods on an anonymous object? | No (unless chained via return this) | No reference = no second access point |
Is new X().method() valid Java? | Yes | Perfectly legal โ creates object, calls method, object becomes unreachable |
| Anonymous object vs anonymous class? | Different things | Anonymous object = instance of named class, no reference; Anonymous class = inline class definition |
๐ One-Liner Definitions
- Anonymous object โ An object created with
newbut not assigned to any reference variable; usable only once at the point of creation. - GC eligibility โ An object becomes eligible for garbage collection when no live reference points to it; anonymous objects reach this state immediately after their creating expression completes.
- Method chaining on anonymous object โ Possible only when each method in the chain returns
thisor another object, enabling the next call to have a receiver.
Inheritance โ Introduction & All Types
๐งฌ What is Inheritance?
Inheritance is an OOP mechanism where a child class (subclass) acquires the properties and behaviours of a parent class (superclass). It models the IS-A relationship โ a Dog IS-A Animal, a Car IS-A Vehicle, a SavingsAccount IS-A BankAccount.
Inheritance promotes code reuse โ common fields and methods are written once in the parent and inherited by all children, avoiding duplication. It also enables polymorphism (Topic 19).
extendsHAS-A (composition): Car has a Engine โ use a field of type Engine
A common design mistake is using inheritance when composition is more appropriate. Rule of thumb: if you cannot truthfully say "X IS-A Y", don't use inheritance โ use composition instead.
๐ The extends Keyword
๐ What IS and IS NOT Inherited
| Inherited โ | NOT Inherited โ |
|---|---|
| Public & protected fields | Private fields (hidden but accessible via public getters) |
| Public & protected methods | Private methods |
| Default (package) members โ same package only | Constructors (called via super(), not inherited) |
| Static members (accessible, not truly "inherited") | Static members cannot be overridden (only hidden) |
๐ณ Types of Inheritance in Java
1. Single-Level Inheritance
2. Multi-Level Inheritance
3. Hierarchical Inheritance
4. Multiple Inheritance โ NOT Supported for Classes
5. Hybrid Inheritance
A combination of two or more inheritance types. Achievable in Java through a mix of class inheritance and interface implementation. Since classes can only extend one class but implement multiple interfaces, true hybrid inheritance uses interfaces for the "multiple" part.
extend exactly one class (or implicitly extends Object if none specified). Attempting to extend two classes is a compile error. For multiple inheritance of behaviour, Java uses interfaces โ a class can implement any number of interfaces.
๐๏ธ The Object Class โ Root of All
Every class in Java implicitly extends java.lang.Object if it doesn't explicitly extend another class. This means every object has methods like toString(), equals(), hashCode(), getClass(), clone(), and finalize() available โ inherited from Object.
๐ป Program 1 โ Single & Hierarchical Inheritance
class Animal { String name; Animal(String name) { this.name = name; } void eat() { System.out.println(name + " is eating"); } void sleep() { System.out.println(name + " is sleeping"); } } // Hierarchical: Dog and Cat both extend Animal class Dog extends Animal { Dog(String name) { super(name); } // call parent constructor void bark() { System.out.println(name + " says: Woof!"); } } class Cat extends Animal { Cat(String name) { super(name); } void meow() { System.out.println(name + " says: Meow!"); } } public class HierarchicalInheritance { public static void main(String[] args) { Dog dog = new Dog("Bruno"); dog.eat(); // inherited from Animal dog.sleep(); // inherited from Animal dog.bark(); // own method Cat cat = new Cat("Whiskers"); cat.eat(); // inherited from Animal cat.meow(); // own method } }
๐ป Program 2 โ Multi-Level Inheritance
class Vehicle { String brand; int speed; Vehicle(String brand, int speed) { this.brand = brand; this.speed = speed; } void move() { System.out.println(brand + " moving at " + speed + " km/h"); } } class Car extends Vehicle { int doors; Car(String brand, int speed, int doors) { super(brand, speed); // call Vehicle constructor this.doors = doors; } void honk() { System.out.println(brand + " honks! Doors: " + doors); } } class ElectricCar extends Car { int batteryRange; ElectricCar(String brand, int speed, int doors, int batteryRange) { super(brand, speed, doors); // call Car constructor this.batteryRange = batteryRange; } void charge() { System.out.println(brand + " charging | range: " + batteryRange + " km"); } } public class MultiLevelInheritance { public static void main(String[] args) { ElectricCar tesla = new ElectricCar("Tesla", 250, 4, 600); tesla.move(); // from Vehicle (grandparent) tesla.honk(); // from Car (parent) tesla.charge(); // own method // instanceof checks the entire chain System.out.println(tesla instanceof ElectricCar); // true System.out.println(tesla instanceof Car); // true System.out.println(tesla instanceof Vehicle); // true } }
๐ป Program 3 โ Diamond Problem Demo (Why Java Blocks Multiple Class Inheritance)
// Classes: single inheritance only class A { void show() { System.out.println("A"); } } class B extends A { void show() { System.out.println("B"); } } class C extends A { void show() { System.out.println("C"); } } // class D extends B, C { } โ COMPILE ERROR โ multiple inheritance // โ Solution: interfaces CAN have multiple parents interface Flyable { default void fly() { System.out.println("Flying"); } } interface Swimmable { default void swim() { System.out.println("Swimming"); } } // A class can implement MULTIPLE interfaces class Duck extends A implements Flyable, Swimmable { void quack() { System.out.println("Quack!"); } } public class DiamondProblem { public static void main(String[] args) { Duck duck = new Duck(); duck.show(); // from A (class inheritance) duck.fly(); // from Flyable (interface) duck.swim(); // from Swimmable (interface) duck.quack(); // own method } }
instanceof returns true for the actual type and every type in its inheritance chain. An ElectricCar is an instance of ElectricCar, Car, Vehicle, and Object. This is the foundation of polymorphism โ you can treat a child object as its parent type.
extends keyword. It models the IS-A relationship: a Dog IS-A Animal, so Dog inherits Animal's behaviour.Purpose: code reuse (define common behaviour once in parent), and the foundation for polymorphism (a parent reference can hold a child object).
NOT inherited: private fields and methods (they exist in the object but are not accessible), constructors (they can be called via
super() but are never inherited and cannot be overridden).
Java's solution: a class can extend only ONE class, but can
implement multiple interfaces. Interfaces handle the multiple-inheritance-of-behaviour scenario with clearer rules (the implementing class must provide its own override if there is ambiguity between default methods).
Multi-level: A โ B โ C (chain of inheritance)
Hierarchical: one parent โ multiple children
Multiple: not supported for classes (Diamond Problem); supported via interfaces
Hybrid: combination โ achieved in Java via class inheritance + multiple interface implementation
java.lang.Object is the root of every class hierarchy in Java. Every class implicitly extends Object if it doesn't explicitly extend another class. This guarantees that every Java object has a common set of methods: toString(), equals(), hashCode(), getClass(), clone(), finalize(), wait(), notify(), notifyAll().This is why
Object obj = new Student() is always valid โ every class IS-A Object.
extends. Example: Dog extends Animal โ a Dog is an Animal.HAS-A (composition): one class contains another as a field. Example:
Car has an Engine field โ a Car has an Engine.Design rule: use inheritance only when IS-A is genuinely true. If you're tempted to inherit just to reuse code but the IS-A relationship is weak, use composition instead โ it's more flexible and avoids tight coupling.
instanceof returns true if the object is an instance of the specified type or any of its supertypes.For
ElectricCar ec = new ElectricCar():ec instanceof ElectricCar โ trueec instanceof Car โ trueec instanceof Vehicle โ trueec instanceof Object โ trueThis is the IS-A relationship made explicit at runtime. It is used before downcasting to prevent
ClassCastException.
๐ฑ Inheritance in Spring Boot
Inheritance is used throughout Spring Boot โ in entity hierarchies, exception hierarchies, base entity classes, and service layering. Understanding it helps you write DRY, maintainable Spring code.
Exception hierarchy โ custom exceptions extending RuntimeException
@MappedSuperclass โ JPA annotation for a parent entity whose fields are mapped to child tables
Abstract service โ shared service logic in a base class, specialised by concrete services
// โโ @MappedSuperclass โ fields inherited by all entity tables โโ @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class BaseEntity { @Id @GeneratedValue private Long id; @CreatedDate private LocalDateTime createdAt; @LastModifiedDate private LocalDateTime updatedAt; } // User inherits id, createdAt, updatedAt from BaseEntity @Entity public class User extends BaseEntity { // IS-A BaseEntity private String name; private String email; } // โโ Exception hierarchy โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ public class AppException extends RuntimeException { private final int statusCode; public AppException(String msg, int statusCode) { super(msg); this.statusCode = statusCode; } public int getStatusCode() { return statusCode; } } public class UserNotFoundException extends AppException { public UserNotFoundException(Long id) { super("User not found: " + id, 404); } }
@MappedSuperclass: parent class fields are mapped to each child's own table โ no separate table for the parent. Used for shared audit fields.@Entity + @Inheritance: parent gets its own table. Complex JPA inheritance mapping (SINGLE_TABLE, JOINED, TABLE_PER_CLASS strategies). Most Spring Boot projects use @MappedSuperclass for the base audit entity pattern.
- Inheritance keyword:
extends(for classes),implements(for interfaces) - Java supports single class inheritance only โ one
extendsper class - Private members are NOT inherited (exist in object but not accessible)
- Constructors are NOT inherited โ called via
super() - Every class implicitly extends
Objectif no explicit parent - Diamond Problem โ reason Java blocks multiple class inheritance
instanceofreturnstruefor the object's type AND all supertypes- IS-A = inheritance; HAS-A = composition (field)
- Multi-level: A โ B โ C; Hierarchical: A โ B, A โ C
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Can a class extend two classes? | No | Diamond Problem โ compile error |
| Are constructors inherited? | No | Called via super() but not inherited or overridden |
| Are private fields inherited? | No (not accessible) | They exist in the object memory but subclass cannot access them directly |
| What does every class implicitly extend? | Object | java.lang.Object is the universal root |
Dog d = new Dog(); d instanceof Animal? | true | instanceof checks the full chain, not just direct type |
| Can a class implement multiple interfaces? | Yes | Only extends is limited to one; implements has no limit |
| What type of inheritance does AโBโC represent? | Multi-level | Not hierarchical (that is one parent, many children) |
๐ One-Liner Definitions
- Inheritance โ Mechanism where a subclass acquires fields and methods of its superclass via
extends. - IS-A relationship โ The semantic contract of inheritance: a Dog IS-A Animal; used to validate whether inheritance is appropriate.
- extends โ Keyword used by a class to inherit from exactly one parent class.
- Multi-level inheritance โ A chain: A โ B โ C, where C inherits from B which inherits from A.
- Hierarchical inheritance โ One parent, multiple children: A โ B, A โ C, A โ D.
- Diamond Problem โ Ambiguity arising when a class tries to inherit the same method from two parents; the reason Java blocks multiple class inheritance.
- @MappedSuperclass โ JPA annotation for a non-entity parent whose fields are mapped into each child entity's table.
super Keyword & Constructor Chaining
๐ The super Keyword โ Three Uses
The super keyword is a reference to the immediate parent class. It has three distinct uses: accessing a parent field, calling a parent method, and calling a parent constructor.
Use 1 โ Accessing Parent Field
Use 2 โ Calling Parent Method
Use 3 โ Calling Parent Constructor (super())
The most important use. When a child class constructor must initialise parent fields, it calls the parent constructor via super(args). This must be the very first statement in the child constructor.
๐ Constructor Chaining
Constructor chaining is when one constructor calls another. There are two kinds:
- this() chaining โ calling another constructor in the same class (covered in Topic 9)
- super() chaining โ calling a constructor in the parent class
๐๏ธ Object Class โ Implicit Root Constructor
Every constructor chain eventually reaches Object(). Even if you don't write super() in your constructor, the compiler inserts it automatically, and this propagates all the way to Object. This guarantees that the Object part of every object is always properly initialised.
this() and super() must be the first statement in a constructor. Since a constructor can only have one first statement, you cannot use both in the same constructor. However, a chain is fine:Constructor A calls
this() โ Constructor B calls super()The rule is: every chain must eventually reach a
super() call (explicit or implicit) so Object is initialised.
๐ป Program 1 โ All Three Uses of super
class Animal { String type = "Animal"; // Use 1: field that child will hide String name; Animal(String name) { // Use 3: parent constructor this.name = name; System.out.println("Animal constructor: " + name); } void sound() { // Use 2: method child will override System.out.println(name + " makes a sound"); } } class Dog extends Animal { String type = "Dog"; // hides Animal.type String breed; // Use 3: super() โ calls Animal constructor Dog(String name, String breed) { super(name); // MUST be first line this.breed = breed; System.out.println("Dog constructor: " + name + " / " + breed); } // Use 2: super.method() โ calls Animal's sound(), then adds own void sound() { super.sound(); // "Bruno makes a sound" System.out.println(name + " says: Woof!"); } void showTypes() { // Use 1: super.field โ access hidden parent field System.out.println("Own type : " + type); // "Dog" System.out.println("super type: " + super.type); // "Animal" } } public class SuperKeyword { public static void main(String[] args) { Dog dog = new Dog("Bruno", "Labrador"); dog.sound(); dog.showTypes(); } }
๐ป Program 2 โ Constructor Chaining: Object โ Vehicle โ Car โ ElectricCar
class Vehicle { String brand; Vehicle(String brand) { this.brand = brand; System.out.println("1. Vehicle()"); } } class Car extends Vehicle { int doors; Car(String brand, int doors) { super(brand); // calls Vehicle() this.doors = doors; System.out.println("2. Car()"); } } class ElectricCar extends Car { int range; ElectricCar(String brand, int doors, int range) { super(brand, doors); // calls Car() this.range = range; System.out.println("3. ElectricCar()"); } } public class ConstructorChain { public static void main(String[] args) { System.out.println("Creating ElectricCar:"); ElectricCar ec = new ElectricCar("Tesla", 4, 600); System.out.printf("%s | %d doors | %dkm range%n", ec.brand, ec.doors, ec.range); } }
๐ป Program 3 โ Compile Error: Missing super() When Parent Has No No-Arg
class Parent { String name; // Only parameterized constructor โ no no-arg! Parent(String name) { this.name = name; } } class ChildBroken extends Parent { ChildBroken() { // compiler tries to insert super() here // but Parent has no no-arg constructor! // COMPILE ERROR: There is no default constructor available in Parent } } class ChildFixed extends Parent { ChildFixed(String name) { super(name); // explicitly calls Parent(String) โ OK System.out.println("ChildFixed created: " + name); } } public class MissingSuperError { public static void main(String[] args) { ChildFixed c = new ChildFixed("Rahul"); System.out.println("name from parent: " + c.name); } }
super() or this() in a constructor is a compile error. You cannot do:
System.out.println("hi"); super(name); โ the print must come after. This is enforced strictly by the compiler.
super.method() when you want to extend rather than replace the parent's behaviour. Common in GUI frameworks (call super.paintComponent(g) before adding your own drawing) and in Spring (sometimes calling super.configure(http) in security config). If you override a method and never call super.method(), you are fully replacing the parent's logic โ make sure that is your intent.
2. super.method() โ call the parent's version of a method that the child has overridden. Used when you want to extend rather than fully replace the parent's behaviour.
3. super(args) โ call the parent class constructor. Must be the first statement in the child constructor. Used to initialise the parent part of the object.
super() as the first statement of every constructor that doesn't already start with super() or this().If the parent class has a no-arg constructor, this works fine silently. But if the parent has only parameterized constructors (no no-arg), the implicit
super() fails to find a match โ compile error: "There is no default constructor available in ParentClass". The fix: explicitly call the correct super(args).
However, they can work together in a chain: Constructor A uses
this() to delegate to Constructor B, and Constructor B then uses super() to call the parent. The rule is simply that each constructor can only have one of them as its first statement. Every chain must ultimately reach a super() (explicit or implicit) to ensure the parent is initialised.
For
new ElectricCar():1. ElectricCar() calls
super() โ pauses2. Car() calls
super() โ pauses3. Vehicle() calls
super() โ pauses4. Object() completes first
5. Vehicle() body completes
6. Car() body completes
7. ElectricCar() body completes last
This ensures every parent is fully initialised before a child uses it. Output: parent print messages appear before child print messages.
super inside a child class uses dynamic dispatch โ the JVM resolves the method based on the actual runtime type of the object. If the child overrides the method, the child's version always runs.super.method() bypasses dynamic dispatch and directly calls the parent's version of the method, regardless of overriding. It is a static, compile-time resolution.Practical use: in an overriding method, call
super.method() to execute the parent's logic first, then add the child's extra behaviour โ rather than rewriting the parent's logic from scratch.
super cannot be used in a static context. The super keyword refers to the parent part of the current object (this). Static methods have no this reference โ they belong to the class, not to an instance. Therefore, both super.field, super.method(), and super() are all illegal inside a static method. The compiler will give an error: "Non-static variable super cannot be referenced from a static context."
๐ฑ super in Spring Boot
The super keyword appears in Spring Boot primarily when extending framework classes, building exception hierarchies, and calling parent constructors in entity/service base classes.
super(message) and super(message, cause) to pass details to RuntimeExceptionBaseEntity โ child entity calls parent constructor if base has one
WebSecurityConfigurerAdapter โ calling
super.configure(http) (legacy Spring Security)Abstract service โ child service calls
super.validate() to reuse parent validation logic
// โโ Custom exception using super() โโโโโโโโโโโโโโโโโโโโโโโโโโ public class AppException extends RuntimeException { private final int httpStatus; public AppException(String message, int httpStatus) { super(message); // passes message to RuntimeException this.httpStatus = httpStatus; } public int getHttpStatus() { return httpStatus; } } public class ResourceNotFoundException extends AppException { public ResourceNotFoundException(String resource, Long id) { super(resource + " not found with id: " + id, 404); } } // โโ Abstract base service โ super.method() to reuse logic โโโโ public abstract class BaseService<T> { protected void validateNotNull(T entity) { if (entity == null) throw new IllegalArgumentException("Entity must not be null"); } } @Service public class UserService extends BaseService<User> { public void save(User user) { super.validateNotNull(user); // reuse parent validation // ... additional UserService-specific logic } }
super(message, originalException)This preserves the full stack trace, making debugging much easier. Spring's
@ControllerAdvice can then unwrap and log it properly.
superhas three uses: access field, call method, call constructorsuper()must be the first statement in a constructorthis()andsuper()cannot both appear in the same constructor- If no
super()orthis()written, compiler insertssuper()automatically - If parent has no no-arg constructor โ implicit
super()fails โ compile error - Constructor bodies execute parent first, child last (top-down)
supercannot be used in a static context- Every chain eventually reaches
Object()
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Where must super() appear in a constructor? | First statement | Any other position is a compile error |
| Can you use super() and this() together? | No โ in same constructor | Both require first-statement position; only one can be first |
| What does compiler insert if no super() or this()? | super() โ no-arg | Always inserted; fails if parent has no no-arg constructor |
| In what order do constructor bodies run? | Parent first, child last | Object runs first, deepest child runs last |
| Can super be used in a static method? | No | super needs this reference โ unavailable in static context |
| super.method() โ which method does it call? | Immediate parent's version | Bypasses dynamic dispatch; not necessarily grandparent |
๐ One-Liner Definitions
- super keyword โ Reference to the immediate parent class; used to access hidden fields, call overridden methods, or invoke parent constructors.
- super() โ Calls the parent class constructor; must be the first statement in a child constructor; implicitly inserted by compiler if omitted.
- Constructor chaining โ A sequence of constructor calls (via this() or super()) that propagates up the inheritance hierarchy to Object.
- Implicit super() โ The no-arg parent constructor call inserted automatically by the compiler when no explicit super() or this() is written.
- super.method() โ Directly invokes the parent class's version of an overridden method, bypassing runtime polymorphism.
Method Overriding
๐ What is Method Overriding?
Method overriding occurs when a subclass provides its own implementation of a method that is already defined in its superclass, using the same name, same parameter list, and same (or covariant) return type. The subclass version replaces the parent version for objects of that subclass.
Overriding is the mechanism behind runtime polymorphism โ the JVM decides at runtime which version to call based on the actual type of the object, not the reference type.
๐ Rules for Method Overriding
| Rule | Detail |
|---|---|
| Same method name | Exact same name as in parent |
| Same parameter list | Same number, types, and order of parameters (different = overloading, not overriding) |
| Same or covariant return type | Return type must be same or a subtype of the parent's return type |
| Access modifier | Must be same or wider โ can widen (protected โ public) but NOT narrow (public โ private) |
| Checked exceptions | Can throw fewer or narrower checked exceptions โ cannot throw new or broader ones |
| IS-A required | Only possible between parent and child class (inheritance required) |
โ What CANNOT Be Overridden
๐ฌ Overriding vs Overloading โ Side by Side
| Overriding | Overloading | |
|---|---|---|
| Where | Subclass overrides parent method | Same class (or subclass), different signature |
| Signature | Same name + same params | Same name + different params |
| Return type | Same or covariant | Can be anything (doesn't affect resolution) |
| Resolved at | Runtime (dynamic dispatch) | Compile time (static dispatch) |
| Polymorphism | Runtime polymorphism | Compile-time polymorphism |
| @Override | Applicable and recommended | Not applicable |
| Inheritance | Required | Not required |
๐ @Override Annotation
The @Override annotation tells the compiler: "I intend this method to override a parent method." If the signature doesn't match any parent method, the compiler gives an error โ catching typos and signature mismatches before runtime. Always use it.
tostring() instead of toString()) without @Override, Java creates a brand new method. Your override never happens โ the parent's version is still called. @Override would have caught this at compile time.
๐ Covariant Return Type
Since Java 5, an overriding method can return a subtype of the parent method's return type. This is called a covariant return type.
๐ป Program 1 โ Method Overriding & Runtime Polymorphism
class Shape { String color; Shape(String color) { this.color = color; } double area() { return 0; // default โ overridden by each shape } void describe() { System.out.printf("%-12s | color: %-6s | area: %.2f%n", getClass().getSimpleName(), color, area()); } } class Circle extends Shape { double radius; Circle(String color, double radius) { super(color); this.radius = radius; } @Override double area() { return Math.PI * radius * radius; } } class Rectangle extends Shape { double width, height; Rectangle(String color, double w, double h) { super(color); width = w; height = h; } @Override double area() { return width * height; } } class Triangle extends Shape { double base, heightT; Triangle(String color, double b, double h) { super(color); base = b; heightT = h; } @Override double area() { return 0.5 * base * heightT; } } public class MethodOverriding { public static void main(String[] args) { // Parent reference array โ polymorphic behaviour Shape[] shapes = { new Circle("Red", 5), new Rectangle("Blue", 4, 6), new Triangle("Green", 3, 8), new Shape("None") // base shape โ area 0 }; for (Shape s : shapes) s.describe(); // calls each class's area() at runtime } }
๐ป Program 2 โ Access Modifier Widening & Covariant Return
class Vehicle { // protected โ child can widen to public, cannot narrow to private protected Vehicle getInstance() { System.out.println("Vehicle.getInstance()"); return new Vehicle(); } protected void fuel() { System.out.println("Vehicle uses fuel"); } } class Car extends Vehicle { @Override public Car getInstance() { // โ wider (public) + covariant (Car) System.out.println("Car.getInstance() โ covariant return"); return new Car(); } @Override public void fuel() { // โ widened: protected โ public System.out.println("Car uses petrol"); } // โ would be compile error: // @Override private void fuel() { } narrowing public โ private } public class OverrideRules { public static void main(String[] args) { Vehicle v = new Car(); // parent ref, child object v.getInstance(); // Car's version runs โ runtime dispatch v.fuel(); // Car's version runs Car c = new Car(); Car ref = c.getInstance(); // covariant: returns Car directly (no cast) ref.fuel(); } }
๐ป Program 3 โ Method Hiding (static) vs Overriding
class Parent { static void staticMethod() { System.out.println("Parent - static"); } void instanceMethod() { System.out.println("Parent - instance"); } } class Child extends Parent { static void staticMethod() { // METHOD HIDING โ not overriding System.out.println("Child - static"); } @Override void instanceMethod() { // TRUE OVERRIDING System.out.println("Child - instance"); } } public class HidingVsOverriding { public static void main(String[] args) { Parent p = new Child(); // Static โ resolved by REFERENCE TYPE (compile time) p.staticMethod(); // "Parent - static" โ reference is Parent // Instance โ resolved by OBJECT TYPE (runtime) p.instanceMethod(); // "Child - instance" โ object is Child } }
@Override on a static method causes a compile error.
@Override is free insurance. It catches: misspelled method names, wrong parameter types, trying to override a final or static method, and trying to override when there is no parent method to override. Make it a habit to always add it when intending to override.
2. Same parameter list โ same types, count, and order (different params = overloading)
3. Same or covariant return type โ return type must be the same or a subtype of the parent's return type
4. Access modifier must be same or wider โ can widen (protected โ public) but cannot narrow (public โ private/protected)
5. Checked exceptions must be same or narrower โ cannot declare new or broader checked exceptions
6. Inheritance required โ can only override a method from a superclass
static methods โ they are hidden, not overridden; resolved by reference type, not object type
private methods โ not inherited at all; a child defining the same name creates a brand new independent method
Constructors โ constructors are never inherited and cannot be overridden
@Override tells the compiler: "I intend this to override a parent method." The compiler then verifies the contract:โ If no matching parent method exists โ compile error (catches typos, wrong params)
โ If trying to override a final or static method โ compile error
Without
@Override, a typo like tostring() instead of toString() silently creates a new independent method. The parent's toString() is never overridden and incorrect output appears at runtime with no error. @Override is optional but universally considered mandatory in professional code.
Method hiding (static methods): defining a static method in a child with the same name as a parent static method does not override it. Which version runs is determined by the reference type at compile time. No runtime polymorphism occurs. A
Parent reference always calls the Parent static method even if the object is a Child.
Example: if the parent declares
Animal getInstance(), the child can override it as Dog getInstance() since Dog is a subtype of Animal. This is valid since Java 5.Benefit: callers using a
Dog reference get back a Dog directly without casting; callers using an Animal reference still work because a Dog IS-A Animal. Factory and builder patterns use this extensively.
If the parent declares
void read() throws IOException, the child can:โ throw
FileNotFoundException (subclass of IOException โ narrower โ
)โ throw nothing at all โ
โ throw
Exception (superclass of IOException โ broader โ โ compile error)Reason: code holding a
Parent reference only catches IOException. If the child threw Exception, that catch block would miss it โ violating the Liskov Substitution Principle.Note: unchecked exceptions (RuntimeException and subclasses) have no such restriction โ they can be freely added or broadened in overrides.
Mechanism: each object carries a reference to its class's virtual method table (vtable), which maps method names to their most-derived implementation. When a method is called via a reference, the JVM follows the reference โ object โ vtable โ most specific override.
This is why:
Animal a = new Dog(); a.sound(); calls Dog.sound() โ the vtable for the Dog object points to Dog.sound().
๐ฑ Method Overriding in Spring Boot
Overriding is fundamental to Spring Boot โ the framework is designed around polymorphism. You override methods to customise framework behaviour, implement interfaces, and extend base classes.
UserDetailsService.loadUserByUsername() โ override to provide custom user lookup for Spring Security
WebSecurityConfigurerAdapter methods โ override
configure(HttpSecurity) to define security rules (legacy)JpaRepository custom query methods โ override default save/delete behaviour in repository
AbstractHealthIndicator.doHealthCheck() โ override to add custom health checks to Actuator
// โโ Override loadUserByUsername for Spring Security โโโโโโโโโโ @Service public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepo; public CustomUserDetailsService(UserRepository userRepo) { this.userRepo = userRepo; } @Override // implements UserDetailsService contract public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return userRepo.findByEmail(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); } } // โโ Override toString/equals/hashCode on JPA entity โโโโโโโโโโ @Entity public class User { @Id @GeneratedValue private Long id; private String email; @Override public String toString() { return "User{id=" + id + ", email='" + email + "'}"; } @Override // JPA entities should use id for equality public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User)) return false; return id != null && id.equals(((User) o).id); } @Override public int hashCode() { return getClass().hashCode(); } }
Object.equals() uses memory address โ two entities loaded at different times representing the same DB row would be unequal. Hibernate collections and caching rely on equals()/hashCode() for identity. Best practice: base equality on the database ID, and use getClass().hashCode() as a stable hash that works even before the ID is set.
- Overriding: same name + same params + same/covariant return + same/wider access
- Cannot override: final, static, private methods, constructors
- Static methods are hidden (compile-time), not overridden (runtime)
@Overrideโ optional but causes compile error if no parent method matches- Access modifier: can widen (protectedโpublic), cannot narrow (publicโprivate)
- Checked exceptions: can throw fewer/narrower, not more/broader
- Covariant return type: child can return a subtype (since Java 5)
- Runtime: resolved by object type; Static: resolved by reference type
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Can you override a private method? | No | Private methods aren't inherited โ child defines a new independent method |
| Can you override a static method? | No โ it is hidden | Static methods use compile-time (reference type) dispatch, not runtime dispatch |
| Can overriding method narrow access (publicโprivate)? | No โ compile error | Must be same or wider access modifier |
| Parent: void m() throws IOException. Child can throwโฆ? | Nothing, or FileNotFoundException | Cannot throw Exception (broader) โ compile error |
| Animal a = new Dog(); a.sound() callsโฆ? | Dog's sound() | Runtime dispatch โ object type (Dog) determines method |
| Parent a = new Child(); a.staticMethod() callsโฆ? | Parent's staticMethod() | Static uses reference type โ method hiding, not polymorphism |
| Is @Override mandatory for overriding? | No โ but strongly recommended | Optional, but without it typos create new methods silently |
๐ One-Liner Definitions
- Method overriding โ A subclass providing its own implementation of an inherited method with the same signature; resolved at runtime.
- Dynamic method dispatch โ The JVM mechanism that resolves an overridden method call based on the actual runtime type of the object, not the reference type.
- Method hiding โ Redefining a static method in a subclass; resolved at compile time by reference type โ not polymorphism.
- Covariant return type โ An overriding method returning a subtype of the parent method's return type; valid since Java 5.
- @Override โ Annotation that instructs the compiler to verify a method truly overrides a parent method; causes a compile error if the signature doesn't match.
Packages
๐ฆ What is a Package?
A package is a namespace that groups related classes, interfaces, enums, and annotations together. On the file system, a package maps directly to a folder/directory. Package names follow the reverse domain name convention to guarantee global uniqueness.
com.app.util.Date vs java.util.Date2. Access control โ the
default (package-private) access modifier restricts visibility to classes in the same package; packages form the boundary for that access level
๐ Package โ Folder Mapping
๐ The package Statement
The package statement must be the very first non-comment line in a Java source file. Only one package statement is allowed per file.
๐ฅ The import Statement
To use a class from another package you either use its fully qualified name every time, or add an import statement so you can use the simple name.
๐ Built-In Java Packages
| Package | Key Contents |
|---|---|
java.lang | String, Math, Object, System, Integer, Thread โ auto-imported |
java.util | ArrayList, HashMap, List, Collections, Arrays, Scanner, Date |
java.io | File, InputStream, OutputStream, BufferedReader, FileWriter |
java.nio | Path, Files, ByteBuffer โ modern I/O |
java.net | URL, HttpURLConnection, Socket |
java.sql | Connection, Statement, ResultSet, DriverManager |
java.time | LocalDate, LocalDateTime, Duration, ZonedDateTime |
java.util.concurrent | ExecutorService, Future, ConcurrentHashMap, AtomicInteger |
import java.util.* gives you ArrayList, HashMap, etc. โ but not java.util.concurrent.ConcurrentHashMap. You need a separate import java.util.concurrent.* for that. Each package level requires its own import.
๐ป Program 1 โ Declaring & Using a Package
// File must be at: src/com/example/model/Student.java package com.example.model; // first non-comment statement public class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return "Student{" + name + ", age=" + age + "}"; } }
package com.example.service; import com.example.model.Student; // explicit single-type import import java.util.ArrayList; import java.util.List; public class StudentService { private List<Student> students = new ArrayList<>(); public void addStudent(Student s) { students.add(s); } public void printAll() { students.forEach(s -> System.out.println(" " + s)); } }
package com.example; import com.example.model.Student; import com.example.service.StudentService; public class Main { public static void main(String[] args) { StudentService svc = new StudentService(); svc.addStudent(new Student("Rahul", 20)); svc.addStudent(new Student("Priya", 22)); svc.addStudent(new Student("Arjun", 21)); System.out.println("All students:"); svc.printAll(); } }
๐ป Program 2 โ Static Import & Fully Qualified Name
package com.example; // Static import โ use Math constants/methods without "Math." import static java.lang.Math.PI; import static java.lang.Math.sqrt; import static java.lang.Math.pow; public class ImportDemo { public static void main(String[] args) { // Static import: use PI and sqrt directly double radius = 7; double area = PI * radius * radius; double hyp = sqrt(pow(3,2) + pow(4,2)); // = 5.0 System.out.printf("Circle area (r=7): %.4f%n", area); System.out.printf("Hypotenuse (3,4) : %.1f%n", hyp); // Fully qualified name โ no import needed but verbose java.util.Date today = new java.util.Date(); System.out.println("Today: " + today.toString().substring(0,10)); } }
๐ป Program 3 โ Name Clash: Resolving with Fully Qualified Names
package com.example; // Import one โ the other must use fully qualified name import java.sql.Date; // sql.Date imported by simple name public class NameClash { public static void main(String[] args) { // java.sql.Date โ uses simple name (imported) Date sqlDate = new Date(System.currentTimeMillis()); // java.util.Date โ must use fully qualified name to avoid clash java.util.Date utilDate = new java.util.Date(); System.out.println("sql.Date : " + sqlDate); System.out.println("util.Date : " + utilDate); System.out.println("Same name, different packages โ no conflict!"); } }
import java.util.*; and import java.sql.*;, both packages have a Date class. Any use of Date becomes ambiguous โ compile error. The fix: import one by its simple name and use the fully qualified name for the other, as shown above.
java.lang contains String, System, Math, Object, Integer, Thread, etc. It is automatically imported in every Java file โ you never write import java.lang.*. Every other package (including java.util) requires an explicit import.
Two key purposes:
1. Namespace management: prevents naming collisions โ two classes named
Date can coexist in java.util and java.sql without conflict2. Access control: the default (package-private) access modifier makes members visible only within the same package, providing encapsulation at the package level
package statement must be the first non-comment line in the source file. Only one package statement is allowed per file. It must appear before any import statements and before the class declaration.If there is no package statement, the class belongs to the default (unnamed) package. Classes in the default package cannot be imported by classes in named packages โ so the default package is only suitable for quick experiments and single-file demos.
import java.util.ArrayList): imports exactly one class. Explicit and clear โ anyone reading the file immediately sees which classes are used from which packages.Wildcard import (
import java.util.*): imports all public types in a package. Does not slow down compilation (the compiler loads only what is actually used). Does not import sub-packages. Can cause ambiguity if two packages contain a class with the same name โ compile error.Professional preference: single-type imports, enforced by Checkstyle and most IDE code style settings.
java.lang is the only package that is automatically imported in every Java source file without any explicit import statement. It contains the most fundamental classes: String, Object, System, Math, Integer, Long, Thread, Runnable, Exception, etc.Every other package โ including
java.util โ requires an explicit import statement.
import static imports a static member (field or method) of a class, allowing it to be used without the class name prefix.Example:
import static java.lang.Math.PI; allows writing PI instead of Math.PI.When to use: in test code (
import static org.junit.Assert.* is idiomatic), and for frequently used constants/utility methods where the class prefix adds no clarity. When to avoid: in production code where it can obscure which class a method comes from โ Math.sqrt() is clearer than a bare sqrt().
package statement.Limitations:
1. Classes in the default package cannot be imported by classes in named packages โ only other default-package classes can use them
2. No way to have two classes with the same name (no namespace separation)
3. Default package classes cannot use
protected package-access across sub-pathsUse only for: quick experiments, single-file compilation demos, and practice programs. Never use for any real project.
com.google.gson, org.apache.commons) guarantees global uniqueness of package names.Since domain names are unique by definition (enforced by internet registrars), reversing them creates a namespace where no two organisations should produce the same package prefix โ unless they control the same domain. This convention prevents collisions between libraries from different vendors even when they contain classes with identical names.
๐ฑ Packages in Spring Boot
Spring Boot relies heavily on its own package scanning to auto-discover beans. Understanding the package structure is critical to making component scanning work correctly.
@SpringBootApplication โ scanning starts herecontroller/:
@RestController, @Controller classesservice/:
@Service classesrepository/:
@Repository, JPA repository interfacesmodel/ or entity/:
@Entity JPA classesdto/: Data Transfer Objects
config/:
@Configuration classesexception/: custom exception classes
// com/example/ecommerce/EcommerceApplication.java package com.example.ecommerce; // โ ROOT package @SpringBootApplication // scans com.example.ecommerce and ALL sub-packages public class EcommerceApplication { public static void main(String[] args) { SpringApplication.run(EcommerceApplication.class, args); } } // โ Discovered automatically (sub-package of root): // package com.example.ecommerce.service; โ @Service beans found // package com.example.ecommerce.controller; โ @RestController found // โ NOT discovered (different root โ outside scan range): // package com.other.service; โ beans here are NOT registered! // To scan additional packages explicitly: @SpringBootApplication(scanBasePackages = {"com.example.ecommerce", "com.shared.utils"}) public class EcommerceApplication { ... }
@SpringBootApplication class in the topmost package of your application, and let all other packages be sub-packages of it.
packagestatement = first non-comment line in the file- Package maps to a directory/folder on the file system
java.lang= only auto-imported package- Wildcard import (
*) does NOT import sub-packages - Name clash between two wildcard imports โ compile error
- Fix for name clash: import one by simple name, use fully qualified name for the other
- Default package: no
packagestatement โ cannot be imported by named packages - Static import:
import static ClassName.memberโ allows using static members without class prefix - Reverse domain naming โ guarantees global uniqueness
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Which package is auto-imported? | java.lang only | java.util, java.io etc. all need explicit import |
| Does import java.util.* import java.util.concurrent? | No | Wildcard doesn't cross into sub-packages |
| Two wildcard imports with same class name? | Compile error | Ambiguous reference โ must use fully qualified name |
| Can a default-package class be imported? | No | Named packages cannot import default-package classes |
| Where must package statement appear? | First non-comment line | Before imports and class declaration |
| Does wildcard import slow compilation? | No | Compiler only loads actually used types โ wildcard is just a convenience |
| What does static import do? | Imports a static field/method | Allows use without class prefix; import static Math.PI โ use PI directly |
๐ One-Liner Definitions
- Package โ A namespace grouping related Java types, mapping to a directory; declared with the
packagekeyword as the first statement. - Import โ A directive that allows using a class by its simple name rather than its fully qualified name.
- Wildcard import โ
import pkg.*; imports all public types from a single package level; does not include sub-packages. - Static import โ
import static pkg.Class.member; allows using a static field or method without the class name prefix. - Fully qualified name โ The complete package + class name:
java.util.ArrayList; always unique and never requires an import. - Default package โ The unnamed package for classes with no package statement; cannot be imported by named packages.
- Reverse domain naming โ Convention of using reversed internet domain as package root (e.g.,
com.google.*) to ensure global uniqueness.
Access Modifiers
๐ What are Access Modifiers?
Access modifiers control the visibility and accessibility of classes, fields, methods, and constructors. Java has four access levels โ from most to least restrictive: private, default (no keyword), protected, and public.
๐ The Access Modifier Table
๐ Each Modifier Explained
1. private
Accessible only within the same class. The most restrictive level. Use for all instance variables (the cornerstone of encapsulation) and for helper methods that are implementation details not meant for external callers.
2. default (package-private)
No keyword is written. Accessible to all classes in the same package. Classes in other packages (even subclasses in a different package) cannot access it. Used when you want package-internal helpers but not wider exposure.
3. protected
Accessible within the same package AND by subclasses in any package. The key nuance: a subclass in a different package can access protected members only through its own reference or subclass reference, not through a parent class reference.
4. public
Accessible from everywhere โ any class in any package. Use for the intentional public API of your class: constructors you want others to call, methods you want others to use, and constants you want to share.
๐๏ธ Modifiers for Top-Level Classes
๐ protected โ The Tricky One
private โ expose via getters/settersHelper methods:
private โ not part of the public contractMethods for subclasses to override/use:
protectedPublic API methods:
publicPackage utilities: default (no modifier)
Rule of thumb: use the most restrictive modifier that still allows the class to function correctly.
๐ป Program 1 โ All Four Modifiers in One Class
package com.example; public class Employee { public String companyName = "TechCorp"; // anyone protected String department = "Engineering"; // package + subclasses String teamCode = "T42"; // package only (default) private String salary = "CONFIDENTIAL"; // this class only public void introduce() { // can access ALL four from within the class System.out.println("Company : " + companyName); System.out.println("Dept : " + department); System.out.println("Team : " + teamCode); System.out.println("Salary : " + salary); } public void publicMethod() { System.out.println("public method"); } protected void protectedMethod() { System.out.println("protected method"); } void defaultMethod() { System.out.println("default method"); } private void privateMethod() { System.out.println("private method"); } } // Same package โ can access public, protected, default; NOT private class SamePackageTest { public static void main(String[] args) { Employee e = new Employee(); e.introduce(); // public โ OK e.publicMethod(); e.protectedMethod(); // same package โ OK e.defaultMethod(); // same package โ OK // e.privateMethod(); โ compile error System.out.println(e.companyName); // public โ OK System.out.println(e.teamCode); // default โ OK (same pkg) // System.out.println(e.salary); โ compile error } }
๐ป Program 2 โ protected in Subclass (Different Package)
// โโ com/animals/Animal.java โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ package com.animals; public class Animal { protected String name; protected int age; public Animal(String name, int age) { this.name = name; this.age = age; } protected void breathe() { System.out.println(name + " is breathing"); } } // โโ com/pets/Dog.java โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ package com.pets; import com.animals.Animal; public class Dog extends Animal { String breed; public Dog(String name, int age, String breed) { super(name, age); // calls Animal constructor this.breed = breed; } public void describe() { // protected fields accessible via inheritance in subclass System.out.printf("%s | age:%d | breed:%s%n", name, age, breed); breathe(); // protected method โ accessible in subclass // โ Would be compile error (different pkg, parent ref): // Animal a = new Animal("Cat",1); a.name โ not accessible } public static void main(String[] args) { new Dog("Bruno", 3, "Labrador").describe(); } }
๐ป Program 3 โ Access Modifier on Constructor (Singleton + Factory)
// Private constructor = Singleton (Topic 11 revisited) class Config { private static final Config INSTANCE = new Config(); private Config() {} // private constructor public static Config getInstance() { return INSTANCE; } public String getDbUrl() { return "jdbc:mysql://localhost/db"; } } // Protected constructor โ only subclasses can call new class AbstractShape { String color; protected AbstractShape(String color) { // protected constructor this.color = color; } double area() { return 0; } } class Square extends AbstractShape { double side; public Square(String color, double side) { super(color); // can call protected parent constructor this.side = side; } @Override double area() { return side * side; } } public class ConstructorAccess { public static void main(String[] args) { Config cfg = Config.getInstance(); System.out.println("DB: " + cfg.getDbUrl()); // new Config(); โ compile error โ private constructor Square sq = new Square("Blue", 5); System.out.printf("Square area: %.1f | color: %s%n", sq.area(), sq.color); // new AbstractShape("Red"); โ compile error (same pkg ok, diff pkg no) } }
protected is nearly as open as public. In practice, the sub-package nuance makes it tricky: a subclass in a different package can access protected members only via its own inherited reference โ not via a Parent type reference. This is frequently tested in exams and catches even experienced developers.
private (always)2. Getters/setters โ
public (for external consumers) or protected (for subclasses only)3. Template/hook methods for subclasses โ
protected4. Internal utility methods โ
private5. Package-scoped helpers โ default (no modifier)
6. Class itself โ
public if used outside the package, else default
default (no keyword): visible to all classes in the same package
protected: visible to all classes in the same package, AND to subclasses in any package
public: visible everywhere โ any class in any package
Order from most to least restrictive: private < default < protected < public
public โ accessible from any package
default (no keyword) โ accessible only within the same package
private and protected are not valid for top-level classes โ they cause a compile error. Additionally, a file can contain only one public class, and its name must match the filename exactly.
protected member only through its own reference or an instance of its own subclass type โ not through a reference of the parent class type.Example: if
Dog is in a different package from Animal and Animal.name is protected:โ
this.name inside Dog โ โ
own inherited fieldโ
new Dog().name inside Dog โ โ
same typeโ
new Animal().name inside Dog โ โ compile error (parent type reference, different package)
private enforces encapsulation:1. Validation: setters can validate values before assignment (
if (age < 0) throw)2. Controlled mutation: read-only fields can have getters but no setters
3. Implementation freedom: internal representation can change without breaking callers (change
int age to LocalDate birthDate and update the getter โ all callers still use getAge())4. Thread safety: easier to synchronise access through controlled methods
5. Debugging: all mutations go through one point โ easy to add logging/breakpoints
Valid:
protected โ public (widening โ
)Valid:
public โ public (same โ
)Invalid:
public โ protected (narrowing โ โ compile error)Reason: callers using the parent reference expect the same or more accessible contract. If a child narrowed access, code that compiled against the parent type would break at runtime โ violating the Liskov Substitution Principle.
Protected: accessible to all classes in the same package AND to subclasses anywhere (including different packages), subject to the own-reference rule.
Summary: protected adds cross-package subclass access on top of what default provides.
๐ฑ Access Modifiers in Spring Boot
Spring Boot code follows strict access modifier conventions. Framework-specific annotations interact with access modifiers in important ways.
public (Spring proxying requires it in most cases)Injected fields โ keep
private final; use constructor injection not field injection@RequestMapping handler methods โ
public (Spring MVC requires this)@Bean methods in @Configuration โ
public or protectedRepository interfaces โ
public interfaceInternal service helpers โ
private methods within the service classTemplate methods for subclasses โ
protected in abstract base service
@Service public class OrderService { // public class โ Spring proxy private final OrderRepository orderRepo; // private final โ encapsulated private final PaymentService paymentSvc; public OrderService(OrderRepository r, PaymentService p) { this.orderRepo = r; this.paymentSvc = p; } public Order createOrder(CreateOrderRequest req) { // public API validateRequest(req); // calls private helper Order order = buildOrder(req); return orderRepo.save(order); } private void validateRequest(CreateOrderRequest req) { // private helper if (req == null) throw new IllegalArgumentException("Request must not be null"); } private Order buildOrder(CreateOrderRequest req) { // private builder return new Order(req.getProduct(), req.getQuantity()); } }
@Transactional and @Cacheable. These proxies wrap your bean. If the method you annotate is private, Spring cannot intercept it โ the annotation is silently ignored and no transaction/cache is applied. Always make @Transactional and @Cacheable methods public.
- private โ same class only
- default โ same package (no keyword written)
- protected โ same package + subclasses anywhere
- public โ everywhere
- Top-level class can only be public or default โ never private/protected
- One file โ one
publicclass, name must match filename - Override: access can be same or wider โ never narrower
- protected + different package: only via own/subclass reference โ NOT via parent reference
- Best practice: fields always
private; expose viapublicgetters
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Can a top-level class be private? | No โ compile error | Only public or default allowed for top-level classes |
| Default access = which packages? | Same package only | NOT accessible to subclasses in different packages |
| Protected subclass in different package accesses via parent ref? | Compile error | Must access via own/subclass ref, not parent type ref |
| Can override narrow access from public to protected? | No โ compile error | Must be same or wider access modifier |
| How many public classes in one .java file? | One maximum | Must match filename; multiple classes allowed but only one public |
| Can private methods be overridden? | No | Not inherited โ child creates a new independent method |
| Which is more restrictive: default or protected? | default | default blocks cross-package subclasses; protected allows them |
๐ One-Liner Definitions
- private โ Most restrictive modifier; member accessible only within its declaring class.
- default (package-private) โ No keyword; member accessible to all classes in the same package, and no others.
- protected โ Member accessible within the same package and to subclasses in any package (via own reference).
- public โ Least restrictive; member accessible from any class in any package.
- Access widening in override โ The rule that an overriding method's access modifier must be equal to or wider than the parent method's modifier.
Polymorphism & Dynamic Method Dispatch
๐ญ What is Polymorphism?
Polymorphism (Greek: "many forms") means one interface, many implementations. In Java it means a single method call can behave differently depending on the actual object type at runtime. It is one of the four pillars of OOP and is made possible by inheritance and method overriding.
Runtime polymorphism (Dynamic / Late binding) โ achieved via method overriding + parent reference pointing to child object. The JVM decides which method to call at runtime based on the actual object type.
๐ Compile-Time Polymorphism โ Method Overloading
๐ Runtime Polymorphism โ Dynamic Method Dispatch
Dynamic Method Dispatch (DMD) is the mechanism by which Java selects the correct overridden method to call at runtime, based on the actual object type โ not the declared reference type.
๐ง How Dynamic Method Dispatch Works (vtable)
โก What Participates in Runtime Polymorphism
| Runtime Polymorphism? | Reason | |
|---|---|---|
| Instance methods (overridden) | โ YES | Resolved via vtable at runtime |
| Static methods | โ NO | Resolved by reference type at compile time (method hiding) |
| Instance variables (fields) | โ NO | Field access resolved by reference type, not object type |
| Constructors | โ NO | Constructors are not inherited or overridden |
| Private methods | โ NO | Not inherited โ no vtable entry in child |
| final methods | โ NO | Cannot be overridden โ single vtable entry |
๐ The Power: Polymorphic Arrays & Collections
๐ Field Hiding โ The Non-Polymorphic Trap
๐ป Program 1 โ Runtime Polymorphism: Shape Calculator
abstract class Shape { String color; Shape(String color) { this.color = color; } abstract double area(); // must override abstract double perimeter(); // must override void printInfo() { // calls overridden methods via DMD System.out.printf("%-12s color:%-6s area:%7.2f perim:%7.2f%n", getClass().getSimpleName(), color, area(), perimeter()); } } class Circle extends Shape { double r; Circle(String c, double r) { super(c); this.r = r; } @Override double area() { return Math.PI * r * r; } @Override double perimeter() { return 2 * Math.PI * r; } } class Rectangle extends Shape { double w, h; Rectangle(String c, double w, double h) { super(c); this.w = w; this.h = h; } @Override double area() { return w * h; } @Override double perimeter() { return 2 * (w + h); } } class Triangle extends Shape { double a, b, c; Triangle(String col, double a, double b, double c) { super(col); this.a=a; this.b=b; this.c=c; } @Override double area() { double s = (a+b+c)/2; return Math.sqrt(s*(s-a)*(s-b)*(s-c)); } @Override double perimeter() { return a+b+c; } } public class Polymorphism { static void printTotal(Shape[] shapes) { double total = 0; for (Shape s : shapes) { s.printInfo(); // DMD: each shape prints its own info total += s.area(); } System.out.printf("Total area: %.2f%n", total); } public static void main(String[] args) { Shape[] shapes = { new Circle("Red", 5), new Rectangle("Blue", 4, 6), new Triangle("Green", 3, 4, 5) }; printTotal(shapes); } }
๐ป Program 2 โ Field Hiding vs Method Overriding
class Base { String type = "Base"; // instance field void show() { System.out.println("Base.show()"); } } class Derived extends Base { String type = "Derived"; // hides Base.type @Override void show() { System.out.println("Derived.show()"); } } public class FieldVsMethod { public static void main(String[] args) { Base ref = new Derived(); // parent ref, child object // Method โ RUNTIME resolution (actual object type) ref.show(); // "Derived.show()" โ DMD // Field โ COMPILE-TIME resolution (reference type) System.out.println(ref.type); // "Base" โ reference type wins! // Cast to Derived to access Derived.type Derived d = (Derived) ref; System.out.println(d.type); // "Derived" } }
๐ป Program 3 โ Compile-Time Polymorphism: Overloading Resolution
public class Printer { void print(int n) { System.out.println("int: " + n); } void print(double d) { System.out.println("double: " + d); } void print(String s) { System.out.println("String: " + s); } void print(int a, int b) { System.out.println("int,int: " + a + "," + b); } public static void main(String[] args) { Printer p = new Printer(); p.print(42); // โ print(int) p.print(3.14); // โ print(double) p.print("Hello"); // โ print(String) p.print(10, 20); // โ print(int, int) p.print('A'); // โ print(int) โ char promoted to int! } }
printTotal(Shape[]) method never needs to change). This is the foundation of scalable, maintainable software design.
Compile-time polymorphism (static/early binding): achieved via method overloading. The compiler resolves the method call at compile time based on the method signature. The reference type determines which overloaded method runs.
Runtime polymorphism (dynamic/late binding): achieved via method overriding + parent reference pointing to child object. The JVM resolves the method call at runtime based on the actual object type. This is Dynamic Method Dispatch.
The JVM uses a virtual method table (vtable) โ a per-class table mapping method names to their most-derived implementation. When a method is called, the JVM follows: reference โ object โ object's class โ vtable โ call the correct override.
This is what makes
Animal a = new Dog(); a.sound(); call Dog.sound() instead of Animal.sound().
Instance variables (fields) โ also resolved by reference type (field hiding)
Private methods โ not inherited, so no vtable entry in child
final methods โ cannot be overridden, single vtable entry
Constructors โ never inherited or overridden
Only non-static, non-private, non-final instance methods that are overridden participate in DMD.
1. A parent reference (or interface reference)
2. A child/implementing object assigned to it
3. The method overridden in the child
However, with interfaces you don't need class inheritance โ a class can implement an interface and be referenced by that interface type, achieving full runtime polymorphism without extending any class. This is actually the preferred form in modern Java and Spring Boot.
Late binding (runtime / dynamic binding): the method to call is determined at runtime by the JVM based on the actual object type. Applies to: overridden instance methods. The binding is "late" because it is deferred until the program is running.
Dynamic Method Dispatch is Java's implementation of late binding.
a of type Animal:You CAN access: all members declared in
Animal (fields and methods). The compiler uses the reference type (Animal) to check what is accessible.You CANNOT access: members declared only in
Dog (e.g., a.bark() โ compile error โ bark() is not in Animal).At runtime, if you call
a.sound() and Dog overrides sound(), the JVM calls Dog's version โ DMD.If you call
a.type (a field), you get Animal's field value โ no DMD for fields.To access Dog-specific members, you must downcast:
((Dog)a).bark().
Polymorphism enables this: a method that accepts a
Shape parameter works with any current and future Shape subclass without modification. Adding a Pentagon class requires zero changes to the calling code โ just write Pentagon and pass it in. The loop for (Shape s : shapes) s.area() works forever without touching it.Without polymorphism you would need
if (shape instanceof Circle) ... else if (shape instanceof Rectangle)... โ and every new shape requires editing that code. Polymorphism eliminates those conditionals.
๐ฑ Polymorphism in Spring Boot
Spring Boot is built on polymorphism. Every bean injection, service interface, and repository relies on runtime polymorphism to swap implementations without changing calling code.
Multiple implementations โ inject a
List<PaymentService> to get all implementations@Primary / @Qualifier โ choose which concrete bean to inject when multiple exist
Strategy pattern via Spring โ pick payment processor at runtime based on order type
Repository interfaces โ JpaRepository is polymorphically resolved to SimpleJpaRepository at runtime
// Interface โ the polymorphic supertype public interface PaymentService { void processPayment(double amount); String getType(); } // Concrete implementations @Service class UpiPaymentService implements PaymentService { public void processPayment(double amount) { System.out.println("UPI: paying " + amount); } public String getType() { return "UPI"; } } @Service class CardPaymentService implements PaymentService { public void processPayment(double amount) { System.out.println("Card: paying " + amount); } public String getType() { return "CARD"; } } // OrderService โ inject ALL PaymentService implementations @Service public class OrderService { private final List<PaymentService> paymentServices; public OrderService(List<PaymentService> services) { this.paymentServices = services; } public void pay(String type, double amount) { paymentServices.stream() .filter(s -> s.getType().equalsIgnoreCase(type)) .findFirst() .ifPresent(s -> s.processPayment(amount)); // DMD: calls the right processPayment() at runtime } }
private final PaymentService paymentSvc, Spring decides at runtime which concrete bean to inject โ UpiPaymentService or CardPaymentService. The calling class never needs to know which implementation it got. This is the Strategy pattern powered by runtime polymorphism.
- Compile-time polymorphism โ method overloading โ resolved by reference type at compile time
- Runtime polymorphism โ method overriding โ resolved by object type at runtime
- Dynamic Method Dispatch = JVM mechanism for runtime polymorphism via vtable
- Fields (instance variables) use early binding โ NOT polymorphic
- Static methods use early binding โ NOT polymorphic (method hiding)
- private, final methods โ NOT polymorphic
- Pattern:
Parent ref = new Child()โ method call uses Child's override - Cannot call Child-specific methods via Parent reference (compile error)
- Downcast needed:
((Child)ref).childMethod()
๐ MCQ Traps
| Code / Question | Output / Answer | Trap / Reason |
|---|---|---|
Animal a=new Dog(); a.sound() where Dog overrides sound() | Dog's sound() | Runtime polymorphism โ object type wins |
Animal a=new Dog(); a.type where both have field type | Animal's type value | Fields use reference type โ no DMD for fields |
Animal a=new Dog(); a.staticMethod() | Animal's static method | Static uses reference type โ method hiding, not overriding |
| Overloading is which type of polymorphism? | Compile-time | Resolved at compile time by signature โ not runtime |
| Can private method be polymorphic? | No | Private not inherited โ child's same-name method is a NEW method |
| What resolves which overloaded method to call? | Argument types at compile time | The compiler picks based on declared argument types, not runtime types |
๐ One-Liner Definitions
- Polymorphism โ The ability of one interface/method to take many forms; implemented via overloading (compile-time) and overriding (runtime) in Java.
- Dynamic Method Dispatch โ JVM mechanism that resolves overridden method calls at runtime based on the actual object type, using a virtual method table (vtable).
- Early binding โ Method resolution at compile time; applies to overloaded, static, private, and final methods.
- Late binding โ Method resolution at runtime; applies to overridden instance methods; implemented via vtable lookup.
- vtable (virtual method table) โ A per-class table maintained by the JVM mapping method names to their most-derived implementations; the mechanism behind DMD.
- Field hiding โ When a child class declares a field with the same name as the parent; resolved by reference type (not runtime type) โ does not participate in polymorphism.
final Keyword
๐ The final Keyword โ Three Contexts
The final keyword means "cannot be changed after this point." It applies in three completely different contexts โ variable, method, and class โ and the meaning is analogous in each: lock it down.
1๏ธโฃ final Variable
A final variable can be assigned only once. After the first assignment it becomes read-only. This applies to local variables, method parameters, instance fields, and static fields.
final reference variable means you cannot reassign the variable to point to a different object. But the object itself can still be mutated through its own methods. To make a truly immutable object, you need the class itself to be designed for immutability (private fields, no setters, final class โ like String).
2๏ธโฃ final Method
A final method cannot be overridden by any subclass. The implementation is locked. Useful when a method's behaviour must not change in subclasses for correctness or security reasons.
3๏ธโฃ final Class
A final class cannot be extended. No class can declare extends FinalClass. This is the strongest form of locking โ the class design is frozen completely.
๐ final + static = Constant
final on local variables, fields, and parameters wherever possible:โ Makes code easier to reason about (value won't change unexpectedly)
โ Enables compiler and JVM optimisations
โ Catches accidental reassignment bugs at compile time
โ Signals intent to other developers: "this is not meant to change"
Modern Java style: constructor-injected Spring beans are
private final fields โ immutable references.
๐ป Program 1 โ final Variable in All Contexts
import java.util.ArrayList; import java.util.List; public class FinalVariable { // C) static final constant โ class-level, shared, immutable ref public static final double TAX_RATE = 0.18; public static final String APP_NAME = "ShopApp"; // B) final instance field โ must be set in constructor private final String orderId; private final double basePrice; public FinalVariable(String orderId, double basePrice) { this.orderId = orderId; // assigned once here this.basePrice = basePrice; // assigned once here } public void processOrder() { // A) final local variable final double tax = basePrice * TAX_RATE; final double total = basePrice + tax; // tax = 0; โ compile error: cannot assign to final System.out.printf("[%s] App: %s%n", orderId, APP_NAME); System.out.printf("Base: %.2f | Tax(%.0f%%): %.2f | Total: %.2f%n", basePrice, TAX_RATE * 100, tax, total); } public static void demonstrateFinalRef() { // D) final reference โ reference locked, object mutable final List<String> items = new ArrayList<>(); items.add("Apple"); // โ mutating the object is fine items.add("Mango"); System.out.println("Items: " + items); // items = new ArrayList<>(); โ compile error: final ref! } public static void main(String[] args) { new FinalVariable("ORD-001", 500).processOrder(); demonstrateFinalRef(); } }
๐ป Program 2 โ final Method & final Class
class Payment { protected double amount; Payment(double amount) { this.amount = amount; } // final method โ security-critical; no subclass can change this logic final void applyGST() { amount *= 1.18; System.out.printf("GST applied. New amount: %.2f%n", amount); } // non-final โ subclasses can customise void processPayment() { System.out.println("Generic payment: " + amount); } } class UpiPayment extends Payment { UpiPayment(double amount) { super(amount); } // โ Cannot override final method: // void applyGST() { } โ compile error @Override void processPayment() { // โ can override non-final System.out.printf("UPI payment via VPA: %.2f%n", amount); } } // final class โ cannot be subclassed final class SSLCertificate { private final String issuer; private final String subject; SSLCertificate(String issuer, String subject) { this.issuer = issuer; this.subject = subject; } void display() { System.out.printf("Cert โ Issuer: %s | Subject: %s%n", issuer, subject); } } // class HackedCert extends SSLCertificate {} โ compile error! public class FinalMethodClass { public static void main(String[] args) { UpiPayment upi = new UpiPayment(1000); upi.applyGST(); // calls Payment.applyGST() โ locked upi.processPayment(); // calls UpiPayment.processPayment() โ overridden new SSLCertificate("DigiCert", "example.com").display(); } }
๐ป Program 3 โ final Parameter & Blank final
public class FinalAdvanced { // Blank final โ declared but not initialised at declaration private final int level; // blank final field FinalAdvanced(int level) { this.level = level; // assigned in constructor โ OK } // final parameter โ cannot be reassigned inside method body static double calculateDiscount(final double price, final double rate) { // price = 0; โ compile error: final parameter return price * rate; } // final in enhanced for โ each iteration variable is effectively final static void printSquares(int[] nums) { for (final int n : nums) { System.out.printf("%d^2 = %d%n", n, n*n); // n++; โ compile error: final } } public static void main(String[] args) { FinalAdvanced fa = new FinalAdvanced(3); System.out.println("Level: " + fa.level); double discount = calculateDiscount(2000, 0.10); System.out.printf("Discount (10%% of 2000): %.2f%n", discount); printSquares(new int[]{2, 3, 4}); } }
final keyword. Lambda expressions and anonymous classes can capture effectively final local variables โ they behave just like explicitly final variables from the compiler's perspective. If you try to reassign an effectively final variable that a lambda captures, you get a compile error.
static final field is a constant.2. final method: cannot be overridden by any subclass. The implementation is locked. Useful for security-critical logic and template method patterns.
3. final class: cannot be extended โ no class can use
extends FinalClass. All methods are implicitly final. Used for security, immutability, and design completeness. Examples: String, Integer, Math.
final reference variable means the reference cannot be reassigned to point to a different object. The object itself can still be modified through its own methods.Example:
final List<String> list = new ArrayList<>();โ
list.add("hello") โ
โ modifying the object is allowedโ
list = new ArrayList<>() โ โ reassigning the reference is forbiddenTo create a truly immutable object: make the class final, make all fields private and final, provide no setters, and return defensive copies of mutable fields.
String is the canonical example.
final instance field that is declared without an initial value at the point of declaration. It must be assigned exactly once before the constructor finishes โ either inside every constructor path or in an instance initialiser block.private final int level; โ blank finalFinalAdvanced(int l) { this.level = l; } โ assigned in constructorBlank finals are useful when the value depends on constructor arguments and cannot be known at class load time. If any constructor path fails to assign it, the compiler reports an error.
String is final for three key reasons:Security: if String could be subclassed, a malicious subclass could override methods like
hashCode() or equals() to break collections, or override internal methods to bypass security checks (e.g., in file paths or network URLs).String pool (interning): Java maintains a pool of String literals. If String were mutable or subclassable, the shared pool would be unsafe โ one reference modifying a pooled String would corrupt all others sharing it.
Thread safety: immutable strings are inherently thread-safe โ no synchronisation needed since the state never changes after creation.
final class can contain methods that are not explicitly marked final. However, since the class itself cannot be extended, those methods effectively become final anyway โ there is no subclass that could override them.Declaring individual methods as
final inside a final class is redundant but not a compile error.
final keyword.Java 8 allows lambda expressions and anonymous classes to capture local variables from the enclosing scope if they are final or effectively final.
int x = 10; // effectively final โ never reassignedRunnable r = () -> System.out.println(x); // โ
If you later add
x = 20, x is no longer effectively final, and the lambda capture becomes a compile error. This rule ensures lambda closures are safe โ they capture a stable value, not a changing variable.
๐ฑ final in Spring Boot
The final keyword is used throughout Spring Boot best practices โ especially for dependency injection fields and configuration constants.
static final constants โ shared configuration keys, header names, path prefixes
final local variables in service methods โ makes data flow traceable and safe
@Value injected final fields โ not directly possible with constructor injection (use @ConfigurationProperties instead)
// โโ Constants file โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ public final class AppConstants { // final class โ not extendable private AppConstants() {} // private constructor โ no instances public static final String API_V1 = "/api/v1"; public static final String AUTH_HEADER = "Authorization"; public static final int MAX_PAGE_SIZE = 100; public static final long JWT_EXPIRY_MS = 86_400_000L; // 24h } // โโ Controller with private final dependencies โโโโโโโโโโโโโโโ @RestController @RequestMapping(AppConstants.API_V1 + "/products") public class ProductController { private final ProductService productService; // immutable ref private final CategoryService categoryService; // immutable ref // Constructor injection โ Spring sets these once, never again public ProductController(ProductService ps, CategoryService cs) { this.productService = ps; this.categoryService = cs; } @GetMapping("/{id}") public ResponseEntity<Product> getById(@PathVariable Long id) { final Product product = productService.findById(id); return ResponseEntity.ok(product); } }
private final for Spring-injected fields (via constructor injection) provides:1. Null safety: if the bean is not provided, the app fails to start โ not with a NullPointerException later
2. Immutability: the dependency cannot be swapped out at runtime
3. Testability: constructor injection makes the dependencies explicit โ easy to pass mocks in unit tests
4. Lombok:
@RequiredArgsConstructor auto-generates the constructor for all private final fields
- final variable โ assigned once only; after that read-only
- final method โ cannot be overridden; can be inherited and called
- final class โ cannot be extended; all methods effectively final
static final= constant; convention: ALL_CAPS_UNDERSCORES- final reference โ immutable object โ reference locked, object mutable
- Blank final = declared without value; must be set in constructor
- final instance field: must be assigned by the time any constructor completes
String,Integer,Mathare all final classes- Effectively final (Java 8+) โ never reassigned; can be captured by lambdas
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Can you call a final method from a subclass? | Yes โ it can be inherited and called | final only blocks OVERRIDING, not calling or inheriting |
| Can a final class have abstract methods? | No โ compile error | abstract requires subclassing to implement; final blocks subclassing โ contradiction |
| final List<String> list; list.add("x") โ valid? | Yes | final locks the reference, not the object contents |
| Where can a blank final field be assigned? | Constructor or instance init block | Cannot be assigned in a regular method after construction |
| Can you override a final method in the same class? | N/A โ same class doesn't override, it defines | final only restricts SUBCLASSES from overriding |
| Is String a final class? | Yes | java.lang.String is final โ cannot be subclassed |
| What happens if a lambda captures a reassigned local variable? | Compile error | Lambda requires variable to be final or effectively final |
๐ One-Liner Definitions
- final variable โ A variable that can be assigned exactly once; read-only after its first assignment.
- final method โ A method that cannot be overridden by any subclass; can still be inherited and called.
- final class โ A class that cannot be subclassed; all its methods are effectively final.
- blank final โ A final instance field declared without an initial value; must be assigned in every constructor path.
- static final constant โ A class-level constant shared across all instances; named in ALL_CAPS_UNDERSCORES by convention.
- effectively final โ A local variable that is never reassigned after initialisation; can be captured by lambdas as if explicitly final (Java 8+).
Object Class โ toString, equals, hashCode
๐๏ธ java.lang.Object โ The Root of All Classes
Every class in Java implicitly extends java.lang.Object. This means every object you ever create โ whether from your own class, a JDK class, or a library โ inherits the methods defined in Object. Three of these methods are critical to override correctly: toString(), equals(), and hashCode().
๐ toString()
toString() is called automatically whenever an object is used in a String context โ printing, concatenation, logging. The default implementation returns something like Student@1b6d3586 which is useless. Always override it to return a meaningful, human-readable representation.
๐ฐ equals()
The default equals() uses reference equality (==): two objects are equal only if they are the exact same object in memory. For value objects (Student, Book, Point), you almost always want content equality: two objects are equal if their meaningful fields have the same values.
#๏ธโฃ hashCode()
hashCode() returns an integer used by hash-based collections (HashMap, HashSet, Hashtable) to locate objects efficiently in a bucket structure. The golden rule: if two objects are equal (via equals()), they MUST return the same hashCode. The reverse is not required โ two objects with the same hashCode can be unequal (a collision).
equals() without hashCode() (or vice versa), your class will behave incorrectly in all hash-based collections: HashMap, HashSet, LinkedHashMap, Hashtable. This is one of the most common subtle bugs in Java. Your IDE can generate both together โ always do so.
java.util.Objects.equals(a, b) โ null-safe: returns false if either is null, never throws NPEjava.util.Objects.hash(field1, field2, ...) โ computes a combined hash of multiple fieldsThese are the preferred building blocks for manual equals/hashCode implementations.
๐ป Program 1 โ Default vs Overridden toString, equals, hashCode
import java.util.Objects; class Student { private String name; private int rollNo; public Student(String name, int rollNo) { this.name = name; this.rollNo = rollNo; } // 1. toString โ human-readable representation @Override public String toString() { return "Student{name='" + name + "', roll=" + rollNo + "}"; } // 2. equals โ content equality by name + rollNo @Override public boolean equals(Object o) { if (this == o) return true; // same ref fast path if (!(o instanceof Student)) return false; // null + type check Student other = (Student) o; return rollNo == other.rollNo && Objects.equals(name, other.name); // null-safe compare } // 3. hashCode โ consistent with equals (same fields) @Override public int hashCode() { return Objects.hash(name, rollNo); // combines both fields } } public class ObjectMethods { public static void main(String[] args) { Student s1 = new Student("Rahul", 101); Student s2 = new Student("Rahul", 101); Student s3 = new Student("Priya", 102); // toString System.out.println(s1); // auto toString() System.out.println("Student: " + s2); // concat toString() // equals System.out.println("s1==s2 (ref): " + (s1 == s2)); // false System.out.println("s1.eq(s2): " + s1.equals(s2)); // true System.out.println("s1.eq(s3): " + s1.equals(s3)); // false System.out.println("s1.eq(null): " + s1.equals(null)); // false (no NPE) // hashCode System.out.printf("hash(s1)=%d hash(s2)=%d match=%b%n", s1.hashCode(), s2.hashCode(), s1.hashCode() == s2.hashCode()); } }
๐ป Program 2 โ Breaking HashSet Without hashCode Override
import java.util.HashSet; import java.util.Objects; // Broken: equals overridden but hashCode is NOT class BrokenBook { String isbn; BrokenBook(String isbn) { this.isbn = isbn; } @Override public boolean equals(Object o) { if (!(o instanceof BrokenBook)) return false; return Objects.equals(isbn, ((BrokenBook)o).isbn); } // hashCode NOT overridden โ uses Object default (memory address) } // Fixed: both equals AND hashCode overridden class FixedBook { String isbn; FixedBook(String isbn) { this.isbn = isbn; } @Override public boolean equals(Object o) { if (!(o instanceof FixedBook)) return false; return Objects.equals(isbn, ((FixedBook)o).isbn); } @Override public int hashCode() { return Objects.hash(isbn); } } public class HashCodeBug { public static void main(String[] args) { // BROKEN โ equals says same, but HashSet stores both! HashSet<BrokenBook> broken = new HashSet<>(); broken.add(new BrokenBook("978-1")); broken.add(new BrokenBook("978-1")); // same ISBN โ should be duplicate! System.out.println("BrokenBook set size: " + broken.size()); // 2 โ BUG! // FIXED โ HashSet correctly deduplicates HashSet<FixedBook> fixed = new HashSet<>(); fixed.add(new FixedBook("978-1")); fixed.add(new FixedBook("978-1")); System.out.println("FixedBook set size: " + fixed.size()); // 1 โ correct } }
๐ป Program 3 โ All Object Methods + instanceof pattern (Java 16+)
import java.util.Objects; class Point { private final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public String toString() { return "Point(" + x + ", " + y + ")"; } // Modern Java 16+ pattern matching โ cleaner equals @Override public boolean equals(Object o) { return o instanceof Point p // pattern var p โ cast + check in one && x == p.x && y == p.y; } @Override public int hashCode() { return Objects.hash(x, y); } } public class ModernEquals { public static void main(String[] args) { Point p1 = new Point(3, 4); Point p2 = new Point(3, 4); Point p3 = new Point(1, 2); System.out.println(p1); // Point(3, 4) System.out.println("p1.equals(p2): " + p1.equals(p2)); // true System.out.println("p1.equals(p3): " + p1.equals(p3)); // false System.out.println("getClass: " + p1.getClass().getSimpleName()); // Symmetry check System.out.println("Symmetric: " + (p1.equals(p2) == p2.equals(p1))); // hashCode consistency System.out.println("Hash match: " + (p1.hashCode() == p2.hashCode())); } }
Eclipse: Right-click โ Source โ Generate hashCode() and equals()
Lombok: add
@EqualsAndHashCode annotation to generate at compile timeJava 16+ Records:
record Point(int x, int y) {} โ equals/hashCode/toString auto-generated!
1. If
a.equals(b) is true, then a.hashCode() == b.hashCode() must be true.2. If
a.hashCode() == b.hashCode(), then a.equals(b) may or may not be true (hash collision is allowed).The reverse is not required โ two unequal objects can share a hash code (this is a collision and degrades HashMap performance to O(n) for that bucket). But two equal objects that return different hash codes will break HashMap, HashSet, Hashtable, and any hash-based collection.
HashMap:
map.get(key2) returns null even though an equal key was put in โ because key2's hash goes to a different bucket than key1's hash.HashSet: stores logically duplicate objects (two objects that are
equals() end up in different buckets, so contains() returns false and both are added).This is a silent bug โ no exception is thrown. The code compiles and runs; it just produces wrong results. IDEs and tools like SonarQube flag this as a critical warning.
1. Reflexive:
x.equals(x) must always be true2. Symmetric:
x.equals(y) iff y.equals(x)3. Transitive: if
x.equals(y) and y.equals(z), then x.equals(z)4. Consistent: repeated calls return same result as long as neither object is modified
5. Null-safe:
x.equals(null) must return false, never throw NullPointerException
toString() automatically in any context that requires a String representation:โ
System.out.println(obj) โ print statementโ
"prefix " + obj โ String concatenationโ
String.valueOf(obj) โ explicit conversionโ Logger frameworks like SLF4J:
log.info("{}", obj)โ
StringBuilder.append(obj)If not overridden, the default returns
ClassName@hexHashCode (e.g., Student@1b6d3586) โ useless for debugging and logging.
Step 1 โ Find the bucket: computes
key.hashCode(), applies a secondary hash function, and uses the result to determine the array index (bucket).Step 2 โ Find the entry: iterates through all entries in that bucket and calls
key.equals(existingKey) to find the exact match.hashCode is for efficiency (quickly narrowing candidates to one bucket); equals is for correctness (finding the exact match). If hashCode is wrong, you reach the wrong bucket and equals never even gets a chance to run.
== compares references โ are the two variables pointing to the exact same object in memory?equals() for String compares content โ do the two strings have the same sequence of characters?String a = "hello";String b = "hello";a == b โ true only because of String pool interning (both point to same literal)String c = new String("hello");a == c โ false (c is a new heap object, not from the pool)a.equals(c) โ true (same content)Always use
equals() for String comparison in logic; == only for reference identity checks.
๐ฑ equals, hashCode, toString in Spring Boot
These three methods have specific implications in Spring Boot โ especially for JPA entities, caching, and logging.
JPA Entity equals() โ use database ID only; if ID is null (transient entity) use reference equality
JPA Entity hashCode() โ return a constant or use only the type; avoid mutable fields
DTOs and value objects โ freely override all three with all meaningful fields
Lombok on entities โ avoid
@Data (generates problematic equals/hashCode); use @Getter @Setter @ToString(exclude={"orders"})
@Entity public class Product { @Id @GeneratedValue private Long id; private String name; private double price; @OneToMany(mappedBy = "product") private List<OrderItem> items; // lazy collection // โ toString โ EXCLUDE lazy collections! @Override public String toString() { return "Product{id=" + id + ", name='" + name + "', price=" + price + "}"; // NOT including items โ would trigger lazy load! } // โ equals โ use DB id only (follow Hibernate best practice) @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Product)) return false; Product other = (Product) o; return id != null && id.equals(other.id); // if id is null (unsaved entity) โ reference equality only } // โ hashCode โ constant, stable across entity lifecycle @Override public int hashCode() { return getClass().hashCode(); // constant per class โ entities in Sets remain stable // even when their id changes from null โ generated value } }
@Data generates equals() and hashCode() using all fields including collection fields. This causes:1. StackOverflowError if two entities have bidirectional relationships (A has List<B>, B has A โ infinite recursion)
2. LazyInitializationException in toString if lazy collections are included
3. Incorrect Set behaviour if entity's id changes (hash changes after persist)
Use
@Getter @Setter @ToString(exclude = "items") instead, and write equals/hashCode manually or with a business-key approach.
- Every class inherits from
java.lang.Object - Default
equals()โ reference equality (==) - Default
hashCode()โ memory-address-based integer - Default
toString()โClassName@hexHashCode - equals-hashCode contract: equals true โ same hashCode (mandatory)
- Override both equals and hashCode together โ always
- equals contract: reflexive, symmetric, transitive, consistent, null-safe
Objects.equals(a,b)โ null-safe;Objects.hash(f1,f2)โ combined hash==for String โ reference;.equals()for String โ content- Java records auto-generate all three methods
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Two objects with equals()=true โ must hashCode() be same? | Yes โ mandatory | Core contract; violating it breaks all hash collections |
| Two objects with same hashCode โ must equals() be true? | No โ collision allowed | Hash collision is valid; equals() is the definitive check |
| Default equals() on two new objects with same fields? | false | Default uses reference (==); objects are different heap locations |
| What does default toString() return? | ClassName@hexHashCode | Not class name alone โ includes @ and hash in hex |
| Override equals but not hashCode โ HashSet.add() twice same value? | Size = 2 (bug!) | Different hashCodes โ different buckets โ duplicate stored |
| "abc" == new String("abc")? | false | new String() creates a new heap object outside the string pool |
| Which Object method is final (cannot be overridden)? | getClass() | wait(), notify(), notifyAll() are also final; toString/equals/hashCode are not |
๐ One-Liner Definitions
- toString() โ Returns a human-readable String representation of an object; called automatically in print and concatenation contexts.
- equals() โ Defines logical equality between objects; must be reflexive, symmetric, transitive, consistent, and null-safe.
- hashCode() โ Returns an integer used by hash collections for bucket placement; must be consistent with equals(): if a.equals(b) then a.hashCode() == b.hashCode().
- equals-hashCode contract โ The rule that equal objects must produce the same hash code; violating it silently breaks HashMap and HashSet.
- Objects.hash() โ Utility method in java.util.Objects that computes a combined hash of multiple fields; preferred over manual prime multiplication.
- Reference equality (==) โ Checks if two variables point to the same object in memory; distinct from content equality tested by equals().
Upcasting & Downcasting
๐ผ๐ฝ What is Casting?
Casting is treating an object reference as a different type. In an inheritance hierarchy you can move the reference up toward the parent (upcasting) or down toward the child (downcasting). The object itself never changes โ only the reference type changes, which determines what members are accessible via that reference.
๐ผ Upcasting
Upcasting assigns a child object to a parent reference. It is implicit (no cast syntax needed) and always safe โ a Dog IS-A Animal, so treating a Dog as an Animal is always valid. The trade-off: you can only access members declared in the parent type through that reference.
๐ฝ Downcasting
Downcasting converts a parent reference back to a child type. It requires an explicit cast operator (ChildType). The compiler allows it (it trusts you), but the JVM checks at runtime whether the actual object really is of that type. If not, it throws ClassCastException.
๐ instanceof Operator
instanceof tests whether an object is of a given type (or a subtype). Use it before every downcast to prevent ClassCastException. Java 16 introduced pattern matching for instanceof that combines the check and cast into one expression.
๐ Memory View โ What Actually Happens
ClassCastException is a runtime exception thrown when you try to downcast an object to a type it is not actually an instance of. The compiler cannot catch this (it only checks whether a cast is possible in the hierarchy, not whether it is safe for the actual object). Always guard explicit downcasts with instanceof.
๐ป Program 1 โ Upcast, Downcast, ClassCastException
class Animal { String name; Animal(String name) { this.name = name; } void eat() { System.out.println(name + " eats"); } } class Dog extends Animal { String breed; Dog(String name, String breed) { super(name); this.breed = breed; } void bark() { System.out.println(name + " (" + breed + "): Woof!"); } } class Cat extends Animal { Cat(String name) { super(name); } void meow() { System.out.println(name + ": Meow!"); } } public class CastingDemo { public static void main(String[] args) { // โโ UPCASTING โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Dog dog = new Dog("Bruno", "Labrador"); Animal a = dog; // implicit upcast โ no cast syntax a.eat(); // โ in Animal // a.bark(); // โ compile error โ not in Animal // โโ SAFE DOWNCAST โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Dog d2 = (Dog) a; // explicit downcast โ actual obj IS Dog d2.bark(); // โ now accessible // โโ instanceof GUARD before downcast โโโโโโโโโโโโโโโโโโโโโโ Animal[] animals = { new Dog("Rex", "German Shepherd"), new Cat("Whiskers"), new Dog("Max", "Poodle"), new Cat("Luna") }; for (Animal animal : animals) { animal.eat(); // polymorphic โ all animals eat if (animal instanceof Dog d) { // Java 16 pattern matching d.bark(); // d already cast โ no manual cast } else if (animal instanceof Cat c) { c.meow(); } } // โโ ClassCastException demo โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ try { Animal cat = new Cat("Luna"); Dog wrongCast = (Dog) cat; // Cat โ Dog: RUNTIME error } catch (ClassCastException e) { System.out.println("Caught: " + e.getClass().getSimpleName()); } } }
๐ป Program 2 โ instanceof Chain & getClass()
public class InstanceofDemo { public static void main(String[] args) { Dog dog = new Dog("Bruno", "Labrador"); // instanceof checks the full inheritance chain System.out.println("instanceof Dog: " + (dog instanceof Dog)); System.out.println("instanceof Animal: " + (dog instanceof Animal)); System.out.println("instanceof Object: " + (dog instanceof Object)); System.out.println("instanceof Cat: " + (dog instanceof Cat)); // null instanceof anything โ false (never NPE) Animal nullAnimal = null; System.out.println("null instanceof: " + (nullAnimal instanceof Animal)); // getClass() vs instanceof โ exact type vs hierarchy Animal a = new Dog("Rex", "Husky"); System.out.println("getClass: " + a.getClass().getSimpleName()); // Dog System.out.println("a instanceof Animal: " + (a instanceof Animal)); // true System.out.println("getClass==Animal: " + (a.getClass() == Animal.class)); // false โ exact type is Dog } }
๐ป Program 3 โ Practical Downcast: Shape Renderer
abstract class Shape { abstract String type(); } class Circle extends Shape { double radius; Circle(double r) { radius = r; } String type() { return "Circle"; } } class Rect extends Shape { double w, h; Rect(double w, double h) { this.w=w; this.h=h; } String type() { return "Rect"; } } public class ShapeRenderer { // Process a mixed array of shapes โ downcast only when needed static void render(Shape[] shapes) { for (Shape s : shapes) { // Common behaviour via polymorphism โ no cast needed System.out.print("Rendering " + s.type() + ": "); // Type-specific behaviour โ downcast with pattern matching if (s instanceof Circle c) System.out.printf("radius=%.1f, area=%.2f%n", c.radius, Math.PI * c.radius * c.radius); else if (s instanceof Rect r) System.out.printf("w=%.1f h=%.1f, area=%.2f%n", r.w, r.h, r.w * r.h); } } public static void main(String[] args) { render(new Shape[] { new Circle(5), new Rect(4,6), new Circle(3) }); } }
JVM check: Is the actual heap object really of that type? Cat object cast to Dog โ
ClassCastException at runtime.You can fool the compiler (it can't see what's in the variable at runtime), but you can't fool the JVM.
Trade-off: through the parent reference you can only access members declared in the parent type. Child-specific members are not accessible (compile error). However, if the child overrides a parent method, that overridden version still runs at runtime via Dynamic Method Dispatch.
(Dog) animal.It can fail because the compiler only checks whether the cast is possible in the class hierarchy โ it cannot know what actual object is in the variable at runtime. If the actual heap object is a
Cat and you cast to Dog, the JVM throws ClassCastException at runtime.Prevention: always check with
instanceof before downcasting. Java 16 pattern matching (instanceof Dog d) makes this a single safe step.
ClassCastException is a RuntimeException thrown by the JVM when an explicit downcast fails at runtime โ i.e., the actual object is not an instance of the target type.Example:
Animal a = new Cat(); Dog d = (Dog) a; โ the compiler allows this (Dog and Cat are both in the Animal hierarchy, so the cast is structurally possible). At runtime the JVM discovers the heap object is a Cat, not a Dog, and throws ClassCastException.It is never thrown by upcasting or by
instanceof checks. The only way it can occur is from an explicit cast operator (Type).
instanceof: returns true if the object is an instance of the specified type or any subtype. It checks the full inheritance chain. dog instanceof Animal โ true.getClass(): returns the exact runtime class of the object. dog.getClass() == Animal.class โ false (it returns Dog.class). For exact-type equality you use getClass() == SomeClass.class.Use
instanceof in equals() when subclassing should be considered equal. Use getClass() when only objects of the exact same type should be equal (strict type check). The choice affects how subclass instances behave in equals() comparisons โ a frequently debated design decision.
This is why upcasting and downcasting are very cheap operations โ there is no data copying, no allocation, no constructor call. The JVM simply changes how the compiler checks which members are accessible through that reference variable.
(String) new Dog() is a compile error because String and Dog share no ancestry beyond Object, and the compiler knows a Dog can never be a String.The compiler only allows casts between types that could theoretically be related at runtime (parent-child or same hierarchy). The JVM then enforces the actual runtime check with ClassCastException if needed.
๐ฑ Casting in Spring Boot
Casting appears in Spring Boot when working with the ApplicationContext, event listeners, bean retrieval, and handler method arguments.
ApplicationEvent subclasses โ event listener receives parent type, downcasts to access payload
HttpServletRequest / ServletRequest โ upcasted in filter chain, downcasted when servlet-specific features needed
Spring Security Authentication โ cast to specific implementation (e.g., UsernamePasswordAuthenticationToken) to access custom principal
ResponseEntity body โ upcasted to Object for generic method signatures
// โโ ApplicationEvent downcast in listener โโโโโโโโโโโโโโโโโโโ public class OrderEvent extends ApplicationEvent { private final String orderId; public OrderEvent(Object source, String orderId) { super(source); this.orderId = orderId; } public String getOrderId() { return orderId; } } @Component public class OrderEventListener { // ApplicationEvent is the parent โ downcast to OrderEvent for payload @EventListener public void handleEvent(ApplicationEvent event) { if (event instanceof OrderEvent oe) { // safe downcast System.out.println("Order placed: " + oe.getOrderId()); } } } // โโ Security context โ cast principal to custom UserDetails โ @RestController public class ProfileController { @GetMapping("/profile") public String getProfile(@AuthenticationPrincipal Object principal) { if (principal instanceof CustomUserDetails u) { // pattern match return "Hello, " + u.getDisplayName(); } return "Hello, anonymous"; } }
ApplicationEvent and downcasting, you can directly type the listener to your specific event: @EventListener public void handle(OrderEvent event). Spring will only invoke this listener when an OrderEvent is published โ no downcast needed. This is the preferred, cleaner approach.
- Upcasting โ implicit, always safe, child โ parent
- Downcasting โ explicit
(Type), runtime-checked, parent โ child ClassCastExceptionโ runtime exception from failed downcast- Compiler checks: is the cast possible? JVM checks: is the actual object that type?
- Casting never creates a new object โ only reference type changes
instanceofโ checks type AND full hierarchy;null instanceof Xโ alwaysfalsegetClass()โ exact runtime type only (not hierarchy)- Through upcast reference โ can only access parent members (compile check)
- Through upcast reference โ overridden methods still call child version (DMD)
- Java 16 pattern matching:
instanceof Dog dโ check + cast in one step
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Animal a = new Dog(); โ is this upcasting? | Yes โ implicit upcast | Dog IS-A Animal; no cast syntax needed; always safe |
| Dog d = (Dog) new Cat(); โ compile error or runtime error? | Runtime โ ClassCastException | Compiler allows (same hierarchy); JVM catches actual type mismatch |
| Does casting create a new object? | No | Only the reference type changes; same heap object |
| null instanceof Animal? | false โ never NPE | instanceof always returns false for null; safe to call without null check |
| Animal a = new Dog(); a.bark() โ compile error? | Yes โ compile error | bark() not declared in Animal; compiler uses reference type for access check |
| instanceof vs getClass() for hierarchy? | instanceof checks hierarchy; getClass() exact type only | dog instanceof Animal โ true; dog.getClass()==Animal.class โ false |
| Dog d = (Dog) someAnimalRef; โ what does compiler check? | Only that Dog and Animal are in the same hierarchy | Compiler cannot know the actual object type at compile time |
๐ One-Liner Definitions
- Upcasting โ Implicit assignment of a child object to a parent reference; always safe; restricts accessible members to the parent type.
- Downcasting โ Explicit cast of a parent reference back to a child type; requires
(ChildType)syntax; checked by JVM at runtime. - ClassCastException โ Runtime exception thrown when a downcast fails because the actual object is not an instance of the target type.
- instanceof โ Operator that returns true if an object is an instance of the specified type or any subtype; returns false for null.
- Pattern matching instanceof (Java 16) โ Combined type check and cast:
if (x instanceof Dog d)binds d as a Dog if the check passes. - getClass() โ Returns the exact runtime class of an object; unlike instanceof, does not check supertypes.
Abstraction
๐ญ What is Abstraction?
Abstraction means hiding the implementation details and showing only the essential features to the outside world. It answers: "What does this do?" โ not "How does it do it?". You use a car without knowing how the engine works; you call list.sort() without knowing the sorting algorithm.
Encapsulation: hides data / state โ bundles fields with methods and controls access. "How its data is protected." Achieved via private fields + getters/setters.
A class can be both: fields are encapsulated (private), behaviour is abstracted (method signature visible, body hidden in subclass).
๐๏ธ Abstract Class
An abstract class is declared with the abstract keyword. It can have abstract methods (signature only, no body) and concrete methods (with a body). It cannot be instantiated directly โ you must create a concrete subclass that implements all abstract methods.
๐ Abstract Class Rules
| Rule | Detail |
|---|---|
| Declared with | abstract keyword before class |
| Instantiation | Cannot be instantiated โ compile error on new AbstractClass() |
| Abstract methods | Can have zero or more โ no body, ends with semicolon |
| Concrete methods | Can have any number of fully implemented methods |
| Fields & constructors | Can have both โ constructor called via super() from subclass |
| Subclass contract | Concrete subclass MUST implement ALL abstract methods โ or be abstract itself |
| Access modifiers | Abstract methods can be public, protected, or default โ NOT private |
| static abstract | NOT allowed โ abstract methods need overriding; static cannot be overridden |
| final abstract | NOT allowed โ contradiction (final cannot be overridden; abstract must be) |
๐ Abstract Class vs Interface โ Quick Comparison
| Abstract Class | Interface | |
|---|---|---|
| Keyword | abstract class | interface |
| Multiple inheritance | Only one (extends) | Multiple (implements) |
| Fields | Any type (instance + static) | Only public static final |
| Constructor | Yes (called via super) | No |
| Access modifiers | Any modifier on members | All methods implicitly public |
| Use when | Shared state + partial implementation + IS-A | Pure contract, multiple types |
๐ฏ When to Use Abstract Class
๐ Template Method Pattern
One of the most important design patterns, enabled directly by abstract classes. The abstract class defines a final template method that calls abstract "hook" methods. Subclasses implement the hooks but cannot change the overall algorithm skeleton.
๐ป Program 1 โ Abstract Shape Hierarchy
abstract class Shape { protected String color; Shape(String color) { this.color = color; } // Abstract methods โ subclasses MUST implement abstract double area(); abstract double perimeter(); // Concrete method โ shared behaviour, calls abstract via DMD void describe() { System.out.printf("%-12s color:%-7s area:%8.3f perim:%7.3f%n", getClass().getSimpleName(), color, area(), perimeter()); } } class Circle extends Shape { private double r; Circle(String c, double r) { super(c); this.r = r; } @Override public double area() { return Math.PI * r * r; } @Override public double perimeter() { return 2 * Math.PI * r; } } class Rectangle extends Shape { private double w, h; Rectangle(String c, double w, double h) { super(c); this.w=w; this.h=h; } @Override public double area() { return w * h; } @Override public double perimeter() { return 2 * (w + h); } } class Triangle extends Shape { private double a, b, c; Triangle(String col, double a, double b, double c) { super(col); this.a=a; this.b=b; this.c=c; } @Override public double area() { double s = (a+b+c)/2; return Math.sqrt(s*(s-a)*(s-b)*(s-c)); } @Override public double perimeter() { return a+b+c; } } public class AbstractShape { public static void main(String[] args) { // new Shape("red"); โ compile error: cannot instantiate abstract Shape[] shapes = { new Circle ("Red", 7), new Rectangle("Blue", 4, 6), new Triangle ("Green", 3, 4, 5) }; double totalArea = 0; for (Shape s : shapes) { s.describe(); // concrete method calls abstract area()/perimeter() via DMD totalArea += s.area(); } System.out.printf("Total area: %.3f%n", totalArea); } }
๐ป Program 2 โ Template Method Pattern
// Abstract class defines the SKELETON โ subclasses fill the STEPS abstract class ReportGenerator { // Template method โ final so no subclass can change the order final void generate() { fetchData(); processData(); formatReport(); exportReport(); } abstract void fetchData(); // step 1 โ subclass decides source abstract void processData(); // step 2 โ subclass decides logic abstract void formatReport(); // step 3 โ subclass decides format void exportReport() { // concrete โ default export System.out.println("[EXPORT] Saved to /reports/"); } } class PdfReportGenerator extends ReportGenerator { @Override void fetchData() { System.out.println("[PDF] Fetching from DB"); } @Override void processData() { System.out.println("[PDF] Aggregating rows"); } @Override void formatReport() { System.out.println("[PDF] Applying PDF layout"); } } class ExcelReportGenerator extends ReportGenerator { @Override void fetchData() { System.out.println("[XLS] Fetching from REST API"); } @Override void processData() { System.out.println("[XLS] Pivoting data"); } @Override void formatReport() { System.out.println("[XLS] Applying cell styles"); } @Override void exportReport() { System.out.println("[XLS] Emailing the file"); } } public class TemplateMethod { public static void main(String[] args) { System.out.println("=== PDF Report ==="); new PdfReportGenerator().generate(); System.out.println("=== Excel Report ==="); new ExcelReportGenerator().generate(); } }
๐ป Program 3 โ Abstract Class with Constructor & Partial Implementation
abstract class Employee { protected String name; protected double baseSalary; // Abstract class CAN have a constructor (called via super) Employee(String name, double baseSalary) { this.name = name; this.baseSalary = baseSalary; } // Abstract โ each employee type calculates bonus differently abstract double calculateBonus(); // Concrete โ shared total salary logic double totalSalary() { return baseSalary + calculateBonus(); } void printPayslip() { System.out.printf("%-15s | Base:%8.2f | Bonus:%7.2f | Total:%8.2f%n", name, baseSalary, calculateBonus(), totalSalary()); } } class Manager extends Employee { int teamSize; Manager(String n, double b, int t) { super(n,b); teamSize=t; } @Override double calculateBonus() { return baseSalary * 0.20 + teamSize * 500; } } class Developer extends Employee { String level; // "junior" | "senior" Developer(String n, double b, String l) { super(n,b); level=l; } @Override double calculateBonus() { return level.equals("senior") ? baseSalary * 0.15 : baseSalary * 0.05; } } class Intern extends Employee { Intern(String n, double b) { super(n,b); } @Override double calculateBonus() { return 0; } // no bonus } public class AbstractEmployee { public static void main(String[] args) { Employee[] staff = { new Manager ("Anita Sharma", 80000, 5), new Developer("Rohan Verma", 60000, "senior"), new Developer("Priya Singh", 40000, "junior"), new Intern ("Arjun Mehta", 15000) }; for (Employee e : staff) e.printPayslip(); } }
abstract with no abstract methods is perfectly legal. It simply cannot be instantiated. This is useful when you want to prevent direct instantiation of a base class while still providing concrete utility methods โ forcing callers to use a specific subclass.
In Java, abstraction is achieved through two mechanisms:
1. Abstract classes โ partially abstract; can mix abstract methods (no body) with concrete methods (with body). Use when sharing state and partial implementation among related types.
2. Interfaces โ fully abstract contract (before Java 8); use when defining a pure contract that unrelated types can implement. Covered fully in Topics 25โ26.
new Shape() where Shape is abstract โ "Shape is abstract; cannot be instantiated".However, you can:
โ Create an instance of a concrete subclass and assign it to an abstract class reference:
Shape s = new Circle("red", 5);โ Create an anonymous subclass inline:
Shape s = new Shape("red") { double area() { return 0; } double perimeter() { return 0; } }; โ this is a one-time concrete subclass defined anonymously.
super() from the concrete subclass constructor to initialise the shared fields defined in the abstract class.Example:
abstract class Employee { Employee(String name, double salary) { this.name = name; } }class Manager extends Employee { Manager(String n, double s) { super(n, s); } }Without a constructor in the abstract class, every subclass would have to initialise those shared fields itself โ defeating the purpose of the shared base class.
โ Has no body โ ends with a semicolon:
abstract double area();โ Must be declared inside an abstract class or interface
โ Cannot be private (private means not inherited, but abstract requires overriding)
โ Cannot be static (static methods cannot be overridden)
โ Cannot be final (final cannot be overridden, but abstract must be)
โ Every concrete subclass must provide an implementation, or itself be declared abstract
Interface: fields are implicitly public static final (constants). No instance fields. No constructors. Methods are implicitly public abstract (pre-Java 8); can have default and static methods (Java 8+). A class can implement multiple interfaces. Best for defining a pure contract that unrelated types can fulfil.
Rule of thumb: if related classes share state and behaviour โ abstract class. If unrelated types need to fulfil a contract โ interface.
The abstract class controls the flow (the template method is final and cannot be overridden). Subclasses customise individual steps without changing the overall structure.
Real-world uses: Spring's
JdbcTemplate (fixed DB operation skeleton, customisable query/mapping), AbstractHealthIndicator in Spring Actuator (template calls doHealthCheck() which subclasses implement), servlet lifecycle (HttpServlet.service() calls doGet()/doPost() which subclasses override).
๐ฑ Abstract Classes in Spring Boot
Spring Boot uses abstract classes extensively in its own framework code, and you write abstract base classes for shared service/controller logic.
AbstractHealthIndicator โ Spring Actuator base; override
doHealthCheck() to add custom health checksAbstractMessageConverterMethodArgumentResolver โ Spring MVC internals
Abstract integration test base โ common
@SpringBootTest setup in an abstract class; test classes extend itAbstract scheduled job โ shared logging/metrics in abstract base; concrete jobs implement the work method
// Abstract base service โ template method pattern in Spring public abstract class AbstractCrudService<T, ID> { // Template method โ fixed workflow, subclass fills the steps public final T create(T entity) { validate(entity); // abstract โ subclass validates beforeSave(entity); // hook โ optional override T saved = save(entity); // abstract โ subclass persists afterSave(saved); // hook โ optional override return saved; } protected abstract void validate(T entity); protected abstract T save(T entity); // Hooks โ concrete no-ops by default; subclass may override protected void beforeSave(T entity) {} protected void afterSave (T saved) {} } @Service public class ProductService extends AbstractCrudService<Product, Long> { private final ProductRepository repo; public ProductService(ProductRepository r) { this.repo = r; } @Override protected void validate(Product p) { if (p.getPrice() <= 0) throw new IllegalArgumentException("Price must be positive"); } @Override protected Product save(Product p) { return repo.save(p); } @Override protected void afterSave(Product p) { System.out.println("Product saved: " + p.getName()); } }
AbstractHealthIndicator is a textbook template method pattern. You extend it and implement doHealthCheck(Health.Builder builder). The abstract class handles the try-catch, status propagation, and integration with the Actuator health endpoint. Your subclass just fills in the check logic โ the infrastructure is handled for you.
- Abstract class declared with
abstractkeyword - Cannot instantiate abstract class โ compile error
- Abstract class CAN have: constructor, fields, concrete methods, static methods
- Abstract method: no body, ends with
;, not private/static/final - Concrete subclass must implement ALL abstract methods โ or itself be abstract
- Abstract class with 0 abstract methods is valid โ just blocks instantiation
abstract+finalon same class โ compile error (contradiction)abstract+privateon same method โ compile errorabstract+staticon same method โ compile error- Template Method Pattern = abstract class defines skeleton + subclasses fill steps
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Can abstract class have concrete methods? | Yes | It can mix abstract and concrete methods freely |
| Can abstract class have a constructor? | Yes | Called via super() from subclass โ not for direct instantiation |
| abstract + final class โ valid? | No โ compile error | final blocks subclassing; abstract requires it โ direct contradiction |
| abstract + private method โ valid? | No โ compile error | private not inherited; abstract requires overriding โ contradiction |
| Abstract class with 0 abstract methods โ valid? | Yes | Still cannot be instantiated; forces use of subclass |
| Concrete subclass doesn't implement one abstract method โ valid? | No โ unless subclass is also abstract | Every concrete subclass must implement ALL abstract methods |
| Can abstract method be static? | No โ compile error | static methods cannot be overridden โ no-op for abstract |
๐ One-Liner Definitions
- Abstraction โ OOP principle of hiding implementation details and exposing only the essential interface; achieved via abstract classes and interfaces.
- Abstract class โ A class declared with
abstract; cannot be instantiated; may contain abstract and concrete members. - Abstract method โ A method with no body, declared with
abstract; must be implemented by every concrete subclass. - Concrete subclass โ A non-abstract subclass that provides implementations for all inherited abstract methods.
- Template Method pattern โ A design pattern where an abstract class defines a fixed algorithm skeleton in a final method, and subclasses implement the variable steps via abstract methods.
- Anonymous subclass โ An inline, unnamed implementation of an abstract class or interface, defined with
new AbstractClass() { ... }.
Inner Classes
๐ช What is an Inner Class?
An inner class (nested class) is a class defined inside another class. Java supports four kinds. Each has a different relationship with the enclosing class and different rules for instantiation and member access.
1๏ธโฃ Non-Static Inner Class (Member Inner Class)
Defined as a member of the outer class without the static keyword. It is associated with an instance of the outer class โ it can directly access all instance and static members of the outer class, including private ones.
2๏ธโฃ Static Nested Class
Declared with static inside an outer class. It is NOT associated with any outer instance. It can only access the static members of the outer class directly. It behaves like a regular top-level class that happens to live in the outer class namespace.
3๏ธโฃ Local Class
Defined inside a method body. It is only visible within that method. It can access the method's local variables if they are final or effectively final.
4๏ธโฃ Anonymous Inner Class
A class declared and instantiated in a single expression with no name. It extends a class or implements an interface inline. Widely used before Java 8 lambdas โ now partially replaced by lambdas, but still required for multi-method interfaces and abstract classes.
๐ All Four Types โ Summary Table
| Non-Static Inner | Static Nested | Local | Anonymous | |
|---|---|---|---|---|
| Needs outer instance | โ Yes | โ No | N/A (in method) | N/A |
| Access outer instance members | โ Yes | โ No | โ Effectively final | โ Effectively final |
| Can have static members | โ No* | โ Yes | โ No | โ No |
| Has a name | โ Yes | โ Yes | โ Yes (local) | โ No |
| Generates separate .class | Outer$Inner.class | Outer$Nested.class | Outer$1Local.class | Outer$1.class |
๐ป Program 1 โ Non-Static Inner Class & Static Nested Class
class Car { private String brand; private int year; private static int totalCars = 0; Car(String brand, int year) { this.brand = brand; this.year = year; totalCars++; } // 1. Non-static inner class โ tied to Car instance class Engine { private int horsepower; Engine(int hp) { this.horsepower = hp; } void start() { // Accesses outer class PRIVATE fields directly System.out.println("Engine of " + brand // outer private + " (" + year + ") started โ " // outer private + horsepower + " HP"); } void showOuterRef() { // Explicit outer reference: Car.this System.out.println("Outer: " + Car.this.brand); } } // 2. Static nested class โ independent of any Car instance static class Factory { static Car makeSuv(String brand) { // Can access static outer members System.out.println("[Factory] Total cars so far: " + totalCars); return new Car(brand, 2024); // Cannot access: brand, year (instance members) } } } public class InnerClasses { public static void main(String[] args) { // Static nested โ no Car instance needed Car suv = Car.Factory.makeSuv("Toyota"); // Non-static inner โ needs Car instance first Car car = new Car("Honda", 2023); Car.Engine eng = car.new Engine(150); // outerRef.new InnerClass() eng.start(); eng.showOuterRef(); } }
๐ป Program 2 โ Anonymous Inner Class (Abstract Class & Interface)
import java.util.Arrays; import java.util.Comparator; import java.util.List; abstract class Greeting { abstract void greet(String name); void farewell(String name) { System.out.println("Goodbye, " + name); } } public class AnonymousClass { public static void main(String[] args) { // 1. Anonymous class extending abstract class Greeting formal = new Greeting() { @Override public void greet(String name) { System.out.println("Good morning, " + name + "."); } }; formal.greet("Dr. Sharma"); formal.farewell("Dr. Sharma"); // inherited concrete method // 2. Anonymous class implementing interface Runnable task = new Runnable() { @Override public void run() { System.out.println("Task running in anonymous class"); } }; task.run(); // 3. Anonymous Comparator โ sorting with custom logic List<String> names = Arrays.asList("Rohan", "Asha", "Priya", "Ali"); names.sort(new Comparator<String>() { @Override public int compare(String a, String b) { return a.length() - b.length(); // sort by length } }); System.out.println("Sorted by length: " + names); // Lambda equivalent (Java 8+) โ replaces single-method anonymous class names.sort((a, b) -> a.compareTo(b)); System.out.println("Sorted alphabetically: " + names); } }
๐ป Program 3 โ Local Class & Outer Reference Access
public class OrderProcessor { private String currency = "INR"; void processOrders(int[] orderIds) { final String prefix = "ORD"; // effectively final local var // Local class โ only visible inside this method class OrderLabel { String format(int id, double amount) { // Accesses: outer instance field (currency) // + effectively final local var (prefix) return prefix + "-" + id + " [" + currency + " " + amount + "]"; } } OrderLabel label = new OrderLabel(); double amount = 1000.0; for (int id : orderIds) { System.out.println(label.format(id, amount)); amount += 500; } // OrderLabel cannot be used outside this method } public static void main(String[] args) { new OrderProcessor().processOrders(new int[]{101, 102, 103}); } }
outerRef.new Inner().2. Static nested class: declared with
static inside the outer class; not tied to any outer instance; can only access static outer members; instantiated with new Outer.Nested().3. Local class: declared inside a method body; visible only within that method; can access effectively final local variables.
4. Anonymous inner class: unnamed class declared and instantiated in a single expression; extends a class or implements an interface inline; cannot have a constructor; partially replaced by lambdas for single-method interfaces.
outerRef.new Inner():Car car = new Car("Honda", 2023);Car.Engine engine = car.new Engine(150);Static nested class: does not need an outer instance; instantiated like a regular class but with the outer class as a namespace qualifier:
Car.Factory factory = new Car.Factory(); or Car car = Car.Factory.makeSuv("Toyota");The key difference: non-static inner needs
outerInstance.new; static nested uses new Outer.Nested().
Lambda: only for functional interfaces (exactly one abstract method); more concise; does not create a new scope for
this (this inside a lambda refers to the enclosing class); does not generate a separate .class file at compile time (uses invokedynamic bytecode).Use anonymous class when: implementing a multi-method interface, subclassing an abstract class, or needing per-instance state. Use lambda for functional interfaces โ it is cleaner and more performant.
Java's solution: the class gets a copy of the captured variable's value. For this copy to be meaningful, the variable must never change โ it must be final or effectively final. If you try to reassign a captured variable, the compiler rejects it with: "local variable used in lambda expression must be final or effectively final".
OuterClass.this.You need
OuterClass.this explicitly only when there is a name conflict โ for example, when both the inner class and outer class have a field called name. Inside the inner class, name refers to the inner field; Car.this.name explicitly refers to the outer field.Normally, outer members are accessed directly (no prefix needed) because the compiler resolves them via the implicit outer reference automatically.
static final int MAX = 100; is allowed inside a non-static inner class because the value is inlined by the compiler and does not actually require a class-level context.If you need a nested class with static members, use a static nested class instead.
๐ฑ Inner Classes in Spring Boot
Inner classes appear in Spring Boot in configuration, test utilities, builder patterns, and event/DTO design.
Product.Builder or ApiResponse.Error as static nested classes for clean namespacingStatic nested @Configuration โ inner
@Configuration classes inside a parent config for grouping related beansAnonymous Comparator / Callable โ pre-Java-8 style; now mostly replaced by lambdas
Test base class โ abstract test class with
@SpringBootTest; test classes extend itStatic nested exception classes โ domain-specific exceptions as static nested classes of a service
// Clean REST response wrapper using static nested classes public class ApiResponse<T> { private final boolean success; private final T data; private final Error error; private ApiResponse(boolean s, T d, Error e) { success=s; data=d; error=e; } public static <T> ApiResponse<T> ok(T data) { return new ApiResponse<>(true, data, null); } public static <T> ApiResponse<T> fail(String code, String msg) { return new ApiResponse<>(false, null, new Error(code, msg)); } // Static nested class โ no outer instance needed, clean namespace public static class Error { private final String code, message; Error(String code, String msg) { this.code=code; this.message=msg; } public String getCode () { return code; } public String getMessage() { return message; } } // Getters omitted for brevity } // Usage in controller: // return ResponseEntity.ok(ApiResponse.ok(product)); // return ResponseEntity.badRequest().body(ApiResponse.fail("NOT_FOUND", "..."));
- Non-static inner: instance-tied, accesses all outer members, instantiated with
outer.new Inner() - Static nested: no outer instance needed, accesses only outer static members, instantiated with
new Outer.Nested() - Local class: defined inside method, visible only there, captures effectively final vars
- Anonymous class: no name, declared + instantiated once, extends class OR implements interface
- Non-static inner class cannot have static members (except static final constants)
- Anonymous class cannot have a constructor
- Anonymous class vs lambda: lambda only for single-abstract-method (functional) interfaces
OuterClass.thisโ explicit outer reference from inside inner class- Non-static inner class holds implicit outer reference โ potential memory leak
- Each inner class generates a separate
.classfile at compile time
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Can non-static inner class access private outer fields? | Yes | Inner class has full access to all outer members including private |
| Can static nested class access outer instance fields? | No | No outer instance โ only static outer members accessible |
| Anonymous class implements interface with 2 methods โ compile error? | No โ valid | Anonymous classes can implement multi-method interfaces; lambdas cannot |
| Anonymous class can have a constructor? | No | No name = no constructor; initialise with instance initialiser block instead |
| Local class can access non-final local variable? | No โ compile error | Must be final or effectively final; compiler copies the value |
How to instantiate non-static inner: new Outer.Inner()? | Wrong โ compile error | Correct syntax: outerInstance.new Inner() |
| this inside anonymous class refers to? | The anonymous class instance | Unlike lambda, anonymous class creates its own scope; this = anonymous object |
๐ One-Liner Definitions
- Non-static inner class โ A class defined as an instance member of another class; holds an implicit reference to the outer instance; can access all outer members.
- Static nested class โ A class declared with
staticinside another class; independent of any outer instance; accesses only outer static members. - Local class โ A class defined inside a method body; scoped to that method; can capture effectively final local variables.
- Anonymous inner class โ An unnamed class declared and instantiated as a single expression; extends one class or implements one interface; has no constructor.
- Effectively final โ A local variable that is never reassigned after initialisation; can be captured by local/anonymous classes and lambdas.
- OuterClass.this โ The explicit syntax used inside a non-static inner class to refer to the enclosing outer class instance.
Interfaces โ Fundamentals
๐ What is an Interface?
An interface is a pure contract โ it defines what a class must do, without saying how. Any class that implements an interface must provide a concrete body for every abstract method in that interface. Interfaces achieve full abstraction (before Java 8) and are the primary vehicle for multiple inheritance of type in Java.
๐ Interface Syntax & Default Modifiers
Members of an interface have implicit (compiler-added) modifiers. You can write them explicitly, but they are redundant.
public. The interface declares it as public abstract; any implementation that reduces visibility (e.g., to package-private) would violate the contract and causes a compile error. This is a very common exam trap.
๐ Multiple Interface Implementation
A class can implement multiple interfaces โ this is how Java achieves multiple inheritance of type without the Diamond Problem that plagued multiple class inheritance.
๐ Interface Reference Type (Polymorphism)
An interface acts as a reference type. You can hold any implementing object in an interface variable โ full runtime polymorphism applies.
๐ Interface vs Abstract Class โ When to Use Which
| Feature | Interface | Abstract Class |
|---|---|---|
| Keyword | interface / implements | abstract class / extends |
| Multiple inheritance | โ implement many | โ extend only one |
| Constructor | โ none | โ yes |
| Instance fields | โ only constants | โ any fields |
| Concrete methods | Only default/static (Java 8+) | โ freely |
| Access modifiers on methods | implicitly public | any modifier |
| Relationship | CAN-DO capability | IS-A relationship |
| Use when | Unrelated classes need same capability | Related classes share state + behaviour |
default methods, interfaces can now provide method implementations too. Modern design favours interfaces: they allow multiple inheritance of type, are easier to mock in tests, and decouple code more effectively. Use abstract classes only when you genuinely need shared state (instance fields) or a protected/package-private contract.
๐ป Program 1 โ Interface Basics: Vehicle Contract
interface Vehicle { // implicitly public static final int MAX_SPEED = 200; String FUEL_TYPE = "Petrol"; // implicitly public abstract void start(); void stop(); int getSpeed(); } interface Electric { void charge(); int getBatteryLevel(); } class PetrolCar implements Vehicle { private int speed = 0; public void start() { speed = 80; System.out.println("Petrol car started"); } public void stop() { speed = 0; System.out.println("Petrol car stopped"); } public int getSpeed() { return speed; } } // Implements TWO interfaces โ multiple inheritance of type class ElectricCar implements Vehicle, Electric { private int speed = 0; private int battery = 100; public void start() { speed = 120; System.out.println("EV started silently"); } public void stop() { speed = 0; System.out.println("EV stopped"); } public int getSpeed() { return speed; } public void charge() { battery = 100; System.out.println("Charging to 100%"); } public int getBatteryLevel() { return battery; } } public class InterfaceBasics { static void testDrive(Vehicle v) { // accepts ANY Vehicle v.start(); System.out.println("Speed: " + v.getSpeed() + " | Max: " + Vehicle.MAX_SPEED); v.stop(); } public static void main(String[] args) { testDrive(new PetrolCar()); testDrive(new ElectricCar()); // Multiple reference types for ElectricCar ElectricCar ev = new ElectricCar(); Vehicle vRef = ev; // upcast to Vehicle Electric eRef = ev; // upcast to Electric eRef.charge(); System.out.println("Battery: " + eRef.getBatteryLevel() + "%"); } }
๐ป Program 2 โ Interface Polymorphism: Payment Gateway
interface PaymentGateway { boolean processPayment(double amount); String getGatewayName(); double getTransactionFee(double amount); } class RazorpayGateway implements PaymentGateway { public boolean processPayment(double amt) { System.out.printf(" Razorpay: processing โน%.2f%n", amt); return true; } public String getGatewayName() { return "Razorpay"; } public double getTransactionFee(double a) { return a * 0.02; } // 2% } class PayPalGateway implements PaymentGateway { public boolean processPayment(double amt) { System.out.printf(" PayPal: processing $%.2f%n", amt); return true; } public String getGatewayName() { return "PayPal"; } public double getTransactionFee(double a) { return a * 0.029 + 0.30; } // 2.9% + $0.30 } class Checkout { private final PaymentGateway gateway; // interface ref โ loosely coupled Checkout(PaymentGateway gateway) { this.gateway = gateway; } void pay(double amount) { double fee = gateway.getTransactionFee(amount); double total = amount + fee; System.out.printf("Gateway: %s | Base: %.2f | Fee: %.2f | Total: %.2f%n", gateway.getGatewayName(), amount, fee, total); gateway.processPayment(total); } } public class PaymentGatewayDemo { public static void main(String[] args) { new Checkout(new RazorpayGateway()).pay(1000); new Checkout(new PayPalGateway()).pay(1000); } }
๐ป Program 3 โ Interface Extending Interface
interface Readable { String read(); } interface Writable { void write(String data); } // Interface can extend multiple interfaces interface ReadWritable extends Readable, Writable { void flush(); // adds its own method } // Implementing class must honour ALL inherited + own methods class FileStream implements ReadWritable { private String buffer = ""; public String read() { return "Reading: " + buffer; } public void write(String d) { buffer = d; System.out.println("Written: " + d); } public void flush() { buffer = ""; System.out.println("Buffer flushed"); } } public class InterfaceInheritance { public static void main(String[] args) { FileStream fs = new FileStream(); fs.write("Hello, Java!"); System.out.println(fs.read()); fs.flush(); System.out.println(fs.read()); // Use as any parent interface reference โ polymorphism Readable r = fs; Writable w = fs; ReadWritable rw = fs; System.out.println("fs instanceof Readable: " + (fs instanceof Readable)); System.out.println("fs instanceof ReadWritable: " + (fs instanceof ReadWritable)); } }
public static final fields) are a legacy pattern. In modern Java, prefer enum for named constant groups, or a dedicated final class with a private constructor as a constants holder. Putting too many constants in an interface leads to the "Constant Interface Antipattern" โ classes implementing the interface just to use constants, polluting their public API.
public abstract (before Java 8); all fields are implicitly public static final.Key differences from abstract class:
โ A class can
implement multiple interfaces but extend only one classโ Interfaces have no constructors and no instance fields
โ Interface represents a CAN-DO capability; abstract class represents an IS-A relationship
โ Interface methods are implicitly public; abstract class methods can have any access modifier
โ Use interfaces when unrelated classes need a common capability; use abstract class when related classes share state or behaviour
new Vehicle().However, you can create an instance in two indirect ways:
1. Implementing class:
Vehicle v = new Car(); โ Car implements Vehicle2. Anonymous class:
Vehicle v = new Vehicle() { public void start(){...} ... }; โ creates a nameless class on the spot that implements the interfaceJava 8 lambdas also provide a concise way to instantiate functional interfaces (interfaces with a single abstract method):
Runnable r = () -> System.out.println("running");
public abstract. When you implement them in a class, you are overriding them. Java's override rule states that you cannot reduce the visibility of an overridden method. Since the interface declares the method as public, any implementation must also be public โ or the access would be narrowed (e.g., to package-private), which is not allowed.Failing to add
public in the implementing class results in a compile error: "cannot reduce the visibility of the inherited method."
extends keyword: interface C extends A, B { }. The implementing class must implement all methods from C, A, and B.Interface extending a class: No โ an interface cannot extend a class. Interfaces exist in a separate type hierarchy. They can only extend other interfaces.
public static final:โ public: accessible to all implementing classes and outside code
โ static: belongs to the interface type itself (no object needed); implementing classes do not each get their own copy
โ final: cannot be reassigned โ ensures constants are truly constant
This means you cannot declare instance variables in an interface, and you cannot declare a field without initialising it (blank final is not allowed in an interface). This is why interfaces cannot hold mutable state โ they are stateless contracts by design.
Java avoids this with interfaces because (before Java 8) interfaces had no method implementations โ only abstract methods. A class implementing two interfaces with the same method signature simply provides one implementation that satisfies both contracts โ no conflict.
With Java 8
default methods, the Diamond Problem can resurface. Java resolves it with a clear rule: if two interfaces provide conflicting default methods, the implementing class must override the method to resolve the ambiguity โ otherwise a compile error occurs.
๐ฑ Interfaces are the backbone of Spring Boot
Every major Spring abstraction is an interface. Your service, repository, and component code is built on top of interface types โ Spring injects the right concrete implementation at runtime.
Repository layer โ
JpaRepository<T, ID> is an interface; Spring Data generates the implementationController accepts interface โ loosely coupled; easy to swap implementations or mock in tests
@Qualifier โ when multiple beans implement the same interface, select which one to inject
Testability โ interfaces make it trivial to replace real beans with Mockito mocks
// Contract โ the interface public interface NotificationService { void send(String to, String message); String getChannel(); } // Implementation 1 @Service public class EmailNotificationService implements NotificationService { public void send(String to, String msg) { System.out.printf("Email โ %s: %s%n", to, msg); } public String getChannel() { return "EMAIL"; } } // Implementation 2 @Service public class SmsNotificationService implements NotificationService { public void send(String to, String msg) { System.out.printf("SMS โ %s: %s%n", to, msg); } public String getChannel() { return "SMS"; } } // Controller โ depends on interface, not concrete class @RestController public class AlertController { private final List<NotificationService> services; // all impls injected public AlertController(List<NotificationService> services) { this.services = services; } @PostMapping("/alert") public void sendAll(@RequestParam String to, @RequestParam String msg) { services.forEach(svc -> svc.send(to, msg)); // DMD per service } }
public interface ProductRepository extends JpaRepository<Product, Long> { }, you are declaring an interface with zero method bodies. Spring Data generates a complete implementation at startup using dynamic proxies โ a runtime implementation of your interface. This is the ultimate example of interface-driven design: the caller (your service) depends on the interface; the implementation is created invisibly at runtime.
- Interface methods โ implicitly
public abstract - Interface fields โ implicitly
public static final(must be initialised) - A class can
implementmultiple interfaces โ multiple inheritance of type - A class can
extendone class ANDimplementmultiple interfaces simultaneously - Interface cannot have constructors or instance fields
- Implementing class must override ALL abstract methods or be declared
abstract - Implementing method must be declared
publicโ cannot reduce visibility - Interface can
extendmultiple other interfaces - Interface cannot
extenda class - You cannot instantiate an interface directly
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Access modifier of interface method by default? | public abstract | Both public AND abstract โ not just one |
| Access modifier of interface field by default? | public static final | All three โ must be initialised at declaration |
| Can a class implement two interfaces with same method signature? | Yes โ provide one implementation | One method body satisfies both contracts; no conflict |
| Implementing class declares method as package-private? | Compile error | Cannot reduce visibility from public (interface) to package-private |
| Can interface extend another interface? | Yes โ even multiple | interface C extends A, B โ valid; uses extends not implements |
| Can interface extend a class? | No โ compile error | Interfaces can only extend other interfaces |
| A class extends one class and implements two interfaces โ valid? | Yes | class D extends B implements X, Y โ fully valid Java syntax |
๐ One-Liner Definitions
- Interface โ A pure contract in Java that defines what a class must do; all methods are implicitly public abstract, all fields are public static final.
- implements โ Keyword used by a class to commit to honouring one or more interface contracts.
- Multiple interface implementation โ A class can implement any number of interfaces, enabling multiple inheritance of type without the Diamond Problem.
- Interface field โ Implicitly public static final; a compile-time constant shared by all implementors; cannot be instance state.
- Interface extending interface โ An interface can extend one or more other interfaces using
extends; the implementing class must honour all inherited contracts. - CAN-DO relationship โ The semantic meaning of implementing an interface: the class gains a capability, distinct from the IS-A relationship of class inheritance.
Interface Modern Features โ JDK 8 & 9
๐ Why Were These Features Added?
Before Java 8, adding a new abstract method to an interface broke every existing implementing class โ a massive backward compatibility problem. Java 8 introduced default methods and static methods inside interfaces to evolve APIs without breaking existing code. Java 9 added private methods to allow code reuse inside interfaces themselves.
1๏ธโฃ default Methods (Java 8)
A default method has a complete body inside the interface. Implementing classes inherit it automatically โ no need to override. They can override it if they need different behaviour. This is how Java evolved the Collections API (e.g., Iterable.forEach(), Collection.stream()) without breaking millions of existing classes.
โ ๏ธ Diamond Problem with default Methods
2๏ธโฃ static Methods (Java 8)
Interface static methods belong to the interface type itself โ they are not inherited by implementing classes or sub-interfaces. Call them with InterfaceName.methodName(). They are typically used as factory methods or utility helpers tightly tied to the interface contract.
3๏ธโฃ private & private static Methods (Java 9)
Private methods in interfaces allow code reuse between default and static methods without exposing internal helpers to implementing classes. Before Java 9, shared logic had to be duplicated across multiple default methods.
๐ Complete Interface Method Summary
| Method Type | Since | Has Body? | Inherited? | Can Override? | Access |
|---|---|---|---|---|---|
| abstract | Always | โ | โ (as contract) | โ (must) | public |
| default | Java 8 | โ | โ (auto) | โ (optional) | public |
| static | Java 8 | โ | โ | โ (hidden) | public |
| private | Java 9 | โ | โ | โ | private |
| private static | Java 9 | โ | โ | โ | private |
๐ป Program 1 โ default Methods: Logger Interface
interface Logger { // Abstract โ must implement void log(String msg); // Default โ inherited for free, can override default void logInfo(String msg) { log("[INFO] " + msg); } default void logWarn(String msg) { log("[WARN] " + msg); } default void logError(String msg) { log("[ERROR] " + msg); } } // Only needs to implement log() โ gets the rest for free class ConsoleLogger implements Logger { public void log(String msg) { System.out.println("Console: " + msg); } } // Overrides one default to add timestamp class FileLogger implements Logger { public void log(String msg) { System.out.println("File: " + msg); } @Override public void logError(String msg) { // custom override log("[ERROR][ts=" + System.currentTimeMillis() % 100000 + "] " + msg); } } public class DefaultMethodDemo { public static void main(String[] args) { Logger console = new ConsoleLogger(); console.logInfo("App started"); // inherited default console.logWarn("Low memory"); // inherited default console.logError("Disk full"); // inherited default System.out.println("---"); Logger file = new FileLogger(); file.logInfo("Writing report"); // inherited default file.logError("Write failed"); // overridden with timestamp } }
๐ป Program 2 โ static Methods + private Methods (Java 8 & 9)
interface MathOps { // Abstract double compute(double a, double b); // static factory methods (Java 8) โ called as MathOps.adder() static MathOps adder() { return (a, b) -> a + b; } static MathOps multiplier() { return (a, b) -> a * b; } static MathOps maxOf() { return (a, b) -> Math.max(a, b); } // default methods calling private helper (Java 9) default double computeAndLog(double a, double b) { double result = compute(a, b); printResult(a, b, result); // calls private method return result; } default double computeDoubled(double a, double b) { double result = compute(a, b) * 2; printResult(a, b, result); // same private helper โ no duplication return result; } // private method โ Java 9, NOT visible to implementing classes private void printResult(double a, double b, double r) { System.out.printf(" compute(%.1f, %.1f) = %.1f%n", a, b, r); } } public class StaticPrivateMethodDemo { public static void main(String[] args) { // static factory โ called on interface name, not object MathOps add = MathOps.adder(); MathOps mul = MathOps.multiplier(); MathOps maxF = MathOps.maxOf(); System.out.println("=== add.computeAndLog ==="); add.computeAndLog(3, 4); System.out.println("=== mul.computeDoubled ==="); mul.computeDoubled(3, 4); System.out.println("=== max of 7, 2 ==="); maxF.computeAndLog(7, 2); } }
๐ป Program 3 โ Diamond Problem Resolution
interface A { default void greet() { System.out.println("Hello from A"); } } interface B extends A { default void greet() { System.out.println("Hello from B"); } } interface C extends A { default void greet() { System.out.println("Hello from C"); } } // Case 1: B is more specific than A โ B wins automatically class D implements A, B { // No override needed โ B.greet() wins (B extends A โ more specific) } // Case 2: B and C both extend A โ true ambiguity โ MUST override class E implements B, C { @Override public void greet() { B.super.greet(); // explicitly delegate to B System.out.println("(resolved in E)"); } } public class DiamondResolution { public static void main(String[] args) { new D().greet(); // B wins (more specific) new E().greet(); // explicitly resolved in E } }
default methods to existing interfaces without breaking any existing code:Iterable.forEach(), Iterable.spliterator()Collection.stream(), Collection.removeIf()Map.getOrDefault(), Map.computeIfAbsent(), Map.merge()Comparator.reversed(), Comparator.thenComparing()Every Java application uses these daily without knowing the API evolution magic behind them.
InterfaceName.super.method() syntax ยท JDK examples of default methods
Java 8 wanted to add powerful new methods to existing interfaces like
Collection, Iterable, and Map for the Streams API. Adding these as abstract methods would have broken every library and application that implemented those interfaces.Default methods let the JDK provide a default implementation. Existing classes inherit it automatically. Classes that want different behaviour can still override it. This is how
Collection.stream(), Iterable.forEach(), and Map.computeIfAbsent() were added without breaking the Java ecosystem.
InterfaceName.staticMethod().This is different from static methods in classes, which are inherited (though not overridden โ hidden instead). The decision for interface static methods was deliberate: they are utility/factory helpers scoped to the interface contract, not part of the implementing class's API.
Example:
Comparator.naturalOrder() must be called as Comparator.naturalOrder(), not as MyComparator.naturalOrder().
1. Duplicate the code in each default method
2. Extract to a public default method โ but this exposes internal logic as part of the public API, cluttering the interface contract
Java 9 private methods solve this cleanly: you write the shared logic once in a
private method, and all default/static methods inside the interface call it. The private method is completely invisible to implementing classes and outside code โ it is a pure internal implementation detail.
Rule 1 โ Class wins: If the class itself (or a superclass) provides a concrete method, it takes priority over any interface default method. A class override always wins.
Rule 2 โ More specific interface wins: If one interface extends the other, the more specific (child) interface's default method wins. Example: if B extends A and both define
show(), and a class implements both A and B, B's version wins automatically.Rule 3 โ Must override explicitly: If neither rule resolves the ambiguity (two unrelated interfaces both provide the same default method), the implementing class gets a compile error and must override the method to pick one:
B.super.greet() or C.super.greet(), or provide its own body.
InterfaceName.super.methodName() syntax, introduced specifically for this purpose.Example:
@Overridepublic void greet() { B.super.greet(); // calls B's default greet() System.out.println("extra behaviour");}This is similar to
super.method() for class inheritance, but uses the interface name as a qualifier to disambiguate between multiple interfaces.
default method cannot be final. That would contradict itself: final means it cannot be overridden, but default is explicitly designed to be overridable. The two modifiers are mutually exclusive.abstract: No โ a
default method cannot be abstract. A default method must have a body; an abstract method cannot have a body. They are mutually exclusive.A default method can however be overridden by an implementing class and then declared
final in that class โ but that is a feature of the class, not the interface.
๐ฑ default & static Interface Methods in Spring Boot
Spring Boot itself leverages interface default methods in several key APIs. Understanding them helps you extend Spring behaviour cleanly.
HandlerInterceptor โ preHandle/postHandle/afterCompletion all have empty default implementations
UserDetailsService โ your implementation provides loadUserByUsername(); other helpers via default methods
HealthIndicator โ default implementation calls doHealthCheck(); you override just the check logic
Comparator (JDK) โ reversed(), thenComparing() are default methods used heavily in Spring batch sorting
// WebMvcConfigurer has ~15 methods โ ALL are default with empty bodies. // You only override the ones relevant to your configuration. @Configuration public class WebConfig implements WebMvcConfigurer { // Override only CORS config โ all other defaults inherited @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("https://myfrontend.com") .allowedMethods("GET", "POST", "PUT", "DELETE"); } // Override only interceptor config @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoggingInterceptor()) .addPathPatterns("/api/**"); } // All other 13 WebMvcConfigurer methods inherited as no-ops // This is the power of default methods โ clean, minimal overrides } // โโ HandlerInterceptor with default no-op implementations โโโโโ @Component public class LoggingInterceptor implements HandlerInterceptor { // Only preHandle needed โ postHandle and afterCompletion are default no-ops @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) { System.out.println("โ " + req.getMethod() + " " + req.getRequestURI()); return true; } }
WebMvcConfigurerAdapter) that implemented all interface methods as empty stubs. You extended the Adapter and overrode only what you needed. Since Java 8 default methods, these Adapters were deprecated โ the interface itself now serves the same role. WebMvcConfigurerAdapter was removed in Spring 5.
defaultmethods โ Java 8 ยท have a body ยท implicitly public ยท inherited ยท can overridestaticmethods โ Java 8 ยท have a body ยท NOT inherited ยท call viaInterfaceName.method()privatemethods โ Java 9 ยท have a body ยท NOT inherited ยท used inside interface onlyprivate staticโ Java 9 ยท shared by static methods inside the interface- Diamond Problem rule: class > specific interface > general interface > must override
B.super.greet()โ syntax to call a specific interface's default methoddefault+finalโ compile error (contradictory)default+abstractโ compile error (contradictory)- Motivation: backward compatibility โ add methods to existing interfaces without breaking code
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Can implementing class inherit a default method? | Yes โ automatically | default methods are inherited without any override needed |
| Can implementing class call interface static method? | No โ must use InterfaceName.method() | Static methods are NOT inherited by implementing classes |
| Can a private interface method be called by implementing class? | No โ invisible outside interface | private = only accessible within the interface body |
| Two interfaces with same default method โ class implements both? | Compile error unless overridden | Ambiguity must be resolved explicitly by the class |
| Interface B extends A; both have default show(); class implements A,B? | B's version wins โ no error | More specific (B) wins over general (A) automatically |
| Can default method be declared abstract? | No โ compile error | default requires a body; abstract forbids a body โ contradictory |
| Which Java version added private interface methods? | Java 9 | default and static were Java 8; private is Java 9 |
๐ One-Liner Definitions
- default method โ An interface method with a body (Java 8); automatically inherited by implementing classes; overridable; solves backward compatibility when evolving APIs.
- static interface method โ A method belonging to the interface type (Java 8); not inherited; called as
InterfaceName.method(); used for factory/utility helpers. - private interface method โ A helper method inside an interface (Java 9); not visible to implementing classes; eliminates code duplication between default methods.
- Diamond Problem (default methods) โ Ambiguity when two interfaces provide same default method; resolved by: class override > specific interface > explicit
X.super.method(). - InterfaceName.super.method() โ Syntax to invoke a specific interface's default method from an overriding class, used to resolve Diamond Problem ambiguity.
Enum
๐ฏ What is an Enum?
An enum (enumeration) is a special class in Java that represents a fixed set of named constants. Every constant in an enum is a public static final instance of that enum type. Enums are type-safe โ the compiler guarantees you can only use values from the defined set, eliminating the classic "magic number" and "magic string" bugs.
๐๏ธ Enum Internals โ What the Compiler Generates
An enum is syntactic sugar. The compiler converts it into a class that extends java.lang.Enum. Each constant becomes a public static final instance of the enum class, created exactly once when the class loads.
๐ง Built-in Enum Methods
โ๏ธ Enum with Fields, Constructor & Methods
Enums can have instance fields, a private constructor, and methods โ making them far more powerful than simple named constants. Each constant passes arguments to the constructor at declaration time.
๐ Enum in switch Statements
๐ Enum Implementing an Interface
Enums can implement interfaces. Each constant can even override the interface method differently using constant-specific bodies.
enum AppConfig { INSTANCE; } beats all other approaches.
๐ป Program 1 โ Enum Basics: Day with switch
enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } public class EnumBasics { public static void main(String[] args) { // Basic usage Day today = Day.WEDNESDAY; System.out.println("Today: " + today); System.out.println("Name: " + today.name()); System.out.println("Ordinal: " + today.ordinal()); System.out.println("valueOf: " + Day.valueOf("FRIDAY")); // Iterate all constants System.out.print("All days: "); for (Day d : Day.values()) System.out.print(d.ordinal() + ":" + d + " "); System.out.println(); // switch with enum for (Day d : new Day[] {Day.MONDAY, Day.SATURDAY}) { String type = switch (d) { case SATURDAY, SUNDAY -> "Weekend ๐"; default -> "Weekday ๐ผ"; }; System.out.println(d + " โ " + type); } // enum comparison โ use == (singletons) System.out.println("today == WEDNESDAY: " + (today == Day.WEDNESDAY)); } }
๐ป Program 2 โ Enum with Fields, Constructor & Methods
enum HttpStatus { OK (200, "OK"), CREATED (201, "Created"), BAD_REQUEST (400, "Bad Request"), UNAUTHORIZED (401, "Unauthorized"), NOT_FOUND (404, "Not Found"), INTERNAL_ERROR (500, "Internal Server Error"); private final int code; private final String message; HttpStatus(int code, String message) { // implicitly private this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; } public boolean isSuccess() { return code >= 200 && code < 300; } public boolean isError() { return code >= 400; } // Reverse lookup โ find enum constant by code public static HttpStatus fromCode(int code) { for (HttpStatus s : values()) if (s.code == code) return s; throw new IllegalArgumentException("Unknown code: " + code); } @Override public String toString() { return code + " " + message; } } public class EnumWithFields { static void handleResponse(HttpStatus status) { System.out.printf("%-30s success=%-5b error=%b%n", status, status.isSuccess(), status.isError()); } public static void main(String[] args) { for (HttpStatus s : HttpStatus.values()) handleResponse(s); System.out.println("Lookup 404: " + HttpStatus.fromCode(404)); } }
๐ป Program 3 โ EnumMap & EnumSet
import java.util.EnumMap; import java.util.EnumSet; enum Priority { LOW, MEDIUM, HIGH, CRITICAL } public class EnumCollections { public static void main(String[] args) { // EnumMap โ faster than HashMap when keys are enum constants EnumMap<Priority, String> actions = new EnumMap<>(Priority.class); actions.put(Priority.LOW, "Log and ignore"); actions.put(Priority.MEDIUM, "Email team"); actions.put(Priority.HIGH, "Page on-call"); actions.put(Priority.CRITICAL, "Wake CEO + rollback"); System.out.println("=== Incident Actions ==="); actions.forEach((p, a) -> System.out.printf("%-10s โ %s%n", p, a)); // EnumSet โ faster than HashSet for enum values EnumSet<Priority> urgent = EnumSet.of(Priority.HIGH, Priority.CRITICAL); EnumSet<Priority> all = EnumSet.allOf(Priority.class); EnumSet<Priority> nonUrgent = EnumSet.complementOf(urgent); // inverse of urgent System.out.println(" === Priority Sets ==="); System.out.println("All: " + all); System.out.println("Urgent: " + urgent); System.out.println("Non-urgent:" + nonUrgent); System.out.println("HIGH urgent? " + urgent.contains(Priority.HIGH)); } }
EnumMap uses a simple array indexed by ordinal internally โ no hashing, no collision, O(1) guaranteed. It is significantly faster than HashMap<MyEnum, V>.EnumSet uses a single long bitmask (for enums with โค64 constants) โ all operations are single bitwise instructions. Dramatically faster than HashSet<MyEnum>.
final class that implicitly extends java.lang.Enum<E>. Each named constant becomes a public static final field of that class, initialised exactly once when the class loads.Because it is a class, an enum can have fields, a constructor (always private โ cannot be called externally), methods, and can implement interfaces. Because it is
final, it cannot be subclassed by user code. Because it extends java.lang.Enum, it cannot extend any other class (Java single inheritance).
new Day() or otherwise construct additional instances.This is what makes each enum constant a singleton โ there is exactly one instance of
Day.MONDAY, one of Day.TUESDAY, etc., in the entire JVM. Because they are singletons, you compare enum values with == rather than equals() โ though equals() also works (it is final in java.lang.Enum and uses == internally).
final in java.lang.Enum โ you cannot override it. Always returns the declaration name.toString(): by default returns the same as
name(), but it is overridable. Override it when you want a different String representation (e.g., a display label) without changing the declaration name.ordinal(): returns the 0-based position of the constant in its declaration order. Avoid using ordinal() for business logic โ it is fragile and breaks if constants are reordered. Use custom fields instead.
java.lang.Enum, and Java allows only single class inheritance. There is no room for a second parent class.Implement an interface: Yes. An enum can implement one or more interfaces. Each constant can even have its own body that overrides the interface method differently (constant-specific class bodies). This is a powerful pattern for providing type-safe command objects or strategy objects.
== is therefore always correct, safe, and slightly faster (no method call).equals() also works (the Enum.equals() implementation uses == internally, and it is final so cannot be overridden). However, == gives you an extra safety benefit: it is a compile-time type check. If you write day == "MONDAY" it is a compile error (comparing Day to String). With equals() it compiles but always returns false โ a silent bug. So == is both safer and clearer for enums.
HashMap when keys are enum constants. Iteration is always in declaration order.EnumSet<E extends Enum>: a Set implementation backed by a single
long bitmask (for enums with โค 64 constants). Every operation โ add, remove, contains, complement โ is a single bitwise instruction. Dramatically faster than HashSet for enum values. Also provides convenience factory methods: allOf(), noneOf(), range(), complementOf().Rule: whenever you use an enum as a Map key or Set element, always use EnumMap / EnumSet instead of HashMap / HashSet.
๐ฑ Enums in Spring Boot
Enums are used throughout Spring Boot for configuration, JPA column values, REST request/response mapping, and state machines.
@RequestParam / @PathVariable โ Spring auto-converts String โ enum in REST endpoints
@JsonProperty / @JsonValue โ Jackson serialises/deserialises enums to/from JSON
Order/Payment status state machine โ enum represents all possible states with transition methods
Spring Security roles โ
enum Role { USER, ADMIN, MODERATOR } stored in UserDetails
// Enum with @JsonValue for clean JSON output public enum OrderStatus { PENDING ("pending"), CONFIRMED ("confirmed"), SHIPPED ("shipped"), DELIVERED ("delivered"), CANCELLED ("cancelled"); private final String value; OrderStatus(String v) { this.value = v; } @JsonValue // Jackson uses this for serialisation public String getValue() { return value; } } // JPA Entity โ @Enumerated(STRING) recommended over ORDINAL @Entity public class Order { @Id @GeneratedValue private Long id; @Enumerated(EnumType.STRING) // stores "PENDING" not 0 in DB private OrderStatus status; } // REST endpoint โ Spring auto-converts "PENDING" String โ OrderStatus enum @RestController public class OrderController { @GetMapping("/orders") public List<Order> getByStatus( @RequestParam OrderStatus status) { // "PENDING" auto-converted return orderRepo.findByStatus(status); } }
EnumType.ORDINAL stores the 0-based position (0, 1, 2โฆ) in the database. If you ever reorder, insert, or remove an enum constant, all existing database rows silently map to the wrong value โ catastrophic data corruption with no error. Always use EnumType.STRING โ it stores the name as a VARCHAR, which is robust to reordering and self-documenting in the DB.
- Enum is a final class that implicitly
extends java.lang.Enum - Each constant is
public static finalโ a singleton instance - Constructor is always private โ cannot call
new MyEnum() - Cannot extend another class (already extends java.lang.Enum)
- Can implement one or more interfaces
name()โ declaration name, final ยทordinal()โ 0-based position ยทtoString()โ overridablevalues()โ all constants as array ยทvalueOf(String)โ parse string to constant- Use
==for comparison (singletons);equals()also works but==is safer - In switch, write
case MONDAYnotcase Day.MONDAY - Enum Singleton is thread-safe, serialisation-safe, reflection-safe
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Can enum extend a class? | No โ compile error | Already implicitly extends java.lang.Enum; Java single inheritance |
| Can enum implement an interface? | Yes | Enum is a class; classes can implement interfaces freely |
| Can you call new on an enum? | No โ constructor is private | Only JVM creates instances at class load time |
| First constant ordinal value? | 0 (zero-based) | ordinal() is 0-based like arrays, not 1-based |
| Can enum constructor be public? | No โ must be private or package-private (effectively private) | Compiler forces private; public constructor is a compile error |
| Difference: name() vs toString()? | name() is final; toString() is overridable | Both return declaration name by default, but only toString() can be overridden |
| Enum in switch โ correct case syntax? | case MONDAY (no enum type prefix) | The type is known from switch expression; prefixing causes compile error |
๐ One-Liner Definitions
- enum โ A special final class in Java representing a fixed set of named constants; each constant is a public static final singleton instance of the enum type.
- ordinal() โ Returns the 0-based position of an enum constant in its declaration order; avoid using for business logic as it breaks on reordering.
- name() โ Returns the exact declaration name of the enum constant as a String; final and cannot be overridden.
- values() โ Static method returning an array of all enum constants in declaration order; creates a new array each call.
- valueOf(String) โ Parses a String and returns the matching enum constant; throws IllegalArgumentException if no match.
- EnumMap / EnumSet โ High-performance collections backed by array/bitmask respectively; always prefer over HashMap/HashSet when keys or elements are enum constants.
Annotations
๐ What is an Annotation?
An annotation is a metadata tag attached to code elements (classes, methods, fields, parameters, packages). It begins with @. Annotations carry information about code without changing what the code does. They can be read by the compiler, build tools, or at runtime via reflection โ enabling powerful frameworks like Spring Boot, JPA, and JUnit to work their magic with minimal boilerplate.
๐ง Built-in Java Annotations
๐๏ธ Meta-Annotations โ Annotations on Annotations
Meta-annotations configure the behaviour of custom annotations. The four main ones from java.lang.annotation:
โ๏ธ Creating a Custom Annotation
๐ Annotation Processing Lifecycle
@interface MyAnnotation, the compiler generates an interface that extends java.lang.annotation.Annotation. Each element (like String value()) becomes an abstract method in that interface. This is why annotation elements look like methods and why you read them via method calls: ann.value().
๐ป Program 1 โ Built-in Annotations in Action
import java.util.ArrayList; import java.util.List; class Animal { void speak() { System.out.println("..."); } @Deprecated void oldSound() { System.out.println("generic sound"); } } class Dog extends Animal { @Override // compile error if speak() not in Animal void speak() { System.out.println("Woof!"); } @SuppressWarnings("deprecation") // silence the warning for this call void callOldMethod() { oldSound(); } } @FunctionalInterface interface Transformer<T, R> { R transform(T input); // exactly 1 abstract method โ SAM // void extra(); // โ would cause compile error! } public class BuiltInAnnotations { @SuppressWarnings("unchecked") // suppress raw type warning static List rawList() { return new ArrayList(); // raw type โ would warn without annotation } public static void main(String[] args) { new Dog().speak(); // Transformer as lambda (works because @FunctionalInterface) Transformer<String, Integer> len = s -> s.length(); System.out.println("Length of "hello": " + len.transform("hello")); new Dog().callOldMethod(); // no compiler warning } }
๐ป Program 2 โ Custom Annotation + Runtime Reflection
import java.lang.annotation.*; import java.lang.reflect.Method; // โโ Define custom annotation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ @Retention(RetentionPolicy.RUNTIME) // survives to runtime @Target(ElementType.METHOD) // only on methods @Documented @interface Audit { String action() default "CALL"; boolean logArgs() default false; } // โโ Apply annotation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ class OrderService { @Audit(action = "CREATE_ORDER", logArgs = true) public void createOrder(String item, int qty) { System.out.println("Creating order: " + item + " x" + qty); } @Audit // uses defaults: action="CALL", logArgs=false public void listOrders() { System.out.println("Listing all orders"); } public void helperMethod() { // no annotation System.out.println("internal helper"); } } // โโ Read annotation at runtime via reflection โโโโโโโโโโโโโโโโโ public class CustomAnnotation { public static void main(String[] args) { System.out.println("=== Scanning @Audit annotations ==="); for (Method m : OrderService.class.getDeclaredMethods()) { if (m.isAnnotationPresent(Audit.class)) { Audit a = m.getAnnotation(Audit.class); System.out.printf("Method: %-14s | action=%-14s | logArgs=%b%n", m.getName(), a.action(), a.logArgs()); } } System.out.println("=== Invoking service ==="); OrderService svc = new OrderService(); svc.createOrder("Book", 3); svc.listOrders(); } }
๐ป Program 3 โ Repeatable Annotation (Java 8)
import java.lang.annotation.*; import java.lang.reflect.Method; // Container annotation (holds array of @Role) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Roles { Role[] value(); } // Repeatable annotation @Repeatable(Roles.class) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Role { String value(); } class AdminController { @Role("ADMIN") @Role("SUPERUSER") // multiple @Role on same method public void deleteAll() { System.out.println("Deleting all records"); } @Role("USER") public void viewDashboard() { System.out.println("Viewing dashboard"); } } public class RepeatableAnnotation { public static void main(String[] args) { for (Method m : AdminController.class.getDeclaredMethods()) { Role[] roles = m.getAnnotationsByType(Role.class); if (roles.length > 0) { System.out.print(m.getName() + " allowed for: "); for (Role r : roles) System.out.print(r.value() + " "); System.out.println(); } } } }
boolean, byte, short, int, long, float, double, char, String, Class, any enum, another annotation type, or a 1D array of any of the above. You cannot use Object, List, Map, or other reference types โ this is enforced at compile time.
@ prefix. It provides information about the code to the compiler, build tools, or runtime framework โ without directly altering what the code does.Annotations themselves do not change program logic. What changes behaviour is the processor that reads the annotation and acts on it: the compiler (for
@Override), a build-time processor like Lombok (for @Getter), or a runtime framework like Spring (for @Autowired, @Transactional). The annotation is the instruction; the framework is the executor.
.class file. Used for annotations that are purely for developer tools or IDEs: @Override, @SuppressWarnings.CLASS: The annotation is stored in the
.class file but is not loaded into the JVM at runtime. Not accessible via reflection. This is the default if @Retention is not specified. Used by bytecode analysis tools.RUNTIME: The annotation survives compilation, is stored in the
.class file, and is loaded into the JVM. Accessible at runtime via reflection. Required for all framework annotations: Spring, JPA, JUnit, Jackson, etc.
@Override tells the compiler that the annotated method is intended to override a method in a supertype (class or interface). The compiler verifies the claim: if no matching method exists in the supertype, it produces a compile error.Why always use it:
1. Catches typos: if you write
tostring() instead of toString() and add @Override, you get a compile error instead of a silent bug where your method is never called.2. Documents intent: clearly signals this method participates in polymorphism.
3. Refactoring safety: if the parent method is renamed or removed, all
@Override sites immediately fail to compile โ no silent orphaned methods.
@FunctionalInterface marks an interface as a Functional Interface โ one that has exactly one abstract method (SAM: Single Abstract Method). This is required for the interface to be used as a lambda expression target.The annotation does two things:
1. Documents intent: signals to readers that this interface is designed for lambdas.
2. Enforces the contract: if a second abstract method is added to the interface, the compiler immediately reports an error โ preventing accidental breakage of all lambda usage sites.
Default and static methods do not count toward the SAM limit. Examples:
Runnable, Comparator, Predicate, Function, Consumer, Supplier are all functional interfaces.
Adding the
@Inherited meta-annotation to a custom annotation changes this: if the annotation is placed on a class, any subclass of that class is treated as if it also has the annotation (when queried via getAnnotation()).Important limitations of
@Inherited:โ Works only for class-level annotations (
@Target(ElementType.TYPE))โ Does NOT work for interfaces (annotating an interface does not make implementing classes inherit it)
โ Does NOT work for method-level or field-level annotations
@Retention(RetentionPolicy.RUNTIME):1. Get the reflective element:
Method m = MyClass.class.getMethod("methodName");2. Check presence:
m.isAnnotationPresent(MyAnnotation.class)3. Retrieve the instance:
MyAnnotation ann = m.getAnnotation(MyAnnotation.class);4. Read element values:
ann.value(), ann.enabled(), etc.The same API works for classes (
MyClass.class.getAnnotation(...)), fields (Field.getAnnotation(...)), and parameters (Parameter.getAnnotation(...)). This is precisely what Spring does at startup when scanning your beans for @Autowired, @Transactional, etc.
๐ฑ Annotations Power All of Spring Boot
Spring Boot is almost entirely annotation-driven. Understanding which annotations belong to which layer โ and what each one tells the framework โ is essential for every Spring developer.
@Component, @Service, @Repository, @Controller, @RestController โ register beansDependency injection:
@Autowired, @Qualifier, @Primary, @ValueWeb layer:
@RequestMapping, @GetMapping, @PostMapping, @PathVariable, @RequestBody, @ResponseBodyData layer:
@Entity, @Table, @Id, @Column, @OneToMany, @TransactionalConfiguration:
@Configuration, @Bean, @Profile, @ConditionalOnPropertyAOP / cross-cutting:
@Aspect, @Before, @Around, @AfterReturning
// โโ Custom annotation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface LogExecutionTime {} // โโ Service using the annotation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ @Service public class ReportService { @LogExecutionTime // mark this method for timing public void generateReport() { // simulate work try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } // โโ AOP aspect reads the annotation at runtime โโโโโโโโโโโโโโโโ @Aspect @Component public class TimingAspect { @Around("@annotation(LogExecutionTime)") // pointcut = our annotation public Object timeMethod(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); // run the actual method long elapsed = System.currentTimeMillis() - start; System.out.printf("%s took %d ms%n", pjp.getSignature().getName(), elapsed); return result; } }
@Transactional is called, the proxy intercepts it, starts a transaction, runs your method, then commits or rolls back. @Cacheable works the same way โ the proxy checks the cache before calling your method. All of this is powered by reflection reading annotations at runtime โ exactly the same mechanism as the custom @LogExecutionTime example above.
- Annotation = metadata; defined with
@interface - Does NOT change logic โ processor reads & acts on it
@Overrideโ SOURCE retention; compile-time check only@Deprecatedโ compiler warning when used@SuppressWarningsโ silences specific compiler warnings@FunctionalInterfaceโ enforces exactly 1 abstract method (SAM)@Retention: SOURCE โ class file only โ RUNTIME (needed for reflection)@Target: restricts where annotation can be placed@Inherited: subclass inherits class-level annotation (not interface)- Custom annotation elements: only primitives, String, Class, enum, annotation, 1D arrays
- Read at runtime:
isAnnotationPresent(),getAnnotation()
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Default RetentionPolicy if @Retention not specified? | CLASS | Not SOURCE, not RUNTIME โ CLASS is the default |
| Can annotation element return List<String>? | No โ compile error | Only primitives, String, Class, enum, annotation, 1D arrays allowed |
| @Override retention policy? | SOURCE โ discarded after compilation | Compiler uses it; JVM never sees it |
| Can @FunctionalInterface have default methods? | Yes โ unlimited | Only abstract methods count toward the SAM limit; default/static are fine |
| Does @Inherited work on interfaces? | No โ only class inheritance | Implementing a @Inherited-annotated interface does NOT give the class the annotation |
| Can you place two @SuppressWarnings on one element? | Yes โ use array: @SuppressWarnings({"unchecked","deprecation"}) | Not @Repeatable; use array syntax for multiple values |
| What keyword defines a custom annotation? | @interface | Not interface, not annotation โ specifically @interface |
๐ One-Liner Definitions
- Annotation โ Metadata attached to code elements using
@; provides instructions to compiler, build tools, or runtime frameworks without altering logic. - @interface โ Keyword used to declare a custom annotation type; the compiler generates an interface extending
java.lang.annotation.Annotation. - @Retention โ Meta-annotation specifying how long an annotation survives: SOURCE (compile only), CLASS (in .class file), or RUNTIME (available via reflection).
- @Target โ Meta-annotation restricting which code elements an annotation can be applied to (TYPE, METHOD, FIELD, PARAMETER, etc.).
- @FunctionalInterface โ Marks an interface as having exactly one abstract method; enables lambda expression usage and enforces SAM at compile time.
- RetentionPolicy.RUNTIME โ Required for any annotation that needs to be read at runtime by frameworks like Spring, JPA, or JUnit via reflection.
Var-Arg Methods
๐ฆ What are Var-Args?
Variable-arity arguments (var-args) allow a method to accept zero or more arguments of a specified type without the caller needing to create an array manually. The syntax is Type... paramName. Inside the method, the parameter is treated as an array. Var-args were introduced in Java 5 to eliminate the need for overloaded versions of methods like printf().
๐ Var-Arg Rules
๐ How the Compiler Handles Var-Args
โ ๏ธ Var-Arg Overloading โ Ambiguity Traps
๐ Var-Arg with Generics โ Heap Pollution Warning
System.out.printf(String format, Object... args)String.format(String format, Object... args)Arrays.asList(T... a)List.of(E... elements)Collections.addAll(Collection c, T... elements)Objects.hash(Object... values)All of these are var-arg methods that make the caller's code clean without requiring explicit array construction.
๐ป Program 1 โ Var-Arg Basics: sum, print, log
public class VarArgBasics { // Plain vararg โ zero or more ints static int sum(int... nums) { int total = 0; for (int n : nums) total += n; return total; } // Regular param + vararg โ level prefix before variable messages static void log(String level, String... messages) { for (String msg : messages) System.out.println("[" + level + "] " + msg); } // Vararg with array access and length static double average(double... vals) { if (vals.length == 0) return 0; double sum = 0; for (double v : vals) sum += v; return sum / vals.length; } public static void main(String[] args) { // sum โ zero to many args System.out.println("sum() = " + sum()); System.out.println("sum(5) = " + sum(5)); System.out.println("sum(1,2,3) = " + sum(1, 2, 3)); System.out.println("sum(array) = " + sum(new int[]{10, 20, 30})); // log โ fixed param + vararg log("INFO", "App started"); log("WARN", "Low disk", "Low memory"); log("DEBUG"); // zero messages โ OK // average System.out.printf("avg(1,2,3,4,5) = %.1f%n", average(1,2,3,4,5)); System.out.printf("avg() = %.1f%n", average()); } }
๐ป Program 2 โ Overloading with Var-Args
public class VarArgOverloading { static void display(int x) { System.out.println("fixed int: " + x); } static void display(int... nums) { System.out.println("vararg int[]: len=" + nums.length); } static void display(String s, Object... extras) { System.out.print("String+"+extras.length+" extras: " + s); for (Object o : extras) System.out.print(" | " + o); System.out.println(); } public static void main(String[] args) { display(42); // fixed int wins (exact match preferred) display(1, 2, 3); // vararg (only option for 3 ints) display(); // vararg (only option for 0 args) display("Hello"); // String + 0 extras display("Hello", 1, true, "extra"); // String + 3 extras // Passing explicit array as vararg int[] data = {10, 20, 30}; display(data); // vararg accepts int[] directly } }
๐ป Program 3 โ Practical Var-Arg: Builder-style tag formatter
import java.util.Arrays; import java.util.stream.Collectors; public class VarArgPractical { // Build SQL IN clause from variable number of IDs static String buildInClause(String column, Object... values) { if (values.length == 0) return column + " IN ()"; String placeholders = Arrays.stream(values) .map(v -> "'" + v + "'") .collect(Collectors.joining(", ")); return column + " IN (" + placeholders + ")"; } // Validate multiple non-null fields at once static void requireNonNull(String context, Object... fields) { for (int i = 0; i < fields.length; i++) if (fields[i] == null) throw new IllegalArgumentException( context + ": field[" + i + "] is null"); System.out.println(context + ": all " + fields.length + " fields valid"); } // Concatenate strings with separator static String joinWith(String sep, String... parts) { return String.join(sep, parts); } public static void main(String[] args) { // SQL IN clause System.out.println(buildInClause("status", "ACTIVE", "PENDING")); System.out.println(buildInClause("id", 1, 2, 3, 4)); System.out.println(buildInClause("tag")); // Null validation requireNonNull("createUser", "Alice", "alice@ex.com", "password"); try { requireNonNull("createUser", "Bob", null, "password"); } catch (IllegalArgumentException e) { System.out.println("Caught: " + e.getMessage()); } // joinWith System.out.println(joinWith(", ", "Java", "Spring", "Hibernate")); System.out.println(joinWith(" / ", "home", "user", "docs")); } }
method((String) null) โ passes a single null element; args.length == 1, args[0] == nullmethod((String[]) null) โ passes null as the entire array; calling args.length throws NullPointerExceptionAlways null-check
args itself if callers might pass an explicit null array, or use method((Type) null) intentionally to pass a null element.
Type... syntax. The compiler translates multiple arguments at the call site into an array automatically โ the caller doesn't need to construct one.The difference is purely at the call site:
โ With an array parameter, the caller must create an array:
method(new int[]{1,2,3})โ With var-arg, the caller simply passes values:
method(1, 2, 3)โ Both look identical inside the method body (it's an array either way)
You can also pass an explicit array to a var-arg method โ both forms compile. However, you cannot have both a var-arg and an array overload for the same method signature โ they produce duplicate bytecode.
void m(int a, String... s) โ
โ void m(String... s, int a) โ2. Only one var-arg per method:
void m(int... a, String... b) โ3. Can have zero regular parameters before it:
void m(String... s) โ
4. Inside the method, it is an array โ use
.length, indexing, for-each5. Passing an explicit array is valid:
m(new String[]{"a","b"}) โ
6. Var-arg and array of same type cannot both be overloads โ same bytecode signature
Java's method overload resolution has three phases:
1. First, try to find a match without boxing/unboxing or var-arg expansion
2. Then, try with boxing/unboxing but no var-arg
3. Finally, try with var-arg expansion
Var-arg is only considered in phase 3 โ the last resort. So
display(5) calls display(int) over display(int...) because the fixed version is found in phase 1, before var-arg even enters consideration.
The compiler converts
Type... param into Type[] param in the bytecode. So void show(String... s) and void show(String[] s) produce exactly the same method descriptor in the .class file. The compiler sees this as a duplicate and rejects it.This also means a var-arg method can always be called with an explicit array โ there is no ambiguity because they are, under the hood, the same thing.
Case 1:
method((String) null) โ cast to the element type. The compiler creates a single-element array containing null. Inside the method, args.length == 1 and args[0] == null. Safe โ no NPE unless you dereference args[0].Case 2:
method((String[]) null) โ cast to the array type. The compiler passes null as the entire vararg array. Inside the method, args itself is null. Calling args.length immediately throws NullPointerException.Without an explicit cast, just passing
null is ambiguous and causes a compiler warning. Always cast null explicitly when calling var-arg methods.
T... items), the compiler warns about potential heap pollution โ because T... is internally stored as Object[], and mixing generic types in that array can lead to ClassCastException at unexpected places.@SafeVarargs suppresses this warning. It is the developer's declaration that the method does not perform any unsafe operations on the vararg array (e.g., it only reads from it, not writes a value of a potentially wrong type).Restrictions: only applicable on
final, static, or private methods (Java 7+) and on constructors. Not applicable on overridable methods (they could be overridden unsafely).
๐ฑ Var-Args in Spring Boot Context
Var-args appear in Spring Boot in logging, validation utilities, assertion helpers, and bean definition convenience methods.
log.info("{} ordered {} items", username, count) uses Object... argsMockMvc perform โ
mockMvc.perform(get("/api").param("ids", "1","2","3"))Spring Assert โ
Assert.notNull(obj, "msg") and custom multi-field variantsBDDMockito / Mockito โ
given(svc.find(anyString())).willReturn(...) uses var-arg matchers@ConditionalOnProperty โ
havingValue patterns with multiple property names
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; @Service public class ProductService { private static final Logger log = LoggerFactory.getLogger(ProductService.class); // SLF4J uses vararg: Object... args โ {} placeholders public void createProduct(String name, double price, int stock) { validateFields("createProduct", name, price, stock); log.info("Creating product: {} price={} stock={}", name, price, stock); // ... persist } // Custom var-arg validation โ validate any number of required string fields private void validateFields(String method, Object... fields) { for (int i = 0; i < fields.length; i++) { if (fields[i] instanceof String s && !StringUtils.hasText(s)) { throw new IllegalArgumentException( method + ": field["+i+"] must not be blank"); } if (fields[i] instanceof Number n && n.doubleValue() < 0) { throw new IllegalArgumentException( method + ": field["+i+"] must be non-negative"); } } } }
log.info("{} placed order {}", user, orderId) โ the Object... args var-arg means the String is only formatted if the INFO level is actually enabled. This avoids expensive toString() calls when logging is disabled โ a performance optimisation you get for free because the logger is var-arg-based.
- Syntax:
Type... paramNameโ the...comes after the type - Internally treated as
Type[]โ all array operations available inside - Must be the last parameter โ only one var-arg per method
- Can be called with zero to many args, or with an explicit array
- Cannot coexist with
Type[]overload โ same bytecode signature - Fixed-arity method always wins over var-arg in overload resolution
nullas(Type) nullโ array with one null elementnullas(Type[]) nullโ null array itself โ NPE on.length@SafeVarargsโ suppresses heap pollution warning on generic var-args- JDK examples:
printf(),String.format(),Arrays.asList(),List.of()
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| void m(int... a, String b) โ valid? | No โ compile error | Var-arg must be LAST parameter; String b cannot come after |
| void m(int... a, double... b) โ valid? | No โ compile error | Only ONE var-arg allowed per method |
| void m(int... a) and void m(int[] a) โ can both exist? | No โ duplicate method | Same bytecode signature; compiler rejects as duplicate |
| m(5) when both m(int) and m(int...) exist โ which called? | m(int) โ fixed wins | Fixed-arity wins over var-arg in overload resolution |
| Can var-arg accept zero arguments? | Yes โ array length 0 | m() with var-arg โ valid; array is empty but not null |
| Inside var-arg method, what type is the parameter? | An array: Type[] | Can use .length, indexing, for-each โ it IS an array |
| m((String[]) null) โ what happens inside method? | NullPointerException on .length | The entire array reference is null; accessing length throws NPE |
๐ One-Liner Definitions
- Var-arg โ A method parameter declared as
Type...that accepts zero or more arguments of that type; internally treated as an array. - Last-parameter rule โ A var-arg must always be the last parameter in a method signature; only one var-arg is allowed per method.
- Fixed vs var-arg overload resolution โ When both a fixed-arity and a var-arg overload match a call, the fixed-arity method always takes priority (var-arg is last resort).
- Var-arg and array overload conflict โ
Type... pandType[] pproduce identical bytecode signatures; they cannot coexist as overloads. - @SafeVarargs โ Annotation suppressing heap pollution warnings on generic var-arg parameters; applicable only on final, static, or private methods.
Functional Interface & Lambda Expressions
๐ฏ What is a Functional Interface?
A functional interface is any interface with exactly one abstract method (SAM โ Single Abstract Method). It may have any number of default and static methods. The @FunctionalInterface annotation is optional but recommended โ it lets the compiler enforce the SAM rule. Functional interfaces are the target type for lambda expressions and method references.
โก Lambda Expressions โ Syntax Evolution
๐ฆ Built-in Functional Interfaces (java.util.function)
๐ Method References โ Four Forms
๐ Lambda vs Anonymous Class โ Key Differences
final keyword, but the variable must behave as if it were. This is because lambdas may be executed on a different thread โ mutating a shared local would cause a race condition. Instance fields and static fields have no such restriction.
๐ป Program 1 โ Custom Functional Interface + Lambda Evolution
@FunctionalInterface interface MathOperation { int operate(int a, int b); default int operateAndDouble(int a, int b) { return operate(a, b) * 2; } } public class LambdaEvolution { static int apply(int a, int b, MathOperation op) { return op.operate(a, b); } public static void main(String[] args) { // Step 1 โ anonymous class (old style) MathOperation addAnon = new MathOperation() { public int operate(int a, int b) { return a + b; } }; // Step 2 โ lambda full form MathOperation addFull = (int a, int b) -> { return a + b; }; // Step 3 โ infer types MathOperation addInfer = (a, b) -> { return a + b; }; // Step 4 โ drop braces + return MathOperation add = (a, b) -> a + b; MathOperation multiply = (a, b) -> a * b; MathOperation power = (a, b) -> (int) Math.pow(a, b); System.out.println("add(3,4) = " + apply(3, 4, add)); System.out.println("multiply(3,4) = " + apply(3, 4, multiply)); System.out.println("power(2,8) = " + apply(2, 8, power)); System.out.println("add.operateAndDouble(3,4) = " + add.operateAndDouble(3, 4)); // Lambda as inline argument (no variable needed) System.out.println("inline sub(10,3) = " + apply(10, 3, (a,b) -> a-b)); } }
๐ป Program 2 โ Built-in Functional Interfaces + Method References
import java.util.*; import java.util.function.*; import java.util.stream.Collectors; public class BuiltInFunctional { public static void main(String[] args) { // Predicate<T> โ T โ boolean Predicate<String> isLong = s -> s.length() > 5; Predicate<String> startA = s -> s.startsWith("A"); Predicate<String> both = isLong.and(startA); // Predicate composition System.out.println("Predicate ---"); System.out.println(" isLong("Hello"): " + isLong.test("Hello")); System.out.println(" both("Alexander"):" + both.test("Alexander")); // Function<T,R> โ T โ R Function<String, Integer> len = String::length; // method ref Function<Integer, String> stars = n -> "*".repeat(n); Function<String, String> bar = len.andThen(stars); // compose System.out.println("Function ---"); System.out.println(" len("Java"): " + len.apply("Java")); System.out.println(" bar("Hello"): " + bar.apply("Hello")); // Consumer<T> โ T โ void Consumer<String> upper = s -> System.out.print(s.toUpperCase() + " "); Consumer<String> excl = s -> System.out.print(s + "! "); System.out.println(" Consumer ---"); List.of("java","lambda","stream") .forEach(upper.andThen(excl)); // chain consumers // Supplier<T> โ () โ T Supplier<String> ts = () -> "generated@" + System.currentTimeMillis() % 10000; System.out.println(" Supplier ---"); System.out.println(" " + ts.get()); // Method references โ all 4 forms Function<String,Integer> parseInt = Integer::parseInt; // static ref Consumer<String> printer = System.out::println; // instance ref Function<String,String> toUpper = String::toUpperCase; // arbitrary ref Supplier<ArrayList> newList = ArrayList::new; // constructor ref System.out.println(" Method refs ---"); System.out.println(" parseInt("42"): " + parseInt.apply("42")); printer.accept(" printer: hello"); System.out.println(" toUpper("lambda"): " + toUpper.apply("lambda")); System.out.println(" newList: " + newList.get().getClass().getSimpleName()); } }
๐ป Program 3 โ Lambda Closure + Predicate/Function Chaining
import java.util.function.*; import java.util.*; public class LambdaChaining { public static void main(String[] args) { // Closure โ lambda captures effectively final local variable int threshold = 100; // effectively final Predicate<Integer> isAbove = n -> n > threshold; // captures threshold System.out.println("150 above 100? " + isAbove.test(150)); System.out.println("50 above 100? " + isAbove.test(50)); // Predicate composition: and, or, negate Predicate<Integer> isEven = n -> n % 2 == 0; Predicate<Integer> isPos = n -> n > 0; Predicate<Integer> posEven = isEven.and(isPos); Predicate<Integer> isOdd = isEven.negate(); List<Integer> nums = List.of(-4, -3, 0, 2, 5, 6, 9); System.out.print("positive evens: "); nums.stream().filter(posEven).forEach(n -> System.out.print(n + " ")); System.out.print(" odds: "); nums.stream().filter(isOdd).forEach(n -> System.out.print(n + " ")); // Function compose vs andThen Function<Integer,Integer> times2 = x -> x * 2; Function<Integer,Integer> plus10 = x -> x + 10; // andThen: times2 first, then plus10 โ (5*2)+10 = 20 // compose: plus10 first, then times2 โ (5+10)*2 = 30 System.out.println(" times2.andThen(plus10).apply(5) = " + times2.andThen(plus10).apply(5)); System.out.println("times2.compose(plus10).apply(5) = " + times2.compose(plus10).apply(5)); } }
Outer$1.class), lambdas use the JVM invokedynamic instruction introduced in Java 7. The lambda body is compiled as a synthetic private static method in the enclosing class โ no separate class file is created. This makes lambdas lighter on class loading and faster to instantiate than anonymous classes.
โ Any number of
default methodsโ Any number of
static methodsโ Re-declarations of
java.lang.Object methods (equals, hashCode, toString) โ these do NOT count as additional abstract methods@FunctionalInterface is optional but strongly recommended โ it instructs the compiler to enforce the SAM rule and makes intent clear. Examples from JDK: Runnable (run), Callable (call), Comparator (compare), Predicate (test), Function (apply).
final keyword โ the compiler infers it.A lambda can only capture local variables that are effectively final because lambdas may be passed to other threads and executed later. If the lambda could read a mutable local variable that's changing on the original thread, you'd have a race condition with no synchronisation.
Instance fields and static fields are not subject to this rule โ they are accessed through the heap (not the stack frame) and their thread-safety is the programmer's responsibility via normal synchronisation.
Attempting to capture a non-final local variable gives the compile error: "Variable used in lambda expression should be final or effectively final."
this keyword: In a lambda,
this refers to the enclosing class instance. In an anonymous class, this refers to the anonymous class instance itself.Class file: An anonymous class generates a separate
Outer$1.class file. A lambda does not โ it is compiled as a synthetic method using invokedynamic.Performance: Lambdas are slightly faster to load and instantiate than anonymous classes due to
invokedynamic deferring implementation to runtime.Target type: A lambda can only implement a functional interface. An anonymous class can implement any interface (including multi-method ones) or extend any class.
State: An anonymous class can declare its own fields. A lambda cannot โ it captures variables from the enclosing scope only.
boolean test(T t) โ takes T, returns boolean. Used for filtering. Composed with and(), or(), negate().Function<T, R>:
R apply(T t) โ takes T, returns R. Used for transformations. Composed with andThen() (apply this then that) and compose() (apply that then this).Consumer<T>:
void accept(T t) โ takes T, returns nothing. Used for side effects (printing, saving). Chained with andThen().Supplier<T>:
T get() โ takes nothing, returns T. Used for lazy evaluation, factory methods, deferred computation.
ClassName::staticMethod โ e.g., Integer::parseInt is s -> Integer.parseInt(s)2. Instance method of a specific object:
instance::method โ e.g., System.out::println is s -> System.out.println(s)3. Instance method of an arbitrary object of a type:
ClassName::instanceMethod โ e.g., String::length is s -> s.length(). The first argument becomes the receiver.4. Constructor reference:
ClassName::new โ e.g., ArrayList::new is () -> new ArrayList()Use method references when the lambda body is just a direct call to a single existing method โ they are more readable and convey the same intent with less noise.
andThen(after): apply this function first, then pass the result to after.
f.andThen(g).apply(x) = g(f(x))compose(before): apply before first, then pass the result to this function.
f.compose(g).apply(x) = f(g(x))Example with
times2.andThen(plus10) vs times2.compose(plus10) on input 5:โ andThen: (5ร2)+10 = 20
โ compose: (5+10)ร2 = 30
Memory aid:
andThen reads left-to-right (f then g); compose reads right-to-left (g then f).
๐ฑ Lambdas & Functional Interfaces Throughout Spring Boot
Lambda expressions are the backbone of modern Spring Boot code. They appear in stream pipelines, Spring Security configuration, event listeners, bean definitions, and more.
list.stream().filter(...).map(...).collect(...) everywhereSpring Security (lambda DSL) โ
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())@Bean definitions โ
@Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); }ApplicationEventListener โ
context.addApplicationListener(event -> log.info("started"))RestTemplate / WebClient โ
webClient.get().retrieve().bodyToMono(String.class) uses Function chainsSpecification API (JPA) โ
Specification<User> spec = (root, query, cb) -> cb.equal(root.get("active"), true)
// โโ Spring Security 6 โ lambda DSL โโโโโโโโโโโโโโโโโโโโโโโโโโโ @Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth // Consumer<AuthorizationManagerRequestMatcherRegistry> .requestMatchers("/public/**").permitAll() .anyRequest().authenticated()) .formLogin(form -> form // Consumer<FormLoginConfigurer> .loginPage("/login").permitAll()) .build(); } } // โโ JPA Specification โ lambda implements Specification<T> โโโ public class UserSpec { public static Specification<User> isActive() { return (root, query, cb) -> // lambda = SAM toPredicate() cb.isTrue(root.get("active")); } public static Specification<User> nameContains(String q) { return (root, query, cb) -> cb.like(cb.lower(root.get("name")), "%" + q.toLowerCase() + "%"); } } // Usage: userRepo.findAll(isActive().and(nameContains("alice"))) // โโ Stream pipeline in service layer โโโโโโโโโโโโโโโโโโโโโโโโโ @Service public class OrderService { public Map<String, List<Order>> groupByStatus(List<Order> orders) { return orders.stream() .filter(o -> o.getTotal() > 0) // Predicate .sorted(Comparator.comparing(Order::getDate)) // method ref .collect(Collectors.groupingBy(Order::getStatus)); // method ref } }
http.authorizeRequests().antMatchers("/admin").hasRole("ADMIN").and().formLogin(). Spring 6 replaced this with the lambda DSL where every configurer takes a Consumer<T> โ each block is a lambda. This is cleaner, fully IDE-navigable, and avoids the confusing .and() connector calls. Understanding functional interfaces is essential to reading modern Spring Security configurations.
- Functional interface = exactly 1 abstract method (SAM); default/static don't count
@FunctionalInterfaceis optional but enforces SAM at compile time- Object methods (equals, hashCode, toString) do NOT count as abstract methods
- Lambda shorthand: omit types โ omit parens (1 param) โ omit braces+return (1 expr)
- Zero params:
() -> ...โ parentheses mandatory - Two+ params:
(a, b) -> ...โ parentheses mandatory - Lambda captures effectively final locals only; instance/static fields unrestricted
- Lambda
this= enclosing class; anonymous classthis= anonymous class itself - Lambda does NOT generate a separate .class file (uses invokedynamic)
- Predicate:
test()ยท Function:apply()ยท Consumer:accept()ยท Supplier:get() andThen: f then g ยทcompose: g then f (opposite order)- 4 method reference forms: static, specific instance, arbitrary instance, constructor
๐ MCQ Traps
| Question | Answer | Trap / Reason |
|---|---|---|
| Interface with 2 abstract methods โ valid @FunctionalInterface? | No โ compile error | SAM rule: exactly 1 abstract method required |
| Interface re-declares Object.equals() โ does it count as abstract? | No โ does not count | Object method re-declarations are excluded from SAM count |
| Can lambda capture a local variable that changes after lambda creation? | No โ compile error | Variable must be effectively final; modification after assignment disqualifies it |
| In lambda body, what does this refer to? | The enclosing class instance | Unlike anonymous class, lambda does not introduce a new this scope |
| f.andThen(g).apply(5) โ which runs first? | f runs first, then g | andThen = "apply this, THEN the argument"; compose is the reverse |
| Lambda for zero-param method โ can you write: x -> "hi"? | No โ must be: () -> "hi" | Zero params requires empty parentheses; x -> ... implies one parameter named x |
| Does lambda create a new .class file like anonymous class? | No โ uses invokedynamic | Lambda is compiled as synthetic method; no Outer$1.class is generated |
๐ One-Liner Definitions
- Functional Interface โ An interface with exactly one abstract method (SAM); the target type for lambda expressions and method references.
- Lambda Expression โ An anonymous function implementing a functional interface's SAM; syntax:
(params) -> body; compiled viainvokedynamicwithout generating a .class file. - Method Reference โ Shorthand for a lambda that calls a single existing method; four forms: static, instance, arbitrary instance, constructor.
- Effectively Final โ A local variable assigned exactly once; can be captured by a lambda without the
finalkeyword. - Predicate<T> โ Functional interface:
boolean test(T t); used for boolean conditions, filtering. - Function<T,R> โ Functional interface:
R apply(T t); used for transformations; composable viaandThen/compose. - Consumer<T> โ Functional interface:
void accept(T t); used for side-effecting operations. - Supplier<T> โ Functional interface:
T get(); used for lazy/deferred value production.
Introduction โ Classes & Objects โ Methods โ Memory โ Arrays โ Static โ Encapsulation โ this โ Constructors โ Copy Constructor โ Singleton โ Naming โ Anonymous Objects โ Inheritance โ super โ Overriding โ Packages โ Access Modifiers โ Polymorphism โ final โ Object Class โ Casting โ Abstraction โ Inner Classes โ Interfaces โ Interface Modern Features โ Enum โ Annotations โ Var-Args โ Functional Interface & Lambda
Master Summary โ Complete OOPs Quick Reference
๐๏ธ The Four Pillars of OOP
| Pillar | One-Line Definition | Java Mechanism | Real-World Analogy |
|---|---|---|---|
| Encapsulation | Wrap data + behaviour; hide internals | private fields + getters/setters | Capsule pill โ medicine inside, safe exterior |
| Inheritance | Child reuses parent's code (IS-A) | extends keyword | Child inherits traits from parent |
| Polymorphism | One interface, many forms | Overloading + Overriding + DMD | A person is employee at work, parent at home |
| Abstraction | Show essential, hide complexity | abstract class + interface | Car dashboard โ you drive without knowing engine |
๐ All 30 Topics โ Condensed Reference
| # | Topic | Key Point | Must-Know |
|---|---|---|---|
| 01 | Intro to OOP | 4 pillars: APIE. OOP models real world as objects | Procedural vs OOP tradeoffs |
| 02 | Classes & Objects | Class = blueprint (no memory); Object = instance (Heap) | Reference copy trap; dot operator |
| 03 | Methods | Overloading by type/count/order of params; pass-by-value always | Method chaining; return types |
| 04 | Memory Management | Stack = local vars + frames; Heap = objects | GC handles Heap; Stack auto-cleared on return |
| 05 | Arrays | Object on Heap; .length field; 0-indexed; ArrayIndexOutOfBounds | Object array default = null |
| 06 | Static | Class-level (not instance); static block runs once at class load | Cannot access instance vars from static context |
| 07 | Encapsulation | private fields + public getters/setters; data hiding | Encapsulation โ Abstraction |
| 08 | this keyword | Current object ref; this() constructor chain; resolves shadowing | this() must be first statement |
| 09 | Constructors | Same name as class; no return type; auto default if none defined | Overloading; default disappears when param constructor added |
| 10 | Copy Constructor | Manual in Java; shallow vs deep copy | Java has no built-in copy constructor unlike C++ |
| 11 | Singleton | Private constructor + static instance; Enum Singleton is best | Double-checked locking; volatile keyword |
| 12 | Naming Conventions | PascalCase (class), camelCase (method/var), ALL_CAPS (constant) | Package = reverse domain lowercase |
| 13 | Anonymous Objects | Object without reference; single-use; immediately GC-eligible | Anonymous object โ anonymous class |
| 14 | Inheritance | extends; IS-A; single + multilevel + hierarchical; no multiple | Diamond Problem โ no multiple class inheritance in Java |
| 15 | super Keyword | Parent field/method/constructor; implicit super() in every constructor | super() must be first statement; Object is root |
| 16 | Method Overriding | Same name+params in subclass; runtime decision; @Override | Cannot override static/final/private; covariant return OK |
| 17 | Packages | package keyword; import; java.lang auto-imported; reverse domain | Wildcard import doesn't import sub-packages |
| 18 | Access Modifiers | private < default < protected < public | protected = package + subclass (even different package) |
| 19 | Polymorphism | Compile-time (overloading) vs Runtime (overriding + DMD) | Parent ref + child object โ parent ref cannot access child-only methods |
| 20 | final | final var = constant; final method = no override; final class = no extend | Blank final must be assigned in constructor |
| 21 | Object Class | Root of all classes; toString/equals/hashCode contract | equals+hashCode must be consistent; == vs equals |
| 22 | Casting | Upcasting implicit (safe); downcasting explicit (risky) | instanceof check before downcast; ClassCastException |
| 23 | Abstraction | abstract class: partial; interface: full contract | Abstract class can have constructors; cannot instantiate |
| 24 | Inner Classes | Non-static (accesses outer); static (independent); anonymous (inline) | Static inner class = no outer instance needed |
| 25 | Interfaces | Contract; implements; default public abstract / public static final | Multiple interface implementation solves multiple inheritance |
| 26 | Interface Modern | default (Java 8, inherited), static (Java 8, not inherited), private (Java 9) | Diamond problem โ must override; B.super.method() syntax |
| 27 | Enum | final class extends java.lang.Enum; constants = static final singletons | Cannot extend; can implement; constructor always private |
| 28 | Annotations | Metadata (@interface); @Retention / @Target configure behaviour | RUNTIME retention needed for reflection; default is CLASS |
| 29 | Var-Args | Type... syntax; treated as array; must be last parameter; one per method | Overloading priority: exact > widening > vararg |
| 30 | Lambda & Functional | Lambda = anonymous function; requires @FunctionalInterface (SAM) | Effectively final capture; method references (4 forms) |
๐ Critical Comparisons at a Glance
| Comparison | A | B | Key Distinction |
|---|---|---|---|
| Overloading vs Overriding | Compile-time; same class; different params | Runtime; subclass; same signature | Overloading = ad hoc polymorphism; Overriding = subtype polymorphism |
| Abstract class vs Interface | Partial impl; constructor; single inherit | Full contract; no constructor; multiple implement | Abstract = IS-A with shared state; Interface = CAN-DO contract |
| == vs equals() | Reference equality (same object?) | Logical equality (same value?) | Always override equals() for value classes; use == for enum/null check |
| Stack vs Heap | Local vars, method frames; LIFO; auto-freed | Objects, instance vars; GC-managed | Stack overflow = deep recursion; Heap OutOfMemory = too many objects |
| this() vs super() | Calls another constructor in same class | Calls parent class constructor | Both must be FIRST statement; cannot use both in same constructor |
| final vs finally vs finalize | Modifier (var/method/class) | try-catch block; finalize() = GC callback (deprecated) | Three completely different concepts sharing a name prefix |
| Checked vs Unchecked Exception | Must handle/declare (IOException) | RuntimeException (NullPointerException) | Checked = recoverable; Unchecked = programming bug |
| Shallow vs Deep Copy | Copies references to nested objects | Recursively copies all nested objects | Shallow = fast but shared state; Deep = safe but expensive |
๐ Access Modifier Quick Reference
| Modifier | Same Class | Same Package | Subclass (diff pkg) | Everywhere |
|---|---|---|---|---|
private | โ | โ | โ | โ |
default | โ | โ | โ | โ |
protected | โ | โ | โ | โ |
public | โ | โ | โ | โ |
๐ Inheritance Rules Summary
๐๏ธ Core OOP Concepts (Q1โQ10)
Inheritance โ child class reuses parent's code via
extends; IS-A relationship.Polymorphism โ one interface, many forms: compile-time (overloading) and runtime (overriding + DMD).
Abstraction โ exposing only essential features; hiding implementation via abstract classes and interfaces.
new; it resides on the Heap and has its own copy of instance variables. Multiple objects can be created from one class, each with independent state.private. Access is controlled through public getters/setters. Benefits: data validation in setters, hiding implementation details, reducing coupling, making code easier to maintain and test without breaking callers.Encapsulation is about implementation โ how is data protected? It hides the internal state by restricting field access (achieved via access modifiers).
Analogy: A TV's remote control is the abstraction (you know what buttons do); the circuit board inside is encapsulated (hidden, protected).
Compile-time (Static) polymorphism โ method overloading; resolved at compile time based on parameter types.
Runtime (Dynamic) polymorphism โ method overriding + Dynamic Method Dispatch; resolved at runtime based on actual object type.
Example:
Animal a = new Dog(); a.speak(); โ the JVM calls Dog.speak() at runtime even though the reference is Animal.Overriding: same method name AND same parameter list, in a subclass. Resolved at runtime via DMD. Return type can be covariant (narrowed). Cannot override static/final/private methods.
Example:
Animal a = new Cat(); When a.speak() is called, Java looks at the actual object (Cat) and calls Cat.speak(), even though the reference is typed Animal. This is the engine behind runtime polymorphism.extends (IS-A relationship).Single: one child, one parent.
Multilevel: A โ B โ C (chain).
Hierarchical: one parent, multiple children.
Multiple inheritance via class: NOT supported (Diamond Problem). Solved via multiple interface implementation.
B.super.method()).extends (class) or implements (interface). Tightly couples child to parent.HAS-A โ composition/aggregation. A Car HAS-A Engine. Modelled by holding a reference to another object as a field. Looser coupling; generally preferred over deep inheritance hierarchies ("favour composition over inheritance").
๐ง Constructors, this & super (Q11โQ18)
new; cannot be abstract, final, static, or synchronized. A method is a named block of behaviour with a return type, called explicitly.this(args) calls a sibling constructor in the same class. super(args) calls the parent's constructor. Rules: the chaining call must be the first statement in the constructor body; you cannot use both this() and super() in the same constructor; every constructor implicitly calls super() if no explicit chain call is present.this.name = name distinguishes field from parameter.2. Constructor chaining โ
this(args) calls another constructor in the same class (must be first statement).3. Pass current object โ
someMethod(this) passes the current object as an argument.4. Return current object โ enables method chaining (builder pattern).
5. Non-static context only โ cannot use
this inside a static method.this() โ calls another constructor within the same class. Used for constructor overload chaining.super() โ calls a constructor in the parent class. Used to initialise inherited state. Every constructor has an implicit super() unless this() or an explicit super() is provided. Both must be the first statement, so they are mutually exclusive in the same constructor body.new. Primary uses: Singleton pattern (controlled single instance via static factory method), utility/helper classes (like java.lang.Math โ no need to instantiate), and static factory method pattern (object creation through named static methods).enum Singleton { INSTANCE; } โ JVM guarantees one instance, serialisation-safe, reflection-safe.๐ Interfaces & Abstract Classes (Q19โQ28)
Interface: traditionally pure contract (all abstract); since Java 8 can have default/static methods; since Java 9 private methods; fields are public static final; no constructors; supports multiple implementation.
Use abstract class when sharing code among closely related classes (IS-A + shared state).
Use interface when defining a capability/contract unrelated to class hierarchy (CAN-DO).
new AbstractClass() directly causes a compile error. However, you can instantiate a concrete subclass and hold it in an abstract class reference (AbstractClass ref = new ConcreteSubclass();). You can also create an anonymous class inline that provides implementations for all abstract methods โ this effectively creates an unnamed subclass instance.@FunctionalInterface (optional but recommended โ enforces SAM at compile time). Required for lambda expressions. Built-in examples: Runnable, Callable, Predicate<T>, Function<T,R>, Consumer<T>, Supplier<T>, Comparator<T>.default. Implementing classes inherit them automatically and can optionally override. They were added for backward compatibility: the JDK needed to add new methods (like Collection.stream(), Iterable.forEach()) to existing widely-implemented interfaces without breaking every class that already implemented them.B.super.methodName().public static final โ class-level constants). Since Java 9, interfaces can have private methods for internal code reuse, but still no constructors.instanceof. Classic Java examples: Serializable (marks class as serialisable), Cloneable (allows Object.clone()), RandomAccess. Modern Java prefers annotations for this purpose, but marker interfaces still exist for type-safety reasons.Static nested class: declared static; no outer instance needed; can only access outer static members directly.
Local class: defined inside a method; can access effectively final local variables.
Anonymous class: nameless inline class; common for one-off interface implementations; replaced by lambdas for functional interfaces.
Animal a = new Dog();Downcasting: casting a superclass reference back to a subclass type โ explicit and potentially unsafe (throws
ClassCastException at runtime if the object is not actually the target type).Dog d = (Dog) a; โ always check with instanceof first. Java 16+ pattern matching: if (a instanceof Dog d) { d.fetch(); }๐ค Keywords & Modifiers (Q29โQ36)
final method: cannot be overridden in any subclass.
final class: cannot be extended. Examples:
String, Integer, all wrapper classes are final.No โ static methods cannot directly access instance (non-static) variables or call instance methods, because there is no implicit
this reference. They can access static fields and other static methods. Workaround: pass an object reference as a parameter.== compares references (memory addresses) for objects โ are they the exact same object in memory? For primitives, it compares values.equals() compares logical content โ are the objects meaningfully equal? The default Object.equals() uses ==; you override it to define value-based equality (e.g., two String objects with the same characters).Always override
hashCode() when overriding equals() โ they must be consistent.java.lang.Object โ it is the root of the entire class hierarchy. Key methods: toString() (default: className@hashCode โ override for meaningful representation), equals(Object) (default: reference equality โ override for value equality), hashCode() (must be consistent with equals), clone() (shallow copy โ class must implement Cloneable), getClass() (runtime class info), wait()/notify()/notifyAll() (thread coordination).private (same class only), default/package-private (same package), protected (same package + subclasses in any package), public (everywhere). For top-level classes only public and default are allowed. Best practice: use the most restrictive access possible โ prefer private fields, package-private or protected for internal use, public only for intentional API.static { ... }: runs once when the class is first loaded by the JVM, before any object is created or main() runs. Used for one-time static initialisation (loading a driver, reading config).Instance initialiser block
{ ... }: runs every time a new object is created, before the constructor body but after the super() call. Copied into every constructor by the compiler. Used for shared initialisation logic across overloaded constructors.type... name) allows a method to accept zero or more arguments of a type without the caller building an array explicitly. The JVM treats it as an array internally. Rules: (1) must be the last parameter; (2) only one vararg per method; (3) can pass an explicit array. Overload resolution priority: exact match > widening > varargs (varargs are chosen last). printf and String.format are classic vararg methods.String: immutable; every modification creates a new object; thread-safe by design; lives in String Pool.StringBuilder: mutable; no synchronisation; not thread-safe; fastest for single-threaded string building.StringBuffer: mutable; all methods synchronized; thread-safe; slower due to synchronisation overhead.Use
StringBuilder for most cases; StringBuffer only when shared across threads.โ๏ธ Enum, Annotations & Lambdas (Q37โQ44)
final class that implicitly extends java.lang.Enum. Each constant is a public static final singleton instance created when the class loads. Enum can have fields, a private constructor, and methods. It cannot extend another class but can implement interfaces. Constants are compared with == (singletons). The single-constant enum (enum Singleton { INSTANCE; }) is the best Singleton implementation.@; it does not change logic but provides instructions to the compiler, build tools, or runtime framework. Define with @interface; configure with meta-annotations: @Retention (SOURCE/CLASS/RUNTIME), @Target (where it can be placed). Read at runtime via reflection: method.getAnnotation(MyAnnotation.class). Elements can only be primitives, String, Class, enum, annotation, or 1D arrays of these.(params) -> body. It provides an implementation of a functional interface (SAM) inline, without creating a named class. Lambdas capture effectively final local variables from the enclosing scope. They do not generate a separate .class file (handled by invokedynamic at the bytecode level). Examples: () -> System.out.println("hi"), x -> x * 2, (a, b) -> a + b.ClassName::methodName.1. Static method ref:
Integer::parseInt โ s -> Integer.parseInt(s)2. Instance method ref (bound):
str::toUpperCase โ () -> str.toUpperCase()3. Instance method ref (unbound):
String::toUpperCase โ s -> s.toUpperCase()4. Constructor ref:
ArrayList::new โ () -> new ArrayList()final. Lambdas can capture only effectively final local variables from the enclosing scope. This restriction exists because lambdas may outlive the stack frame where the variable was declared; to prevent stale/inconsistent data, Java requires the captured value to be stable. Modifying a captured variable after the lambda is defined causes a compile error.Predicate<T>: boolean test(T t) โ boolean condition / filtering.Function<T,R>: R apply(T t) โ transformation; composable.Consumer<T>: void accept(T t) โ side-effecting operation.Supplier<T>: T get() โ deferred / lazy value production.BiFunction<T,U,R>: two inputs, one output.UnaryOperator<T>: Function where T == R.BinaryOperator<T>: BiFunction where all types == T.Comparable<T>: defines the class's natural ordering; implemented by the class itself via int compareTo(T other); one ordering per class.Comparator<T>: external, custom ordering; passed to sort methods; multiple comparators possible for the same class; functional interface (single compare(T o1, T o2)) โ can be written as a lambda. Use Comparable for the default sort; use Comparator for alternate orderings.throw: a statement inside a method body that actually throws an exception instance: throw new IllegalArgumentException("msg");. Execution stops at this point.throws: a clause in the method signature that declares which checked exceptions the method may propagate to the caller: void readFile() throws IOException. Callers must handle or re-declare the exception. throws is a declaration; throw is the action.๐งฉ Miscellaneous & Advanced (Q45โQ50)
Deep copy: recursively copies all nested objects so the copy is completely independent. Achieved via manual copy constructor, serialisation/deserialisation, or libraries like Apache Commons Lang's
SerializationUtils.clone(). Deep copy is safer but more expensive.Coupling: degree of dependency between classes. Low coupling = classes can change independently = good. High coupling = change in one class breaks many others = bad.
Goal: High cohesion, low coupling. Achieved through encapsulation, interfaces, dependency injection.
O โ Open/Closed: open for extension, closed for modification (use inheritance/interfaces).
L โ Liskov Substitution: subclass must be usable wherever parent is expected without breaking logic.
I โ Interface Segregation: prefer many small interfaces over one fat interface.
D โ Dependency Inversion: depend on abstractions (interfaces), not concrete implementations.
Aggregation (weak HAS-A): the contained object can exist independently of the container. Example: University HAS-A Department โ Department can exist without the University object in memory.
Composition (strong HAS-A): the contained object's lifecycle is owned by the container โ it cannot exist independently. Example: House HAS-A Room โ if the House object is destroyed, Rooms are destroyed too. Composition implies complete ownership.
public static void main(String[] args) as the entry point โ static methods are not overridden (they are hidden). A subclass can declare its own main but the parent's main is still a distinct static method, not polymorphically dispatched. So: overloading yes, polymorphic overriding no.Use abstract class when: sharing code (concrete methods) among closely related classes; sharing state (instance fields); you need a template method pattern; you are designing a framework base class. In modern Java (8+), with default methods on interfaces, the line is blurrier โ but abstract classes still win when shared mutable state is needed.
โก Quick Revision Cheat Sheet
๐ง Comprehensive MCQ Tricky Points โ All Topics
These are the most commonly tested traps across all 30 topics. Each row is a potential MCQ or True/False question.
Classes, Objects & Memory
| Statement / Question | Answer | Trap Explained |
|---|---|---|
| A class occupies memory when declared | FALSE | Only objects occupy Heap memory; class is just a blueprint |
| Two reference variables pointing to same object: change via one affects the other? | TRUE | Both refs point to same Heap object โ shared state |
| Java is pass-by-value or pass-by-reference? | Always pass-by-value | For objects, the VALUE of the reference is passed โ not the reference itself |
| Local variables are stored where? | Stack | Instance variables are on Heap (inside the object); local vars on Stack |
| Default value of an object reference? | null | Not 0 or empty โ null. Calling method on null โ NullPointerException |
Constructors & this/super
| Statement / Question | Answer | Trap Explained |
|---|---|---|
| Constructor has a return type of void | FALSE | Constructors have NO return type at all โ not even void. void makes it a regular method |
| Compiler adds default constructor when class has a parameterised constructor? | FALSE | Compiler adds default ONLY when NO constructor is defined |
| Can both this() and super() appear in same constructor? | No | Both must be first statement โ contradictory; compiler error |
| Can a constructor call itself recursively? | No | this() chain must terminate; circular this() chain is a compile error |
| Is it possible to have a constructor in an interface? | No | Interfaces cannot be instantiated; constructors serve no purpose there |
Inheritance, Overriding & Polymorphism
| Statement / Question | Answer | Trap Explained |
|---|---|---|
| Static methods can be overridden | FALSE โ they are hidden | Static methods are resolved at compile time based on reference type; not polymorphic |
| Private methods can be overridden | FALSE | Private methods are invisible to subclasses; same name in subclass = new method, not override |
| Parent reference to child object: parent ref can call child-specific method? | FALSE (compile error) | Ref type determines accessible methods at compile time; must downcast |
| Can overriding method throw a broader checked exception? | FALSE | Can throw narrower or no checked exception; broader = compile error |
| Method overloading can differ by return type alone | FALSE | Return type alone is NOT enough; parameter list must differ |
Static, final & Keywords
| Statement / Question | Answer | Trap Explained |
|---|---|---|
| Static block runs every time an object is created | FALSE | Static block runs ONCE when class is first loaded โ not per object |
| A final reference variable cannot have its object's fields changed | FALSE | final ref = reference cannot be reassigned; object's internal fields CAN change |
| Can a static method use the this keyword? | No | No instance โ no this. Compile error if used in static context |
| final class can have subclasses? | FALSE | final class cannot be extended. String is final. |
| abstract class must have at least one abstract method | FALSE | An abstract class can have zero abstract methods โ still cannot be instantiated |
Interfaces & Abstract Classes
| Statement / Question | Answer | Trap Explained |
|---|---|---|
| Interface fields are implicitly public static final | TRUE | Cannot declare private/non-static/non-final fields in an interface |
| Interface methods are implicitly public abstract (before Java 8) | TRUE | Explicitly declaring private or protected = compile error (pre-Java 8 for abstract) |
| A class can implement multiple interfaces and extend a class simultaneously | TRUE | class Foo extends Bar implements A, B โ perfectly valid |
| default methods in an interface are inherited by sub-interfaces | TRUE | Sub-interface inherits default but can re-abstract it or override it |
| static methods in an interface are inherited by implementing classes | FALSE | Called as InterfaceName.method() only; not part of implementing class API |
Enum, Annotations & Lambda
| Statement / Question | Answer | Trap Explained |
|---|---|---|
| Enum can extend another class | FALSE | Already extends java.lang.Enum implicitly; single inheritance full |
| Enum constructor can be public | FALSE | Must be private (or package-private); public is a compile error |
| ordinal() returns 1-based index | FALSE โ 0-based | First constant ordinal = 0, like array indices |
| Default @Retention if not specified? | CLASS | Not SOURCE, not RUNTIME. CLASS = in .class file but not available via reflection |
| Lambda can capture non-effectively-final local variables | FALSE | Captured locals must be effectively final (never reassigned); compile error otherwise |
| Vararg parameter must be the first parameter | FALSE โ must be LAST | Having vararg before other params is a compile error |
| A method can have two vararg parameters | FALSE | Only one vararg parameter allowed per method |
- Java is always pass-by-value (even for object references)
- Constructor has no return type โ not even void
- Compiler adds default constructor only if NO constructor is defined
- Static methods are hidden in subclasses, not overridden
- Abstract class can have zero abstract methods โ still cannot be instantiated
- Interface static methods are not inherited by implementing classes
- Default
@Retentionis CLASS โ not SOURCE, not RUNTIME - Enum ordinal is 0-based; first constant = 0
- final reference = reference cannot change; object's fields CAN still change
this()andsuper()cannot both appear in the same constructor