What is OOPs in Java
What is OOPs in Java
Object-Oriented Programming — OOPs — is a programming paradigm that organises code around objects rather than functions. An object bundles the data it holds and the operations it performs into a single unit. Java was built from the ground up as an object-oriented language, which is why almost everything you write in Java is structured around classes and objects.
The reason OOPs exists is not philosophical — it is practical. As programs grow, function-based code becomes difficult to maintain because data and the logic that manipulates it are separated. When a requirement changes, you must find and update every function that touches the affected data. OOPs solves this by keeping related data and behaviour together, making large codebases easier to extend without breaking what already works.
What Is Object-Oriented Programming?
Object-Oriented Programming (OOP) is a way of designing software where the primary building blocks are objects — self-contained units that combine state (data stored in fields) and behaviour (operations defined as methods). Each object is an instance of a class, which acts as a blueprint describing what data the object holds and what it can do.
Java's four OOP pillars are:
- ›Encapsulation — bundling data and behaviour together, hiding internal details
- ›Inheritance — deriving new classes from existing ones to reuse and extend behaviour
- ›Polymorphism — one interface, multiple implementations
- ›Abstraction — exposing what an object does, hiding how it does it
These four principles are not independent ideas — they work together. A class that uses encapsulation to protect its data also uses abstraction to present a clean interface. Inheritance enables polymorphism. Understanding how they connect is what separates surface-level knowledge from the depth interviewers look for.
Why OOPs Matters in Real Development
Before OOPs became standard, most programs were written procedurally — a sequence of functions that operated on shared data. This worked for small programs. For large systems with hundreds of developers and thousands of moving parts, the approach broke down in predictable ways.
The problems procedural code creates at scale:
- ›Change one thing, break another — data is shared globally, so a change to how data is structured requires updating every function that touches it
- ›No natural grouping — related logic is scattered across files with no structure enforcing cohesion
- ›Difficult to test — functions that depend on global state are hard to test in isolation
- ›No built-in reuse mechanism — sharing behaviour between modules requires copying code
OOPs addresses each of these. Encapsulation ensures data changes only propagate through a controlled interface. Classes provide natural grouping. Inheritance allows reuse without duplication. These are the reasons Java adopted OOPs as its core design, and why understanding the paradigm is the foundation of every Java interview.
The Four Pillars of OOPs in Java
Encapsulation
Encapsulation is the practice of bundling data (fields) and the methods that operate on that data into a single class, and restricting direct access to the data from outside the class. External code interacts with the object only through its public methods — it cannot see or directly modify internal state.
1// File: BankAccount.java
2
3public class BankAccount {
4
5 // Private fields — cannot be accessed directly from outside the class
6 private String accountNumber;
7 private double balance;
8
9 public BankAccount(String accountNumber, double initialBalance) {
10 this.accountNumber = accountNumber;
11 this.balance = (initialBalance >= 0) ? initialBalance : 0.0;
12 }
13
14 // Controlled access through public methods
15 public double getBalance() {
16 return balance;
17 }
18
19 public void deposit(double amount) {
20 if (amount > 0) {
21 balance += amount;
22 }
23 }
24
25 public boolean withdraw(double amount) {
26 if (amount > 0 && balance >= amount) {
27 balance -= amount;
28 return true;
29 }
30 return false;
31 }
32
33 public String getAccountNumber() {
34 return accountNumber;
35 }
36}1// File: EncapsulationDemo.java
2
3public class EncapsulationDemo {
4
5 public static void main(String[] args) {
6
7 BankAccount account = new BankAccount("ACC-2024-001", 5000.0);
8
9 System.out.println("Balance: Rs." + account.getBalance());
10 account.deposit(2000.0);
11 System.out.println("After deposit: Rs." + account.getBalance());
12
13 boolean withdrawn = account.withdraw(8000.0);
14 System.out.println("Withdrawal of Rs.8000 succeeded: " + withdrawn);
15 System.out.println("Balance after attempt: Rs." + account.getBalance());
16 }
17}Output:
Balance: Rs.5000.0
After deposit: Rs.7000.0
Withdrawal of Rs.8000 succeeded: false
Balance after attempt: Rs.7000.0
The balance field is private — no code outside BankAccount can directly set it to a negative value or bypass the withdrawal check. The validation logic lives where the data lives. If the business rule for withdrawals changes, there is exactly one place to update it.
Inheritance
Inheritance allows one class to acquire the fields and methods of another class using the extends keyword. The class being extended is called the parent (or superclass); the extending class is the child (or subclass). The child inherits everything non-private from the parent and can add its own fields and methods, or override inherited methods.
1// File: Employee.java
2
3public class Employee {
4
5 private final String employeeId;
6 private final String name;
7 private double baseSalary;
8
9 public Employee(String employeeId, String name, double baseSalary) {
10 this.employeeId = employeeId;
11 this.name = name;
12 this.baseSalary = baseSalary;
13 }
14
15 public String getEmployeeId() { return employeeId; }
16 public String getName() { return name; }
17 public double getBaseSalary() { return baseSalary; }
18
19 public double calculateMonthlyPay() {
20 return baseSalary;
21 }
22
23 public String getPaySlipSummary() {
24 return employeeId + " | " + name + " | Monthly Pay: Rs." + calculateMonthlyPay();
25 }
26}1// File: SalesEmployee.java
2
3public class SalesEmployee extends Employee {
4
5 private final double commissionRate;
6 private double monthlySales;
7
8 public SalesEmployee(String employeeId, String name,
9 double baseSalary, double commissionRate) {
10 super(employeeId, name, baseSalary); // calls Employee constructor
11 this.commissionRate = commissionRate;
12 }
13
14 public void recordSales(double salesAmount) {
15 this.monthlySales = salesAmount;
16 }
17
18 // Overrides the parent method to include commission
19 @Override
20 public double calculateMonthlyPay() {
21 return getBaseSalary() + (monthlySales * commissionRate);
22 }
23}1// File: InheritanceDemo.java
2
3public class InheritanceDemo {
4
5 public static void main(String[] args) {
6
7 Employee regularEmployee = new Employee("EMP-001", "Rahul Sharma", 45000.0);
8 SalesEmployee salesPerson = new SalesEmployee("EMP-002", "Priya Mehta", 30000.0, 0.05);
9
10 salesPerson.recordSales(200000.0);
11
12 // getPaySlipSummary() is inherited — no need to rewrite it in SalesEmployee
13 System.out.println(regularEmployee.getPaySlipSummary());
14 System.out.println(salesPerson.getPaySlipSummary());
15 }
16}Output:
EMP-001 | Rahul Sharma | Monthly Pay: Rs.45000.0
EMP-002 | Priya Mehta | Monthly Pay: Rs.40000.0
SalesEmployee inherits getPaySlipSummary() from Employee without rewriting it. When getPaySlipSummary() calls calculateMonthlyPay(), it uses the overridden version in SalesEmployee because of polymorphism — the actual method that runs is determined by the object's runtime type, not the reference type. That is inheritance and polymorphism working together.
Polymorphism
Polymorphism means "many forms." In Java, it allows one method call to behave differently depending on the object it is called on. There are two kinds:
- ›Compile-time polymorphism — method overloading (same name, different parameter lists, resolved at compile time)
- ›Runtime polymorphism — method overriding (same name and signature, resolved at runtime based on the actual object type)
Runtime polymorphism is what makes OOPs powerful in practice — you can write code that works on a parent type and have it automatically use the correct subclass implementation.
1// File: NotificationService.java
2
3public abstract class NotificationService {
4
5 private final String serviceType;
6
7 public NotificationService(String serviceType) {
8 this.serviceType = serviceType;
9 }
10
11 public String getServiceType() { return serviceType; }
12
13 // Each subclass provides its own implementation
14 public abstract void send(String recipient, String message);
15}1// File: SmsNotification.java
2
3public class SmsNotification extends NotificationService {
4
5 public SmsNotification() {
6 super("SMS");
7 }
8
9 @Override
10 public void send(String recipient, String message) {
11 System.out.println("[SMS to " + recipient + "]: " + message);
12 }
13}1// File: EmailNotification.java
2
3public class EmailNotification extends NotificationService {
4
5 public EmailNotification() {
6 super("Email");
7 }
8
9 @Override
10 public void send(String recipient, String message) {
11 System.out.println("[Email to " + recipient + "]: " + message);
12 }
13}1// File: PolymorphismDemo.java
2
3import java.util.List;
4
5public class PolymorphismDemo {
6
7 public static void main(String[] args) {
8
9 // All referenced as the parent type — actual type is determined at runtime
10 List<NotificationService> services = List.of(
11 new SmsNotification(),
12 new EmailNotification(),
13 new SmsNotification()
14 );
15
16 String[] recipients = {"9876543210", "priya@example.com", "8765432109"};
17 String message = "Your OTP is 847291. Valid for 5 minutes.";
18
19 for (int i = 0; i < services.size(); i++) {
20 services.get(i).send(recipients[i], message);
21 }
22 }
23}Output:
[SMS to 9876543210]: Your OTP is 847291. Valid for 5 minutes.
[Email to priya@example.com]: Your OTP is 847291. Valid for 5 minutes.
[SMS to 8765432109]: Your OTP is 847291. Valid for 5 minutes.
The loop calls send() on every NotificationService without knowing whether it is SMS or Email. The JVM looks up the actual type at runtime and invokes the correct implementation. Adding a new channel — WhatsAppNotification or PushNotification — requires adding a new subclass and no changes to the loop. This is the open-closed principle enabled by polymorphism.
Abstraction
Abstraction means exposing what an object does without revealing how it does it. Users of the object interact with its interface — the public methods — without needing to know the internal implementation. In Java, abstraction is achieved through abstract classes and interfaces.
The payment processing example below shows abstraction in action — the caller knows what operations are available but is completely isolated from how each payment gateway implements them.
1// File: PaymentGateway.java
2
3public interface PaymentGateway {
4
5 boolean initiatePayment(String orderId, double amount, String currency);
6 boolean verifyPayment(String transactionId);
7 boolean refundPayment(String transactionId, double amount);
8}1// File: RazorpayGateway.java
2
3public class RazorpayGateway implements PaymentGateway {
4
5 @Override
6 public boolean initiatePayment(String orderId, double amount, String currency) {
7 // Internal Razorpay API integration details hidden from the caller
8 System.out.println("Razorpay: initiating Rs." + amount + " for order " + orderId);
9 return true;
10 }
11
12 @Override
13 public boolean verifyPayment(String transactionId) {
14 System.out.println("Razorpay: verifying transaction " + transactionId);
15 return true;
16 }
17
18 @Override
19 public boolean refundPayment(String transactionId, double amount) {
20 System.out.println("Razorpay: refunding Rs." + amount + " for " + transactionId);
21 return true;
22 }
23}1// File: AbstractionDemo.java
2
3public class AbstractionDemo {
4
5 public static void processOrder(PaymentGateway gateway,
6 String orderId, double amount) {
7 // Works with any PaymentGateway — no knowledge of which one
8 boolean initiated = gateway.initiatePayment(orderId, amount, "INR");
9 if (initiated) {
10 System.out.println("Payment initiated successfully for " + orderId);
11 }
12 }
13
14 public static void main(String[] args) {
15
16 PaymentGateway razorpay = new RazorpayGateway();
17 processOrder(razorpay, "ORD-9001", 1499.0);
18 }
19}Output:
Razorpay: initiating Rs.1499.0 for order ORD-9001
Payment initiated successfully for ORD-9001
processOrder() works with any implementation of PaymentGateway. Swapping from Razorpay to PayU or Stripe requires changing one line — the object creation. The processing logic is completely insulated from gateway-specific details. This is abstraction delivering its core promise: write code against the contract, not the implementation.
OOPs vs Procedural Programming
| Aspect | Procedural | Object-Oriented |
|---|---|---|
| Primary unit | Functions | Objects (classes) |
| Data handling | Shared global or passed to functions | Encapsulated inside objects |
| Code reuse | Copy-paste or function calls | Inheritance and composition |
| Change impact | Affects all functions using shared data | Contained within the class |
| Modelling | Steps and operations | Real-world entities and relationships |
| Examples | C, Pascal | Java, Python, C++ |
How the Four Pillars Work Together
The pillars are not independent — each one depends on and reinforces the others.
Encapsulation protects object state
|
+---> Inheritance reuses protected behaviour
|
+---> Polymorphism enables flexible method dispatch
|
+---> Abstraction hides implementation details
|
+---> Encapsulation protects the hidden details
A class that is well-encapsulated exposes a clean interface — that is abstraction. When another class inherits from it, it reuses the encapsulated behaviour. When you have a collection of subclass objects referenced as the parent type, polymorphism ensures the correct subclass method runs automatically.
Best Practices
Design classes around responsibilities, not data
A class should represent a single, well-defined concept. A User class should not contain payment processing logic. A PaymentService should not contain user authentication logic. When a class has one clear responsibility, it is easier to test, change, and reuse.
Favour composition over deep inheritance chains
Inheritance chains beyond two levels become difficult to understand. When a class needs behaviour from another class, prefer building the dependency as a field rather than extending the class. This is the composition over inheritance principle that production teams apply consistently.
Use interfaces to define contracts, abstract classes for shared behaviour
When you want to define what a group of classes must do — without specifying how — use an interface. When you want to provide some shared implementation alongside the contract, use an abstract class.
Keep public APIs minimal
Only make methods and fields public when callers genuinely need them. Everything else should be private or package-private. A minimal public API means fewer things that can break when internal implementation changes.
Common Mistakes
Treating OOPs as just syntax — Writing classes with public fields and no methods is not OOP — it is a struct by another name. The power of OOPs comes from combining data and behaviour and protecting that combination through encapsulation.
Inheriting for code reuse when composition fits better — Inheriting from a class just to reuse its utility methods creates a misleading "is-a" relationship. If a ReportGenerator extends DatabaseConnector just to use its executeQuery method, the class hierarchy is wrong. Make DatabaseConnector a field instead.
Defining behaviour in the wrong class — A common mistake in fresher code is putting logic that belongs inside an object onto the class that holds the object. If Order has an amount field, the discount calculation belongs in Order, not in a DiscountCalculator that receives an Order and pokes at its fields.
Ignoring abstraction when adding new implementations — Writing code directly against a concrete class type (RazorpayGateway instead of PaymentGateway) makes swapping implementations difficult. Always code against the most abstract type that satisfies the requirement.
Interview Questions
Q1. What is OOPs and what are its four pillars in Java?
Object-Oriented Programming is a paradigm that organises code around objects — instances of classes that bundle state and behaviour. The four pillars are encapsulation (protecting data inside a class with controlled access), inheritance (deriving new classes from existing ones), polymorphism (one interface resolving to different implementations at runtime), and abstraction (exposing what an object does while hiding how). Java enforces OOPs as its core design — everything except primitives is an object.
Q2. What is the difference between encapsulation and abstraction?
Encapsulation is the mechanism of hiding internal data by making fields private and providing controlled access through methods. Abstraction is the concept of hiding implementation details and exposing only essential behaviour through a clean interface. Encapsulation is about data protection at the class level. Abstraction is about interface design at the system level. A well-encapsulated class that exposes only what callers need is also demonstrating abstraction — the two reinforce each other.
Q3. What is the difference between compile-time and runtime polymorphism in Java?
Compile-time polymorphism is achieved through method overloading — multiple methods with the same name but different parameter lists in the same class. The compiler decides which method to call based on the argument types at compile time. Runtime polymorphism is achieved through method overriding — a subclass provides a specific implementation of a method declared in the parent class. The JVM decides which implementation to call at runtime based on the actual type of the object, not the reference type.
Q4. What is the difference between inheritance and composition in Java?
Inheritance models an "is-a" relationship — a SalesEmployee is an Employee. Composition models a "has-a" relationship — an Order has a PaymentGateway. Inheritance allows the child to reuse and extend parent behaviour but creates tight coupling. Composition allows a class to use behaviour from another class through a field, making it easier to swap implementations and test independently. Teams following clean architecture strongly prefer composition for code reuse and reserve inheritance for genuine type hierarchy relationships.
Q5. Why is Java called an object-oriented language if it has primitive types?
Java is called object-oriented because its primary design organises code around classes and objects, and almost all code is written through class and object constructs. Primitive types — int, double, boolean, and so on — exist for performance: they are stored directly on the stack without the overhead of object creation on the heap. Java provides wrapper classes (Integer, Double, Boolean) for every primitive, which are true objects. The Java designers chose this pragmatic trade-off rather than a pure OOP model where everything would have been an object.
Q6. How does polymorphism support the open-closed principle?
The open-closed principle states that software should be open for extension but closed for modification. Polymorphism enables this by allowing new behaviour to be added through new subclasses without changing the code that works with the parent type. A send() loop that operates on NotificationService references works correctly with a new WhatsAppNotification class without any changes to the loop. The loop is closed for modification but open for extension — adding new types does not require touching existing code.
FAQs
What is OOPs in simple terms?
OOPs is a way of organising programs where code is grouped into objects — units that combine data and behaviour. Instead of writing a list of functions that operate on shared data, you create objects that know their own data and know how to operate on it. Java is built around this approach.
What are the four pillars of OOPs in Java?
Encapsulation, Inheritance, Polymorphism, and Abstraction. Encapsulation hides data inside classes. Inheritance lets new classes reuse and extend existing ones. Polymorphism allows one method call to behave differently based on the object type. Abstraction hides implementation details behind clean interfaces.
Is Java 100% object-oriented?
No. Java has eight primitive types — byte, short, int, long, float, double, char, and boolean — which are not objects. Because of this, Java is described as a strongly object-oriented language rather than a purely object-oriented one. Every primitive has a corresponding wrapper class for use in object-oriented contexts.
What is the difference between a class and an object in Java?
A class is a blueprint — it defines what fields an object will have and what methods it will support. An object is an instance of that class — a specific entity created from the blueprint with its own state. Employee is a class; new Employee("EMP-001", "Rahul", 45000) creates an object.
What is method overriding and how does it relate to OOPs?
Method overriding is when a subclass provides its own implementation of a method declared in the parent class, using the same name and parameter signature. It is the mechanism that enables runtime polymorphism in Java — when the parent-type reference calls the method, the JVM uses the subclass implementation. This allows behaviour to vary by object type without changing the calling code.
Why is OOPs important for Java interviews?
OOPs concepts are the foundation of almost every Java design question. Understanding encapsulation, inheritance, polymorphism, and abstraction helps you explain why code is structured the way it is — not just how to write it. Product-based companies test OOPs through design scenarios: how would you model a notification system, how would you handle different payment gateways, how would you build an employee payroll system? The answers always involve applying one or more OOPs pillars.
Summary
Object-Oriented Programming organises Java programs around objects that combine data and behaviour. The four pillars — encapsulation, inheritance, polymorphism, and abstraction — are not separate rules but a coherent design system. Encapsulation protects state. Inheritance reuses behaviour. Polymorphism allows flexible dispatch. Abstraction hides implementation. Each pillar makes the others more powerful.
The practical payoff is software that is easier to change without breaking what already works. New payment gateways are added without touching the processing logic. New employee types are added without rewriting payroll calculations. New notification channels are added without changing the dispatch loop. That extensibility without modification is what OOPs exists to deliver.
For interviews, be ready to explain each pillar with a concrete example, distinguish encapsulation from abstraction precisely, and explain compile-time versus runtime polymorphism with actual code. Knowing the "why" behind each pillar — not just the syntax — is what separates candidates who understand OOPs from those who have memorised definitions.
What to Read Next
| Topic | Link |
|---|---|
| How encapsulation is implemented using classes, fields, and access modifiers | Java Classes and Objects → |
| How inheritance works in Java and when to prefer composition instead | Java Inheritance → |
| How polymorphism enables method overriding and dynamic dispatch | Java Polymorphism → |
| How interfaces define contracts for abstraction in Java | Java Interfaces → |
| How abstract classes combine abstraction with shared implementation | Java Abstract Classes → |