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
Do Now!
Do it.
// In ArrayUtils <U> ArrayList<U> buildList(int n, IFunc<Integer, U> func) { ... }
// In ArrayUtils <U> ArrayList<U> buildList(int n, IFunc<Integer, U> func) { for (int i = 0; i < n; i = i + 1) { ... } }
// 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; }
// 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.
Do Now!
Design a method in ArrayUtils to capitalize all titles. Which loop form should we use?
// In ArrayUtils // Capitalizes the titles of all books in the given ArrayList ??? capitalizeTitles(ArrayList<Book> books) { for (Book b : books) { ... b.title.toUpperCase() ... } }
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.
// 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?
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"); } }
// 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.
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.
// 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); } }