☕ [6] Object-Oriented Programming in Java — The Four Pillars Explained!

☕ [6] Object-Oriented Programming in Java — The Four Pillars Explained!

☕ [6] Object-Oriented Programming in Java — The Four Pillars Explained!

🧭 Why OOP Matters in Java

Java is more than just writing lines of code — it's about thinking in objects. Object-Oriented Programming (OOP) lets you model the real world into your programs.

OOP in Java stands on four main pillars:

  • 🛡️ Abstraction
  • 📦 Encapsulation
  • 🌳 Inheritance
  • 🔀 Polymorphism

Mastering these pillars will make your code modular, reusable, and easy to maintain.

📖 A Quick Recap: What is OOP?

Object-Oriented Programming organizes software as a collection of objects — each with its own state (data) and behavior (methods).

Example in the real world:

  • A Car has properties like color, brand, speed.
  • It also has behaviors like drive(), brake(), honk().
  • In Java, these are represented as fields and methods inside a class.

🛡️ Pillar 1: Abstraction

Abstraction means showing only the essential details and hiding the rest. Think of a TV — you know how to use the remote but don't need to know the complex electronics inside.

In Java, abstraction is achieved using:

  • abstract classes
  • interfaces
// Example of Abstraction in Java
// Save this as Main.java to run

// Abstract class representing a general Animal
abstract class Animal {
    
    // Abstract method (no body) - must be implemented by subclasses
    abstract void makeSound();

    // Concrete (non-abstract) method - has implementation
    void eat() {
        System.out.println("This animal eats food.");
    }
}

// Dog is a specific type of Animal
class Dog extends Animal {

    // Implementing the abstract method from Animal
    @Override
    void makeSound() {
        System.out.println("Woof! Woof!");
    }
}

// Main class to run the program
public class Main {
    public static void main(String[] args) {
        
        // You cannot create an object of an abstract class directly:
        // Animal a = new Animal(); // ❌ This would cause an error

        // Instead, you create an object of a subclass
        Animal myDog = new Dog();

        // Call the implemented abstract method (Dog's version)
        myDog.makeSound(); // Output: Woof! Woof!

        // Call the concrete method defined in Animal
        myDog.eat(); // Output: This animal eats food.
    }
}

🛡️ Pillar 1: Abstraction — Concept Questions

Q1. What is Abstraction and why do we use abstract classes?

// Save as Main.java

// Abstract class: represents the idea of a Vehicle
abstract class Vehicle {
    // Abstract method — no body, must be implemented by subclasses
    abstract void start();

    // Concrete method — has a body, shared by all subclasses
    void stop() {
        System.out.println("Vehicle stopped.");
    }
}

// Car provides its own version of start()
class Car extends Vehicle {
    @Override
    void start() {
        System.out.println("Car starts with a key.");
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle v = new Car(); // We use abstraction to hide implementation details
        v.start(); // Calls Car's version
        v.stop();  // Calls shared method from Vehicle
    }
}

Explanation: Abstraction hides the complexity (how the vehicle starts) and shows only the necessary details (start/stop methods). Abstract classes can have both abstract and concrete methods.

Expected Mistake: Trying to create an object of an abstract class directly (new Vehicle()) will cause a compile-time error.

Q2. Can abstract classes have constructors?

// Abstract class with constructor
abstract class Animal {
    String name;

    // Constructor
    Animal(String name) {
        this.name = name;
        System.out.println("Animal constructor called for: " + name);
    }

    abstract void makeSound();
}

class Dog extends Animal {
    Dog(String name) {
        super(name); // Calls Animal constructor
    }

    @Override
    void makeSound() {
        System.out.println(name + " says Woof!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog("Buddy");
        myDog.makeSound();
    }
}

Explanation: Abstract classes can have constructors. These constructors are called when a subclass object is created, helping initialize shared data.

Expected Mistake: Assuming abstract classes can't have constructors — they can, but you can't call them directly with new.

Q3. Difference between abstract classes and interfaces in abstraction.

// Abstract class
abstract class Shape {
    // Public so it matches the visibility of interface methods
    public abstract void draw();
    
    void info() {
        System.out.println("This is a shape.");
    }
}

// Interface
interface Drawable {
    void draw(); // Implicitly public and abstract
}

// Circle implements both abstraction methods
class Circle extends Shape implements Drawable {
    @Override
    public void draw() { // Must be public to match Drawable
        System.out.println("Drawing a circle.");
    }
}

public class Main {
    public static void main(String[] args) {
        Circle c = new Circle();
        c.draw();
        c.info();
    }
}

Diagram:

// Diagram:
             (abstract class)        (interface)
               Shape   <----------->  Drawable
                 ↑                         ↑
                 └───────────┬─────────────┘
                             │
                           Circle

Explanation: Abstract classes can contain both abstract and non-abstract methods, as well as state (fields). Interfaces declare methods that are implicitly public abstract (and can also have default and static methods since Java 8). A class can extend only one abstract class but can implement multiple interfaces. When implementing an interface method, its visibility must remain public.

Expected Mistake: Using a lower access modifier (like default or protected) when overriding an interface method will cause a compile-time error: “Cannot reduce the visibility of the inherited method.”

Q4. Abstraction with multiple subclasses (real-world analogy).

// Abstract base class for Payment
abstract class Payment {
    abstract void pay(double amount);
}

// Credit Card payment
class CreditCardPayment extends Payment {
    @Override
    void pay(double amount) {
        System.out.println("Paid $" + amount + " using Credit Card.");
    }
}

// PayPal payment
class PayPalPayment extends Payment {
    @Override
    void pay(double amount) {
        System.out.println("Paid $" + amount + " using PayPal.");
    }
}

public class Main {
    public static void main(String[] args) {
        Payment p1 = new CreditCardPayment();
        Payment p2 = new PayPalPayment();

        p1.pay(100.0);
        p2.pay(250.5);
    }
}

Explanation: Abstraction allows using the same method (pay()) for different payment types without knowing their internal implementation.

Expected Mistake: Writing extra unrelated methods in the abstract class that subclasses can't logically implement — this breaks abstraction.

Q5. Partial abstraction in Java.

// Abstract class with some implemented methods
abstract class Report {
    abstract void generate(); // Must be implemented by subclasses

    void printHeader() { // Concrete method
        System.out.println("=== Report Header ===");
    }
}

class SalesReport extends Report {
    @Override
    void generate() {
        printHeader();
        System.out.println("Generating sales data...");
    }
}

public class Main {
    public static void main(String[] args) {
        Report r = new SalesReport();
        r.generate();
    }
}

Explanation: Abstract classes can be partially abstract — they can have both abstract and implemented methods, giving flexibility to subclasses.

Expected Mistake: Declaring all methods as abstract when some can be reused by multiple subclasses causes unnecessary code duplication.

🛡️ Pillar 1: Abstraction — Difficult Problems, Easy Explanations

Q1: Robot Actions Controller

We want to control a robot that can walk, talk, and pick up things — but the person using it shouldn't know how these actions are programmed inside.

// Abstract class - only tells what robot CAN do, not how
abstract class Robot {
    abstract void walk(); // No details yet
    abstract void talk();
    abstract void pickUpItem();
}

// Real robot that knows how to do actions
class ServiceRobot extends Robot {
    @Override
    void walk() {
        System.out.println("Robot is walking forward.");
    }

    @Override
    void talk() {
        System.out.println("Robot says: Hello, friend!");
    }

    @Override
    void pickUpItem() {
        System.out.println("Robot picks up the item carefully.");
    }
}

public class RobotTest {
    public static void main(String[] args) {
        Robot r = new ServiceRobot();
        r.walk();
        r.talk();
        r.pickUpItem();
    }
}

Easy Explanation: Think of this like a game controller — you press “Walk” or “Jump” without knowing the code inside the robot. Abstraction hides the complicated steps and just gives you easy buttons to press.

Expected Mistake: If you try to give the steps inside Robot instead of just saying what it should do, you break abstraction.

Q2: Online Exam System

We want a system where students can take exams, but they don't see how questions are loaded from a database.

// Abstract class - blueprint for exam
abstract class OnlineExam {
    abstract void loadQuestions();
    abstract void submitAnswers();
}

class MathExam extends OnlineExam {
    @Override
    void loadQuestions() {
        System.out.println("Loading Math questions from database...");
    }

    @Override
    void submitAnswers() {
        System.out.println("Submitting Math answers to server...");
    }
}

public class ExamTest {
    public static void main(String[] args) {
        OnlineExam exam = new MathExam();
        exam.loadQuestions();
        exam.submitAnswers();
    }
}

Easy Explanation: Like opening a Google Form — you see the questions, but you have no clue where they were stored or how they came to your screen.

Expected Mistake: Putting database connection code directly in main() removes the benefit of abstraction.

Q3: Music Player

A music player can play songs from different sources — CD, USB, or Internet — without the user knowing the details.

// Abstract class
abstract class MusicPlayer {
    abstract void play();
}

class CDPlayer extends MusicPlayer {
    @Override
    void play() {
        System.out.println("Playing music from CD...");
    }
}

class USBPlayer extends MusicPlayer {
    @Override
    void play() {
        System.out.println("Playing music from USB...");
    }
}

public class MusicTest {
    public static void main(String[] args) {
        MusicPlayer player = new USBPlayer();
        player.play();
    }
}

Easy Explanation: You just press “Play” and don't worry whether the song is from a CD, USB, or YouTube.

Expected Mistake: If you force the user to write if-else checks for CD or USB in main(), you're removing abstraction.

Q4: Travel Ticket Booking

A booking system can issue tickets for Bus, Train, or Flight — the traveler doesn't care about the backend process.

// Abstract class
abstract class TicketBooking {
    abstract void bookTicket();
}

class BusBooking extends TicketBooking {
    @Override
    void bookTicket() {
        System.out.println("Bus ticket booked successfully.");
    }
}

class FlightBooking extends TicketBooking {
    @Override
    void bookTicket() {
        System.out.println("Flight ticket booked successfully.");
    }
}

public class BookingTest {
    public static void main(String[] args) {
        TicketBooking booking = new FlightBooking();
        booking.bookTicket();
    }
}

Easy Explanation: Like using an app to book — you click “Book” and the magic happens in the backend. You don't see API calls or databases.

Expected Mistake: Writing the booking steps in the main program instead of inside the specific booking class.

Q5: Drawing Shapes

A graphics program can draw shapes — Circle, Square, Triangle — without the user knowing the math behind each one.

// Abstract class
abstract class Shape {
    abstract void draw();
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a circle using radius...");
    }
}

class Square extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a square using 4 equal sides...");
    }
}

public class ShapeTest {
    public static void main(String[] args) {
        Shape s = new Circle();
        s.draw();
    }
}

Easy Explanation: You click “Draw Circle” and it appears — you don't calculate πr² or think about points.

Expected Mistake: Making the user do the math before calling draw() removes the benefit of abstraction.

🛡️ Pillar 1: Abstraction — DSA-Based Questions

Q1. Abstract class for different sorting algorithms

// Abstract class defining the "skeleton" of a sorting algorithm
abstract class SortAlgorithm {
    // Abstract method — forces subclasses to implement sorting
    abstract void sort(int[] arr);

    // Common method — can be reused by all subclasses
    void printArray(int[] arr) {
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

// Concrete class implementing Bubble Sort
class BubbleSort extends SortAlgorithm {
    void sort(int[] arr) {
        // Outer loop controls number of passes
        for (int i = 0; i < arr.length - 1; i++) {
            // Inner loop compares adjacent elements
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    // Swap if out of order
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        SortAlgorithm sorter = new BubbleSort();
        int[] arr = {5, 2, 9, 1, 5};
        sorter.sort(arr);
        sorter.printArray(arr); // Output: 1 2 5 5 9
    }
}

Example of Bubble Sort in action (arr = {5, 2, 9, 1, 5}):

Pass 1:
[5, 2, 9, 1, 5] → compare 5 & 2 → swap → [2, 5, 9, 1, 5]
[2, 5, 9, 1, 5] → compare 5 & 9 → no swap
[2, 5, 9, 1, 5] → compare 9 & 1 → swap → [2, 5, 1, 9, 5]
[2, 5, 1, 9, 5] → compare 9 & 5 → swap → [2, 5, 1, 5, 9]

Pass 2:
[2, 5, 1, 5, 9] → compare 2 & 5 → no swap
[2, 5, 1, 5, 9] → compare 5 & 1 → swap → [2, 1, 5, 5, 9]
[2, 1, 5, 5, 9] → compare 5 & 5 → no swap

Pass 3:
[2, 1, 5, 5, 9] → compare 2 & 1 → swap → [1, 2, 5, 5, 9]
[1, 2, 5, 5, 9] → compare 2 & 5 → no swap

Pass 4:
[1, 2, 5, 5, 9] → already sorted

Final Result: [1, 2, 5, 5, 9]

Explanation: The abstract class SortAlgorithm defines a blueprint for sorting algorithms. Each specific algorithm (BubbleSort, QuickSort, etc.) provides its own sort() method implementation but can reuse common logic like printArray(). Bubble Sort works by repeatedly comparing adjacent elements and swapping them if they are in the wrong order. With each pass, the largest remaining element "bubbles" to the end.

Expected Mistake: Forgetting to implement sort() in a subclass — or writing it incorrectly — will cause either a compile-time error (if not implemented) or incorrect sorting (if implemented wrongly).

Q2. Abstract data type for Stack

// Abstract Stack defining basic stack operations
abstract class Stack {
    abstract void push(int value);
    abstract int pop();
    abstract boolean isEmpty();
}

// Concrete implementation using array
class ArrayStack extends Stack {
    private int[] stack; // Array to store stack elements
    private int top;     // Index of the top element

    ArrayStack(int size) {
        stack = new int[size];
        top = -1; // -1 means stack is empty
    }

    void push(int value) {
        if (top == stack.length - 1) {
            System.out.println("Stack overflow!");
            return;
        }
        stack[++top] = value; // Increase top then add element
    }

    int pop() {
        if (isEmpty()) {
            System.out.println("Stack underflow!");
            return -1;
        }
        return stack[top--]; // Return top element, then decrease top
    }

    boolean isEmpty() {
        return top == -1;
    }
}

public class Main {
    public static void main(String[] args) {
        Stack s = new ArrayStack(5); // Stack with capacity 5
        s.push(10); // Push element 10
        s.push(20); // Push element 20
        System.out.println(s.pop()); // Removes 20, prints 20
    }
}

Example Step-by-Step:

Initial: top = -1, stack = [ _ , _ , _ , _ , _ ]

Push(10):
top = 0 → stack = [ 10 , _ , _ , _ , _ ]

Push(20):
top = 1 → stack = [ 10 , 20 , _ , _ , _ ]

Pop():
returns stack[1] = 20
top = 0 → stack = [ 10 , _ , _ , _ , _ ]

Diagram of Stack Operations:

// Diagram:
   (Top)
    ↑
   ┌───┐
   │20 │  ← Push 20
   ├───┤
   │10 │  ← Push 10
   ├───┤
   │   │
   ├───┤
   │   │
   ├───┤
   │   │
   └───┘
   (Bottom)

Pop → removes 20 → top now points to 10

Explanation: The abstract class Stack defines a "contract" for stack operations (push(), pop(), isEmpty()) without specifying how they work. The ArrayStack class implements these methods using an array as storage. The top variable tracks the position of the last inserted element. The LIFO (Last-In, First-Out) principle is followed: the most recently added element is removed first.

Expected Mistake: Attempting new Stack() will cause a compile-time error because Stack is abstract and cannot be instantiated directly.

Q3. Graph representation abstraction

import java.util.*;

// Abstract Graph class
abstract class Graph {
    abstract void addEdge(int u, int v);
    abstract void printGraph();
}

// Adjacency List implementation
class AdjacencyListGraph extends Graph {
    private Map<Integer, List<Integer>> adjList = new HashMap<>();

    void addEdge(int u, int v) {
        adjList.putIfAbsent(u, new ArrayList<>());
        adjList.get(u).add(v);
    }

    void printGraph() {
        for (var entry : adjList.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Graph g = new AdjacencyListGraph();
        g.addEdge(1, 2);
        g.addEdge(2, 3);
        g.printGraph();
        // Output:
        // 1 -> [2]
        // 2 -> [3]
    }
}

Example Step-by-Step (Directed Graph):

Start: adjList = { }

addEdge(1, 2):
adjList = { 1: [2] }

addEdge(2, 3):
adjList = { 1: [2], 2: [3] }

printGraph():
1 -> [2]
2 -> [3]

Diagram (Adjacency List Representation):

// Diagram:
1 → [2]
2 → [3]
3 → []

Explanation: The abstract class Graph defines the core operations (addEdge(), printGraph()) without specifying how the graph is stored. This allows flexibility — we can later replace AdjacencyListGraph with an AdjacencyMatrixGraph without changing client code. The adjacency list uses a Map<Integer, List<Integer>> where each key is a vertex and its value is a list of connected vertices.

Expected Mistake: Mixing adjacency list and adjacency matrix logic in one class — this breaks the abstraction because the implementation details should be isolated inside their respective classes.

Q4. Searching algorithms abstraction

// Abstract Search Algorithm
abstract class SearchAlgorithm {
    abstract int search(int[] arr, int target);
}

// Linear Search implementation
class LinearSearch extends SearchAlgorithm {
    int search(int[] arr, int target) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == target) return i; // Found target, return index
        }
        return -1; // Not found
    }
}

public class Main {
    public static void main(String[] args) {
        SearchAlgorithm searcher = new LinearSearch();
        int[] arr = {4, 2, 7, 1};
        System.out.println(searcher.search(arr, 7)); // Output: 2
    }
}

Step-by-Step Linear Search Example:

Array: [4, 2, 7, 1]
Target: 7

Step 1: Compare arr[0] = 4 with target 7 → not equal
Step 2: Compare arr[1] = 2 with target 7 → not equal
Step 3: Compare arr[2] = 7 with target 7 → match found → return index 2

Diagram:

// Diagram:
Index:   0    1    2    3
Array:  [4]  [2]  [7]  [1]
         ↑    ↑    ✔
         |    |   Found target (index 2)
         |    |
      Compare until match

Explanation: The abstract class SearchAlgorithm defines a general search method signature, but doesn’t decide how the search is done. LinearSearch provides one implementation by checking each element sequentially. This design allows us to later create other search classes (e.g., BinarySearch) and swap them in without modifying client code.

Expected Mistake: If we replace LinearSearch with a BinarySearch implementation, the array must be sorted first — otherwise, the search result will be incorrect.

Q5. Priority Queue abstraction

import java.util.*;

// Abstract Priority Queue
abstract class PriorityQueueDS {
    abstract void insert(int value);
    abstract int extractMax();
}

// Max-Heap Implementation using Java's PriorityQueue
class MaxHeap extends PriorityQueueDS {
    // PriorityQueue with reverse order comparator to make it a Max-Heap
    private PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());

    void insert(int value) {
        maxHeap.add(value);
    }

    int extractMax() {
        return maxHeap.poll(); // Returns and removes highest priority element
    }
}

public class Main {
    public static void main(String[] args) {
        PriorityQueueDS pq = new MaxHeap();
        pq.insert(10);
        pq.insert(30);
        pq.insert(20);
        System.out.println(pq.extractMax()); // Output: 30
    }
}

Step-by-Step Example:

Initial: maxHeap = [ ]

Insert(10): maxHeap = [10]
Insert(30): maxHeap = [30, 10]  (30 is highest priority)
Insert(20): maxHeap = [30, 10, 20]

extractMax():
Removes 30 → returns 30
maxHeap = [20, 10]

Diagram (Max-Heap Priority Queue):

// Diagram:
        [30]
       /    \
    [10]   [20]

After extractMax():
        [20]
       /
    [10]

Explanation: The abstract class PriorityQueueDS defines the operations insert() and extractMax() without dictating how they are implemented. The MaxHeap class uses Java’s PriorityQueue with Collections.reverseOrder() to behave like a max-heap, ensuring the largest value always has the highest priority. This design allows changing to an array-based or custom heap implementation later without modifying client code.

Expected Mistake: Assuming poll() will throw an error when the queue is empty — it actually returns null. Always check for null before using the returned value.

🛡️ Pillar 1: Abstraction — 20 Questions with Solutions

These questions will help you master Abstraction in Java — from the basics to slightly tricky cases. Each one includes an explanation of the right answer and the common mistake you should avoid.

  • Q1: Which keyword is used to define an abstract class in Java?

    Explanation: The abstract keyword is used before the class declaration.

    Expected Mistake: Thinking it's interface — but interfaces are different from abstract classes.

  • Q2: Can an abstract class have concrete (implemented) methods?

    Explanation: Yes, abstract classes can have both abstract and fully implemented methods.

    Expected Mistake: Assuming all methods must be abstract.

  • Q3: What happens if you try to create an object of an abstract class?

    Explanation: You cannot instantiate an abstract class directly. It will cause a compilation error.

    Expected Mistake: Thinking you can create an object like a normal class.

  • Q4: Can an abstract class have a constructor?

    Explanation: Yes, abstract classes can have constructors which can be called from subclasses.

    Expected Mistake: Believing constructors are not allowed because the class is abstract.

  • Q5: How do you declare an abstract method?

    Explanation: Use the abstract keyword, no body, and a semicolon. Example: abstract void run();

    Expected Mistake: Adding curly braces {} to an abstract method, which is illegal.

  • Q6: Can an abstract class be final?

    Explanation: No, because final prevents inheritance, but abstract classes must be extended.

    Expected Mistake: Thinking final and abstract can be combined.

  • Q7: Can an abstract class extend another abstract class?

    Explanation: Yes, and it may choose to implement some or none of the inherited abstract methods.

    Expected Mistake: Believing it must implement all methods immediately.

  • Q8: What's the main difference between an interface and an abstract class?

    Explanation: An interface defines only contracts (until Java 8's default methods), while an abstract class can hold state (fields) and partial implementations.

    Expected Mistake: Thinking they are interchangeable in all situations.

  • Q9: Can you mark an abstract method as private?

    Explanation: No, because private methods cannot be overridden, and abstract methods must be overridden.

    Expected Mistake: Trying to hide an abstract method as private.

  • Q10: Can an abstract class implement an interface?

    Explanation: Yes, and it may leave some or all methods unimplemented for subclasses to complete.

    Expected Mistake: Believing it must implement all methods itself.

  • Q11: Can an abstract method be static?

    Explanation: No, because static methods are bound at compile time and cannot be overridden.

    Expected Mistake: Thinking static abstract makes sense — it doesn't in Java.

  • Q12: If a class has even one abstract method, what must the class be?

    Explanation: It must be declared abstract.

    Expected Mistake: Forgetting to mark it abstract, causing a compilation error.

  • Q13: Can an abstract class have main() method?

    Explanation: Yes, it can have a main method and be run like any class — but you still can't create its instance.

    Expected Mistake: Thinking abstract classes cannot have any executable code.

  • Q14: Can we use abstract with static for a class?

    Explanation: No, classes in Java cannot be static at top level.

    Expected Mistake: Trying to make an abstract static class at top level.

  • Q15: Can abstract methods be synchronized?

    Explanation: No, because synchronization applies to method body, which abstract methods don't have.

    Expected Mistake: Adding synchronized to an abstract method declaration.

  • Q16: Can you declare an abstract method in a non-abstract class?

    Explanation: No, if you declare an abstract method, the class must be abstract too.

    Expected Mistake: Trying to mix abstract methods in concrete classes.

  • Q17: Can abstract classes have final methods?

    Explanation: Yes, final methods cannot be overridden but can be present in an abstract class.

    Expected Mistake: Thinking final is forbidden in abstract classes.

  • Q18: What's the default modifier for an interface method (before Java 8)?

    Explanation: It's implicitly public and abstract.

    Expected Mistake: Assuming it's package-private.

  • Q19: Can an abstract class have instance variables?

    Explanation: Yes, unlike interfaces, abstract classes can have instance variables.

    Expected Mistake: Thinking only static variables are allowed.

  • Q20: Can we declare an abstract class without any abstract methods?

    Explanation: Yes, sometimes you make a class abstract just to prevent instantiation.

    Expected Mistake: Believing all abstract classes must have at least one abstract method.

🛡️ Pillar 1: Abstraction — Basic Questions

Q1. Create an abstract class Shape with an abstract method area() and a concrete method display().

// Save as Main.java

// Abstract class representing a general shape
abstract class Shape {
    // Abstract method: no body, forces subclasses to define their own version
    abstract double area();

    // Concrete method: already implemented and can be reused by all subclasses
    void display() {
        System.out.println("This is a shape.");
    }
}

// Circle is a specific type of Shape
class Circle extends Shape {
    double radius; // Unique property for Circle

    // Constructor to initialize radius
    Circle(double radius) {
        this.radius = radius;
    }

    // Implementing the abstract method from Shape
    @Override
    double area() {
        // Formula for area of a circle: π * r^2
        return Math.PI * radius * radius;
    }
}

// Main class to run the program
public class Main {
    public static void main(String[] args) {
        // Shape s = new Shape(); // ❌ Not allowed, Shape is abstract

        // Create a Circle object but store it in a Shape reference (polymorphism)
        Shape s = new Circle(5);

        // Call the shared concrete method from Shape
        s.display(); // Output: This is a shape.

        // Call the overridden abstract method (Circle's implementation)
        System.out.println("Area: " + s.area()); // Output: Area: 78.53981633974483
    }
}

Explanation: The abstract class Shape defines a common contract for all shapes using area(). Any concrete shape like Circle must implement this method. The display() method is reusable for all shapes without rewriting code. By storing Circle in a Shape reference, we show polymorphism — the call to area() executes the version defined in Circle.

Expected Mistake: Attempting new Shape() directly will fail because abstract classes cannot be instantiated. Only subclasses that provide all abstract method implementations can be instantiated.

Q2. Implement abstraction for different types of animals with sound() method.

// Abstract class representing a generic animal
abstract class Animal {
    // Abstract method: forces each animal type to define its own sound
    abstract void sound();

    // Concrete method: same behavior for all animals
    void sleep() {
        System.out.println("Sleeping...");
    }
}

// Dog is a specific type of Animal
class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Woof! Woof!");
    }
}

// Cat is another specific type of Animal
class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Meow!");
    }
}

public class Main {
    public static void main(String[] args) {
        // Using Animal reference to store different animal objects
        Animal a1 = new Dog();
        Animal a2 = new Cat();

        // Each animal has its own sound
        a1.sound(); // Output: Woof! Woof!
        a2.sound(); // Output: Meow!

        // Both share the same sleep behavior
        a1.sleep(); // Output: Sleeping...
    }
}

Explanation: The abstract class Animal defines sound() without implementation, forcing every subclass to provide its own version. The sleep() method is implemented once in Animal and reused by all animals, avoiding duplication.

Expected Mistake: If you accidentally change the method signature in a subclass (e.g., void sound(String type)), you won't be overriding the parent method — you'll be overloading it instead, breaking the abstraction contract.

Q3. Use abstraction to define payment methods like CreditCard and UPI.

// Abstract class representing a payment system
abstract class Payment {
    // Abstract method for paying a certain amount
    abstract void pay(double amount);

    // Concrete method for printing a payment receipt
    void receipt() {
        System.out.println("Payment completed. Receipt generated.");
    }
}

// Payment by credit card
class CreditCard extends Payment {
    @Override
    void pay(double amount) {
        System.out.println("Paid $" + amount + " using Credit Card.");
    }
}

// Payment by UPI
class UPI extends Payment {
    @Override
    void pay(double amount) {
        System.out.println("Paid $" + amount + " using UPI.");
    }
}

public class Main {
    public static void main(String[] args) {
        // Two payment types under the same Payment reference
        Payment p1 = new CreditCard();
        Payment p2 = new UPI();

        p1.pay(100); // Output: Paid $100.0 using Credit Card.
        p1.receipt(); // Output: Payment completed. Receipt generated.

        p2.pay(50); // Output: Paid $50.0 using UPI.
        p2.receipt(); // Output: Payment completed. Receipt generated.
    }
}

Explanation: The Payment abstract class defines a standard payment interface through the pay() method. Each subclass implements its own payment logic. The receipt() method is shared and consistent for all payment types.

Expected Mistake: Writing abstract void pay(double amount) { ... } will cause a compile-time error — abstract methods cannot have a body.

Q4. Demonstrate abstraction with Vehicle types: Car and Bike.

// Abstract class representing a vehicle
abstract class Vehicle {
    // Abstract method: all vehicles must define how they start
    abstract void start();

    // Concrete method: default fuel type message
    void fuelType() {
        System.out.println("Fuel type information not available.");
    }
}

// Car class defines its own start method
class Car extends Vehicle {
    @Override
    void start() {
        System.out.println("Car starts with a key.");
    }
}

// Bike class defines its own start method
class Bike extends Vehicle {
    @Override
    void start() {
        System.out.println("Bike starts with a self-start button.");
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle v1 = new Car();
        Vehicle v2 = new Bike();

        v1.start();      // Output: Car starts with a key.
        v2.start();      // Output: Bike starts with a self-start button.
        v1.fuelType();   // Output: Fuel type information not available.
    }
}

Explanation: Abstract class Vehicle forces subclasses to implement start(). The fuelType() method is optional to override — if a subclass doesn't override it, the default implementation will run.

Expected Mistake: If you forget to implement start() in a concrete subclass, you'll get a compile-time error unless the subclass is also declared abstract.

Q5. Abstract class for Employee with different roles implementing calculateSalary().

// Abstract class representing an employee
abstract class Employee {
    String name; // Common property for all employees

    // Constructor to initialize name
    Employee(String name) {
        this.name = name;
    }

    // Abstract method to calculate salary
    abstract double calculateSalary();

    // Concrete method to display employee details
    void showDetails() {
        System.out.println("Employee: " + name);
    }
}

// Manager is a specific type of employee
class Manager extends Employee {
    double baseSalary;

    Manager(String name, double baseSalary) {
        super(name); // Call Employee constructor
        this.baseSalary = baseSalary;
    }

    @Override
    double calculateSalary() {
        // Managers get a fixed bonus
        return baseSalary + 5000;
    }
}

// Developer is another type of employee
class Developer extends Employee {
    double baseSalary;

    Developer(String name, double baseSalary) {
        super(name); // Call Employee constructor
        this.baseSalary = baseSalary;
    }

    @Override
    double calculateSalary() {
        // Developers get a smaller bonus
        return baseSalary + 2000;
    }
}

public class Main {
    public static void main(String[] args) {
        Employee e1 = new Manager("Aelify", 30000);
        Employee e2 = new Developer("Bob", 25000);

        e1.showDetails(); // Output: Employee: Aelify
        System.out.println("Salary: " + e1.calculateSalary()); // Output: Salary: 35000.0

        e2.showDetails(); // Output: Employee: Bob
        System.out.println("Salary: " + e2.calculateSalary()); // Output: Salary: 27000.0
    }
}

Explanation: The abstract class Employee defines the core structure: every employee has a name and must implement calculateSalary(). Each subclass handles the salary calculation differently, demonstrating how abstraction allows for flexible, role-specific logic while keeping a common interface.

Expected Mistake: If you forget to call super(name) in the subclass constructor, the compiler will throw an error because the superclass has no default constructor.

📦 Pillar 2: Encapsulation

Encapsulation means wrapping data and methods together and restricting direct access to the data. Like how your phone hides its internal storage behind an interface.

In Java, encapsulation is done by:

  • Making fields private
  • Providing public getter and setter methods
// Example of Encapsulation in Java
// Save this as Main.java to run

// BankAccount class that encapsulates account details
class BankAccount {
    
    // Private field - cannot be accessed directly from outside the class
    private double balance;

    // Public method to deposit money into the account
    public void deposit(double amount) {
        // Increase the balance by the given amount
        balance += amount;
    }

    // Public method to retrieve the current balance
    public double getBalance() {
        // Return the value of the private balance field
        return balance;
    }
}

// Main class to run the program
public class Main {
    public static void main(String[] args) {
        
        // Create a new BankAccount object
        BankAccount account = new BankAccount();

        // Deposit some money
        account.deposit(500.0); // Adds 500.0 to balance

        // Retrieve and print the balance
        System.out.println("Current Balance: " + account.getBalance()); // Output: Current Balance: 500.0

        // ❌ The following line would cause an error because balance is private:
        // account.balance = 1000; // Not allowed!

        // We can only modify balance via the deposit method
        account.deposit(250.0); // Adds another 250.0

        // Check the updated balance
        System.out.println("Updated Balance: " + account.getBalance()); // Output: Updated Balance: 750.0
    }
}

📦 Pillar 2: Encapsulation — Concept Questions

Q1. Why use private variables and public methods?

// Save as Main.java

// BankAccount demonstrates encapsulation
class BankAccount {
    // Private variable — can't be accessed directly outside this class
    private double balance;

    // Public method to deposit money
    public void deposit(double amount) {
        if (amount > 0) { // Validation inside method
            balance += amount;
        } else {
            System.out.println("Deposit amount must be positive.");
        }
    }

    // Public method to retrieve the balance safely
    public double getBalance() {
        return balance;
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount acc = new BankAccount();
        acc.deposit(1000);
        System.out.println("Balance: " + acc.getBalance()); // Output: Balance: 1000.0
    }
}

Explanation: Encapsulation hides the internal state (balance) from direct modification. Access is controlled through methods that can validate input and enforce rules.

Expected Mistake: Making balance public allows anyone to set it to any value without checks, breaking data integrity.

Q2. Can we make setters optional in encapsulation?

// Immutable Person class
class Person {
    private final String name; // Once set, cannot be changed

    // Constructor sets the value
    public Person(String name) {
        this.name = name;
    }

    // Getter only — no setter
    public String getName() {
        return name;
    }
}

public class Main {
    public static void main(String[] args) {
        Person p = new Person("Aelify");
        System.out.println("Name: " + p.getName()); // Output: Name: Aelify
        // p.name = "Bob"; // ❌ Not allowed, 'name' is private and final
    }
}

Explanation: Encapsulation doesn't require both getters and setters — sometimes you want to expose data as read-only (immutable objects).

Expected Mistake: Assuming every private field must have a setter — this can unintentionally allow unwanted modifications.

Q3. Encapsulation with validation logic.

// Employee with controlled salary updates
class Employee {
    private double salary;

    public void setSalary(double salary) {
        if (salary >= 30000) { // Minimum salary rule
            this.salary = salary;
        } else {
            System.out.println("Salary must be at least 30000.");
        }
    }

    public double getSalary() {
        return salary;
    }
}

public class Main {
    public static void main(String[] args) {
        Employee emp = new Employee();
        emp.setSalary(25000); // Invalid
        emp.setSalary(50000); // Valid
        System.out.println("Salary: " + emp.getSalary());
    }
}

Explanation: Encapsulation lets you embed rules (validation) inside setters, ensuring that only valid data enters the system.

Expected Mistake: Updating the salary directly if it were public would bypass validation entirely.

Q4. Encapsulation with computed properties (no direct storage).

// Rectangle where area is computed, not stored
class Rectangle {
    private double length;
    private double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    // Computed getter
    public double getArea() {
        return length * width; // No separate 'area' variable
    }
}

public class Main {
    public static void main(String[] args) {
        Rectangle rect = new Rectangle(5, 4);
        System.out.println("Area: " + rect.getArea());
    }
}

Explanation: Encapsulation can hide internal calculation logic, exposing only the results to the outside world.

Expected Mistake: Storing area as a variable and forgetting to update it when dimensions change can lead to incorrect results.

Q5. Encapsulation in real-world analogy (ATM).

// ATM controlling access to bank account
class ATM {
    private double balance = 1000;

    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Withdrawn: " + amount);
        } else {
            System.out.println("Invalid withdrawal.");
        }
    }

    public double getBalance() {
        return balance;
    }
}

public class Main {
    public static void main(String[] args) {
        ATM atm = new ATM();
        atm.withdraw(200);
        System.out.println("Remaining balance: " + atm.getBalance());
    }
}

Explanation: The ATM hides the details of how balance is stored and updated. The user interacts through a controlled interface (deposit, withdraw).

Expected Mistake: Allowing direct access to balance would let users change it without authentication or rules.

📦 Pillar 2: Encapsulation — Difficult Problems, Easy Explanations

Q1: Bank Safe Deposit

We want to protect a bank account's balance so nobody can change it directly — only through safe deposit and withdrawal methods.

// Class with private balance (hidden from outside)
class BankAccount {
    private double balance; // Nobody outside can touch this

    // Method to deposit money
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println(amount + " deposited. New balance: " + balance);
        } else {
            System.out.println("Deposit must be positive.");
        }
    }

    // Method to withdraw money
    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println(amount + " withdrawn. Remaining balance: " + balance);
        } else {
            System.out.println("Invalid withdrawal.");
        }
    }

    // Method to check balance
    public double getBalance() {
        return balance;
    }
}

public class BankTest {
    public static void main(String[] args) {
        BankAccount acc = new BankAccount();
        acc.deposit(1000);
        acc.withdraw(200);
        System.out.println("Final balance: " + acc.getBalance());
    }
}

Easy Explanation: It's like a piggy bank with a lock — you can only put money in or take it out through the slot (methods), not by breaking it open.

Expected Mistake: Making balance public so anyone can change it to any number without following the rules.

Q2: School Report Card

We want to protect student marks so they can only be updated by a teacher.

class Student {
    private int marks; // Hidden from everyone

    public void setMarks(int marks) {
        if (marks >= 0 && marks <= 100) {
            this.marks = marks;
        } else {
            System.out.println("Invalid marks.");
        }
    }

    public int getMarks() {
        return marks;
    }
}

public class SchoolTest {
    public static void main(String[] args) {
        Student s = new Student();
        s.setMarks(85);
        System.out.println("Student marks: " + s.getMarks());
    }
}

Easy Explanation: Imagine your report card is kept in the teacher's drawer — only they can update it after checking your exam paper.

Expected Mistake: Making marks public so students can just set their marks to 100 without exams.

Q3: Video Game Player Health

We want to keep a player's health safe — no one can set it to a random number outside the allowed range.

class Player {
    private int health = 100;

    public void takeDamage(int damage) {
        if (damage > 0) {
            health -= damage;
            if (health < 0) health = 0;
        }
    }

    public void heal(int amount) {
        if (amount > 0) {
            health += amount;
            if (health > 100) health = 100;
        }
    }

    public int getHealth() {
        return health;
    }
}

public class GameTest {
    public static void main(String[] args) {
        Player p = new Player();
        p.takeDamage(30);
        System.out.println("Health after damage: " + p.getHealth());
        p.heal(50);
        System.out.println("Health after healing: " + p.getHealth());
    }
}

Easy Explanation: Like in a game, your health can only go between 0 and 100 — the rules stop it from being broken or infinite.

Expected Mistake: Letting anyone directly set health to 999 or -10.

Q4: Library Book Stock

We want to manage the number of books in a library without letting random people mess with the count.

class Library {
    private int totalBooks;

    public void addBooks(int count) {
        if (count > 0) {
            totalBooks += count;
        }
    }

    public void borrowBook() {
        if (totalBooks > 0) {
            totalBooks--;
        } else {
            System.out.println("No books available.");
        }
    }

    public int getTotalBooks() {
        return totalBooks;
    }
}

public class LibraryTest {
    public static void main(String[] args) {
        Library lib = new Library();
        lib.addBooks(5);
        lib.borrowBook();
        System.out.println("Books left: " + lib.getTotalBooks());
    }
}

Easy Explanation: It's like a librarian keeping track of all books — you can borrow or return, but you can't just change the number in their notebook.

Expected Mistake: Making totalBooks public so people set it to any number they want.

Q5: Smartphone Battery

We want to keep a phone's battery percentage between 0% and 100%.

class Smartphone {
    private int battery = 100;

    public void useBattery(int amount) {
        if (amount > 0) {
            battery -= amount;
            if (battery < 0) battery = 0;
        }
    }

    public void chargeBattery(int amount) {
        if (amount > 0) {
            battery += amount;
            if (battery > 100) battery = 100;
        }
    }

    public int getBattery() {
        return battery;
    }
}

public class PhoneTest {
    public static void main(String[] args) {
        Smartphone phone = new Smartphone();
        phone.useBattery(40);
        System.out.println("Battery after use: " + phone.getBattery());
        phone.chargeBattery(70);
        System.out.println("Battery after charging: " + phone.getBattery());
    }
}

Easy Explanation: Your phone can't have 150% battery — the rules keep it real.

Expected Mistake: Allowing anyone to set battery to any number without checks.

🌳 Pillar 3: Inheritance — Difficult Problems, Easy Explanations

Q1: Animal Family Sounds

We want different animals to make different sounds, but all of them still have the same ability to eat.

// Parent class (base class)
class Animal {
    void eat() {
        System.out.println("This animal is eating.");
    }
}

// Child class 1
class Dog extends Animal {
    void bark() {
        System.out.println("Woof! Woof!");
    }
}

// Child class 2
class Cat extends Animal {
    void meow() {
        System.out.println("Meow! Meow!");
    }
}

public class AnimalTest {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.eat();  // inherited from Animal
        d.bark(); // specific to Dog

        Cat c = new Cat();
        c.eat();  // inherited from Animal
        c.meow(); // specific to Cat
    }
}

Easy Explanation: If "Animal" is a parent, then "Dog" and "Cat" are its kids. Both kids know how to eat (because their parent taught them), but they speak differently (bark or meow).

Expected Mistake: Writing the eat method separately for Dog and Cat instead of reusing it from Animal.

Q2: Vehicle Types

We want all vehicles to be able to start, but each type can have its own special features.

// Parent class
class Vehicle {
    void start() {
        System.out.println("Vehicle started.");
    }
}

// Child class
class Car extends Vehicle {
    void playMusic() {
        System.out.println("Playing music.");
    }
}

// Another child class
class Bike extends Vehicle {
    void kickStart() {
        System.out.println("Bike kick-started.");
    }
}

public class VehicleTest {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();     // inherited
        car.playMusic(); // own feature

        Bike bike = new Bike();
        bike.start();     // inherited
        bike.kickStart(); // own feature
    }
}

Easy Explanation: The "Vehicle" is like the grandparent who teaches all grandchildren how to start moving. But each grandchild has its own extra talent — one plays music, another can kick-start.

Expected Mistake: Copy-pasting the start method into Car and Bike instead of inheriting it.

Q3: School Staff Hierarchy

We want teachers and janitors to share common details, but also have their own tasks.

// Parent class
class Staff {
    void workHours() {
        System.out.println("Works 8 hours a day.");
    }
}

// Child class
class Teacher extends Staff {
    void teach() {
        System.out.println("Teaches students.");
    }
}

// Another child class
class Janitor extends Staff {
    void clean() {
        System.out.println("Cleans the school.");
    }
}

public class SchoolTest {
    public static void main(String[] args) {
        Teacher t = new Teacher();
        t.workHours(); // inherited
        t.teach();     // own method

        Janitor j = new Janitor();
        j.workHours(); // inherited
        j.clean();     // own method
    }
}

Easy Explanation: Every school staff works the same hours, but what they do in those hours is different.

Expected Mistake: Making separate workHours() methods in both Teacher and Janitor instead of putting it in Staff.

Q4: Game Characters

We want all characters to move, but some can fly and others can swim.

// Parent class
class GameCharacter {
    void move() {
        System.out.println("Character moves forward.");
    }
}

// Child class
class Bird extends GameCharacter {
    void fly() {
        System.out.println("Bird flies in the sky.");
    }
}

// Another child class
class Fish extends GameCharacter {
    void swim() {
        System.out.println("Fish swims in the water.");
    }
}

public class GameTest {
    public static void main(String[] args) {
        Bird b = new Bird();
        b.move(); // inherited
        b.fly();  // own feature

        Fish f = new Fish();
        f.move(); // inherited
        f.swim(); // own feature
    }
}

Easy Explanation: All characters can move, but some have special moves — flying or swimming.

Expected Mistake: Writing the move method separately for Bird and Fish instead of putting it in GameCharacter.

Q5: Electronic Devices

We want all devices to turn on, but each type has its own feature.

// Parent class
class ElectronicDevice {
    void turnOn() {
        System.out.println("Device is now ON.");
    }
}

// Child class
class Laptop extends ElectronicDevice {
    void code() {
        System.out.println("Coding on laptop.");
    }
}

// Another child class
class TV extends ElectronicDevice {
    void watch() {
        System.out.println("Watching TV shows.");
    }
}

public class DeviceTest {
    public static void main(String[] args) {
        Laptop laptop = new Laptop();
        laptop.turnOn(); // inherited
        laptop.code();   // own feature

        TV tv = new TV();
        tv.turnOn(); // inherited
        tv.watch();  // own feature
    }
}

Easy Explanation: All devices can turn on, but what they do after that depends on the type — laptops code, TVs show movies.

Expected Mistake: Rewriting turnOn() in Laptop and TV instead of using the parent's method.

📦 Pillar 2: Encapsulation — DSA-Based Questions

Q1. Encapsulated Linked List

// A Node class for Linked List (kept private inside LinkedList class)
class LinkedList {
    // Private inner class - hides internal structure from the outside world
    private class Node {
        int data;
        Node next;
        Node(int data) { this.data = data; }
    }

    private Node head; // Private: can't be directly accessed from outside

    // Public method to insert at the end
    public void insert(int value) {
        Node newNode = new Node(value);
        if (head == null) {
            head = newNode;
            return;
        }
        Node temp = head;
        while (temp.next != null) {
            temp = temp.next;
        }
        temp.next = newNode;
    }

    // Public method to print list
    public void display() {
        Node temp = head;
        while (temp != null) {
            System.out.print(temp.data + " ");
            temp = temp.next;
        }
        System.out.println();
    }
}

public class Main {
    public static void main(String[] args) {
        LinkedList list = new LinkedList();
        list.insert(10);
        list.insert(20);
        list.insert(30);
        list.display(); // Output: 10 20 30
    }
}

Explanation: The Node class and head pointer are private. External code cannot modify them directly, ensuring data safety.

Expected Mistake: Making head public and modifying it directly from outside will break the list's integrity.

Q2. Encapsulated Queue (Array Implementation)

class Queue {
    private int[] arr;   // Hidden array
    private int front;   
    private int rear;    
    private int size;    

    public Queue(int capacity) {
        arr = new int[capacity];
        front = 0;
        rear = -1;
        size = 0;
    }

    public void enqueue(int value) {
        if (size == arr.length) {
            System.out.println("Queue is full!");
            return;
        }
        rear = (rear + 1) % arr.length;
        arr[rear] = value;
        size++;
    }

    public int dequeue() {
        if (isEmpty()) {
            System.out.println("Queue is empty!");
            return -1;
        }
        int value = arr[front];
        front = (front + 1) % arr.length;
        size--;
        return value;
    }

    public boolean isEmpty() {
        return size == 0;
    }
}

public class Main {
    public static void main(String[] args) {
        Queue q = new Queue(3);
        q.enqueue(1);
        q.enqueue(2);
        q.enqueue(3);
        System.out.println(q.dequeue()); // Output: 1
    }
}

Explanation: Internal array arr is private, so elements can only be added/removed through public methods, enforcing safe operations.

Expected Mistake: Trying to access arr[0] directly from outside — not allowed due to encapsulation.

Q3. Encapsulated Binary Search Tree

class BST {
    // Private Node class hides structure
    private class Node {
        int data;
        Node left, right;
        Node(int data) { this.data = data; }
    }

    private Node root;

    public void insert(int value) {
        root = insertRec(root, value);
    }

    private Node insertRec(Node root, int value) {
        if (root == null) return new Node(value);
        if (value < root.data) root.left = insertRec(root.left, value);
        else if (value > root.data) root.right = insertRec(root.right, value);
        return root;
    }

    public void inorder() {
        inorderRec(root);
        System.out.println();
    }

    private void inorderRec(Node root) {
        if (root != null) {
            inorderRec(root.left);
            System.out.print(root.data + " ");
            inorderRec(root.right);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        BST tree = new BST();
        tree.insert(50);
        tree.insert(30);
        tree.insert(70);
        tree.inorder(); // Output: 30 50 70
    }
}

Explanation: Node structure is private and only accessible inside BST class, preventing direct manipulation of tree nodes from outside.

Expected Mistake: Trying to assign tree.root = null from outside — not possible because root is private.

Q4. Encapsulated Min Heap

import java.util.*;

class MinHeap {
    private List<Integer> heap = new ArrayList<>();

    public void insert(int value) {
        heap.add(value);
        heapifyUp(heap.size() - 1);
    }

    public int extractMin() {
        if (heap.isEmpty()) return -1;
        int min = heap.get(0);
        heap.set(0, heap.remove(heap.size() - 1));
        heapifyDown(0);
        return min;
    }

    private void heapifyUp(int index) {
        while (index > 0 && heap.get(index) < heap.get((index - 1) / 2)) {
            Collections.swap(heap, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    private void heapifyDown(int index) {
        int smallest = index;
        int left = 2 * index + 1;
        int right = 2 * index + 2;

        if (left < heap.size() && heap.get(left) < heap.get(smallest)) smallest = left;
        if (right < heap.size() && heap.get(right) < heap.get(smallest)) smallest = right;

        if (smallest != index) {
            Collections.swap(heap, index, smallest);
            heapifyDown(smallest);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MinHeap heap = new MinHeap();
        heap.insert(5);
        heap.insert(3);
        heap.insert(8);
        System.out.println(heap.extractMin()); // Output: 3
    }
}

Explanation: The heap list and heapify logic are hidden; only insert() and extractMin() are exposed to users.

Expected Mistake: Modifying heap list directly from outside — not possible because it's private.

Q5. Encapsulated Graph (Adjacency Matrix)

class Graph {
    private int[][] adjMatrix; // Hidden data
    private int vertices;

    public Graph(int vertices) {
        this.vertices = vertices;
        adjMatrix = new int[vertices][vertices];
    }

    public void addEdge(int u, int v) {
        adjMatrix[u][v] = 1;
        adjMatrix[v][u] = 1; // For undirected graph
    }

    public void printGraph() {
        for (int i = 0; i < vertices; i++) {
            for (int j = 0; j < vertices; j++) {
                System.out.print(adjMatrix[i][j] + " ");
            }
            System.out.println();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Graph g = new Graph(4);
        g.addEdge(0, 1);
        g.addEdge(1, 2);
        g.printGraph();
    }
}

Explanation: Adjacency matrix is private so nobody can modify it directly; operations are done through public methods.

Expected Mistake: Accessing adjMatrix[0][1] from outside — breaks encapsulation and is not allowed.

🔀 Pillar 4: Polymorphism — Difficult Problems, Easy Explanations

Q1: Animal Sounds (Method Overriding)

We want different animals to make their own sounds, but all of them share the same method name makeSound().

// Parent class
class Animal {
    void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

// Child class
class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Woof! Woof!");
    }
}

// Another child class
class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("Meow! Meow!");
    }
}

public class AnimalTest {
    public static void main(String[] args) {
        Animal myAnimal = new Dog(); // Reference type Animal, object type Dog
        myAnimal.makeSound(); // Calls Dog's version

        myAnimal = new Cat(); // Now object type is Cat
        myAnimal.makeSound(); // Calls Cat's version
    }
}

Easy Explanation: Imagine you say "make a sound" to different pets. Even though you used the same words, each pet answers in its own style.

Expected Mistake: Forgetting @Override and accidentally making a new method instead of replacing the old one.

Q2: Shape Areas (Method Overriding)

We want different shapes to calculate their own areas, but all of them use the method name area().

// Parent class
class Shape {
    double area() {
        return 0;
    }
}

// Child class
class Circle extends Shape {
    double radius;
    Circle(double radius) {
        this.radius = radius;
    }
    @Override
    double area() {
        return Math.PI * radius * radius;
    }
}

// Another child class
class Rectangle extends Shape {
    double length, width;
    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    @Override
    double area() {
        return length * width;
    }
}

public class ShapeTest {
    public static void main(String[] args) {
        Shape shape = new Circle(5);
        System.out.println("Circle area: " + shape.area());

        shape = new Rectangle(4, 6);
        System.out.println("Rectangle area: " + shape.area());
    }
}

Easy Explanation: It's like telling different kids "do your homework" — one does math homework, another writes an essay. Same instruction, different result.

Expected Mistake: Forgetting to use the Math.PI constant for the circle and getting a wrong answer.

Q3: Payment Systems (Method Overriding)

We want to process payments in different ways, but all of them use the same method pay().

// Parent class
class Payment {
    void pay() {
        System.out.println("Generic payment process");
    }
}

// Child class
class CreditCardPayment extends Payment {
    @Override
    void pay() {
        System.out.println("Processing credit card payment...");
    }
}

// Another child class
class PaypalPayment extends Payment {
    @Override
    void pay() {
        System.out.println("Processing PayPal payment...");
    }
}

public class PaymentTest {
    public static void main(String[] args) {
        Payment payment = new CreditCardPayment();
        payment.pay(); // Credit card version

        payment = new PaypalPayment();
        payment.pay(); // PayPal version
    }
}

Easy Explanation: It's like giving money to a shop — one customer uses a card, another uses PayPal. The shop still calls it "payment".

Expected Mistake: Forgetting to make the parent method pay() and ending up with two unrelated methods.

Q4: Method Overloading — Adding Numbers

We want to add numbers, but sometimes they are integers and sometimes they are doubles.

// Utility class
class MathUtils {
    int add(int a, int b) {
        return a + b;
    }
    double add(double a, double b) {
        return a + b;
    }
}

public class OverloadTest {
    public static void main(String[] args) {
        MathUtils m = new MathUtils();
        System.out.println("Sum of ints: " + m.add(5, 10));
        System.out.println("Sum of doubles: " + m.add(5.5, 10.3));
    }
}

Easy Explanation: It's like asking a friend to "carry boxes" — sometimes it's two small boxes, sometimes it's two big boxes. They handle it either way.

Expected Mistake: Forgetting that both methods must have different parameter types or counts.

Q5: Transportation Choices (Method Overriding)

We want different transport methods to move, but all use move().

// Parent class
class Transport {
    void move() {
        System.out.println("Generic transport movement");
    }
}

// Child class
class Car extends Transport {
    @Override
    void move() {
        System.out.println("Car drives on roads");
    }
}

// Another child class
class Plane extends Transport {
    @Override
    void move() {
        System.out.println("Plane flies in the sky");
    }
}

public class TransportTest {
    public static void main(String[] args) {
        Transport t = new Car();
        t.move(); // Car's move

        t = new Plane();
        t.move(); // Plane's move
    }
}

Easy Explanation: If you say "Go!" to different vehicles, a car will drive and a plane will fly — same command, different results.

Expected Mistake: Not using @Override and accidentally making a separate method that doesn't actually override the parent's version.

📦 Pillar 2: Encapsulation — DSA-Based Questions

Q1. Encapsulated Linked List

// A Node class for Linked List (kept private inside LinkedList class)
class LinkedList {
    // Private inner class - hides internal structure from the outside world
    private class Node {
        int data;
        Node next;
        Node(int data) { this.data = data; }
    }

    private Node head; // Private: can't be directly accessed from outside

    // Public method to insert at the end
    public void insert(int value) {
        Node newNode = new Node(value);
        if (head == null) {
            head = newNode;
            return;
        }
        Node temp = head;
        while (temp.next != null) {
            temp = temp.next;
        }
        temp.next = newNode;
    }

    // Public method to print list
    public void display() {
        Node temp = head;
        while (temp != null) {
            System.out.print(temp.data + " ");
            temp = temp.next;
        }
        System.out.println();
    }
}

public class Main {
    public static void main(String[] args) {
        LinkedList list = new LinkedList();
        list.insert(10);
        list.insert(20);
        list.insert(30);
        list.display(); // Output: 10 20 30
    }
}

Explanation: The Node class and head pointer are private. External code cannot modify them directly, ensuring data safety.

Expected Mistake: Making head public and modifying it directly from outside will break the list's integrity.

Q2. Encapsulated Queue (Array Implementation)

class Queue {
    private int[] arr;   // Hidden array
    private int front;   
    private int rear;    
    private int size;    

    public Queue(int capacity) {
        arr = new int[capacity];
        front = 0;
        rear = -1;
        size = 0;
    }

    public void enqueue(int value) {
        if (size == arr.length) {
            System.out.println("Queue is full!");
            return;
        }
        rear = (rear + 1) % arr.length;
        arr[rear] = value;
        size++;
    }

    public int dequeue() {
        if (isEmpty()) {
            System.out.println("Queue is empty!");
            return -1;
        }
        int value = arr[front];
        front = (front + 1) % arr.length;
        size--;
        return value;
    }

    public boolean isEmpty() {
        return size == 0;
    }
}

public class Main {
    public static void main(String[] args) {
        Queue q = new Queue(3);
        q.enqueue(1);
        q.enqueue(2);
        q.enqueue(3);
        System.out.println(q.dequeue()); // Output: 1
    }
}

Explanation: Internal array arr is private, so elements can only be added/removed through public methods, enforcing safe operations.

Expected Mistake: Trying to access arr[0] directly from outside — not allowed due to encapsulation.

Q3. Encapsulated Binary Search Tree

class BST {
    // Private Node class hides structure
    private class Node {
        int data;
        Node left, right;
        Node(int data) { this.data = data; }
    }

    private Node root;

    public void insert(int value) {
        root = insertRec(root, value);
    }

    private Node insertRec(Node root, int value) {
        if (root == null) return new Node(value);
        if (value < root.data) root.left = insertRec(root.left, value);
        else if (value > root.data) root.right = insertRec(root.right, value);
        return root;
    }

    public void inorder() {
        inorderRec(root);
        System.out.println();
    }

    private void inorderRec(Node root) {
        if (root != null) {
            inorderRec(root.left);
            System.out.print(root.data + " ");
            inorderRec(root.right);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        BST tree = new BST();
        tree.insert(50);
        tree.insert(30);
        tree.insert(70);
        tree.inorder(); // Output: 30 50 70
    }
}

Explanation: Node structure is private and only accessible inside BST class, preventing direct manipulation of tree nodes from outside.

Expected Mistake: Trying to assign tree.root = null from outside — not possible because root is private.

Q4. Encapsulated Min Heap

import java.util.*;

class MinHeap {
    private List<Integer> heap = new ArrayList<>();

    public void insert(int value) {
        heap.add(value);
        heapifyUp(heap.size() - 1);
    }

    public int extractMin() {
        if (heap.isEmpty()) return -1;
        int min = heap.get(0);
        heap.set(0, heap.remove(heap.size() - 1));
        heapifyDown(0);
        return min;
    }

    private void heapifyUp(int index) {
        while (index > 0 && heap.get(index) < heap.get((index - 1) / 2)) {
            Collections.swap(heap, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    private void heapifyDown(int index) {
        int smallest = index;
        int left = 2 * index + 1;
        int right = 2 * index + 2;

        if (left < heap.size() && heap.get(left) < heap.get(smallest)) smallest = left;
        if (right < heap.size() && heap.get(right) < heap.get(smallest)) smallest = right;

        if (smallest != index) {
            Collections.swap(heap, index, smallest);
            heapifyDown(smallest);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MinHeap heap = new MinHeap();
        heap.insert(5);
        heap.insert(3);
        heap.insert(8);
        System.out.println(heap.extractMin()); // Output: 3
    }
}

Explanation: The heap list and heapify logic are hidden; only insert() and extractMin() are exposed to users.

Expected Mistake: Modifying heap list directly from outside — not possible because it's private.

Q5. Encapsulated Graph (Adjacency Matrix)

class Graph {
    private int[][] adjMatrix; // Hidden data
    private int vertices;

    public Graph(int vertices) {
        this.vertices = vertices;
        adjMatrix = new int[vertices][vertices];
    }

    public void addEdge(int u, int v) {
        adjMatrix[u][v] = 1;
        adjMatrix[v][u] = 1; // For undirected graph
    }

    public void printGraph() {
        for (int i = 0; i < vertices; i++) {
            for (int j = 0; j < vertices; j++) {
                System.out.print(adjMatrix[i][j] + " ");
            }
            System.out.println();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Graph g = new Graph(4);
        g.addEdge(0, 1);
        g.addEdge(1, 2);
        g.printGraph();
    }
}

Explanation: Adjacency matrix is private so nobody can modify it directly; operations are done through public methods.

Expected Mistake: Accessing adjMatrix[0][1] from outside — breaks encapsulation and is not allowed.

📦 Pillar 2: Encapsulation — 20 Questions with Solutions

These questions will strengthen your understanding of Encapsulation in Java. Encapsulation is about wrapping data (fields) and methods together while controlling access via access modifiers.

  • Q1: What is the main goal of encapsulation?

    Explanation: To hide the internal state of an object and control access through methods.

    Expected Mistake: Thinking encapsulation only means “making fields private” — that's just part of it.

  • Q2: Which access modifier is most commonly used for encapsulation of class fields?

    Explanation: private is most commonly used to restrict direct access.

    Expected Mistake: Using protected or default thinking it still hides from all external classes.

  • Q3: How do you allow controlled read access to a private field?

    Explanation: By creating a public getter method that returns the field value.

    Expected Mistake: Making the field public instead of using a getter.

  • Q4: How do you allow controlled modification of a private field?

    Explanation: By creating a public setter method that validates and updates the value.

    Expected Mistake: Giving public access without validation.

  • Q5: What's the benefit of using getters/setters instead of direct public fields?

    Explanation: They allow validation, logging, or transformation before reading/writing data.

    Expected Mistake: Thinking getters/setters are useless extra code.

  • Q6: Can you have a setter without a getter?

    Explanation: Yes, sometimes you allow writing but not reading (write-only property).

    Expected Mistake: Assuming both must always exist together.

  • Q7: What's the relationship between encapsulation and data hiding?

    Explanation: Data hiding is a concept achieved through encapsulation using access modifiers.

    Expected Mistake: Treating them as completely separate ideas.

  • Q8: Which access modifier gives access only within the same package?

    Explanation: The default (no modifier) gives package-private access.

    Expected Mistake: Confusing default with protected.

  • Q9: How does encapsulation improve maintainability?

    Explanation: Internal changes don't affect external code as long as the interface (methods) stays the same.

    Expected Mistake: Thinking any change inside a class will break outside code.

  • Q10: In Java, can you have private methods?

    Explanation: Yes, they are part of encapsulation — hiding helper methods from outside.

    Expected Mistake: Thinking private applies only to variables.

  • Q11: Can encapsulation prevent all unauthorized access to data?

    Explanation: It reduces risk but doesn't guarantee security against reflection or malicious access.

    Expected Mistake: Believing encapsulation is the same as security.

  • Q12: How do you make a class completely immutable?

    Explanation: Make fields private & final, no setters, initialize via constructor, and avoid exposing mutable objects directly.

    Expected Mistake: Forgetting to copy mutable objects in getters/setters.

  • Q13: Can static fields be encapsulated?

    Explanation: Yes, you can make them private and expose them via static getters/setters.

    Expected Mistake: Thinking encapsulation applies only to instance fields.

  • Q14: Why might you use final with encapsulated fields?

    Explanation: To prevent reassignment after initialization, strengthening immutability.

    Expected Mistake: Assuming final makes the object itself immutable — it only prevents reassignment.

  • Q15: Can encapsulation be broken with reflection?

    Explanation: Yes, Java reflection can override access modifiers at runtime.

    Expected Mistake: Believing private is an absolute barrier.

  • Q16: How does encapsulation support abstraction?

    Explanation: By hiding implementation details and exposing only the necessary API.

    Expected Mistake: Thinking encapsulation and abstraction are unrelated.

  • Q17: Can getters return computed values instead of direct fields?

    Explanation: Yes, getters can calculate values dynamically to control output.

    Expected Mistake: Assuming getters must directly return a stored field.

  • Q18: What's wrong with making all fields public?

    Explanation: It violates encapsulation by allowing uncontrolled access and modification.

    Expected Mistake: Thinking it's harmless for small projects.

  • Q19: Can you use encapsulation in enums?

    Explanation: Yes, enums can have private fields and public methods to control access.

    Expected Mistake: Assuming enums are just constants without behavior.

  • Q20: How does encapsulation improve testing?

    Explanation: Changes inside the class won't affect tests as long as the public API remains the same.

    Expected Mistake: Thinking encapsulation makes testing harder because of private fields.

📦 Pillar 2: Encapsulation — Basic Questions

Q1. Create a BankAccount class with private balance and methods to deposit and check balance.

// Save as Main.java

class BankAccount {
    // Private field: cannot be accessed directly outside the class
    private double balance;

    // Public method: allows controlled deposit
    public void deposit(double amount) {
        if (amount > 0) { // Ensure positive amount
            balance += amount; // Update balance
        } else {
            System.out.println("Invalid deposit amount.");
        }
    }

    // Public method: allows reading balance safely
    public double getBalance() {
        return balance;
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();

        account.deposit(500); // Add money
        System.out.println("Balance: $" + account.getBalance()); // Output: Balance: $500.0
    }
}

Explanation: Encapsulation hides the balance field from outside access. The only way to change it is through deposit(), which validates the input before updating. This prevents accidental or malicious modification.

Expected Mistake: Declaring balance as public allows direct changes like account.balance = -100;, breaking data integrity.

Q2. Implement encapsulation for Student details with getters and setters.

class Student {
    private String name;
    private int age;

    // Setter for name
    public void setName(String name) {
        this.name = name;
    }

    // Getter for name
    public String getName() {
        return name;
    }

    // Setter for age with validation
    public void setAge(int age) {
        if (age > 0) { // Age must be positive
            this.age = age;
        } else {
            System.out.println("Invalid age.");
        }
    }

    // Getter for age
    public int getAge() {
        return age;
    }
}

public class Main {
    public static void main(String[] args) {
        Student s = new Student();

        s.setName("Alice");
        s.setAge(20);

        System.out.println(s.getName() + " is " + s.getAge() + " years old.");
    }
}

Explanation: Fields name and age are private. The only way to update them is through setters, which can validate data before storing it. This ensures no invalid data is saved.

Expected Mistake: Forgetting validation in setters allows incorrect values (e.g., negative ages) to be stored, making your object unreliable.

Q3. Encapsulate a Car class with private speed and methods to control it.

class Car {
    private int speed;

    // Increase speed safely
    public void accelerate(int increment) {
        if (increment > 0) {
            speed += increment;
        } else {
            System.out.println("Invalid speed increment.");
        }
    }

    // Decrease speed safely
    public void brake(int decrement) {
        if (decrement > 0 && speed - decrement >= 0) {
            speed -= decrement;
        } else {
            System.out.println("Invalid brake value.");
        }
    }

    // Getter for current speed
    public int getSpeed() {
        return speed;
    }
}

public class Main {
    public static void main(String[] args) {
        Car c = new Car();
        c.accelerate(50);
        System.out.println("Speed: " + c.getSpeed()); // Output: 50
        c.brake(20);
        System.out.println("Speed: " + c.getSpeed()); // Output: 30
    }
}

Explanation: The speed variable is private and can only be modified through controlled methods accelerate() and brake(), which prevent unsafe speed changes.

Expected Mistake: Without validation, braking could reduce speed to a negative value, which is illogical in real-world scenarios.

Q4. Use encapsulation to store and validate Product price.

class Product {
    private double price;

    // Setter with validation
    public void setPrice(double price) {
        if (price >= 0) {
            this.price = price;
        } else {
            System.out.println("Price cannot be negative.");
        }
    }

    // Getter
    public double getPrice() {
        return price;
    }
}

public class Main {
    public static void main(String[] args) {
        Product p = new Product();
        p.setPrice(99.99);
        System.out.println("Price: $" + p.getPrice());
    }
}

Explanation: The price field is hidden from direct access. All changes go through setPrice(), which ensures the price can never be negative, keeping the data logical.

Expected Mistake: If price were public, anyone could set it to an invalid value without any checks.

Q5. Encapsulate login credentials with private fields and authentication method.

class User {
    private String username;
    private String password; // Stored securely in real applications

    // Setter for username
    public void setUsername(String username) {
        this.username = username;
    }

    // Setter for password
    public void setPassword(String password) {
        this.password = password;
    }

    // Authenticate method
    public boolean authenticate(String inputUser, String inputPass) {
        return username.equals(inputUser) && password.equals(inputPass);
    }
}

public class Main {
    public static void main(String[] args) {
        User u = new User();
        u.setUsername("admin");
        u.setPassword("1234");

        if (u.authenticate("admin", "1234")) {
            System.out.println("Login successful!");
        } else {
            System.out.println("Login failed!");
        }
    }
}

Explanation: The username and password fields are private. The only way to check credentials is through authenticate(), which compares stored and provided values, protecting the actual data from direct access.

Expected Mistake: Making password public exposes sensitive information, allowing it to be read or modified without restriction.

🌳 Pillar 3: Inheritance

Inheritance lets one class acquire the properties and methods of another class. It promotes code reuse.

  • extends — For classes
  • implements — For interfaces
// Example of Inheritance in Java
// Save this as Main.java to run

// Parent class (superclass)
class Vehicle {

    // Method that all vehicles have
    void start() {
        System.out.println("Vehicle started.");
    }
}

// Child class (subclass) that inherits from Vehicle
class Car extends Vehicle {

    // Additional method specific to Car
    void playMusic() {
        System.out.println("Playing music.");
    }
}

// Main class to run the program
public class Main {
    public static void main(String[] args) {
        
        // Create an object of Car
        Car myCar = new Car();

        // Call the inherited method from Vehicle
        myCar.start(); // Output: Vehicle started.

        // Call the Car's own method
        myCar.playMusic(); // Output: Playing music.

        // You can also use the superclass reference
        Vehicle myVehicle = new Car(); // Polymorphism

        // This works because Car "is-a" Vehicle
        myVehicle.start(); // Output: Vehicle started.

        // But myVehicle cannot call playMusic() directly because
        // the reference type is Vehicle, which doesn't know about playMusic()
        // myVehicle.playMusic(); // ❌ Compile-time error
    }
}

🌳 Pillar 3: Inheritance — Concept Questions

Q1. What is single inheritance?

// Save as Main.java

// Parent class
class Vehicle {
    void start() {
        System.out.println("Vehicle started.");
    }
}

// Child class
class Car extends Vehicle {
    void honk() {
        System.out.println("Car horn: Beep!");
    }
}

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.start(); // Inherited from Vehicle
        myCar.honk();  // Defined in Car
    }
}

Explanation: Single inheritance means a class inherits from exactly one parent class, reusing and extending its functionality.

Expected Mistake: Thinking Java supports multiple class inheritance — it only supports one parent class but can implement multiple interfaces.

Q2. What is multilevel inheritance?

// Grandparent class
class Animal {
    void eat() {
        System.out.println("This animal eats food.");
    }
}

// Parent class
class Mammal extends Animal {
    void breathe() {
        System.out.println("This mammal breathes air.");
    }
}

// Child class
class Dog extends Mammal {
    void bark() {
        System.out.println("Woof! Woof!");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat();     // From Animal
        dog.breathe(); // From Mammal
        dog.bark();    // From Dog
    }
}

Explanation: Multilevel inheritance means a chain of inheritance where a child inherits from a parent, and that parent inherits from another class.

Expected Mistake: Forgetting that all methods from ancestors are available to the deepest child class.

Q3. Can a child override parent methods?

// Parent class
class Shape {
    void draw() {
        System.out.println("Drawing a generic shape.");
    }
}

// Child class overriding method
class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a circle.");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape = new Shape();
        shape.draw(); // Generic shape

        Shape circle = new Circle();
        circle.draw(); // Circle version
    }
}

Explanation: Overriding lets the child provide its own version of a method to change or specialize behavior.

Expected Mistake: Changing the method signature in the child — this would be overloading, not overriding.

Q4. Using super keyword in inheritance.

// Parent class
class Person {
    String name;

    Person(String name) {
        this.name = name;
    }

    void display() {
        System.out.println("Name: " + name);
    }
}

// Child class
class Employee extends Person {
    double salary;

    Employee(String name, double salary) {
        super(name); // Call parent constructor
        this.salary = salary;
    }

    @Override
    void display() {
        super.display(); // Call parent's display
        System.out.println("Salary: " + salary);
    }
}

public class Main {
    public static void main(String[] args) {
        Employee emp = new Employee("Alice", 50000);
        emp.display();
    }
}

Explanation: The super keyword calls the parent constructor or methods, allowing you to reuse parent functionality in a child.

Expected Mistake: Forgetting to call super() when the parent class has no default constructor.

Q5. Inheritance with polymorphism behavior.

// Parent class
class Animal {
    void sound() {
        System.out.println("Some generic sound");
    }
}

// Child class
class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Meow!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Cat(); // Polymorphism
        myAnimal.sound(); // Calls Cat's method
    }
}

Explanation: Inheritance enables polymorphism — the same reference type can point to different object types, and overridden methods run based on the actual object.

Expected Mistake: Expecting parent class methods to run just because the reference type is the parent — in overriding, the child's method runs instead.

🌳 Pillar 3: Inheritance — DSA-Based Questions

Q1. Create an abstract LinearStructure and implement ArrayStack and ArrayQueue by extending it.

// Save as Main.java
// Demonstrates using inheritance to share common API/fields for linear structures.

abstract class LinearStructure {
    protected int capacity;        // shared property for derived classes
    protected int size = 0;        // current number of elements

    // constructor to initialize capacity
    LinearStructure(int capacity) {
        this.capacity = capacity;
    }

    // common methods every linear DS should provide (abstract - implemented by children)
    abstract void add(int value);    // add element (push/enqueue)
    abstract int remove();           // remove element (pop/dequeue)
    abstract boolean isEmpty();

    // convenience method implemented once for all
    int getSize() {
        return size;
    }
}

// Stack implemented using array; extends LinearStructure
class ArrayStack extends LinearStructure {
    private int[] arr;
    private int top = -1;

    ArrayStack(int capacity) {
        super(capacity);            // initialize capacity in parent
        arr = new int[capacity];
    }

    // push
    @Override
    void add(int value) {
        if (size == capacity) {
            System.out.println("Stack overflow");
            return;
        }
        arr[++top] = value;
        size++;
    }

    // pop
    @Override
    int remove() {
        if (isEmpty()) {
            System.out.println("Stack underflow");
            return -1;
        }
        size--;
        return arr[top--];
    }

    @Override
    boolean isEmpty() {
        return size == 0;
    }
}

// Queue implemented using circular array; extends LinearStructure
class ArrayQueue extends LinearStructure {
    private int[] arr;
    private int front = 0;
    private int rear = -1;

    ArrayQueue(int capacity) {
        super(capacity);
        arr = new int[capacity];
    }

    // enqueue
    @Override
    void add(int value) {
        if (size == capacity) {
            System.out.println("Queue full");
            return;
        }
        rear = (rear + 1) % capacity;
        arr[rear] = value;
        size++;
    }

    // dequeue
    @Override
    int remove() {
        if (isEmpty()) {
            System.out.println("Queue empty");
            return -1;
        }
        int val = arr[front];
        front = (front + 1) % capacity;
        size--;
        return val;
    }

    @Override
    boolean isEmpty() {
        return size == 0;
    }
}

// Demonstration
public class Main {
    public static void main(String[] args) {
        // Use parent type to hold child objects (polymorphism)
        LinearStructure stack = new ArrayStack(3);
        stack.add(1);
        stack.add(2);
        System.out.println("Stack pop: " + stack.remove()); // 2

        LinearStructure queue = new ArrayQueue(3);
        queue.add(10);
        queue.add(20);
        System.out.println("Queue remove: " + queue.remove()); // 10
    }
}

Explanation: We defined a shared abstract API in LinearStructure and provided two concrete implementations. Shared fields/methods live in the parent; behavior specifics are implemented by children.

Expected Mistake: Trying to instantiate new LinearStructure(3) (abstract) or relying on parent-specific internal fields directly from client code—use methods only.

Q2. Implement a base BinaryTree with traversal methods, then extend it to a BinarySearchTree that overrides insertion logic.

// Save as Main.java
// Shows inheritance where a BST extends a generic BinaryTree and customizes behavior.

class BinaryTree {
    // Node is protected so subclasses can access structure
    protected static class Node {
        int val;
        Node left, right;
        Node(int v) { val = v; }
    }

    protected Node root;

    // Generic add (just places nodes breadth-wise or as you choose).
    // Here we keep it simple: append to leftmost null (not balanced).
    void add(int value) {
        if (root == null) {
            root = new Node(value);
            return;
        }
        // simple BFS insertion to first empty spot (not typical BST)
        java.util.Queue q = new java.util.LinkedList<>();
        q.add(root);
        while (!q.isEmpty()) {
            Node n = q.poll();
            if (n.left == null) { n.left = new Node(value); return; }
            else q.add(n.left);
            if (n.right == null) { n.right = new Node(value); return; }
            else q.add(n.right);
        }
    }

    // Inorder traversal (works for any binary tree)
    void inorder() {
        inorderRec(root);
        System.out.println();
    }
    private void inorderRec(Node node) {
        if (node == null) return;
        inorderRec(node.left);
        System.out.print(node.val + " ");
        inorderRec(node.right);
    }
}

// BST extends BinaryTree and overrides add() with BST insertion logic
class BinarySearchTree extends BinaryTree {
    @Override
    void add(int value) {
        root = insertRec(root, value);
    }
    private Node insertRec(Node node, int value) {
        if (node == null) return new Node(value);
        if (value < node.val) node.left = insertRec(node.left, value);
        else if (value > node.val) node.right = insertRec(node.right, value);
        // duplicates ignored for simplicity
        return node;
    }
}

public class Main {
    public static void main(String[] args) {
        // BinaryTree generic insertion
        BinaryTree bt = new BinaryTree();
        bt.add(1); bt.add(2); bt.add(3);
        System.out.print("BinaryTree inorder: "); bt.inorder(); // may not be sorted

        // BST insertion
        BinarySearchTree bst = new BinarySearchTree();
        bst.add(50); bst.add(30); bst.add(70); bst.add(60);
        System.out.print("BST inorder (sorted): "); bst.inorder(); // sorted order
    }
}

Explanation: BinarySearchTree reuses traversal code from BinaryTree but overrides insertion to maintain BST invariant. This demonstrates specialization via inheritance.

Expected Mistake: Assuming the parent add() will produce BST order — you must override with correct BST logic; also exposing root publicly breaks encapsulation.

Q3. Create an abstract Graph class; implement UndirectedGraph and DirectedGraph with different edge behaviors.

// Save as Main.java
// Shows polymorphic graph implementations sharing same API.

import java.util.*;

abstract class Graph {
    protected int vertices;
    Graph(int v) { vertices = v; }
    abstract void addEdge(int u, int v);
    abstract List neighbors(int u);
    abstract void printGraph();
}

// Undirected graph using adjacency list
class UndirectedGraph extends Graph {
    private List[] adj;
    @SuppressWarnings("unchecked")
    UndirectedGraph(int v) {
        super(v);
        adj = new List[v];
        for (int i=0;i();
    }
    @Override
    void addEdge(int u, int v) {
        adj[u].add(v);
        adj[v].add(u);
    }
    @Override
    List neighbors(int u) { return adj[u]; }
    @Override
    void printGraph() {
        for (int i=0;i " + adj[i]);
    }
}

// Directed graph using adjacency list
class DirectedGraph extends Graph {
    private List[] adj;
    @SuppressWarnings("unchecked")
    DirectedGraph(int v) {
        super(v);
        adj = new List[v];
        for (int i=0;i();
    }
    @Override
    void addEdge(int u, int v) {
        adj[u].add(v); // only one direction
    }
    @Override
    List neighbors(int u) { return adj[u]; }
    @Override
    void printGraph() {
        for (int i=0;i " + adj[i]);
    }
}

public class Main {
    public static void main(String[] args) {
        Graph ug = new UndirectedGraph(3);
        ug.addEdge(0,1);
        ug.addEdge(1,2);
        System.out.println("Undirected graph:");
        ug.printGraph();

        Graph dg = new DirectedGraph(3);
        dg.addEdge(0,1);
        dg.addEdge(1,2);
        System.out.println("Directed graph:");
        dg.printGraph();
    }
}

Explanation: The abstract class defines the graph API. Subclasses implement storage/edge semantics (undirected adds two-way edges; directed adds single direction). Client code can use the Graph type without caring about implementation details.

Expected Mistake: Using the undirected addEdge for directed graph or vice-versa — semantics differ and must be implemented correctly in each subclass.

Q4. Abstract SearchAlgorithm (search in arrays) and implement LinearSearch and BinarySearch. Show swapping implementations easily.

// Save as Main.java
// Use inheritance to switch search strategy without changing client code.

abstract class SearchAlgorithm {
    // Returns index of target or -1 if not found
    abstract int search(int[] arr, int target);
}

// Linear search implementation
class LinearSearch extends SearchAlgorithm {
    @Override
    int search(int[] arr, int target) {
        for (int i=0;i

Explanation: Abstracting search algorithms allows swapping strategies easily. Use LinearSearch for unsorted arrays and BinarySearch for sorted arrays (faster: O(log n)).

Expected Mistake: Using BinarySearch on unsorted data — yields incorrect/no results. Always ensure preconditions (sorted input) are met.

Q5. Path-finding base class PathFinder, implement BFSPathFinder (unweighted shortest path) and DijkstraPathFinder (weighted).

// Save as Main.java
// Demonstrates inheritance for algorithms solving similar problems with different internals.

import java.util.*;

// Simple graph representation using adjacency list with weights for Dijkstra
class WeightedGraph {
    int n;
    List[] adj; // adj[u] contains int[]{v, weight}
    @SuppressWarnings("unchecked")
    WeightedGraph(int n) {
        this.n = n;
        adj = new List[n];
        for (int i=0;i();
    }
    void addEdge(int u, int v, int w) {
        adj[u].add(new int[]{v,w});
    }
}

// Base path finder API
abstract class PathFinder {
    abstract List shortestPath(WeightedGraph g, int src, int dest);
}

// BFS-based path finder for unweighted graphs (weights ignored)
class BFSPathFinder extends PathFinder {
    @Override
    List shortestPath(WeightedGraph g, int src, int dest) {
        int n = g.n;
        boolean[] vis = new boolean[n];
        int[] parent = new int[n];
        Arrays.fill(parent, -1);
        Queue q = new LinkedList<>();
        q.add(src); vis[src]=true;
        while (!q.isEmpty()) {
            int u = q.poll();
            if (u==dest) break;
            for (int[] vw : g.adj[u]) {
                int v = vw[0];
                if (!vis[v]) {
                    vis[v]=true;
                    parent[v]=u;
                    q.add(v);
                }
            }
        }
        if (parent[dest]==-1 && src!=dest) return Collections.emptyList();
        // reconstruct path
        List path = new ArrayList<>();
        for (int at = dest; at!=-1; at = parent[at]) path.add(at);
        Collections.reverse(path);
        return path;
    }
}

// Dijkstra for weighted shortest path
class DijkstraPathFinder extends PathFinder {
    @Override
    List shortestPath(WeightedGraph g, int src, int dest) {
        int n = g.n;
        long[] dist = new long[n];
        int[] parent = new int[n];
        Arrays.fill(dist, Long.MAX_VALUE);
        Arrays.fill(parent, -1);
        dist[src] = 0;
        PriorityQueue pq = new PriorityQueue<>(Comparator.comparingLong(a -> a[0]));
        // pq stores {distance, node}
        pq.add(new long[]{0, src});
        while (!pq.isEmpty()) {
            long[] cur = pq.poll();
            long d = cur[0];
            int u = (int) cur[1];
            if (d>dist[u]) continue;
            for (int[] vw : g.adj[u]) {
                int v = vw[0], w = vw[1];
                if (dist[u] + w < dist[v]) {
                    dist[v] = dist[u] + w;
                    parent[v] = u;
                    pq.add(new long[]{dist[v], v});
                }
            }
        }
        if (dist[dest] == Long.MAX_VALUE) return Collections.emptyList();
        List path = new ArrayList<>();
        for (int at = dest; at != -1; at = parent[at]) path.add(at);
        Collections.reverse(path);
        return path;
    }
}

public class Main {
    public static void main(String[] args) {
        // Build a simple weighted graph
        WeightedGraph g = new WeightedGraph(5);
        // add some edges (directed for simplicity)
        g.addEdge(0,1,1);
        g.addEdge(1,2,1);
        g.addEdge(0,3,5);
        g.addEdge(3,2,1);
        g.addEdge(2,4,2);

        // BFS path finder (weights ignored) - good for unweighted graphs
        PathFinder bfs = new BFSPathFinder();
        System.out.println("BFS path (as nodes): " + bfs.shortestPath(g, 0, 4));

        // Dijkstra path finder (weighted)
        PathFinder djk = new DijkstraPathFinder();
        System.out.println("Dijkstra path (as nodes): " + djk.shortestPath(g, 0, 4));
    }
}

Explanation: Both path finders share the same API but implement different logic. Client code uses the abstract PathFinder type and can swap implementations without changes.

Expected Mistake: Using BFS for weighted graphs expecting shortest-by-weight paths — BFS ignores weights and gives shortest-by-edges path, not by weights.

⚙️ Pillar 3: Inheritance — 20 Questions with Solutions

These questions will strengthen your understanding of Inheritance in Java. Inheritance allows a class (child) to acquire properties and behavior from another class (parent).

  • Q1: What is inheritance?

    Explanation: It's the mechanism where one class acquires fields and methods from another class.

    Expected Mistake: Thinking inheritance copies the code — it shares it at runtime.

  • Q2: Which keyword is used to inherit a class in Java?

    Explanation: extends is used for class inheritance.

    Expected Mistake: Using implements instead of extends for classes.

  • Q3: Can a class extend more than one class in Java?

    Explanation: No, Java supports single inheritance for classes.

    Expected Mistake: Assuming multiple inheritance of classes is allowed like in C++.

  • Q4: What's the main advantage of inheritance?

    Explanation: Code reuse — common logic can be placed in a parent class and reused by children.

    Expected Mistake: Thinking it only helps with method overriding.

  • Q5: Which method is used to call a parent class constructor?

    Explanation: super() is used to call a parent constructor.

    Expected Mistake: Trying to call the parent's constructor by its class name directly.

  • Q6: What is method overriding?

    Explanation: When a subclass provides a specific implementation for a method already defined in the parent class.

    Expected Mistake: Confusing overriding with overloading.

  • Q7: Which annotation is used to indicate method overriding?

    Explanation: @Override is used to make the intention clear and catch mistakes.

    Expected Mistake: Forgetting it and silently introducing overloading instead of overriding.

  • Q8: Can private methods be inherited?

    Explanation: No, private methods are not visible to subclasses.

    Expected Mistake: Assuming all methods are inherited automatically.

  • Q9: Can constructors be inherited?

    Explanation: No, constructors are not inherited but can be called via super().

    Expected Mistake: Believing subclasses automatically get the parent's constructors.

  • Q10: How can a subclass access a parent's protected member?

    Explanation: Directly, since protected members are accessible to subclasses even in different packages.

    Expected Mistake: Confusing protected with package-private.

  • Q11: What is hierarchical inheritance?

    Explanation: When multiple classes inherit from the same parent class.

    Expected Mistake: Confusing it with multiple inheritance of classes.

  • Q12: Can final classes be inherited?

    Explanation: No, final classes cannot be extended.

    Expected Mistake: Trying to extend classes like String or Math.

  • Q13: Can you prevent a method from being overridden?

    Explanation: Yes, by declaring it as final.

    Expected Mistake: Thinking private methods prevent overriding — they don't; they just aren't inherited.

  • Q14: Can abstract classes use inheritance?

    Explanation: Yes, abstract classes can extend other classes (abstract or concrete).

    Expected Mistake: Believing abstract classes can't inherit.

  • Q15: How do you call a parent's overridden method?

    Explanation: Use super.methodName() inside the subclass.

    Expected Mistake: Thinking you must remove the overriding to call the parent's method.

  • Q16: What's the difference between interface inheritance and class inheritance?

    Explanation: Interface inheritance is done with implements and allows multiple inheritance of type; class inheritance is with extends and is single.

    Expected Mistake: Thinking both behave identically.

  • Q17: Can static methods be overridden?

    Explanation: No, static methods are hidden, not overridden.

    Expected Mistake: Believing static method overriding works like instance methods.

  • Q18: What's the “is-a” relationship in inheritance?

    Explanation: It means a subclass is a specialized version of its parent — e.g., Dog is-a Animal.

    Expected Mistake: Misusing inheritance where “has-a” (composition) would be better.

  • Q19: Can you inherit from multiple interfaces?

    Explanation: Yes, Java allows implementing multiple interfaces.

    Expected Mistake: Thinking it's not possible because of single inheritance rule.

  • Q20: How does inheritance relate to polymorphism?

    Explanation: Inheritance enables polymorphism by allowing a parent reference to point to a child object.

    Expected Mistake: Thinking polymorphism can exist without any inheritance at all.

🌳 Pillar 3: Inheritance — Basic Questions

Q1. Create a basic inheritance between Vehicle and Car.

// Save as Main.java

// Parent class
class Vehicle {
    void start() {
        System.out.println("Vehicle started.");
    }
}

// Child class inheriting from Vehicle
class Car extends Vehicle {
    void playMusic() {
        System.out.println("Playing music.");
    }
}

public class Main {
    public static void main(String[] args) {
        Car c = new Car();
        c.start(); // Inherited from Vehicle
        c.playMusic(); // Defined in Car
    }
}

Explanation: Car inherits from Vehicle, so it can use both its own method (playMusic()) and the inherited start() method. This promotes code reuse.

Expected Mistake: Trying to access playMusic() from a Vehicle reference directly will fail unless you cast it to Car.

Q2. Demonstrate multi-level inheritance with AnimalMammalDog.

// Base class
class Animal {
    void breathe() {
        System.out.println("Breathing...");
    }
}

// Intermediate class
class Mammal extends Animal {
    void feedMilk() {
        System.out.println("Feeding milk to young.");
    }
}

// Derived class
class Dog extends Mammal {
    void bark() {
        System.out.println("Woof! Woof!");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.breathe(); // From Animal
        d.feedMilk(); // From Mammal
        d.bark(); // From Dog
    }
}

Explanation: In multi-level inheritance, Dog gets methods from Mammal and Animal. This allows building complex hierarchies without rewriting code.

Expected Mistake: Overcomplicating the hierarchy can make maintenance difficult — only use multi-level inheritance when there's a clear "is-a" relationship.

Q3. Show method overriding in inheritance.

// Parent class
class Shape {
    void draw() {
        System.out.println("Drawing a generic shape.");
    }
}

// Child class overrides the method
class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a circle.");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape s1 = new Shape();
        Shape s2 = new Circle(); // Polymorphism

        s1.draw(); // Output: Drawing a generic shape.
        s2.draw(); // Output: Drawing a circle. (Overridden method runs)
    }
}

Explanation: Method overriding lets the subclass change how an inherited method behaves. Here, Circle provides its own implementation of draw().

Expected Mistake: Forgetting @Override might lead to accidental overloading instead of overriding if method signatures differ.

Q4. Inherit and add new functionality to a base class.

// Base class
class Employee {
    void work() {
        System.out.println("Doing assigned work.");
    }
}

// Subclass adds new method
class Manager extends Employee {
    void conductMeeting() {
        System.out.println("Conducting a meeting.");
    }
}

public class Main {
    public static void main(String[] args) {
        Manager m = new Manager();
        m.work(); // Inherited
        m.conductMeeting(); // New method in Manager
    }
}

Explanation: Inheritance allows Manager to use work() from Employee while adding its own specialized conductMeeting() method.

Expected Mistake: Avoid assuming all employees can conduct meetings — that's a Manager-specific role, so it belongs in the subclass, not the parent.

Q5. Use super to call parent class methods and constructors.

// Parent class
class Person {
    String name;

    // Constructor
    Person(String name) {
        this.name = name;
    }

    void introduce() {
        System.out.println("Hi, I'm " + name);
    }
}

// Child class
class Teacher extends Person {
    String subject;

    // Constructor calls parent constructor
    Teacher(String name, String subject) {
        super(name); // Calls Person's constructor
        this.subject = subject;
    }

    @Override
    void introduce() {
        super.introduce(); // Calls parent's method
        System.out.println("I teach " + subject);
    }
}

public class Main {
    public static void main(String[] args) {
        Teacher t = new Teacher("Alice", "Math");
        t.introduce();
    }
}

Explanation: The super keyword lets you reuse parent constructor logic and call parent methods. This avoids repeating code when the base behavior is still relevant.

Expected Mistake: Forgetting to call super() in a subclass constructor when the parent has no default constructor will cause a compile-time error.

🔀 Pillar 4: Polymorphism

Polymorphism means “many forms” — the ability of an object to behave differently based on context.

Types of polymorphism in Java:

  • Compile-time (Method Overloading)
  • Runtime (Method Overriding)
// Example of Polymorphism in Java
// Save this as Main.java to run

// ----------- Method Overloading (Compile-time Polymorphism) -----------
class MathUtils {

    // Method to add two integers
    int add(int a, int b) {
        return a + b;
    }

    // Overloaded method to add two doubles
    double add(double a, double b) {
        return a + b;
    }
}

// ----------- Method Overriding (Runtime Polymorphism) -----------

// Parent class
class Animal {

    // Method that can be overridden by subclasses
    void makeSound() {
        System.out.println("Some sound");
    }
}

// Child class overriding the method
class Cat extends Animal {

    @Override // This tells the compiler we are overriding the parent's method
    void makeSound() {
        System.out.println("Meow!");
    }
}

// Main class to run the program
public class Main {
    public static void main(String[] args) {

        // ----------- Testing Method Overloading -----------
        MathUtils math = new MathUtils();

        // Calls the int version
        System.out.println("Sum of integers: " + math.add(5, 10)); // Output: 15

        // Calls the double version
        System.out.println("Sum of doubles: " + math.add(5.5, 10.3)); // Output: 15.8


        // ----------- Testing Method Overriding -----------
        Animal genericAnimal = new Animal();
        genericAnimal.makeSound(); // Output: Some sound

        Cat myCat = new Cat();
        myCat.makeSound(); // Output: Meow!

        // Runtime polymorphism: reference type is Animal but object is Cat
        Animal anotherCat = new Cat();
        anotherCat.makeSound(); // Output: Meow! (Cat's version is called)
    }
}

🔀 Pillar 4: Polymorphism — Concept Questions

Q1. What is method overloading (compile-time polymorphism)?

// Save as Main.java

// Class with overloaded methods
class MathUtils {
    // Version for integers
    int add(int a, int b) {
        return a + b;
    }

    // Version for doubles
    double add(double a, double b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        MathUtils math = new MathUtils();

        // Calls int version
        System.out.println("Sum (int): " + math.add(5, 10));

        // Calls double version
        System.out.println("Sum (double): " + math.add(3.5, 2.2));
    }
}

Explanation: Overloading means multiple methods with the same name but different parameter lists. The method is chosen at compile-time based on argument types.

Expected Mistake: Assuming return type alone can differentiate methods — it can't; parameters must differ.

Q2. What is method overriding (runtime polymorphism)?

// Parent class
class Animal {
    void sound() {
        System.out.println("Some generic sound");
    }
}

// Child class overrides method
class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Woof! Woof!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Dog(); // Polymorphic reference
        myAnimal.sound(); // Runs Dog's version
    }
}

Explanation: Overriding allows a child class to provide a new implementation for a parent method. At runtime, the JVM calls the version based on the actual object type.

Expected Mistake: Expecting the parent's method to run just because the reference is of the parent type.

Q3. How does polymorphism work with arrays of objects?

// Parent class
class Shape {
    void draw() {
        System.out.println("Drawing a generic shape.");
    }
}

// Child classes
class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a circle.");
    }
}

class Square extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a square.");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape[] shapes = { new Circle(), new Square(), new Shape() };

        for (Shape s : shapes) {
            s.draw(); // Runs the correct version based on the object
        }
    }
}

Explanation: An array of parent references can store different child objects. Polymorphism ensures each object's overridden method runs correctly at runtime.

Expected Mistake: Assuming all elements run the parent's version — overriding ensures the child's method runs instead.

Q4. Polymorphism with interfaces.

// Interface
interface Drawable {
    void draw();
}

// Implementing classes
class Circle implements Drawable {
    public void draw() {
        System.out.println("Drawing a circle.");
    }
}

class Square implements Drawable {
    public void draw() {
        System.out.println("Drawing a square.");
    }
}

public class Main {
    public static void main(String[] args) {
        Drawable shape1 = new Circle();
        Drawable shape2 = new Square();

        shape1.draw();
        shape2.draw();
    }
}

Explanation: Polymorphism works with interfaces — one interface type reference can point to different implementations, and the actual method run is based on the object.

Expected Mistake: Forgetting to mark the method in the class as public when implementing an interface — it must be public.

Q5. Casting and polymorphism.

// Parent class
class Animal {
    void makeSound() {
        System.out.println("Some sound");
    }
}

// Child class
class Cat extends Animal {
    void makeSound() {
        System.out.println("Meow!");
    }

    void purr() {
        System.out.println("Purring...");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Cat(); // Upcasting
        myAnimal.makeSound(); // Runs Cat's version

        // Downcasting to access child-specific methods
        Cat myCat = (Cat) myAnimal;
        myCat.purr();
    }
}

Explanation: Upcasting (child to parent) happens automatically and allows polymorphic behavior. Downcasting (parent to child) is needed to access child-specific features.

Expected Mistake: Downcasting to the wrong type causes a ClassCastException at runtime.

🔀 Pillar 4: Polymorphism — DSA Practice Questions

Q1: Shape Area Calculator using Method Overriding

Write a Java program that uses runtime polymorphism to calculate the area of different shapes (Circle, Rectangle) using overridden methods.

// Base class
class Shape {
    double area() { 
        return 0; // Default implementation
    }
}

// Subclass for Circle
class Circle extends Shape {
    double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * radius * radius; // πr²
    }
}

// Subclass for Rectangle
class Rectangle extends Shape {
    double length, width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    double area() {
        return length * width; // Area formula for rectangle
    }
}

public class ShapeTest {
    public static void main(String[] args) {
        Shape s1 = new Circle(5); // Polymorphic reference
        Shape s2 = new Rectangle(4, 6);

        System.out.println("Circle Area: " + s1.area());
        System.out.println("Rectangle Area: " + s2.area());
    }
}

Explanation: Here, Shape is the parent class, and Circle & Rectangle override the area() method. We use a Shape reference to call the correct area() method based on the actual object at runtime.

Expected Mistake: Forgetting to use @Override might cause incorrect method signature errors or unintended overloading instead of overriding.

Q2: Sorting Algorithm Selector using Method Overriding

Write a program that uses polymorphism to choose different sorting algorithms at runtime.

// Base class
abstract class SortAlgorithm {
    abstract void sort(int[] arr);
}

// Bubble Sort Implementation
class BubbleSort extends SortAlgorithm {
    @Override
    void sort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
}

// Insertion Sort Implementation
class InsertionSort extends SortAlgorithm {
    @Override
    void sort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            int key = arr[i];
            int j = i - 1;
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;
        }
    }
}

public class SortTest {
    public static void main(String[] args) {
        int[] numbers = {5, 3, 8, 1, 2};

        SortAlgorithm sorter = new BubbleSort(); // Try InsertionSort here
        sorter.sort(numbers);

        for (int num : numbers) {
            System.out.print(num + " ");
        }
    }
}

Explanation: We define an abstract base SortAlgorithm and override sort() in different sorting classes. At runtime, we can choose which algorithm to use by simply changing the object creation.

Expected Mistake: Writing algorithm-specific code inside main() instead of separate classes breaks polymorphism principles.

Q3: Payment Gateway Simulator

Use polymorphism to simulate different payment methods like CreditCard and PayPal, each with its own processing logic.

// Base class
abstract class Payment {
    abstract void processPayment(double amount);
}

class CreditCardPayment extends Payment {
    @Override
    void processPayment(double amount) {
        System.out.println("Processing Credit Card Payment of $" + amount);
    }
}

class PayPalPayment extends Payment {
    @Override
    void processPayment(double amount) {
        System.out.println("Processing PayPal Payment of $" + amount);
    }
}

public class PaymentTest {
    public static void main(String[] args) {
        Payment payment = new CreditCardPayment();
        payment.processPayment(150.75);

        payment = new PayPalPayment();
        payment.processPayment(89.99);
    }
}

Explanation: By defining a common Payment base class, each payment type has its own processPayment() method. Switching payment types doesn't require changing the rest of the code.

Expected Mistake: Forgetting to make the base method abstract could allow incomplete implementations without compiler errors.

Q4: Logger System with Different Output Formats

Implement a logger system where the output format can change (Plain text, JSON, XML) without modifying existing code.

// Base class
abstract class Logger {
    abstract void log(String message);
}

class PlainTextLogger extends Logger {
    @Override
    void log(String message) {
        System.out.println("LOG: " + message);
    }
}

class JSONLogger extends Logger {
    @Override
    void log(String message) {
        System.out.println("{ \"log\": \"" + message + "\" }");
    }
}

public class LoggerTest {
    public static void main(String[] args) {
        Logger logger = new PlainTextLogger();
        logger.log("Application started");

        logger = new JSONLogger();
        logger.log("Application crashed");
    }
}

Explanation: Each logger class overrides log() to format the output differently. We can add new formats later without changing the calling code.

Expected Mistake: Printing format logic in main() defeats the point of polymorphism — it should be inside subclasses.

Q5: Search Algorithm Selector

Write a program that uses polymorphism to switch between different search algorithms (Linear Search, Binary Search) at runtime.

// Base class
abstract class SearchAlgorithm {
    abstract int search(int[] arr, int target);
}

class LinearSearch extends SearchAlgorithm {
    @Override
    int search(int[] arr, int target) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == target) return i;
        }
        return -1;
    }
}

class BinarySearch extends SearchAlgorithm {
    @Override
    int search(int[] arr, int target) {
        int left = 0, right = arr.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (arr[mid] == target) return mid;
            if (arr[mid] < target) left = mid + 1;
            else right = mid - 1;
        }
        return -1;
    }
}

public class SearchTest {
    public static void main(String[] args) {
        int[] data = {1, 3, 5, 7, 9};
        SearchAlgorithm searcher = new BinarySearch();

        int index = searcher.search(data, 7);
        System.out.println("Element found at index: " + index);
    }
}

Explanation: Both LinearSearch and BinarySearch implement search() differently. We can switch the algorithm easily at runtime without touching the main program logic.

Expected Mistake: Forgetting that Binary Search requires a sorted array will lead to incorrect results.

🎭 Pillar 4: Polymorphism — 20 Questions with Solutions

These questions will strengthen your understanding of Polymorphism in Java. Polymorphism means "many forms" — allowing the same method or object reference to behave differently based on the actual object type.

  • Q1: What is polymorphism?

    Explanation: The ability of a single reference type to refer to objects of different types and call methods that behave differently.

    Expected Mistake: Confusing it with method overloading only.

  • Q2: What are the two main types of polymorphism in Java?

    Explanation: Compile-time (method overloading) and runtime (method overriding) polymorphism.

    Expected Mistake: Thinking Java supports operator overloading like C++.

  • Q3: Which type of polymorphism is achieved using method overriding?

    Explanation: Runtime polymorphism.

    Expected Mistake: Saying compile-time polymorphism.

  • Q4: Which type of polymorphism is achieved using method overloading?

    Explanation: Compile-time polymorphism.

    Expected Mistake: Thinking overloading is resolved at runtime.

  • Q5: What is method overloading?

    Explanation: Defining multiple methods with the same name but different parameter lists.

    Expected Mistake: Believing return type alone can differentiate overloaded methods.

  • Q6: What is method overriding?

    Explanation: Providing a new implementation for an inherited method in the subclass.

    Expected Mistake: Forgetting the method signature must match exactly.

  • Q7: Can static methods be overridden?

    Explanation: No, they can be hidden but not overridden.

    Expected Mistake: Thinking static method behavior changes at runtime via polymorphism.

  • Q8: How does the JVM determine which overridden method to call at runtime?

    Explanation: By looking at the actual object type (dynamic method dispatch).

    Expected Mistake: Assuming it depends on the reference type only.

  • Q9: Can final methods participate in polymorphism?

    Explanation: They can be inherited and called, but cannot be overridden.

    Expected Mistake: Thinking final methods can be overridden if needed.

  • Q10: What is upcasting in polymorphism?

    Explanation: Treating a subclass object as an instance of its superclass type.

    Expected Mistake: Thinking upcasting loses method access — overridden methods still work.

  • Q11: What is downcasting?

    Explanation: Casting a superclass reference back to a subclass type.

    Expected Mistake: Forgetting to check type before downcasting, causing ClassCastException.

  • Q12: Is constructor overloading an example of polymorphism?

    Explanation: Yes, it's a form of compile-time polymorphism.

    Expected Mistake: Thinking it's unrelated to polymorphism.

  • Q13: Can interfaces demonstrate polymorphism?

    Explanation: Yes, different classes implementing the same interface can be used interchangeably.

    Expected Mistake: Believing polymorphism only happens with class inheritance.

  • Q14: What is dynamic method dispatch?

    Explanation: The process where the JVM decides at runtime which overridden method to call.

    Expected Mistake: Thinking method calls are always decided at compile-time.

  • Q15: Can overloaded methods have different return types?

    Explanation: Yes, as long as parameter lists differ.

    Expected Mistake: Believing return type alone is enough to overload.

  • Q16: Can you override a method with a more restrictive access modifier?

    Explanation: No, you can only keep or widen access.

    Expected Mistake: Thinking you can make a public method private in the subclass.

  • Q17: How does polymorphism help in code maintenance?

    Explanation: It allows changing or adding new subclasses without modifying existing code using the parent type.

    Expected Mistake: Believing polymorphism only affects method naming.

  • Q18: What happens if you call a method that exists only in the subclass via a superclass reference?

    Explanation: It won't compile unless you downcast to the subclass first.

    Expected Mistake: Thinking the JVM will “find it” at runtime automatically.

  • Q19: Can abstract methods be used in polymorphism?

    Explanation: Yes, they force subclasses to provide implementation, enabling runtime polymorphism.

    Expected Mistake: Believing abstract methods can't be called via a reference to the abstract type.

  • Q20: How does polymorphism relate to design patterns?

    Explanation: Many design patterns like Strategy, Factory, and Observer rely heavily on polymorphism.

    Expected Mistake: Thinking design patterns don't need polymorphism at all.

🔀 Pillar 4: Polymorphism — Basic Questions

Q1. Demonstrate method overloading (compile-time polymorphism).

// Save as Main.java

class MathUtils {
    // Method to add two integers
    int add(int a, int b) {
        return a + b;
    }

    // Overloaded method to add two doubles
    double add(double a, double b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        MathUtils math = new MathUtils();
        
        System.out.println("Sum of integers: " + math.add(5, 10)); // Calls int version
        System.out.println("Sum of doubles: " + math.add(5.5, 10.3)); // Calls double version
    }
}

Explanation: Method overloading lets you use the same method name with different parameter lists. The compiler decides which version to call based on argument types.

Expected Mistake: Changing only the return type but keeping the same parameters is not valid overloading — it will cause a compile-time error.

Q2. Demonstrate method overriding (runtime polymorphism).

// Parent class
class Animal {
    void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

// Child class overrides the method
class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Woof! Woof!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a1 = new Animal();
        Animal a2 = new Dog(); // Polymorphic reference

        a1.makeSound(); // Output: Some generic animal sound
        a2.makeSound(); // Output: Woof! Woof! (Dog's version)
    }
}

Explanation: In overriding, the method called depends on the actual object type at runtime, not the reference type.

Expected Mistake: Forgetting @Override can hide issues if method signatures don't exactly match, leading to overloading instead.

Q3. Use polymorphism with an array of objects.

// Base class
class Shape {
    void draw() {
        System.out.println("Drawing a generic shape");
    }
}

// Subclass 1
class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a circle");
    }
}

// Subclass 2
class Square extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a square");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape[] shapes = { new Circle(), new Square(), new Shape() };

        for (Shape s : shapes) {
            s.draw(); // Calls appropriate overridden method
        }
    }
}

Explanation: Storing different subclass objects in a parent type array allows uniform method calls. At runtime, the correct method for each object is executed.

Expected Mistake: Trying to call subclass-specific methods (like getRadius() in Circle) on a Shape reference will cause a compile-time error unless you cast.

Q4. Demonstrate polymorphism with interfaces.

// Interface
interface Payment {
    void pay(double amount);
}

// CreditCard implementation
class CreditCardPayment implements Payment {
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using Credit Card.");
    }
}

// PayPal implementation
class PayPalPayment implements Payment {
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using PayPal.");
    }
}

public class Main {
    public static void main(String[] args) {
        Payment p1 = new CreditCardPayment();
        Payment p2 = new PayPalPayment();

        p1.pay(100.0);
        p2.pay(250.5);
    }
}

Explanation: Interfaces enable polymorphism by allowing different classes to implement the same methods with different logic.

Expected Mistake: Forgetting to make the interface methods public in the implementation will cause a compile-time visibility error.

Q5. Combine method overloading and overriding in one program.

// Base class
class Printer {
    void print(String text) {
        System.out.println("Printing text: " + text);
    }
}

// Subclass
class ColorPrinter extends Printer {
    // Overloaded method
    void print(String text, String color) {
        System.out.println("Printing text in " + color + ": " + text);
    }

    // Overridden method
    @Override
    void print(String text) {
        System.out.println("Printing in default color: " + text);
    }
}

public class Main {
    public static void main(String[] args) {
        Printer p = new ColorPrinter(); // Polymorphic reference
        p.print("Hello World"); // Calls overridden version

        ColorPrinter cp = new ColorPrinter();
        cp.print("Hello World", "Red"); // Calls overloaded version
    }
}

Explanation: This program shows compile-time polymorphism (overloading) and runtime polymorphism (overriding) in the same hierarchy.

Expected Mistake: Thinking overloading is decided at runtime — in reality, it's resolved at compile time.

🛡️ Abstraction — Practice Time!

💡 Beginner

  • Create an abstract class Animal with an abstract method makeSound(). Implement it in a Dog class.
  • Write an abstract class Shape with an abstract method area() and a normal method display() that prints “Calculating area”.
  • Create an abstract class Vehicle with abstract method start() and implement it in Car and Bike classes.

🧪 Intermediate

  • Build an abstract class Employee with an abstract method calculateSalary(). Create FullTimeEmployee and PartTimeEmployee classes.
  • Create an abstract class Appliance with methods turnOn() and turnOff() (abstract). Implement Fan and Light.
  • Make an abstract class Game with abstract methods initialize() and startPlay(). Implement it for Football and Chess.

🔥 Challenge

  • Design a payment system where Payment is abstract with abstract method processPayment(). Implement CreditCardPayment and PayPalPayment.
  • Create an abstract class DataProcessor with methods readData(), processData(), and writeData(). Implement for CSVProcessor and JSONProcessor.
  • Make a simulation of Animal behaviors where each subclass implements its own eat() and sleep() logic.

📦 Encapsulation — Practice Time!

💡 Beginner

  • Create a BankAccount class with private balance and public deposit() and getBalance() methods.
  • Make a Student class with private name and rollNumber variables, with getters and setters.
  • Write a Book class with private variables title and author and provide public methods to set/get them.

🧪 Intermediate

  • Build a Car class with private speed variable and methods to increase/decrease speed safely.
  • Make a Rectangle class with private length and width, plus methods to set them with validation.
  • Write a User class with private username and password, and a method checkPassword().

🔥 Challenge

  • Create a ShoppingCart class with private list of items and methods to add/remove items.
  • Build a SecureDoor class with private isLocked and methods to lock/unlock only with a passcode.
  • Make a WeatherStation class with private temperature, humidity, and pressure, and methods to update and read them.

🌳 Inheritance — Practice Time!

💡 Beginner

  • Create a parent class Vehicle with start() method and child class Car with playMusic().
  • Make a Person class with walk() method and Student class extending it with study() method.
  • Write a Device class with turnOn() and a Phone class extending it with call().

🧪 Intermediate

  • Make a Shape class and extend it into Circle and Square classes with extra methods.
  • Create Employee as a base class and extend it into Manager and Developer.
  • Write a Food base class and extend it into Fruit and Vegetable.

🔥 Challenge

  • Create a Transport hierarchy: VehicleCarElectricCar with extra features at each level.
  • Make a GameCharacter class extended by Warrior and Wizard with different attack methods.
  • Design a Building class extended by House and Office with specific facilities.

🔀 Polymorphism — Practice Time!

💡 Beginner

  • Write an Animal base class with makeSound() and override it in Dog and Cat.
  • Create a Shape base class with draw() method and override it in Circle and Square.
  • Make a Vehicle base class with move() method and override it in Car and Bike.

🧪 Intermediate

  • Create a payment processing system with a base class Payment and child classes CreditCardPayment and PaypalPayment.
  • Make a MediaPlayer class with play() method and override in AudioPlayer and VideoPlayer.
  • Use method overloading to create multiple print() versions for different data types.

🔥 Challenge

  • Create a base class Employee with work() method, override it in Manager, Developer, and Designer.
  • Build a game where Weapon is the base class and Sword and Gun override attack() differently.
  • Make a Transport base class with move(), override it in Ship, Plane, and Train.

⚙️ Step-by-Step Learning Path for OOP in Java

  1. Understand class and object.
  2. Learn fields (variables) and methods.
  3. Apply encapsulation to protect data.
  4. Use inheritance to reuse and extend code.
  5. Apply abstraction for cleaner design.
  6. Use polymorphism for flexibility.
  7. Practice by building small real-world projects.

📂 Real-World Examples of OOP in Java

  • 💼 Banking apps (Account, Customer, Transaction classes)
  • 🎮 Games (Player, Enemy, Weapon objects)
  • 🚗 Car rental systems (Car, Truck, Motorcycle classes)
  • 📱 Android apps (Activities, Fragments, Services)

🚧 Common Mistakes in OOP

  • Mixing unrelated logic in one class.
  • Not using private for sensitive data.
  • Overusing inheritance instead of composition.

🔮 Advanced OOP in Java

Once you master the basics, explore:

  • Interfaces with default and static methods
  • Abstract classes vs Interfaces — when to choose which
  • Composition over inheritance
  • Design Patterns (Singleton, Factory, Observer)

🧠 Final Words

The four OOP pillars — Abstraction, Encapsulation, Inheritance, Polymorphism — are not just rules, they're the foundation of clean, maintainable Java code.

Master them, and you'll be building applications that are easy to scale, debug, and extend for years to come.

In our next post, we'll dive into ⚙️ [2] Setting Up Java (JDK + IDE) — From Installation to First Line of Code!

— Blog by Aelify (ML2AI.com)