If you ask someone who's not a programmer, or software engineer, or the like, "what's an interface?", most probably you'll get answers around "it's a boundary where two entities interact", "it's some sort of connection between two things through which they interact with each other", or "it's the means through which two unrelated elements interact with each other".
Then, what's a Java interface?
Interfaces in Java define behaviors. These behaviors are expressed as a group of methods in the body of the interface.
Then, some class can implement the interface methods as it sees fit, that's it, it defines how the behaviors specified in the interface are actually implemented. This class would be the first "entity" from our previous wider definition of interface.
The second "entity", the one that would interact with the class implementation through the interface, would be the caller: the one that calls the methods.
In summary we could say: the interface defines what behaviors/methods it provides to callers, and that must be implemented by any concrete class. It establishes a well-defined API contract between two parties: one party that provides the implementation and another party that will consume it.
If we were to create a simulation of a restaurant to illustrate the usage of an interface. We could have three elements:
We could call these three elements like this, respectively:
Customer
(class)Waiter
(another class)OrderProcessor
(the interface)Let's look at an example, by following our previous analogy of the restaurant:
// interface declaration
interface OrderProcessor {
// here we declare only the method (behavior) - but not the actual implementation
public void bringFood(String dishName);
}
// a class that implements the interface
class Waiter implements OrderProcessor {
// the actual logic of the method (body of the method) is defined here
public void bringFood(String dishName) {
System.out.println("Brought " + dishName + " to customer");
}
}
// another class that can ask for food
class Customer {
private String favoriteDish = "Fish and Vegetables";
void askForFood(OrderProcessor orderProcessor) {
// the customer interacts with the interface OrderProcessor
orderProcessor.bringFood(favoriteDish);
}
}
This interface is called OrderProcessor
and it defines the "bring food" behavior, which is declared as
a method named: bringFood
.
This is a very simple example; let's dive deeper into the intricacies of interfaces in Java.
There are two types of interface declarations:
In this article only the normal interface declaration is covered. Annotation interface declaration will be covered in a separate article.
Let's see the structure of an interface declaration:
{InterfaceModifier} interface TypeIdentifier [TypeParameters] [InterfaceExtends] [InterfacePermits] InterfaceBody
Where
{InterfaceModifier}
is one of:
Annotation
, public
, protected
, private
, abstract
,
static
, sealed
, non-sealed
, strictfp
.
interface
is the Java keyword used to specify we're declaring an interface.TypeIdentifier
is the name we want to give the interface. In our previous example we used OrderProcessor
.
[TypeParameters]
is a collection of types we can define within angle brackets (<>
).
For instance,<T>
. See
generics
and
§8.1.1
for more info about this.
[InterfaceExtends]
is the portion of the interface declaration where we can specify, after the keyword extends
,
the interface (if any) from which our interface is a direct subinterface. For instance,
extends Processor
.
[InterfacePermits]
is used when the sealed
modifier is specified, and it indicates, after the keyword
permits
, the other classes or interfaces (if any) that are allowed to implement/extend our
interface. For example, permits OrderProcessor, SuperWaiter
.
InterfaceBody
is the body
of our interface, including the curly braces ({}
). See next section.
It's important to note that unlike a class, an interface cannot be declared final
. However, an
interface may be declared sealed
(
§9.1.1.4
) to limit its subclasses and subinterfaces.
The structure of an interface body (
{InterfaceMemberDeclaration}
) is as follows:
{
ConstantDeclaration
InterfaceMethodDeclaration
ClassDeclaration
InterfaceDeclaration
}
Let's see what each of these elements are, and some examples of how they're used.
Constants (
ConstantDeclaration
) can be referred to as interface attributes, properties, fields. They are similar to class fields, except they
can NOT be modified —they're final—, and their declaration is as follows:
{ConstantModifier} UnannType VariableDeclaratorList;
In this case
ConstantModifier
can be any of the previously mentioned Annotation
, public
, static
,
and/or final
.
UnannType
, in short, refers to all the types we can use to declare what type the field is. For example,
Integer
.
VariableDeclaratorList
, simply put, is the actual name of the field (variable) and, optionally, and initializer.
Let's see an example of a constant in our OrderProcessor
interface.
interface OrderProcessor {
public int MAX_ORDERS_NUMBER = 1000;
// ... rest of the code omitted for brevity
}
Here, we declared a constant MAX_ORDERS_NUMBER
of type int
with a value of
1000
. It is public
(default in interfaces), so it's publicly accessible by anyone with
access to the interface.
Method declarations (
InterfaceMethodDeclaration
) are the declarations of the methods defined in the interface. They
are the methods that must be implemented by any class that implements the interface, unless the interface itself
provides a default implementation.
Default method implementations is a feature added in Java 8 that allows us to define the method body (its actual implementation) in the interface itself, as opposite to abstract methods, which require a concrete class to implement them.
More formally, a default method is an instance method declared in an interface with the
default
modifier. Its body is always represented by a block, which provides a default implementation
for any class that implements the interface without overriding the method. These methods are distinct from concrete
methods, which are declared in classes, and from private
interface methods, which are neither
inherited nor overridden (
§9.4
).
Let's see now how to declare these methods in our OrderProcessor
interface.
interface OrderProcessor {
public int MAX_ORDERS_NUMBER = 1000;
// abstract method -- must be implemented by the concrete class
public void bringFood(String dishName);
// also, an abstract method -- must be implemented by the concrete class
public abstract void bringWater();
// any class implementing the interface will inherit this
// default method implementation, unless overridden
default String getType() {
return "Waiter";
}
// only available to be called within the interface itself
private int getMaxOrders() {
return MAX_ORDERS_NUMBER;
}
}
Nested (or inner) class declarations (
ClassDeclaration
) were covered in a previous post (
Java classes and objects
). Let's quickly review an example of how it would look like in the body of an interface:
interface OrderProcessor {
// nested class declaration
class BillCalculator {
}
// ... rest of the code omitted for brevity
}
Nested interface declarations (
InterfaceDeclaration
) were also covered in
that post
. Let's see how to use it in our example.
interface OrderProcessor {
interface Calculator {
}
// ... rest of the code omitted for brevity
}
An interface is not of much use without a concrete class that implements its methods. So, let's get into how a class can implement the methods defined in the body of an interface.
A class can be declared to directly implement one or more interfaces (
§9,
§8.1.5
). This means that any instance of that class implements all the abstract
methods specified by
the interface.
A class necessarily implements all the interfaces that its direct superclasses and direct superinterfaces do. This multiple interface inheritance allows objects to support multiple common behaviors without sharing a superclass (§9).
Let's review again our previous example.
interface OrderProcessor {
public void bringFood(String dishName);
}
class Waiter implements OrderProcessor {
public void bringFood(String dishName) {
System.out.println("Brought " + dishName + " to customer");
}
}
Here, we specify after the keyword implements
all the interfaces that the class implements. Notice that
it's mandatory for the Waiter
class to implement the method bringFood
, otherwise the
code won't compile.
Let's see how the Waiter
class can implement another interface: Serializable
, which
belongs to the java.io
package.
class Waiter implements OrderProcessor, Serializable {
public void bringFood(String dishName) {
System.out.println("Brought " + dishName + " to customer");
}
}
As you notice, we only need to add the new interface being implemented after a comma (,
). This way we
can implement all the interfaces we want.
Sometimes there are interfaces that do not provide all the method declarations we need. In such cases, sometimes we need to create an interface that "inherits" that other interface method declarations, and, on top of that, add our own. This way any class implementing the new interface will inherit all the methods. This is a common pattern used when implementing our own Spring Data JPA Repositories.
An interface may be declared to be a direct extension of one or more other interfaces, meaning that it
inherits all the member classes and interfaces, instance methods, and static
fields of the interfaces
it extends, except for any members that it may override or hide (
§9).
Let's see in our example how would such extension look like.
interface Processor<T> { // 1
public void process(T element); // 2
}
interface OrderProcessor extends Processor<String> { // 3
public void bringFood(String dishName);
public default void process(String dishName) { // 4
bringFood(dishName);
}
}
There are several things going on in this example, so let's review it, point by point from the previous commented lines:
Processor
is declared as a generic interface that accepts a type T
(this is the type of the elements the interface will work with). See
§9.1.2
for more information about this.
Processor
declares an abstract method called process
, which takes an argument of type
T
.
OrderProcessor
extends Processor
and it specifies that the
elements it will work with are of type String
.
OrderProcessor
provides a default implementation for the process
method, inherited
from Processor
. The default implementation is actually just a call to the abstract method
bringFood
and it will be inherited by any concrete class.
This example shows how powerful our implementations can be by combining different Java features.
When an interface is defined and made available to client code (let it be our own code, library users, or third party vendors who rely on our interfaces definitions), it's extremely important to be careful with how we evolve it so we don't break existing code that use the current API contract defined by the interface.
While this is a topic for another whole article. There are some important points we should touch upon here. The following points are an extract from §13.5 .
public
is changed to not be declared public
, then
an IllegalAccessError
is thrown if a pre-existing binary is linked that needs but no longer has
access to the interface type, so such a change is not recommended for widely distributed interfaces.
sealed
, then an IncompatibleClassChangeError
is thrown
if a binary of a pre-existing subclass or subinterface of this interface is loaded and is not a permitted direct
subclass or subinterface of this interface (
§9.1.4
); such a change is also not recommended for widely distributed classes.
sealed
interface will not
break compatibility with pre-existing binaries. However, it may cause the execution of an exhaustive
switch
(
§14.11.1
) to
fail with an error (a MatchException
may be thrown) if the switch
encounters
an instance of the new permitted direct subinterface that was not known at compile time (
§14.11.3,
§15.28.2).
sealed
interface, then an IncompatibleClassChangeError
is thrown if the pre-existing
binary of the removed class or interface is loaded.
sealed
to be declared non-sealed
does not
break compatibility with pre-existing binaries. However, a non-sealed
interface X must
have a sealed
direct superinterface. Deleting the non-sealed
modifier would prevent
X from being recompiled, as every interface with a sealed
direct superinterface must
be sealed
or non-sealed
.
Let's see some more examples.
public interface Processor<T> {
int MAX_PROCESSES = 5;
boolean LANGUAGE_AGNOSTIC = true;
public void process(T element);
private boolean isValid(T e) {
return e != null;
}
}
public interface OrderProcessor {
class Sender { // <-- the 'static' keyword is default here before the keyword 'class'
}
static interface Calculator {
}
}
interface OrderProcessor {
public static interface Calculator {
}
}
sealed interface Processor<T> permits OrderProcessor {
void process(T element);
}
sealed interface OrderProcessor extends Processor<String> {
void process(String order);
}
final class Waiter implements OrderProcessor {
public void process(String order) {
System.out.println("Brought " + order + " to customer");
}
}
sealed interface Processor<T> permits OrderProcessor {
void process(T element);
}
non-sealed interface OrderProcessor extends Processor<String> {
void process(String order);
}
class Waiter implements OrderProcessor {
public void process(String order) {
System.out.println("Brought " + order + " to customer");
}
}
sealed interface OrderProcessor permits Waiter {
void process(String order);
}
final class Waiter implements OrderProcessor {
public void process(String order) {
System.out.println("Brought " + order + " to customer");
}
}
While interfaces are not a mandatory component of the software we implement, it's true our programs can use
interfaces to provide a common supertype for otherwise unrelated classes, and to make it unnecessary for related
classes to share a common abstract
superclass (
§9).
Many software patterns have been put into practice taking into account the concept of interfaces. These patterns solve well-known problems with a defined context and solution. Just to mention a few, we could point out SOLID, and many of the patterns presented by GoF; thus they bring flexibility, robustness, re-usability and a lot more benefits to our well-designed software solutions.
Nowadays, when systems are more complex, and we need to model more complex scenarios as well, interfaces play a vital role in promoting a loose coupling between the elements of our software. They're also a very powerful element of Java: they allow a class to inherit behaviors from multiple supertypes (so we can overcome the "limitation" of not having multiple inheritance in Java ), they play a pivotal role in polymorphism , they allow us to define clear boundaries and responsibilities for each software component and in most cases they make easier for us to implement automated test for the code we write.
There is so much material we could revise about Java interfaces that this single article wouldn't be enough to cover it all. I've presented here high level information as well as several of the implementation details about this topic which I hope, along with the linked documentation, is a good start for you to review and deepen your knowledge about Java.