Java, one of the most popular programming languages, is built around the principles of Object-Oriented Programming (OOP). Understanding these principles is important for developers to write clean, efficient, and maintainable code. In this guide, we’ll explore OOP concepts with real-world examples and practical code snippets.
Understanding Classes and Objects in Java: Building Blocks of OOP
A class is a blueprint that defines the attributes (fields) and behaviors (methods) of objects.
An object is an instance of a class, representing real-world entities in your application.
Classes and objects help in modularizing and organizing your code, making it reusable and easier to maintain.
Think of a Car as a class. Each specific car (Eg:- a red Toyota) is an object.

class Car {
String brand;
String color;
void drive() {
System.out.println(brand + " is driving.");
}
}
public class Main {
public static void main(String[] args) {
Car car1 = new Car();
car1.brand = "Toyota";
car1.color = "Red";
Car car2 = new Car();
car2.brand = "Honda";
car2.color = "Blue";
car1.drive(); // Output: Toyota is driving.
car2.drive(); // Output: Honda is driving.
}
}

Inheritance: Creating Powerful and Reusable Code Hierarchies
Inheritance allows a class (child) to derive properties and methods from another class (parent). This promotes code reuse and a hierarchical structure.
A Vehicle can be a base class for more specific types like Truck or Bike. The base class provides common functionality (like start()
), while the subclasses extend or override this behavior.
class Vehicle {
String fuelType;
void start() {
System.out.println("Vehicle is starting...");
}
}
class Truck extends Vehicle {
int loadCapacity;
void loadGoods() {
System.out.println("Truck is loading goods...");
}
}
public class Main {
public static void main(String[] args) {
Truck truck = new Truck();
truck.fuelType = "Diesel";
truck.start(); // Output: Vehicle is starting...
truck.loadGoods(); // Output: Truck is loading goods...
}
}
This helps to :
- Reduces redundancy: Write common methods in the parent class.
- Simplifies code: Reuse methods without redefining them.

Encapsulation: Protecting Data and Implementing Access Modifiers
Encapsulation is the practice of keeping sensitive data hidden from outside interference. You achieve this by marking fields as private and exposing controlled access through public methods (getters and setters).
Why is Encapsulation Useful?
- Data Protection: Prevents unauthorized access.
- Code Flexibility: Enables changes in the internal logic without affecting external code.
Consider an ATM. While you interact with it to deposit money, the internal mechanisms are hidden, ensuring security.
class BankAccount {
private double balance;
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("Deposited: " + amount);
} else {
System.out.println("Invalid deposit amount.");
}
}
public double getBalance() {
return balance;
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount();
account.deposit(500);
System.out.println("Current Balance: " + account.getBalance());
}
}

Polymorphism
Polymorphism allows a single interface or method to represent different types of behavior. It can be achieved through method overriding (runtime polymorphism) and method overloading (compile-time polymorphism).
A smartphone can function as a camera, a phone, or a gaming device depending on how it is used.
// Abstract base class
abstract class Device {
public abstract void use();
}
// Subclasses for different functionalities of a smartphone
class CameraFunction extends Device {
@Override
public void use() {
System.out.println("Using the smartphone as a camera to take photos.");
}
}
class PhoneFunction extends Device {
@Override
public void use() {
System.out.println("Using the smartphone to make a call.");
}
}
class GamingFunction extends Device {
@Override
public void use() {
System.out.println("Using the smartphone as a gaming console.");
}
}
public class SmartphonePolymorphism {
public static void main(String[] args) {
// A smartphone behaving differently based on its use
Device camera = new CameraFunction();
Device phone = new PhoneFunction();
Device gaming = new GamingFunction();
camera.use();
phone.use();
gaming.use();
}
}
Abstraction
Abstraction hides the implementation details and exposes only the essential features.
Two Approaches in Java:
- Abstract Classes: Use when you have common functionality but also need specific implementation details in subclasses.
- Interfaces: Use when you want to enforce certain behaviors without dictating implementation.
Consider a remote control. You know which buttons to press (interface) but are unaware of the internal workings (implementation).
abstract class RemoteControl {
public abstract void powerOn();
public abstract void powerOff();
public abstract void changeChannel(int channel);
}
class TelevisionRemote extends RemoteControl {
private boolean isOn = false;
private int currentChannel = 1;
@Override
public void powerOn() {
if (!isOn) {
isOn = true;
System.out.println("TV is now ON.");
} else {
System.out.println("TV is already ON.");
}
}
@Override
public void powerOff() {
if (isOn) {
isOn = false;
System.out.println("TV is now OFF.");
} else {
System.out.println("TV is already OFF.");
}
}
@Override
public void changeChannel(int channel) {
if (isOn) {
currentChannel = channel;
System.out.println("Channel changed to: " + currentChannel);
} else {
System.out.println("Please turn ON the TV first.");
}
}
}
public class RemoteControlDemo {
public static void main(String[] args) {
RemoteControl remote = new TelevisionRemote();
// Using the remote control interface
remote.powerOn();
remote.changeChannel(5);
remote.powerOff();
remote.changeChannel(3);
}
}
Composition vs Inheritance: Choosing the Right Design Approach
Composition means that a class is made up of one or more objects of other classes, promoting loose coupling.
A Car has an Engine. Instead of inheriting from Engine, it uses Engine as a component.
class Engine {
void start() {
System.out.println("Engine is starting...");
}
}
class Car {
Engine engine = new Engine();
void drive() {
engine.start();
System.out.println("Car is driving...");
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.drive(); // Output: Engine is starting... Car is driving...
}
}
When to Use Composition vs Inheritance:
- Use composition when you want flexibility and loose coupling.
- Use inheritance when there is a strict “is-a” relationship.
Extra materials: https://www.digitalocean.com/community/tutorials/composition-vs-inheritance
Conclusion
Java’s Object-Oriented Programming principles—encapsulation, inheritance, polymorphism, and abstraction—provide a foundation for building scalable, maintainable, and real-world-inspired applications. These concepts ensure code reusability, flexibility, and simplicity, making them essential for any developer. By applying these principles to real-world problems and practicing consistently, you can master OOP and elevate your Java programming skills.
Greetings! Very helpful advice in this particular
post! It is the little changes that produce the greatest changes.
Thanks for sharing!