Lecture notes for Tuesday, 7/29

Today we're going to write a stand-alone application that simulates a deck of ten cards that have the numbers 1-10 written on them. We'll represent the deck as a Java List, which will make it easier to do things like shuffling and removing and adding cards. But the List interface is a special kind of thing in Java: a generic.

Generics

The short definition is this: a generic type is a type* that is parametrized over another type. Of course, that's not a terribly useful definition if you don't already know what the word "parametrized" means. So here's another definition: a generic type is a bunch of types that are indexed by another type (called the parameter). All these types are essentially identical only one of them deals with Strings, another one deals with Applets, another one deals with inputStreams, another one deals with arrays of Floats

, and so-on. Because each one of these types is essentially the same (except for the parameter), we usually talk about them as if they're all one thing: a generic type. If you haven't seen them before, it's hard to imagine what sort of thing a generic type could be, but once you see a couple examples, it makes a lot more sense.

I haven't defined the word type yet, but I've been using it a lot. In Java, a type is just a general word for a class (such as Integer, String, JButton, or HttpServlet), or an interface (such as ActionListener, Stream, or List). In some contexts, type might also refer to a primitive type (such as int, char, float, or boolean), but not here.

The classic example of a generic type is the list. You can have a list of Long integers, or a list of Characters, or a list of PrintWriters. In Java, the generic class for lists is List. The specific case of the class for lists of Dates is List<Date>. Most of the generic types you'll have to deal with are very similar to lists. For example, another important generic class is the Set, which is just like a List, only the order doesn't matter, and you can't have the same element appear in a Set more than once. So the class for sets of InputStreams would be Set<InputStream> Another useful generic is the OrderedPair, which actually has two parameters, one defining the type of object in the first coordinate and one defining the type of object in the second coordinate. You could have ordered pairs of floating point numbers (OrderedPair<Float>), or you could have ordered pairs where the first coordinate is a string, and the second is a list of integers (OrderedPair<String, List<Integer>>).

Lists

There's a lot that can be said about generics, but today, we're just going to be working with one particular kind of generic type: the List. The first thing you need to know about Java Lists is that List is an interface, not a class. So the definition of List that appears in the java.util package declares a bunch of methods (such as add(), remove(), size(), and contains()), but doesn't actually say how the methods are supposed to be carried out. That job belongs to a class that implements the List interface. (See yesterday's notes for more details on interfaces in general.)

Fortunately for us, we aren't the ones who have to do the implementing. Someone has already made an implementation of List that does all the hard work for us. Actually, it's been implemented several times in different ways. The two most useful implementations are the classes ArrayList and LinkedList. The different implementations all compute the same thing (e.g., myList.add(42) always adds the number 42 to the end of myList regardless of whether myList is an ArrayList or a LinkedList), but they do it in different ways.* To add something to a list, we don't care what specific kind of List it is. In fact, when we're using Lists, we almost never care what kind of Lists they are, and the code we'll write will be exactly the same for any implementation of List.

Some ways might be faster for doing certain things and others might take up less memory in certain circumstances. It won't really matter which implementation we use in this class because our programs are all fairly small.

There is one important place where we have to pick an implementation, and that's when we create a List. Any method that takes a List as an argument will work if you give it an ArrayList or a LinkedList (or any other implementation of List), but if you want to make a list from scratch, you need to call the constructor, and saying something like new List<Integer>() won't work because List doesn't have an implemented constructor (it's only an interface; it doesn't have an implemented anything). But you can write new ArrayList<Integer>, and that object will work fine anywhere you want a List of Integers.

So let's do that. Let's create a card testing class that makes a list of integers, adds all the integers we want, prints them out, shuffles the deck, and then prints the cards out again.

import java.util.*;

public class CardTest {
  public static void main(String args[]) {

    List<Integer> deck = new ArrayList<Integer>();

    for(int i=1; i<=10; i++) {
      deck.add(i);
    }

    System.out.println("deck is: " + deck);
    System.out.println("Shuffling deck...");
    Collections.shuffle(deck);
    System.out.println("deck is now: " + deck);

  }
}

Now Lists are not the same thing as arrays. They are much more versatile than arrays, partly because of the many useful methods that use them. But one of the biggest advantages that Lists have over arrays is that a List does not have a fixed length. This means you can always add elements to or remove them from a list, which is not possible for arrays. This does mean that if you want to build a List that doesn't have a fixed length*, you'll have to add the members of the list one-by-one. Fortunately, we can put them all in a simple for loop, like we did here.

The helper class Arrays from the java.util package has a method called asList() that makes a List that is "backed" by a particular array. But this probably isn't as useful as you think it is. The kind of List you get is fixed in length, so you can't add or remove any elements. If you change any of the elements in the List, they'll be changed in the array and vice versa.

It was pointed out in class that the third line could have been simplified to List<Integer> deck = new ArrayList<>();, and that's true, at least since version 7 of Java. Since version 7, there are places where you can leave out a type and Java will look at the rest of the line of code and figure out what type it has to be.

The List interface is a subinterface of the interface Collection, which includes several different subinterfaces that are all based around a collection of things. Some other important kinds of Collection other than List include Set, Queue (which is a list that's designed to be accessed on a first-in/first-out basis), and Deque (which is kind of like a Queue that's meant to be accessed at both end).

There are a lot of methods that are designed to work with any kind of Collection, and many of them are part of the helper class Collections from the java.util package. You can see the shuffle() method in use here. Notice how it actually changes the original List.

You need to be a little careful with Lists. Many of the methods and operations that work with them make changes to the original List even though similar functions in other programming languages will return a new list with the changes made, instead of actually changing the original object. Here's an example. Before you actually run this one, think about what you expect to happen.

import java.util.*;

public class CardTest {
  public static void main(String args[]) {

    List<Integer> deck = new ArrayList<Integer>();

    for(int i=1; i<=10; i++) {
      deck.add(i);
    }
    
    List<Integer> deck2 = deck;

    System.out.println("deck is: " + deck);
    System.out.println("deck2 is: " + deck2);
    System.out.println("Shuffling deck...");
    Collections.shuffle(deck);
    System.out.println("deck is now: " + deck);
    System.out.println("deck2 is now: " + deck2);

  }
}

Here's what the output of this program looks like:

deck is: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
deck2 is: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Shuffling deck...
deck is now: [3, 4, 10, 9, 2, 7, 6, 8, 1, 5]
deck2 is now: [3, 4, 10, 9, 2, 7, 6, 8, 1, 5]

Make sure you understand why the output came out the way it did. When you use = to assign one List variable to another, you're saying that the two variables (deck and deck2, in this case) refer to the exact same object. They are identical. This means that when you make a change to one of them, you make the same change to the other. This is always going on when you use the assignment operator =, but most of the time we don't have to think about it because most of the classes we use can't be changed at all (the int 5 is always 5). If the variable i refers to the int 7, a command like i++ doesn't change the int 7. It just changes which int i refers to.

But suppose you wanted to make a copy of deck and not just another name for the same object. As usual, there is more than one way to do this. In class, I tried to work out the details of the copy() method and couldn't quite figure it out. It turns out there's an issue not with the length of the list, but with how much memory it takes up. There are other issues with copy() as well, and I think it's better to use the method we finally went with, which was to use the ArrayList constructor that takes another List as an argument. Like so:

import java.util.*;

public class CardTest {
  public static void main(String args[]) {

    List<Integer> deck = new ArrayList<Integer>();

    for(int i=1; i<=10; i++) {
      deck.add(i);
    }
    
    List<Integer> deck2 = deck;
    List<Integer> deck3 = new ArrayList<Integer>(deck);

    System.out.println("deck is: " + deck);
    System.out.println("deck2 is: " + deck2);
    System.out.println("deck3 is: " + deck3);
    System.out.println("Shuffling deck...");
    Collections.shuffle(deck);
    System.out.println("deck is now: " + deck);
    System.out.println("deck2 is now: " + deck2);
    System.out.println("deck3 is now: " + deck3);

  }
}

This has the desired behavior. The list deck3 starts out as a copy of deck, but it doesn't change when we change the original deck.

Iterators

One of the nice features of using Collections is that there's a special kind of generic class called an Iterator that gives us some powerful tools for looping through all the elements of a Collection. An iterator is an object that's attached to a particular collection. It comes with a sort of pointer that can be moved from one element in the collection to another. It keeps moving through the collection until there are no more elements.

There are three important iterator methods: next(), hasNext(), and remove().The hasNext() method is used to check to see if there are anymore elements in the collection. If the iterator has moved beyond the last element, then hasNext() is false. It's a good idea to use hasNext() to check to see if we've finished the list before calling next(). The next() method does two different things: first, it returns the element that the iterator is currently pointing at, and second, it moves the iterator to the next element in the collection. If we've just looked at an element by calling next(), we might want to decide to remove that element from the list, and we can do that using the remove() method, which removes the element that the iterator was pointing at most recently (even though the iterator has moved on to the next element now).

It's kind of hard to think about iterators until you see an example at work. So we'll use an iterator to run through all the numbers in deck, and we'll remove all of the ones that are smaller than 4. We can do that with a for loop. The initialization step consists of creating the iterator object: Iterator<Integer> it = deck.iterator(). After running each loop, we use the hasNext() method to see if there are any more elements in the collection: it.hasNext(). We'll leave the last part blank because when we call it.next() in the body of the loop, it will automatically move the iterator forward to the next element. Here's what the loop looks like:

for(Iterator<Integer> it = deck.iterator(); it.hasNext() ;  ) {
  if(it.next() < 4) {
    it.remove();
  }
}

The question came up in class as to how we would go about putting all the removed numbers into their own list, and that's a really good question. It sometimes involves doing two different things with the value we get from it.next(): we check to see if it's smaller than four, and we also add it to the new list. But we shouldn't call next() twice in the same loop because that would move the iterator twice, and we don't want that. So instead, we call next() once and store that value in a temporary variable, so we can use it as many times as we need.


List<Integer> removedCards = new ArrayList<Integer>();
int currentCard;

for(Iterator<Integer> it = deck.iterator(); it.hasNext() ;  ) {
  currentCard = it.next();
  if(currentCard < 4) {
    it.remove();
    removedCards.add(curentCard);
  }
}