Week 3 - OOP Pillars & Advanced Concepts

Class 7 - Recording

▶ Watch Video

Inheritance in Java

Inheritance allows a subclass to acquire the properties and behaviors of a superclass. It promotes code reusability and method overriding.

Example:


class Animal {
    void sound() {
        System.out.println("Animals make sounds");
    }
}

class Dog extends Animal {
    void sound() {
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.sound(); // Output: Dog barks
    }
}

        

Important Points:

  • Java does not support multiple inheritance with classes.
  • All classes inherit from the Object class.
  • Use super to access parent methods or constructors.

Related Exceptions:


// ClassCastException example
Animal a = new Animal();
Dog d = (Dog) a; // Throws ClassCastException

        

Polymorphism in Java

Polymorphism means one interface, many implementations. It allows methods to behave differently based on the object.

1. Compile-time Polymorphism (Method Overloading)


class MathOps {
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
}

        

2. Runtime Polymorphism (Method Overriding)


class Bird {
    void fly() {
        System.out.println("Bird is flying");
    }
}

class Eagle extends Bird {
    void fly() {
        System.out.println("Eagle flies high");
    }
}

public class Test {
    public static void main(String[] args) {
        Bird b = new Eagle();
        b.fly(); // Output: Eagle flies high
    }
}

        

Important Points:

  • Method overloading is resolved at compile-time.
  • Method overriding is resolved at runtime.
  • Overridden methods must have the same signature.
  • Return type can be covariant (subtype allowed).

Related Exceptions:


// Wrong downcasting -> ClassCastException
Bird b = new Bird();
Eagle e = (Eagle) b; // Throws ClassCastException

        

Encapsulation in Java

Encapsulation wraps data (variables) and methods within a class and protects them from unauthorized access using private access modifiers.

Example:


class Student {
    private String name;
    private int age;

    public String getName() { return name; }

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

    public int getAge() { return age; }

    public void setAge(int age) {
        if (age > 0) {
            this.age = age;
        } else {
            System.out.println("Invalid age");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Student s = new Student();
        s.setName("John");
        s.setAge(20);

        System.out.println(s.getName());
        System.out.println(s.getAge());
    }
}

        

Important Points:

  • Use private variables and public getters/setters.
  • Encapsulation protects data and enables validation.
  • Improves maintainability and flexibility.

Related Exceptions:


// Custom exception in setter
public void setAge(int age) {
    if (age <= 0) {
        throw new IllegalArgumentException("Age must be positive");
    }
    this.age = age;
}

        

Class 8 - Recording

▶ Watch Video

Abstract Classes in Java

Abstract classes are classes that cannot be instantiated directly. They serve as blueprints for other classes and can contain both abstract and concrete methods.

What is an Abstract Class?

An abstract class is declared with the abstract keyword. It can have:

  • Abstract methods (without body)
  • Concrete methods (with body)
  • Instance variables
  • Constructors

Example: Basic Abstract Class


abstract class Vehicle {
    // abstract method (no body)
    abstract void start();
    abstract void stop();

    // concrete method
    void honk() {
        System.out.println("Beep Beep!");
    }
}

class Car extends Vehicle {
    @Override
    void start() {
        System.out.println("Car engine started");
    }

    @Override
    void stop() {
        System.out.println("Car stopped");
    }
}

public class Main {
    public static void main(String[] args) {
        // Vehicle v = new Vehicle();  // ❌ Compile Error: Cannot instantiate abstract class
        
        Car car = new Car();
        car.start();  // Output: Car engine started
        car.honk();   // Output: Beep Beep!
        car.stop();   // Output: Car stopped
    }
}

        

Real-World Use Case

Imagine a payment system. The Payment class is abstract because different payment methods work differently:


abstract class Payment {
    abstract void processPayment(double amount);
    abstract void refund();

    void printReceipt() {
        System.out.println("--- Receipt ---");
    }
}

class CreditCardPayment extends Payment {
    @Override
    void processPayment(double amount) {
        System.out.println("Processing credit card payment: $" + amount);
    }

    @Override
    void refund() {
        System.out.println("Refunding to credit card");
    }
}

class UPIPayment extends Payment {
    @Override
    void processPayment(double amount) {
        System.out.println("Processing UPI payment: $" + amount);
    }

    @Override
    void refund() {
        System.out.println("Refunding UPI amount");
    }
}

        

Key Points:

  • Cannot create instance of abstract class
  • Subclass MUST override all abstract methods
  • A subclass can also be abstract (doesn't need to implement all methods)
  • Abstract classes can have constructors (for initialization)
  • Use abstract classes for "IS-A" relationships

Abstract Class vs Concrete Class

Feature Abstract Class Concrete Class
Can Instantiate No Yes
Can have abstract methods Yes No
Can have concrete methods Yes Yes
Constructor Yes Yes

Interfaces in Java

Interfaces are contracts that define what methods a class must implement. They promote loose coupling and multiple inheritance of behavior.

What is an Interface?

An interface is a blueprint of a class. It contains only abstract methods and constants (before Java 8). From Java 8 onwards, interfaces can have default methods and static methods.

Example: Basic Interface


interface Animal {
    void eat();
    void sleep();
}

class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating");
    }

    @Override
    public void sleep() {
        System.out.println("Dog is sleeping");
    }
}

public class Main {
    public static void main(String[] args) {
        // Animal a = new Animal();  // ❌ Compile Error: Cannot instantiate interface
        
        Dog dog = new Dog();
        dog.eat();    // Output: Dog is eating
        dog.sleep();  // Output: Dog is sleeping
    }
}

        

Real-World Use Case: Multiple Implementations


interface Drawable {
    void draw();
}

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

class Rectangle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

public class Main {
    public static void main(String[] args) {
        Drawable c = new Circle();
        Drawable r = new Rectangle();

        c.draw();  // Output: Drawing a circle
        r.draw();  // Output: Drawing a rectangle
    }
}

        

Multiple Inheritance with Interfaces

A class can implement multiple interfaces, allowing multiple inheritance:


interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

class Duck implements Flyable, Swimmable {
    @Override
    public void fly() {
        System.out.println("Duck is flying");
    }

    @Override
    public void swim() {
        System.out.println("Duck is swimming");
    }
}

public class Main {
    public static void main(String[] args) {
        Duck duck = new Duck();
        duck.fly();    // Output: Duck is flying
        duck.swim();   // Output: Duck is swimming
    }
}

        

Default Methods (Java 8+)

Interfaces can now have concrete methods with the default keyword:


interface Logger {
    void log(String msg);

    default void logInfo(String msg) {
        System.out.println("[INFO] " + msg);
    }
}

class ConsoleLogger implements Logger {
    @Override
    public void log(String msg) {
        System.out.println("Console: " + msg);
    }
}

public class Main {
    public static void main(String[] args) {
        Logger logger = new ConsoleLogger();
        logger.log("Hello");        // Output: Console: Hello
        logger.logInfo("Info msg");  // Output: [INFO] Info msg
    }
}

        

Static Methods in Interfaces (Java 8+)


interface MathOps {
    static int add(int a, int b) {
        return a + b;
    }

    static int multiply(int a, int b) {
        return a * b;
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(MathOps.add(5, 3));        // Output: 8
        System.out.println(MathOps.multiply(5, 3));   // Output: 15
    }
}

        

Key Points:

  • A class can implement multiple interfaces
  • An interface can extend other interfaces
  • All methods in interface are public and abstract (by default)
  • From Java 8: interfaces can have default and static methods
  • Use interfaces for "HAS-A" or "CAN-DO" relationships
  • Interfaces support loose coupling and flexibility

Abstract Class vs Interface

Feature Abstract Class Interface
Multiple Inheritance No (single) Yes (multiple)
Access Modifiers public, private, protected public (implicit)
Constructor Yes No
Instance Variables Yes (any type) Only constants
When to Use IS-A relationship CAN-DO relationship

When to Use What?

  • Abstract Class: When classes share common code and state
  • Interface: When defining behavior contract for unrelated classes
  • Both: Abstract class can implement multiple interfaces