Interface in Java

Just like abstract classes, we use an interface to implement abstraction in Java. It plays a crucial role in scenarios when multiple classes need to follow a common set of rules or provide a specific set of behaviours.

In this blog, we will discuss various properties, advantages and use cases of interfaces in Java.

Introduction

In object-oriented programming, abstraction simplifies the representation of complex systems by focusing on essential features and hiding implementation details. So, interfaces provide a way to achieve abstraction in Java by separating the definition of behaviour from its implementation.

  • It is just like a contract, which defines a list of methods that classes should implement. So clients can use these methods, without knowing their implementation details.
  • Multiple classes can implement the same interface and provide their own implementation for the methods. So interface helps us to group related classes together based on a common set of behaviours.
  • Since the clients only depend on the interface, we can easily substitute one implementation with another as long as they implement the same interface. This will make it easier to extend or modify the system without affecting other parts of the code.
  • By programming to interfaces rather than concrete implementations, we can achieve a higher level of abstraction. In simple words, the interface in Java helps us achieve modularity, code reusability, loose coupling, extensibility and maintainability. Think and explore!

What is an Interface in Java?

An interface is a collection of abstract methods (methods without an implementation) and all the implementing classes must provide an implementation for these methods.

  • An interface declaration consists of an access modifier, interface keyword and interface name.
  • It can contain constants. All constants in an interface are implicitly public, static, and final.
  • Interfaces can include default methods, which provide a default implementation for the method. Classes implementing the interface can use or override the default implementation.
  • Interfaces can contain static methods. Static methods are defined with the static keyword and can have an implementation. We can call it directly using the interface name.
  • All methods in an interface are implicitly public.
public interface Animal {
    // Declaration of abstract method
    void makeSound();
    void eat();
  
    // Constant declaration
    String CATEGORY = "Animals";
  
    // Default method with implementation
    default void sleep() {
        System.out.println("The animal is sleeping");
    }
  
    // Static method
    static void info() {
        System.out.println("This is an Animal interface");
    }
}

To use an interface, a class must implement it using the implements keyword.

public class Dog implements Animal {
    // Implementing the abstract methods
    public void makeSound() {
        System.out.println("The dog barks");
    }
  
    public void eat() {
        System.out.println("The dog is eating");
    }
  
    // Overriding the default method
    public void sleep() {
        System.out.println("The dog is sleeping");
    }
}

Interfaces can extend other interfaces using the extends keyword. This will help us to create more specialized interfaces that build upon the functionality of their parent interfaces.

public interface Animal {
    void makeSound();
}

public interface Bird extends Animal {
    void fly();
}

public class Eagle implements Bird {
    public void makeSound() {
        System.out.println("The eagle screeches");
    }
  
    public void fly() {
        System.out.println("The eagle is flying");
    }
}

In Java, a class can implement multiple interfaces by using the implements keyword followed by a comma-separated list of interface names. Due to this, it inherits the method declarations from each interface and must provide concrete implementations for all of them.

As we know, Java doesn't support multiple Inheritance because one class can extend only one class (Why? Explore and think). But we can use interfaces to achieve the same purpose because a class can extend two or more interfaces.

public interface Interface1 {
    void method1();
}

public interface Interface2 {
    void method2();
}

public class MyClass implements Interface1, Interface2 {
    @Override
    public void method1() {
        // Implementation for method1
    }
    
    @Override
    public void method2() {
        // Implementation for method2
    }
}

Here is another example in which we use interfaces for multiple inheritance by using default methods. Here MyClass class implements both interfaces and overrides the default method. To resolve the conflict and specify which default method implementation should be invoked, we are explicitly calling the default methods from the interfaces.

interface Interface1 {
    default void method() {
        System.out.println("Interface1 method call");
    }
}

interface Interface2 {
    default void method() {
        System.out.println("Interface2 method call");
    }
}

class MyClass implements Interface1, Interface2 {
    public void method() {
        // Invoke the method()from Interface1
        Interface1.super.method();
        // Invoke the method() from Interface2
        Interface2.super.method();
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass obj = new MyClass();
        obj.method();
    }
}

If a class wants to extend another class and implement multiple interfaces, the extends clause specifying the superclass comes before the implements clause for the interfaces.

Various forms of interface implementation in Java

Implementing runtime polymorphism using interface

In Java, we can use the interface as a reference data type just like any other class. But a reference of interface type must be assigned with an instance of a class that implements that interface.

The above idea will help us to implement runtime polymorphism in Java. For this, we create an interface reference and dynamically assign objects of different classes that implement the same interface at runtime. Let's understand this via an example:

public interface Animal {
    void sound();
}

public class Dog implements Animal {
    public void sound() {
        System.out.println("The dog barks.");
    }
}

public class Cat implements Animal {
    public void sound() {
        System.out.println("The cat meows.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        dog.sound();  // Calls the sound method of Dog class
        cat.sound();  // Calls the sound method of Cat class
    }
}

We declared the objects of Dog and Cat classes as Animal types. This allows us to treat both objects as generic Animal objects, regardless of their specific class. At runtime, when the makeSound() method is called on the Animal objects, the JVM determines the actual class of the object and executes the corresponding implementation of the method.

By using the above idea, we can easily add new classes that implement the Animal interface without modifying the existing code. Now consider this code example:

public interface Animal {
    void sound();
}

public class Dog implements Animal{
    public void name(){
        System.out.println("JIMMY");
    }
    //implementing method sound()
    public void sound(){
        System.out.println("BARK");
    }
}

public class Test {
    public static final void main(String[] args) {
        Animal animal = new Dog();
        animal.sound(); 
        animal.name();
    }
}

In the above code, animal.name() will cause a compilation error because the Animal reference (animal) does not have a name() method defined in the interface. Even though the object being referenced is an instance of the Dog class, the reference type determines which methods can be accessed. To use the name() method, we have to use the reference of Dog type.

public static final void main(String[] args) {
    Animal animal = new Dog();
    animal.sound();
    Dog dog = (Dog)(animal);//This is type casting.
    dog.name();
    dog.sound();
}

Why do we need interfaces?

Many times, separate groups of developers have to agree on a basic reference for how their software interacts. Ideally, each group should be able to write their code without knowledge of the other group's code. This is where interfaces come into play in defining such contracts.

For example, let's consider cars being driven by software. Automobile manufacturers write software to operate any car, which includes operations like stopping, turning left, turning right, starting, etc. Now, let's assume you have knowledge of machine learning and have written algorithms for a self-driving car. However, to make the car drive, you need to know how to make it stop, turn in either direction, start, etc.

So, there is a need for a common interface between the two groups: manufacturers and the driving unit. The manufacturers must provide the interface that specifies methods that can be invoked to operate the car. The driving unit then writes software using these interface methods according to their requirements. This way, neither group needs to know how the other group is writing its code.

Updating Interfaces

Imagine that in the future, the manufacturing team of the self-driving car has to add some features to their interface. How can we do this? Here are three possible ways:

  1. We can add new features by declaring the new method within the existing interface. Since the new method is added to the interface, all implementing classes need to provide an implementation for it. Note: modifying an existing interface can impact the classes that implement it.
  2. If we want to add new features without modifying the existing interface, we can create a new interface that extends the existing interface and add the new method to this new interface. Now we need to implement this new method in existing classes. Note: Adding a new interface can impact the classes that implement the existing interface.
  3. Another approach is to add the new method as a default method in the existing interface. Here implementing classes will automatically inherit the default implementation unless they explicitly choose to override them. Note: Default methods should be used wisely because they can lead to method conflicts if multiple interfaces provide default methods with the same signature.

Can we think to add new features using some other approach? Explore and think!

Benefits of using interfaces in Java

  1. Help us achieve abstraction in Java.
  2. Help us define common behaviour that can be shared across multiple classes. So interfaces promote consistency and provide a clear understanding of behaviours a class should provide.
  3. Help us implement an idea of multiple inheritance!
  4. Interfaces allow objects of different classes to be treated interchangeably based on their shared interface. This will promote flexibility because different implementations of the same interface can be used interchangeably in the same context.
  5. By programming to interfaces rather than concrete implementations, we can achieve loose coupling between components of our system. This makes it easier to modify or replace implementations without affecting the rest of the codebase.

Best practices to use interfaces in Java

  1. Define methods that are relevant to specific functionality and avoid adding unnecessary methods or including implementation details in the interface. In other words, interfaces should be minimal and focused on specific functionality. If required, design interfaces with a single responsibility and a clear purpose.
  2. Avoid creating large, monolithic interfaces. Instead, break them down into smaller, more specialized interfaces to promote loose coupling.
  3. Choose clear and descriptive names for interfaces to convey the purpose. The best practice would be to use nouns or noun phrases to describe what the interface represents.
  4. Whenever possible, use interfaces as a means of composition rather than inheritance.
  5. Use comments or documentation to describe the contract and the expected behaviour of each interface method. Clearly specify any preconditions, postconditions, or invariants that implementing classes must adhere to.
  6. Default methods were introduced in Java 8 to provide backward compatibility when adding new methods to existing interfaces. But use default methods carefully because they can introduce complexity in the inheritance hierarchy. How? Think!
  7. Prefer using a separate class or an enum for defining constants instead of including them in interfaces.
  8. When testing classes that depend on interfaces, use mock implementations of those interfaces to test the behaviour of the classes independently.
  9. Once an interface is in use, maintain its compatibility and stability. So we should avoid making breaking changes to existing interfaces because it can require updates across the implementing classes.
  10. Instead of programming to a specific implementation class, program to the interface. This means: Use the interface type as the reference type for variables, method parameters, and return types. This will provide flexibility and easy substitution of different implementations.
  11. Follow the Liskov Substitution Principle! For this, ensure that each implementation of an interface can be substituted with an object of the interface type without altering the correctness of the code. This principle guarantees that the behaviour specified by the interface is consistent across different implementations.
  12. Use meaningful names for methods and parameters.
  13. When defining interfaces, consider future extensibility. Anticipate potential new methods or behaviours that implementing classes may require. This will help in the easier adoption of new functionality without breaking existing implementations.

Critical ideas to think!

  1. What is a functional interface? How is it related to lambda expressions in Java?
  2. What is the interface segregation principle?
  3. What is the purpose of marker interfaces in Java?
  4. How can you ensure backward compatibility when modifying an existing interface in Java?

Thanks to Ankit Nishad for his contribution in creating the first version of this content. If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning, Enjoy OOP!

Share Your Insights

☆ 16-week live DSA course
☆ 16-week live ML course
☆ 10-week live DSA course

More from EnjoyAlgorithms

Self-paced Courses and Blogs

Coding Interview

Machine Learning

System Design

Our Newsletter

Subscribe to get well designed content on data structure and algorithms, machine learning, system design, object orientd programming and math.