Skip to main content

OOPs Concepts in Java with Real-World Examples: Encapsulation, Inheritance, Polymorphism, Abstraction

OOPs concepts in Java explained with one bank-account example: encapsulation, inheritance, polymorphism and abstraction, plus class vs object, IS-A vs HAS-A, diagrams, real compiler errors and 15 interview questions. Verified on JDK 25.

Object-oriented programming (OOP) in Java means designing a program as a set of objects that hold data and the methods that work on it. Java’s four OOP concepts are encapsulation (keep data private behind methods), inheritance (a class reuses and extends another), polymorphism (the same method call runs different code depending on the object) and abstraction (callers see what an object does, not how it does it). This article explains each one with a single running example, a bank account, so you can see how the four ideas fit together instead of meeting them as four unrelated definitions. Every program and every line of output below was compiled and run on JDK 25. The code is inline on this page; there is no repository, and each file can be downloaded at the end.
Encapsulationhide the data,expose methodsInheritancereuse and extenda parent classPolymorphismone call, manybehavioursAbstractionshow what, hidehowAll four are demonstrated below with one bank-account domain and one runnable file.Encapsulation protects, inheritance reuses, polymorphism varies, abstraction simplifies.
The four boxes are the whole answer in one glance: protect, reuse, vary, simplify. The rest of the page is the same account domain, built up one concept at a time, with the compiler errors and runtime output that prove each claim.
Versions. Every program on this page was compiled and run on JDK 25.0.4.1 (Temurin, LTS, build 25.0.4.1+1-LTS). Where Java 8 behaved differently the text says so. The compiler errors and console output are quoted verbatim from those runs; the “Download” links at the end of each section point to the exact .java files.

Class vs object: the blueprint and the thing

A class is a description: what data an account has and what an account can do. An object is one actual account created from that description, living in memory with its own values. You write the class once and create as many objects as you need; each object has its own copy of the instance fields, which is why changing one customer’s balance does not touch another’s.
class Accountid, balancedeposit(), withdraw()object SB-101balance = 105000.0object CA-201balance = -4000.0Same class,separate state.
The arrows run from one blueprint to two independent objects. Everything that follows is a refinement of this picture: how much of the object is visible (encapsulation), how a class borrows from another (inheritance), how two objects answer the same call differently (polymorphism), and what a caller is allowed to know (abstraction).

Encapsulation: the account decides how its balance changes

Encapsulation means making the fields private and letting the outside world change them only through methods that enforce your rules. A bank account that exposes balance as a public field can be set to a million by any line of code in the program. A bank account that exposes deposit() and withdraw() can refuse a negative deposit.
Account private balancedeposit()withdraw()account.balance = 1e6blocked by javacCallers go through methods that validate; the direct write never compiles.
The methods are the only door, and the door has a lock: validation. Here is the account from the running example, with private state and rules in the mutators:
abstract class Account implements Statement {
    private final String id;
    private double balance;

    protected Account(String id, double opening) {
        if (opening < 0) throw new IllegalArgumentException("opening balance must be >= 0");
        this.id = id;
        this.balance = opening;
    }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("deposit must be positive");
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount > balance + overdraftLimit()) throw new IllegalStateException("insufficient funds");
        balance -= amount;
    }

    public double balance() { return balance; }
    public String id() { return id; }

    protected double overdraftLimit() { return 0; }

    public abstract double monthEndInterest();

    @Override
    public String summary() {
        return id + " | balance " + balance;
    }
}
Source: BankOop.java. The constructor refuses a negative opening balance, deposit refuses non-positive amounts, and withdraw refuses anything beyond the balance plus an overdraft limit that subclasses may set. Running the encapsulation section of the program:
$ java BankOop.java
-- Encapsulation: state changes only through rules --
SB-101 balance = 105000.0
rejected: deposit must be positive
rejected: insufficient funds
The two rejected: lines are the rules firing. Now try to bypass them from another class. private is enforced by the compiler, so this does not even build:
$ javac BrokenEncapsulation.java
BrokenEncapsulation.java:14: error: balance has private access in Account
        a.balance = 1_000_000;
         ^
1 error
Source: BrokenEncapsulation.java. The same file shows a surprise worth remembering: in Java private is a class-level rule, not an object-level one. The line a.balance = 1_000_000; inside main compiles because main lives in the class that nests Account; only the line inside the unrelated Outsider class is rejected (line 14).
Encapsulation is not just getters and setters. A class that makes every field private and then adds a public getter and setter for each one has hidden nothing. Expose behaviour (deposit), not storage. When you must hand out a collection, hand out a copy: the Customer class below returns List.copyOf(accounts), the same snapshot idea covered in Immutable Collections in Java. The same reasoning is why thread-safe classes keep their state private: see Top 40 Java Concurrency Interview Questions.
Going deeper: why immutability is the strongest form of encapsulation

If a field is private final and never mutated, no method can break it. That is why String is immutable and why records are popular for value types. Interview questions that start with “why is String immutable?” are really encapsulation questions; the worked answers are in 30 Java String Programs for Interviews.

Inheritance: SavingsAccount IS-A Account

Inheritance lets one class (the subclass) take the fields and methods of another (the superclass) and add or change behaviour. You declare it with extends. A SavingsAccount is an Account, so everything an account can do, a savings account can do, plus interest.
Account (abstract)id, balance, deposit(), withdraw()SavingsAccountadds: annual rate, interestCurrentAccountoverrides: overdraftLimit() = 5000
The arrows point from child to parent, the usual UML direction. The parent holds everything shared, so deposit() is written once and both children get it; each child adds only what is different. Here are the two subclasses:
class SavingsAccount extends Account {
    private final double annualRatePercent;

    SavingsAccount(String id, double opening, double annualRatePercent) {
        super(id, opening);
        this.annualRatePercent = annualRatePercent;
    }

    @Override
    public double monthEndInterest() {
        return balance() * annualRatePercent / 100 / 12;
    }
}

class CurrentAccount extends Account {
    CurrentAccount(String id, double opening) { super(id, opening); }

    @Override
    protected double overdraftLimit() { return 5_000; }

    @Override
    public double monthEndInterest() { return 0; }

    @Override
    public String summary() {
        return super.summary() + " | overdraft limit " + overdraftLimit();
    }
}
Source: BankOop.java. Two details are easy to miss. The constructors call super(id, opening) to initialise the parent part first; constructors are covered in depth on the Constructors in Java page. And CurrentAccount overrides overdraftLimit(), so the inherited withdraw() behaves differently for it:
$ java BankOop.java
-- Overdraft limit is inherited behaviour, overridden --
CA-201 balance after 24000 withdrawal = -4000.0
The current account went to -4000.0 after withdrawing 24,000 from 20,000, which a savings account would have refused with insufficient funds. The method body was never copied; the child changed one hook and the parent’s method picked it up. Java allows a class to extend only one class, but a class can implement many interfaces. That rule exists to avoid ambiguity about which parent’s field or method wins; the interface half of the story, including the diamond problem, is in Abstract Class vs Interface in Java. For two short stand-alone programs on inheritance, see the practice programs Inheritance Example in Java and Polymorphism Example in Java.

Polymorphism: one call, different behaviour

Polymorphism (“many forms”) means a variable of a parent type can refer to any child object, and a method call runs the child’s version. The calling code is written once against Account; which monthEndInterest() runs is decided at runtime by the actual object.
for (Account a : accounts)a.monthEndInterest()SavingsAccount: balance x rate / 100 / 12CurrentAccount: 0The loop never asks which kind of account it has; the JVM picks the method from the object.
The same line of code produces two results because the JVM looks at the real object, not the variable’s declared type. Running the polymorphism section:
$ java BankOop.java
-- Polymorphism: one call, different behaviour --
SB-101 (SavingsAccount) month-end interest = 350.00
CA-201 (CurrentAccount) month-end interest = 0.00
SB-101 returns 350.00 (105,000 at 4% a year, divided by 12) and CA-201 returns 0.00, from one loop body. This is runtime polymorphism (method overriding). Java also has compile-time polymorphism (method overloading: same name, different parameters, chosen by the compiler). The rules and traps of both, including which overload wins when several fit, are in Method Overloading vs Overriding in Java.
Going deeper: how the JVM dispatches an overridden call

For an overridden instance method, the JVM uses the object’s class to find the method at runtime (a virtual call, invokevirtual in bytecode). Static methods, private methods and fields are not dispatched this way; they are resolved from the declared type. The comparison table and a real run showing static-method and field hiding are in the overloading-vs-overriding article linked above.

Abstraction: depend on what, not how

Abstraction means exposing a simple contract and hiding the machinery behind it. In Java you build it with interfaces and abstract classes. In the example, Statement is a one-method contract. Code that prints statements needs to know nothing about savings rates or overdrafts:
Caller: printStatements()interface Statementsummary()SavingsAccount (rate hidden)CurrentAccount (overdraft hidden)
The dashed box is the abstraction, a contract with no behaviour of its own. The caller depends on it; the implementations hide everything else, and you can add a third account type without editing the caller. The program uses it like this:
$ java BankOop.java
-- Abstraction: callers depend on Statement only --
SB-101 | balance 105000.0
CA-201 | balance -4000.0 | overdraft limit 5000.0
Each account formatted itself, and the loop that printed them held only a Statement reference. Account itself is an abstract class: it declares public abstract double monthEndInterest(); with no body, so you cannot write new Account(...), and every concrete subclass must supply the method or the compiler refuses.
Abstraction vs encapsulation, in one sentence each. Encapsulation protects state: private fields behind validating methods. Abstraction reduces what you have to know: callers see a small interface instead of an implementation. They cooperate (an interface is only useful if the implementation behind it is hidden) but they answer different questions, and interviewers ask for the difference precisely because candidates blur them.

IS-A vs HAS-A: inheritance or composition?

IS-A is inheritance: a SavingsAccount is an Account. HAS-A is composition: a Customer has accounts, but a customer is not an account. In code, HAS-A is a field that refers to another object. Choosing wrongly produces designs like Customer extends ArrayList<Account>, which forces every ArrayList method onto your domain class.
SavingsAccountIS-AAccountCustomerHAS-AList<Account>inheritance: reuse + subtypecomposition: contains, delegates
The top row can be read aloud as “a savings account is an account”, the bottom row as “a customer has accounts”. If the sentence sounds wrong, the relationship is wrong. The last lines of the program test both sentences:
$ java BankOop.java
-- IS-A vs HAS-A --
SavingsAccount IS-A Account: true
Customer HAS-A list of accounts: 2
Customer IS-A Account: false
The rule of thumb that most Java style guides repeat is prefer composition over inheritance: inheritance couples a child to the parent’s implementation, while composition lets you swap the part. Use extends only when the IS-A sentence is true and you want to substitute the child anywhere the parent is accepted.

The complete running example

Show the full BankOop.java (about 125 lines)
import java.util.ArrayList;
import java.util.List;

public class BankOop {
    public static void main(String[] args) {
        Customer asha = new Customer("Asha");
        asha.open(new SavingsAccount("SB-101", 100_000, 4.0));
        asha.open(new CurrentAccount("CA-201", 20_000));

        System.out.println("-- Encapsulation: state changes only through rules --");
        Account sb = asha.accounts().get(0);
        sb.deposit(5_000);
        System.out.println(sb.id() + " balance = " + sb.balance());
        try {
            sb.deposit(-1);
        } catch (IllegalArgumentException e) {
            System.out.println("rejected: " + e.getMessage());
        }
        try {
            sb.withdraw(1_000_000);
        } catch (IllegalStateException e) {
            System.out.println("rejected: " + e.getMessage());
        }

        System.out.println("-- Polymorphism: one call, different behaviour --");
        for (Account a : asha.accounts()) {
            System.out.printf("%s (%s) month-end interest = %.2f%n",
                    a.id(), a.getClass().getSimpleName(), a.monthEndInterest());
        }

        System.out.println("-- Overdraft limit is inherited behaviour, overridden --");
        Account ca = asha.accounts().get(1);
        ca.withdraw(24_000);
        System.out.println(ca.id() + " balance after 24000 withdrawal = " + ca.balance());

        System.out.println("-- Abstraction: callers depend on Statement only --");
        for (Account a : asha.accounts()) {
            Statement s = a;
            System.out.println(s.summary());
        }

        System.out.println("-- IS-A vs HAS-A --");
        System.out.println("SavingsAccount IS-A Account: " + (sb instanceof Account));
        System.out.println("Customer HAS-A list of accounts: " + asha.accounts().size());
        System.out.println("Customer IS-A Account: " + ((Object) asha instanceof Account));
    }
}

interface Statement {
    String summary();
}

abstract class Account implements Statement {
    private final String id;
    private double balance;

    protected Account(String id, double opening) {
        if (opening < 0) throw new IllegalArgumentException("opening balance must be >= 0");
        this.id = id;
        this.balance = opening;
    }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("deposit must be positive");
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount > balance + overdraftLimit()) throw new IllegalStateException("insufficient funds");
        balance -= amount;
    }

    public double balance() { return balance; }
    public String id() { return id; }

    protected double overdraftLimit() { return 0; }

    public abstract double monthEndInterest();

    @Override
    public String summary() {
        return id + " | balance " + balance;
    }
}

class SavingsAccount extends Account {
    private final double annualRatePercent;

    SavingsAccount(String id, double opening, double annualRatePercent) {
        super(id, opening);
        this.annualRatePercent = annualRatePercent;
    }

    @Override
    public double monthEndInterest() {
        return balance() * annualRatePercent / 100 / 12;
    }
}

class CurrentAccount extends Account {
    CurrentAccount(String id, double opening) { super(id, opening); }

    @Override
    protected double overdraftLimit() { return 5_000; }

    @Override
    public double monthEndInterest() { return 0; }

    @Override
    public String summary() {
        return super.summary() + " | overdraft limit " + overdraftLimit();
    }
}

class Customer {
    private final String name;
    private final List<Account> accounts = new ArrayList<>();

    Customer(String name) { this.name = name; }

    void open(Account a) { accounts.add(a); }

    List<Account> accounts() { return List.copyOf(accounts); }
}
Download the code. BankOop.java · BrokenEncapsulation.java.

Run a runnable one with java File.java (JDK 11 or later; JDK 25 recommended). Files starting with Broken are meant to fail to compile.

Common interview questions

1. What are the four pillars of OOP in Java?

Encapsulation, inheritance, polymorphism and abstraction. Encapsulation hides state behind methods, inheritance lets a class reuse another, polymorphism lets one call run different code per object, and abstraction exposes a contract while hiding the implementation.

2. What is the difference between a class and an object?

A class is the blueprint (fields and methods). An object is one instance created with new, with its own copy of the instance fields. Many objects can come from one class.

3. What is the difference between encapsulation and abstraction?

Encapsulation protects state with private fields and validating methods. Abstraction hides complexity behind a simple interface or abstract class. One is about data protection, the other about reducing what callers must know.

4. How do you achieve encapsulation in Java?

Make fields private, expose behaviour through public methods that validate input, and return copies or immutable views of mutable internals.

5. What is the difference between method overloading and overriding?

Overloading: same name, different parameter lists, resolved by the compiler (compile-time polymorphism). Overriding: a subclass replaces an inherited instance method with the same signature, resolved at runtime by the object’s class. See the full comparison in the overloading-vs-overriding article.

6. Does Java support multiple inheritance?

Not for classes: a class extends exactly one class. A class can implement any number of interfaces, and since Java 8 interfaces can carry default methods, so a class that inherits two conflicting defaults must override the method and choose (A.super.hello()). This page’s sibling article shows the exact compiler error.

7. What is the difference between IS-A and HAS-A?

IS-A is inheritance (extends/implements): the child is a kind of the parent. HAS-A is composition: one class holds a reference to another. Prefer composition unless a true substitutable subtype is intended.

8. What is runtime polymorphism?

A parent-typed reference calls an overridden method and the JVM runs the version belonging to the actual object, decided at runtime through dynamic dispatch.

9. Can you override a static or private method?

No. Static methods are hidden, not overridden, and the version called depends on the reference type. Private methods are not inherited, so a subclass method with the same name is an unrelated method.

10. Can a constructor be inherited or overridden?

No. Constructors belong to their class. A subclass constructor must call a superclass constructor with super(...), explicitly or implicitly. See the constructors article.

11. What is an abstract class and can it have a constructor?

A class declared abstract cannot be instantiated and may contain abstract methods. It can have constructors, which run when a concrete subclass is created. The Account constructor above does exactly that.

12. What is the difference between an abstract class and an interface?

An abstract class can hold instance state and constructors and a class can extend only one. An interface defines a contract (with default, static and private methods since Java 8/9), has no instance state, and a class can implement many. See the dedicated 12-row comparison article.

13. Why is String immutable and how is that related to encapsulation?

Immutability is the strongest encapsulation: no code can change the value, so strings are safe as map keys, thread-safe and cacheable. See the String interview programs article for worked answers.

14. What does final do on a class, method and variable?

Final class: cannot be extended. Final method: cannot be overridden. Final variable: cannot be reassigned (the object it refers to can still change). The three-keyword comparison is in the final-vs-finally-vs-finalize article.

15. When would you use composition instead of inheritance?

When the relationship is HAS-A, when you want to swap behaviour at runtime, or when subclassing would expose methods that break the parent’s invariants (for example, extending ArrayList to build a domain class). The collections interview guide shows why this matters for real JDK classes.

Common mistakes

  • Public fields. Anyone can set an invalid value; the compiler cannot help you later. Make fields private and validate in methods.
  • Getters and setters for everything. That exposes storage, not behaviour. Ask what the object should do.
  • Inheriting to reuse code. If “B is an A” sounds wrong aloud, use a field instead of extends.
  • Returning internal mutable collections. Return List.copyOf(list) or an unmodifiable view, otherwise callers can mutate your state.
  • Calling an overridable method from a constructor. The subclass override can run before the subclass fields are assigned; the Flexible Constructor Bodies in Java 25 (JEP 513) article shows the exact failure.
  • Confusing overloading with overriding. Overloads are chosen at compile time from the declared types; overrides are chosen at runtime from the object.
  • Assuming private is per object. It is per class: another instance of the same class can read your private fields.

FAQ

What are OOPs concepts in Java?

The four core OOP concepts in Java are encapsulation, inheritance, polymorphism and abstraction. Related ideas that interviewers also ask about are class vs object, IS-A vs HAS-A, interfaces and abstract classes.

What are OOPs concepts in Java with examples?

Use one domain, such as a bank account: private balance with validated deposit and withdraw methods (encapsulation), SavingsAccount extending Account (inheritance), one loop calling monthEndInterest on different account types (polymorphism), and a Statement interface callers depend on (abstraction). All of it is in the runnable BankOop.java on this page.

What is the difference between abstraction and encapsulation in Java?

Encapsulation hides data by making fields private and exposing validating methods. Abstraction hides complexity by exposing an interface or abstract class. Encapsulation protects state; abstraction simplifies use.

Is Java 100% object oriented?

No. Java has primitive types (int, double, boolean and others) that are not objects, and static members that belong to classes rather than objects. Everything else you write lives inside a class.

Does Java support multiple inheritance?

Not with classes. A class extends one class, but it can implement multiple interfaces. Conflicting default methods must be resolved explicitly in the implementing class.

Where to go next

Other articles in this Java fundamentals series:

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.