Equality and Similarity of Objects
We’ve seen how to compare two strings: we use a method called .equals that takes another string and returns a boolean indicating whether the two strings have the same characters (including upper/lower case). We’ve also seen how to compare two numbers using ==.
What if we want to compare entire objects?
As a concrete example, recall our class for Books:
// A class for books in a library |
class Book { |
String title; |
String callNum; |
int timesOut = 0; |
boolean isAvailable = true; |
|
// constructor with just variable fields |
Book(String title, String callNum) { |
this.title = title; |
this.callNum = callNum; |
} |
|
// full constructor |
Book(String title, String callNum, |
int timesOut, boolean isAvailable) { |
this.title = title; |
this.callNum = callNum; |
this.timesOut = timesOut; |
this.isAvailable = isAvailable; |
} |
|
// mark a book as checked out of the library |
Book checkOut() { |
this.isAvailable = false; |
this.timesOut = this.timesOut + 1; |
return this; |
} |
} |
Imagine that we had the book "Hamlet" and we wanted to check it out:
> Book hamletBook = new Book("Hamlet", "PR2807.A2"); |
> hamletBook.checkOut() |
We know what the checkOut method should do: it should return a book with the same title and call number, but with the book out one time and not available. In other words, we would expect to get a book with the contents of the following:
new Book("Hamlet", "PR2807.A2", 1, false); |
How might we check whether these this is indeed the result of checking out? Based on what we’ve seen about comparing things in Java, it looks like we could try one of the following (assuming we had just created hamletBook and never checked it out before):
hamletBook.checkOut() == new Book ("Hamlet", "PR2807.A2", 1, false)
hamletBook.checkOut().equals(new Book ("Hamlet", "PR2807.A2", 1, false))
It turns out neither of these will produce true, even though both books have the same contents in their fields.
Why not?
It turns out that "equality" is a subtle concept. When you ask whether two objects are equal, you could be asking "are these the exact same object down in the objects space in memory", or "do these have the same contents, whether or not they are the same object in the object space in memory".
If you think about what the object space would look like on our hamletBook example, we have two Book objects because we used new twice (once to create hamletBook and once to create the new book to which we are comparing hamletBook).
hamletBook == hamletBook |
Instead, we might want two Book objects to be considered the same if they had the same contents in their fields. This is a useful idea in practice, but it gets subtle. Do we want two Book objects to be "the same" only if all their fields have the same contents, or is it enough that the titles and call numbers match?
In short, Java doesn’t know what you (the programmer) wants to count as equality, and it doesn’t try to guess. By default, if you use .equals on two objects, Java just checks for ==. However, Java lets you customize .equals as you see fit. It’s just a method after all, so you can put a .equals method in your class to tell Java when two objects should be considered equal.
1 Writing your own equals methods
Here is an equals method for Book that compares all four fields. If you wanted to only consider the title and call number, you could delete the lines that compare timesOut and isAvailable. It would be up to you which of these you wanted to use in practice.
class Book { |
String title; |
String callNum; |
int timesOut = 0; |
boolean isAvailable = true; |
|
// an equals method that compares all four fields |
public boolean equals(Book other) { |
return this.title.equals(other.title) && |
this.callNum.equals(other.callNum) && |
this.timesOut == other.timesOut && |
this.isAvailable == other.isAvailable ; |
} |
} |
Now that we have equals, we can compare the hamletBook to a new Book:
> hamletBook.checkOut().equals(new Book ("Hamlet", |
"PR2807.A2", |
1, false)) |
true |
2 Equality on Lists
Now that you are working with lists, you may also want to be able to compare two LinkedList objects. Consider the following three lists – which ones should be considered equal?
LinkedList<Book> list1 = new LinkedList<Book>(); |
LinkedList<Book> list2 = new LinkedList<Book>(); |
LinkedList<Book> list3 = new LinkedList<Book>(); |
LinkedList<Book> list4 = new LinkedList<Book>(); |
|
Book javaBook = new Book("Effective Java", "QA76.73 J38 B57"); |
Book hamletBook = new Book("Hamlet", "PR2807.A2"); |
|
list1.add(hamletBook) |
list1.add(javaBook) |
|
list2.add(javaBook) |
list2.add(hamletBook) |
|
list3.add(hamletBook) |
list3.add(javaBook) |
|
list4.add(new Book("Hamlet", "PR2807.A2")); |
list4.add(javaBook) |
None of these lists is == to any other, because each list was created with a separate use of new.
list1 and list2 have the same elements, but in different orders. The equals method on LinkedList cannot handle this situaton, and will report the lists as not equal. (This is why the homework asks you to maintain the order of athletes in the BiathlonDNF method.)
list1 and list3 have the exact same objects in the same order. Therefore, list1.equals(list3) (and vice-versa), even though the lists themselves are different objects.
Note: If you write your Examples so that you only add elements to lists through variable names (such as javaBook) as we did for list1, list2 and list3, then you can use equals to check equality of lists for the homework.
list4 has elements that are equals to those in list1 and list3 on a position-by-position basis. We would expect list1.equals(list4) to be true, but it isn’t, due to a subtlety in Java’s type system. The next section explains how to write your equals method in the Book class to allow the lists to also be equal even if you create the elements as new objects. You can skip the next part for now, if you wish.
For homework 2, we recommend you add items through variable names for elements, unless you are feeling comfortable in Java!
2.1 More advanced: making equals work for lists with new objects
When comparing the contents of lists of Books, Java can’t use our equals method on Book as written. The problem is that our equals method says the other input will be a Book. Java knows that a LinkedList can be any type of object. Java therefore needs the input to our equals method to allow any object, not just books. We need to modify equals to work with any type of object as the other input.
Java has a type Object that we can use to say "any object". We modify the type of other to Object, as shown in the code just below.
Once we do that, however, the compiler will complain if we try to write other.title (what if other isn’t a book?). So inside the method, we have to tell Java that we assume the other argument will indeed be a Book. If it isn’t, Java will report an error when it runs the program.
public boolean equals(Object other) { |
// need to tell Java that the other really is a book -- |
// next line is the notation that does it: it tells |
// Java to trust us that the type of other is Book |
Book otherBook = (Book)other; |
return this.title.equals(otherBook.title) && |
this.callNum.equals(otherBook.callNum) && |
this.timesOut == otherBook.timesOut && |
this.isAvailable == otherBook.isAvailable; |
} |
This new annotation (Book) other is called a cast. It is a way of telling Java that you know something about the type that Java isn’t able to figure out. When you write a cast, you ask the Java compiler to trust you (and not complain about type errors on that variable); Java will, but it will terminate your program if you break the promise at run time.
If you’ve written equals methods before, you might be wondering why we don’t have this set up to return false if a non-book object is passed as other. We’re not doing that here because it requires us to get into checking what types objects have, and I don’t want to get into that just yet. For purposes of the problems we are solving now, we know what types of objects we will be comparing, so this approach suffices, and lets us introduce additional concepts more gradually.