Home CPSC 240

Interfaces

 

Overview

There are two primary reasons to use inheritance:

  1. To be able to reuse bits of code for different classes. For example, we were able to get the code dealing with names and ids in both the Student and Employee classes by putting it into the base class of Person.
  2. To be able to treat lots of different types of objects the same, AKA polymorphism. We did this when we just called the attack() method on lots of different types of enemies and they worked differently.

It turns out that this second use is actually the bigger deal. Today we will talk about another Java feature that allows us to use polymorphism, which is the interface.


 

Multiple Inheritance

To understand interfaces, we first have to talk about a restriction when it comes to inheritance. Java does not allow us to have more than one base class. The primary reason for this is called the "Diamond Problem". Imagine we have the following inheritance hierarchy:

The diamond problem

Here, we have the Person/Student/Employee class hierarchy we had before (with most things removed for clarity). However, now we are also trying to create a StudentWorker class which inherits from both Student and Employee. However, this presents some problems:

Java decided to disallow multiple inheritance to prevent these situations from occurring. In Java, each class has exactly one base class.


 

Interfaces

However, there are times when this would be useful. In order to get around this, Java has something called an interface. An interface is like a base class except:

An interface can only specify a set of method declarations:


interface Animal {
    public void speak();
    public void eat();
}

If we want a class to inherit from this interface, we say that it implements it:


class Cat implements Animal {
    public void speak() {
        System.out.println("Meow.");
    }

    public void eat() {
        // eat cat food
    }
}

So an interface is essentially the same thing as an abstract class, except it can only have abstract methods, and it can't have any instance variables. Because of this, they only make sense for the second use of inheritance (polymorphism) and not the second (sharing of code).

Java does allow us to implement multiple interfaces for our classes. Notice that without method bodies or instance variables, the issues with the diamond problem go away. They can't have instance variables, so there's no question of how many copies of one we might get. And since they don't have method implementations, there's no question of which one we call.

A class has exactly one base class, but can implement as many interfaces as it wishes.


 

Example

Let's say that we have a program where many aspects of it need to be written into files before the program ends. Perhaps this could be a game where we need to save the state of everything when the player saves. We might want to do all of this in one place in the code, so let's say we have a class called SaveManager:


public class SaveManager {
    private String filename;

    public SaveManager(String filename) {
        this.filename = filename;
    }

    public void save() {

    }

    public void load() {

    }
}

Here, we need to somehow have the save and load methods save/load many different types of objects. One solution would be to have it take in as parameters all the different classes in our program, such as a Player, Enemy, Map, Inventory, etc. objects. But that is probably going to be too much to keep track of and not scale well. As we add new objects, we need to change the SaveManager to be able to handle them.

A better way is to make an interface. We can make an interface called Saveable perhaps, which includes methods for doing saving and loading:


public interface Saveable {
    public void save(PrintWriter pw);
    public void load(Scanner in);
}

Now we can write the SaveManager so it only has to deal with Saveable objects:


public class SaveManager {
    private String filename;
    private ArrayList<Saveable> list;

    public SaveManager(String filename) {
        this.filename = filename;
        list = new ArrayList<>();
    }

    public void addSavedObject(Saveable s) {
        list.add(s);
    }

    public void save() {
        File f = new File(filename);
        PrintWriter pw = new PrintWriter(f);
        for (Saveable s : list) {
            s.save(pw);
        }
        f.close();
    }

    public void load() {
        File f = new File(filename);
        Scanner in = new Scanner(f);
        for (Saveable s : list) {
            s.load(in);
        }
        f.close();
    }
}

Now this system is capable of saving and loading any object which implements the Saveable interface. We have separated out the code which deals with the file from needing to know exactly what kinds of objects its working with. Now to make a class able to be saved, we can just implement the interface:


public class Player implements Saveable {
    private int hp;
    private int x, y;
    
    public void save(PrintWriter pw) {
        pw.println(hp);
        pw.println(x);
        pw.println(y);
    }

    public void load(Scanner in) {
        hp = in.nextInt();
        x = in.nextInt(); 
        y = in.nextInt();
    }
}

Since this is just an interface, the Player class could also inherit from a class, and implement other interfaces as well.

Copyright © 2024 Ian Finlayson | Licensed under a Creative Commons BY-NC-SA 4.0 License.