1. Overview
In this lesson, we’ll explore the concept of interfaces, which act as a “contract” that a class can agree to follow. Essentially, an interface defines a set of behaviors (methods) that a class must provide, but it doesn’t specify how those behaviors should be implemented.
To follow along with the examples, let’s create a new project in our IDE and add a Main class in the com.baeldung package. We can place the code snippets from this lesson inside the main() method of that class.
If we want to reference the complete code we’ll have at the end of this lesson, we can look at: interfaces-end.
2. What Is an Interface?
An interface in Java is a reference type, similar to a class, but containing a collection of abstract methods or constants. An abstract method is a method that is declared without an implementation; it has a method signature, but no method body.
This concept is closely related to the idea of an API (Application Programming Interface). An API is a contract that allows different software components to communicate with each other. In Java, an interface is a powerful way to define an API for a specific behavior. It defines what a class can do, leaving it up to the class to decide how to do it.
The core purpose of an interface is to separate the definition of a behavior from its implementation. It defines what a class can do, leaving it up to the class to decide how to do it. This creates a “CAN-DO” relationship.
For example, a Task object CAN-BE exported. A Campaign object also CAN-BE exported. The behavior is the same, but the objects themselves are unrelated. The interface defines the export API, and any class that wants to offer this functionality must adhere to that contract.
3. Defining and Implementing an Interface
Moving on, let’s create an interface that defines the behavior of being exportable to a CSV format. In the com.baeldung.domain package, let’s define an interface by creating a file named Exportable.java:
public interface Exportable {
String FILE_TYPE = "CSV";
void exportToCSV();
}
In the code above, we use the interface keyword instead of class. Then, we define an abstract method named exportToCSV() and a constant named FILE_TYPE.
In interfaces, methods without a body are implicitly public and abstract, so we only write the method signature; no curly braces are needed for implementation.
Now, let’s create a class named Task that implements this contract using the implements keyword:
public class Task implements Exportable {
private String description;
private boolean completed;
private int workedHours;
// constructor, getters and setters
}
At this point, the compiler shows an error. It’s enforcing the contract, telling us that Task must provide an implementation for the exportToCSV() method. Let’s add it now:
public class Task implements Exportable {
// ...
@Override
public void exportToCSV() {
System.out.println("Exporting Task to CSV file...");
}
}
By overriding the exportToCSV() method, the Task class now fulfills the Exportable contract. Any object of type Task can now be treated as an Exportable.
4. Using Interfaces Polymorphically
Like classes, interfaces also support polymorphism. A variable of an interface type can hold a reference to an object of any class that implements that interface.
To demonstrate this, let’s create a new class called Campaign in the com.baeldung.domain package that also implements Exportable:
public class Campaign implements Exportable {
private String code;
// constructor
@Override
public void exportToCSV() {
System.out.println("Exporting Campign to CSV file...");
}
}
Next, in our Main class, we can define a helper method that accepts an Exportable object, allowing it to work with any current or future class that implements the interface:
public class Main {
public static void performExport(Exportable item) {
item.exportToCSV();
}
// ...
}
This method accepts a parameter of type Exportable. This means we can pass any object into this method as long as its class implements the Exportable interface.
Finally, let’s verify this by calling the method with a Task and a Campaign object in the main() method:
public static void main(String[] args) {
Task task = new Task("Finish lesson on interfaces");
Campaign campaign = new Campaign("C1");
performExport(task);
performExport(campaign);
}
Our single performExport() method works seamlessly with both Task and Campaign objects. When we pass a Task, its exportToCSV() method is called. When we pass a Campaign, its version is called.
Here’s the code output:
Exporting Task to CSV file...
Exporting Campaign to CSV file...
5. Inheritance vs. Interfaces
Furthermore, it’s crucial to know when to use inheritance and when to use an interface. Here’s a summary of the key differences:
- Relationship Type: We should use inheritance for an “IS-A” relationship, for example, a PriorityTask is a Task. We should use an interface for a “CAN-DO” relationship. For instance, a Task can be exported, and a Campaign can also be exported.
- Code Reuse: Inheritance allows a subclass to inherit fields and implemented methods, promoting code reuse. Interfaces traditionally define only method signatures and constants, not instance fields or full implementations (though they may contain default and static methods in modern Java).
- Multiple Inheritance: This is a critical distinction. A class in Java can only extend one superclass. However, a class can implement many interfaces. This makes interfaces the standard way to add common behaviors to classes that may belong to different inheritance hierarchies.
6. Default Methods in Interfaces
Before Java 8, adding a new method to an interface was a major problem. If we add a new abstract method to Exportable, every single class that implemented it (like Task and Campaign) would instantly fail to compile until they implemented the new method. This made evolving APIs very difficult.
Since Java 8, interfaces can contain methods with an implementation, known as default methods. This feature allows us to add new methods to an interface without breaking the classes that already implement it.
Let’s add a default method to our Exportable interface:
default void exportWithTimestamp() {
exportToCSV();
System.out.println(java.time.Instant.now());
}
Here, the exportWithTimestamp() method calls exportToCSV(), which is an abstract method. This works because any class that implements the Exportable interface is required to provide a concrete implementation of exportToCSV(). At runtime, Java will invoke the implementation specific to the actual object (e.g., Task or Campaign).
Our Task and Campaign classes don’t need to change, but they now automatically inherit this new method. We can call it from our main() method:
task.exportWithTimestamp();
campaign.exportWithTimestamp();
The addition of default methods to interfaces allows them to share implementation, just like an abstract class. The key difference is that interfaces cannot contain state (instance fields), while abstract classes can.
A simple rule is to use an abstract class for an “IS-A” relationship where subclasses are closely related and need to share common state (fields) and implementation. On the other hand, an interface should be used for a “CAN-DO” relationship where unrelated classes need to share a common behavior.
7. Conclusion
In this lesson, we learned that interfaces are used to define a contract of behaviors for classes.
Also, we saw that a class uses the implements keyword to promise it will provide an implementation for the interface’s abstract methods. The key distinction to remember is the relationship type: we should use inheritance for “IS-A” relationships and code reuse, and use interfaces for “CAN-DO” relationships to share behaviors across unrelated classes.
By programming to interfaces instead of concrete classes, we can write code that is more abstract, loosely coupled, and far more flexible.