1 Making IBST be an ISet
1.1 Have BSTs Implement Multiple Interfaces
1.2 Extending Interfaces
1.3 But Aren’t We Repeating Method Headers?
1.4 Casting: Giving Java Guarantees on Types

Connecting BSTs and ISets

Kathi Fisler

Remember where this sequence of lectures started: we wanted to implement sets using BSTs. We have written an implementation of BSTs, but we need BSTs to be valid ISets to satisfy our original motivations. Let’s consider a concrete example in which we use BSTs as classes for ISet. Here is a class for a lottery ticket containing three numbers. The constructor takes an initial (empty) set, then adds three fixed numbers to it (not an interesting lottery ticket system—a real one would generate random numbers—but ignore that issue for now):

  class LotteryTicket {

    ISet numbers;

  

    LotteryTicket(ISet initSet) {

      this.numbers = initSet.addElt(5);

      this.numbers = this.numbers.addElt(23);

      this.numbers = this.numbers.addElt(6);

    }

  }

Our decision to use a BST for the set implementation would get made when we create a LotteryTicket in another application. Here, we’ll do that in the Examples class:

  class Examples {

    Examples(){}

  

    LotteryTicket LT1 = new LotteryTicket(new MtBST());

  }

If you put this file in the same directory as our BST implementation and try to compile, Java will give the following error message (on the line defining LT1):

The constructor LotteryTicket(MtBST) is undefined

This message does NOT mean that the LotteryTicket class lacks a constructor. It means that no constructor in LotteryTicket takes an MtBST as input. The LotteryTicket constructor takes an ISet as input, but nowhere have we said that an MtBST is an ISet, so Java complains.

Our first task, then, is to get Java to recognize IBSTs as ISets.

1 Making IBST be an ISet

Recall our ISet interface:

  interface Iset {

    Iset addElt (int elt);

    Iset remElt (int elt);

    int size ();

    boolean hasElt (int elt);

  }

At quick glance, it might look like IBSTs give everything that the ISet interface wants: all the methods have the same names and input types. They don’t have the same output types though, and even if they did, Java wouldn’t view an IBST as an ISet. Instead, we have to find a way to tell Java explicitly that our BSTs are also ISets.

1.1 Have BSTs Implement Multiple Interfaces

We’ve discussed that Java lets one class implement multiple interfaces. What if we had MTBST and DataBST implement ISet as well as IBST? In other words:

  class MtBST implements IBST, ISet {

    MtBST() {}

    ...

  }

  

  class DataBST implements IBST, ISet {

    int data;

    ...

  }

Java produces the following error on public IBST addElt (int elt) ... in the MtBST class:

Error: The return type is incompatible with ISet.addElt(int)

In MtBST returns an IBST, but to implement ISet, addElt must return an ISet. We told Java that each of MtBST and DataBST are ISets, but we didn’t tell Java that IBST is also an ISet. Hence the error.

1.2 Extending Interfaces

The problem of relating interfaces comes up frequently in Java. To handle it, Java allows interfaces to extend one another. We add the phrase extends ISet to the IBST interface as shown below:

  interface IBST extends ISet {

    IBST addElt (int elt);

    IBST remElt (int elt);

    int size ();

    boolean hasElt (int elt);

    int largestElt();

    IBST remParent(IBST sibling);

    IBST mergeToRemoveParent(IBST sibling);

  }

Just as a class can extend at most one other class, an interface can extend at most one interface (but a class can still implement arbitrarily many interfaces). With this extension, it is as if all of the method headers in ISet have been copied into IBST. The code then compiles and runs fine.

Once we have MtBST and DataBST implementing IBST, which extends ISet, there is no need to have the Iset in the implements clauses for each of MtBST and DataBST separately. We can restore these to their original implements clauses:

  class MtBST implements IBST {

    MtBST() {}

    ...

  }

  

  class DataBST implements IBST {

    int data;

    ...

  }

1.3 But Aren’t We Repeating Method Headers?

Having addElt and the other ISet method names repeated in both the ISet and IBST interfaces might feel a bit redundant. Technically, it is not redundant, since the return types are different in each case. But still, it feels like we should be able to leverage Java’s knowledge that IBSTs are ISets to eliminate the almost-replication.

As an experiment, make sure your IBST implementation extends ISet, then comment out the addElt method header in IBST. What happens?

At the first return statement inside addElt in the DataBST class, Java complains that

Error: The constructor DataBST(int, ISet, IBST) is undefined

Now that addElt comes only from ISet, it returns an ISet. So in the call to new DataBST, the second argument is an ISet, whereas the constructor for DataBST expects an IBST. Although every IBST is an ISet, not every ISet is an IBST, so Java is reasonable in complaining about this.

Given that the method headers really are different, it is fine to just repeat them in this case. But if you want to understand more about how Java works, and the almost-repeated headers bother you, the next section shows what you can do about it.

1.4 Casting: Giving Java Guarantees on Types

Java is complaining that addElt is returning an ISet, and DataBST expects a IBST. The problem lies in the argument this.left.addElt(elt). As the programmers of the BST classes, we know that starting from a BST (as this.left is) and calling addElt returns an IBST; Java, however, can’t figure that out. However, we can promise Java that the result of addElt in this particular case will be an IBST, and ask it to trust us.

Such promises about types are called casts in Java. We cast the result of addElt here to an IBST by writing the promised type name in parentheses before the expression, as follows:

(IBST) this.left.addElt(elt)

The cast tells Java to treat the expression as an IBST. If we are right and the result of addElt at this point is always an IBST, the code will run fine. If we misunderstood our program and made this promise incorrectly, some aspect of our code might break while running later. When you cast expressions, Java will check that the type you promised is valid when the program runs. If Java finds that you lied, it will stop your program. Thus, casts are not to be used lightly. You should only use a cast to change a type if you are sure that the object you are casting really does have that type.

If you are working along in these notes with the code open, add the cast to the line on which Java reported the error, recompile, and you’ll notice that the number of errors decreased by 1. Similarly, we need to cast the results anywhere we call addElt and Java is expecting to see an IBST (including in the Examples class).

Deleting remElt from the IBST interface similarly induces the need for casts, but you apply them in similar fashion as we did for addElt. Here is the bst code using casts, with the IBST interface slimmed down accordingly.

Once you have hierarchies of classes and interfaces, casts are sometimes necessary to make code compile. They slightly hurt the performance of running programs (since the types are checked at run-time rather than compile time). As a Java programmer, you should be careful to only use a cast when you are confident that the objects you are casting can actually be of the indicated type.