Table of Contents

Abstraction in Java

  • Last Updated: July 27, 2025
  • By: javahandson
  • Series
img

Abstraction in Java

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.

1. Introduction

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.

1.1 What This Article Covers

  • What abstraction means, and how it differs from encapsulation
  • Abstract classes, with their rules and their constructors
  • Interfaces, including default, static, and private methods
  • Multiple inheritance, and how Java solves the diamond problem
  • A side-by-side table so you can pick the right tool
  • High-level and low-level abstraction, with real examples
  • Best practices, common mistakes, and a payment-system walkthrough
  • Ten interview questions with short, honest answers

2. What Abstraction Really Means

Abstraction shows the essential features of an object and hides the rest. You focus on what it does, never on how.

2.1 The Car Analogy

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.

2.2 Abstraction vs Encapsulation

Interviewers love this question, because the two ideas sound identical. They are not.

  • Abstraction is about design. You decide which operations to expose and which to leave out.
  • Encapsulation is about protection. You keep fields private and let methods guard them.

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.

2.3 How Java Provides Abstraction

Java gives you exactly two constructs for this job:

  • An abstract class gives a partial blueprint, mixing finished code with unfinished promises
  • An interface gives a pure contract, listing what a class must be able to do

Both stop you from creating an object directly. Both force a real class to fill in the blanks.

3. Abstract Classes

An abstract class sits halfway between an idea and a real class.

3.1 What Makes a Class Abstract

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.

3.2 A Working Example

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.

3.3 Rules You Must Remember

A handful of rules govern abstract classes, and interviewers ask about all of them:

  • You cannot create an object of an abstract class with new
  • A class holding even one abstract method must itself be abstract
  • An abstract class may hold zero abstract methods, which is legal but unusual
  • Abstract methods cannot be private, static, or final, since each of those blocks overriding
  • A subclass must implement every abstract method, or declare itself abstract too

That last rule creates a useful escape hatch. Middle layers in a hierarchy can stay abstract and pass the work down.

3.4 Abstract Classes Can Have Constructors

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.

3.5 When to Use an Abstract Class

Reach for an abstract class when these signs appear:

  • Your classes share real code, not just method names
  • They also share state, such as a name or an id field
  • They belong to one family, and “is a” reads naturally
  • You want a default behaviour that subclasses may override
  • You need protected members that only the family can touch

Our dedicated guide on the abstract class in Java covers these cases in more depth.

3.6 A Quick Dry Run

Let us trace the animal program step by step. No magic, just six short steps.

  • We call new Dog(“Tommy”)
  • The Dog constructor starts, and it calls super(name)
  • The Animal constructor sets the name field to Tommy
  • Our code parks that new object in a variable of type Animal
  • A call to makeSound runs the Dog version
  • A call to eat runs the shared Animal version

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.

4. Interfaces

An interface takes abstraction further. It describes capability and nothing else.

4.1 A Pure Contract

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.

4.2 A Working Example

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.

4.3 What an Interface Can Hold Today

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.

4.4 default and static Methods

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.

4.5 When to Use an Interface

Pick an interface when you see these signals:

  • Unrelated classes need the same capability, with no shared ancestor
  • One class must take on several roles at once
  • You want callers to depend on a contract, so implementations stay swappable
  • There is no shared state and no shared setup code

The interface in Java article walks through the syntax and rules in full.

5. Multiple Inheritance With Interfaces

Java famously bans multiple inheritance of classes. Interfaces give you most of the benefit without the pain.

5.1 Why Classes Cannot Do It

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.

5.2 Implementing Several Interfaces

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.

5.3 The Diamond Problem With default Methods

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 Greeter

The 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.

6. Abstract Class vs Interface

This comparison decides your design, so it deserves a proper table.

6.1 Side-by-Side Comparison

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

6.2 How to Choose

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.

7. Levels of Abstraction

Abstraction is a dial, not a switch. The type you declare sets where that dial sits.

7.1 High-Level Abstraction

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 studies

Both variables use the general type. Swap in a Cat or a SchoolStudent tomorrow and this code survives untouched.

7.2 Low-Level Abstraction

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 this

Sometimes you genuinely need the specific type. Just be aware that the variable now locks you to one class.

7.3 Abstraction in the Collections API

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.

8. Why Abstraction Matters

Abstraction is not academic decoration. It buys you four practical things.

8.1 Less to Hold in Your Head

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.

8.2 Changes Stay Contained

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.

8.3 Tests Get Easier

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.

8.4 Teams Move in Parallel

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.

9. Best Practices

Four habits separate useful abstractions from ceremonial ones.

9.1 Program to the Abstraction

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.

9.2 Keep Contracts Small

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.

9.3 Name the Role, Not the Shape

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.

9.4 Do Not Leak the Implementation

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.

10. Common Mistakes and Pitfalls

10.1 An Interface for Every Class

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.

10.2 Fat Interfaces

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.

10.3 Calling an Overridable Method in a Constructor

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.NullPointerException

The parent constructor runs first, so title is still null when render fires. Keep constructors free of overridable calls.

10.4 Forgetting to Mark the Class abstract

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.

11. Practical Walkthrough

Let us combine everything into one small program.

11.1 The Goal

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.

11.2 The Code

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.0

11.3 Walking Through the Output

Start 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.

11.4 Adding a New Payment Type

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.

12. Interview Questions

Q: What is abstraction in Java?

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.

Q: What is the difference between abstraction and encapsulation?

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.

Q: Can an abstract class have a constructor?

A: Yes. Subclasses call it through super, and it initialises the shared fields. You still cannot create an object of the abstract class itself.

Q: Can an abstract class have zero abstract methods?

A: Yes, and it compiles fine. The abstract keyword alone stops anyone from instantiating the class, which is sometimes exactly what you want.

Q: Why can an abstract method not be private, static, or final?

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.

Q: When should you pick an interface over an abstract class?

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.

Q: Can an interface have instance variables?

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.

Q: Why did Java 8 add default methods?

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.

Q: How does Java handle two interfaces with the same default method?

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().

Q: Can a class extend a class and implement interfaces at the same time?

A: Yes, and it is very common. Write extends first, then implements with a comma-separated list of interfaces.

13. Conclusion

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.

Further Reading

Leave a Comment