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.
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 String
s, another one deals with Applet
s, another one deals with inputStream
s, another one deals with arrays of Float
s
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 Character
s, or a list of PrintWriter
s. In Java, the generic class for lists is List
. The specific case of the class for lists of Date
s 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 InputStream
s 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>>
).
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 List
s 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 List
s, we almost never care what kind of List
s 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 Integer
s.
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 List
s 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 List
s 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 List
s. 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
.
One of the nice features of using Collection
s 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);
}
}