A good explanation of how interfaces work and why they're used can be done with the following example. Let's say we have a zoo object with animals. We want all the animals to be able to make a noise depending on what animal they are. We start with the following class:
public class Zoo {
private List<Animal> animals;
public Zoo() {
animals = new ArrayList<>();
}
public void addAnimal(Animal animal) {
animals.add(animal);
}
public void roar() {
for(Animal a : animals) {
a.makeNoise();
}
}
}
This is basically just an object containing a list of animals and we can add animals to this list. When we call the method roar
we want all the animals to make their own noise and print it to the console. Now this is where interfaces are useful, since we know we will have multiple types of animals we can specify what a basic "animal" can do, these are generic traits that describe that they can do something but not how. For instance, a box and a human can both move, but a human can move on its own and a box cannot. Or maybe a human can move up stairs but a dog cannot. Knowing this, we create a basic interface describing that an animal can make a noise:
public interface Animal {
void makeNoise();
}
This will allow us to create as many animals as we want, while forcing them to implement our defined functionalities. So now, we can create some animals:
public class Cat implements Animal {
private String name;
public Cat(String name) {
this.name = name;
}
@Override
public void makeNoise() {
System.out.println(name + "said Meow");
}
}
public class Dog implements Animal {
@Override
public void makeNoise() {
System.out.println("Woof");
}
}
As you can see, we can give both classes their own functionality, while still enforcing both classes to at least be able to make their respective noise. In this case a cat can have a name, while a dog cannot. This means that we can now fill our Zoo
with any animal we want, and since they all have the same overlaying interface we don't have to, for instance, create more than one Collection to store each type of animal. We can just throw them all on one big pile based on their interface and we can call the proper method through the interface:
public void run() {
Zoo zoo = new Zoo();
zoo.addAnimal(new Cat("Bob"));
zoo.addAnimal(new Dog());
zoo.addAnimal(new Cat("William"));
zoo.roar();
}
As far as types are concerned. It works like it does in real life. Following our example, a dog is an Animal, but an animal is not necessarily a dog. Since our code in this case (when it's inside the zoo) only knows that it has Animals, but not what specific types, it only allows us access to the functionalities defined in the interface.
This means that, we are allowed to do stuff like:
Animal a = new Dog();
Dog b = new Dog();
Animal c = new Cat("Bob");
Cat d = new Cat("Wilbert");
But not:
Animal a = new Dog();
Dog b = a;
Since, as soon as we assign a
and say "you're of type animal" it doesn't know if the data inside a
is actually a dog or not.