On this page:
23.1 Warmup:   build-list
23.2 Loops, Aliasing and Variables
23.3 Adding and removing items from lists
8.10

Lecture 23: For-each loops and Counted-for loops

Comparing and contrasting for-each loops and counted-for loops; a note about aliasing, parameters, and local variables

23.1 Warmup: build-list

Suppose we want to implement a method akin to Racket’s build-list function: it should take a number \(n\) and a function object, and produce an ArrayList that results from invoking that function object on all numbers from \(0\) to \(n-1\).

Do Now!

Do it.

There are several questions we need to ask: first, what should the signature of our method be? We don’t know what kind of ArrayList we need to build, nor do we care: it should contain whatever data the function object gives us. So our initial signature might be
// In ArrayUtils <U> ArrayList<U> buildList(int n, IFunc<Integer, U> func) {
...
}

Second, what kind of loop should we use? We want to produce an ArrayList, but we don’t yet have one available, so using a for-each loop seems to be infeasible. Accordingly, our method should use a counted-for loop, counting from \(0\) up to but not including \(n\):
// In ArrayUtils <U> ArrayList<U> buildList(int n, IFunc<Integer, U> func) {
for (int i = 0; i < n; i = i + 1) {
...
}
}

Finally, we need to deal with the result list. Since the function object will merely return a value of type T, we need to construct and manage the list ourselves. Each time we invoke the function object, it will give us a value to add to the end of our list; fortunately, this is exactly what the add method on ArrayLists does. With that, our full method implementation is:
// In ArrayUtils <U> ArrayList<U> buildList(int n, IFunc<Integer, U> func) {
ArrayList<U> result = new ArrayList<U>();
for (int i = 0; i < n; i = i + 1) {
result.add(func.apply(i));
}
return result;
}

(Note how similar this code is to our implementation of map:
// 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;
}

Conceptually, this makes sense: in buildList we’re “mapping” across the numbers \(0\) through \(n-1\), so it stands to reason that both buildList and map will iteratively produce a list of results by applying the supplied function object. The differences also are reasonable: in buildList we’re mapping across numbers only, while in map we could be mapping across values of any type. Moreover, in map we do not care how many items are in the supplied list; in buildList we very much care about counting off exactly \(n\) items.)

23.2 Loops, Aliasing and Variables

Suppose we had an ArrayList<Book>, and we wanted to modify the array-list in various ways. Some modifications turn out to be easy; others turn out to be surprisingly hard.

Converting a string to title-case is only moderately trickier, but is a distraction for now.

First, let’s consider modifying each book in the array-list so that its title is entirely in uppercase letters. We’ll want to use the toUpperCase() method on Strings to accomplish this.

Do Now!

Design a method in ArrayUtils to capitalize all titles. Which loop form should we use?

Since we’re effectively mapping a method across all the books in the array-list, we’ll want to use a for-each loop. The skeleton of our method will therefore look something like this:
// In ArrayUtils // Capitalizes the titles of all books in the given ArrayList ??? capitalizeTitles(ArrayList<Book> books) {
for (Book b : books) {
... b.title.toUpperCase() ...
}
}
At this point, we have several choices:
  • We could attempt to remove the current book b and insert a new Book (with the capitalized title) in the list of books.

  • We could attempt to change b to be a new Book (with the capitalized title).

  • We could attempt to modify the current book b’s title.

The first approach is tricky. Removing and adding items to a list while in a loop is dangerous (see Adding and removing items from lists below), so let’s try the second option. We would therefore try writing
// In ArrayUtils // EFFECT: Modifies all the books in the given ArrayList, to capitalize their titles void capitalizeTitles_bad(ArrayList<Book> books) {
for (Book b : books) {
b = new Book(b.title.toUpperCase(), b.author);
}
}

Do Now!

What goes wrong with this approach?

Exercise

What goes wrong if we tried removing the old book and adding the new one, as in the first option?

Let’s try writing some tests, to see whether our approach has worked:
class ExamplesCapitalize {
void testCapitalizeTitles_bad(Tester t) {
// Initialize data: Author mf = new Author("Matthias Felleisen", 1953);
Book htdp = new Book("How to Design Programs", mf);
ArrayList<Book> books = new ArrayList<Book>();
books.add(htdp);
// Modify it (new ArrayUtils()).capitalizeTitles_bad(books);
// Test for changes t.checkExpect(books.get(0).title, "HOW TO DESIGN PROGRAMS");
}
}
Sadly this test fails. Why? As the for-each loop executes, b is bound as an alias to each item of the list in turn. That means that any modifications to the object referred to by b will persist beyond the end of the loop, but any modifications to b itself will not. When we wrote b = new Book(...), we modified b and bound it to a new object — but that had no effect whatsoever on the books ArrayList. All we accomplished was to break the aliasing between b and an element of books.

Fortunately, however, this suggests that the third approach above will actually work! In particular, we might try the following:
// In ArrayUtils // EFFECT: Modifies all the books in the given ArrayList, to capitalize their titles void capitalizeTitles_good(ArrayList<Book> books) {
for (Book b : books) {
b.capitalizeTitle();
}
}
 
// In Book // EFFECT: Capitalizes this book's title void capitalizeTitle() {
this.title = this.title.toUpperCase();
}

Do Now!

Confirm that this approach works, by drawing the object diagram for the test above (suitably modified to call capitalizeTitles_good), and work through where the changes occur.

As a bonus: we now have a separate capitalizeTitle method on individual books, which is both of general utility and easy to test.

The moral of this example is a subtle but important lesson in the differences between references and variables: when we “pass a variable to a method”, we actually do no such thing at all! Instead, we pass the value of that variable to the method, and that value just might be a reference to an object. If so, inside the method we bind that reference to the parameter of the method, and obtain an alias to the original object, completely independent of the reference that was in the original variable.

23.3 Adding and removing items from lists

Above we claimed that trying to remove an item from an ArrayList and add a new one, while iterating over that list, is a dangerous idea. We’ll see in Lecture 25: Iterator and Iterable exactly why it’s so fraught, but the intuition should be clear enough. However it is that for-each loops work “under the hood”, they clearly must be keeping track of which element in the list is the current one. If we remove that element from the list, what should the “next” element be? Worse yet, if we add a new element into the list, should we ignore it (because it wasn’t there when we began the loop), or iterate over it (because it’s “ahead” of the current point of the iteration)? In general it is an error to modify the contents of an ArrayList (by adding or removing items) while concurrently iterating over them via a for-each loop: Java will throw a ConcurrentModificationException.

But there still is a way to achieve this effect. If we use a different kind of loop, we simply won’t run into this problem. We could rewrite our first attempt above as
// In ArrayUtils // EFFECT: Modifies all the books in the given ArrayList, to capitalize their titles void capitalizeTitles_ok(ArrayList<Book> books) {
for (int i = 0; i < books.size(); i = i + 1) {
// get the old book... Book oldB = books.get(i);
// ... construct the new book ... Book newB = new Book(oldB.title.toUpperCase(), oldB.author);
// and set it in place of the old book, at the current index books.set(i, newB);
}
}
Because we are counting indices, rather than iterating over the contents of the ArrayList directly, we are manually managing the details of which item is “next”, rather than letting the for-each loop manage that for us. Arguably, the good solution above is better than this ok solution, precisely because it avoids our having to write such manual index-managing code, and it gave us a useful helper method in the process. (Indeed, it’s particularly easy to get confused when writing this sort of code, especially if it involves removing and re-adding items, instead of just overwriting existing ones as we did here. In such cases, we could easily wind up processing the same element twice, or not at all, just by inadvertently getting our indices wrong.)