Abstract Classes vs. Interfaces in Java: What are the Differences?

August 01, 2023
Written by
Pedro Lopes
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Diane Phan
Twilion

header - Abstract Classes vs. Interfaces in Java: What are the Differences?

Java was the third most-used programming language in 2022, according to the TIOBE Index. So, you can get an idea of the relevancy of Java in modern software engineering.

Interfaces and abstract classes are fundamental concepts for any Java developer. Mastering these concepts helps you develop more maintainable applications.

In this article, I'll show you how to use interfaces and abstract classes, their differences, and real-world applications.

Prerequisites

For this tutorial, you'll need a few things before starting:

What is an Interface?

The interface keyword in Java defines a contract between the developer and an API. It’s an agreement on how the client code should use defined methods.

The main purpose of an interface is to implement subtype polymorphism. Therefore, an interface is a kind of type that can be replaced with any of its subtypes at any time. More informally, it provides a list of methods used as an entry point to concrete implementations.

There are two types of methods in interfaces. Default methods, which describe a behavior that you can opt to override. And abstract methods that describe no behavior, and you must always override them.

Ultimately, all interface fields are implicitly public, static, and final. That means three things.

  • Firstly, you must always initialize all interface fields to some value.
  • Secondly, all fields are visible through the interface name, not its instance.
  • Thirdly, you can’t change the value of any field after initialization.

Interface’s implementations inherit all of its fields. However, you can override them by declaring the same field name in a subclass with another value. That technique is called field hiding: you effectively hide the original field with the new value.

All these concepts and terms will become clearer in the examples in the following section.

Create Interfaces and Multiple Implementations

You can create interfaces using the interface keyword. I’ll use a zombie invasion game application to illustrate its usage.

First, create a Java package named zombieattack in your application. Inside that package, create the Zombie interface with the content below:

public interface Zombie {

    int STEP_NUMBER = 1;
    int ATTACK_NUMBER = 1;

    default void walk() {
        System.out.println("Walking " + STEP_NUMBER + "step.");
    }

    void attack();
}

The Zombie interface contains one default method and one abstract method. To create any concrete subclass of Zombie, you must override the attack() method. In contrast, you can override or not the walk() method.

To illustrate method overrides, imagine two special types of zombies: the jumper and the warrior. The jumper walks two steps and attacks once. And the warrior walks 1 step and attacks twice.

You can define special jumper and warrior zombies as implementations of Zombie. To do so, create the JumperZombie file in the zombieattack package and add the following content:

public class JumperZombie implements Zombie {

    int STEP_NUMBER = 2;

    @Override
    public void walk() {
        System.out.println("Moving " + STEP_NUMBER + " steps.");
    }

    @Override
    public void attack() {
        System.out.println("Attacks " + ATTACK_NUMBER + " two times");
    }
}

In that same package, create the WarriorZombie class and add the following content:

public class WarriorZombie implements Zombie {

    int ATTACK_NUMBER = 2;

    @Override
    public void attack() {
        System.out.println("Attacks " + ATTACK_NUMBER + " two times");
    }
}

The JumperZombie class overrides both the attack() and walk(). Meanwhile, the WarriorZombie class only overrides the attack() method since the behavior of walk() already achieves the desired result.

In the Zombie interface, I’ve also defined two fields, STEP_NUMBER and ATTACK_NUMBER, initialized with 1. In its subclasses, the original values were hidden with new values. That means in the JumperZombie class, the value of STEP_NUMBER is now 2, and the inherited value of 1 is ignored. The same applies to the ATTACK_NUMBER field in the WarriorZombie class.

Implement multiple interfaces

One important point about method overriding is that a class can implement any number of interfaces. In that case, the subclass must implement all abstract methods from both interfaces like in the code below:

public class JumperZombie implements Zombie, Readable {
    int STEP_NUMBER = 2;

    @Override
    public void walk() {
        System.out.println("Walking " + STEP_NUMBER + " steps.");
    }

    @Override
    public void attack() {
        System.out.println("Attacks " + ATTACK_NUMBER + " two times");
    }

    @Override
    public int read(CharBuffer cb) throws IOException {
         return 0;
    }
}

Note the read() method at the bottom of the above class. That is an abstract method in the Readable interface, so you must override in the JumperZombie class.

Make an interface extend another interface

An interface can also extend methods from another interface. In that case, subclasses must implement the methods of the direct interface and the methods of the interface above it. So, for example, if you change the Zombie class signature to the following:

public interface Zombie extends Comparable<Zombie>{

    int STEP_NUMBER = 1;
    int ATTACK_NUMBER = 1;

    default void walk() {
        System.out.println("Walking " + STEP_NUMBER + "step.");
    }

    void attack();
}

Now both JumperZombie and WarriorZombie classes should also implement the compareTo() method of Comparable and the attack(). With that said, the code for JumperZombie looks like the following:

public class JumperZombie implements Zombie{
    int STEP_NUMBER = 2;

    @Override
    public void walk() {
        System.out.println("Walking " + STEP_NUMBER + " steps.");
    }

    @Override
    public void attack() {
        System.out.println("Attacks " + ATTACK_NUMBER + " two times");
    }

    @Override
    public int compareTo(Zombie zombie) {
        return 0;
    }
}

And the WarriorZombie looks like following:

public class WarriorZombie implements Zombie {

    int ATTACK_NUMBER = 2;

    @Override
    public void attack() {
        System.out.println("Attacks " + ATTACK_NUMBER + " two times");
    }

    @Override
    public int compareTo(Zombie zombie) {
        return 0;
    }
}

Real-world applications for interfaces

You can find good examples of interfaces at the JDK. For instance, the Comparable interface provides a custom way to compare objects of its subclasses. Likewise, the Closeable interface allows customization of how to close a connection to a file or a database.

Or, you can use interfaces to implement the Strategy Design Pattern. That pattern uses an interface as an entry point to find and use a specific algorithm in its subclasses based on an input field.

What is an Abstract Class?

Simply put, an abstract class is a class that uses the abstract keyword. In those classes, you define abstract methods and override them in subclasses, just like interfaces. The non-abstract methods work exactly like methods in regular classes. Additionally, methods in abstract classes can have any Java modifier: public, default, protected, or private.

As with interfaces, you can instantiate an abstract class only via its subclasses, not directly.

Unlike interfaces, fields in abstract classes are mutable, and you can change its values at runtime.

Define Abstract Classes and Subclasses

In our zombie invasion game, different types of soldiers fight against zombies. So, I’ll illustrate abstract classes using different types of soldiers in the game. To do that, create the Soldier abstract class in the zombieattack package with the content below:

public abstract class Soldier {

    protected int life = 10;
    protected int damage = 1;

    abstract protected void attack();

    protected boolean isDead() {
        return life == 0;
    }
}

The Soldier abstract class defines one abstract method, attack(), and one non-abstract, isDead(). So, attack() defines the signature that any concrete implementation of Soldier must override, similarly to interfaces. Whereas, with the isDead() method, you can opt to override.

I’ve also defined two fields, life and damage initialized with 10 and 1, respectively. They are default fields that you can use in subclasses. Contrasting with interface field hiding, the state of these two fields is preserved. Thus, changing its values will effectively change them, not just hide them.

Now, create the class Shooter with the content below:

public class Shooter extends Soldier {

    public Shooter() {
        this.damage = 6;
    }

    @Override
    protected void attack() {
        System.out.println("Shooting. That gives " + this.damage + " damage.");
    }
}

And, create the class Fighter with the content below:

public class Fighter extends Soldier {

    public Fighter() {
        this.life = 25;
        this.damage = 2;
    }

    @Override
    protected void attack() {
        System.out.println("Attacking with hands. That gives " + this.damage + " damage.");
    }
}

Note that I’ve changed the life and damage fields to something else in the subclasses’ constructors. With abstract classes, you don’t need field hiding to achieve that. You can change the state of the inherited field with another value.

For instance, in the Shooter class, the damage is now 6, but life is still 10. Whereas in the Fighter class, life is 25 and damage is 2. However, the same fields defined in Soldier were mutated without defining different life and damage fields.

Real-world applications for abstract classes

One good example of an abstract class found in the JDK is the InputStream class. That class specifies the top class for all classes representing an input stream of bytes. It’s widely used to read and write byte files.

Abstract classes appear in the template method to create a kind of template for subclasses. The abstract class, called template, defines a skeleton of an algorithm as a set of fields and methods. That skeleton is common to all subclass implementations of that algorithm. Thus, when you override the template, you get that predefined logic. The main advantage of it is that you can reuse the skeleton code in many other classes without rewriting it all over.

Conclusion

You may note that interfaces and abstract classes are very similar. In both, you can define default and abstract methods. Also, you can define fields that subclasses inherit. Finally, you can only instantiate them via sub-classes, not directly.

The big difference between them is their state. Abstract classes are stateful, so you can mutate the values of their fields, as seen in the usage of this in the subclasses of Soldier. However, interfaces are stateless, so you need to specify a field again as a constant in subclasses if you want a new value for it, as seen in the field hiding in subclasses of Zombie.

Therefore, each of them is suitable for different situations, as you’ve seen in the real-world applications section.

Pedro Lopes is a backend developer at MercadoLibre and an independent writer. He's a specialist and enthusiastic about distributed systems, big data, and high-performance computing. He can be found on LinkedIn, Github, and his Blog.