Java OOPs Concepts — Class, Object, 4 Pillars, Examples & Best Practices
Everything you need to know about Java Object-Oriented Programming — Class & Object, Constructors, Access Modifiers, Encapsulation, Inheritance, Polymorphism, Abstraction, Abstract Classes, Interfaces, and real-world production code examples.
Last Updated
March 2026
Read Time
28 min
Level
Beginner to Intermediate
Chapter
21 of 35
What is OOPs in Java?
Object-Oriented Programming (OOPs) is a programming paradigm that organizes software design around objects — self-contained units that bundle data (fields/attributes) and behaviour (methods/functions) together. Instead of writing a program as a sequence of instructions operating on separate data (procedural style), OOPs models the real world as a collection of interacting objects, each responsible for its own state and actions.
Java is a purely object-oriented language (with the exception of primitive types) — virtually everything in Java is an object. The power of OOPs lies in its four foundational pillars: Encapsulation, Inheritance, Polymorphism, and Abstraction. These principles work together to make Java code modular, reusable, maintainable, and scalable.
Think of a Bank Account: it has DATA (balance, accountNumber, owner) and BEHAVIOUR (deposit, withdraw, getBalance). Encapsulation hides the balance behind methods. A SavingsAccount INHERITS from BankAccount (inheritance). deposit() behaves differently for a CurrentAccount vs SavingsAccount (polymorphism). You use deposit() without knowing its internal implementation (abstraction).
Procedural code (functions + data) becomes unmanageable at scale — any function can modify any data. OOPs solves this: objects own their data, expose controlled interfaces, and can be swapped/extended independently. Result: modular codebases where changing one class doesn't break unrelated classes.
Java enforces OOPs more strictly than C++ or Python. Everything lives in a class. There are no global functions or variables — even main() must be inside a class. Primitive types (int, boolean) are the only exceptions to the 'everything is an object' rule; Java provides wrapper classes (Integer, Boolean) to treat them as objects when needed.
Class and Object — The Foundation of OOPs
A class is a blueprint or template that defines the structure (fields) and behaviour (methods) shared by all objects of that type. A class exists at design time — it occupies no memory until objects are created from it. An object is a concrete instance of a class — a real entity created in heap memory at runtime using the new keyword. Multiple objects can be created from one class, each with its own independent state.
// ✅ Class definition — blueprint
public class BankAccount {
// Fields (instance variables) — each object gets its own copy
private String accountNumber;
private String ownerName;
private double balance;
// Constructor — called when object is created with 'new'
public BankAccount(String accountNumber, String ownerName, double initialBalance) {
this.accountNumber = accountNumber;
this.ownerName = ownerName;
this.balance = initialBalance;
}
// Methods — define behaviour
public void deposit(double amount) {
if (amount > 0) balance += amount;
}
public boolean withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
return true;
}
return false;
}
public double getBalance() { return balance; }
public String getOwnerName() { return ownerName; }
@Override
public String toString() {
return "Account[" + accountNumber + ", " + ownerName + ", ₹" + balance + "]";
}
}
// ✅ Creating and using objects
public class Main {
public static void main(String[] args) {
// Creating objects with 'new' — each gets own memory in heap
BankAccount acc1 = new BankAccount("SB001", "Ravi Kumar", 10000.0);
BankAccount acc2 = new BankAccount("SB002", "Priya Singh", 5000.0);
// Each object has its own independent state
acc1.deposit(2500);
acc2.withdraw(1000);
System.out.println(acc1); // Account[SB001, Ravi Kumar, ₹12500.0]
System.out.println(acc2); // Account[SB002, Priya Singh, ₹4000.0]
// acc1 and acc2 share the SAME methods but have DIFFERENT state
System.out.println(acc1.getBalance()); // 12500.0
System.out.println(acc2.getBalance()); // 4000.0
}
}Constructors — Initializing Objects
A constructor is a special method in Java that is automatically called when an object is created using new. Its purpose is to initialize the object's fields to valid starting values. Constructors have the same name as the class and no return type (not even void). If you don't write any constructor, Java provides a default no-argument constructor — but if you write any constructor, the default is no longer provided.
Provided by Java if no constructor is written. Takes no arguments. Initializes all fields to default values (0 for numbers, null for objects, false for boolean). Once you define ANY constructor, Java removes the default — you must write a no-arg constructor explicitly if you need it.
Takes arguments to initialize fields with specific values at creation time. Most common type. Allows you to enforce that objects are created in a valid state — e.g., BankAccount cannot be created without accountNumber and ownerName.
Defining multiple constructors with different parameter lists in the same class. Allows objects to be created in different ways. Example: Employee() — no-arg, Employee(String name) — name only, Employee(String name, int age, double salary) — full init.
Takes another object of the same class as parameter and copies its field values. Java doesn't provide one automatically (unlike C++) — you write it manually. Used to create an independent copy: Employee copy = new Employee(original).
public class Employee {
private String name;
private int age;
private double salary;
private String department;
// 1. No-arg constructor — explicit (needed once we add parameterized ones)
public Employee() {
this.name = "Unknown";
this.age = 0;
this.salary = 0.0;
this.department = "Unassigned";
}
// 2. Parameterized constructor — partial
public Employee(String name, String department) {
this.name = name;
this.department = department;
this.age = 0;
this.salary = 0.0;
}
// 3. Parameterized constructor — full initialization
public Employee(String name, int age, double salary, String department) {
this.name = name;
this.age = age;
this.salary = salary;
this.department = department;
}
// 4. Copy constructor — creates independent copy of another Employee
public Employee(Employee other) {
this.name = other.name;
this.age = other.age;
this.salary = other.salary;
this.department = other.department;
}
// ✅ Constructor chaining with this() — avoid code duplication
// (Alternative approach — calls the full constructor from shorter ones)
// public Employee(String name) {
// this(name, 0, 0.0, "Unassigned"); // Calls 4-arg constructor
// }
@Override
public String toString() {
return name + " | Age: " + age + " | Dept: " + department + " | ₹" + salary;
}
public static void main(String[] args) {
Employee e1 = new Employee();
Employee e2 = new Employee("Anita Sharma", "Engineering");
Employee e3 = new Employee("Rahul Verma", 29, 75000, "Finance");
Employee e4 = new Employee(e3); // Copy of e3
System.out.println(e1); // Unknown | Age: 0 | Dept: Unassigned | ₹0.0
System.out.println(e2); // Anita Sharma | Age: 0 | Dept: Engineering | ₹0.0
System.out.println(e3); // Rahul Verma | Age: 29 | Dept: Finance | ₹75000.0
System.out.println(e4); // Rahul Verma | Age: 29 | Dept: Finance | ₹75000.0
// e3 and e4 are INDEPENDENT — changing e4 doesn't affect e3
}
}Access Modifiers — Controlling Visibility
Access modifiers in Java control the visibility of classes, fields, methods, and constructors. They are the first line of defense for encapsulation — determining which parts of your code can be seen and used by other parts. Java has four access levels: private, default (package-private), protected, and public.
public class Person {
private String ssn; // ✅ Only this class can access — most restrictive
String nickname; // Default: only classes in same package
protected String name; // Subclasses and same package
public int age; // ✅ Anyone can access — least restrictive
// ✅ BEST PRACTICE: Fields private, access through public methods
private double salary;
public double getSalary() {
return salary; // Controlled read access
}
public void setSalary(double salary) {
if (salary >= 0) { // Validation before assignment
this.salary = salary;
}
// Negative salary silently rejected — internal rule enforced
}
private void validateAge(int age) {
// Private helper — internal use only, not part of public API
if (age < 0 || age > 150) throw new IllegalArgumentException("Invalid age");
}
}
// Usage from another class:
// Person p = new Person();
// p.age = 25; ✅ public — accessible
// p.name = "Arjun"; ✅ protected — accessible in same package
// p.ssn = "123"; ❌ COMPILE ERROR — private
// p.salary = 50000; ❌ COMPILE ERROR — private
// p.setSalary(50000); ✅ public setter — correct waythis Keyword — Referring to the Current Object
The this keyword in Java is a reference to the current object — the instance on which the method or constructor is being called. It is used to resolve naming conflicts between instance fields and method/constructor parameters, to call another constructor in the same class, and to pass the current object as an argument to another method.
public class Student {
private String name;
private int rollNumber;
private double gpa;
// USE 1: Resolve naming conflict between field and parameter
public Student(String name, int rollNumber, double gpa) {
this.name = name; // 'this.name' = field, 'name' = parameter
this.rollNumber = rollNumber; // Without 'this', left side = parameter = self-assign
this.gpa = gpa;
}
// USE 2: this() — call another constructor from this class (constructor chaining)
// Must be the FIRST statement in constructor body
public Student(String name) {
this(name, 0, 0.0); // Calls 3-arg constructor — avoids duplicate init code
}
// USE 3: Pass current object as argument to another method
public void registerForCourse(CourseRegistry registry) {
registry.enroll(this); // Passes current Student object to registry
}
// USE 4: Return current object — enables method chaining (Builder pattern)
public Student setName(String name) {
this.name = name;
return this; // Returns current object for chaining
}
public Student setGpa(double gpa) {
this.gpa = gpa;
return this;
}
@Override
public String toString() {
return "Student[" + rollNumber + ": " + name + ", GPA: " + gpa + "]";
}
public static void main(String[] args) {
Student s1 = new Student("Neha Joshi", 101, 8.9);
Student s2 = new Student("Amit Gupta"); // Uses chained constructor
// Method chaining using 'return this'
s2.setName("Amit Gupta").setGpa(7.5);
System.out.println(s1); // Student[101: Neha Joshi, GPA: 8.9]
System.out.println(s2); // Student[0: Amit Gupta, GPA: 7.5]
}
}Encapsulation — First Pillar of OOPs
Encapsulation is the OOPs principle of bundling data (fields) and methods that operate on that data into a single unit (a class), and restricting direct access to the internal state using access modifiers. External code interacts with the object only through a public interface (getters, setters, and other public methods). The internal representation can change freely without affecting external code — this is called information hiding.
Private fields cannot be set to invalid values by external code. A setAge(int age) setter can reject negative ages; direct field access (person.age = -5) cannot be prevented without encapsulation. Encapsulation enforces object invariants — rules about what constitutes a valid state.
External code depends on the PUBLIC interface (method signatures), not the internal representation. If you change how balance is stored (e.g., from double to BigDecimal), only the class internals change — all code using getBalance() and setBalance() continues to work without modification.
You control what can be read (getters), what can be written (setters), and WHEN — with full validation logic in setters. You can make fields read-only (getter only, no setter), write-once (set in constructor only), or computed on-the-fly (getter that calculates from other fields).
// ❌ POOR ENCAPSULATION — public fields, no protection
class BadAccount {
public double balance; // Anyone can set balance = -99999 directly!
public String owner;
}
// ✅ GOOD ENCAPSULATION — private fields, controlled access
public class SavingsAccount {
private final String accountId; // final = set once in constructor, never changed
private String ownerName;
private double balance;
private double interestRate;
public SavingsAccount(String accountId, String ownerName, double initialDeposit) {
if (initialDeposit < 500) {
throw new IllegalArgumentException("Minimum opening deposit is ₹500");
}
this.accountId = accountId;
this.ownerName = ownerName;
this.balance = initialDeposit;
this.interestRate = 0.035; // 3.5% default
}
// Read-only: accountId should never change after creation
public String getAccountId() { return accountId; }
// Read + controlled write for ownerName
public String getOwnerName() { return ownerName; }
public void setOwnerName(String ownerName) {
if (ownerName == null || ownerName.isBlank()) {
throw new IllegalArgumentException("Owner name cannot be blank");
}
this.ownerName = ownerName;
}
// Read-only: balance changes only through deposit/withdraw
public double getBalance() { return balance; }
// No setBalance() — balance must change through proper business operations
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
this.balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
if (amount > this.balance) throw new IllegalStateException("Insufficient balance");
this.balance -= amount;
}
// Computed property — no separate field needed
public double getAnnualInterest() {
return balance * interestRate;
}
}Inheritance — Second Pillar of OOPs
Inheritance is the OOPs mechanism where a child class (subclass) automatically acquires the fields and methods of a parent class (superclass) using the extends keyword. The child class can use the inherited members as-is, override them to provide specialized behaviour, or add new fields and methods of its own. Inheritance models the "IS-A" relationship: a SavingsAccount IS-A BankAccount; a Dog IS-A Animal.
Common fields and methods written once in the parent class are automatically available in all child classes — no duplication. Changes to shared behaviour need to be made only in the parent class and propagate to all children.
Use inheritance ONLY for genuine IS-A relationships. A Car IS-A Vehicle ✅. A Car IS-A Engine ❌ (a car HAS-A engine — use composition). Misusing inheritance for code reuse without an IS-A relationship leads to fragile, confusing class hierarchies.
Java supports only SINGLE inheritance for classes — a class can extend only ONE parent class. This prevents the 'diamond problem' of C++. Multiple inheritance is supported for INTERFACES — a class can implement multiple interfaces. Java's Object class is the implicit parent of every class.
// Parent class (Superclass)
public class Vehicle {
protected String brand;
protected String model;
protected int year;
protected double fuelLevel; // 0.0 to 1.0
public Vehicle(String brand, String model, int year) {
this.brand = brand;
this.model = model;
this.year = year;
this.fuelLevel = 1.0; // Full tank
}
public void startEngine() {
System.out.println(brand + " " + model + " engine started.");
}
public void refuel() {
fuelLevel = 1.0;
System.out.println("Refuelled. Tank full.");
}
public String getInfo() {
return year + " " + brand + " " + model;
}
}
// Child class — inherits from Vehicle
public class Car extends Vehicle {
private int numberOfDoors;
private String transmission; // "Manual" or "Automatic"
// super() calls the parent constructor
public Car(String brand, String model, int year, int doors, String transmission) {
super(brand, model, year); // Must be first statement
this.numberOfDoors = doors;
this.transmission = transmission;
}
// Car-specific method — not in Vehicle
public void openTrunk() {
System.out.println("Trunk opened.");
}
// Overriding parent method — Car has specific gear-shifting
@Override
public void startEngine() {
System.out.println("Car " + brand + " " + model + " ready. " + transmission + " mode.");
}
@Override
public String getInfo() {
return super.getInfo() + " | " + numberOfDoors + "-door " + transmission;
}
}
// Another child class
public class Truck extends Vehicle {
private double payloadCapacityTons;
public Truck(String brand, String model, int year, double payload) {
super(brand, model, year);
this.payloadCapacityTons = payload;
}
public void loadCargo(double tons) {
System.out.println("Loading " + tons + " tons (max: " + payloadCapacityTons + ")");
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car("Maruti", "Swift", 2023, 4, "Manual");
Truck truck = new Truck("Tata", "Prima", 2022, 25.0);
car.startEngine(); // Car Maruti Swift ready. Manual mode.
car.refuel(); // Inherited from Vehicle — no duplication!
car.openTrunk(); // Car-specific
System.out.println(car.getInfo()); // 2023 Maruti Swift | 4-door Manual
truck.startEngine(); // Vehicle's startEngine (not overridden)
truck.loadCargo(15); // Truck-specific
}
}super Keyword — Accessing the Parent Class
The super keyword in Java refers to the parent (superclass) object. It is used inside a child class to: call the parent class's constructor (super()), access parent class methods that have been overridden (super.methodName()), and access parent class fields that are shadowed by child fields (super.fieldName). Like this(), super() must be the first statement in a constructor body.
public class Animal {
protected String name;
protected String sound;
public Animal(String name, String sound) {
this.name = name;
this.sound = sound;
}
public void makeSound() {
System.out.println(name + " says: " + sound);
}
public String describe() {
return "Animal: " + name;
}
}
public class Dog extends Animal {
private String breed;
// super() — call parent constructor (MUST be first statement)
public Dog(String name, String breed) {
super(name, "Woof"); // Calls Animal(name, sound)
this.breed = breed;
}
@Override
public void makeSound() {
super.makeSound(); // Call parent's makeSound() first
System.out.println(name + " wags tail enthusiastically!");
}
@Override
public String describe() {
return super.describe() + " | Breed: " + breed; // Reuse parent's describe()
}
// Output: 'Animal: Bruno | Breed: Labrador'
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("Bruno", "Labrador");
dog.makeSound();
// Output: Bruno says: Woof
// Bruno wags tail enthusiastically!
System.out.println(dog.describe());
// Output: Animal: Bruno | Breed: Labrador
}
}Polymorphism — Third Pillar of OOPs
Polymorphism means "many forms" (from Greek: poly = many, morphe = form). In Java, it is the ability of a single reference type to refer to objects of different classes, and for the same method call to produce different behaviour depending on the actual object type at runtime. Java supports two types: Compile-time Polymorphism (Method Overloading) and Runtime Polymorphism (Method Overriding).
Method Overloading — Compile-Time Polymorphism
Method Overloading is defining multiple methods in the same class with the same name but different parameter lists (different number, type, or order of parameters). The compiler selects the correct method at compile time based on the arguments provided. Return type alone cannot differentiate overloaded methods — the parameter list must differ.
public class Calculator {
// Overloaded add() — same name, different parameter types
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; } // Concatenation
public long add(long a, long b) { return a + b; }
// ❌ This does NOT count as overloading — only return type differs
// public double add(int a, int b) { return a + b; } // COMPILE ERROR
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(5, 3)); // 8 — int version
System.out.println(calc.add(2.5, 3.5)); // 6.0 — double version
System.out.println(calc.add(1, 2, 3)); // 6 — 3-arg version
System.out.println(calc.add("Hello", "!")); // Hello! — String version
// Compiler automatically picks the right method based on argument types
}
}
// Real-world example: PrintStream.println() is overloaded for all types
// System.out.println(5); → println(int)
// System.out.println(3.14); → println(double)
// System.out.println(true); → println(boolean)
// System.out.println("hello"); → println(String)
// System.out.println(obj); → println(Object)Method Overriding — Runtime Polymorphism
Method Overriding is redefining a method in a subclass with the same name, same parameters, and same return type as in the parent class. The JVM decides at runtime which version to call based on the actual object type, not the reference type. The @Override annotation is strongly recommended — it lets the compiler verify you are actually overriding a parent method and not accidentally creating a new method with a typo.
// Parent class
public class Shape {
protected String color;
public Shape(String color) { this.color = color; }
// This method will be overridden by each Shape subclass
public double calculateArea() {
return 0.0; // Default — subclasses must provide real implementation
}
public void draw() {
System.out.println("Drawing a " + color + " shape.");
}
}
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color); this.radius = radius;
}
@Override // ✅ Compiler verifies we're actually overriding
public double calculateArea() {
return Math.PI * radius * radius; // Circle-specific formula
}
@Override
public void draw() {
System.out.println("Drawing a " + color + " circle (r=" + radius + ")");
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(String color, double width, double height) {
super(color); this.width = width; this.height = height;
}
@Override
public double calculateArea() {
return width * height; // Rectangle-specific formula
}
}
public class Main {
public static void main(String[] args) {
// ✅ RUNTIME POLYMORPHISM — Shape reference, different actual types
Shape[] shapes = {
new Circle("Red", 5.0),
new Rectangle("Blue", 4.0, 6.0),
new Circle("Green", 3.0)
};
for (Shape shape : shapes) {
// JVM calls the correct calculateArea() at RUNTIME based on actual type
System.out.printf("Area: %.2f%n", shape.calculateArea());
}
// Output:
// Area: 78.54 ← Circle.calculateArea()
// Area: 24.00 ← Rectangle.calculateArea()
// Area: 28.27 ← Circle.calculateArea()
// Same method call (calculateArea) → different behaviour = Polymorphism!
}
}Abstraction — Fourth Pillar of OOPs
Abstraction is the OOPs principle of hiding implementation details and exposing only the essential features of an object — the WHAT, not the HOW. A user of a class should be able to use it without knowing its internal workings. In Java, abstraction is achieved through abstract classes and interfaces. You interact with a TV remote without knowing its circuit implementation; you call list.sort() without knowing its algorithm — both are abstraction in action.
Declared with the 'abstract' keyword. Cannot be instantiated directly. Can have both abstract methods (no body — subclasses MUST implement) and concrete methods (with body). Can have constructors, instance fields, and static members. Use when related classes share common state and some common behaviour, with variation in specific methods.
Defines a CONTRACT — a set of method signatures that implementing classes must fulfill. Java 8+: can have default and static methods. Java 9+: can have private methods. Fields are implicitly public static final (constants). A class can implement MULTIPLE interfaces. Use to define capabilities shared by unrelated classes.
Abstract class = IS-A relationship + partial implementation. Interface = CAN-DO capability contract. Example: Animal is an abstract class (Dog IS-A Animal). Serializable, Comparable, Runnable are interfaces (Dog CAN-DO serialization). When in doubt: prefer interface for type definition, abstract class only when shared state/implementation is genuinely needed.
Abstract Class — Partial Abstraction
An abstract class is a class declared with the abstract keyword that cannot be instantiated directly. It serves as a base class defining a common template. It can contain abstract methods (declared without a body — subclasses MUST provide an implementation) and concrete methods (with body — subclasses inherit or override). Abstract classes enforce a contract while providing default behaviour.
// Abstract class — cannot be instantiated
public abstract class PaymentMethod {
protected String transactionId;
protected double amount;
public PaymentMethod(String transactionId, double amount) {
this.transactionId = transactionId;
this.amount = amount;
}
// Abstract methods — MUST be implemented by subclasses (no body here)
public abstract boolean processPayment();
public abstract String getPaymentType();
// Concrete method — shared by ALL payment methods (no override needed)
public void generateReceipt() {
System.out.println("--- Receipt ---");
System.out.println("TxnID : " + transactionId);
System.out.println("Type : " + getPaymentType()); // Calls overridden method
System.out.println("Amount: ₹" + amount);
System.out.println("Status: " + (processPayment() ? "SUCCESS" : "FAILED"));
}
// Template method pattern — defines algorithm structure, defers steps to subclasses
public final void executePayment() { // 'final' — subclasses cannot override this
System.out.println("Initiating " + getPaymentType() + " payment...");
if (validateAmount()) {
processPayment(); // Subclass-specific processing
generateReceipt();
} else {
System.out.println("Payment rejected: invalid amount.");
}
}
private boolean validateAmount() { return amount > 0; }
}
// Concrete subclass — implements all abstract methods
public class UpiPayment extends PaymentMethod {
private String upiId;
public UpiPayment(String txnId, double amount, String upiId) {
super(txnId, amount); // Call abstract parent constructor
this.upiId = upiId;
}
@Override
public boolean processPayment() {
System.out.println("Processing UPI payment to " + upiId);
return true; // Simulate success
}
@Override
public String getPaymentType() { return "UPI"; }
}
public class NetBankingPayment extends PaymentMethod {
private String bankName;
public NetBankingPayment(String txnId, double amount, String bankName) {
super(txnId, amount);
this.bankName = bankName;
}
@Override
public boolean processPayment() {
System.out.println("Processing Net Banking via " + bankName);
return true;
}
@Override
public String getPaymentType() { return "Net Banking"; }
}
public class Main {
public static void main(String[] args) {
// PaymentMethod p = new PaymentMethod(...); // ❌ COMPILE ERROR — abstract!
PaymentMethod upi = new UpiPayment("TXN001", 1500.0, "rahul@upi");
PaymentMethod nb = new NetBankingPayment("TXN002", 5000.0, "SBI");
upi.executePayment();
System.out.println();
nb.executePayment();
}
}Interface — Full Abstraction & Multiple Contracts
An interface in Java is a pure contract — it defines what a class must do, without specifying how. A class implements an interface by providing concrete implementations of all its abstract methods. Java 8 added default methods (with bodies) and static methods to interfaces, allowing interface evolution without breaking existing implementations. A class can implement multiple interfaces, enabling a form of multiple inheritance.
// Interface — defines a capability contract
public interface Printable {
void print(); // Abstract method — implicitly public abstract
void printPreview(); // Must be implemented by all classes
}
public interface Saveable {
boolean save(String filePath);
boolean load(String filePath);
// Java 8+: default method — provides a default implementation
default boolean saveWithBackup(String filePath) {
System.out.println("Creating backup before saving...");
return save(filePath + ".bak") && save(filePath);
}
}
public interface Shareable {
void shareViaEmail(String recipient);
void shareViaLink(String platform);
// Java 8+: static method in interface
static String generateShareUrl(String documentId) {
return "https://docs.techsustainify.com/share/" + documentId;
}
}
// ✅ A class can IMPLEMENT MULTIPLE INTERFACES
public class Document implements Printable, Saveable, Shareable {
private String title;
private String content;
private String documentId;
public Document(String title, String content) {
this.title = title;
this.content = content;
this.documentId = java.util.UUID.randomUUID().toString().substring(0, 8);
}
// Implementing Printable
@Override public void print() {
System.out.println("Printing: " + title + "\n" + content);
}
@Override public void printPreview() {
System.out.println("[Preview] " + title + " — " + content.length() + " chars");
}
// Implementing Saveable
@Override public boolean save(String filePath) {
System.out.println("Saved '" + title + "' to " + filePath);
return true;
}
@Override public boolean load(String filePath) {
System.out.println("Loaded from " + filePath);
return true;
}
// Implementing Shareable
@Override public void shareViaEmail(String recipient) {
System.out.println("Emailing '" + title + "' to " + recipient);
}
@Override public void shareViaLink(String platform) {
System.out.println("Sharing on " + platform + ": " + Shareable.generateShareUrl(documentId));
}
}
public class Main {
public static void main(String[] args) {
Document doc = new Document("OOPs Guide", "Object-Oriented Programming in Java");
doc.printPreview(); // Printable
doc.saveWithBackup("/docs/oops.txt"); // Saveable default method
doc.shareViaLink("LinkedIn"); // Shareable
// Interface references — polymorphism
Printable p = doc; p.print(); // Only Printable methods visible
Saveable s = doc; s.save("/backup.txt"); // Only Saveable methods visible
}
}Abstract Class vs Interface — When to Use Which
This is one of the most asked questions in Java interviews. Both provide abstraction, but they serve different design purposes and have important structural differences. Java 8+ narrowed the gap significantly by adding default methods to interfaces, but key distinctions remain.
OOPs vs Procedural Programming — Why Objects?
Understanding why OOPs was created requires understanding the problems of Procedural Programming (C, Pascal, early BASIC). In procedural style, a program is a sequence of functions that operate on shared data. As programs grew larger, this led to unmanageable complexity — any function could modify any data, leading to unpredictable bugs. OOPs solves this by encapsulating data with the functions that operate on it into objects.
Common Mistakes & Pitfalls — Bugs That Fool Everyone
These are the most frequent OOPs-related mistakes made by Java beginners and junior developers. Each one either causes compile errors, runtime exceptions, or subtle logical bugs.
// ❌ MISTAKE 1: Calling instance method/field on null reference → NullPointerException
BankAccount account = null;
account.deposit(1000); // ❌ NullPointerException — object was never created!
// ✅ Fix: always initialize before use
BankAccount account = new BankAccount("SB001", "Ravi", 5000);
// ❌ MISTAKE 2: Forgetting @Override — typo creates new method silently
class MyList extends ArrayList<String> {
// Intended to override but typo — creates new method, doesn't override
public boolean contain(Object o) { return true; } // ❌ 'contain' not 'contains'
// The original ArrayList.contains() still runs — bug is invisible!
}
// ✅ Fix: always use @Override
class MyListFixed extends java.util.ArrayList<String> {
@Override
public boolean contains(Object o) { return true; } // ✅ Compiler verifies this
}
// ❌ MISTAKE 3: Accessing subclass method via parent reference without casting
Vehicle v = new Car("Toyota", "Camry", 2023, 4, "Auto");
// v.openTrunk(); // ❌ COMPILE ERROR — Vehicle reference doesn't know about openTrunk()
// ✅ Fix: downcast when you need subclass-specific method
if (v instanceof Car c) { // Java 16+ pattern matching
c.openTrunk(); // ✅ Safe downcast
}
// ❌ MISTAKE 4: Using == to compare objects (compares references, not values)
String s1 = new String("hello");
String s2 = new String("hello");
if (s1 == s2) { /* never true — different objects */ } // ❌
if (s1.equals(s2)) { /* true — same content */ } // ✅
// ❌ MISTAKE 5: Not calling super() — parent fields uninitialized
class Dog extends Animal {
public Dog(String name) {
// Missing super(name, "Woof") — Animal fields name and sound = null!
System.out.println("Dog created: " + name); // name here is local param
// this.name is null because Animal constructor was never called
}
}
// ✅ Fix: always call super(...) as first statement in child constructor
// ❌ MISTAKE 6: Instantiating an abstract class
// PaymentMethod p = new PaymentMethod("T1", 100); // COMPILE ERROR
// ✅ Fix: instantiate a concrete subclass
PaymentMethod p = new UpiPayment("T1", 100, "user@upi"); // ✅Bad Practices & Anti-Patterns — What Senior Developers Reject
These OOPs anti-patterns are the most common causes of code review rejections, brittle codebases, and unmaintainable systems in professional Java teams.
Classes with only fields and getters/setters — no real behaviour. All business logic lives in separate 'service' or 'manager' classes that operate on these data bags. This is essentially procedural programming in OOPs clothing. Fix: move behaviour that belongs to an object INTO the object (e.g., account.withdraw() instead of AccountService.withdraw(account, amount)).
Extending a class just to reuse its methods when there's no genuine IS-A relationship. Example: class Stack extends ArrayList — Stack IS-NOT-A ArrayList (stacks should only push/pop; ArrayList exposes add, remove at arbitrary indices). Use composition instead: class Stack { private ArrayList list; }. Favour composition over inheritance when there's no true IS-A relationship.
One class that does everything — hundreds of fields, thousands of lines, handles UI, DB, business logic, and networking. Violates the Single Responsibility Principle. Fix: split into focused classes, each with one clear responsibility. A class should have one reason to change.
Declaring fields as public — exposes internal state directly, allows external code to set invalid values, and makes it impossible to add validation or change the internal representation later without breaking all dependent code. ALWAYS use private fields with getters/setters.
Class chains 5-6 levels deep (A extends B extends C extends D extends E) become fragile and hard to understand. Changes to a parent class can unexpectedly break distant descendants. Modern best practice (Joshua Bloch, Effective Java): prefer 2-3 level hierarchies max. Beyond that, use interfaces and composition.
Overriding methods without @Override annotation is a silent bug factory. A typo in the method name silently creates a new method — the parent's version is called instead of the intended override, with no compiler warning. Always annotate overriding methods with @Override — it's the single most valuable habit for OOPs correctness.
Real-World Production Code Examples — OOPs in Enterprise Java
The following examples demonstrate all four OOPs pillars working together in a real Spring Boot-style enterprise Java codebase — an e-commerce notification system.
package com.techsustainify.notification;
// ABSTRACTION: Interface defines the contract for all notification channels
public interface NotificationChannel {
boolean send(String recipient, String subject, String message);
String getChannelType();
// Java 8+ default method — available to all implementors
default String formatMessage(String subject, String body) {
return "[" + getChannelType() + "] " + subject + "\n" + body;
}
}
// ABSTRACTION + ENCAPSULATION: Abstract base with shared state and template behaviour
public abstract class BaseNotification implements NotificationChannel {
// ENCAPSULATION: private fields, controlled access
private final String senderId;
private int sentCount;
private boolean enabled;
protected BaseNotification(String senderId) {
this.senderId = senderId;
this.sentCount = 0;
this.enabled = true;
}
// Template method — structure defined here, steps overridden by subclasses
@Override
public final boolean send(String recipient, String subject, String message) {
if (!enabled) {
System.out.println(getChannelType() + " channel is disabled.");
return false;
}
if (!isValidRecipient(recipient)) {
System.out.println("Invalid recipient: " + recipient);
return false;
}
boolean result = doSend(recipient, subject, message); // Subclass implements
if (result) sentCount++;
return result;
}
// ABSTRACTION: subclasses must implement actual sending
protected abstract boolean doSend(String recipient, String subject, String message);
protected abstract boolean isValidRecipient(String recipient);
// Getters — read-only access to private fields
public String getSenderId() { return senderId; }
public int getSentCount() { return sentCount; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
}
// INHERITANCE + POLYMORPHISM: Concrete implementation — Email channel
public class EmailNotification extends BaseNotification {
private String smtpServer;
private int smtpPort;
public EmailNotification(String senderId, String smtpServer, int smtpPort) {
super(senderId);
this.smtpServer = smtpServer;
this.smtpPort = smtpPort;
}
@Override
protected boolean doSend(String recipient, String subject, String message) {
System.out.println("[SMTP:" + smtpServer + ":" + smtpPort + "] ");
System.out.println(" To: " + recipient);
System.out.println(" Subject: " + subject);
return true;
}
@Override
protected boolean isValidRecipient(String email) {
return email != null && email.contains("@") && email.contains(".");
}
@Override
public String getChannelType() { return "EMAIL"; }
}
// INHERITANCE + POLYMORPHISM: SMS channel — same interface, different behaviour
public class SmsNotification extends BaseNotification {
private String apiKey;
public SmsNotification(String senderId, String apiKey) {
super(senderId);
this.apiKey = apiKey;
}
@Override
protected boolean doSend(String recipient, String subject, String message) {
String sms = subject + ": " + message;
if (sms.length() > 160) sms = sms.substring(0, 157) + "...";
System.out.println("[SMS API] To: " + recipient + " → " + sms);
return true;
}
@Override
protected boolean isValidRecipient(String phone) {
return phone != null && phone.matches("\\+?[0-9]{10,13}");
}
@Override
public String getChannelType() { return "SMS"; }
}
// POLYMORPHISM in action: Notification dispatcher
public class NotificationDispatcher {
private java.util.List<NotificationChannel> channels = new java.util.ArrayList<>();
public void addChannel(NotificationChannel channel) {
channels.add(channel);
}
// RUNTIME POLYMORPHISM: same send() call, different behaviour per channel
public void broadcast(String recipient, String subject, String message) {
for (NotificationChannel channel : channels) {
boolean sent = channel.send(recipient, subject, message);
System.out.println(channel.getChannelType() + ": " + (sent ? "✅" : "❌"));
}
}
}
// Wiring it together
public class Main {
public static void main(String[] args) {
NotificationDispatcher dispatcher = new NotificationDispatcher();
dispatcher.addChannel(new EmailNotification("noreply@shop.com", "smtp.gmail.com", 587));
dispatcher.addChannel(new SmsNotification("MYSHOP", "sms-api-key-xyz"));
dispatcher.broadcast(
"customer@example.com",
"Order Confirmed",
"Your order #ORD1234 has been confirmed!"
);
}
}OOPs Concept Map — How All Pillars Connect
This concept map shows how the four OOPs pillars, classes, objects, interfaces, and abstract classes all relate to each other in Java.
Code Execution Flow — from source to output
Java OOPs Interview Questions — Beginner to Advanced
These questions are asked in virtually every Java developer interview — from fresher campus placements to senior developer technical rounds.
Practice Questions — Test Your OOPs Knowledge
Attempt each question independently before reading the answer. For OOPs, drawing class diagrams on paper is one of the most effective ways to build understanding.
1. What is the output? class A { A() { System.out.println("A constructor"); } } class B extends A { B() { System.out.println("B constructor"); } } class C extends B { C() { System.out.println("C constructor"); } } new C();
Easy2. Identify the problem and fix it: class Animal { public void speak() { System.out.println("..."); } } class Cat extends Animal { public void Speak() { // Note capital S System.out.println("Meow"); } } Animal a = new Cat(); a.speak();
Easy3. Design a class hierarchy for shapes: Shape (base), Circle, Rectangle, Triangle. Each shape must calculate its area. Write the class structure using the appropriate OOPs concept.
Medium4. What is wrong with this code? public class Stack extends ArrayList<Integer> { public void push(int x) { add(x); } public int pop() { return remove(size()-1); } }
Medium5. What is the output? class Parent { public String getType() { return "Parent"; } public static String getStatic() { return "Parent-Static"; } } class Child extends Parent { @Override public String getType() { return "Child"; } public static String getStatic() { return "Child-Static"; } } Parent obj = new Child(); System.out.println(obj.getType()); System.out.println(obj.getStatic());
Medium6. Design an interface-based system for a payment gateway supporting UPI, Card, and Wallet payments. Show how polymorphism allows adding new payment types without modifying existing code.
Hard7. What is the output and why? class Base { int x = 10; int getX() { return x; } } class Derived extends Base { int x = 20; @Override int getX() { return x; } } Base obj = new Derived(); System.out.println(obj.x); System.out.println(obj.getX());
Hard8. Spot and fix all OOPs violations in this code: public class Order { public int orderId; public String status = "NEW"; public double total; public void updateStatus(String s) { status = s; } public double calculateTax() { return total * 0.18; } }
HardConclusion — OOPs: The Architecture of Scalable Java Software
Object-Oriented Programming is not just a set of rules — it is a mental model for solving problems. Once you internalize OOPs thinking, you will naturally see every problem as a collection of objects with responsibilities, relationships, and behaviours. Every real-world Java codebase — from Spring Boot microservices to Android applications to enterprise systems — is built on this foundation.
The four pillars work together as a system: Encapsulation protects object state. Inheritance enables code reuse through type hierarchies. Polymorphism allows flexible, extensible code that works with new types without modification. Abstraction simplifies complex systems into understandable interfaces. Master all four — not just their definitions, but their practical application — and you will write Java that senior developers are proud to review.
Your next step: Java Inheritance (Deep Dive) — where you will explore multi-level inheritance, method hiding, the final keyword, and how Java's single-inheritance model with interface-based multiple inheritance creates a powerful and safe type system. ☕