Learn · OOP · Core
OOP — The 4 Pillars
Object-Oriented Programming means organising code around objects — bundles of data plus the actions on that data. Siemens always asks you to “explain OOP with an example.” After this, you’ll never fumble it.
Before we start
- What OOP is and why we organise code around objects
- The 4 pillars — each with a real example AND the code
- How they show up in Java and C#
- How to answer “explain OOP with an example” without freezing
- Explain the 4 pillars in your own words with examples
- Read and write the basic code for each pillar
- Model a real thing (like a payment) as classes
Why you’re learning it: OOP is half the MCQs and a guaranteed interview question — and it’s how real Java/C# codebases (including your Ayris one) are built. ⏱️ ~30 min.
First — what’s an object?
Think of a car. It has data (colour, speed, fuel) and actions (accelerate, brake, honk). An object bundles exactly that: data + the actions on it. A class is the blueprint (“Car”); an object is one built from it (“your red Swift”). OOP organises a program as objects talking to each other, instead of one long script.
What actually sits in memory
One class, as many objects as you like — each with its own copy of the data. When you write new BankAccount(...), the object itself lives on the heap; your variable on the stack only holds a reference — an arrow pointing at the heap object.
BankAccount a1 = new BankAccount("Ravi", 5000.0);
BankAccount a2 = new BankAccount("Priya", 12000.0);a2 ──▶
Priya · 12000
Change a1’s balance and a2 doesn’t care — separate objects, separate data. That separation is the whole point. In a real job every user session, every payment, every order is one object built from one class. Miss this and every “why did changing X also change Y?” bug will confuse you.
Pillar 1 · 📦 Encapsulation
Bundle data + behaviour, and hide the internals. A BankAccount keeps its balance private — you deposit() or withdraw(), but you can’t just set balance = 1000000. The object guards its own data, like an ATM: buttons outside, cash logic locked inside.
class BankAccount {
private long balance; // hidden from outside
public void deposit(long amt) { balance += amt; }
public long getBalance() { return balance; }
}The constructor is a gatekeeper
A constructor runs once, when the object is born. Its job: make sure the object can never exist in a half-built, invalid state. The this keyword means “the object being built right now” — you need it when a parameter name shadows a field name (the standard convention, so you’ll write this.x = x constantly).
public BankAccount(String owner, double balance) {
if (balance < 0)
throw new IllegalArgumentException("Balance cannot be negative");
this.owner = owner; // 'this.owner' = the field; 'owner' = the parameter
this.balance = balance;
}Now a BankAccount with -500 rupees cannot exist. That’s design, not syntax. Without this, Java would assign the parameter to itself and silently do nothing.
Getters are a decision, not boilerplate
Fields are private — always start there. Tutorials get the next part badly wrong: they tell you to auto-generate a getter and setter for every field. That’s just a public field with extra steps. The real rule: expose only what callers legitimately need, in the form they need it.
public double getBalance() { return balance; } // yes — callers must read balance
// setBalance(x)? NO. A balance changes through business ops, never a raw overwrite.
public void deposit(double amt) {
if (amt <= 0) throw new IllegalArgumentException("Deposit must be positive");
balance += amt;
}
public void withdraw(double amt) {
if (amt > balance) throw new IllegalArgumentException("Insufficient funds");
balance -= amt;
}- Getter + setter for every field
setBalance(x)- Rules scattered across the codebase
- Any code can break the object
- Getter only if callers must read it
deposit(x)/withdraw(x)with validation- Rules live in one place — the class
- Object can’t reach an invalid state
Static vs instance — the classic beginner trap
Instance members = one copy per object (balance — every account has its own). Static members = one copy per class, shared by all objects (counters, constants, utility methods).
private static int totalAccounts = 0; // ONE counter, shared by all accounts private double balance; // one balance PER account // ...constructor does totalAccounts++ on every birth
The trap: marking things static just to silence the compiler (“non-static method cannot be referenced from a static context” inside main). main is static because it runs before any object exists — the fix is to create an object in main, not to make everything static. If everything in your class is static, you don’t have objects — you have a script wearing a class costume.
Pillar 2 · 🎛️ Abstraction
Show WHAT it does, hide HOW. You call list.add(x) without knowing it may be resizing an array underneath. Like a steering wheel — you turn it; you don’t think about the rack and pinion. Simple surface, complex insides.
Pillar 3 · 🧬 Inheritance
A class reuses and extends another. SavingsAccount extends Account — it gets deposit/withdraw for free and adds addInterest(). “A SavingsAccount is an Account, plus interest.”
class Account { void deposit(long a) { ... } }
class SavingsAccount extends Account {
void addInterest() { ... } // deposit() inherited for free
}Calling the parent’s constructor first is done with super(...), and overriding a parent method is marked @Override:
class UpiPayment extends Payment {
UpiPayment(double amount, String upiId) {
super(amount); // run the parent constructor first
this.upiId = upiId;
}
@Override
public void process() { ... } // replace the parent's behaviour
}But inheritance is the most over-used tool in OOP
It looks clean, but it welds the child to the parent — every change in Payment ripples into every subclass forever, and each class is locked to exactly one parent. What happens when CardPayment needs retry logic that NetBankingPayment shares but UpiPayment doesn’t? The hierarchy starts contorting and never recovers. Use inheritance only when the relationship is genuinely, permanently is-a and the parent was designed to be extended.
Composition — “has-a”, and why real code prefers it
Instead of being a thing, your class has a thing — a swappable part held behind a field:
class Payment {
private final double amount;
private final PaymentMethod method; // composition: has-a
Payment(double amount, PaymentMethod method) {
this.amount = amount;
this.method = method;
}
void process() { method.pay(amount); } // delegate the work to the part
}- Fixed at compile time
- Can’t swap behaviour later — parent is parent
- Tight coupling — parent changes break children
- One parent max
- Chosen at runtime
- Swap by handing in a different part
- Loose — only the contract matters
- Hold as many parts as you need
The rule you’ll hear in code reviews for the rest of your career: favour composition over inheritance. Reach for inheritance only when composition genuinely can’t express it.
Interfaces — contracts, not implementations
An interface says what must be doable, with zero opinion on how. Any class that implements it promises to provide those methods; the caller stops caring which class it’s talking to — only that the contract is honoured. Like a wall socket: it doesn’t care if you plug in a charger or a lamp, as long as the plug fits.
interface PaymentMethod {
void pay(double amount); // the contract — every method can pay an amount
}Pillar 4 · 🎭 Polymorphism
Same call, different behaviour per object. Call shape.area() — a Circle computes πr², a Square computes side². Same method name, right answer for each type. The caller doesn’t care which.
Shape s = new Circle(); s.area(); // runs Circle's area() s = new Square(); s.area(); // same call, now runs Square's area()
Why this is a superpower — it deletes the if-else jungle
Put the three payment types behind one PaymentMethod reference and loop over them. The loop has no idea which type it holds — Java looks at the actual object at runtime and dispatches to the right pay. This is called dynamic dispatch.
PaymentMethod[] methods = {
new UpiPayment("darshan@upi"),
new CardPayment("4242"),
new NetBankingPayment("SBI")
};
for (PaymentMethod m : methods) {
m.pay(499.0); // Java picks the right pay() at RUNTIME
}Add WalletPayment tomorrow — the loop doesn’t change. Compare that with the alternative: an if (type.equals("upi")) ... else if (type.equals("card")) ... chain you must hunt down and edit in fifteen places every time a payment type appears. Polymorphism deletes that chain. This is exactly your RupeeRail BankAdapter design.
The gotcha that ships bugs · == lies for objects
Every object inherits three methods from Object — and two of them trip up almost everyone:
toString()— what printing the object shows. Default is garbage likePayment@6f2b958e. Override it to print something useful.equals()—==on objects compares references (same arrow into the heap?), not contents. Twonew UpiPayment("a@upi")are==-different even though logically identical. Overrideequalsto define what “same” means.hashCode()— a number hash-based collections use to find objects fast. Rule: if two objects areequals, they must share ahashCode. Break it andHashSet/HashMapsilently misbehave.
String s1 = new String("upi");
String s2 = new String("upi");
System.out.println(s1 == s2); // false — different heap objects
System.out.println(s1.equals(s2)); // true — same contentsBurn this in: == for primitives, equals for objects. This one line of knowledge prevents a whole category of bugs that AI-generated code happily ships.
Say it like this in the interview
“OOP bundles data and behaviour into objects. Encapsulation hides internals behind safe methods — a bank account guarding its balance. Abstraction exposes a simple surface and hides the how — a steering wheel. Inheritance lets a class reuse another’s code — a SavingsAccount is an Account with extra. Polymorphism lets the same call behave differently per type — shape.area() on a circle vs a square.”
Where you’ll use it — real life
Payment, Merchant, Transaction as classes with guarded fields — encapsulation and abstraction in your real job.
Beans, services, controllers are objects with clear jobs — OOP is the shape of every Java/C# backend.
Inheritance and interfaces let teams share code safely at scale.
Polymorphism lets you swap a SimBankAdapter for a RealBankAdapter with zero caller changes — your RupeeRail design.
Now YOU do the reps (design, not LeetCode)
OOP is practised by modelling. Do these on paper:
- Model a payment system: what classes? (Merchant, PaymentIntent, Ledger…) What data + methods does each own?
- Show encapsulation: which fields are private, what safe methods expose them?
- Show polymorphism: a
BankAdapterinterface with two implementations behaving differently.
Out loud, one real example each: “Explain encapsulation, abstraction, inheritance and polymorphism.” Fluent = interview-ready. Then log it in your Journal.
Check yourself — the questions Siemens actually asks
Answer out loud first, then check. If you can’t, you don’t have it yet.
Class = blueprint (loaded once). Object = instance on the heap. Variables hold references — arrows pointing at heap objects, not the objects themselves.
this.owner = owner need this?The parameter shadows the field. this.owner means “the field on this object”; without it you assign the parameter to itself and the field stays null.
It’s a public field with ceremony — any code can put the object in an invalid state. Expose business ops (deposit/withdraw) that enforce rules instead.
One. Static = one copy per class, shared by every object. Instance fields get one copy per object.
When the relationship isn’t truly is-a, when you need behaviour from more than one parent, or when behaviour should be swappable at runtime. Reach for composition behind an interface.
pay() to run?At runtime Java checks the actual object’s type (not the reference type) and dispatches to that class’s override — dynamic dispatch.
== return false for identical objects?== compares references — whether both point at the exact same heap object. Two separately created objects are different addresses. Use equals.
equals — why also hashCode?Hash collections find by hashCode first, then confirm with equals. If equal objects give different hash codes, a HashSet holds “duplicates” and a HashMap loses entries.
Model the payment system twice — once with inheritance (each type extends Payment), once with composition (a PaymentMethod interface + a Payment that has a method). Then add a fourth type, WalletPayment, to both and count the lines you touched. Fewer lines wins — that’s why real teams favour composition.
Related: C# (Java’s cousin) → · Back to your Siemens roadmap →