DEV Community

Mayur Bodhare
Mayur Bodhare

Posted on

Java Abstraction for Beginners: From abstract to Interfaces

When I first learned about abstract and interface, I kept asking the same questions. Why would I write a class that I'm not allowed to create objects from? Why would I write a method with no body? And if abstract classes and interfaces both do something similar, why does Java have both?

The answers made sense once I understood the idea underneath them: abstraction. In this post, we'll go through the abstract keyword, abstract methods, abstract classes, interfaces, why we need them and the different types of interfaces. I'll also show what Java does behind the scenes, and how to choose between abstract classes and interfaces.

Unless I show a full class, the snippets go inside a main method.

The idea: show what, hide how

Abstraction means showing what something does and hiding how it does it.

When you drive a car, you use the steering wheel and the pedals. You don't need to know how the engine burns fuel. When you tap "Pay" in a payment app, you don't care whether the money moves by card or UPI behind the scenes.

In Java, we do the same: we define what must be possible (the promise), and let different classes decide how to do it. abstract classes and interfaces are the two tools for that.

The abstract keyword

abstract means "incomplete on purpose." You can put it in front of two things:

  • a class, and
  • a method.

You can't use it on variables or constructors. Let's look at the two uses.

Abstract method

An abstract method is a method with a name and a signature but no body. It ends with a semicolon instead of curly braces.

abstract double area();
Enter fullscreen mode Exit fullscreen mode

It says, "Every shape must be able to give its area, but I don't know how to calculate it. That's the job of the specific shape."

Rules for abstract methods:

  • It has no body.
  • It can live only inside an abstract class (or an interface).
  • A child class must implement it (give it a body), unless the child is abstract too.
  • It can't be private, static or final, because each of those would stop a child from implementing it.

Abstract class

An abstract class is a class marked with abstract. You cannot create objects from it directly, because it may be incomplete. You can only extend it.

It's a mix of two things:

  • Finished parts: normal fields, constructors and methods with bodies.
  • Unfinished parts: abstract methods that children must fill in.

Here's an example:

abstract class Shape {
    private final String name;

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

    abstract double area();          // unfinished: each child decides

    void describe() {                // finished: shared by all children
        System.out.println(name + " with area " + area());
    }
}

class Circle extends Shape {
    private final double radius;

    Circle(double radius) {
        super("Circle");
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * radius * radius;
    }
}

class Rectangle extends Shape {
    private final double width, height;

    Rectangle(double width, double height) {
        super("Rectangle");
        this.width = width;
        this.height = height;
    }

    @Override
    double area() {
        return width * height;
    }
}
Enter fullscreen mode Exit fullscreen mode
Shape[] shapes = { new Circle(2), new Rectangle(3, 4) };

for (Shape s : shapes) {
    s.describe();
}
Enter fullscreen mode Exit fullscreen mode

Output:

Circle with area 12.566370614359172
Rectangle with area 12.0
Enter fullscreen mode Exit fullscreen mode

Look at what happened. describe() is written once, in Shape, but it calls area(), and each child supplies its own version. The shared part stays in one place, and only the part that changes is left to the children. (This pattern is often called the template method pattern.)

If you try to create the abstract class directly:

Shape s = new Shape("Blob");
Enter fullscreen mode Exit fullscreen mode
Shape is abstract; cannot be instantiated
Enter fullscreen mode Exit fullscreen mode

What if a child forgets to implement the method?

class Triangle extends Shape {
    Triangle() { super("Triangle"); }
}
Enter fullscreen mode Exit fullscreen mode
Triangle is not abstract and does not override abstract method area() in Shape
Enter fullscreen mode Exit fullscreen mode

A child must either implement every abstract method, or be declared abstract itself and pass the job down to its own children.

Behind the scenes

An abstract class is a normal class with an "abstract" flag set in its .class file. That flag is what makes the compiler (and the JVM) refuse new Shape(...). Two interesting things follow:

  • It still has a constructor. You can't call new Shape(...), but when you build a Circle, Circle's constructor calls super("Circle"), which runs Shape's constructor. That's how the shared fields get set up.
  • The object on the heap is the child. There's no separate Shape object. There's one Circle object that contains the Shape part inside it.
   new Circle(2)

   Circle object on the heap
  +----------------------------+
  | Shape part:  name="Circle" |   <- built by Shape's constructor (via super)
  | Circle part: radius=2.0    |
  +----------------------------+

   s.describe()     runs the code in Shape
        |
        +--> calls area()  --> JVM looks at the REAL object (a Circle) --> Circle.area()
Enter fullscreen mode Exit fullscreen mode

That last step is dynamic dispatch: the JVM picks the method based on the real object, not the variable's type. It's the same engine that powers method overriding.

An abstract class doesn't have to contain abstract methods. You can mark a class abstract just to stop people from creating it directly. That's uncommon, though.

Interfaces

An interface is a pure contract. It lists what a class must be able to do, without saying how.

interface PaymentMethod {
    void pay(double amount);
}
Enter fullscreen mode Exit fullscreen mode

A class that signs the contract uses the keyword implements and must provide every method:

class CreditCard implements PaymentMethod {
    public void pay(double amount) {
        System.out.println("Paid " + amount + " by card");
    }
}

class Upi implements PaymentMethod {
    public void pay(double amount) {
        System.out.println("Paid " + amount + " by UPI");
    }
}

class Checkout {
    static void complete(PaymentMethod method, double total) {
        method.pay(total);
    }
}
Enter fullscreen mode Exit fullscreen mode
Checkout.complete(new CreditCard(), 499.0);
Checkout.complete(new Upi(), 120.5);
Enter fullscreen mode Exit fullscreen mode

Output:

Paid 499.0 by card
Paid 120.5 by UPI
Enter fullscreen mode Exit fullscreen mode

Notice that Checkout doesn't know or care which payment method it gets. It only knows that the object can pay. That's the power of an interface.

A few rules to remember:

  • Methods in an interface are public and abstract by default. You don't need to write those words.
  • Fields in an interface are public static final by default, so they're constants.
  • A class can implement many interfaces, separated by commas.
  • An interface can extend other interfaces (even several of them).
  • You can't create an object of an interface directly.

The need for interfaces

Why does Java have interfaces when it already has abstract classes? Here are the real reasons:

1. Loose coupling. Code that depends on an interface doesn't depend on any specific class. You can swap implementations without touching the caller. Think of this line you've probably written:

List<String> names = new ArrayList<>();
Enter fullscreen mode Exit fullscreen mode

Your code talks to List. Later you can change to LinkedList by editing one word, and nothing else breaks.

              +----------------+
 Checkout --> | PaymentMethod  | <-- CreditCard
              |   (contract)   | <-- Upi
              +----------------+ <-- NetBanking (added later, no change to Checkout)
Enter fullscreen mode Exit fullscreen mode

2. A way around single inheritance. A class can extend only one class, but it can implement many interfaces. So Duck can be both Flyable and Swimmable. (This is Java's answer to the multiple inheritance problem.)

3. Sharing abilities between unrelated classes. A Bird and a Plane have nothing in common, but both can fly(). An abstract class forces a family tree. An interface doesn't.

4. Teamwork and testing. Two developers can agree on an interface, and then work in parallel: one builds the real version, and the other builds a fake version for testing.

Behind the scenes

An interface compiles into its own .class file, with an "interface" flag. When you call a method through an interface variable, like method.pay(total), the compiler emits an invokeinterface instruction. At run time, the JVM looks at the real object behind the variable and finds its pay method. It's dynamic dispatch again, just reached through the contract.

default, static and private methods in interfaces

Interfaces used to have only abstract methods. Since Java 8, they can also have:

  • default methods: methods with a body, which implementing classes get for free (and may override).
  • static methods: helper methods that belong to the interface itself.

And since Java 9, they can have private methods to share code between the default methods.

interface Greeter {
    String name();

    default void greet() {
        System.out.println(greeting() + ", " + name());
    }

    private String greeting() {
        return "Hello";
    }

    static void printLine() {
        System.out.println("-----");
    }
}

class Person implements Greeter {
    public String name() {
        return "Asha";
    }
}
Enter fullscreen mode Exit fullscreen mode
new Person().greet();
Greeter.printLine();
Enter fullscreen mode Exit fullscreen mode

Output:

Hello, Asha
-----
Enter fullscreen mode Exit fullscreen mode

Person only wrote name(), but it got greet() for free.

Why did Java add default methods? So that library designers can add new methods to an existing interface without breaking every class that already implements it. (If two interfaces give the same default method, the class must override it to decide which one wins, as we saw in the multiple inheritance discussion.)

Types of interfaces

You'll hear about a few kinds of interfaces. They're all written with the same keyword. The difference is in what's inside.

   Interface
     |
     |-- Normal       : any number of methods              (List, Map, Collection)
     |-- Functional   : exactly ONE abstract method        (Runnable, Comparator)
     |-- Marker       : NO methods, only a tag             (Serializable, Cloneable)
Enter fullscreen mode Exit fullscreen mode

1. Normal interface

Any number of abstract methods, plus optional default and static ones. Most interfaces you design are this type, like PaymentMethod above.

2. Functional interface

An interface with exactly one abstract method. It may also have default and static methods, but only one abstract one. Runnable (with just run()) and Comparator are examples. You can mark it with the annotation @FunctionalInterface, and the compiler will check the rule for you.

@FunctionalInterface
interface Validator {
    boolean isValid(String text);
}
Enter fullscreen mode Exit fullscreen mode

Functional interfaces are special because they're the foundation of lambda expressions. We'll look at that closely in a later post, so for now, just remember: "one abstract method = functional interface."

3. Marker interface

An interface with no methods at all. It works like a label stuck on a class that says "this class has a certain property." Famous examples are Serializable (can be saved as bytes) and Cloneable (can be copied).

interface Printable { }          // a marker interface

class Report implements Printable { }
Enter fullscreen mode Exit fullscreen mode
Object o = new Report();
System.out.println(o instanceof Printable);
Enter fullscreen mode Exit fullscreen mode

Output:

true
Enter fullscreen mode Exit fullscreen mode

The marker adds no behavior by itself. Other code checks for it (with instanceof) and decides what to do. For example, Java's serialization code refuses to save an object whose class doesn't implement Serializable. (Newer code often uses annotations for this kind of tagging, but the idea is the same.)

There are also nested interfaces, which are declared inside a class or another interface. They're less common, and we'll see nesting in the next post.

Abstract class vs interface

Here's how they compare:

  • State: an abstract class can have normal instance fields. An interface can only have constants.
  • Constructors: an abstract class has them. An interface doesn't.
  • How many: a class can extend only one abstract class, but implement many interfaces.
  • Methods: an abstract class can mix abstract and normal methods of any access level. An interface's methods are public (with private helpers since Java 9).
  • Relationship: an abstract class says "is a kind of" (a Circle is a Shape). An interface says "can do" (a Duck can fly).

You can also combine them. The Java library often gives an interface (List) and an abstract class with a partial implementation (AbstractList), so you can start from the shared code and fill in the gaps.

Which one should I use?

  • Do the classes share fields or real code? Use an abstract class, because it can hold state and finished methods.
  • Do you just need a contract that many unrelated classes can follow? Use an interface.
  • Do you need a class to get abilities from several places? Use interfaces, because you can only extend one class.
  • Are you designing a public API that will change over time? Prefer an interface. It keeps callers independent of your implementation, and default methods let you add to it safely.
  • Not sure? Start with an interface. You can always add an abstract class under it later, but it's harder to go the other way around.

Common mistakes

  1. Trying to create an object of an abstract class or interface. new Shape(...) gives "is abstract; cannot be instantiated." Create a child or an implementing class instead.
  2. Forgetting to implement all the abstract methods. The compiler says the class "is not abstract and does not override abstract method." Either implement them, or mark the class abstract.
  3. Forgetting public when implementing an interface method. Interface methods are public, and your version can't be less visible:
pay(double) in CreditCard cannot implement pay(double) in PaymentMethod; attempting to assign weaker access privileges; was public
Enter fullscreen mode Exit fullscreen mode
  1. Combining abstract with final, private or static. An abstract method needs to be overridden, and those keywords stop that. You'll see "illegal combination of modifiers: abstract and final."
  2. Creating a "fat" interface with too many methods. Classes are then forced to write methods they don't need. Split it into smaller interfaces.
  3. Thinking interface fields can change, or that a marker adds behavior. Interface fields are final, and a marker interface is only a tag. Some other code has to act on it.

Quick recap

  • Abstraction means showing what and hiding how.
  • The abstract keyword means "incomplete on purpose," and it applies to classes and methods.
  • An abstract method has no body. Child classes must implement it.
  • An abstract class can't be instantiated, but it can have fields, constructors and finished methods. The child object on the heap contains the abstract parent part.
  • An interface is a pure contract. Its methods are public and abstract by default, its fields are constants, and a class can implement many of them.
  • Interfaces give you loose coupling, a way around single inheritance, shared abilities for unrelated classes and easier testing.
  • Since Java 8, interfaces can have default and static methods, and since Java 9 private ones.
  • Types of interfaces: normal, functional (exactly one abstract method) and marker (no methods).
  • Use an abstract class for shared state and code, and an interface for a contract.

Try it yourself

Here's a small practice idea: build a tiny payroll program. Create an abstract class Employee with a name, a constructor and an abstract method calculateSalary(). Add FullTime and PartTime children, then put them in an Employee[] and print each salary. After that, create an interface Taxable with a default method, and make only FullTime implement it.

For a bonus experiment, run javap -p on your compiled Employee class and look for the word abstract in the output.

If this helped, or if you spotted something I should fix, tell me in the comments. I'm still learning too.

Next in this series: inner classes, static nested classes and anonymous inner classes.

Top comments (0)