Java Interfaces vs. Abstract Classes
Even though interfaces and abstract classes have many similarities at first look, especially after introducing default
methods, they have different use cases and capabilities.
Table of Contents
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 also possible.
Think of a simple alarm clock with a single alarm:
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:
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:
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:
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:
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:
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:
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:
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?
Without an explicit @Override
, the compiler won’t know which default method implementation to call, so we’re forced to override it:
But we can still rely on the default implementations by leveraging <type>.super
:
Another option is to extend the interface itself:
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:
Now an actual DAO
service interface can just extend DAO
, and its concrete implementation can extend HibernateDAO
:
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-basedDAO
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 classMockDAO
.
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
- Java 8 Interfaces: Default methods for backward compatibility
- What Is an Interface? (Oracle)
- Abstract Methods and Classes (Oracle)
- Interfaces (Java Language Specification)
- Abstract Classes (Java Language Specification)