OOP Concepts in Java

1. What is OOP?

Object-Oriented Programming (OOP) is a programming paradigm that organises software design around data (objects) rather than functions and logic. Java is fundamentally an object-oriented language — almost everything in Java is an object.

OOP is built on four core pillars:

Pillar One-Line Definition Key Java Construct
Encapsulation Bundling data and methods; hiding internal details private fields + getters/setters
Inheritance A class acquires properties of another class extends
Polymorphism One interface, many forms Overloading & @Override
Abstraction Hiding complexity, exposing only essentials abstract class / interface

Why OOP? OOP promotes code reusability, modularity, maintainability, and makes it easier to model real-world entities in software.

2. Classes and Objects

A class is a blueprint or template that defines the state (fields) and behaviour (methods) that objects of its type will have. An object is a concrete instance of a class, created using the new keyword.

// Class = blueprint
public class Car {
    // Fields (state)
    String brand;
    String model;
    int year;
    double speed;

    // Method (behaviour)
    public void accelerate(double amount) {
        speed += amount;
        System.out.println(brand + " is now going at " + speed + " km/h");
    }

    public void brake(double amount) {
        speed = Math.max(0, speed - amount);
        System.out.println(brand + " slowed down to " + speed + " km/h");
    }

    public void displayInfo() {
        System.out.println(year + " " + brand + " " + model);
    }
}

// Main class to use the Car object
public class Main {
    public static void main(String[] args) {
        // Creating objects (instances) of Car
        Car car1 = new Car();   // object 1
        car1.brand = "Toyota";
        car1.model = "Corolla";
        car1.year = 2022;

        Car car2 = new Car();   // object 2
        car2.brand = "Honda";
        car2.model = "Civic";
        car2.year = 2023;

        car1.displayInfo();     // 2022 Toyota Corolla
        car1.accelerate(60);    // Toyota is now going at 60.0 km/h
        car2.displayInfo();     // 2023 Honda Civic
    }
}
2022 Toyota Corolla
Toyota is now going at 60.0 km/h
2023 Honda Civic

Memory: Each object gets its own copy of instance fields but shares class-level (static) fields and methods.

3. Constructors

A constructor is a special method that is automatically called when an object is created with new. It has the same name as the class and no return type.

Default Constructor

If you do not define any constructor, Java provides a no-argument default constructor automatically. Once you define any constructor, the default is no longer provided.

Parameterized Constructor

public class Car {
    String brand;
    String model;
    int year;

    // Default constructor
    public Car() {
        brand = "Unknown";
        model = "Unknown";
        year = 0;
    }

    // Parameterized constructor
    public Car(String brand, String model, int year) {
        this.brand = brand;   // 'this' distinguishes field from parameter
        this.model = model;
        this.year  = year;
    }

    // Copy constructor
    public Car(Car other) {
        this.brand = other.brand;
        this.model = other.model;
        this.year  = other.year;
    }

    // Constructor chaining with this()
    public Car(String brand) {
        this(brand, "Generic", 2024);  // calls parameterized constructor
    }

    public void display() {
        System.out.println(year + " " + brand + " " + model);
    }
}

public class Main {
    public static void main(String[] args) {
        Car c1 = new Car();                        // default
        Car c2 = new Car("Ford", "Mustang", 2023); // parameterized
        Car c3 = new Car(c2);                      // copy
        Car c4 = new Car("BMW");                   // chained

        c1.display();  // 0 Unknown Unknown
        c2.display();  // 2023 Ford Mustang
        c3.display();  // 2023 Ford Mustang
        c4.display();  // 2024 BMW Generic
    }
}
0 Unknown Unknown
2023 Ford Mustang
2023 Ford Mustang
2024 BMW Generic

Note: this() must be the first statement in a constructor. You cannot call both this() and super() in the same constructor.

4. The this Keyword

this is a reference to the current object — the object whose method or constructor is being called. It has three main uses:

public class Person {
    String name;
    int age;

    // Use 1: Disambiguate fields from parameters
    public Person(String name, int age) {
        this.name = name;  // 'this.name' = field, 'name' = parameter
        this.age  = age;
    }

    // Use 2: Call another constructor (constructor chaining)
    public Person(String name) {
        this(name, 0);     // delegates to Person(String, int)
    }

    // Use 3: Pass current object as argument
    public void register(Registry r) {
        r.add(this);       // passes the current Person object
    }

    public String getInfo() {
        return this.name + ", age " + this.age;
    }
}

class Registry {
    public void add(Person p) {
        System.out.println("Registered: " + p.getInfo());
    }
}

public class Main {
    public static void main(String[] args) {
        Person p = new Person("Alice", 30);
        System.out.println(p.getInfo());   // Alice, age 30

        Registry reg = new Registry();
        p.register(reg);                   // Registered: Alice, age 30
    }
}
Alice, age 30
Registered: Alice, age 30

5. Encapsulation

Encapsulation means wrapping data (fields) and code (methods) together in a single unit and restricting direct access to the fields from outside the class. This is achieved by declaring fields private and providing public getter and setter methods.

public class BankAccount {
    private String owner;
    private double balance;   // cannot be accessed directly from outside

    public BankAccount(String owner, double initialBalance) {
        this.owner   = owner;
        this.balance = (initialBalance >= 0) ? initialBalance : 0;
    }

    // Getter
    public double getBalance() {
        return balance;
    }

    // Setter with validation — this is the power of encapsulation
    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit amount must be positive.");
        }
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount <= 0 || amount > balance) {
            throw new IllegalArgumentException("Invalid withdrawal amount.");
        }
        balance -= amount;
    }

    public String getOwner() { return owner; }

    @Override
    public String toString() {
        return owner + "'s account: $" + String.format("%.2f", balance);
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount acc = new BankAccount("Alice", 1000.0);
        // acc.balance = -500;  // COMPILE ERROR — field is private
        acc.deposit(250.0);
        acc.withdraw(100.0);
        System.out.println(acc);            // Alice's account: $1150.00
        System.out.println(acc.getBalance()); // 1150.0
    }
}
Alice's account: $1150.00
1150.0

Benefits of Encapsulation: Data validation, flexibility to change internal implementation without affecting callers, improved security, and easier unit testing.

6. Inheritance

Inheritance allows a class (child/subclass) to acquire properties and methods of another class (parent/superclass) using the extends keyword. Java supports single, multilevel, and hierarchical inheritance (but NOT multiple class inheritance).

// Parent (superclass)
public class Vehicle {
    String brand;
    int speed;

    public Vehicle(String brand, int speed) {
        this.brand = brand;
        this.speed = speed;
    }

    public void move() {
        System.out.println(brand + " is moving at " + speed + " km/h");
    }
}

// Single Inheritance: Car extends Vehicle
public class Car extends Vehicle {
    int doors;

    public Car(String brand, int speed, int doors) {
        super(brand, speed);   // call parent constructor
        this.doors = doors;
    }

    public void honk() {
        System.out.println(brand + " goes Beep!");
    }
}

// Multilevel Inheritance: ElectricCar extends Car
public class ElectricCar extends Car {
    int batteryCapacity;

    public ElectricCar(String brand, int speed, int doors, int battery) {
        super(brand, speed, doors);
        this.batteryCapacity = battery;
    }

    public void chargeBattery() {
        System.out.println(brand + " is charging (" + batteryCapacity + " kWh battery)");
    }
}

// Hierarchical Inheritance: Truck also extends Vehicle
public class Truck extends Vehicle {
    double loadCapacity;

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

    public void loadCargo() {
        System.out.println(brand + " loaded " + loadCapacity + " tons");
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car("Toyota", 120, 4);
        car.move();    // inherited from Vehicle
        car.honk();

        ElectricCar ev = new ElectricCar("Tesla", 200, 4, 100);
        ev.move();     // inherited from Vehicle via Car
        ev.honk();     // inherited from Car
        ev.chargeBattery();

        Truck truck = new Truck("Volvo", 80, 20.5);
        truck.move();
        truck.loadCargo();
    }
}
Toyota is moving at 120 km/h
Toyota goes Beep!
Tesla is moving at 200 km/h
Tesla goes Beep!
Tesla is charging (100 kWh battery)
Volvo is moving at 80 km/h
Volvo loaded 20.5 tons

Java does NOT support multiple class inheritance (a class cannot extend two classes at once) to avoid the diamond problem. Use interfaces for multiple inheritance of type.

7. Polymorphism

Polymorphism means "many forms". In Java it comes in two flavours:

Compile-time Polymorphism — Method Overloading

Same method name, different parameter lists. Resolved at compile time.

public class Calculator {
    // Overloaded add methods
    public int add(int a, int b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }

    public int add(int a, int b, int c) {
        return a + b + c;
    }

    public String add(String a, String b) {
        return a + b;  // String concatenation
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(2, 3));          // 5
        System.out.println(calc.add(2.5, 3.5));      // 6.0
        System.out.println(calc.add(1, 2, 3));       // 6
        System.out.println(calc.add("Hello, ", "World!")); // Hello, World!
    }
}
5
6.0
6
Hello, World!

Runtime Polymorphism — Method Overriding

A subclass provides its own implementation of a method defined in the parent. Resolved at runtime via dynamic dispatch.

public class Animal {
    public void sound() {
        System.out.println("Some animal makes a sound");
    }
}

public class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("Dog says: Woof!");
    }
}

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

public class Cow extends Animal {
    @Override
    public void sound() {
        System.out.println("Cow says: Moo!");
    }
}

public class Main {
    public static void main(String[] args) {
        // Upcasting: parent reference holds child object
        Animal a1 = new Dog();
        Animal a2 = new Cat();
        Animal a3 = new Cow();

        // Runtime decides which sound() to call
        a1.sound();   // Dog says: Woof!
        a2.sound();   // Cat says: Meow!
        a3.sound();   // Cow says: Moo!

        // Polymorphic array
        Animal[] animals = { new Dog(), new Cat(), new Cow() };
        for (Animal a : animals) {
            a.sound();  // each calls its own overridden version
        }
    }
}
Dog says: Woof!
Cat says: Meow!
Cow says: Moo!
Dog says: Woof!
Cat says: Meow!
Cow says: Moo!

Always use @Override annotation when overriding. It tells the compiler to verify the method actually overrides a parent method, catching typos early.

8. Abstraction

Abstraction means hiding complex implementation details and exposing only the essential features. In Java, abstraction is achieved via abstract classes and interfaces. An abstract class can have both abstract (no body) and concrete (with body) methods.

// Abstract class — cannot be instantiated directly
public abstract class Shape {
    String color;

    public Shape(String color) {
        this.color = color;
    }

    // Abstract method — subclasses MUST implement this
    public abstract double area();
    public abstract double perimeter();

    // Concrete method — shared behaviour
    public void display() {
        System.out.println(color + " shape | Area: " + String.format("%.2f", area())
            + " | Perimeter: " + String.format("%.2f", perimeter()));
    }
}

public class Circle extends Shape {
    double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

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

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

public class Rectangle extends Shape {
    double width, height;

    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }

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

    @Override
    public double perimeter() { return 2 * (width + height); }
}

public class Main {
    public static void main(String[] args) {
        // Shape s = new Shape("red");  // COMPILE ERROR — abstract class
        Shape c = new Circle("Red", 5.0);
        Shape r = new Rectangle("Blue", 4.0, 6.0);

        c.display();  // Red shape | Area: 78.54 | Perimeter: 31.42
        r.display();  // Blue shape | Area: 24.00 | Perimeter: 20.00
    }
}
Red shape | Area: 78.54 | Perimeter: 31.42
Blue shape | Area: 24.00 | Perimeter: 20.00

9. The super Keyword

super refers to the immediate parent class of the current object. It is used to access parent class fields, methods, and constructors.

public class Animal {
    String name;

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

    public void eat() {
        System.out.println(name + " eats food.");
    }

    public String describe() {
        return "I am an animal named " + name;
    }
}

public class Dog extends Animal {
    String breed;

    public Dog(String name, String breed) {
        super(name);   // Call parent constructor — must be first line
        this.breed = breed;
        System.out.println("Dog constructor called. Breed: " + breed);
    }

    @Override
    public void eat() {
        super.eat();   // Call parent's eat() first
        System.out.println(name + " also loves bones!");
    }

    @Override
    public String describe() {
        return super.describe() + ", breed: " + breed;  // reuse parent output
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog("Rex", "Labrador");
        // Output during construction:
        //   Animal constructor called for: Rex
        //   Dog constructor called. Breed: Labrador

        d.eat();
        // Rex eats food.
        // Rex also loves bones!

        System.out.println(d.describe());
        // I am an animal named Rex, breed: Labrador
    }
}
Animal constructor called for: Rex
Dog constructor called. Breed: Labrador
Rex eats food.
Rex also loves bones!
I am an animal named Rex, breed: Labrador

10. The Object Class

Every class in Java implicitly extends java.lang.Object. This means every object automatically inherits a set of methods from Object. The three most important ones to override are:

Method Default Behaviour Why Override?
toString() ClassName@hashcode (e.g., Car@1b6d3586) Return a human-readable description
equals(Object o) Reference equality (==) Define logical equality by field values
hashCode() Memory address-based integer Must be consistent with equals() for use in HashMap/HashSet
import java.util.Objects;

public class Student {
    int id;
    String name;

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

    // Override toString
    @Override
    public String toString() {
        return "Student{id=" + id + ", name='" + name + "'}";
    }

    // Override equals
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;          // same reference
        if (!(o instanceof Student)) return false;
        Student s = (Student) o;
        return id == s.id && Objects.equals(name, s.name);
    }

    // Override hashCode — always override when you override equals
    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}

public class Main {
    public static void main(String[] args) {
        Student s1 = new Student(1, "Alice");
        Student s2 = new Student(1, "Alice");
        Student s3 = new Student(2, "Bob");

        System.out.println(s1);              // Student{id=1, name='Alice'}
        System.out.println(s1.equals(s2));   // true  (same id and name)
        System.out.println(s1.equals(s3));   // false
        System.out.println(s1 == s2);        // false (different references)
        System.out.println(s1.hashCode() == s2.hashCode()); // true
    }
}
Student{id=1, name='Alice'}
true
false
false
true

Contract: If a.equals(b) is true, then a.hashCode() == b.hashCode() must also be true. Violating this breaks HashMap and HashSet.

11. Packages

A package is a namespace that organises related classes and interfaces. Packages prevent name conflicts, control access, and make large projects manageable.

// File: com/myapp/model/Employee.java
package com.myapp.model;   // package declaration — first line of file

public class Employee {
    private int id;
    private String name;
    private double salary;

    public Employee(int id, String name, double salary) {
        this.id     = id;
        this.name   = name;
        this.salary = salary;
    }

    public int    getId()     { return id; }
    public String getName()   { return name; }
    public double getSalary() { return salary; }

    @Override
    public String toString() {
        return "Employee[" + id + ", " + name + ", $" + salary + "]";
    }
}

// File: com/myapp/service/PayrollService.java
package com.myapp.service;

import com.myapp.model.Employee;  // import specific class
// import com.myapp.model.*;      // import all classes from package

public class PayrollService {
    public double calculateBonus(Employee emp) {
        return emp.getSalary() * 0.10;
    }
}

// File: com/myapp/Main.java
package com.myapp;

import com.myapp.model.Employee;
import com.myapp.service.PayrollService;

public class Main {
    public static void main(String[] args) {
        Employee e  = new Employee(101, "Alice", 75000);
        PayrollService ps = new PayrollService();

        System.out.println(e);
        System.out.println("Bonus: $" + ps.calculateBonus(e));
    }
}
Employee[101, Alice, $75000.0]
Bonus: $7500.0

Naming Convention: Package names are all lowercase and typically follow the reverse domain name convention: com.companyname.projectname.module. Java's built-in packages start with java. or javax..

Abstract Class vs Interface

This distinction is one of the most commonly tested Java concepts.

Feature Abstract Class Interface
Instantiation Cannot be instantiated Cannot be instantiated
Methods Abstract + concrete methods Abstract + default + static (Java 8+)
Fields Any type (instance variables) Only public static final constants
Constructors Yes — can have constructors No constructors
Inheritance Single inheritance only (extends) Multiple interfaces (implements)
Access modifiers Any (public, protected, private) Methods are public by default
Use when Classes share common code/state Defining a contract/capability
Keyword extends implements

Continue learning