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.
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?