Guide to Interfaces in Java

Introduction

Interfaces in Java are one of the basic concepts of object-oriented programming that are used quite often alongside classes and abstract classes. An interface represents a reference type, meaning that it is essentially just a specification that a particular class that implements it needs to obey. Interfaces can contain only constants, method signatures, default methods, and static methods. By default, interfaces only allow the use of a public specifier, contrary to classes that can also use the protected and private specifiers.

In this guide, we'll take a look at interfaces in Java - how they work and how to use them. We'll also cover all the concepts you might need to understand when working with interfaces in Java. After reading this guide you should have a comprehensive understanding of Java interfaces.

Method bodies exist only for default and static methods. However, even if they allow a body to be present within an interface, this is generally not a good practice as it can lead to a lot of confusion and make the code less readable. Interfaces cannot be instantiated - they can only be implemented by classes, or extended by other interfaces.

Why Use Interfaces?

We should already know that Java classes support inheritance. But when it comes to multiple inheritances, Java classes simply don't support it, unlike say, Python. To overcome this problem we use interfaces!

Classes extend other classes, and interfaces can also extend other interfaces, but a class only implements an interface. Interfaces also help in achieving absolute abstraction when needed.

Interfaces also allow for loose coupling. Loose coupling in Java represents a situation when two components have low dependencies on each other - the components are independent of each other. The only knowledge one class has about the other class is what the other class has exposed through its interfaces in loose coupling.

Note: Loose coupling is desirable because it makes modularization and testing easier. The more coupled classes are, the harder it is to individually test them and isolate them from the effects of other classes. An ideal state of class relationships includes loose coupling and high cohesion - they can be separated fully, but also enable each other with additional functionality. The closer the elements of a module are to each other, the higher cohesion. The closer your architecture is to this ideal state - the easier it'll be to scale, maintain and otherwise test your system.

How to Define Interfaces in Java

Defining interfaces isn't at all that hard. In fact, it's quite similar to defining a class. For the sake of this guide, we'll be defining a simple Animal interface, and then implement it within a variety of different classes:

public interface Animal {
    public void walk();
    public void eat();
    public void sleep();
    public String makeNoise();
}

We can make it have a variety of different methods to describe different behaviors of animals, but the functionality and the point stay the same no matter how many variables or methods we add. Therefore, we'll just keep it simple with these four methods.

This simple interface defines some animal behaviors. In more technical terms, we've defined the methods that must be found within the specific classes that implement this interface. Let's create a Dog class that implements our Animal interface:

public class Dog implements Animal{
    public String name;
    
    public Dog(String name){
        this.name = name;
    }
}

It's a simple class that only has one variable name. The keyword implements allow us to implement the Animal interface within our Dog class. However, we can't leave it just like this. If we tried to compile and run the program having implemented the Dog class like this, we'll get an error along the lines of:

java: Dog is not abstract and does not override abstract method makeNoise() in Animal

This error tells us that we didn't obey the rules set by the interface that we implemented. As it stands, our Dog class must define all four of the methods defined within the Animal interface, even if they return nothing and are just empty. In reality, we'll always want them to do something and won't define any redundant/class-specific methods in an interface. If you can't find a valid implementation of an interface method in a sub-class, it shouldn't be defined in the interface. Instead, skip it in the interface and define it as a member of that subclass. Alternatively, if it's another generic functionality, define another interface, that can be implemented alongside the first one. Our example is a little simplified, but the point stays the same even in more complicated programs:

public class Dog implements Animal{
    public String name;

    public Dog(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
    
    public void walk() {
        System.out.println(getName() + " is walking!");
    }

    public void eat() {
        System.out.println(getName() + " is eating!");
    }

    public void sleep() {
        System.out.println(getName() + " is sleeping!");
    }

    public String makeNoise() {
        return getName() + " says woof!";
    }
}

Once we've implemented our interface within our targeted class, we can use all of those methods as we usually did whenever we used public methods from any classes:

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Shiba Inu");

        dog.eat();
        System.out.println(dog.makeNoise());
        dog.walk();
        dog.sleep();
    }
}

This gives us the output:

Shiba Inu is eating!
Shiba Inu says woof!
Shiba Inu is walking!
Shiba Inu is sleeping!

Multiple Inheritance

As we've mentioned earlier, we use interfaces to solve the issue classes have with inheritance. While a class can't extend more than one class at a time, it can implement more than one interface at a time. This is done by simply separating the interfaces' names by a comma. A situation where a class implements multiple interfaces, or an interface extends multiple interfaces, is called multiple inheritance.

The question naturally arises: why isn't multiple inheritance supported in the case of classes, but is in the case of interfaces? The answer to that question is fairly simple as well - ambiguity. Different classes can define the same methods differently, thus ruining consistency across the board. While in the case of interfaces there is no ambiguity - the class that implements the interface provides the implementation of the methods.

For this example, we'll build on our previous Animal interface. Let's say we want to create a Bird class. Birds are obviously animals, but our Animal interface doesn't have methods to simulate a flying motion. This could easily be solved by adding a fly() method within the Animal interface, right?

Well, yes, but actually no.

Since we can have an infinite number of animal-named classes that extend our interface, we would theoretically need to add a method that simulates the behavior of an animal if it's previously missing so every animal would have to implement the fly() method. To avoid this, we'll just simply make a new interface with a fly() method! This interface would be implemented by all flying animals.

In our example, since the bird would need a method that simulates flying, and let's say flapping its wings, we'd have something like this:

public interface Flying {
    public void flapWings();
    public void fly();
}

Once again, a very simple interface. Now we can create the Bird class as we've discussed earlier:

public class Bird implements Animal, Fly{
    public String name;

    public Bird(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void walk() {
        System.out.println(getName() + " is walking!");
    }

    public void eat() {
        System.out.println(getName() + " is eating!");
    }

    public void sleep() {
        System.out.println(getName() + " is sleeping!");
    }

    public String makeNoise() {
        return getName() + " says: caw-caw!";
    }

    public void fly() {
        System.out.println(getName() + " is flying!");
    }

    public void flapWings(){
        System.out.println(getName() + " is flapping its wings!");
    }
}

Let's create a Bird object within our main class and output the results as we did earlier:

Bird bird = new Bird("Crow");
System.out.println(bird.makeNoise());
bird.flapWings();
bird.fly();
bird.walk();
bird.sleep();

It gives a simple output:

Crow says: caw-caw!
Crow is flapping its wings!
Crow is flying!
Crow is walking!
Crow is sleeping!
Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

Note: There will be cases (especially when implementing multiple interfaces) when not all of the methods declared in all of the interfaces will be defined within our class, despite our best efforts. For example, if our main Animal interface for whatever reason had a swim() method, within our Bird class that method would stay empty (or return null), like birds for the most part don't swim.

Interface Inheritance

Just like when we inherit the properties of one class from another using extends, we can do the same with interfaces. By extending one interface with another, we essentially remove the need for a class to implement multiple interfaces in some cases. In our Bird class example, we had it implement both the Animal and Flying interfaces, but we don't need to. We can simply let our Flying interface extend the Animal interface, and we'll get the same results:

public interface Flying extends Animal {
    public void flapWings();
    public void fly();
}

And the Bird class:

public class Bird implements Fly{
    // the same code as earlier   
}

The code of both the Flying interface and Bird class stays the same, the only thing that changes are single lines within both of these:

  • Flying now extends Animal and
  • Bird implements just the Flying interface (and the Animal interface by extension)

The Main method we used to showcase how to instantiate these objects and use them also stays the same as before.

Note: When our Flying interface extended the Animal interface, we didn't need to define all of the methods stated in the Animal interface - they'll be readily available by default, which is really the point of extending two interfaces.

This couples Flying and Animal together. This might be what you want but also might not be what you want. Depending on your specific use case, if you can guarantee that whatever flies must also be an animal - it's safe to couple them together. However, if you're not certain that what flies must be an animal - don't extend Animal with Flying.

Interfaces vs Abstract Classes

Since we've discussed interfaces in abundance in this guide, let's quickly mention how they compare to abstract classes, since this distinction raises a lot of questions and there are similarities between them. An abstract class permits you to make a functionality that subclasses can implement or override. A class can extend only one abstract class at a time. In the table below, we'll do a small comparison of both of these, and see both the pros and cons of using both interfaces and abstract classes:

Interface Abstract class
Can only have `public` abstract methods. Everything defined inside an interface is assumed `public` Can have `protected` and `public` methods
`abstract` keyword when declaring methods is optional The `abstract` keyword when declaring methods is compulsory
Can extend multiple interfaces at a time Can extend only one class or an abstract class at a time
Can inherit multiple interfaces, but cannot inherit a class Can inherit a class and multiple interfaces
A class may implement multiple interfaces A class can only inherit one abstract class
Cannot declare constructors/destructors Can declare constructors/destructors
Used to make a specification that a class needs to obey by Used to define the identity of a class

Default Methods in Interfaces

What happens when you create a system, let it go live in production and then decide you have to update an interface by adding a method? You have to update all classes that implement it as well - otherwise, everything grinds to a halt. To allow developers to update interfaces with new methods without breaking existing code, you can use default methods, which let you bypass the limit of defining method bodies in interfaces.

Through default methods, you can define the body of a common new method that's to be implemented in all classes, which is then added as the default behavior of all classes automatically without breaking them and without explicitly implementing them. This means that you can update interfaces extended by hundreds of classes, without refactoring!

Note: Using default methods is meant for updating existing interfaces to preserve backward compatibility, not for being added from the get-go. If you're in the designing stage, don't use default methods - only when adding previously unforeseen functionality that you couldn't have implemented earlier.

Say your client is super happy with your application - but they've realized that birds don't only fly() and flapWings() besides the stuff other animals do. They also dive()! You've already implemented a Crow, Pidgeon, Blackbird, and Woodpecker.

Refactoring is annoying and difficult, and due to the architecture you made - it's hard to implement a dive() in all birds before the deadline arrives. You can implement a default void dive() method in the Flying interface.

public interface Flying {
    public void flapWings();
    public void fly();
    default void dive() {System.out.println("The bird is diving from the air!"}
}

Now, within our Bird class, we can simply leave out the implementation of the dive() method, since we've already defined its default behavior in the interface:

public class Bird implements Fly{
    public String name;

    public Bird(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
    
    public void fly() {
        System.out.println(getName() + " is flying!");
    }

    public void flapWings(){
        System.out.println("The " + getName() + " is flapping its wings!");
    }
}

A Bird instance can dive() now, without any refactoring of the Bird class, giving us much-needed time to implement it in a graceful and non-rushed manner:

Bird bird = new Bird("Crow");
bird.dive();

This results in:

The bird is diving from the air!

Static Methods in Interfaces

Finally - we can define static methods in interfaces too! Since these don't belong to any specific instance, can't be overridden and are called by prefixing them with the interface name.

Static interface methods are used for common utility/helper methods, not for implementing specific functionality. The support was added to avoid having non-instantiable helper classes besides interfaces, and bundling the helper methods from separate classes into interfaces. In effect, using static methods helps you avoid an extra class definition which would've held a few helper methods. Instead of having an Animal interface and AnimalUtils as a helper class - you can now bundle the helper methods from the AnimalUtils class into static Animal methods.

This increases the cohesion in your architecture, since you have fewer classes and the ones you do have are more linearly separable.

For instance, say you'd like to validate your Animal implementations, whatever validation would mean for your specific application (such as checking whether an animal is registered in a book). You could define this as an intrinsic static method of all Animals:

interface Animal {
    public void walk();
    public void eat();
    public void sleep();
    public String makeNoise();
    
    static boolean checkBook(Animal animal, List book) {
        return book.contains(animal);
    }
}

The Dog definition is the same as before - you cannot override or otherwise alter this method, and it belongs to the Animal interface. You can then use the interface to check whether a Dog instance, belongs in an arbitrary book (say a registry of household pets in a city) via the Animal utility method:

Dog dog = new Dog("Shiba Inu");

boolean isInBook = Animal.checkBook(dog, new ArrayList());
System.out.println(isInBook); // false
        
isInBook = Animal.checkBook(dog, List.of(dog));
System.out.println(isInBook); // true

Functional Interfaces

Functional interfaces were introduced in Java 8, and they represent an interface that contains only a single abstract method within it. You can define your own functional interfaces, there the plethora of built-in functional interfaces in Java such as Function, Predicate, UnaryOperator, BinaryOperator, Supplier, and so on are highly probable to cover your needs out of the box. These can all be found within the java.util.function package. However, we won't be diving deeper into these, as they're not really the main topic of this guide.

If you'd like to read a holistic, in-depth and detailed guide to functional interfaces, read our "Guide to Functional Interfaces and Lambda Expressions in Java"!

Interface Naming Conventions

So, how do you name interfaces? There is no set rule, and depending on the team you're working with, you might see different conventions. Some developers prefix interface names with I, such as IAnimal. This isn't very common with Java developers, and is mainly carried over from developers who worked in other ecosystems before.

Java has a clear naming convention. For example, List is an interface while ArrayList, LinkedList, etc. are implementations of that interface. Additionally, some interfaces describe the abilities of a class - such as Runnable, Comparable and Serializable. It mainly depends on what your interface's intentions are:

  • If your interface is a generic backbone for a common family of classes where each set can be fairly accurately described by its family - name it as the family name, such as Set, and then implement a LinkedHashSet.
  • If your interface is a generic backbone for a common family of classes where each set cannot be fairly accurately described by its family - name it as the family name, such as Animal, and then implement a Bird, rather than a FlyingAnimal (because that isn't a good description).
  • If your interface is used to describe the abilities of a class - name it as an ability, such as Runnable, Comparable.
  • If your interface is used to describe a service - name it as the service, such as UserDAO and then implement a UserDaoImpl.

Conclusion

In this guide, we've covered one of the most important basic concepts for object-oriented programming in Java. We've explained what interfaces are and discussed their pros and cons. We've also shown how to define them and use them in a few simple examples, covering multiple inheritances and interface inheritance. We discussed the differences and similarities between interfaces and abstract classes, default and static methods, naming conventions and functional interfaces.

Interfaces are fairly simple structures with a simple goal in mind, yet they are a very powerful tool that should be utilized whenever the opportunity presents itself so that the code becomes more readable and clearer.

Last Updated: August 29th, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms