Implementations: Commentary

Steven Zeil

Last modified: Oct 5, 2016
Contents:

There’s a basic design pattern implicit in the Collections framework that might not be obvious.

In this manner, your functions will be flexible enough to work with any class that provides the data abstraction described by the interface, even classes that might get written in the future.

I’ll give some examples of this design principle later in this reading.

Several of the sections that follow will talk about concurrency and synchronization. In this class, we won’t be getting into Java’s support for parallel (multi-threaded) programs, so you can skip those discussions.

1 Set Implementations

2 List Implementations

Let’s suppose that we are writing a program to play a card game and are looking at writing a function to shuffle the deck. We might look first at something like this:

public class DeckOfCards {
   private Card[] cards;
   private java.util.Random rand = new java.util.Random();
   ⋮
   public void addToTop (Card c) {...}
   public void addToBottom (Card c) {...}
   public Card drawFromTop() {...}

   public void shuffle() {
	  for (int i = 1; i < cards.length; ++i) {
		 int k = rand.nextInt(i+1);
		 Card temp = cards[k];
		 cards[k] = cards[i];
		 cards[i] = temp;
	  }
   }
}; 

The shuffle function shown here will, indeed, arrange the cards into a random order. But looking at the other functions provided on the deck, we might wonder if the use of an array as storage is really appropriate. These functions seem to alter the number of cards left in the deck. That suggests that we need something more flexible. So we might look at something like:

public class DeckOfCards {
   private ArrayList<Card> cards;
   private java.util.Random rand = new java.util.Random();

   public DeckOfCards (ArrayList<Card> initialSetOfCards)
   {
      cards = (ArrayList<Card>)initialSetOfCards.clone();
   }

   public void addToTop (Card c) {cards.add(c);}
   public void addToBottom(Card c) {cards.add (0, c);}
   public Card drawFromTop() 
   {
      return cards.remove(cards.size()-1);
   }

   public void shuffle() {
	  for (int i = 1; i < cards.size(); ++i) {
		 int k = rand.nextInt(i+1);
		 Card temp = cards.get(k);
		 cards.set(k, cards.get(i));
		 cards.set (i, temp);
	  }
   }
}; 

Now, the use of ArrayList might seem to be an obvious choice here. The shuffle function relies on random access (literally) to locations in the list, and that would be far, far slower in a linked list than in an array-based structure.

That’s fine for games like poker where we always draw from the top of the deck and even for games like hearts or spades where we draw from the top of one deck and discard to the top of another (initially empty) deck. But what about a game like “war” where each player has a deck and draws cards from the top while adding cards to the bottom. Adding to the bottom will be terribly inefficient with an array-based structure. Over an entire game, that’s likely to outweigh the one-time cost of an inefficient shuffle.

So we might actually be better off with

public class DeckOfCards {
   private List<Card> cards;
   private java.util.Random rand = new java.util.Random();

   public DeckOfCards (List<Card> initialSetOfCards)
   {
      cards = (List<Card>)initialSetOfCards.clone();
   }

   public void addToTop (Card c) {cards.add(c);}
   public void addToBottom(Card c) {cards.add (0, c);}
   public Card drawFromTop() 
   {
      return cards.remove(cards.size()-1);
   }

   public void shuffle() {
	  for (int i = 1; i < cards.size(); ++i) {
		 int k = rand.nextInt(i+1);
		 Card temp = cards.get(k);
		 cards.set(k, cards.get(i));
		 cards.set (i, temp);
	  }
   }
}; 

which would allow the specific applications to select the underlying implementation of the list:

public class Poker {
   DeckOfCards deck;
   public Poker ()
   {
	  ArrayList<Card> startingDeck = ...;
      deck = new DeckOfCards (startingDeck);
   }
   ⋮
   } 


   public class War {
	  DeckOfCards deck;
	  public Poker ()
	  {
	     LinkedList<Card> startingDeck = ⋮;
	     deck = new DeckOfCards (startingDeck);
	  }
   ⋮
}

So what gets stored in the DeckOfCards could be either kind of list, because both ArrayList and LinkedList implement the List interface.

3 Map Implementations

Another practical example of the design pattern I have been discussing. I was once working on a project that required us to rapidly classify phrases read from a document. Possible classes included things like names of people, place names, company names, etc. A somewhat simplified sketch of the code would be:

public class Classifier {
   public Classification {
      public String name;
      public Map<String, Integer> phrases;
   };
   private Classification[] knownClasses;
   ⋮
   public List<String> classify (String phrase) {
	  int numWords = countWordsIn(phrase);
	  List<String> classes = new LinkedList<String>;
	  for (int i = 0; i < knownClasses; ++i) {
		 Classification possibility = knownClasses[i];
		 if (phrase.get(phrase) == numWords)
		    classes.add (possibility.name);
	  }
	  return classes;
   }

Now, it was tempting to assume that the Classification maps would all be HashMaps. After all, some of the collections of known phrases were quite large, and the speed of a hash table would come in handy.

But, as it happens, hard coding that in by using HashMap instead of Map would have been a mistake. Some of the collections turned out to be so large that they would not fit in memory, and had to be kept in a database. So, in the end, we created out own implementation of the Map interface:

public class HugeMap implements Map<String,Integer>
{
   private ... database;
   ⋮
   public Integer get (String key) {
	  DatabaseRecord dbrec = database.search(key);
	  if (dbrec != null) {
	     return new Integer (dbrec.field(2).toString());
	  } else
	     return new Integer(0);
	  }
   }
   ⋮
}

and some of our classifications used this while others did indeed use an in-memory HashMap.

4 Queue Implementations

5 Wrapper Implementations

6 Convenience Implementations

7 Summary of Implementations