Abstraction in Java
-
Last Updated: July 27, 2025
-
By: javahandson
-
Series
Learn Java in a easy way
Abstraction in Java means hiding the messy details and showing only what matters. This guide covers abstract classes, interfaces, multiple inheritance, and the rules that decide which one you should pick.
Abstraction is one of the four pillars of object-oriented programming. It also has the worst reputation for being explained badly.
Textbooks say it “hides implementation details”. True, but that sentence teaches nobody anything.
Here is a plainer version. Abstraction lets you use something without understanding how it works inside.
You do that every day. You send a text without knowing how a cell tower routes it, and you feel no loss.
Java gives you two tools for this: abstract classes and interfaces. We will look at both, compare them honestly, and end with a program you can run.
Abstraction shows the essential features of an object and hides the rest. You focus on what it does, never on how.
Think about driving. You turn a wheel, press a pedal, and the car responds.
Behind that simple set of controls sits a gearbox, a fuel injector, and a few kilometres of wiring. None of it reaches you.
The car exposes a tiny interface and hides an enormous machine. That is abstraction in one picture.
Better still, the engine can change completely. Swap petrol for electric and your pedals still work the same way.
Interviewers love this question, because the two ideas sound identical. They are not.
One picks the shape of your API. The other stops outsiders from reaching in and breaking things.
They work together in practice. Read our guide on encapsulation in Java for the other half of the story.
Java gives you exactly two constructs for this job:
Both stop you from creating an object directly. Both force a real class to fill in the blanks.
An abstract class sits halfway between an idea and a real class.
Add the abstract keyword to the class declaration. From then on, nobody can call new on it.
Inside, you can mix two kinds of methods. An abstract method ends at a semicolon with no body, and a concrete method has a normal body.
The unfinished methods become a to-do list. Any subclass must complete that list before it can exist.
Every animal eats the same way, roughly. Each one makes a different sound.
package com.java.handson.abstraction;
abstract class Animal {
String name;
Animal(String name) {
this.name = name;
}
// Abstract: every subclass must write its own version
abstract void makeSound();
// Concrete: shared by every subclass
void eat() {
System.out.println(name + " is eating.");
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " says: Woof! Woof!");
}
}
class Cat extends Animal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " says: Meow! Meow!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog("Tommy");
Animal cat = new Cat("Billi");
dog.makeSound();
dog.eat();
cat.makeSound();
cat.eat();
}
}
// Output:
// Tommy says: Woof! Woof!
// Tommy is eating.
// Billi says: Meow! Meow!
// Billi is eating.Notice the variable types in main. We declared Animal, not Dog, so the calling code never learns which animal it holds.
That single choice is the abstraction. The caller knows the promise, and the subclass keeps it.
Meanwhile eat lives in one place. Add ten more animals and you still write that method once.
A handful of rules govern abstract classes, and interviewers ask about all of them:
That last rule creates a useful escape hatch. Middle layers in a hierarchy can stay abstract and pass the work down.
Wait, you cannot instantiate the class, so why does it own a constructor?
Because subclasses still need it. When you call new Dog(“Tommy”), the Dog constructor calls super(name) and the Animal constructor runs.
Its job is initialising the shared state, not creating a standalone object. Fields like name get set exactly once, in one place.
Interfaces cannot do this at all. No constructors, no instance fields, no shared setup.
Reach for an abstract class when these signs appear:
Our dedicated guide on the abstract class in Java covers these cases in more depth.
Let us trace the animal program step by step. No magic, just six short steps.
Look hard at step four. The label on the box says Animal. The thing inside the box is a Dog.
Java reads the object, not the label. So the Dog version of makeSound wins.
That is the whole trick in one line. The type you write decides what you may call, and the object decides what runs.
An interface takes abstraction further. It describes capability and nothing else.
Picture a job description. It lists the tasks the role demands, and it says nothing about who fills it.
An interface works the same way. It names the methods, and every implementing class supplies the bodies.
package com.java.handson.abstraction;
interface Student {
void study();
void payFees();
}Both methods are implicitly public and abstract. Writing those keywords yourself adds noise and changes nothing.
Two very different students can honour the same contract.
package com.java.handson.abstraction;
class SchoolStudent implements Student {
private final String name;
SchoolStudent(String name) {
this.name = name;
}
@Override
public void study() {
System.out.println(name + " is studying school subjects.");
}
@Override
public void payFees() {
System.out.println(name + " pays fees of 10000 Rs.");
}
}
class CollegeStudent implements Student {
private final String name;
CollegeStudent(String name) {
this.name = name;
}
@Override
public void study() {
System.out.println(name + " is preparing for college semesters.");
}
@Override
public void payFees() {
System.out.println(name + " pays fees of 100000 Rs.");
}
}
public class Demo {
public static void main(String[] args) {
Student school = new SchoolStudent("Suraj");
school.study();
school.payFees();
Student college = new CollegeStudent("Shweta");
college.study();
college.payFees();
}
}
// Output:
// Suraj is studying school subjects.
// Suraj pays fees of 10000 Rs.
// Shweta is preparing for college semesters.
// Shweta pays fees of 100000 Rs.Watch the overrides. Each one must be public, because interface methods are public and Java forbids narrowing that.
Now imagine adding an EveningStudent class next month. The main method would not change by a single character.
Interfaces grew over the years. Here is the current picture:
| Member | Allowed? | Notes |
|---|---|---|
| Abstract methods | Yes | Implicitly public and abstract |
| Constants | Yes | Implicitly public static final |
| default methods | Yes, since Java 8 | Carry a body that subclasses may override |
| static methods | Yes, since Java 8 | Called on the interface name |
| private methods | Yes, since Java 9 | Helpers shared by default methods |
| Instance fields | No | Interfaces hold no per-object state |
| Constructors | No | Nothing to construct |
One trap hides in that constants row. Every field you declare turns into a public static final constant, whether you meant it or not.
Java 8 added default methods to solve a versioning problem. Adding a method to an old interface used to break every existing implementation.
A default method ships with a body, so old classes keep compiling.
interface Student {
void study();
void payFees();
// Existing classes inherit this automatically
default void introduce() {
System.out.println("I am a student, and " + describe());
}
// Java 9 and later: a private helper for the default method
private String describe() {
return "I attend classes regularly.";
}
// Called as Student.minimumAge(), never on an object
static int minimumAge() {
return 5;
}
}Use default methods sparingly. They are a compatibility tool, not a place to park your business logic.
Pick an interface when you see these signals:
The interface in Java article walks through the syntax and rules in full.
Java famously bans multiple inheritance of classes. Interfaces give you most of the benefit without the pain.
Suppose a class could extend two parents, and both defined a field called count. Which one would your object hold?
The same clash hits methods and constructors. Language designers call this the diamond problem, and it has no tidy answer.
Java sidesteps it. A class extends exactly one class, so state can never come from two directions.
Interfaces carry no instance state, so a class may implement as many as it likes.
package com.java.handson.abstraction;
interface Student {
void study();
}
interface Athlete {
void playSport();
}
interface PartTimeWorker {
void work();
}
class SchoolStudent implements Student, Athlete {
private final String name;
SchoolStudent(String name) {
this.name = name;
}
@Override
public void study() {
System.out.println(name + " is studying school subjects.");
}
@Override
public void playSport() {
System.out.println(name + " plays cricket after school.");
}
}
class CollegeStudent implements Student, PartTimeWorker {
private final String name;
CollegeStudent(String name) {
this.name = name;
}
@Override
public void study() {
System.out.println(name + " is preparing for college semesters.");
}
@Override
public void work() {
System.out.println(name + " works part time as a chef.");
}
}
public class RolesDemo {
public static void main(String[] args) {
SchoolStudent suraj = new SchoolStudent("Suraj");
suraj.study();
suraj.playSport();
CollegeStudent shweta = new CollegeStudent("Shweta");
shweta.study();
shweta.work();
}
}
// Output:
// Suraj is studying school subjects.
// Suraj plays cricket after school.
// Shweta is preparing for college semesters.
// Shweta works part time as a chef.Each interface describes one role. A school student studies and plays, while a college student studies and works.
People call this multiple inheritance of type. You inherit several contracts, and you inherit no state at all.
Default methods brought bodies into interfaces, so a small diamond returned. Java handles it strictly.
interface Greeter {
default void hello() {
System.out.println("Hello from Greeter");
}
}
interface Welcomer {
default void hello() {
System.out.println("Hello from Welcomer");
}
}
class Host implements Greeter, Welcomer {
// Without this override the code will not compile
@Override
public void hello() {
Greeter.super.hello(); // Pick a parent explicitly
}
}
// Output: Hello from GreeterThe compiler refuses to guess. You must override the clashing method and choose a winner yourself.
Note the unusual syntax. Writing Greeter.super.hello() calls that specific interface version.
This comparison decides your design, so it deserves a proper table.
| Feature | Abstract class | Interface |
|---|---|---|
| Keyword to use it | extends | implements |
| How many per class | One only | As many as you need |
| Instance fields | Yes | No |
| Constructors | Yes | No |
| Method bodies | Yes, freely | Only default, static, or private |
| Access modifiers on methods | Any level | public, or private for helpers |
| Relationship it models | “is a” family member | “can do” capability |
| Best for | Shared code and shared state | Contracts and swappable parts |
Ask one question first. Do these classes share code, or only a capability?
Shared code and shared fields point to an abstract class. A shared capability across unrelated types points to an interface.
When you cannot decide, start with the interface. It keeps callers loosely coupled and leaves your options open.
Nothing stops you from using both. Java library classes routinely extend one base class and implement several interfaces.
Abstraction is a dial, not a switch. The type you declare sets where that dial sits.
High-level code talks to general types. It cares about the promise and ignores the class behind it.
Animal animal = new Dog("Tommy");
animal.makeSound(); // We only know that it makes a sound
Student student = new CollegeStudent("Shweta");
student.study(); // We only know that it studiesBoth variables use the general type. Swap in a Cat or a SchoolStudent tomorrow and this code survives untouched.
Low-level code names the concrete class. You gain access to everything, and you lose flexibility.
SchoolStudent schoolStudent = new SchoolStudent("Suraj");
schoolStudent.playSport(); // Only SchoolStudent offers thisSometimes you genuinely need the specific type. Just be aware that the variable now locks you to one class.
The Java Collections Framework is the clearest example of this idea shipping in real code.
List<String> names = new ArrayList<>(); // Declare the contract
names.add("Suraj");
names.add("Shweta");
// Switching the engine touches exactly one line
List<String> others = new LinkedList<>();
others.add("Tommy");List is an interface, and ArrayList is one implementation of it. Every method that accepts a List works with either class.
Compare our List interface guide for how the implementations differ underneath.
Abstraction is not academic decoration. It buys you four practical things.
A caller reads a short list of methods instead of a thousand lines of logic. Your working memory thanks you.
Smaller surfaces also shorten code review. Reviewers check the contract, not the plumbing.
Rewrite the body of a method and callers never notice, as long as the signature holds.
This is why teams can replace a whole storage layer over a weekend. The contract absorbs the shock.
Depend on an interface and you can hand your test a fake implementation. No database, no network, no waiting.
Mocking libraries lean on exactly this. They generate a stand-in class that satisfies the contract.
Agree the interface on Monday, then two developers build against it separately.
Neither one waits for the other to finish. The contract is the meeting point.
Four habits separate useful abstractions from ceremonial ones.
Declare variables, parameters, and return types using the general type wherever you can.
// Rigid: only one implementation fits
ArrayList<String> loadNames(ArrayList<String> source) { ... }
// Flexible: any List works
List<String> loadNames(List<String> source) { ... }The second signature accepts more callers and promises less. Both of those are wins.
An interface with three methods is easy to implement. One with fifteen is a burden nobody wants.
Split large contracts by role. Readable and Writable beat a single FileHandler that does both.
Good names describe what the type promises. Payable, Comparable, and Runnable all pass that test.
Avoid decorating names with prefixes like IStudent or suffixes like StudentInterface. Java developers do not follow that convention.
A method named save should not return a database row object. That detail belongs behind the wall.
Check your signatures. If a caller must import your internal classes to use your API, the abstraction has sprung a leak.
Some codebases pair every class with a matching interface that has exactly one implementation.
That doubles the file count and buys nothing. Add the interface when a second implementation appears, or when tests genuinely need one.
A bloated contract forces classes to implement methods they do not want.
interface Machine {
void print();
void scan();
void fax(); // A basic printer has to fake this
}
// Better: one interface per capability
interface Printer { void print(); }
interface Scanner { void scan(); }Watch for methods that throw UnsupportedOperationException. That exception usually means the interface is too wide.
This one bites hard, and the compiler stays silent.
abstract class Report {
Report() {
render(); // Dangerous: the subclass is not ready yet
}
abstract void render();
}
class SalesReport extends Report {
private String title = "Sales";
@Override
void render() {
System.out.println(title.toUpperCase());
}
}
// Throws: java.lang.NullPointerExceptionThe parent constructor runs first, so title is still null when render fires. Keep constructors free of overridable calls.
Add an abstract method to an ordinary class and compilation stops immediately.
The fix takes one word. Mark the class abstract, or give the method a body.
Let us combine everything into one small program.
We will build a tiny checkout. Every payment type can pay, and only some of them can refund.
That split maps perfectly onto our two tools. An abstract class carries the shared state, and an interface adds the optional capability.
package com.java.handson.abstraction;
abstract class PaymentMethod {
private final String owner;
protected PaymentMethod(String owner) {
this.owner = owner;
}
protected String getOwner() {
return owner;
}
// Each payment type settles money its own way
public abstract void pay(double amount);
// Shared by every payment type
public void printReceipt(double amount) {
System.out.println(" Receipt for " + owner + " : " + amount);
}
}
// An optional capability, not every method supports it
interface Refundable {
void refund(double amount);
}
class CardPayment extends PaymentMethod implements Refundable {
CardPayment(String owner) {
super(owner);
}
@Override
public void pay(double amount) {
System.out.println(getOwner() + " paid " + amount + " by card");
}
@Override
public void refund(double amount) {
System.out.println(" Refunded " + amount + " to the card");
}
}
class UpiPayment extends PaymentMethod {
UpiPayment(String owner) {
super(owner);
}
@Override
public void pay(double amount) {
System.out.println(getOwner() + " paid " + amount + " by UPI");
}
}
public class Checkout {
public static void main(String[] args) {
PaymentMethod[] methods = {
new CardPayment("Suraj"),
new UpiPayment("Shweta")
};
for (PaymentMethod method : methods) {
method.pay(500.0);
method.printReceipt(500.0);
if (method instanceof Refundable refundable) {
refundable.refund(100.0);
}
}
}
}
// Output:
// Suraj paid 500.0 by card
// Receipt for Suraj : 500.0
// Refunded 100.0 to the card
// Shweta paid 500.0 by UPI
// Receipt for Shweta : 500.0Start with the array. Both objects sit in a PaymentMethod array, so the loop never names a concrete class.
Then the loop calls pay. Java picks the right override at runtime, which is polymorphism doing the work.
After that, printReceipt runs from the abstract parent. Neither subclass had to write it.
Next comes the interesting line. The instanceof check asks whether this payment can refund, and only the card can.
That pattern-matching syntax arrived in Java 16. It tests the type and declares the variable in one step.
Add a WalletPayment class tomorrow and main stays identical. Implement Refundable and refunds start working automatically.
Say the shop now takes cash. How much code must we touch?
Just one new class. Extend PaymentMethod, write pay, and stop there.
class CashPayment extends PaymentMethod {
CashPayment(String owner) {
super(owner);
}
@Override
public void pay(double amount) {
System.out.println(getOwner() + " paid " + amount + " in cash");
}
}Now drop one into the array. The loop picks it up at once.
We did not touch main. We did not touch the two classes that were already there.
Cash cannot be sent back, so this class skips Refundable. The if check then skips the refund line, and nothing breaks.
There is the payoff. New types slot in, and old code sits still.
A: Abstraction hides how something works and exposes only what it does. Java delivers it through abstract classes and interfaces, which declare methods that concrete classes must implement.
A: Abstraction is a design decision about which operations to expose. Encapsulation is a protection mechanism that keeps fields private and controls access through methods.
A: Yes. Subclasses call it through super, and it initialises the shared fields. You still cannot create an object of the abstract class itself.
A: Yes, and it compiles fine. The abstract keyword alone stops anyone from instantiating the class, which is sometimes exactly what you want.
A: Each of those blocks overriding. An abstract method exists only to be overridden, so the combination makes no sense and the compiler rejects it.
A: Choose an interface when unrelated classes need the same capability, or when one class must fill several roles. Choose an abstract class when the classes share real code and state.
A: No. Any field you declare in an interface becomes public static final automatically, so it is a shared constant rather than per-object state.
A: To let library authors add methods to existing interfaces without breaking every implementing class. A default method ships with a body, so old code keeps compiling.
A: The class must override the clashing method, or it will not compile. Inside the override you can call a specific version with syntax like Greeter.super.hello().
A: Yes, and it is very common. Write extends first, then implements with a comma-separated list of interfaces.
Let us wrap up what we covered. Abstraction in Java is the discipline of showing what and hiding how.
Abstract classes give you a partial blueprint. They hold fields, constructors, and finished methods, and they leave the rest to subclasses.
Interfaces give you a pure contract. They hold no state, they carry no constructors, and any number of unrelated classes can honour them.
Pick between them with one question. Shared code points to an abstract class, while a shared capability points to an interface.
Multiple interfaces let one class play several roles. When two default methods collide, Java makes you choose the winner yourself.
Keep your contracts small and your names honest. A three-method interface gets implemented, while a fifteen-method one gets resented.
Watch the two traps we saw. Do not create an interface out of habit, and never call an overridable method from a constructor.
Run the checkout program and extend it. Add a WalletPayment class, make it Refundable, and notice that main never changes. That moment is when abstraction stops being a definition and starts being useful.