Java is fundamentally an object-oriented language. Everything in Java (except primitives) is an object. OOP organizes code around objects — bundles of state (fields) and behavior (methods). The four pillars of OOP are Encapsulation, Inheritance, Polymorphism, and Abstraction. Mastering these makes you a professional Java developer.


Step 1 — Classes and Objects

A class is a blueprint. An object is an instance of that blueprint. A class defines what properties (fields) and behaviors (methods) its objects will have.

BankAccount.javajava
// Class = Blueprint
public class BankAccount {

    // --- FIELDS (state/attributes) ---
    // private: only accessible within this class (encapsulation)
    private String owner;
    private double balance;
    private String accountNumber;
    private static int totalAccounts = 0; // static field: shared across ALL instances

    // --- CONSTRUCTOR: called when 'new BankAccount(...)' is executed ---
    // Same name as the class. No return type (not even void).
    public BankAccount(String owner, double initialBalance) {
        this.owner = owner;              // 'this' refers to the current object
        this.balance = initialBalance;
        this.accountNumber = "ACC-" + (++totalAccounts); // auto-generate
    }

    // --- METHODS (behavior) ---
    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        this.balance += amount;
    }

    public void withdraw(double amount) {
        if (amount > this.balance) throw new IllegalStateException("Insufficient funds");
        this.balance -= amount;
    }

    // --- GETTERS: controlled read access to private fields ---
    public double getBalance()      { return balance; }
    public String getOwner()        { return owner; }
    public String getAccountNumber(){ return accountNumber; }

    // --- Static method: belongs to the class, not an instance ---
    public static int getTotalAccounts() { return totalAccounts; }

    // --- toString: called automatically in String context ---
    @Override
    public String toString() {
        return String.format("BankAccount[%s, owner=%s, balance=%.2f]",
                              accountNumber, owner, balance);
    }
}

// --- Main class to use BankAccount ---
class Main {
    public static void main(String[] args) {
        // 'new' allocates object on heap, calls constructor
        BankAccount acc1 = new BankAccount("Kuldeep", 1000.0);
        BankAccount acc2 = new BankAccount("Priya",   500.0);

        acc1.deposit(500.0);
        acc1.withdraw(200.0);

        System.out.println(acc1); // BankAccount[ACC-1, owner=Kuldeep, balance=1300.00]
        System.out.println(acc2); // BankAccount[ACC-2, owner=Priya,   balance=500.00]
        System.out.println("Total accounts: " + BankAccount.getTotalAccounts()); // 2

        // acc1.balance = 999; // COMPILE ERROR: balance is private
    }
}

Step 2 — Encapsulation

Encapsulation means hiding internal state and requiring all interaction to go through well-defined methods (getters/setters). It prevents external code from putting an object into an invalid state.

Person.java — Encapsulation with validationjava
public class Person {
    private String name;
    private int age;
    private String email;

    public Person(String name, int age, String email) {
        setName(name);   // use setters in constructor for validation
        setAge(age);
        setEmail(email);
    }

    public String getName() { return name; }

    public void setName(String name) {
        if (name == null || name.isBlank())
            throw new IllegalArgumentException("Name cannot be empty");
        this.name = name.trim();
    }

    public int getAge() { return age; }

    public void setAge(int age) {
        if (age < 0 || age > 150)
            throw new IllegalArgumentException("Invalid age: " + age);
        this.age = age;
    }

    public String getEmail() { return email; }

    public void setEmail(String email) {
        if (email == null || !email.contains("@"))
            throw new IllegalArgumentException("Invalid email");
        this.email = email.toLowerCase();
    }

    // Read-only computed property
    public boolean isAdult() { return age >= 18; }
}

Step 3 — Constructors: Default, Parameterized, and this()

Product.java — Constructor chainingjava
public class Product {
    private String name;
    private double price;
    private int stock;

    // No-arg constructor (also called default constructor)
    public Product() {
        this("Unknown", 0.0, 0); // calls the 3-arg constructor — this() must be FIRST line
    }

    // 2-arg constructor
    public Product(String name, double price) {
        this(name, price, 100); // chain to 3-arg constructor
    }

    // The "master" constructor — all others delegate here
    public Product(String name, double price, int stock) {
        this.name  = name;
        this.price = price;
        this.stock = stock;
    }

    @Override
    public String toString() {
        return name + " ($" + price + ", stock: " + stock + ")";
    }

    public static void main(String[] args) {
        Product p1 = new Product();                    // Unknown ($0.0, stock: 0)
        Product p2 = new Product("Laptop", 999.99);    // Laptop ($999.99, stock: 100)
        Product p3 = new Product("Phone", 499.99, 50); // Phone ($499.99, stock: 50)
        System.out.println(p1);
        System.out.println(p2);
        System.out.println(p3);
    }
}

Step 4 — Inheritance

Inheritance allows a child class (subclass) to inherit fields and methods from a parent class (superclass). Java supports single inheritance for classes (one parent only) but multiple inheritance through interfaces.

Inheritance.javajava
// --- Parent class (Superclass) ---
public class Animal {
    protected String name; // protected: accessible in this class AND subclasses
    protected int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age  = age;
    }

    public void eat() {
        System.out.println(name + " is eating.");
    }

    public void sleep() {
        System.out.println(name + " is sleeping.");
    }

    public String describe() {
        return "Animal: " + name + ", Age: " + age;
    }
}

// --- Child class: Dog extends Animal ---
// Dog INHERITS all non-private fields and methods of Animal
class Dog extends Animal {
    private String breed;

    // super(...) calls the parent constructor — MUST be first line
    public Dog(String name, int age, String breed) {
        super(name, age); // initialize the Animal part
        this.breed = breed;
    }

    // Dog adds its OWN behavior
    public void bark() {
        System.out.println(name + " says: Woof!");
    }

    // @Override: overrides the parent's describe() method
    @Override
    public String describe() {
        return super.describe() + ", Breed: " + breed; // reuse parent's output
    }
}

// --- Another child: Cat extends Animal ---
class Cat extends Animal {
    private boolean isIndoor;

    public Cat(String name, int age, boolean isIndoor) {
        super(name, age);
        this.isIndoor = isIndoor;
    }

    public void purr() {
        System.out.println(name + " purrs...");
    }

    @Override
    public String describe() {
        return super.describe() + (isIndoor ? " (indoor)" : " (outdoor)");
    }
}

class InheritanceDemo {
    public static void main(String[] args) {
        Dog dog = new Dog("Rex", 3, "Labrador");
        Cat cat = new Cat("Whiskers", 5, true);

        dog.eat();    // inherited from Animal: "Rex is eating."
        dog.bark();   // Dog's own method: "Rex says: Woof!"
        dog.sleep();  // inherited: "Rex is sleeping."
        System.out.println(dog.describe()); // Animal: Rex, Age: 3, Breed: Labrador

        cat.eat();    // inherited: "Whiskers is eating."
        cat.purr();   // Cat's own: "Whiskers purrs..."
        System.out.println(cat.describe()); // Animal: Whiskers, Age: 5 (indoor)

        // IS-A relationship: Dog IS-A Animal
        Animal a = new Dog("Buddy", 2, "Poodle"); // Dog reference stored as Animal
        a.eat();    // works: eat() is in Animal
        // a.bark(); // COMPILE ERROR: Animal reference doesn't know about bark()
        ((Dog) a).bark(); // downcast to access Dog-specific method
    }
}

Step 5 — Polymorphism

Polymorphism means 'many forms'. In Java there are two types: Compile-time polymorphism (method overloading — decided at compile time) and Runtime polymorphism (method overriding — decided at runtime based on the actual object type).

Polymorphism.javajava
// Runtime Polymorphism — the most powerful form
class Shape {
    public double area() {
        return 0; // base implementation
    }
    public String describe() {
        return "Shape with area: " + area();
    }
}

class Circle extends Shape {
    private double radius;
    public Circle(double radius) { this.radius = radius; }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

class Rectangle extends Shape {
    private double width, height;
    public Rectangle(double w, double h) { this.width = w; this.height = h; }

    @Override
    public double area() {
        return width * height;
    }
}

class Triangle extends Shape {
    private double base, height;
    public Triangle(double b, double h) { this.base = b; this.height = h; }

    @Override
    public double area() {
        return 0.5 * base * height;
    }
}

class PolymorphismDemo {
    // This method works for ANY Shape — present or future!
    static void printArea(Shape shape) {
        // At RUNTIME, Java calls the ACTUAL object's area() method
        System.out.printf("%s -> Area: %.2f%n",
            shape.getClass().getSimpleName(), shape.area());
    }

    public static void main(String[] args) {
        // All stored as 'Shape' references
        Shape[] shapes = {
            new Circle(5),
            new Rectangle(4, 6),
            new Triangle(3, 8)
        };

        // Dynamic dispatch: the correct area() is called based on actual type
        for (Shape s : shapes) {
            printArea(s);
        }
        // Circle    -> Area: 78.54
        // Rectangle -> Area: 24.00
        // Triangle  -> Area: 12.00

        // describe() calls area() which is polymorphic!
        System.out.println(new Circle(5).describe());
        // Shape with area: 78.53981633974483
    }
}

Step 6 — Abstraction: Abstract Classes and Interfaces

Abstraction means hiding implementation details and exposing only what is necessary. Java provides two mechanisms: abstract classes and interfaces.

AbstractClasses.javajava
// Abstract class: cannot be instantiated directly
// Can have both abstract methods AND concrete methods AND fields
public abstract class Vehicle {
    protected String brand;
    protected int year;
    protected double fuelLevel;

    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year  = year;
        this.fuelLevel = 100.0;
    }

    // Abstract method: MUST be implemented by all subclasses
    public abstract void startEngine();
    public abstract double fuelConsumptionPerKm();

    // Concrete method: shared logic for all vehicles
    public void drive(double km) {
        double fuelNeeded = km * fuelConsumptionPerKm();
        if (fuelLevel < fuelNeeded)
            throw new IllegalStateException("Not enough fuel!");
        fuelLevel -= fuelNeeded;
        System.out.printf("%s drove %.1f km. Fuel left: %.1f%%%n",
                           brand, km, fuelLevel);
    }

    public String getInfo() {
        return brand + " (" + year + ")";
    }
}

class Car extends Vehicle {
    public Car(String brand, int year) { super(brand, year); }

    @Override
    public void startEngine() {
        System.out.println(brand + ": Vroom! Engine started.");
    }

    @Override
    public double fuelConsumptionPerKm() { return 0.08; } // 8L/100km
}

class Truck extends Vehicle {
    private double cargoWeight;

    public Truck(String brand, int year, double cargoWeight) {
        super(brand, year);
        this.cargoWeight = cargoWeight;
    }

    @Override
    public void startEngine() {
        System.out.println(brand + ": ROAR! Diesel engine started.");
    }

    @Override
    public double fuelConsumptionPerKm() {
        return 0.15 + (cargoWeight * 0.001); // heavier cargo = more fuel
    }
}
Interfaces.javajava
// Interface: a pure contract (Java 8+ can have default/static methods)
public interface Flyable {
    // Constants in interfaces are implicitly public static final
    int MAX_ALTITUDE = 40000;

    // Abstract method: all implementing classes must implement this
    void fly(int altitude);
    void land();

    // Default method (Java 8+): provides a default implementation
    default void hover() {
        System.out.println("Hovering in place...");
    }

    // Static method (Java 8+): utility method on the interface
    static void printMaxAltitude() {
        System.out.println("Max altitude: " + MAX_ALTITUDE + " ft");
    }
}

interface Swimmable {
    void swim(int depth);
    default void float_() { System.out.println("Floating on surface..."); }
}

// Java supports MULTIPLE interface implementation (unlike extends)
class Duck extends Animal implements Flyable, Swimmable {
    public Duck(String name) { super(name, 1); }

    @Override
    public void fly(int altitude) {
        System.out.println(name + " is flying at " + altitude + " ft.");
    }

    @Override
    public void land() {
        System.out.println(name + " has landed.");
    }

    @Override
    public void swim(int depth) {
        System.out.println(name + " is swimming at " + depth + " m depth.");
    }
}

class InterfaceDemo {
    public static void main(String[] args) {
        Duck duck = new Duck("Donald");
        duck.fly(500);
        duck.swim(2);
        duck.hover();    // uses default method from Flyable
        duck.float_();   // uses default method from Swimmable

        // Interfaces as types (polymorphism!)
        Flyable f = duck;  // Duck IS-A Flyable
        f.fly(1000);
        // f.swim(5); // COMPILE ERROR: Flyable reference doesn't know swim()

        Flyable.printMaxAltitude(); // static interface method
    }
}

Abstract Class vs Interface: When to Use Which

  • Use abstract class when: classes share common STATE (fields) and behavior; you have an IS-A relationship; you need constructors or protected members.
  • Use interface when: defining a CONTRACT that unrelated classes can implement; you need multiple 'types' for one class; you have no shared state.
  • Rule of thumb: interfaces define WHAT an object can do; abstract classes define WHAT an object IS.
  • Java 8+: interfaces can have default methods, blurring the line slightly. Prefer interfaces for most abstractions in modern Java.

Step 7 — equals(), hashCode(), and Comparable

These three methods are critical for using your objects correctly in collections (HashMap, HashSet, TreeSet, etc.).

Student.java — Proper equals/hashCode/Comparablejava
import java.util.*;

public class Student implements Comparable<Student> {
    private String id;
    private String name;
    private double gpa;

    public Student(String id, String name, double gpa) {
        this.id   = id;
        this.name = name;
        this.gpa  = gpa;
    }

    // equals() must be consistent with hashCode()!
    // Two objects that are equal() MUST have the same hashCode()
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                  // same reference
        if (o == null || getClass() != o.getClass()) return false; // null/type check
        Student student = (Student) o;
        return Objects.equals(id, student.id);       // equality based on ID
    }

    @Override
    public int hashCode() {
        return Objects.hash(id); // same field(s) as equals!
    }

    // Comparable: defines NATURAL ORDERING (e.g., for TreeSet, Collections.sort)
    @Override
    public int compareTo(Student other) {
        // Sort by GPA descending (highest GPA first)
        return Double.compare(other.gpa, this.gpa);
    }

    @Override
    public String toString() {
        return String.format("Student[%s, %s, GPA=%.2f]", id, name, gpa);
    }

    public static void main(String[] args) {
        Student s1 = new Student("S001", "Kuldeep", 3.9);
        Student s2 = new Student("S001", "Kuldeep", 3.9);
        Student s3 = new Student("S002", "Priya",   3.7);

        System.out.println(s1.equals(s2)); // true (same id)
        System.out.println(s1 == s2);      // false (different objects)

        Set<Student> set = new HashSet<>();
        set.add(s1);
        set.add(s2); // NOT added — equals() and hashCode() say it's a duplicate
        System.out.println(set.size()); // 1

        List<Student> students = new ArrayList<>(Arrays.asList(s1, s3,
            new Student("S003", "Amit", 3.95)));
        Collections.sort(students); // uses compareTo — sorts by GPA desc
        students.forEach(System.out::println);
        // Student[S003, Amit, GPA=3.95]
        // Student[S001, Kuldeep, GPA=3.90]
        // Student[S002, Priya, GPA=3.70]
    }
}

Step 8 — Enums

Enums.javajava
// Enum: a special class for a fixed set of constants
// Enums can have fields, constructors, and methods!
public enum OrderStatus {
    PENDING("Order placed, awaiting processing"),
    PROCESSING("Order is being prepared"),
    SHIPPED("Order has been shipped"),
    DELIVERED("Order delivered successfully"),
    CANCELLED("Order was cancelled");

    private final String description; // each enum constant has a description

    // Enum constructor (always private)
    OrderStatus(String description) {
        this.description = description;
    }

    public String getDescription() { return description; }

    public boolean isTerminal() {
        return this == DELIVERED || this == CANCELLED;
    }
}

class EnumDemo {
    public static void main(String[] args) {
        OrderStatus status = OrderStatus.SHIPPED;

        System.out.println(status);                  // SHIPPED
        System.out.println(status.name());           // SHIPPED (string)
        System.out.println(status.ordinal());        // 2 (0-indexed position)
        System.out.println(status.getDescription()); // Order has been shipped
        System.out.println(status.isTerminal());     // false

        // Enums work beautifully with switch
        switch (status) {
            case PENDING    -> System.out.println("Waiting...");
            case SHIPPED    -> System.out.println("On the way!");
            case DELIVERED  -> System.out.println("Enjoy!");
            default         -> System.out.println("Other: " + status);
        }

        // Iterate all enum values
        for (OrderStatus s : OrderStatus.values()) {
            System.out.printf("%-12s -> %s%n", s, s.getDescription());
        }

        // Convert string to enum
        OrderStatus fromString = OrderStatus.valueOf("PENDING");
        System.out.println(fromString.getDescription()); // Order placed...
    }
}

Step 9 — Inner Classes and Anonymous Classes

InnerClasses.javajava
public class Outer {
    private int value = 10;

    // --- Regular inner class: has access to outer class members ---
    class Inner {
        void display() {
            System.out.println("Outer.value = " + value); // accesses outer's private!
        }
    }

    // --- Static nested class: does NOT hold a reference to outer instance ---
    // Use this when the nested class doesn't need outer class members
    static class StaticNested {
        void display() {
            // System.out.println(value); // COMPILE ERROR: no access to outer instance
            System.out.println("I am a static nested class");
        }
    }

    // --- Local class: defined inside a method ---
    void methodWithLocalClass() {
        class Local {
            void greet() { System.out.println("Local class says hi!"); }
        }
        new Local().greet();
    }

    public static void main(String[] args) {
        Outer outer = new Outer();

        // Inner class requires outer instance
        Outer.Inner inner = outer.new Inner();
        inner.display(); // Outer.value = 10

        // Static nested class: no outer instance needed
        Outer.StaticNested nested = new Outer.StaticNested();
        nested.display();

        outer.methodWithLocalClass();

        // --- Anonymous class: one-time implementation of interface/abstract class ---
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("Running anonymously!");
            }
        };
        r.run();

        // Modern equivalent: lambda expression (Java 8+)
        Runnable lambda = () -> System.out.println("Running with lambda!");
        lambda.run();
    }
}