Updated: July 18, 2025

Object-Oriented Programming (OOP) is a programming paradigm built around the concept of “objects,” which can contain data and code: data in the form of fields (often known as attributes or properties), and code in the form of procedures (often known as methods). Java, a widely-used programming language, is inherently object-oriented, making it essential for developers to understand OOP concepts thoroughly to write efficient, modular, and maintainable code.

In this article, we will explore the fundamental concepts of Object-Oriented Programming in Java, including classes and objects, inheritance, encapsulation, polymorphism, and abstraction. We will also look at how these concepts are applied in practical scenarios.

1. Classes and Objects

Classes: The Blueprints of Objects

A class in Java acts as a blueprint or template for creating objects. It defines a datatype by bundling data and methods that operate on that data into one single unit.

Example:

“`java
public class Car {
// Fields (Attributes)
String model;
int year;
String color;

// Method
void displayDetails() {
    System.out.println("Model: " + model);
    System.out.println("Year: " + year);
    System.out.println("Color: " + color);
}

}
“`

Objects: Instances of Classes

An object is an instance of a class. When you create an object from a class, memory is allocated to store variables defined in the class.

“`java
public class Main {
public static void main(String[] args) {
Car myCar = new Car(); // Creating an object
myCar.model = “Toyota Camry”;
myCar.year = 2020;
myCar.color = “Red”;

    myCar.displayDetails();
}

}
“`

Here myCar is an object of the Car class. You can create multiple objects from a class, each with its own state.

2. Encapsulation

Encapsulation is one of the core principles of OOP, which restricts direct access to some of an object’s components. This means that object data (fields) should not be directly accessible from outside the class; instead, access is controlled through methods called getters and setters.

Why Use Encapsulation?

  • Protects object integrity by preventing outsiders from setting invalid values.
  • Hides internal implementation details.
  • Makes the code more maintainable and flexible.

How to Implement Encapsulation in Java?

  • Declare fields as private.
  • Provide public getter and setter methods to access and update the value of private fields.

Example:

“`java
public class Employee {
private String name;
private double salary;

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

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

// Getter method for salary
public double getSalary() {
    return salary;
}

// Setter method for salary with validation
public void setSalary(double salary) {
    if(salary > 0) {
        this.salary = salary;
    } else {
        System.out.println("Invalid salary amount");
    }
}

}
“`

Here, you cannot modify or view the employee’s name or salary directly; you must use getter and setter methods. This protects the internal state of an Employee object.

3. Inheritance

Inheritance is a mechanism wherein a new class inherits the properties (fields) and behaviors (methods) of an existing class. The new class is called the subclass or derived class, and the existing class is referred to as the superclass or base class.

Benefits of Inheritance

  • Promotes code reusability.
  • Supports hierarchical classifications.
  • Enables polymorphism (covered later).

Syntax in Java

Java uses the keyword extends to inherit a class:

“`java
public class Animal {
void eat() {
System.out.println(“This animal eats food.”);
}
}

public class Dog extends Animal {
void bark() {
System.out.println(“The dog barks.”);
}
}
“`

Example Usage

java
public class TestInheritance {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // Inherited method
dog.bark(); // Own method
}
}

Output:
This animal eats food.
The dog barks.

Here, Dog inherits from Animal, so it can use both its own method (bark()) and the inherited method (eat()).

Types of Inheritance

Java supports:

  • Single inheritance – one subclass inherits from one superclass.
  • Multilevel inheritance – subclass inherits from subclass forming a chain.
  • Hierarchical inheritance – multiple subclasses inherit from one superclass.

Note: Java does not support multiple inheritance with classes directly due to ambiguity issues but supports it through interfaces.

4. Polymorphism

Polymorphism means “many forms.” In Java OOP, polymorphism allows objects to be treated as instances of their parent class rather than their actual class. The two types are:

  1. Compile-time Polymorphism (Method Overloading)
  2. Runtime Polymorphism (Method Overriding)

Method Overloading (Compile-time Polymorphism)

Method overloading allows a class to have multiple methods with the same name but different parameter lists (type or number).

“`java
public class Calculator {

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

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

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

}
“`

The compiler determines which method to invoke based on input parameters at compile time.

Method Overriding (Runtime Polymorphism)

Method overriding allows a subclass to provide its specific implementation of a method already defined in its superclass.

“`java
class Animal {
void sound() {
System.out.println(“Animal makes sound”);
}
}

class Cat extends Animal {
@Override
void sound() {
System.out.println(“Cat meows”);
}
}

class Dog extends Animal {
@Override
void sound() {
System.out.println(“Dog barks”);
}
}
“`

When you call sound() on an Animal reference pointing to any subclass object, the overridden version executes at runtime.

“`java
public class TestPolymorphism {
public static void main(String[] args) {
Animal animal;

   animal = new Cat();
   animal.sound();  // Output: Cat meows

   animal = new Dog();
   animal.sound();  // Output: Dog barks

}
}
“`

This behavior demonstrates dynamic method dispatch and runtime polymorphism.

5. Abstraction

Abstraction involves hiding complex implementation details and showing only essential features to the user. It reduces programming complexity and effort.

In Java, abstraction can be achieved using:

  • Abstract classes
  • Interfaces

Abstract Classes

An abstract class cannot be instantiated but can have abstract methods without implementation that subclasses are required to implement.

“`java
abstract class Vehicle {
abstract void start();

void stop() {
System.out.println(“Vehicle stopped.”);
}
}

class Bike extends Vehicle {

@Override
void start() {
System.out.println(“Bike started.”);
}
}
“`

You cannot create an instance of Vehicle, but you can create an instance of Bike.

java
public class TestAbstraction {
public static void main(String[] args) {
Vehicle bike = new Bike();
bike.start(); // Output: Bike started.
bike.stop(); // Output: Vehicle stopped.
}
}

Interfaces

An interface is like a contract that defines abstract methods that implementing classes must override. Interfaces support multiple inheritance since a Java class can implement multiple interfaces.

“`java
interface Drawable {
void draw();
}

interface Printable {
void print();
}

class Graphic implements Drawable, Printable {

@Override
public void draw() {
System.out.println(“Drawing graphic.”);
}

@Override
public void print() {
System.out.println(“Printing graphic.”);
}
}
“`

Interfaces promote loose coupling and increase flexibility in design.

Practical Example Combining OOP Concepts

Let’s design a simple banking system illustrating these concepts:

“`java
// Base Class with Encapsulation and Abstraction
abstract class Account {

private String accountNumber;
private double balance;

public Account(String accountNumber, double initialBalance){
    this.accountNumber = accountNumber;
    this.balance = initialBalance;
}

public String getAccountNumber(){
    return accountNumber;
}

public double getBalance(){
    return balance;
}

public void deposit(double amount){
    if(amount > 0){
        balance += amount;
        System.out.println(amount + " deposited.");
    } else{
        System.out.println("Invalid deposit amount.");
    }
}

abstract void withdraw(double amount);  // Abstract method

}

// Derived Class implementing abstract withdraw()
class SavingsAccount extends Account {

 private double interestRate;

 public SavingsAccount(String accountNumber, double initialBalance, double interestRate){
     super(accountNumber, initialBalance);
     this.interestRate = interestRate;
 }

 @Override
 public void withdraw(double amount){
     if(amount <= getBalance()){
         double newBalance = getBalance() - amount;
         System.out.println(amount + " withdrawn.");
         // Using reflection-like effect since balance is private - we need setter or protected access 
         // but for simplicity let's assume balance is modifiable here via deposit negative amounts 
         // or better we can maintain balance protected or have modifyBalance()
         try{
             java.lang.reflect.Field balanceField = Account.class.getDeclaredField("balance");
             balanceField.setAccessible(true);
             balanceField.set(this,newBalance);
         } catch(Exception e){
             e.printStackTrace();
         }
     } else{
         System.out.println("Insufficient funds!");
     }
 }

 public void addInterest(){
     double interest = getBalance() * interestRate / 100;
     deposit(interest);
     System.out.println("Interest added: " + interest);
 }

}

public class BankSystemDemo {

 public static void main(String[] args){
      SavingsAccount sa = new SavingsAccount("SA12345", 1000.0, 5.0);

      sa.deposit(500);
      sa.withdraw(200);
      sa.addInterest();

      System.out.println("Final Balance: " + sa.getBalance());
 }

}
“`

This example demonstrates:

  • Encapsulation by keeping fields private.
  • Abstraction by declaring an abstract base Account with abstract withdraw().
  • Inheritance with SavingsAccount extending Account.
  • Polymorphism can also be added if other account types override withdraw differently.

Conclusion

Java’s strong adherence to Object-Oriented Programming principles makes it ideal for building scalable and maintainable applications. Understanding classes/objects lays the foundation; encapsulation ensures data safety; inheritance promotes reuse; polymorphism enables flexible behavior; abstraction hides complexity while exposing only what’s necessary.

Mastering these concepts will help developers write clean, efficient Java programs capable of evolving gracefully with changing requirements — essential skills in modern software development.

Whether you’re building simple applications or complex enterprise software systems, leveraging Java’s OOP capabilities is crucial for success.