AP Computer Science

Unit 10, part 2: Interfaces

10.6.0. Overview - Interfaces

One way of re-using code in an Object-Oriented language is by using Inheritance: superclasses and subclasses that are hierarchically organized. This has an enormous number of benefits for us, both in reducing the amount of code that we have to write, and improving the ways that we use the code that we do write.

We've already seen that one of the ways that we can re-use previously written classes is by extending a superclass using inheritance to leverage the variables and methods that have already been created.

Now we'll see that we can reuse previously written methods by writing interfaces that can be used by any class that is written to implement those methods. The actual behavior of the algorithm/interface will vary according to whatever class it is that is "plugged into it."

Recall that the idea that a program's behavior will vary depending on context is called polymorphism.

Polymorphism

In object-oriented programming, the concept of polymorphism refers to the ability of classes, methods, and variables to be written such that they can be re-used for multiple purposes.

This typically occurs via inheritance and interfaces.

Let's begin our study of interfaces by looking at a simple example.

10.6.1. The Measure of a Class

We've been looking at the BankAccount class. What if we needed a static method to calculate the average balance of a series of BankAccount objects? Here's one version of that method.

/**
 * Gets the average of an array of BankAccount objects
 * @return the average, or 0 if no data is in the array
 */
public static double average(BankAccount[] objects)
{ 
    double sum = 0;
    for (BankAccount obj : objects)
        sum += obj.getBalance();
    if (objects.length == 0) 
        return 0;
    else 
        return sum / objects.length;   
}

This code works just fine.

Now, what if I had Country class, and I needed to write a method that would identify the average area of an array of countries?

I'd have to write practically the same method for that new class:

/**
 * Gets the average of an array of Country objects
 * @return the average, or 0 if no data is in the array
 */
public static double average(Country[] objects)
{ 
    double sum = 0;
    for (Country obj : objects)
        sum += obj.getArea();
    if (objects.length == 0) 
        return 0;
    else 
        return sum / objects.length;   
}

It's clear that the algorithm is the same for both of these classes, it's just the values or measures that are different in each case: .getBalance() for the bank accounts and getArea for the countries.

It is for these situations that interfaces were designed.

Our solution is to write a single static method average that will calculate the average of any measurable quantity. That single method will be usable by any class that "implements the Measurable interface."

Let's go through the entire process to get some idea of how it works.

10.6.2. Declaring an Interface Type

Interface types

A Java interface is a collection of constants and abstract methods (methods that have a header but no body of code). The Interface type declares these methods and their "signatures" so that they can be implemented by other classes in a number of different contexts.

Once an interface has been declared, an object can implement that interface type, as long as it agrees to supply code for the header.

In the same way that an abstract class must be implemented by a subclass that inherits it, the methods of an interface type must be implemented later. When declaring an interface type, the only thing indicated is a header for the method:

public interface Measurable
{
    double getMeasure();   // header with no implementation
}

In general, an interface can have multiple methods, but here, we just have one. It can also have parameters, although we don't have any here.

Also:

Note that we added the -able suffix to make the interface type an adjective: something-able. This is something that you'll see for lots of different interfaces, and it's an unofficial standard used by Java coders to make the code a little easier to read. Examples of interface names:

This interface type, now, can be used in our .average() method. Let's see what that looks like:

/**
 * Package that works with Measurable objects
 */
 public class Data
 {
    /**
     * Gets the average of an array of Measurable objects
     * @return the average, or 0 if no data is in the array
     */
    public static double average(Measurable[] objects)
    { 
        double sum = 0;
        for (Measurable obj : objects)
            sum += obj.getMeasure();
        if (objects.length == 0) 
            return 0;
        else 
            return sum / objects.length;  
    } 
}

10.6.3. Writing a class that uses the interface

So, we've got our interface written, and we've got a static method that takes advantage of that interface. We now need to write some classes that actually implement that interface so we can see how it all works. Let's use the BankAccount class.

A class implements an interface type when it declares the interface in an implements clause, and goes on to implement the actual method(s) that the interface requires.

Here's our new BankAccount class, in abbreviated form, with the new getMeasure method at the end:

/**  
  * A bank account has a balance can be changed by deposits
  * and withdrawals.
*/
public class BankAccount implements Measurable
{
    private double balance;  // declares the instance field!
    
    public BankAccount() { balance = 0; }
        
    public BankAccount(double initialBalance) { balance = initialBalance; }

    public void deposit(double amount) { balance = balance + amount; }

    public void withdraw(double amount) { balance = balance - amount; }

    /** Return the balance in the account.
     *  @return the current balance
     */
    // public double getBalance()  is what we had before. Now....
    
    public double getMeasure()
    {
        return balance;
    }
}

You can see that the method getMeasure, which in this implementation returns a double value, is used to return the balance of a bank account. getMeasure can also be used by other implementations to return other types of values.

10.6.4. The differences between interfaces and inheritance

Extending classes via inheritance and implementing interfaces are two ways of re-using code, but they are used for completely different purposes.

InheritanceInterfaces
Inheritance allows a subclass to extends a superclass that is similar in structureInterfaces are used to provide a similar processing capability to classes that are otherwise completely different
A subclass can only extend a single superclass.A class can extend multiple interfaces.
A subclass has a state, represented by the value of its instance variables.An interface type has no state.
Example: A Shape class might have a number of subclasses that extend it: Circle, Square, Rectangle, Hexagon, etc. Other classes like Line, Photo, Text, and Background would probably not extend the Shape class. Example: The Drawable interface might be implemented by the Shape class, the Photo class, the Line class, and the Text class, which all implement a .draw() method for displaying each of those objects in a window on the computer.

Make BankAccount implement Measurable

Take a moment to download MeasurableBankAccount-Student.zip, open it up, and explore the files in that BlueJ directory.

Write the missing code that will get the tester to produce data.

Write a Coin class that implements Measurable

Take a moment to download MeasurableBankAccountAndCoin.zip, open it up, and explore the files in that BlueJ directory.

Note that the Tester in that package has some lines referring to a Coin class that implements measurable. Write that Coin class, and see if you can get it all to work.

10.7.0. Converting from Classes to Interfaces

In working with the Measurable interface, you might have modified the Coin class to implement Measurable as follows:

public class Coin implements Measurable
{   
    private double value;
    private String name;
    
    public Coin(String aName, double aValue) 
    { 
        value = aValue; 
        name = aName;
    }
    
    public double getMeasure()
    {
        return value;
    }
    
    public String getName() 
    {
        return name;
    }
}

The code highlighted above represents the changes that we've made from the original Coin class.

Let's write a utility Data that will describe a getAverage() method.

/**
 * Package that works with Measurable objects
 */
 public class Data
 {
    /**
     * Gets the average of an array of Measurable objects
     * @return the average, or 0 if no data is in the array
     */
    public static double average(Measurable[] objects)
    { 
        double sum = 0;
        for (Measurable obj : objects)
            sum += obj.getMeasure();
        if (objects.length == 0) 
            return 0;
        else 
            return sum / objects.length;  
    } 
}

Assuming that a Data utility has been set up to describe the getAverage() method, we can demonstrate the Measurable interface here:

public class MeasurableTester
{
    public static void main(String[] args)
    {
        // Set up an array of Measurable objects
        Measurable[] coins = new Measurable[3];
        coins[0] = new Coin("quarter",0.25);
        coins[1] = new Coin("bitcoin",400);
        coins[2] = new Coin("drachma",0.01);
        // Find the average balance of those accounts
        System.out.println(Data.average(coins));
    }
}

Take a look at what's happening with the coins array. That array is declared to store Measurable objects, but we're storing Coin objects in there. Shouldn't that be a problem?

Because Coin implements Measurable, this type conversion is legal, and happens automatically. You can convert from a class type (Coin here) to the type of any interface that the class implements (Measurable here).

This is interesting, and can occasionally be a source of difficulty, because when you have an object of type Measurable, there's no way of identifying the exact type of the object to which it refers. All you know is that it has a getMeasure() method.

10.7.2. Examining Conversions

10.7.2.1. Converting from an implementing class to the interface

You can convert from any class type to the type of any interface that the class implements.

The header of the average method states:

public double average(Measurable[] objects)

... which clearly states that the average method expects an array of Measurable objects.

The statement

double averageBalance = Data.average(accounts);

is legal because BankAccount implements the Measurable interface.

You could even do this:

Coin dime = new Coin("dime", 0.10);
Measureable x = dime;

This isn't okay, however:

Rectangle myRect = new Rectangle(0,0,20,10);
Measurable x = myRect;

... because Rectangle doesn't implement the Measurable interface. Rectangle is a Java library that we can't get in to manipulate. (We'll figure out a way around that later.)

10.7.2.2. Converting back from an Interface?

What happens when this code is executed?

Measurable x = new Coin("dime", 0.10);
System.out.println(x.getMeasure());

We've stored a reference to our coin in a Measurable variable, so using the getMeasure() might be tricky—how does Java know which of the .getMeasure() methods to call? It turns out that Java looks for the correct version, based on the actual object (here, the Coin object) and the .getMeasure() defined for that object.

10.7.2.3. Getting a Class back from an Interface

Sometimes you'll actually need to retrieve an object that had been stored as an interface reference. Perhaps, in addition to our Data.average() method we write a Data.larger method:

public static Measurable larger (Measurable object1, Measurable object2)
{
if (object1.getMeasure() > object2.getMeasure())
    return object1;
else
    return object2;
}

This method will return the larger of the two objects, but it doesn't know anything about the actual types of those objects. It only knows them as Measurable references. If I try this:

Coin coin1 = new Coin("old penny", 0.01);
Coin coin2 = new Coin("new penny", 0.011);
Measurable max = Data.larger(coin1, coin2);
System.out.println(max.getName());

... it won't work. The Measurable interface doesn't have a .getName() method, so the compiler won't let this happen.

If you're sure that your Measurable reference points to a specific class, however, you can cast it to that class, and then you'll be able to use that method.

Coin coin1 = new Coin("old penny", 0.01);
Coin coin2 = new Coin("new penny", 0.011);
Measurable max = Data.larger(coin1, coin2);
Coin maxCoin = (Coin) max;
System.out.println(maxCoin.getName());

10.7.3. The Comparable Interface

Up to this point we've been playing around with the Measurable interface that we wrote, and it's a bit contrived. Now let's see if we can see how to take advantage of an actual Java interface that's available to us.

The Comparable API

In the Java API, look up the Comparable interface.

Show/hide interpretation of API

The Comparable interface declares a single method, compareTo.

Based on this, we know that:

  • Any class that implements Comparable has to have a definition for the compareTo method that follows the rules of the API.
  • Any class that implements Comparable will be able to be compared to another object of that class, based on the result of calling compareTo.

10.7.3.1. Java Classes that implement Comparable

What classes implement Comparable?

Of the common classes that we use in this class, do any of them implement Comparable? How would you find out?

7.7.3.2. Implementing Comparable

Let's revisit the Coin class, and modify it so that we can compare coins with one another.

Comparable Coins

Using what we've learned about the Comparable interface, write a Coin class so that coins are comparable.

You can find further instructions here.

Why implement the Comparable interface when we can just compare the values directly? Because we can use sort methods on things that are Comparable, but only if they implement that interface! Add these lines to your ComparableTester:

import java.util.Collections;
.
.
        
Collections.sort(coins);    // only works if Coin implement Comparable
for (Coin aCoin : coins)
{
    System.out.println(aCoin.getName() + " has a value of " + aCoin.getValue());
}

10.7.4. One more Interface Example: Edible

Let's write a brand new interface, a couple of classes that implement it, and a tester to demonstrate it.

The Edible interface

Write the following files:

  1. The public interface Edible, which includes headers for two methods: the String method getFoodGroup() and the double method getCaloriesPerServing()
  2. The Orange class, which implements Edible. The orange is of the "Fruit" food group and has 70 calories. The Orange class should also have an overloaded constructor that creates an orange of an "unknown" type, or of a type specified as a parameter, perhaps "Valencia," or "navel," or "blood."
  3. The CandyBar class, which should have a similarly overloaded constructor, a food group of "Junk Food," and 250 calories per serving.
  4. The AfterSchoolSnack class, for which a public interface is specified here:

    Show/hide interpretation of API

    /**
    * AfterSchoolSnack uses various types of food that 
    * implement the Edible interface.
    */
    public class AfterSchoolSnack
    {
    private double totalCaloriesConsumed;
    
    /**
     * Constructor for AfterSchoolSnack objects
     */
    
    
    
    
    /**
     * The eat method allows a number of servings of an Edible food
     * to be consumed.
     * @param aFood  a food item of a class that implements Edible
     * @param servings  an double value indicating the number of servings consumed
     */
    
    
    
    
    
    
    /**
     * The getCalorieCount method returns the calories consumed.
     * @return the total calories consumed in the snack
     */
    
    
    
    
    
    
    
    }

You can download EdibleTester.java to try out your files.

10.8.0. Overview - Interfaces for Callbacks, Inner Classes

You've got inheritance all figured out by now, and that interface thing is almost comprehensible. Remember: completely different types of classes can "implement an interface," which means

  1. the class must describe specific details of the method(s) that they're going to implement, and
  2. that interface method can then be used to interact with that class.

The Measurable example was a bit silly, but it allowed us to write a getMeasure() method for different classes, and then we could use that method for lots of different things.

More useful was the Comparable interface, which we implemented in the Coin class so that we could compare coins by value.

We've got one more challenge in this unit. Figuring out how to use interfaces for callbacks.

Let's get started.

10.8.1. Using Interfaces for Callbacks

We're going to expand our study of interfaces, because right now, they're limited. We have two issues with interfaces that you may not have realized yet.

  1. We can't use them for classes that we haven't written—we don't have permissions to go in and rewrite existing Java libraries like Rectangle.
  2. We're limited in to a single use of any given method per class. We can't define .getMeasure() for more than a single value, but maybe I want to get measures of both maxValue and balance. Or maybe I want to use Comparable with both the value and the date of the Coin class.

The source of the problem is that each object is responsible for its own implementation of the interface. In the case of the Rectangle class, ordinarily I'd need to somehow dig into the Rectangle code to add the .getMeasure() method, and I can't do that.

Our solution to this is to develop a separate object that we do control—RectangleMeasurer "callback object"—that will carry out the measuring for us. The algorithm with that callback object will call it when information is needed that the callback can provide. We want the Data class to be able to "call back" to the interface method when it needs information about an otherwise inaccessible object.

Let's see an example using the Rectangle class.

  1. The interface Measurer mostly looks the same as it did before: it specifies a method that we're going to be using to measure stuff, and that method will be defined in classes that implement Measurer. There's an explicit parameter here, however. We're going to send in the object that we want to be measuring. Because we don't know exactly what kind of object we're going to be sending it, it's of the type Object. (Remember, Object is a superclass for all objects in Java.)
  2. A new layer is the helper class RectangleMeasurer that's going to implement Measurer. Because of that, we have to write a method definition for measure. If we were trying to measure other things, we'd obviously write helper classes for those types of objects as well.
  3. The Data class looks much as it did before. We're going to add objects in, and keep track of counts, and sums, and be able to calculate averages. Before we were adding Measurable objects, but we were limited in how much we could manipulate them. Now we're adding an Object, and we're going to use a callback to the interface method .measure(), which RectangleMeasurer implements.
  4. The DataSetTester sets up a Measurer, and sets up a Data that will use that Measurer so that when we add objects to the data set, we can use the interface methods.

Take a look at the example and see if you can see how the pieces all fit together.

10.8.2. Inner Classes

Inner classes are classes that are trivial enough that we don't want to have to go to the trouble of writing a whole separate class. For classes that only have a limited, local purpose, we can write short, easy version as "inner classes."

In inner class is one that is contained within the body of an outer class. Writing an inner class is simply an organizational strategy that keeps one from having to write all of the extra code that would be required for a standard, free-standing, class.

Let's see how that works.

10.8.3. The "Doubles" Game

The game of Doubles is an awesome game (that I made up) played with two six-sided dice. Here are the rules:

  1. Roll the two dice.
  2. If you rolled doubles, you win.
  3. If you didn't, you get to roll again.
  4. Go to step 2.

I love this game!

Following good Object-oriented Design Principles I broke the game up into a series of components, which look like this:

The Die class is responsible for managing a single Die, while the DiceGame allows for the rolling of two dice, and checking to see if doubles have been rolled. The DiceGameRunner class, then, actually has the user play the game, with a Scanner for entering Yes/No input.

You can take a look at each of these classes here.

Show/hide Die.java

/**
 * Creates a die and allows it to be cast.
 */

import java.util.Random;

public class Die
{
    private int roll;
    private int sides;
    private Random generator;

    /**
     * Constructor for objects of class Die
     * @param numSides the number of sides on the die
     * @return the result of the roll
     */
    public Die(int numSides)
    {
        sides = numSides;
        generator = new Random();
        cast();
    }

    /**
     * cast rolls the die and returns a result
     * @return     the roll, a value between 1 and sides inclusive 
     */
    public void cast()
    {
        roll = generator.nextInt(sides) + 1;
    }
    
    /**
     * getRoll returns the result of the previous roll
     * @return   the roll
     */
    public int getRoll()
    {
        return roll;
    }
}

Show/hide DiceGame.java

/**
 * DieGame class let's you play a die game. Roll two dice
 * and if you roll double anything, you win. Otherwise, you
 * have to keep on rolling.
 */

public class DiceGame
{
    private Die d1;
    private Die d2;
    int[] results;
    
    /**
     * Constructor for objects of class DieGame
     */
    public DiceGame()
    {
        d1 = new Die(6);
        d2 = new Die(6);
        results = new int[2];
    }

    /**
     * takeTurn method rolls the two dice
     */
    public void takeTurn()
    {
        d1.cast();
        d2.cast();
    }
        
    /**
     * getResults returns an Array with the two dice rolls
     * @return a two-element Array with the two dice rolls in it
     */
    public int[] getResults()
    {
        results[0] = d1.getRoll();
        results[1] = d2.getRoll();
        return results;
    }
    
    /**
     * doubles method returns true if the two dice have the same value
     * @return true if dice have same value, otherwise false
     */
    public boolean doubles()
    {
        return results[0] == results[1];
    }

}

Show/hide DiceGameRunner.java

/**
 * Write a description of class DieGameTester here.
 * 
 * @author (your name) 
 * @version (a version number or a date)
 */

import java.util.Scanner;

public class DiceGameRunner
{
    public static void main(String[] args)
    {
        Scanner in = new Scanner(System.in);
        DiceGame myGame = new DiceGame();
        boolean playAgain = true;
        while (playAgain)
        {
            System.out.println("Try to roll doubles!");
            boolean rolledDoubles = false;
            while (!rolledDoubles)
            {
                System.out.println("Rolling the dice...");
                myGame.takeTurn();
                int[] rolls = myGame.getResults();
                System.out.println("You rolled a " + rolls[0] + " and a " + rolls[1]);
                if (myGame.doubles())
                {
                    System.out.println("You win!");
                    rolledDoubles = true;
                }
                else
                {
                    System.out.println("Roll again!");
                }
            }
            System.out.println("Wanna play again (Y/n)? ");
            String input = in.nextLine();
            if (input.equals("") || input.substring(0,1).equalsIgnoreCase("Y"))
            {
                // nothing
            }
            else
                playAgain = false;
        }
        System.out.println("Okay, thanks for playing. Bye.");
    }
}

These three classes all work together just fine and represent a solid, object-oriented approach to writing this game.

10.8.4. The "Doubles" Game Revised

Having a cohesive class for the Die is fine, but if I were writing this game quickly, and just needed a quick "little class" that I could insert into my main program, I could do so by writing it as an "inner class."

Definition: Inner Class

An inner class is a small, trivial class that is declared inside another class, often because we need it as part of using an interface.

In the case of my game, I might choose to write the entire program in a single file, with the Die class and DiceGame classes declared as inner classes.

/**
 * The DiceGame class, which uses inner classes.
 */

import java.util.Scanner;
import java.util.Random;

public class DiceGame
{
    public static void main(String[] args)
    {
        
        // ===== INNER CLASS for the Die =======
        
        class Die
        {
            private int roll;
            private int sides;
            private Random generator;

            /**
             * Constructor for objects of class Die
             * @param numSides the number of sides on the die
             * @return the result of the roll
             */
            public Die(int numSides)
            {
                sides = numSides;
                generator = new Random();
                cast();
            }

            /**
             * cast rolls the die and returns a result
             * @return     the roll, a value between 1 and sides inclusive 
             */
            public void cast()
            {
                roll = generator.nextInt(sides) + 1;
            }

            /**
             * getRoll returns the result of the previous roll
             * @return   the roll
             */
            public int getRoll()
            {
                return roll;
            }
        }

        // ======= INNER CLASS for the Game ========
        
        class DieGame
        {
            private Die d1;
            private Die d2;
            private int[] results;
            
            /**
             * Constructor for objects of class DieGame
             */
            public DieGame()
            {
                d1 = new Die(6);
                d2 = new Die(6);
                results = new int[2];
            }

            /**
             * takeTurn method rolls the two dice
             */
            public void takeTurn()
            {
                d1.cast();
                d2.cast();
            }

            /**
             * getResults returns an Array with the two dice rolls
             * @return a two-element Array with the two dice rolls in it
             */
            public int[] getResults()
            {
                results[0] = d1.getRoll();
                results[1] = d2.getRoll();
                return results;
            }

            /**
             * doubles method returns true if the two dice have the same value
             * @return true if dice have same value, otherwise false
             */
            public boolean doubles()
            {
                if (results[0] == results[1])
                    return true;
                else
                    return false;
            }
        }

                
        // ======== Here's the main program! ========
        
        Scanner in = new Scanner(System.in);
        DieGame myGame = new DieGame();
        boolean playAgain = true;
        while (playAgain)
        {
            System.out.println("Try to roll doubles!");
            boolean rolledDoubles = false;
            while (!rolledDoubles)
            {
                System.out.println("Rolling the dice...");
                myGame.takeTurn();
                int[] rolls = myGame.getResults();
                System.out.println("You rolled a " + rolls[0] + " and a " + rolls[1]);
                if (myGame.doubles())
                {
                    System.out.println("You win!");
                    rolledDoubles = true;
                }
                else
                {
                    System.out.println("Roll again!");
                }
            }
            System.out.println("Wanna play again (Y/n)? ");
            String input = in.nextLine();
            if (input.equals("") || input.substring(0,1).equalsIgnoreCase("Y"))
            {
                // nothing
            }
            else
                playAgain = false;
        }
        System.out.println("Okay, thanks for playing. Bye.");
    }
}

In my effort to demonstrate how inner classes work I've pushed things a little too far—this program would actually be better written by splitting things up into the original three classes. But you get the point: we can create small blocks of code for trivial classes, if needed, and insert them into our larger class descriptions.