1 Picking a Set Implementation in Code
2 Making BSTs be valid ISets
3 Casting
4 Summary

Implementing Sets, Leaving Choices

We’ve spent the last couple of classes talking about different data structures for sets (BSTs, AVL trees, and Heaps, as well as Lists). We didn’t write the code for them, largely because (a) doing so is just another exercise in using concepts you already know and (b) that exercise isn’t very interesting.

What is more interesting is thinking about how having a choice of data structures for sets might impact how we write code that uses sets. That’s the goal of this lecture.

We will assume for this lecture that we already have implementations of each of BSTs, AVL trees, and Heaps. We’d have interfaces IBST, IAVL and IHeap from each implementation (wait – why interfaces? Don’t we just need classes? Think about it and ask for clarification if you aren’t sure).

1 Picking a Set Implementation in Code

Remember the three bunch-of-strings programs that we started with two days ago (URL history, words from a game, party guest list). Which set data structure might make sense for each? URL history was about searching for elements in large sets, so an AVL tree is likely best; the word game needed to get elements by size, which suggests a heap; it wasn’t clear the data structure mattered for a party list, since the data was small.

How do we make this choice in code? Here’s what the web browser class would look like if we decided to use AVL trees:

  // The web browser code

  class webBrowser {

    IAVL visitedURLs;

  

    webBrowser(IAVL visitedURLs) {

      this.visitedURLs = visitedURLs;

    }

  

    // mark a new URL as visited

    IAVL addURL(String newURL) {

      return this.visitedURLs.addElt(newURL);

    }

  

    // determine whether URL has already been visited

    boolean haveVisited(String aURL) {

      return this.visitedURLs.hasElt(aURL);

    }

  }

And here’s the party guest class, using BSTs.

  // The party guests code

  class PartyGuests {

    IBST guests;

  

    PartyGuests(IBST guests) {

      this.guests = guests;

    }

  

    // record a new guest as coming

    IBST addGuest(String newGuestName) {

      return this.guests.addElt(newGuestName);

    }

  

    // determine whether a guest is coming

    boolean isComing(String name) {

      return this.guests.hasElt(name);

    }

  }

Now, imagine that down the road, you realize that BSTs weren’t the right choice for managing the party guests. Maybe you want to change to using AVL trees, or some other data structure (that we haven’t covered yet). To make the switch, you would have to go through the PartyGuests class and change all the references to IBST to refer to the new data structure. There aren’t many explicit mentions of IBST right now, but if we had the entire class (with all the required methods), there would be more.

Wouldn’t it be nice if we could change the data structure used for the set of guests without having to make any edits to the PartyGuests class? That would let us swap out data structures easily, testing which one works best with our data, and making an informed choice without having to redo a lot of earlier work. In general, writing code to let you change the data structure later is a very powerful programming technique.

This lecture shows you how to do that.

Let’s start by figuring out where the current PartyGuests class depends on having a BST. We clearly rely on BSTs anywhere we refer to IBST, or anywhere we use fields/variables of type IBST. Looking at the code, we see four places where we rely on IBST:

Now, recall that addElt and hasElt are methods we identified for sets. Since we used IBSTs to implement sets, we had them provide all of the sets methods. But any data structure for sets should provide these methods, so the last two places aren’t really specific to IBSTs. Similarly, addElt conceptually returns a set, so the third place isn’t really specific to IBSTs. That just leaves the guests field (and its value in the constructor), which we just want to be a set.

This analysis suggests that nothing in our current code actually depends on having an IBST (as opposed to any other set data structure). So perhaps we could simply write the class differently, to refer to ISet instead of IBST:

  // The party guests code

  class PartyGuests {

    ISet guests;

  

    PartyGuests(ISet guests) {

      this.guests = guests;

    }

  

    // record a new guest as coming

    ISet addGuest(String newGuestName) {

      return this.guests.addElt(newGuestName);

    }

  

    // determine whether a guest is coming

    boolean isComing(String name) {

      return this.guests.hasElt(name);

    }

  }

This code would compile fine, as long as we had an ISet interface such as the following:

  interface ISet {

    ISet addElt(String newElt) ; // adds item to the set

    ISet remElt(String newElt) ; // removes item from the set

    int size();                  // returns number of items in the set

    boolean hasElt(String elt);  // determines if item is in the set

  }

(this interface just captures in code the operations that we discussed informally on sets last week.)

That seemed easy. Let’s just check some examples to make sure everything still works.

So far, we’ve defined the PartyGuests class, but we don’t have any actual PartyGuests objects. Let’s start by making one.

  class Examples {

    Examples(){}

  

    PartyGuests PG = new PartyGuests();

  

    boolean testAddElt (Tester t) {

      PG.addGuest("Fred");

      PG.addGuest("Christina");

      return t.checkExpect(PG.guests.size(), 2);

    }

  

  }

Oops – this isn’t right! The constructor for PartyGuests needs to take an initial ISet as an input. This is the point when we decide which kind of set to use for a particular party. Let’s use a BST. If you want to work through this sequence on your own, here is an initial BST implementation.

  class Examples {

    Examples(){}

  

    PartyGuests PG = new PartyGuests(new MtBST());

  

    boolean testAddElt (Tester t) {

      PG.addGuest("Fred");

      PG.addGuest("Christina");

      return t.checkExpect(PG.guests.size(), 2);

    }

  }

If you had wanted to use an AVL tree instead, you would have passed new MtAVL() to the PartyGuests constructor, and so on.

If we try to compile this, Java complains that "MtBST cannot be converted to ISet". The PartyGuests class is expecting an ISet, but we passed a BST instead. We know that BSTs implement ISets, but we never captured that in our code. We need to tell Java that BSTs are also ISets.

2 Making BSTs be valid ISets

So let’s assert that each of the BST classes are also ISets by adding to their implements statements:

  class MtBST implements IBST, ISet {

    MtBST() {}

    ...

  }

Do we really need two interfaces here? Why not just ISet? Well, we may have some methods that are specific to BSTs. With heaps, for example, we had remMinElt, which is not a typical operator on sets, but does need to be in the IHeap interface.

When we compile this, Java complains that "MtBST does not provide the addElt method from ISet". The addElt in ISet returns an ISet but the addElt in BST returns an IBST. Didn’t we just tell Java that both MtBST and DataBST are ISets? Yes, but we didn’t say that IBSTs themselves are ISets. We need to promise Java that every class that implements IBST will also implement ISet.

This sounds like we want one interface to implement another, which sounds a bit odd. Java doesn’t support that, but it does support the idea that one interface extends another. We can write:

  interface IBST extends ISet {

    IBST addElt (String elt);

    int size ();

    boolean hasElt (String elt);

  }

When we make this change, the code compiles and runs just fine.

Okay, but something feels weird here. We have some methods with exactly the same headers in both the IBST and ISet interfaces. Do we really need both, or does extends mean that IBST can get them for free from ISet? We can indeed remove the ISet methods from the IBST interface. Comment out size and hasElt from IBST and make sure everything still works (it should).

What about addElt, however? In one interface it returns an IBST and in another it returns an ISet. We’ve said that IBSTs are ISets, so perhaps we can comment out addElt as well. Let’s try.

Now, Java gives us an error in the addElt method inside DataBST. The error says "ISet cannot be converted to IBST". What’s going on?

Look at the expression where the error comes up:

  return new DataBST (this.data,

                      this.left.addElt(elt),

                      this.right);

When we call this.left.addElt(elt), what type do we get back? The addElt statement in the ISet interface says that we get back an ISet. But the DataBST expects an IBST (that’s the type of the left field). Hence the error.

Wait – but the result of addElt _is_ an IBST! We know this, because the return type of addElt in the BST classes is IBST. So we’re really okay, right?

Yes and no. We’re okay in the sense that we are guaranteed that the ISet returned from this.left.addElt(elt) is an ISet. But Java doesn’t know this (that’s more analysis than Java does of types – it just follows what the interface says). Until we convince Java that everything is okay, we can’t run the program.

At this point, you have two options.

We’ll show you the latter, in case you prefer that route. The latter is the better way to handle this once you have some Java experience.

3 Casting

We need a way to tell Java that we are confident that the result of this.left.addElt{elt} will be an IBST. We do this by modifying the code as follows:

  return new DataBST (this.data,

                      (IBST) this.left.addElt(elt),

                      this.right);

All we did was put (IBST) (with the parenthesis) in front of this.left.addElt(elt). This tells Java "you can assume that the object from this.left.addElt(elt) will be an IBST when you run the program". Java will trust you when compiling, but it will halt your program (with an error) if you were wrong. So you should only use this technique when you are confident in your assertion. This technique is called casting.

In particular, you cannot use casting casting) to change the type of an object. For example you cannot do

  Boa b = (Boa) new Dillo(10, true)

Java checks that your attempted casts are plausible at compile time. In the case of the BST code, Java knows that this.left.addElt(elt) is an ISet and it knows that IBSTs are also ISets. Your assertion that this particular ISet is an IBST is plausible, so Java allows it at compile time.

This is the same technique we showed you (without explanation) for getting around the different types of contestants in homework 1.

Getting back to BSTs and ISets: if you also add a cast for the other else clause (when you addElt on the right), the code compiles and runs fine. And you avoided the duplication of methods between the IBST and ISet interfaces.

4 Summary

What should you be taking from this? There’s a big high-level design point here that is the key point of this lecture:

Using interfaces as types makes it easier to swap out different implementations of the same core operations

Whenever you are writing code for some kind of data whose details might change, use an interface for the type and create separate classes for the specific data structure or class that provides the details. If you do this, you can improve or change aspects of how your code works without editing (and thus breaking) previous code. This is particularly important when you work on team projects: if each member of the team programs to an interface, you can swap out code with more ease and fewer errors.

This is such an essential idea that we will return to it over and over again through this term. For now, it adds to our understanding of interfaces: interfaces let us change implementation decisions later, which is extremely useful in actual programming.

At a more detailed level, we showed you that you can have one interface promise to satisfy the requirements of another. Sometimes, you need to use casts to tell Java that you are confident that the types are okay. Both interface extension and casts are useful ideas to have in your pocket when writing OO programs (not just in Java).