All Posts

June 11, 2025

4 min read
JavaProgrammingOOPClassesObjectsInheritancePolymorphism

Object-Oriented Programming in Java: Classes, Objects, and Core OOP Principles

Welcome back to the Java Fundamentals series! 👋

Object-Oriented Programming (OOP) is the backbone of Java. Understanding OOP concepts like classes, objects, inheritance, polymorphism, encapsulation, and abstraction is crucial for becoming an effective Java developer.

Classes and Objects

Classes are blueprints or templates that define the structure and behavior of objects. Objects are instances of classes that contain actual data and can perform actions.

Creating a Class

Here's how to create a basic class in Java:

public class Car {
    // Instance variables (attributes)
    private String brand;
    private String model;
    private int year;
    private double price;
    
    // Constructor
    public Car(String brand, String model, int year, double price) {
        this.brand = brand;
        this.model = model;
        this.year = year;
        this.price = price;
    }
    
    // Methods (behaviors)
    public void startEngine() {
        System.out.println(brand + " " + model + " engine started!");
    }
    
    public void displayInfo() {
        System.out.println("Car: " + year + " " + brand + " " + model + 
                          " - $" + price);
    }
    
    // Getters and Setters
    public String getBrand() {
        return brand;
    }
    
    public void setBrand(String brand) {
        this.brand = brand;
    }
    
    public double getPrice() {
        return price;
    }
    
    public void setPrice(double price) {
        if (price > 0) {
            this.price = price;
        }
    }
}

Creating and Using Objects

public class CarDemo {
    public static void main(String[] args) {
        // Creating objects (instances) of the Car class
        Car car1 = new Car("Toyota", "Camry", 2023, 25000.0);
        Car car2 = new Car("Honda", "Civic", 2022, 22000.0);
        
        // Using object methods
        car1.displayInfo();
        car1.startEngine();
        
        car2.displayInfo();
        car2.startEngine();
        
        // Accessing object properties through getters/setters
        System.out.println("Car 1 brand: " + car1.getBrand());
        car2.setPrice(23000.0);
        System.out.println("Updated car 2 price: $" + car2.getPrice());
    }
}

Output:

Car: 2023 Toyota Camry - $25000.0
Toyota Camry engine started!
Car: 2022 Honda Civic - $22000.0
Honda Civic engine started!
Car 1 brand: Toyota
Updated car 2 price: $23000.0

Constructors

Constructors are special methods that initialize objects when they are created. They have the same name as the class and no return type.

Types of Constructors

1. Default Constructor

public class Student {
    private String name;
    private int age;
    
    // Default constructor
    public Student() {
        this.name = "Unknown";
        this.age = 0;
    }
    
    public void displayInfo() {
        System.out.println("Student: " + name + ", Age: " + age);
    }
}

2. Parameterized Constructor

public class Student {
    private String name;
    private int age;
    
    // Parameterized constructor
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // Constructor overloading
    public Student(String name) {
        this.name = name;
        this.age = 18; // Default age
    }
    
    public void displayInfo() {
        System.out.println("Student: " + name + ", Age: " + age);
    }
}

3. Copy Constructor (Manual Implementation)

public class Student {
    private String name;
    private int age;
    
    // Regular constructor
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // Copy constructor
    public Student(Student other) {
        this.name = other.name;
        this.age = other.age;
    }
    
    public void displayInfo() {
        System.out.println("Student: " + name + ", Age: " + age);
    }
}

Constructor Example Usage

public class ConstructorDemo {
    public static void main(String[] args) {
        // Using different constructors
        Student student1 = new Student("Alice", 20);
        Student student2 = new Student("Bob");
        Student student3 = new Student(student1); // Copy constructor
        
        student1.displayInfo(); // Student: Alice, Age: 20
        student2.displayInfo(); // Student: Bob, Age: 18
        student3.displayInfo(); // Student: Alice, Age: 20
    }
}

Inheritance

Inheritance allows a class to inherit properties and methods from another class. It promotes code reusability and establishes an "is-a" relationship.

Basic Inheritance Example

// Parent class (Superclass)
public class Vehicle {
    protected String brand;
    protected int year;
    protected double price;
    
    public Vehicle(String brand, int year, double price) {
        this.brand = brand;
        this.year = year;
        this.price = price;
    }
    
    public void start() {
        System.out.println(brand + " vehicle started.");
    }
    
    public void displayInfo() {
        System.out.println("Vehicle: " + year + " " + brand + " - $" + price);
    }
}

// Child class (Subclass)
public class Motorcycle extends Vehicle {
    private String engineType;
    
    public Motorcycle(String brand, int year, double price, String engineType) {
        super(brand, year, price); // Call parent constructor
        this.engineType = engineType;
    }
    
    // Method overriding
    @Override
    public void start() {
        System.out.println(brand + " motorcycle engine revved up!");
    }
    
    // Additional method specific to Motorcycle
    public void wheelie() {
        System.out.println(brand + " motorcycle doing a wheelie!");
    }
    
    @Override
    public void displayInfo() {
        super.displayInfo(); // Call parent method
        System.out.println("Engine Type: " + engineType);
    }
}

Multilevel Inheritance

// Grandparent class
public class Vehicle {
    protected String brand;
    
    public Vehicle(String brand) {
        this.brand = brand;
    }
    
    public void move() {
        System.out.println(brand + " is moving.");
    }
}

// Parent class
public class Car extends Vehicle {
    protected int doors;
    
    public Car(String brand, int doors) {
        super(brand);
        this.doors = doors;
    }
    
    public void honk() {
        System.out.println(brand + " car is honking!");
    }
}

// Child class
public class SportsCar extends Car {
    private int topSpeed;
    
    public SportsCar(String brand, int doors, int topSpeed) {
        super(brand, doors);
        this.topSpeed = topSpeed;
    }
    
    public void turboBoost() {
        System.out.println(brand + " sports car activated turbo boost! Top speed: " + topSpeed + " mph");
    }
}

Inheritance Usage Example

public class InheritanceDemo {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle("Generic", 2020, 15000);
        Motorcycle motorcycle = new Motorcycle("Harley", 2023, 18000, "V-Twin");
        SportsCar sportsCar = new SportsCar("Ferrari", 2, 200);
        
        // Using inherited and overridden methods
        vehicle.start();           // Generic vehicle started.
        motorcycle.start();        // Harley motorcycle engine revved up!
        motorcycle.wheelie();      // Harley motorcycle doing a wheelie!
        
        sportsCar.move();          // Ferrari is moving.
        sportsCar.honk();          // Ferrari car is honking!
        sportsCar.turboBoost();    // Ferrari sports car activated turbo boost! Top speed: 200 mph
        
        // Polymorphism in action
        Vehicle[] vehicles = {vehicle, motorcycle};
        for (Vehicle v : vehicles) {
            v.start(); // Calls appropriate start() method
        }
    }
}

Polymorphism

Polymorphism allows objects of different types to be treated as objects of a common base type. It enables a single interface to represent different underlying forms (data types).

Method Overriding (Runtime Polymorphism)

public class Animal {
    public void makeSound() {
        System.out.println("The animal makes a sound");
    }
    
    public void eat() {
        System.out.println("The animal eats food");
    }
}

public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("The dog barks: Woof! Woof!");
    }
    
    @Override
    public void eat() {
        System.out.println("The dog eats dog food");
    }
    
    // Dog-specific method
    public void wagTail() {
        System.out.println("The dog wags its tail");
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("The cat meows: Meow! Meow!");
    }
    
    @Override
    public void eat() {
        System.out.println("The cat eats cat food");
    }
    
    // Cat-specific method
    public void purr() {
        System.out.println("The cat purrs");
    }
}

Method Overloading (Compile-time Polymorphism)

public class Calculator {
    // Method overloading - same method name, different parameters
    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;
    }
}

Polymorphism Example Usage

public class PolymorphismDemo {
    public static void main(String[] args) {
        // Runtime polymorphism
        Animal[] animals = {
            new Dog(),
            new Cat(),
            new Animal()
        };
        
        // Same method call, different behaviors
        for (Animal animal : animals) {
            animal.makeSound(); // Calls appropriate overridden method
            animal.eat();
            System.out.println("---");
        }
        
        // Compile-time polymorphism
        Calculator calc = new Calculator();
        System.out.println("Add integers: " + calc.add(5, 3));
        System.out.println("Add doubles: " + calc.add(5.5, 3.2));
        System.out.println("Add three integers: " + calc.add(1, 2, 3));
        System.out.println("Add strings: " + calc.add("Hello, ", "World!"));
    }
}

Output:

The dog barks: Woof! Woof!
The dog eats dog food
---
The cat meows: Meow! Meow!
The cat eats cat food
---
The animal makes a sound
The animal eats food
---
Add integers: 8
Add doubles: 8.7
Add three integers: 6
Add strings: Hello, World!

Encapsulation

Encapsulation is the practice of hiding internal implementation details and exposing only necessary information through public methods. It's achieved using access modifiers and getter/setter methods.

Access Modifiers

ModifierClassPackageSubclassWorld
public✓✓✓✓
protected✓✓✓✗
default✓✓✗✗
private✓✗✗✗

Encapsulation Example

public class BankAccount {
    // Private fields (encapsulated)
    private String accountNumber;
    private String accountHolder;
    private double balance;
    private String pin;
    
    // Constructor
    public BankAccount(String accountNumber, String accountHolder, String pin) {
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
        this.pin = pin;
        this.balance = 0.0;
    }
    
    // Public methods to interact with private data
    public boolean deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Deposited: $" + amount);
            return true;
        }
        System.out.println("Invalid deposit amount");
        return false;
    }
    
    public boolean withdraw(double amount, String inputPin) {
        if (!validatePin(inputPin)) {
            System.out.println("Invalid PIN");
            return false;
        }
        
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Withdrawn: $" + amount);
            return true;
        }
        System.out.println("Invalid withdrawal amount or insufficient funds");
        return false;
    }
    
    public double getBalance(String inputPin) {
        if (validatePin(inputPin)) {
            return balance;
        }
        System.out.println("Invalid PIN");
        return -1;
    }
    
    // Private helper method
    private boolean validatePin(String inputPin) {
        return this.pin.equals(inputPin);
    }
    
    // Getters for safe access to some private data
    public String getAccountNumber() {
        return accountNumber;
    }
    
    public String getAccountHolder() {
        return accountHolder;
    }
    
    // Setter with validation
    public boolean changePin(String oldPin, String newPin) {
        if (validatePin(oldPin) && newPin.length() >= 4) {
            this.pin = newPin;
            System.out.println("PIN changed successfully");
            return true;
        }
        System.out.println("PIN change failed");
        return false;
    }
}

Using the Encapsulated Class

public class EncapsulationDemo {
    public static void main(String[] args) {
        BankAccount account = new BankAccount("12345", "John Doe", "1234");
        
        // Accessing public methods (safe interface)
        account.deposit(1000);
        account.deposit(500);
        
        System.out.println("Balance: $" + account.getBalance("1234"));
        
        account.withdraw(200, "1234");
        account.withdraw(200, "wrong"); // Wrong PIN
        
        System.out.println("Final Balance: $" + account.getBalance("1234"));
        
        // Cannot directly access private fields
        // System.out.println(account.balance); // Compilation error
        // account.pin = "0000"; // Compilation error
        
        // Safe access through public methods
        System.out.println("Account holder: " + account.getAccountHolder());
        account.changePin("1234", "5678");
    }
}

Abstraction

Abstraction hides complex implementation details and shows only essential features. It's achieved through abstract classes and interfaces.

Abstract Classes

// Abstract class
public abstract class Shape {
    protected String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    // Abstract method (must be implemented by subclasses)
    public abstract double calculateArea();
    public abstract double calculatePerimeter();
    
    // Concrete method (can be inherited as-is)
    public void displayColor() {
        System.out.println("Color: " + color);
    }
    
    // Concrete method with common behavior
    public void displayInfo() {
        displayColor();
        System.out.println("Area: " + calculateArea());
        System.out.println("Perimeter: " + calculatePerimeter());
    }
}

// Concrete subclass
public class Circle extends Shape {
    private double radius;
    
    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }
    
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
    
    @Override
    public double calculatePerimeter() {
        return 2 * Math.PI * radius;
    }
}

// Another concrete subclass
public class Rectangle extends Shape {
    private double length;
    private double width;
    
    public Rectangle(String color, double length, double width) {
        super(color);
        this.length = length;
        this.width = width;
    }
    
    @Override
    public double calculateArea() {
        return length * width;
    }
    
    @Override
    public double calculatePerimeter() {
        return 2 * (length + width);
    }
}

Interfaces

// Interface
public interface Drawable {
    // All methods in interface are public and abstract by default
    void draw();
    void erase();
    
    // Default method (Java 8+)
    default void display() {
        System.out.println("Displaying the drawable object");
    }
    
    // Static method (Java 8+)
    static void printInfo() {
        System.out.println("This is a drawable interface");
    }
}

// Another interface
public interface Resizable {
    void resize(double factor);
    boolean isResizable();
}

// Class implementing multiple interfaces
public class GraphicsShape extends Shape implements Drawable, Resizable {
    private boolean resizable;
    
    public GraphicsShape(String color, boolean resizable) {
        super(color);
        this.resizable = resizable;
    }
    
    @Override
    public double calculateArea() {
        return 0; // Generic implementation
    }
    
    @Override
    public double calculatePerimeter() {
        return 0; // Generic implementation
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing a " + color + " graphics shape");
    }
    
    @Override
    public void erase() {
        System.out.println("Erasing the graphics shape");
    }
    
    @Override
    public void resize(double factor) {
        if (resizable) {
            System.out.println("Resizing shape by factor: " + factor);
        } else {
            System.out.println("Shape is not resizable");
        }
    }
    
    @Override
    public boolean isResizable() {
        return resizable;
    }
}

Abstraction Example Usage

public class AbstractionDemo {
    public static void main(String[] args) {
        // Using abstract class through concrete implementations
        Shape circle = new Circle("Red", 5.0);
        Shape rectangle = new Rectangle("Blue", 4.0, 6.0);
        
        circle.displayInfo();
        System.out.println("---");
        rectangle.displayInfo();
        System.out.println("---");
        
        // Using interfaces
        GraphicsShape gShape = new GraphicsShape("Green", true);
        gShape.draw();
        gShape.display(); // Default method from interface
        gShape.resize(1.5);
        gShape.erase();
        
        // Static method from interface
        Drawable.printInfo();
        
        // Cannot instantiate abstract class or interface
        // Shape shape = new Shape("Color"); // Compilation error
        // Drawable drawable = new Drawable(); // Compilation error
    }
}

Key Java Keywords and Concepts

The this Keyword

The this keyword refers to the current object instance.

public class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name; // Distinguish between parameter and instance variable
        this.age = age;
    }
    
    public Person setName(String name) {
        this.name = name;
        return this; // Method chaining
    }
    
    public Person setAge(int age) {
        this.age = age;
        return this; // Method chaining
    }
    
    public void displayInfo() {
        System.out.println("Name: " + this.name + ", Age: " + this.age);
    }
    
    public void compareAge(Person other) {
        if (this.age > other.age) {
            System.out.println(this.name + " is older than " + other.name);
        } else if (this.age < other.age) {
            System.out.println(this.name + " is younger than " + other.name);
        } else {
            System.out.println(this.name + " and " + other.name + " are the same age");
        }
    }
}

The super Keyword

The super keyword refers to the parent class.

public class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public void eat() {
        System.out.println(name + " is eating");
    }
}

public class Dog extends Animal {
    private String breed;
    
    public Dog(String name, String breed) {
        super(name); // Call parent constructor
        this.breed = breed;
    }
    
    @Override
    public void eat() {
        super.eat(); // Call parent method
        System.out.println("The " + breed + " dog is eating dog food");
    }
}

The final Keyword

The final keyword can be used with variables, methods, and classes.

// Final class - cannot be extended
public final class MathConstants {
    // Final variables - constants
    public static final double PI = 3.14159;
    public static final int MAX_VALUE = 100;
    
    // Final method - cannot be overridden
    public final void displayConstants() {
        System.out.println("PI: " + PI + ", MAX_VALUE: " + MAX_VALUE);
    }
}

public class FinalExample {
    public void demonstrateFinal() {
        final int localConstant = 42; // Final local variable
        // localConstant = 50; // Compilation error
        
        final List<String> list = new ArrayList<>();
        list.add("Hello"); // OK - can modify contents
        // list = new ArrayList<>(); // Compilation error - cannot reassign
    }
}

The static Keyword

The static keyword creates class-level members that belong to the class rather than instances.

public class Counter {
    private static int totalCount = 0; // Class variable
    private int instanceCount = 0;     // Instance variable
    
    public Counter() {
        totalCount++;    // Increment class variable
        instanceCount++; // Increment instance variable
    }
    
    // Static method
    public static int getTotalCount() {
        return totalCount;
        // return instanceCount; // Compilation error - cannot access instance variable
    }
    
    // Instance method
    public int getInstanceCount() {
        return instanceCount;
    }
    
    // Static block - executed when class is first loaded
    static {
        System.out.println("Counter class loaded");
        totalCount = 0;
    }
}

Best Practices for OOP in Java

1. Follow Naming Conventions

  • Use PascalCase for class names: BankAccount, StudentManager
  • Use camelCase for method and variable names: calculateTax(), firstName
  • Use UPPER_SNAKE_CASE for constants: MAX_SIZE, DEFAULT_VALUE

2. Encapsulation Guidelines

  • Make fields private unless there's a specific reason not to
  • Provide public getter/setter methods for controlled access
  • Validate input in setter methods

3. Inheritance Best Practices

  • Use inheritance for true "is-a" relationships
  • Prefer composition over inheritance when possible
  • Make classes final if they shouldn't be extended

4. Interface Design

  • Keep interfaces focused and cohesive
  • Use interfaces to define contracts
  • Prefer interfaces over abstract classes for multiple inheritance

5. Method Design

  • Keep methods small and focused on a single responsibility
  • Use meaningful method names that describe what they do
  • Limit the number of parameters (consider using objects for multiple parameters)

Conclusion

Object-Oriented Programming is fundamental to Java development. Understanding these core concepts will make you a more effective Java developer:

  • Classes and Objects: The building blocks of OOP
  • Constructors: Initialize objects properly
  • Inheritance: Reuse code and create hierarchies
  • Polymorphism: Write flexible, maintainable code
  • Encapsulation: Protect data and provide controlled access
  • Abstraction: Hide complexity and focus on essential features

Happy coding! 💻