On this page:
19.1 Introduction
19.2 Phone lists, take 1
19.3 Phone lists, take 2
19.4 Aliasing, or, what’s in a name?
19.5 Aliasing, mutation and equality:   two notions of equality
19.6 Another example of aliasing
19.7 Generalizing to generic lists
19.7.1 Finding an item
19.7.2 Modifying an item
6.1.1

Lecture 19: Mutation, aliasing and testing

Sharing objects between multiple data structures, modifying fields of aliased objects, modifying list structures

19.1 Introduction

Back in the dark ages, before Star Trek-style communicators allowed people to verbally ask their pocket supercomputers to hail other people, there were such things as phone books, which were used to maintain lists of people and their phone numbers. Often, people maintained several such phone books—one for family, one for friends, one for colleagues, etc.—and any given contact might appear in multiple lists. However, when people moved, they couldn’t take their phone number with them (since it was physically associated with their old house with a wire). Updating phone books to keep track of these changes was tricky, since it was easy to forget that someone might appear in multiple lists, and so the information quickly became inconsistent.

Let’s see how we can improve the situation, and represent such phone books in Java.

19.2 Phone lists, take 1

Phone lists were lists of people and their phone numbers, which suggests we can model them as ILoPerson, where

We’re deliberately starting with non-generic lists. Once we figure out how we want our code to behave, we’ll revisit this decision and translate this to work with generics.

class Person {
String name;
int phone;
Person(String name, int phone) {
this.name = name;
this.phone = phone;
}
// Returns true when the given person has the same name and phone number as this person boolean samePerson(Person that) {
return this.name.equals(that.name) && this.phone == that.phone;
}
}
Let’s construct a few examples of people and phone lists:
class ExamplePhoneLists {
Person anne = new Person("Anne", 1234);
Person bob = new Person("Bob", 3456);
Person clyde = new Person("Clyde", 6789);
Person dana = new Person("Dana", 1357);
Person eric = new Person("Eric", 12469);
Person frank = new Person("Frank", 7294);
Person gail = new Person("Gail", 9345);
Person henry = new Person("Henry", 8602);
Person irene = new Person("Irene", 91302);
Person jenny = new Person("Jenny", 8675309);
 
ILoPerson friends, family, work;
void initData() {
this.friends =
new ConsLoPerson(this.anne, new ConsLoPerson(this.clyde,
new ConsLoPerson(this.gail, new ConsLoPerson(this.frank,
new ConsLoPerson(this.jenny, new MtLoPerson())))));
this.family =
new ConsLoPerson(this.anne, new ConsLoPerson(this.dana,
new ConsLoPerson(this.frank, new MtLoPerson())));
this.work =
new ConsLoPerson(this.bob, new ConsLoPerson(this.clyde,
new ConsLoPerson(this.dana, new ConsLoPerson(this.eric,
new ConsLoPerson(this.henry, new ConsLoPerson(this.irene,
new MtLoPerson()))))));
}
}

Do Now!

Draw the object diagram representing these people and the phone lists created by initData. Be careful!

friends
   |
   V
 +-------+  +-------+  +-------+  +-------+  +-------+  ++
 | rest --->| rest --->| rest --->| rest --->| rest --->||
 | first |  | first |  | first |  | first |  | first |  ||
 +--|----+  +--|----+  +--|----+  +--|----+  +--|----+  ++
    |          |          |          |          +------------------------------------------+
    |          +----+     +----------------------------------+                             |
    |               |                +------------+          |                             |
    V               V                             V          V                             V
+------+ +------+ +-------+ +------+ +-------+ +-------+ +------+ +-------+ +-------+ +---------+
| Anne | | Bob  | | Clyde | | Dana | | Eric  | | Frank | | Gail | | Henry | | Irene | | Jenny   |
| 1234 | | 3456 | | 6789  | | 1357 | | 12469 | | 7924  | | 9345 | | 8602  | | 91302 | | 8675309 |
+------+ +------+ +-------+ +------+ +-------+ +-------+ +------+ +-------+ +-------+ +---------+
    ^        ^        ^        ^ ^      ^          ^                 ^             ^
    |        |        |        | |      |          |                 +-----------+ |
    |        |        |        | |      +----------|--------------------------+  | |
    |        |        |        | +-----------------|---------------+          |  | +----------------+
    |        |        +--------|-------------------|----+          |          |  |                  |
    |        +-----------------|-------------+     |    |          |          |  |                  |
    |                          |             |     |    |          |          |  +-------+          |
    |          +---------------+             |     |    |          |          |          |          |
    |          |          +------------------|-----+    |          |          |          |          |
 +--|----+  +--|----+  +--|----+  ++         |          |          |          |          |          |
 | first |  | first |  | first |  ||      +--|----+  +--|----+  +--|----+  +--|----+  +--|----+  +--|----+  ++
 | rest --->| rest --->| rest --->||      | first |  | first |  | first |  | first |  | first |  | first |  ||
 +-------+  +-------+  +-------+  ++      | rest --->| rest --->| rest --->| rest --->| rest --->| rest --->||
  ^                                       +-------+  +-------+  +-------+  +-------+  +-------+  +-------+  ++
  |                                         ^
family                                      |
                                          work
(Additionally, anne, bob, etc. each refer to the appropriate Person box in the diagram; they’re unlabeled here simply because the diagram is already intricate enough.)

The simplest and most useful function of a phone list is to see if someone is present in a given phone list. (Throughout this example, we’ll assume that all names are distinct in each phone list.)
interface ILoPerson {
// Returns true if this list contains a person with the given name boolean contains(String name);
}
We need some tests to confirm our method will work properly:
// In ExamplePhoneLists void testFindPhoneNum(Tester t) {
this.initData();
t.checkExpect(this.friends.contains("Frank"), true);
t.checkExpect(this.work.contains("Zelda"), false);
}

The implementation of this method is straightforward:
// In MtLoPerson // Returns true if this empty list contains a person with the given name public boolean contains(String name) { return false; }
// In ConsLoPerson // Returns true if this non-empty list contains a person with the given name public int contains(String name) {
return this.first.name.equals(name) || this.rest.contains(name);
}

The next simplest operation on phone lists is to look someone up by name and find that person’s phone number. Since we know that phone numbers cannot be negative, let’s choose to return -1 if no such person exists in the phone list.
interface ILoPerson {
// Finds the person in this list with the given name and returns their phone number, // or -1 if no such person is found int findPhoneNum(String name);
}
We need some tests to confirm our method will work properly:
// In ExamplePhoneLists void testFindPhoneNum(Tester t) {
this.initData();
// Should be able to find the correct number of someone in a list t.checkExpect(this.friends.findPhoneNum("Frank"), 7294);
// Should return -1 for someone not in a list t.checkExpect(this.work.findPhoneNum("Zelda"), -1);
// When someone is in two lists, their number should be the same in both t.checkExpect(this.friends.findPhoneNum("Anne"),
this.family.findPhoneNum("Anne"));
}

// In MtLoPerson // Finds the person in this empty list with the given name and returns their phone number, // or -1 if no such person is found public int findPhoneNum(String name) { return -1; }
// In ConsLoPerson // Finds the person in this non-empty list with the given name and returns their phone number, // or -1 if no such person is found public int findPhoneNum(String name) {
if (this.first.name.equals(name)) {
return this.first.num;
}
else {
return this.rest.findPhoneNum(name);
}
}

Suppose that Frank moved, and his new phone number is 9021. The following test ought to pass:
t.checkExpect(this.friends.changePhone("Frank", 9021).find("Frank"),
this.family.find("Frank"));
After all, if Frank’s number changes in one phone list, we want it to be changed in all phone lists where he appears.

We need a way to change the number of someone in a phone list. We might therefore try something like this, which we’ve done many times before:
// In ILoPerson // Change the phone number for the person in this list with the given name ILoPerson changePhone(String name, int newNum);
// In MtLoPerson // Change the phone number for the person in this empty list with the given name ILoPerson changePhone(String name, int newNum) { return this; }
// In ConsLoPerson // Change the phone number for the person in this non-empty list with the given name ILoPerson changePhone(String name, int newNum) {
if (this.first.name.equals(name)) {
return new ConsLoPerson(new Person(name, newNum), this.rest);
}
else {
return new ConsLoPerson(this.first, this.rest.changePhone(name, newNum));
}
}

But when we try running the suggested test above, it fails: Frank’s phone number has not been updated in the family list.

Do Now!

Why not?

As we’ve written changePhone so far, what it actually does is create a new Person with Frank’s name and the new phone number, and then constructs a new phone list with this new Person in place of the original one. It’s no surprise, then, that Frank’s number hasn’t been changed in the family list: in fact, it hasn’t been changed at all!

19.3 Phone lists, take 2

If we want a person’s phone number to be changed in all the lists where that person appears, we actually need the modify the person, and not merely replace them.

Do Now!

Design a method on Person that changes the phone number to the given one. What return type should this method have?

Since the purpose of this method is simply to cause side effects, it does not produce any new result:
// In Person // EFFECT: modifies this person's phone number to be the given one void changeNum(int newNum) {
this.phone = newNum;
}

Testing this code is trickier, since (if everything goes well) the method will modify an object, which might cause problems for our other tests. Remember the recipe we created in the last lecture for testing methods with side effects: first create a test fixture in a consistent, well-known state. Second, run the method. Third, check for the expected side effects, and also (if possible) for unexpected side effects.

Do Now!

We already have the start of a test fixture for phone lists. Modify the definition of initData so that it initializes all the Person fields of the examples class, too, instead of allowing them to be initialized just once (and hoping they never get changed).

Once we’ve revised our example class, we can use initData to create consistent, well-known people with well-known data. Then the following test ought to work:
// In ExamplePhoneLists void testChangeNum(Tester t) {
this.initData();
t.checkExpect(this.frank.phone, 7294);
this.frank.changeNum(9021);
t.checkExpect(this.frank.phone, 9021);
}

Now we can use Person’s changeNum to implement a corrected version of ILoPerson’s changePhone. Since we’re only interested in the side effects of modifying some Person, we do not need to return any value:

// In ILoPerson // EFFECT: Change the phone number for the person in this list with the given name void changePhone(String name, int newNum);
// In MtLoPerson // EFFECT: Change the phone number for the person in this empty list with the given name void changePhone(String name, int newNum) { return; }
// In ConsLoPerson // EFFECT: Change the phone number for the person in this non-empty list with the given name void changePhone(String name, int newNum) {
if (this.first.name.equals(name)) {
this.first.changeNum(newNum); // do the update }
else {
this.rest.changePhone(name, newNum); // keep searching the rest of the list }
}

Now the following tests all pass:
// In ExamplePhoneLists void testFindPhoneNum(Tester t) {
this.initData();
t.checkExpect(this.friends.findPhoneNum("Frank"), 7294);
t.checkExpect(this.family.findPhoneNum("Frank"),
this.friends.findPhoneNum("Frank");
t.checkExpect(this.frank.phone, 7294);
this.family.changePhone("Frank", 9021);
t.checkExpect(this.friends.findPhoneNum("Frank"), 9021);
t.checkExpect(this.family.findPhoneNum("Frank"),
this.friends.findPhoneNum("Frank");
t.checkExpect(this.frank.phone, 9021);
}

19.4 Aliasing, or, what’s in a name?

With just a single method call, which resulted in just a single assignment statement, we’ve managed to change Frank’s phone number in what appears to be three places: in the friends list, in the family list, and on this.frank itself. How can this be? If we draw the object diagram again, we can see what’s going on more clearly:
friends
   |
   V
 +-------+  +-------+  +-------+  +-------+  +-------+  ++
 | rest --->| rest --->| rest --->| rest --->| rest --->||
 | first |  | first |  | first |  | first |  | first |  ||
 +--|----+  +--|----+  +--|----+  +--|----+  +--|----+  ++
    |          |          |          |          +------------------------------------------+
    |          +----+     +----------------------------------+                             |
    |               |                +------------+          |                             |
    V               V                             V          V                             V
+------+ +------+ +-------+ +------+ +-------+ +-------+ +------+ +-------+ +-------+ +---------+
| Anne | | Bob  | | Clyde | | Dana | | Eric  | | Frank | | Gail | | Henry | | Irene | | Jenny   |
| 1234 | | 3456 | | 6789  | | 1357 | | 12469 | | 9021  | | 9345 | | 8602  | | 91302 | | 8675309 |
+------+ +------+ +-------+ +------+ +-------+ +-------+ +------+ +-------+ +-------+ +---------+
    ^        ^        ^        ^ ^      ^          ^                 ^             ^
    |        |        |        | |      |          |                 +-----------+ |
    |        |        |        | |      +----------|--------------------------+  | |
    |        |        |        | +-----------------|---------------+          |  | +----------------+
    |        |        +--------|-------------------|----+          |          |  |                  |
    |        +-----------------|-------------+     |    |          |          |  |                  |
    |                          |             |     |    |          |          |  +-------+          |
    |          +---------------+             |     |    |          |          |          |          |
    |          |          +------------------|-----+    |          |          |          |          |
 +--|----+  +--|----+  +--|----+  ++         |          |          |          |          |          |
 | first |  | first |  | first |  ||      +--|----+  +--|----+  +--|----+  +--|----+  +--|----+  +--|----+  ++
 | rest --->| rest --->| rest --->||      | first |  | first |  | first |  | first |  | first |  | first |  ||
 +-------+  +-------+  +-------+  ++      | rest --->| rest --->| rest --->| rest --->| rest --->| rest --->||
  ^                                       +-------+  +-------+  +-------+  +-------+  +-------+  +-------+  ++
  |                                         ^
family                                      |
                                          work
There really is only one object where Frank’s phone number is recorded, and that one object is where we’ve modified his number. But that object is reachable from three different “names”: this.frank, certainly, but also this.family.rest.rest.first and this.friends.rest.rest.rest.first.

Do Now!

Follow the arrows in the diagram above to convince yourself that all three of these names refer to the same object.

This ability to “share” a single object between multiple data structures, and access it via multiple names, is called aliasing. Now that we have mutation, aliasing can lead to new behaviors that were not visible before.

19.5 Aliasing, mutation and equality: two notions of equality

Consider the following simple example:
// In ExamplesPhoneLists void testAliasing(Tester t) {
// Create two Person objects that are the same Person johndoe1 = new Person("John Doe", 12345);
Person johndoe2 = new Person("John Doe", 12345);
// Alias johndoe1 to johndoe3 Person johndoe3 = johndoe1;
 
// Check that all three John Does are the same according to samePerson t.checkExpect(johndoe1.samePerson(johndoe2), true);
t.checkExpect(johndoe1.samePerson(johndoe3), true);
 
// Modify johndoe1 johndoe1.name = "Johnny Deere";
 
// Now let's try the same tests. Which of them will pass? t.checkExpect(johndoe1.samePerson(johndoe2), true);
t.checkExpect(johndoe1.samePerson(johndoe3), true);
}

Do Now!

Determine which of these tests pass, and which fail. Draw an object diagram to help, if necessary.

After modifying johndoe1, nothing happens to johndoe2 because they are different objects. So the third test fails: “John Doe” is no longer the same name as “Johnny Deere”. On the other hand, johndoe3 is an alias for johndoe1, so they refer to the same object, whose name is now “Johnny Deere”. The fourth test passes.

This is strange: we started with three variables whose values were all the same. We mutated one of them, and after the change that variable still had the same field values as one of the other two, and different field values from the third! This is because in fact two of the variables refered to exactly the same object, as opposed to merely another object with the same field values. In other words, mutation allows us to distinguish whether two items that “look the same” are in fact identically the same exact object or not.

So now we must distinguish two notions of equality:
  • Extensional equality: two items are extensionally equal if their fields are the same: either they’re equal primitive values, or they’re objects that are themselves extensionally equal.

  • Intensional equality: two items are intensionally equal if they are the exact same object.

A helpful mnemonic: extensional equality is when two items have the same extent or shape, while intensional equality is only for identical objects.

So far, every time we define a sameness method (sameShape, sameList, samePerson, etc.), it has been a test of extensional equality, because we recur down the shape, checking that all fields are the same.

In fact, the exact semantics of the == operator are slightly more complicated than this. We’ll see the full meaning of this operator in Lecture 26.

If we truly want to check intensional equality, we can use Java’s == operator. We have not yet explicitly seen any situations where we need this ability, but we have implicitly seen one, though we did not recognize it at the time. The tester library uses a combination of both intensional and extensional equality to check our tests:
  • It must use some form of extensional equality, because we can check some existing data against a newly created object: if the tester library merely used intensional equality, these tests would fail.

  • It must use some form of intensional equality too, because when we created a cycle between a Book and an Author, we wrote a test checking whether the book’s author’s book was the book itself. If it used extensional equality only, then the tester library would get stuck infinitely looping through the cycle.

Precisely how the tester library works is beyond the scope of this course, but even this high-level description helps to motivate both the differences between and the need for both of these two forms of equality.

19.6 Another example of aliasing

Let’s look at our Counter example from two lectures ago. Here is the class definition again, for reference.
class Counter {
int val;
Counter() {
this(0);
}
Counter(int initialVal) {
this.val = initialVal;
}
int get() {
int ans = this.val;
this.val = this.val + 1;
return ans;
}
}
Suppose we create two new Counters, and initialize a third Counter variable to be the same as (i.e. an alias of) the first one. This is a very similar setup to our John Doe examples above, except now we want to test a mutable method on these objects. What should the following tests produce?
class ExamplesCounter {
void testCounter(Tester t) {
Counter c1 = new Counter();
Counter c2 = new Counter(5);
Counter c3 = c1;
// What should these tests be? t.checkExpect(c1.get(), ???); // Test 1 t.checkExpect(c2.get(), ???); // Test 2 t.checkExpect(c3.get(), ???); // Test 3 t.checkExpect(c1.get() == c3.get(), ???); // Test 4 t.checkExpect(c1.get() == c1.get(), ???); // Test 5 t.checkExpect(c2.get() == c1.get(), ???); // Test 6 t.checkExpect(c2.get() == c1.get(), ???); // Test 7 t.checkExpect(c1.get() == c1.get(), ???); // Test 8 t.checkExpect(c2.get() == c1.get(), ???); // Test 9 }
}

Do Now!

Fill in the ??? in the tests above.

  • We initialize c1 to a new counter, with a default initial value of 0.

  • We initialize c2 to a new counter with an initial value of 5.

  • We initialize c3 to c1.

  • In test 1, we get c1’s value, which is currently 0 and c1 updates its internal value to 1.

  • In test 2, we get c2’s value, which is currently 5 and c1 updates its internal value to 6.

  • In test 3, we get c3’s value, but c3 is the same exact object as c1, which we’ve updated, so the value is 1, and the object updates its internal value to 2.

  • In test 4, we get c1’s value, which is now 2, and then we get c3’s value, which is now 3 (why?), so this equality test evaluates to false.

  • In test 5, we get c1’s value, which is now 4, and then we get it again — and it’s now 5! So this equality test evaluates to false also — this function’s return value is not even equal to itself.

  • In test 6, we get c2’s value, which is now 6, and then we get c1’s value, which is now 6, so this equality test happens to be true.

  • Just to be sure, we try it again in test 7. This time, c2 and c1 both evaluate to 7, so the test still is true.

  • Something seems fishy, so we try tests 6 and 7 again. In test 8, we get c1’s value (which is 8), then get it again (now it’s 9), so this test evaluates to false.

  • Finally, in test 9, we try getting c2’s value (which is 8), and c1’s value (which is now 10), so this test, which was true twice, is now false.

Clearly, the exact behavior of these counters depends not only on how often we’ve called their get() method, but on exactly how they are aliased. After all, if we just look at the output of calling get() on the variable c1, we see outputs 0, 2, 4, 5, 6, 7, 8, 9, 10 and 1 and 3 are missing. Those values were produced when we invoked get() on c3, which is aliased to c1.

This may seem like an utterly contrived example, but it can easily occur in a more plausible setting. Suppose we had a method that took two Counters as arguments. There’s no way to tell, a priori, whether the two parameters are distinct Counter objects or whether they are in fact the same Counter object. As a result, like tests 4 through 9, the exact results of using the two Counters’ outputs in tandem may vary widely.

19.7 Generalizing to generic lists

Generic lists IList<T> are more flexible than special-purpose lists, certainly, but that also means we can’t give them a method like findPhoneNum or changePhone, because those methods require Persons and will not work on arbitrary T objects. Nevertheless, the notions of finding some item of a list, or modifying an item in a list, are general enough, if we abstract away the Person-specific parts of our code. For that, we’ll use function objects.

19.7.1 Finding an item

Let’s consider find first. There are three things we’ll need to change from our prior implementation of findPhoneNum:
  • Instead of the specific test this.first.name.equals(name), we need a general test for arbitrary values of type T. We’ve seen this before: we need an IPred<T> predicate.

  • Instead of specifically returning the phone number, we’ll just return the entire T value.

  • In the base case when the item is not found, we can’t return -1 (because T may not be a number). Instead, we’ll have to return null. Remember from Lecture 17 that null is a placeholder value that can be used for any object type, and indicates the lack of an actual, useful value.

Do Now!

Design the method T find(IPred<T> whichOne) for IList<T>, which uses the given predicate to determine which item in the list to return.

// In IList<T> // Finds and returns the person in this list matching the given predicate, // or null if no such person is found T find(IPred<T> whichOne);
// In MtList<T> public T find(IPred<T> whichOne) { return null; }
// In ConsList<T> public T find(IPred<T> whichOne) {
if (whichOne.apply(this.first)) {
return this.first;
}
else {
return this.rest.find(whichOne);
}
}

Do Now!

Revise the tests for findPhoneNum (including the data-initialization test fixture) to use IList<Person> instead of ILoPerson. Create whatever function objects are needed.

19.7.2 Modifying an item

Modifying an item is much like finding an item, but we’ll have to generalize not just the predicate, but also the operation to be done to the item once it’s found. The actual code for this method is straightforward given our earlier implementation of changePhone, but the types of the arguments are somewhat subtle.
// In IList<T> // EFFECT: Finds and modifies the person in this list matching the // given predicate, by using the given operation void find(IPred<T> whichOne, ???? whatToDo);
The whatToDo argument should be a function object, but what exactly should its type be? We want it to produce a side effect such as changing a phone number, rather than return a value. We run into a small Java-specific complication: just as with the primitive types int and double and boolean, we can’t use void as a type parameter for instantiating a generic type. And just as with those types, Java provides an object type named (capitalized) Void which can be used instead.
// In IList<T> // EFFECT: Finds and modifies the person in this list matching the // given predicate, by using the given operation void find(IPred<T> whichOne, IFunc<T, Void> whatToDo);
The Void type is somewhat funny: it’s meant to represent the absence of a value, so it would be strange indeed to construct an instance of this object — we’d have a value that’s meant to represent not having a value! Instead, the Void type does not have any constructors, so we cannot actually create an object of type Void. But implementing the IFunc<T, Void> interface requires that we somehow return a value of type Void; this sounds like a contradiction! Fortunately, again, null can be used as a placeholder for any object type, even Void. And null basically means “there is no useful value here”, which is exactly what we need.
// In MtList<T> public Void find(IPred<T> whichOne, IFunc<T, Void> whatToDo) { return null; }
// In ConsList<T> public Void find(IPred<T> whichOne, IFunc<T, Void> whatToDo) {
if (whichOne.apply(this.first)) {
whatToDo.apply(this.first);
}
else {
this.rest.find(whichOne, whatToDo);
}
}

Do Now!

Revise the rest of the tests for changePhone to use IList<Person>. Create whatever function objects are needed.