Home Interview Java OOP Interview Questions: Polymorphism, Abstraction & More

Java OOP Interview Questions: Polymorphism, Abstraction & More

In Plain English 🔥
Think of a TV remote. You press 'Volume Up' and the TV gets louder — you don't care how the TV processes that signal internally. OOP works the same way: you interact with objects through simple buttons (methods), while the complex wiring stays hidden inside. Polymorphism means that same 'Volume Up' button works on a Samsung AND a Sony. Abstraction is why you never need to open the TV to change the channel. That's Java OOP in one analogy.
⚡ Quick Answer
Think of a TV remote. You press 'Volume Up' and the TV gets louder — you don't care how the TV processes that signal internally. OOP works the same way: you interact with objects through simple buttons (methods), while the complex wiring stays hidden inside. Polymorphism means that same 'Volume Up' button works on a Samsung AND a Sony. Abstraction is why you never need to open the TV to change the channel. That's Java OOP in one analogy.

If you're interviewing for a Java role — junior, mid, or senior — OOP questions are guaranteed to show up. Not because interviewers love theory, but because the way you answer reveals how you actually design software. A candidate who can recite four pillars from memory is forgettable. A candidate who explains WHY encapsulation prevents bugs in a multi-team codebase gets the offer.

The problem is that most resources teach OOP as a list of definitions. That leaves you able to parrot answers but unable to handle the natural follow-up: 'Can you give me a real-world example?' or 'How does that differ from an abstract class?' Those follow-ups are where interviews are actually won or lost.

After working through this article, you'll be able to explain every major OOP concept with a concrete analogy, write runnable code that demonstrates each idea, spot the three classic mistakes candidates make, and handle the tricky follow-up questions interviewers use to separate the memorisers from the thinkers.

The Four Pillars — What They Are and Why Each One Exists

Every Java OOP interview starts here. The four pillars are Encapsulation, Abstraction, Inheritance, and Polymorphism. But interviewers don't want a dictionary. They want to know you understand the problem each pillar solves.

Encapsulation solves the 'who changed my data?' problem. By bundling data with the methods that operate on it and hiding the internals, you prevent other parts of the system from putting an object into an invalid state. Think of a bank account — you never want external code to set the balance directly to a negative number.

Abstraction solves the 'I don't need to know how' problem. You expose only what's necessary and hide everything else. This is why you can call list.sort() without understanding TimSort.

Inheritance solves the 'don't repeat yourself' problem. Common behaviour lives in a parent class; child classes inherit it and specialise where needed.

Polymorphism solves the 'treat different things uniformly' problem. One interface, many implementations. This is what makes your code extensible without modification — the Open/Closed Principle in action.

BankAccountEncapsulation.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
public class BankAccountEncapsulation {

    // --- Encapsulation Demo ---
    // The balance field is private — no outside code can set it directly.
    // This means we can enforce rules (e.g. no negative balances) in ONE place.
    static class BankAccount {
        private double balance;        // hidden internal state
        private String accountHolder;  // also hidden

        public BankAccount(String accountHolder, double openingBalance) {
            this.accountHolder = accountHolder;
            // Using our own setter so the validation rule applies at construction too
            setBalance(openingBalance);
        }

        // Public getter — read-only access from outside
        public double getBalance() {
            return balance;
        }

        // Public method to deposit — enforces business logic
        public void deposit(double amount) {
            if (amount <= 0) {
                throw new IllegalArgumentException("Deposit amount must be positive.");
            }
            balance += amount; // safe: we control every modification
        }

        // Public method to withdraw — enforces business logic
        public void withdraw(double amount) {
            if (amount > balance) {
                throw new IllegalStateException("Insufficient funds.");
            }
            balance -= amount;
        }

        // Private setter — only used internally so we keep validation centralised
        private void setBalance(double balance) {
            if (balance < 0) {
                throw new IllegalArgumentException("Opening balance cannot be negative.");
            }
            this.balance = balance;
        }

        @Override
        public String toString() {
            return accountHolder + "'s balance: $" + balance;
        }
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount("Alice", 500.00);
        System.out.println(account);            // initial state

        account.deposit(200.00);
        System.out.println(account);            // after deposit

        account.withdraw(100.00);
        System.out.println(account);            // after withdrawal

        // This next line would throw IllegalStateException — try it!
        // account.withdraw(1000.00);

        // Without encapsulation, any code could do: account.balance = -999;
        // With encapsulation, that line won't even compile.
    }
}
▶ Output
Alice's balance: $500.0
Alice's balance: $700.0
Alice's balance: $600.0
⚠️
Interview Gold:When asked about encapsulation, mention that it's not just about getters and setters — it's about preserving invariants. A BankAccount with a public balance field has no encapsulation even if it technically compiles. Interviewers love this distinction.

Polymorphism vs Abstraction — The Question That Trips Everyone Up

These two are the most commonly confused pillars, and interviewers exploit that confusion heavily. Here's the clean separation: Abstraction is about DESIGN — hiding complexity behind a simple interface. Polymorphism is about BEHAVIOUR — the same call producing different results depending on the actual object at runtime.

Abstraction is implemented in Java via abstract classes and interfaces. You define WHAT something must do without specifying HOW. Polymorphism is what happens at runtime when Java resolves which overridden method to actually call.

A classic follow-up: 'What's the difference between method overloading and method overriding?' Overloading is compile-time polymorphism — same method name, different parameters, resolved by the compiler. Overriding is runtime polymorphism — same signature in parent and child, resolved by the JVM based on the actual object type.

The real-world consequence: if you write code against an interface (abstraction), you can swap out implementations without touching the calling code (polymorphism). This is the core of dependency injection frameworks like Spring.

NotificationPolymorphism.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
import java.util.List;
import java.util.ArrayList;

public class NotificationPolymorphism {

    // ABSTRACTION: This interface declares WHAT a notifier must do.
    // It says nothing about HOW — that's left to implementations.
    interface Notifier {
        void sendAlert(String message);
        
        // Default method: shared behaviour without forcing subclasses to override
        default String formatMessage(String raw) {
            return "[ALERT] " + raw.toUpperCase();
        }
    }

    // POLYMORPHISM: Each class defines its own version of sendAlert.
    // The caller never needs to know which one it's talking to.
    static class EmailNotifier implements Notifier {
        private String recipientEmail;

        EmailNotifier(String recipientEmail) {
            this.recipientEmail = recipientEmail;
        }

        @Override
        public void sendAlert(String message) {
            // In production this would call an email API
            System.out.println("EMAIL to " + recipientEmail + ": " + formatMessage(message));
        }
    }

    static class SmsNotifier implements Notifier {
        private String phoneNumber;

        SmsNotifier(String phoneNumber) {
            this.phoneNumber = phoneNumber;
        }

        @Override
        public void sendAlert(String message) {
            // In production this would call an SMS gateway
            System.out.println("SMS to " + phoneNumber + ": " + formatMessage(message));
        }
    }

    static class SlackNotifier implements Notifier {
        private String channel;

        SlackNotifier(String channel) {
            this.channel = channel;
        }

        @Override
        public void sendAlert(String message) {
            System.out.println("SLACK #" + channel + ": " + formatMessage(message));
        }
    }

    // KEY POINT: This method takes a List<Notifier> — it doesn't care about
    // the concrete type. Add a new PushNotifier tomorrow and this code needs
    // zero changes. That's polymorphism making your code Open/Closed.
    static void broadcastAlert(List<Notifier> notifiers, String message) {
        for (Notifier notifier : notifiers) {
            notifier.sendAlert(message); // JVM picks the right implementation at runtime
        }
    }

    public static void main(String[] args) {
        List<Notifier> alertChannels = new ArrayList<>();
        alertChannels.add(new EmailNotifier("ops@company.com"));
        alertChannels.add(new SmsNotifier("+1-555-0199"));
        alertChannels.add(new SlackNotifier("incidents"));

        broadcastAlert(alertChannels, "server CPU at 98%");
    }
}
▶ Output
EMAIL to ops@company.com: [ALERT] SERVER CPU AT 98%
SMS to +1-555-0199: [ALERT] SERVER CPU AT 98%
SLAK #incidents: [ALERT] SERVER CPU AT 98%
🔥
The Real Distinction:Abstraction asks 'what contract must be fulfilled?' — Polymorphism asks 'which fulfillment runs right now?' They work together: abstraction creates the contract, polymorphism executes it. Nail this in an interview and you'll stand out from candidates who treat them as synonyms.

Abstract Classes vs Interfaces — When to Use Which

This is arguably the most asked Java OOP question at mid-level interviews. Both enforce a contract. Both support polymorphism. But they're not interchangeable, and using the wrong one reveals a gap in design thinking.

Use an abstract class when you have a true 'is-a' relationship AND shared state or behaviour to inherit. Example: a Vehicle abstract class that stores fuelLevel and has a concrete refuel() method that all vehicles share. Child classes extend this and implement their own accelerate() method.

Use an interface when you're defining a capability that could apply to completely unrelated classes. Serializable, Comparable, and Runnable are capabilities, not identities. A Dog and a BankTransaction can both be Serializable — that doesn't mean they share a parent.

Since Java 8, interfaces can have default and static methods, which blurs the line slightly. The practical rule: if you need instance state (fields) in the shared contract, you need an abstract class. Interfaces can't hold instance state.

A common interview follow-up: 'Can a class extend multiple abstract classes?' No — Java uses single inheritance for classes to avoid the diamond problem. But a class can implement multiple interfaces.

VehicleHierarchy.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
public class VehicleHierarchy {

    // ABSTRACT CLASS: appropriate here because:
    // 1. All vehicles genuinely 'are' vehicles (is-a relationship)
    // 2. They share real instance state (fuelLevel, make)
    // 3. They share some concrete behaviour (refuel)
    static abstract class Vehicle {
        protected String make;         // shared state — inherited by all subclasses
        protected double fuelLevel;    // shared state

        Vehicle(String make, double initialFuel) {
            this.make = make;
            this.fuelLevel = initialFuel;
        }

        // Concrete method — defined once, works for every vehicle
        public void refuel(double litres) {
            fuelLevel += litres;
            System.out.println(make + " refuelled. Fuel level: " + fuelLevel + "L");
        }

        // Abstract method — subclasses MUST provide their own implementation
        public abstract void accelerate();

        // Abstract method — different vehicles report differently
        public abstract String getType();
    }

    // INTERFACE: appropriate for capabilities that cross class hierarchies
    // A Car and a Drone can both be GPS-tracked — they don't share a parent
    interface GpsTrackable {
        String getCurrentLocation();  // every implementor must define this

        // Default method — shared utility behaviour added in Java 8
        default void printLocation() {
            System.out.println("Current position: " + getCurrentLocation());
        }
    }

    // Car extends Vehicle (is-a) AND implements GpsTrackable (has-a capability)
    // Java allows one parent class + multiple interfaces — best of both worlds
    static class Car extends Vehicle implements GpsTrackable {
        private int horsepower;
        private double latitude;
        private double longitude;

        Car(String make, double fuel, int horsepower) {
            super(make, fuel);         // call parent constructor to set shared state
            this.horsepower = horsepower;
            this.latitude = 51.5074;   // London by default for demo
            this.longitude = -0.1278;
        }

        @Override
        public void accelerate() {
            // Car-specific implementation — burns fuel faster with more HP
            fuelLevel -= (horsepower * 0.01);
            System.out.println(make + " car accelerates. Fuel remaining: " + fuelLevel + "L");
        }

        @Override
        public String getType() {
            return "Petrol Car";
        }

        @Override
        public String getCurrentLocation() {
            return latitude + ", " + longitude;  // would call GPS API in production
        }
    }

    static class ElectricBike extends Vehicle {
        private int batteryPercent;

        ElectricBike(String make, double fuel, int battery) {
            super(make, fuel);
            this.batteryPercent = battery;
        }

        @Override
        public void accelerate() {
            batteryPercent -= 2;  // electric: drains battery, not fuel
            System.out.println(make + " e-bike accelerates. Battery: " + batteryPercent + "%");
        }

        @Override
        public String getType() {
            return "Electric Bike";
        }
    }

    public static void main(String[] args) {
        Car tesla = new Car("Tesla", 100.0, 300);
        ElectricBike specialised = new ElectricBike("Specialised", 0, 100);

        // Polymorphism: same method call, different behaviour
        Vehicle[] garage = { tesla, specialised };
        for (Vehicle v : garage) {
            System.out.println("--- " + v.getType() + " ---");
            v.accelerate();
        }

        System.out.println();

        // GpsTrackable: only Tesla implements it — checked at compile time
        tesla.printLocation();   // uses default interface method

        // Refuel is concrete in parent — works without override
        tesla.refuel(20.0);
    }
}
▶ Output
--- Petrol Car ---
Tesla car accelerates. Fuel remaining: 97.0L
--- Electric Bike ---
Specialised e-bike accelerates. Battery: 98%

Current position: 51.5074, -0.1278
Tesla refuelled. Fuel level: 117.0L
⚠️
Watch Out:A very common interview trap: 'From Java 8, interfaces have default methods — so are they just abstract classes now?' No. Interfaces still cannot hold instance state (fields with values per object). That single difference drives the entire design decision. If your shared contract needs to track state per object, you need an abstract class.

Inheritance Pitfalls and the Liskov Substitution Principle

Inheritance looks clean on paper but is the most misused OOP feature in real codebases. The classic mistake: using inheritance for code reuse when there's no genuine 'is-a' relationship. Stack extending Vector in Java's own standard library is the canonical example of this done badly — a Stack is NOT a Vector, but Java's designers used inheritance for convenience, which meant Stack accidentally exposed methods like add(int index, Object element) that make no logical sense for a stack.

The Liskov Substitution Principle (LSP) is the interview gold standard here. It says: if you replace a parent with any of its subtypes, the program should still behave correctly. A Square extending Rectangle violates LSP — if you set width on a Square it must also change the height, which breaks any code that expects to set width and height independently.

When LSP is in danger, favour composition over inheritance. Instead of Square extending Rectangle, give Square a Dimensions object internally.

In interviews, mention LSP and give the Square/Rectangle example. Fewer than 10% of candidates do. It immediately signals that you think about design consequences, not just syntax.

LiskovSubstitution.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
public class LiskovSubstitution {

    // GOOD INHERITANCE: Penguin violating LSP — let's see what goes wrong first,
    // then we'll fix it with the correct design.

    // Scenario: A payroll system processes different employee types.
    // Base class represents any person on payroll.
    static abstract class PayrollEmployee {
        protected String name;
        protected double baseSalary;

        PayrollEmployee(String name, double baseSalary) {
            this.name = name;
            this.baseSalary = baseSalary;
        }

        // Every employee gets paid — the amount may vary by type
        public abstract double calculateMonthlyPay();

        public String getName() { return name; }
    }

    // FullTimeEmployee: gets exactly their monthly salary
    static class FullTimeEmployee extends PayrollEmployee {
        FullTimeEmployee(String name, double annualSalary) {
            super(name, annualSalary / 12); // store as monthly
        }

        @Override
        public double calculateMonthlyPay() {
            return baseSalary;  // straightforward
        }
    }

    // Contractor: paid by hours worked each month
    static class Contractor extends PayrollEmployee {
        private double hourlyRate;
        private int hoursWorkedThisMonth;

        Contractor(String name, double hourlyRate, int hoursWorked) {
            super(name, 0); // contractors don't have a base salary
            this.hourlyRate = hourlyRate;
            this.hoursWorkedThisMonth = hoursWorked;
        }

        @Override
        public double calculateMonthlyPay() {
            return hourlyRate * hoursWorkedThisMonth;
        }
    }

    // BonusEligibleEmployee: full-timer who also gets a performance bonus
    static class BonusEligibleEmployee extends FullTimeEmployee {
        private double bonusMultiplier;  // e.g. 1.2 = 20% bonus on top of salary

        BonusEligibleEmployee(String name, double annualSalary, double bonusMultiplier) {
            super(name, annualSalary);
            this.bonusMultiplier = bonusMultiplier;
        }

        @Override
        public double calculateMonthlyPay() {
            // LSP is respected: substituting this for FullTimeEmployee still makes sense.
            // We return MORE than base pay, never LESS or something nonsensical.
            return super.calculateMonthlyPay() * bonusMultiplier;
        }
    }

    // This method works on ANY PayrollEmployee — it doesn't care about the subtype.
    // LSP guarantees this is safe: every subtype correctly fulfils the contract.
    static void processPayroll(PayrollEmployee[] employees) {
        double totalPayroll = 0;
        for (PayrollEmployee employee : employees) {
            double pay = employee.calculateMonthlyPay(); // runtime polymorphism
            totalPayroll += pay;
            System.out.printf("%-25s $%.2f%n", employee.getName(), pay);
        }
        System.out.printf("%-25s $%.2f%n", "TOTAL PAYROLL:", totalPayroll);
    }

    public static void main(String[] args) {
        PayrollEmployee[] team = {
            new FullTimeEmployee("Sarah Chen", 84000),           // $7,000/month
            new Contractor("Marcus Webb", 95.00, 160),           // $15,200/month
            new BonusEligibleEmployee("Priya Patel", 96000, 1.25) // $10,000/month
        };

        System.out.println("=== Monthly Payroll Run ===");
        processPayroll(team);
    }
}
▶ Output
=== Monthly Payroll Run ===
Sarah Chen $7000.00
Marcus Webb $15200.00
Priya Patel $10000.00
TOTAL PAYROLL: $32200.00
🔥
Interview Gold:If an interviewer asks about inheritance best practices, say 'I try to follow LSP — every subtype should be substitutable for its parent without breaking callers. When I can't guarantee that, it's usually a sign I need composition rather than inheritance.' This one sentence shows you know SOLID principles without being asked directly.
AspectAbstract ClassInterface
Can hold instance state (fields)Yes — instance fields allowedNo — only static final constants
ConstructorYes — can define constructorsNo — interfaces have no constructors
Inheritance limitSingle parent class onlyA class can implement unlimited interfaces
Method types allowedAbstract + concrete + staticAbstract + default + static (Java 8+)
Access modifiers on methodsAny modifier (private, protected, public)Public by default (private since Java 9)
Best used whenShared state + is-a relationship existsShared capability across unrelated classes
Real Java exampleAbstractList in java.utilComparable, Runnable, Serializable

🎯 Key Takeaways

  • Encapsulation isn't about getters and setters — it's about protecting invariants so no external code can put your object into an illegal state.
  • Abstraction defines the contract (what must be done); polymorphism executes it (which version runs at runtime). They're partners, not synonyms.
  • Choose abstract class when subtypes share instance state or a genuine is-a relationship. Choose interface for capabilities that apply across unrelated class hierarchies.
  • Liskov Substitution Principle is your inheritance sanity check: if you can't swap a subtype in wherever the parent is expected without breaking anything, you've got bad inheritance — fix it with composition.

⚠ Common Mistakes to Avoid

  • Mistake 1: Confusing overloading with overriding — Candidates say 'method overriding' when they mean 'method overloading', or claim both are runtime polymorphism. Overloading is resolved at COMPILE time by the compiler based on parameter types. Overriding is resolved at RUNTIME by the JVM based on the actual object type. To fix: remember the rule — overLOADing = LOts of signatures, resolved early. overRIDing = the child RIDes over the parent, resolved late.
  • Mistake 2: Thinking private fields are 'inherited' — Candidates say child classes inherit everything from the parent, including private fields. Private fields exist in the child object's memory but are NOT accessible by name in the child class. The child can only reach them via public or protected getter/setter methods defined in the parent. To fix: declare fields as protected if child classes genuinely need direct access, but prefer private + accessor methods to preserve encapsulation.
  • Mistake 3: Using an interface purely because 'it allows multiple inheritance' — Choosing an interface over an abstract class solely to avoid Java's single-class inheritance limit. If your types genuinely share state and behaviour via is-a, forcing an interface means copying default implementations or creating awkward workarounds. To fix: ask 'do these types share state?' first. If yes, abstract class. Then ask 'do they need additional capabilities?' — add interfaces on top for those capabilities.

Interview Questions on This Topic

  • QWhat's the difference between an abstract class and an interface in Java, and how do you decide which to use in a real design?
  • QCan you explain the Liskov Substitution Principle with an example — and tell me about a time Java's own standard library violates it?
  • QIf method overriding is runtime polymorphism, what happens when you call an overridden method from a parent class constructor — which version runs?

Frequently Asked Questions

What are the four pillars of OOP in Java?

Encapsulation (protecting internal state), Abstraction (hiding complexity behind simple interfaces), Inheritance (reusing and extending behaviour from a parent class), and Polymorphism (one method call behaving differently depending on the actual object type at runtime). Each pillar solves a distinct design problem — they work together rather than independently.

Can a Java interface have a constructor?

No. Interfaces cannot be instantiated directly, so they have no constructors. Since Java 8, interfaces can have default and static methods, and since Java 9 they can have private methods — but none of these are constructors. If you need initialisation logic, you need an abstract class.

What is the difference between method overloading and method overriding in Java?

Overloading is compile-time polymorphism: multiple methods in the same class share a name but differ in parameter type or count, and the compiler decides which to call. Overriding is runtime polymorphism: a subclass provides its own implementation of a parent method with the identical signature, and the JVM decides which to call based on the actual object type — not the reference type.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousTop 50 Java Interview QuestionsNext →Java Collections Interview Questions
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged