On this page:
21.1 Introducing Array  Lists
21.2 Obtaining items from an Array  List
21.3 Manipulating items via indices:   moving two items
21.4 Transforming Array  Lists with map:   introducing for-each loops
21.4.1 Mapping via recursion
21.4.2 Using for-each loops
6.2.1

21 Lecture 21: ArrayLists

Working with Java’s built-in mutable lists

In the last lecture, we determined how to remove items from a mutable list, and saw that carefully considering all the behaviors, including the subtle special case of removing the first item from the list, led to revising our data definitions to make that case not so special. From there, we began designing an interface for our “ideal” generic mutable list.

As it turns out, such a construction is so ubiquitously useful that Java defines a class that implements almost precisely this interface. Let’s start working with this new class, and see what new challenges it brings.

21.1 Introducing ArrayLists

In the last lecture we described an IMutableList interface as follows:
interface IMutableList<T> {
// adds an item to the (front of) the list void addToFront(T t);
// adds an item to the end of the list void addToEnd(T t);
// removes an item from list (uses intensional equality) void remove(T t);
// removes the first item from the list that passes the predicate void remove(IPred<T> whichOne);
// gets the numbered item (starting at index 0) of the list T get(int index);
// sets (i.e. replaces) the numbered item (starting at index 0) with the given item void set(int index, T t);
// inserts the given item at the numbered position void insert(int index, T t);
// returns the length of the list int size();
}
This interface does not actually exist, in the sense that it is not predefined for us by name, and we will not actually define it or any classes that implement it explicitly. Instead, Java defines a class for us that provides essentially these (and other) methods anyway: the ArrayList<T> class.

(Note: to use ArrayLists in code, we have to include the following import statement at the top of each file that uses them:
import java.util.ArrayList;
We have seen these import statements before, though we have not remarked on them: they are essentially like Racket’s require, and include predefined libraries into our code. The naming convention for libraries reads from left to right, and descends from large, general categories to smaller libraries and down to individual classes. Here, we can see from the name that the designers of Java’s libraries deemed ArrayLists to be of great utility to all of java!)

Because Java defines this class for us, several consequences immediately follow. Unlike our IList<T> and its attendant ConsList<T> and MtList<T> classes, we do not control the implementation of ArrayList<T>. We cannot add methods to it; we cannot use dynamic dispatch to recur over its structure; we actually have no visibility into how the class is implemented at all. And we shouldn’t have to! It suffices to know that we can manipulate an ArrayList<T> using its methods, rather than knowing how those methods are implemented.

This is a powerful form of abstraction, known as representation independence: client code (i.e. users of ArrayList<T>) should neither know nor care about how libraries (such as ArrayList<T>) are implemented, but should simply interact with them through their interfaces. Until now, we have been both the author of the library code and the client code, so the distinction was not so crisp. In larger-scale programs, this distinction is a crucial part of software engineering.

Do Now!

Which class have we seen in the homework assignments that is most akin to the ArrayList<T> class? (Hint: which class essentially implements the interface above?)

If we cannot rely on knowledge of the implementation to guide our methods, how can we interact with a ArrayList<T>, and what might we want to do with one? Presumably, it should still be list-like, which means we ought to be able to get items from it, obtain its length, map functions over it, sort it into order, etc.

21.2 Obtaining items from an ArrayList

With ILists, we could implement a method getFirst that would return this.first from a ConsList class, and throw an exception from the MtList class. With ArrayLists, we simply use the get method, and access items by their index, meaning their position in the list (counting from 0):
ArrayList<String> someStrings = ...;
someStrings.get(0) // should be the first item of the list
Of course, if the list is in fact empty, then there is no first item available, and the get method will throw an IndexOutOfBoundsException signaling the error.

Indeed, get is more general than just getting the first item of a list; it can get any item of the list, assuming it is given a valid index:
class ExampleArrayLists {
void testGet(Tester t) {
ArrayList<String> someStrings = new ArrayList<String>();
t.checkException(new IndexOutOfBoundsException("Index: 0, Size: 0"),
someStrings, "get", 0);
someStrings.add("First string");
someStrings.add("Second string");
t.checkExpect(someStrings.get(0), "First string");
t.checkExpect(someStrings.get(1), "Second string");
t.checkException(new IndexOutOfBoundsException("Index: 3, Size: 2"),
someStrings, "get", 3);
}
}

This example also demonstrates adding items to the list: the new items are added to the end of the list, as evidenced by the indices of the middle two tests. If we want to insert an item at a given index, we can use another form of add that takes an index as its first parameter:
class ExampleArrayLists {
void testAdd(Tester t) {
ArrayList<String> someStrings = new ArrayList<String>();
someStrings.add("First string");
someStrings.add("Second string");
t.checkExpect(someStrings.get(0), "First string");
t.checkExpect(someStrings.get(1), "Second string");
 
// Insert this item at index 1, and move everything else back someStrings.add(1, "Squeezed in");
t.checkExpect(someStrings.get(0), "First string");
t.checkExpect(someStrings.get(1), "Squeezed in");
t.checkExpect(someStrings.get(2), "Second string");
}
}

21.3 Manipulating items via indices: moving two items

Adding items to the end of a list and getting items by index are easy enough, but what else can we accomplish? For example, how can we move items around in a list?

Suppose we had the following list, but mistakenly initialized its items in the wrong order:
// In ExampleArrayLists void testSwap(Tester t) {
ArrayList<String> someStrings = new ArrayList<String>();
someStrings.add("Second string");
someStrings.add("First string");
 
// Do something to fix the list's order  
t.checkExpect(someStrings.get(0), "First string");
t.checkExpect(someStrings.get(1), "Second string");
}

We need to exchange the two items somehow, by changing the items at each index. We could write code to do this every time we needed to, or we could define a helper method. But unlike with ILists, where we’d just add a new method to the interface, then implement it on ConsList and MtList, we do not have that luxury here. Instead, we must resort once again to defining a utility class, within which we define our new method:

class ArrayUtils {
// EFFECT: Exchanges the values at the given two indices in the given array <T> void swap(ArrayList<T> arr, int index1, int index2) {
??
}
}

Do Now!

Implement this swap method. What new method on ArrayLists will be needed?

The counterpart to get, which gets the item at a given index, is set, which sets the item at the given index to be the newly given value. So the clearest implementation of swap is:
// In ArrayUtils <T> void swap(ArrayList<T> arr, int index1, int index2) {
T oldValueAtIndex1 = arr.get(index1);
T oldValueAtIndex2 = arr.get(index2);
 
arr.set(index2, oldValueAtIndex1);
arr.set(index1, oldValueAtIndex2);
}

This code is perfectly correct. Still, it is worthwhile practice to see how to streamline this code a bit. First, notice that we don’t need both variables oldValueAtIndex1 and oldValueAtIndex2. When we invoke set with index2, notice that the value at index1 has not yet been modified. So we can eliminate one variable, and write
// In ArrayUtils <T> void swap(ArrayList<T> arr, int index1, int index2) {
T oldValueAtIndex2 = arr.get(index2);
 
arr.set(index2, arr.get(index1));
arr.set(index1, oldValueAtIndex2);
}

(We can actually eliminate the other variable, as well, thanks to a quirk of set that was added precisely to make this next step possible. The set method is defined to return a T value: specifically, it returns the old value at the index being modified. Accordingly, we can write the following version, which is quite terse and “clever”:
// In ArrayUtils <T> void swap(ArrayList<T> arr, int index1, int index2) {
arr.set(index2, arr.set(index1, arr.get(index2)));
}
Reading from the inside out, this code gets the value at index2, sets the value at index1 to be that value and then returns the old value at index1, which is then used to set the value at index2. Perfectly clear, no? No! There is absolutely no difference in the compiled output of any of these three versions of swap, so writing code this clever is simply asking for trouble. Just because we can write such code doesn’t mean we should.)

21.4 Transforming ArrayLists with map: introducing for-each loops
21.4.1 Mapping via recursion

Now that we have some practice with indices, let’s try implementing map: our goal is to produce a new ArrayList containing the results of applying a given function to every item in the original list:
// In ArrayUtils <T, U> ArrayList<U> map(ArrayList<T> arr, IFunc<T, U> func) {
???
}

Do Now!

Try implementing map. What helpers, if any, do you need to define?

We know we have to iterate through all the items in the list, and the only way we currently have to access items is via their indices. How can we implement this method? We don’t have ConsList and MtList against which to dynamically dispatch to methods. Instead we need to keep track of the current index, so that we know which item of the list to process. Additionally, note that adding an item to a list is a void method: if we want to preserve all the transformed items, we’ll need to pass the result list as a second accumulator parameter:
// In ArrayUtils // Computes the result of mapping the given function over the source list // from the given current index to the end of the list, and returns the // given destination list // EFFECT: modifies the destination list to contain the mapped results <T, U> ArrayList<U> mapHelp(ArrayList<T> source, IFunc<T, U> func,
int curIdx, ArrayList<U> dest) {
if (curIdx >= source.size()) {
return dest;
} else {
dest.add(func.apply(source.get(curIdx)));
return this.mapHelp(source, func, curIdx + 1, dest);
}
}
Our base case now occurs when our index has run past the last valid index of the source list, that is, when it is at least the size of the list (since indices start counting from 0). Our recursive case is straightforward: it gets the current item, transforms it, and adds it to the destination list, then recurs at the next index.

All that remains is to kick off the recursion in our original map method. We need to call the helper, starting at index 0, and passing in a new ArrayList of the appropriate element type for use as the accumulator:
// In ArrayUtils <T, U> ArrayList<U> map(ArrayList<T> arr, IFunc<T, U> func) {
ArrayList<U> result = new ArrayList<U>();
return this.mapHelp(arr, func, 0, result);
}

There is something unsatisfying about this implementation of map, though: all this messing about with indices seems unnecessarily distracting. Recall the very first description we gave of map in Fundies I: it returns the result of applying a function to each item of a list. Nowhere in that description did we have to mention indices. Can we improve our current Java code?

21.4.2 Using for-each loops

In fact, the ideas in this section are applicable to more than just ArrayLists. We’ll see in Lecture 25: Iterator and Iterable the details of how this construct works, and how we can make it work with our own datatypes.

The notion of “doing something to each item of a list” is so fundamental that Java actually defines syntax for making it more convenient: a for-each loop. A for-each loop does just what it sounds like: it executes some block of statements for each item in a list. We introduce the syntax of a for-each loop by rewriting map to use one:
// In ArrayUtils <T, U> ArrayList<U> map(ArrayList<T> arr, IFunc<T, U> func) {
ArrayList<U> result = new ArrayList<U>();
for (T t : arr) {
result.add(func.apply(t));
}
return result;
}
Read the code aloud as, “For each item t (of type T) in the list arr, apply func to t and add the answer to result.” In general, code that uses a for-each loop looks like the following sketch:
... myMethod(ArrayList<X> xs) {
...setup...
for (X x : xs) {
...body...
}
...use results...
}
Every loop consists of a header that declares the loop variable x (and its type) and specifies the list to be iterated over, and a body consisting of a block of statements to be executed every iteration of the loop, which happens once per item of the list. Since the body consists of statements, if we want any output from the loop, we must use side effects to do so. Therefore, before the loop begins we must set up any accumulators we want to use within the loop, so that once the loop completes we will have the accumulated answer available. In our map example, the setup step created the result ArrayList, and once the loop completed we merely needed to return it.

Note that when the ArrayList is empty, the body of the loop will execute zero times. We do not need to explicitly check for empty lists as a base case, but we do need to make sure that our setup will yield the correct answer even when the loop never runs. In the case of map, the setup creates an empty result ArrayList, which is exactly the correct answer when the input ArrayList was empty.

Exercise

Implement foldr and foldl for ArrayLists, first using recursion and then again using for-each loops.