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.Encapsulation: the account decides how its balance changes
Encapsulation means making the fieldsprivate 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.
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: theCustomerclass below returnsList.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 withextends. A SavingsAccount is an Account, so everything an account can do, a savings account can do, plus interest.
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 againstAccount; which monthEndInterest() runs is decided at runtime by the actual object.
$ 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:
$ 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: aSavingsAccount 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.
$ 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 withjava File.java(JDK 11 or later; JDK 25 recommended). Files starting withBrokenare 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
privateis 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
- Polymorphism Example in Java and Inheritance Example in Java (short practice programs)
- Top 40 Java Collections Interview Questions (encapsulation, equals/hashCode and immutability in the JDK itself)
- Top 40 Java Concurrency Interview Questions (why shared mutable state needs protecting)
- 30 Java String Programs for Interviews and Top 50 Java Stream API Coding Interview Questions (applying the ideas in coding rounds)
- Java Interview Preparation Guide (the full topic map)
No Comments yet!