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—
Let’s see how we can improve the situation, and represent such phone books in Java.
19.2 Phone lists, take 1
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; } }
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
interface ILoPerson { // Returns true if this list contains a person with the given name boolean contains(String name); }
// In ExamplePhoneLists void testFindPhoneNum(Tester t) { this.initData(); t.checkExpect(this.friends.contains("Frank"), true); t.checkExpect(this.work.contains("Zelda"), false); }
// 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 boolean contains(String name) { return this.first.name.equals(name) || this.rest.contains(name); }
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); }
// 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); } }
t.checkExpect(this.friends.changePhone("Frank", 9021).find("Frank"), this.family.find("Frank"));
// 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)); } }
Do Now!
Why not?
19.3 Phone lists, take 2
Do Now!
Design a method on Person that changes the phone number to the given one. What return type should this method have?
// In Person // EFFECT: modifies this person's phone number to be the given one void changeNum(int newNum) { this.phone = newNum; }
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).
// 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 } }
// 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?
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
Do Now!
Follow the arrows in the diagram above to convince yourself that all three of these names refer to the same object.
19.5 Aliasing, mutation and equality: two notions of equality
// 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.
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.
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.
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.
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.
19.6 Another example of aliasing
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; } }
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 c2 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.
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
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
// 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);
// 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);
// 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); } return null; }
Do Now!
Revise the rest of the tests for changePhone to use IList<Person>. Create whatever function objects are needed.