Usefulness of interfaces and Default methods


Usefulness of interfaces

Interfaces can be extremely helpful in many cases. For example, say you had a list of animals and you wanted to loop through the list, each printing the sound they make.

{cat, dog, bird}

One way to do this would be to use interfaces. This would allow for the same method to be called on all of the classes

public interface Animal {
    public String getSound();
}

Any class that implements Animal also must have a getSound() method in them, yet they can all have different implementations

public class Dog implements Animal {
	public String getSound() {
		return "Woof";
	}
}
 
public class Cat implements Animal {
	public String getSound() {
		return "Meow";
	}
}
 
public class Bird implements Animal{
	public String getSound() {
		return "Chirp";
	}
}

We now have three different classes, each of which has a getSound() method. Because all of these classes implement the Animal interface, which declares the getSound() method, any instance of an Animal can have getSound() called on it

Animal dog = new Dog();
Animal cat = new Cat();
Animal bird = new Bird();
 
dog.getSound(); // "Woof"
cat.getSound(); // "Meow"
bird.getSound(); // "Chirp"

Because each of these is an Animal, we could even put the animals in a list, loop through them, and print out their sounds

Animal[] animals = { new Dog(), new Cat(), new Bird() };
for (Animal animal : animals) {
    System.out.println(animal.getSound());
}

Because the order of the array is Dog, Cat, and then Bird, "Woof Meow Chirp" will be printed to the console.

Interfaces can also be used as the return value for functions. For example, returning a Dog if the input is "dog", Cat if the input is "cat", and Bird if it is "bird", and then printing the sound of that animal could be done using

public Animal getAnimalByName(String name) {
	switch(name.toLowerCase()) {
		case "dog":
			return new Dog();
		case "cat":
			return new Cat();
		case "bird":
			return new Bird();
		default:
			return null;
	}
}
 
public String getAnimalSoundByName(String name){
	Animal animal = getAnimalByName(name);
	if (animal == null) {
		return null;
	} else {
		return animal.getSound();
	}
}
 
String dogSound = getAnimalSoundByName("dog"); // "Woof"
String catSound = getAnimalSoundByName("cat"); // "Meow"
String birdSound = getAnimalSoundByName("bird"); // "Chirp"
String lightbulbSound = getAnimalSoundByName("lightbulb"); // null

Interfaces are also useful for extensibility, because if you want to add a new type of Animal, you wouldn't need to change anything with the operations you perform on them.


Default methods

Introduced in Java 8, default methods are a way of specifying an implementation inside an interface. This could be used to avoid the typical "Base" or "Abstract" class by providing a partial implementation of an interface, and restricting the subclasses hierarchy.

Observer pattern implementation

For example, it's possible to implement the Observer-Listener pattern directly into the interface, providing more flexibility to the implementing classes.

interface Observer {
	void onAction(String a);
}
 
interface Observable{
	public abstract List<Observer> getObservers();
 
	public default void addObserver(Observer o){
		getObservers().add(o);
	}
 
	public default void notify(String something ){
		for( Observer l : getObservers() ){
			l.onAction(something);
		}
	}
}

Now, any class can be made "Observable" just by implementing the Observable interface, while being free to be part of a different class hierarchy.

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
abstract class WorkPerformer {
    public abstract void performWork();
}
 
public class CustomWorker extends WorkPerformer implements EventPublisher {
    private List<EventObserver> customObservers = new ArrayList<>();
 
    @Override
    public List<EventObserver> getEventObservers() {
        return customObservers;
    }
 
    @Override
    public void performWork() {
        notifyObservers("Initiating work");
        // Code implementation goes here...
        notifyObservers("Work completed");
    }
 
    public static void main(String[] args) {
        CustomWorker customWorker = new CustomWorker();
 
        customWorker.addObserver(new EventObserver() {
            @Override
            public void onEvent(String event) {
                System.out.println(event + " (" + new Date() + ")");
            }
        });
 
        customWorker.performWork();
    }
}

Diamond problem

The compiler in Java 8 is aware of the diamond problem which is caused when a class is implementing interfaces containing a method with the same signature.

In order to solve it, an implementing class must override the shared method and provide its own implementation.

interface CustomInterfaceA {
    public default String getCustomName(){
        return "customA";
    }
}
 
interface CustomInterfaceB {
    public default String getCustomName(){
        return "customB";
    }
}
 
public class CustomImpClass implements CustomInterfaceA, CustomInterfaceB {
    @Override
    public String getCustomName() { 
        // Must provide its own implementation
        return CustomInterfaceA.super.getCustomName() + CustomInterfaceB.super.getCustomName();
    }
 
    public static void main(String[] args) { 
        CustomImpClass customObj = new CustomImpClass();
 
        System.out.println(customObj.getCustomName()); // Prints "customAcustomB"
        System.out.println(((CustomInterfaceA)customObj).getCustomName()); // Prints "customAcustomB"
        System.out.println(((CustomInterfaceB)customObj).getCustomName()); // Prints "customAcustomB"
    }
}

There's still the issue of having methods with the same name and parameters with different return types, which will not compile

Use default methods to resolve compatibility issues

The default method implementations come in very handy if a method is added to an interface in an existing system where the interfaces is used by several classes.

To avoid breaking up the entire system, you can provide a default method implementation when you add a method to an interface. This way, the system will still compile and the actual implementations can be done step by step.


Modifiers in Interfaces

The Oracle Java Style Guide states:

  • Modifiers should not be written out when they are implicit.

(See Modifiers in Oracle Official Code Standard for the context and a link to the actual Oracle document.)

This style guidance applies particularly to interfaces. Let's consider the following code snippet:

interface I {
    public static final int VARIABLE = 0;
    public abstract void method();
    public static void staticMethod() { ... }
    public default void defaultMethod() { ... }
}

Variables

All interface variables are implicitly constants with implicit public (accessible for all), static (are accessible by interface name) and final (must be initialized during declaration) modifiers:

public static final int VARIABLE = 0;

Methods

1. All methods which don't provide implementation are implicitly public and abstract.
 
	public abstract void method();
Version ≥ Java SE 8
 
2. All methods with static or default modifier must provide implementation and are implicitly public.
 
public static void staticMethod() { ... }
 
After all of the above changes have been applied, we will get the following:
 
interface I {
	int VARIABLE = 0;
 
	void method();
	static void staticMethod() { ... }
	default void defaultMethod() { ... }
}

Basic Programs