Lecture 07: Using Lists
Announcements
“Collaboration forbidden” means that you can’t submit code that a third party would conclude was written by someone else. The course policy says things along the lines of “don’t write things down when talking with other students; don’t look at other students’ code.” This helps prevent your code from looking too much like others’. But you are allowed to talk to one another!
A note about grading: I’m generally more forgiving about things you do under time pressure or where you cannot easily check your work (in class exercises, quizzes, exam), and less so otherwise (programming assignments, homeworks). Don’t submit code on the homeworks that “looks mostly right” and expect to get more than a small amount of partial credit. Fire up Eclipse and test it!
Garrett has added new office hours, Monday at 3:45, posted on the course site.
Note that if you modify the signatures of public methods in code we give you, it may still compile on your end, but it won’t on Gradescope. That’s because you’ve in essence removed the methods we’ve given you (different signature = different method as far as Java is concerned.) It still compiles for you because Eclipse (may) auto-correct the tests, but we only look at your source, and use our tests, when grading. You can add methods, but you can’t change signatures (e.g., add or remove parameters to the methods, or alter the return type, etc.).
Generics
Methods take parameters: rather than hardcoding all data and values into a method, we can make some parts of the data variable and parameterizable so that the method can be reused with different data and values. This make a lot of sense: Let’s say we want to write a method to compute the average of an array of numbers. We don’t want to have to write a different method for each possible array of numbets, since (1) there’s an infinite number of them! and (2) the operation of taking the average is mechanically the same each time. That is, there’s a generalized algorithm we write, once, and then can use many times.
The insight behind “generics” is that we can do the same thing with types – we can parameterize classes (and methods) with a type, too, and use it on different types of things. Our crappy StringListInterface
, for example, while it lets us hold any list of String
s we want, was still limited to String
data. But it turns out that instead of writing:
public interface StringListInterface {
public void add(String s);
public void add(String s, int i);
public String remove(int i);
public String get(int i);
public int size();
}
you can parameterize the interface on a type using angle brackets:
public interface ListInterface<E> {
public void add(E e);
public void add(E e, int i);
public E remove(int i);
public E get(int i);
public int size();
}
The ListInterface
now sports a generic type name in angle brackets. We’ve defined a family of possible types here; note that each method that used to operate on strings now operates on this mysterious E
.
We can also type parameterize a class:
public class Cell<E> {
private final E contents;
private Cell<E> next;
// more
}
…and the two together let us write generic code, that operates on generic types, based upon the type parameter.
Type parameters
The E
is a type parameter – it says that the programmer who declares a variable of type ListInterface
must also choose a particular type that the declared ListInterface
will handle. ListInterface
s of different type parameters are of different types. For example, you cannot assign one to another unless they have the same type parameter, any more than you can assigne a boolean
to a String
:
boolean x = "banana"; // not allowed
ListInterface<Dog> x;
ListInterface<Cat> y;
... // some code ...
x = y; // not allowed
Type parameters are usually written as a single uppercase letter, and often that letter is an abbreviation. E
stands for Element of a collection; we’ll also see K
ey and V
alue later in the course.
Type parameters, when instantiated (that is, when a generic is declared), must be a non-primitive type. But, Java does something called auto-boxing, so you can generally mix primitives and non-primitives freely using the associated wrapper types, like Integer
. (Integer
and friends also have many useful static methods.)
The final fact for today about type parameters: Usually we think of them as being declared on classes (and indeed, that’s usually where they are declared). But if you write a particular method that would benefit from type parameterization, you can do so:
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
Note the type paramters come before the return type.
Why generics matter
The reasons listed above (generic re-usable code)! But also, in Java 5, the entire Collections library was re-written to use generics. Before then, all container types (List
, etc.) only held things of type Object
, and you, the programmer, had to laboriously cast them each time you used them:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
Not only was this a pain, if you made a mistake:
List list = new ArrayList();
list.add("hello");
...
Dog s = (Dog) list.get(0); // ClassCastException!
you’d find out at run-time, not at compile-time. And while I know you hate compiler errors right now, you’ll learn to love them when writing big programs — every error that the compiler catches is one you can fix at your leisure, while run-time errors are erratic, not always reproducible, and generally result in a (much bigger) headache for you.
Using List
s
For the rest of class today we’re going to practice using the generic List
to solve problems. You’re welcome to use your computer if you have it, and you’re welcome to work together. I’d like you to write down your (final) answers and pass them in at the end of class as your in-class exercise. Feel free to put multiple people’s names and SINs on the paper.
Define a class IceCreamShop
. While it might have many instance variables, focus on two: employees and flavors. Each of these is further defined in a class Employee
and Flavor
which you can assume already exist. Write out the class and constructor for IceCreamShop
that defines empty lists for each of these instance variables. You can assume the relevant import
statements are present.
class IceCreamShop {
List<Employee> employees;
List<Flavor> flavors;
public IceCreamShop() {
employees = new ArrayList<Employee>();
flavors = new ArrayList<Flavor>()
}
}
Write a method of IceCreamShop
with the signature void hire(Employee e)
to add an employee from payroll.
void hire(Employee e) {
employees.add(e);
}
Did you check to make sure they weren’t already on payroll?
void hire(Employee e) {
if (employees.contains(e)) {
// do what?
}
employees.add(e);
}
This is an example of an underspecified problem. When I underspecify things, you can ignore the problem. Or at least, the underspecified bits. :) But when you are asked to do something “for real,” you should at least consider cases like this. Are they worth dealing with? Did your instructor overlook something? Did your boss? Your customer? Maybe you can ignore them and maybe not; some of that is a judgment call.
How about if you want to hire a whole bunch of new employees? Ignore the re-hire problem. void hireAll(List<Employee> l)
Did you write a for
loop? Or did you check the List
interface for something better?
void hireAll(List<Employee> l) {
employees.addAll(l);
}
Suppose you want to add items to a list (say, a storeNumbers
attribute of type List<Integer>
) and you want to keep it sorted. How would you write the public void addStore(int newNumber)
method?
Note we don’t need to pass in an Integer
object; Java autoboxing handles it for us.
Option 1: Insert the number in order:
public void addStore(int newNumber) {
if (storeNumbers.isEmpty()) {
storeNumbers.add(newNumber);
return;
}
for (int i = 0; i < storeNumbers.size(); i++) {
if (storeNumbers.get(i).compareTo(newNumber) >= 0) {
storeNumbers.add(i, newNumber);
return;
}
}
storeNumbers.add(newNumber);
}
You’ll notice I didn’t use an iterator above. Why not? Let’s see
public void addStore(int newNumber) {
if (storeNumbers.isEmpty()) {
storeNumbers.add(newNumber);
return;
}
int i = 0;
for (Integer storeNumber: storeNumbers) {
if (storeNumber.compareTo(newNumber) >= 0) {
storeNumbers.add(i, newNumber);
return;
}
i++;
}
storeNumbers.add(newNumber);
}
This will throw a ConcurrentModificationException
. WTF? It turns out that some (most) implementations of collections are very particular about allowing you to modify them while you are creating an iterator, then modifying the collection, then trying to iterate, is generally not allowed. See, for example, the ArrayList
docs: http://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html and note that “…if the list is structurally modified at any time after the iterator is created, in any way except through the iterator’s own remove
or add
methods, the iterator will throw a ConcurrentModificationException
.”
Option 2: Append the number then sort the list!
public void addStore(int newNumber) {
storeNumbers.add(newNumber);
storeNumbers.sort(null);
}
What’s up with the sort(null)
call? Look at the API: http://docs.oracle.com/javase/8/docs/api/java/util/List.html#sort-java.util.Comparator-
The Comparator
type parameter is an interface that describes how to compare two arbitrary objects (see also its simpler predecessor, the Comparable
interface). You might implement it if you wanted to do something odd with the sort, for example, put all odd numbers before all even numbers (which maybe sounds nonsensical, but think about mail delivery up and down each side of the street).
But you don’t actually have to implement its abstract methods to use it here. Reading the documentation:
If the specified comparator is null then all elements in this list must implement the
Comparable
interface and the elements’ natural ordering should be used.
Do Integer
s implement Comparable
? Let’s check: http://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html
Yup. In general, you should start doing your best to read and follow links in the Java API if you see things in code you don’t understand. You might not understand everything you read, but only by trying are you going to learn, and at some point you can’t expect your instructors to spoon-feed you everything (though I will definitely cover the highlights).