Career OS

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 you’ll learn
  • 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
✅ After this you’ll be able to
  • 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);
Stack (references)
a1 ──▶
a2 ──▶
Heap (real objects)
Ravi · 5000
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;
}
🚫 Auto-generated style
  • Getter + setter for every field
  • setBalance(x)
  • Rules scattered across the codebase
  • Any code can break the object
✅ Designed style
  • 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.”

Account · deposit() withdraw() ▲ extends
SavingsAccount · + addInterest() CurrentAccount · + overdraft()
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
}
🧬 Inheritance (is-a)
  • Fixed at compile time
  • Can’t swap behaviour later — parent is parent
  • Tight coupling — parent changes break children
  • One parent max
🧩 Composition (has-a)
  • 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 · area() ▲ implemented by
Circle → πr² Square → side²
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:

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 contents

Burn 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

🏦 Your Ayris code

Payment, Merchant, Transaction as classes with guarded fields — encapsulation and abstraction in your real job.

🌱 Spring / .NET

Beans, services, controllers are objects with clear jobs — OOP is the shape of every Java/C# backend.

🧩 Reuse

Inheritance and interfaces let teams share code safely at scale.

🔌 Swappable parts

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:

🗣️ The 2-minute explain test

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.

Q · Class vs object in memory?

Class = blueprint (loaded once). Object = instance on the heap. Variables hold references — arrows pointing at heap objects, not the objects themselves.

Q · Why does 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.

Q · Why is a setter on every field a mistake?

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.

Q · A static field, two objects — how many copies?

One. Static = one copy per class, shared by every object. Instance fields get one copy per object.

Q · When is inheritance the wrong tool?

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.

Q · How does the loop know which 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.

Q · Why does == 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.

Q · Override 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.

🛠️ Build it twice to feel the difference

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 →

Saves your progress on this device.
00:00