Java Interfaces vs. Abstract Classes

 ยท 8 min
Photo by xresch on Pixabay

Even though interfaces and abstract classes have many similarities at first look, especially after introducing default methods, they have different use cases and capabilities.

Note: Java 8 is assumed, but the Java 10 feature Local Variable Type Inference is used if appropriate to increase readability.


Interfaces

If a class implements an interface, it’s a behavioral contract of how an instance interacts with the world around it. Usually, there are method signatures present, but an empty “marker” interface is possible too.

Think of a simple alarm clock with a single alarm:

java
interface AlarmClock {

    void setAlarm(LocalTime time);

    Optional<LocalTime> getAlarm();

    void fireAlarm();

    void stopBeeping();

    void snooze();

    long getSnoozeMinutes();
}

Any type implementing AlarmClock must implement the methods to conform to the interface.

Default Methods

Java 8 improved interfaces by introducing default methods, allowing us to provide a default implementation of a method that mustn’t be overridden:

java
interface AlarmClock {

    // ...

    default long getSnoozeMinutes() {
        return 10L;
    }

    default void snooze() {
        stopBeeping();
        getAlarm().map(time -> time.plusMinutes(getSnoozeMinutes())) //
                  .ifPresent(this::setAlarm);
    }
}

Concrete implementations no longer need to provide a snooze-logic, but can still choose to override one or both snooze-methods.

Static Methods

Also introduced with Java 8 were static interface methods:

java
interface AlarmClock {

    static boolean calculateAlarmTime(LocalTime bedTime,
                                      long hoursOfSleep) {
        var time = Optional.ofNullable(bedTime)
                           .orElseGet(LocalTime::now); 

        return time.plusHours(hoursOfSleep);
    }

    // ...
}

Like default methods, static methods provide more cohesion by aggregating related methods/logic in a single place without needing a helper object, like an AlarmTimeCalculator. But unlike default methods, we can’t override static methods.


Abstract Classes

An abstract class is a special kind of non-instantiable class that can be partially implemented. They are designed to be completed by another class. By defining a method signature abstract, the method body must be omitted, like in an interface.

We can even replicate our interface AlarmClock easily:

java
public abstract class AlarmClock {

    public static boolean calculateAlarmTime(LocalTime bedTime,
                                             long hoursOfSleep) {
        var time = Optional.ofNullable(bedTime)
                           .orElseGet(LocalTime::now); 
        return time.plusHours(hoursOfSleep);
    }

    public abstract void setAlarm(LocalTime time);

    public abstract Optional<LocalTime> getAlarm();

    public abstract void fireAlarm();

    public void stopBeeping();

    public long getSnoozeMinutes() {
        return 10L;
    }

    public void snooze() {
        stopBeeping();
        getAlarm().map(time -> time.plusMinutes(getSnoozeMinutes())) //
                  .ifPresent(this::setAlarm);
    }
}

An abstract class’s most significant advantage over interfaces is being able to use state and provide constructors.

State

Like any other class, we can define an internal state and thereby include more logic than with default methods:

java
public abstract class AlarmClock {

    private LocalTime alarmTime;

    public Optional<LocalTime> getAlarm() {
        return Optional.ofNullable(this.alarmTime);
    }

    public void setAlarm(LocalTime time) {
        stopBeeping();
        this.alarmTime = time;
    }

    // ...
}

Constructors

We can also declare constructors in an abstract class. If and what kind of constructors are declared directly influence how we must call them in any subtype.

No Constructors

Without any explicit constructors, we don’t have to call the no argument default constructor in any subtype.

Default Constructor

If a no argument default exists in an abstract class, there’s still no need to call it explicitly in any inheriting type; t the call will be made “behind the scenes”. For example, our AlarmClock could use an Optional<LocalTime>, and initialize it in the constructor:

java
public abstract class AlarmClock {

    private Optional<LocalTime> alarmTime;

    public AlarmClock() {
        this.alarmTime = Optional.empty();
    }

    public Optional<LocalTime> getAlarm() {
        return this.alarmTime;
    }

    public void setAlarm(LocalTime time) {
        stopBeeping();
        this.alarmTime = Optional.ofNullable(time);
    }

    // ...
}

Parameterized Constructors

Like with inheritance between “normal” classes, the existence of the default no-arg constructor depends on parameterized constructors’ presence. If parameterized constructors exist but not an explicit default no-arg one, any inheriting type must call a super constructor in its own constructor.

Let’s say we want to set the snooze duration in a constructor:

java
public abstract class AlarmClock {

    private long snoozeMinutes;

    public AlarmClock(long snoozeMinutes) {
        this.snoozeMinutes;
    }
}

The concrete implementation now needs an explicit constructor calling super to make the compiler happy. It can be a default no-arg constructor, or parameterized one. If multiple constructors exist in the base type, calling any of them will do:

java
public class LongSnoozeClock extends AlarmClock {

    public LongSnoozeClock() {
        super(20L);
    }

    // ...
}

Interface or Abstract Class?

Even though both provide an overlapping set of capabilities, their intended use and applications differ.

(Multiple) Inheritance

Java, unlike other languages, doesn’t support multi-inheritance. A class can only extend a single other class, like LongSnoozeClock.

Interfaces, on the other hand, do not succumb to this restriction. A class can implement multiple interfaces, and an interface can extend numerous interfaces.

All default methods will be inherited, leading to a problem: what if multiple interfaces implement the same method?

java
interface Foo {
    default void doStuff() {
        // ...
    }
}

interface Bar {
    default void doStuff() {
        // ...
    }
}

class FooBar implements Foo, Bar {
    // Compiler error!
}

Without an explicit @Override, the compiler won’t know which default method implementation to call, so we’re forced to override it:

java
class FooBar implements Foo, Bar {
    
    @Override
    public void doStuff() {
         // ...
    }
}

But we can still rely on the default implementations by leveraging <type>.super:

java
class FooBar implements Foo, Bar {

    @Override
    public void doStuff() {
        Foo.super.doStuff();
        Bar.super.doStuff();
    }
}

Another option is to extend the interface itself:

java
interface Foo {
    default void doStuff() {
        // ...
    }
}

interface Bar extends Foo {
    default void doStuff() {
        Foo.super.doStuff();
        // ...
    }
}

class FooBar implements Bar {
    // ...
}

Visibility Modifiers

In an interface, everything is implicitly public, and only static methods are allowed to be private. With the lack of state, this restriction makes sense. Common logic used by default methods can be safely refactored to a private static method to hide it from the implementer.

But without protected, an interface can never only provide a method to its direct implementer that isn’t available to all other types.

The only available visibility restriction is type-based. If the interface declaration itself isn’t declared public, it will only be available on a package-level.

Abstract classes are, well, classes, so they can leverage the whole spectrum of visibility modifiers. When inherited, they obey the same rules: overridden methods must have at least the same visibility but can choose to increase visibility.

(none = package) => protected => public

Extensibility

Before the introduction of interface default methods, abstract classes were the preferred way to ensure extensibility.

If we added a new method to an interface, all of its implementers were forced to implement it, too, most likely breaking a lot of code. On the other hand, an abstract class could easily add a non-abstract method, and all related types were fine.

With default methods, we can finally improve interfaces by adding new methods without breaking existing types. Actually, JDK 8 implements a lot of additional features with default methods, like java.util.Collection#stream().

Even if no default logic is possible, throwing an UnsupportedOperationException is better than breaking every related type.


Why not use both?

Instead of using either an interface or an abstract class, we can also use them both in tandem. An interface can describe the public contract of a type, and an abstract base class provides a starting point to implement it. That’s especially useful for the service locator pattern.

My company uses this kind of combination for our data access objects. A DAO interface describes how to access data, and an abstract class HibernateDAO provides the base functionality to access data with Hibernate.

Here’s a simplified example:

java
public interface DAO<T> {

    List<T> findAll();

    Optional<T> findById(long id);

    int totalCount(String searchQuery);

    default int totalCount() {
        return totalCount(null);
    }
}

public abstract class HibernateDAO<T> implements DAO<T> {
    
    private final Class<T> persistentClass;
    private final Session  session;

    public HibernateDAO(Class<T> persistentClass,
                        Session session) {
        this.persistentClass = persistentClass;
        this.session = session;
    }

    // The session needs to be available to all concrete HibernateDAOs
    protected Session getSession() {
        return this.session;
    }

    // Convenience method
    protected Criteria createCriteria() {
        return getSession().createCriteria(this.persistentClass);
    }

    // Every concrete implementation must decide how to apply a searchQuery
    protected abstract Criteria applySearchQuery(Criteria crit,
                                                 String searchQuery);

    @Override
    public List<T> findAll() {
        return (List<T>) createCriteria().list();
    }

    @Override
    public T findById(long id) {
        var entity = (T) this.getSession().get(this.persistentClass, id);
        return Optional.ofNullable(entity);
    }

    @Override
    public int totalCount(String searchQuery) {
        var criteria = createCriteria();
        applySearchQuery(criteria, searchQuery);
        criteria.setProjection(Projections.rowCount());

        var rowCount = (Number) criteria.uniqueResult();
        return rowCount.intValue();
    }
}

Now an actual DAO service interface can just extend DAO, and its concrete implementation can extend HibernateDAO:

java
public interface BeanDAO extends DAO<Bean> {

    // Additional method signatures might be added
}

public class HibernateBeanDAOImpl extends HibernateDAO<Bean> {

    public HibernateBeanDAOImpl(Session session) {
        super(Bean.class, session);
    }

    @Override
    protected void applySearchQuery(Criteria crit, String searchQuery) {
        // This entity is not searchable
    }
}

Thanks to this construction, we achieved the following:

  • DAO is the minimum contract any DAO must fulfill.
  • HibernateDAO provides the most basic functionality any concrete Hibernate-based DAO might need.
  • Standard-DAOs, like BeanDAO and its concrete implementation, only need a minimal amount of code.
  • Mocking for testing is more straightforward, too. Just like HibernateDAO, we can create a shared abstract class MockDAO.

Contract vs. Shared Logic

The best way to decide which to use is to realize what their original purpose is.

An interface is basically a contract describing the capabilities of type. The multiple inheritances of itself and its implementers allow a wide range of use, from simple marker-interfacer, or bundles of contracts to be followed. The addition of default methods even allows us to provide logic to all implementers and make extensibility possible.

Abstract classes are more than just contracts. They are partially implemented types, sometimes not even providing any implementation at all. This allows the creation of base classes, sharing logic and implementation details, to be completed by the inheritors.


Resources