Definition
Even a single class with no explicit extends, no interfaces, and no sibling classes demonstrates all four OOP pillars.
- Encapsulation is proven by
privatefields andpublicmethods that validate access. There is nosetBalance(): the balance changes only throughdeposit()andwithdraw(). - Abstraction is proven by
publicmethods (WHAT) that hideprivatehelper methods (HOW). The caller seesdeposit(amount)but not the five internal steps. - Compile-time polymorphism is proven by method overloading:
deposit(BigDecimal),deposit(BigDecimal, String), anddeposit(BigDecimal, String, String)share the same name but different parameter lists. - Runtime polymorphism and inheritance are proven because every class implicitly extends
java.lang.Object. OverridingtoString(),equals(), andhashCode()means the JVM dispatches to the child's version at runtime through anObjectreference.
Imagine National Bank of India has only one type of account: just BankAccount. No SavingsAccount, no CurrentAccount. Can this single class still be object-oriented? Yes. Encapsulation: balance is private, only deposit and withdraw can change it. Abstraction: deposit(amount) hides the five internal steps (validate, update, log, notify, audit). Polymorphism: deposit(1000) and deposit(1000, "Salary credit") are the same name with different parameters. Inheritance: BankAccount implicitly extends Object, and you override toString() to print "SAV2001: Rs.125000" instead of "BankAccount@1a2b3c".
Understanding the Concepts
How can a single class prove it follows OOP principles?
This is a tricky interview question designed to test depth of OOP understanding. Most candidates give examples with parent-child class hierarchies. But OOP is baked into Java itself. Every class inherits from Object. Every private field is encapsulation. Every public method hiding private logic is abstraction. Every overloaded method is compile-time polymorphism.
How does a single class prove encapsulation?
The class bundles data (fields) with methods that operate on that data, and restricts access using access modifiers. Private fields cannot be read or written from outside the class without going through public methods that validate and control the access.
package com.bank.core;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class BankAccount {
private final String accountId;
private BigDecimal balance;
private final List<String> transactionLog = new ArrayList<>();
public BankAccount(String accountId, BigDecimal initialBalance) {
this.accountId = accountId;
this.balance = initialBalance;
}
public void deposit(BigDecimal amount) {
if (amount.signum() <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance = balance.add(amount);
transactionLog.add("CREDIT: Rs." + amount);
}
public BigDecimal getBalance() { return balance; }
// No setBalance(): balance is fully encapsulated.
}
How does a single class prove abstraction?
The class has public methods that describe what it does, and private methods that contain how it does it. The caller never sees the internal implementation.
public class BankAccount {
// Public method: WHAT (interface)
public void deposit(BigDecimal amount) {
validatePositive(amount); // step 1: validation
updateBalance(amount); // step 2: balance change
logTransaction("CREDIT", amount); // step 3: audit
notifyHolder("Deposit", amount); // step 4: notification
}
// Caller sees: deposit(amount). That is all.
// Private methods: HOW (hidden implementation)
private void validatePositive(BigDecimal amount) {
if (amount.signum() <= 0) throw new IllegalArgumentException("Must be positive");
}
private void updateBalance(BigDecimal amount) { balance = balance.add(amount); }
private void logTransaction(String type, BigDecimal amount) {
transactionLog.add(type + ": Rs." + amount);
}
private void notifyHolder(String action, BigDecimal amount) {
// Send SMS, email, push notification: caller does not know or care.
}
}
How does a single class prove compile-time polymorphism?
Method overloading provides multiple methods with the same name but different parameters. The compiler decides which version to call based on the argument types at compile time.
public class BankAccount {
// Overload 1: deposit with just amount
public void deposit(BigDecimal amount) {
validatePositive(amount);
balance = balance.add(amount);
logTransaction("CREDIT", amount, "No narration");
}
// Overload 2: deposit with amount + narration
public void deposit(BigDecimal amount, String narration) {
validatePositive(amount);
balance = balance.add(amount);
logTransaction("CREDIT", amount, narration);
}
// Overload 3: deposit with amount + narration + reference number
public void deposit(BigDecimal amount, String narration, String referenceNo) {
validatePositive(amount);
balance = balance.add(amount);
logTransaction("CREDIT", amount, narration + " [REF:" + referenceNo + "]");
}
}
// Three methods named "deposit": the compiler picks the right one.
// account.deposit(new BigDecimal("5000"));
// account.deposit(new BigDecimal("5000"), "Salary");
// account.deposit(new BigDecimal("5000"), "Salary", "NEFT-UTR-123");
How does a single class prove inheritance and runtime polymorphism?
Every Java class that does not explicitly extend another class implicitly extends java.lang.Object. Object provides 11 methods that every class inherits. Overriding these methods proves runtime polymorphism even in a single class.
public class BankAccount /* implicitly extends Object */ {
private String accountId;
private BigDecimal balance;
@Override
public String toString() {
// Overrides Object.toString() which returns "BankAccount@1a2b3c"
return accountId + ": Rs." + balance;
// Now: "SAV2001: Rs.125000"
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof BankAccount that)) return false;
return java.util.Objects.equals(accountId, that.accountId);
}
@Override
public int hashCode() {
return java.util.Objects.hash(accountId);
}
}
// Proof of runtime polymorphism:
// Object ref = new BankAccount("SAV2001", new BigDecimal("125000"));
// ref.toString(); // calls BankAccount.toString(), NOT Object.toString().
// The JVM dispatches to the actual type at runtime.
How is this different from OOP with multiple classes?
With multiple classes, inheritance and runtime polymorphism are explicit: you write extends Account and the JVM dispatches calculateInterest() to the correct subclass. With a single class, the same mechanisms exist but are implicit: the class extends Object and the JVM dispatches toString() to the overridden version.
| OOP Pillar | Single Class Proof | Multi-Class Proof |
|-----------|-------------------|------------------|
| Encapsulation | private balance + validated deposit() | Same |
| Abstraction | public deposit() hides private helpers | Interface hides implementation |
| Compile-time poly | Overloaded deposit() methods | Same |
| Runtime poly | Override Object.toString() | Override Account.calculateInterest() |
| Inheritance | Implicit extends Object | Explicit extends Account |
What are common mistakes in answering this question?
The most common mistake is saying a single class cannot demonstrate polymorphism or inheritance. Both exist through the implicit relationship with Object.
// WRONG answer: "A single class has no polymorphism."
// CORRECT: overloading IS compile-time polymorphism.
// overriding Object methods IS runtime polymorphism.
// WRONG answer: "A single class has no inheritance."
// CORRECT: every class extends Object. You inherit toString(), equals(),
// hashCode(), getClass(), clone(), wait(), notify(), finalize().
What should a senior engineer know?
- The four pillars are not just design patterns you apply. They are fundamental properties of every Java class.
- A HashSet correctly deduplicates BankAccount objects by accountId because equals() and hashCode() are overridden from Object: proving both inheritance and polymorphism.
- In production code, even a single utility class (like
InterestCalculator) should use encapsulation (private fields) and abstraction (public API hiding calculation details). - Records (Java 16) make single-class OOP explicit:
record AccountSummary(String id, BigDecimal balance) {}auto-generates equals, hashCode, and toString from Object, with all fields private and final.
Visual Overview
BankAccount (single class) ┌─────────────────────────────────────────────┐ │ ENCAPSULATION │ │ private String accountId │ │ private BigDecimal balance │ │ No setBalance() │ ├─────────────────────────────────────────────┤ │ ABSTRACTION │ │ public deposit(amount) ← caller sees this │ │ private validatePositive() │ │ private updateBalance() ← hidden │ │ private logTransaction() │ ├─────────────────────────────────────────────┤ │ COMPILE-TIME POLYMORPHISM │ │ deposit(BigDecimal) │ │ deposit(BigDecimal, String) │ │ deposit(BigDecimal, String, String) │ ├─────────────────────────────────────────────┤ │ INHERITANCE + RUNTIME POLYMORPHISM │ │ extends Object (implicit) │ │ @Override toString() │ │ @Override equals(Object) │ │ @Override hashCode() │ └─────────────────────────────────────────────┘
Key Takeaway
Even a single class with no explicit extends or implements demonstrates all four OOP pillars. Encapsulation: private fields like balance can only be changed through public methods like deposit() and withdraw() that validate every modification. There is no setBalance() method. Abstraction: the public deposit() method hides three private steps (validatePositive, updateBalance, logTransaction). The caller knows what happens (deposit money) but not how it happens internally. Polymorphism has two forms in a single class: compile-time via method overloading (three deposit methods with different parameters, the compiler chooses the correct one based on arguments) and runtime via Object inheritance (overriding toString, equals, and hashCode, where the JVM dispatches to the BankAccount version at runtime when called through an Object reference).
In Rajesh's BankAccount class at National Bank of India, deposit(5000), deposit(5000, "Salary"), and deposit(5000, "Salary", "NEFT-REF") are three overloaded methods: compile-time polymorphism. account.toString() returns "SAV2001: Rs.125000" instead of Object's default "BankAccount@1a2b3c": this is runtime polymorphism because the JVM dispatches to the overridden method based on the actual object type. HashSet correctly deduplicates accounts by accountId because equals() and hashCode() are overridden from Object: proving both inheritance and polymorphism.
Remember one sentence: OOP is not something you add to a class; it is a fundamental property of every Java class through private fields, public methods, overloaded signatures, and implicit inheritance from Object.
---