Week3

Interface Types

In last weeks material, an interface was introduce to be able to compare the outcomes of certain types of casino games based on some score/value. The interface looked like this:

public interface GameValue {
    int getValue();
}

We could then implement different classes, for example Dice and PokerHand, that implement this interface.

public class Dice implements GameValue {
    private int d1;
    private int d2;

    public Dice(int die1, int die2) {
        d1 = Math.max(die1, die2);
        d2 = Math.min(die1, die2);
    }

    public int getFirst() {
        return d1;
    }

    public int getSecond() {
        return d2;
    }

    @Override
    public int getValue() {
        if (d1 ==2 && d2 == 1) {
            return 1000;
        }
        if (d1 == d2) {
            return d1*100;
        }
        return 10*d1 + d2;
   }
}

A rough sketch of a what a PokerHand could look like is this:

public class PokerHand implements GameValue {

    private List<String> cards;

    public PokerHand(String card1, String card2, String card3,
                        String card4, String card5) {
        this.cards = List.of(card1, card2, card3, card4, card5);
    }

    @Override
    public int getValue() {
        int score = 0;
        // Some complicated code that uses the cards to compute a score
        // ...
        return score;
    }
}

The advantage of using an interface, is that objects of both classes can now be used as the same GameValue type.

Interface Types in Variable Declarations

When a variable is declared, it must always be clear what the type of the variable is, usually by writing the type in front of the variable name. There are two kinds of type, the primitive-type variables (int, double, ...) and reference-type variables (all objects). We've so far used an object's class as the type of a reference-type variable. An object's type can be other than its class. For example, the type of the Dice class that implements the GameValue interface is both Dice and GameValue.

Dice throwDice = new Dice(3, 4);
GameValue throwDice2 = new Dice(2,5);
GameValue throwDice3 = throwDice;

Interface Types as Method Parameters

The true benefits of interfaces are reaped when they are used as the type of parameter provided to a method. Since an interface can be used as a variable's type, it can also be used as a parameter type in method calls. For example, the printValue method of the class below accepts an argument of type GameValue.

public class Main {
    public static void printValue(GameValue gameValue) {
        System.out.println(gameValue.getValue());
    }

    public static void main(String [] args) {
        GameValue gameValue = new Dice(3, 4);
        printValue(gameValue);
        gameValue = new PokerHand("A♥", "K♥", "Q♥", "J♥", "T♥");
        printValue(gameValue);
    }
}

The benefit of declaring the printValue method such that it accepts a GameValue argument lies in the fact that we can pass objects of any class that implements the GameValue interface as arguments. When we call the method with any object instantiated from a class that implements the GameValue interface, the method would function as desired. This avoids having to create many different methods for different types of casino games.

Interface Types as Return Types

It is also possible to create a method that returns something of an interface type

public static GameValue getHighestValue(GameValue a, GameValue b) {
    if (a.getValue() > b.getValue()) {
        return a;
    }
    return b;
}

Type Conversion

Note that since the Dice class implements the GameValue interface, it is guaranteed that every Dice can be used as a GameValue. However, not all classes that implement the GameValue interface are guaranted to be of type Dice. As a consequence, you can assign an object created from the Dice class to a GameValue-type variable, but it does not work the other way without an explicit type conversion. In case you still want to do this, you have to use an explicit cast:

public void printDiceValue(GameValue dice) {
    // Not sure whether the next step will work
    Dice myDice = (Dice) dice;
    // Extract some information specific to the Dice class
    int firstThrow = myDice.getFirst();
    int secondThrow = myDice.getSecond();
    System.out.print(firstThrow+", " secondThrow);
    System.out.println(" : "+myDice.getValue());
}

The above method attempts to convert a given reference of type GameValue into a reference of type Dice. However, if the method is called with a GameValue reference that actually points to a PokerHand object, you will get a ClassCastException when you run the code, indicating that this type cast has failed. In general, we should avoid this type of type conversion if this is possible. For example, changing the method header to:

public void printDiceValue(Dice dice) { ... }

In this case, we know for sure that the input is actually of type Dice, and therefore a cast is not necessary.

Safe Type Conversions and instanceof

As explained the following code performs an unsafe type conversion:

public void printDiceValue(GameValue dice) {
    // Not sure whether the next step will work
    Dice myDice = (Dice) dice;
    // ... more code here
}

To make sure we perform the type conversion only when this is safe, we can use the instanceof keyword to check whether it is safe to cast a certain reference to a particular type. It is a binary operator which on the left hand side has an expression, and on the right hand side has a type, like so: expr instanceof Type. The operator will return true in case the reference in expr can be converted to the given Type and otherwise returns false. We can adjust the code above as follows to use this mechanism:

public void printDiceValue(GameValue dice) {
    // Not sure whether the next step will work
    if (dice instanceof Dice) {
        // This makes sure we only perform the type conversion
        // when this is safe
        Dice myDice = (Dice) dice;
        // ... more code here
    }
    else {
        // throw an Exception or do something else
    }
}

Typically, it is preferred to use inheritance and method overloading to prevent having to do these kind of type checks, but in some cases it can be very useful.

Exercise

Test your knowledge

In this quiz, you can test your knowledge on the subjects covered in this chapter.

Why do we use interfaces in Java?

Why would one use the interface type as a method parameter or return type?

What can you do to avoid making mistakes while converting between types?

You have reached the end of this section! Continue to the next section: