CSE 331: Equality
Course motto: Your job is to deliver code that you know works.
The objects of an abstract data type stand for values, and a
client reasons about the values, never the fields. Java gives
every object an equals method and leaves each class to say what
it means. So this lecture asks one question: when are two objects
of an abstract data type equal? The answer is a definition, the
same abstract value, and two methods written from that value,
equals and hashCode. Along the way are a trap in Java, an
overload that passes for an override; a case where no answer
exists, a subclass that adds to the value; and a cost, a set that
loses track of an element whose value changes.
The same queue twice
Recall the queue for office hours, HelpQueue: the
students waiting for help, in the order they joined, with each
student in the queue at most once. We have implemented it several
times, and today we need two of the implementations.
LinkedQueue keeps the students as a chain of
nodes, one node per student. FasterQueue keeps
them in two lists and a set. Here is a program that makes one of
each. (Every program in this lecture, the copies included, is in
the course's public repo, to download and run.) Three
students join q1, the FasterQueue, and next serves one. Two
join q2, the LinkedQueue. Then main prints four lines about
the pair:
| Queues.java | |
|---|---|
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | |
Before you read on, write down the four lines you expect it to print. Here is the run:
$ javac Queues.java
$ java Queues
q1.waiting(): [bob, eve]
q2.waiting(): [bob, eve]
q1 == q2: false
q1.equals(q2): false
The first two lines are the same, and here is why, one queue at a time:
q1. ada, bob, and eve joined, in that order, so ada was at the front. Thennextserved ada. So bob and eve are waiting, front first.q2. bob and eve joined, in that order. So the same two students are waiting, front first again.
A client that calls HelpQueue's methods on q1 and on q2
gets the same answers from both. As far as a client can tell, the
two hold the same queue. But the last two lines print false.
== is the easy one. q1 == q2 is true only when the two names
refer to the same object. q1 and q2 came from two new
expressions, so they are two different objects, and == says
so. That is not the question we meant. We asked whether the two
are the same queue, not whether they are the same object.
You have called equals on two strings, and it compared their
characters. So what did it compare here? HelpQueue specifies
no equals, and neither class declares one. So each class
inherits equals from Object, the class that every Java class
extends, whether or not it says so. Here is Object's equals,
from the JDK's own source:
public boolean equals(Object obj) {
return (this == obj);
}
q1.equals(q2) ran this method, computed q1 == q2, and
printed the same false. Nobody has said yet what equals
should mean for a help queue, so Java fell back on the one check
Object can make.
Equal means the same abstract value
What should equals have said? Think about what each object
stands for. q1's fields are an empty inbox, an
outbox holding eve, bob, with the front of the queue last, and
a members set holding both. FasterQueue's abstraction
function says that those fields stand for the queue bob, eve.
q2's fields are a chain of two nodes, bob's and then eve's, and
that chain stands for the queue bob, eve. The fields differ, but
the two objects stand for the same queue:
The fields of q1 and of q2, and the queue that both stand for.
== compared the right side of the picture and found two
objects. The question we meant is about the left side. For a help
queue, and for any abstract data type, equal should mean the same
abstract value.
Definition: equality for an abstract data type
Two objects of an abstract data type are equal when they stand for the same abstract value.
equals is the method that decides it. Like every method of
the type, its specification is about the abstract state, the
value an object stands for, and its body works on the fields.
Why did Object not do this for us? Because Object cannot know
what its subclasses stand for. Some classes stand for particular
things rather than values: a window on the screen, a connection
to a server. Two windows with the same size and title are still
two windows, so for those classes == is the right equals. A
help queue stands for a value, the students waiting, and two
queue objects can stand for the same value. Only the class knows
which kind it is and what its fields mean. So a class whose
objects stand for values writes its own equals, unless its
objects can change, a case we come to at the end.
Object does write down what any equals must do: its
specification. Every class is a subtype of Object, and a
subtype has to keep every promise its supertype
makes, so every equals we write has to satisfy it.1 Here
it is, from the documentation:
The
equalsmethod implements an equivalence relation on non-null object references:
- It is reflexive: for any non-null reference value
x,x.equals(x)should returntrue.- It is symmetric: for any non-null reference values
xandy,x.equals(y)should returntrueif and only ify.equals(x)returnstrue.- It is transitive: for any non-null reference values
x,y, andz, ifx.equals(y)returnstrueandy.equals(z)returnstrue, thenx.equals(z)should returntrue.- It is consistent: for any non-null reference values
xandy, multiple invocations ofx.equals(y)consistently returntrueor consistently returnfalse, provided no information used inequalscomparisons on the objects is modified.- For any non-null reference value
x,x.equals(null)should returnfalse.
The first three bullets say that equals is an equivalence
relation, like = on numbers: every number equals itself,
x = y if and only if y = x, and x = y and y = z give
x = z. That is a lot to promise. But if equals asks whether
two objects stand for the same abstract value, every bullet
comes for free:
=on values of any kind is reflexive, symmetric, and transitive, soequalsis too.equalsis consistent as long as neither object's abstract value changes between the calls.nullstands for no value, so nothing equals it.
And the specification never says that equals is ==. Object's
body computes ==, but its promise is only the five bullets, so a
class may call more objects equal than == does and still keep
every one of them, as a subtype may
strengthen a specification.
Let's write one. For the queues it takes two classes that agree
with each other, since a LinkedQueue and a FasterQueue can
stand for the same queue. So we start with a type that has one
class and one number to compare, learn what equals has to
compute there and what goes wrong when it does not, and then come
back to the queues.
What equals should compute
Here is that type: Duration, a span of time made from minutes
and seconds. Its representation invariant lets the seconds run
past 59, the way a microwave accepts 0 minutes and 99
seconds.2 So new Duration(1, 39) and new Duration(0, 99)
are two values of the fields for one span, just as q1 and q2
were two representations of one queue. The class has no equals
yet. Its main makes those two spans, gives the second a second
name, o, declared an Object, and puts the first in a HashSet
(a set that answers contains in "constant time", the kind
FastQueue keeps its students in). Then it asks
four questions: are the two spans one object, is d1 equal to
d2, is d1 equal to o, and does the set holding d1
contain d2?
/** An immutable span of time, a whole number of seconds long. */
public class Duration {
private final int min;
private final int sec;
// RI: 0 <= min and 0 <= sec
// AF(this) = the span of 60 * min + sec seconds
/**
* @requires 0 <= min and 0 <= sec
* @effects makes the span of min minutes and sec seconds
*/
public Duration(int min, int sec) {
this.min = min;
this.sec = sec;
checkRep();
}
private void checkRep() {
if (min < 0 || sec < 0) {
throw new IllegalStateException("a part is negative");
}
}
/** @return the length of this span, in seconds */
public int seconds() {
return 60 * min + sec;
}
public static void main(String[] args) {
Duration d1 = new Duration(1, 39);
Duration d2 = new Duration(0, 99);
Object o = d2;
Set<Duration> set = new HashSet<Duration>();
set.add(d1);
System.out.println("d1 == d2: " + (d1 == d2));
System.out.println("d1.equals(d2): " + d1.equals(d2));
System.out.println("d1.equals(o): " + d1.equals(o));
System.out.println("set.contains(d2): " + set.contains(d2));
}
}
Write down what the four lines print. Here is the run:
$ javac Duration.java
$ java Duration
d1 == d2: false
d1.equals(d2): false
d1.equals(o): false
set.contains(d2): false
All four say no. == and Object's equals we have seen: they
ask whether d1 and d2 are one object, and they are two. o
is d2 under a second name, so the third line asks the same
question again. A HashSet decides what it contains with
equals and with a second method of Object's, hashCode,
which we come to below. With Object's versions of both, the set
holding d1 does not have d2.
A Duration stands for a length, so let's compare lengths. Here
is an equals that takes a Duration:
/** @return whether d is the same length as this */
public boolean equals(Duration d) {
return seconds() == d.seconds();
}
Now d1.equals(d2) prints true:
$ javac Duration.java
$ java Duration
d1 == d2: false
d1.equals(d2): true
d1.equals(o): false
set.contains(d2): false
But read the next line. o and d2 are one object, and
d1.equals(o) prints false: the same name, the same two
objects, and two different answers. The set cannot find d2
either. Something is wrong with the method, and the third line is
the clue.
An overload is not an override
We meant this method to override Object's equals, so let's
say so: @Override above a method asks javac to check that the
method overrides one. With that line added, javac refuses
the file:
$ javac Duration.java
Duration.java:46: error: method does not override or implement a method from a supertype
@Override
^
1 error
Java tells two methods apart by their names and their parameter
types. Object's equals takes an Object, and
ours takes a Duration, so ours is a second method named
equals, an overload, and Object's is still there beside it.
Duration now has two methods named equals, and each call
picks one of them by the declared type of its argument, when the
program is compiled:
d1.equals(d2).d2is declared aDuration, so the call picks ours, and it printstrue.d1.equals(o).ois declared anObject, so the call picksObject's, which compares references, and it printsfalse.
The object is the same in both calls. Only the declared type of the variable differs, and that was enough to change the answer.
HashSet makes the second kind of call. Its code was written
against Object: inside the set, an element has the type
Object, whatever the set's declared element type, so the one
equals it can call is equals(Object), and our method never
ran during the call to contains.
An override is what we meant to write: a method with the same
name and the same parameter types as the inherited one, which
takes its place. Every call that reached Object's equals then
reaches ours instead, whatever the declared type of the argument.
That is what equals needs, and an overload cannot give it. So
an equals whose parameter is anything but an Object is a
mistake, and @Override above every equals catches it before
the program runs.
The override, and the hash code
Here is an equals that takes an Object:
/** @return whether o is a Duration of the same length as this */
@Override
public boolean equals(Object o) {
if (o instanceof Duration d) {
return seconds() == d.seconds();
}
return false;
}
Two things to read in it:
o instanceof Duration dis true whenois aDuration. When it is,drefers to the same object, with the typeDuration, so that the branch can callsecondson it. The check is false whenoisnull, sincenullis an instance of no class. So the last bullet ofObject's specification,x.equals(null)isfalse, comes free.seconds() == d.seconds()compares lengths, notminandsec. The fields are the representation.seconds()computes the length of the span as anint, a Java value that stands in for the abstract one, so comparing the twoints compares the two spans. TwoDurations are equal when their lengths are, and anequalsthat compared the fields would calld1andd2different.
Here is the run:
$ javac Duration.java
$ java Duration
d1 == d2: false
d1.equals(d2): true
d1.equals(o): true
set.contains(d2): false
Now d1.equals(d2) and d1.equals(o) agree: the override runs
whatever the argument's declared type. And the set still says no.
In this run, as in the last, the set never got as far as equals.
Hash codes. A HashSet keeps its elements in buckets, and
it picks an element's bucket by a number the element reports, its
hash code, the result of a method every class inherits from
Object, hashCode. contains computes its argument's hash
code, looks in that bucket alone, and calls equals on what it
finds there with the same hash code. Object's hashCode, like
Object's equals, is about the object, not the value: as far
as it can, it gives distinct objects distinct numbers. So d1
and d2 report different hash codes, and the set looked for
d2 in a bucket that d1 was never in. Object's
documentation3 states the rule that every class has to
keep, in the specification of hashCode:
If two objects are equal according to the
equalsmethod, then calling thehashCodemethod on each of the two objects must produce the same integer result.
Equal objects have equal hash codes. Our equals compares
lengths, so the hash code has to come from the length too:
@Override
public int hashCode() {
return seconds();
}
Any number computed from the abstract value alone keeps the rule. The length itself is the simplest. Here is the run:
$ javac Duration.java
$ java Duration
d1 == d2: false
d1.equals(d2): true
d1.equals(o): true
set.contains(d2): true
Rule: equals and hashCode together
Write equals(Object) to compare abstract values, and write
hashCode from the same abstract value, in the same class.
An equals whose parameter is anything but an Object is an
overload, which @Override catches. An equals without a
hashCode is a mistake that javac accepts: the program
runs, and a HashSet loses track of things.
Duration has no subclass so far. Let's give it one.
When no answer exists
Suppose we need spans finer than a second. A NanoDuration is a
Duration with a count of nanoseconds added, so a subclass looks
like the natural shape: it gets the minutes, the seconds, and
seconds() from Duration, and adds one field. Here it is,
with an equals written the way Duration's was, from the
abstract value:
/** An immutable span of time, a whole number of nanoseconds long. */
public class NanoDuration extends Duration {
private final int nano;
// RI: 0 <= nano < 1_000_000_000
// AF(this) = the span of 1_000_000_000 * seconds() + nano
// nanoseconds
/**
* @requires 0 <= min, 0 <= sec, and 0 <= nano < 1_000_000_000
* @effects makes the span of min minutes, sec seconds, and nano
* nanoseconds
*/
public NanoDuration(int min, int sec, int nano) {
super(min, sec);
this.nano = nano;
checkRep();
}
private void checkRep() {
if (nano < 0 || 1_000_000_000 <= nano) {
throw new IllegalStateException("nano is out of range");
}
}
/** @return the length of this span, in nanoseconds */
public long nanos() {
return 1_000_000_000L * seconds() + nano;
}
/** @return whether o is a NanoDuration of the same length as this */
@Override
public boolean equals(Object o) {
if (o instanceof NanoDuration n) {
return nanos() == n.nanos();
}
return false;
}
public static void main(String[] args) {
Duration d = new Duration(5, 10);
Duration nd = new NanoDuration(5, 10, 15);
System.out.println("d.equals(nd): " + d.equals(nd));
System.out.println("nd.equals(d): " + nd.equals(d));
}
}
main makes a Duration of 5 minutes 10 seconds and a
NanoDuration of the same 5 minutes 10 seconds and 15
nanoseconds, and asks each whether it equals the other. Write
down both answers before you look:
$ javac Duration.java NanoDuration.java
$ java NanoDuration
d.equals(nd): true
nd.equals(d): false
The two disagree, and Object's specification promised they
would not: equals is symmetric, x.equals(y) if and only if
y.equals(x). Here is how each call went:
d.equals(nd)ranDuration'sequals, which askso instanceof Duration. ANanoDurationis aDuration, byextends, and the two have the same whole seconds, so it said yes.nd.equals(d)ranNanoDuration'sequals.ndis declared aDuration, but a call of an overridden method runs the body of the object's own class, whatever the variable's declared type, by dynamic dispatch. Thatequalsaskso instanceof NanoDuration, anddis not one, so it said no.
Set that beside the overload: an overload is picked by the declared type of the argument, when the program is compiled, and an override by the class of the object the call is made on, when it runs.
The fix most people reach for is to make NanoDuration's
equals agree with Duration's when the other object is a plain
Duration: compare nanoseconds against a NanoDuration, and
otherwise hand the question to Duration's equals, with
super.equals(o). Here is that equals, and a main with two
NanoDurations that differ only in nanoseconds:
/**
* @return whether o is a NanoDuration of the same length as this,
* or a Duration of the same length in whole seconds
*/
@Override
public boolean equals(Object o) {
if (o instanceof NanoDuration n) {
return nanos() == n.nanos();
}
return super.equals(o);
}
public static void main(String[] args) {
Duration nd1 = new NanoDuration(5, 10, 15);
Duration d = new Duration(5, 10);
Duration nd2 = new NanoDuration(5, 10, 30);
System.out.println("d.equals(nd1): " + d.equals(nd1));
System.out.println("nd1.equals(d): " + nd1.equals(d));
System.out.println("d.equals(nd2): " + d.equals(nd2));
System.out.println("nd1.equals(nd2): " + nd1.equals(nd2));
}
Again, predict the four lines first:
$ javac Duration.java NanoDuration.java
$ java NanoDuration
d.equals(nd1): true
nd1.equals(d): true
d.equals(nd2): true
nd1.equals(nd2): false
Symmetry is back, and transitivity is gone: nd1 equals d, d
equals nd2, and nd1 does not equal nd2. We could keep
patching the method, but the trouble is not in the method. Ask
what each class stands for:
- A
Durationstands for a span of whole seconds.dis 5 minutes 10 seconds. - A
NanoDurationstands for a span of nanoseconds.nd1is 5 minutes 10 seconds and 15 nanoseconds, andnd2is 5 minutes 10 seconds and 30 nanoseconds.
So "the same abstract value" has no answer between the two
classes. Suppose it did, and d and nd1 were the same value:
- Then
dandnd2are the same value too, sinceDuration'sequalsgoes by whole seconds andnd2has the same whole seconds asnd1. - Then
nd1andnd2have to be the same value as each other, by transitivity. - But they are not: one is 15 nanoseconds longer than the other.
No equals that calls a NanoDuration equal to the Duration
of its whole seconds, and tells two NanoDurations apart by
their nanoseconds, can keep all five promises.
The lecture on subtyping gives the same verdict in its own
words. Duration's equals promises symmetry and transitivity
to every client, as every equals does. A NanoDuration handed
to such a client breaks one or the other, as the two runs showed,
so NanoDuration is not a behavioral subtype of
Duration, and the rule declare only behavioral
subtypes says not to write extends Duration.
The way out is the second of the two that lecture took for its
square: hold a Duration in a field instead of
extending it, the composition that lecture named. Here is
NanoDuration built that way. It stands for a span of
nanoseconds, its equals and hashCode are written from that
value, and it is not a Duration:
/** An immutable span of time, a whole number of nanoseconds long. */
public class NanoDuration {
private final Duration whole;
private final int nano;
// RI: 0 <= nano < 1_000_000_000
// AF(this) = the span of
// 1_000_000_000 * whole.seconds() + nano nanoseconds
/**
* @requires 0 <= min, 0 <= sec, and 0 <= nano < 1_000_000_000
* @effects makes the span of min minutes, sec seconds, and nano
* nanoseconds
*/
public NanoDuration(int min, int sec, int nano) {
this.whole = new Duration(min, sec);
this.nano = nano;
checkRep();
}
/** @return the length of this span, in nanoseconds */
public long nanos() {
return 1_000_000_000L * whole.seconds() + nano;
}
/** @return whether o is a NanoDuration of the same length as this */
@Override
public boolean equals(Object o) {
if (o instanceof NanoDuration n) {
return nanos() == n.nanos();
}
return false;
}
@Override
public int hashCode() {
return Long.hashCode(nanos());
}
public static void main(String[] args) {
Duration d = new Duration(5, 10);
NanoDuration nd = new NanoDuration(5, 10, 15);
System.out.println("d.equals(nd): " + d.equals(nd));
System.out.println("nd.equals(d): " + nd.equals(d));
}
The hash code comes from the length again. nanos() is a
long, and Long.hashCode folds it into the int that
hashCode returns. Here is the run:
$ javac Duration.java NanoDuration.java
$ java NanoDuration
d.equals(nd): false
nd.equals(d): false
Both say no, and that is the choice we made. Duration's
equals compares whole seconds, and we left it as it was, as you
usually must: the class you would extend is older code, someone
else's, or the library's, with clients that rely on its equals.
A finer value cannot be added to such a class by extending it. It
gets its own class, and a Duration and a NanoDuration never
compare equal.
Java does offer a way to keep the subclass: compare getClass(),
an object's exact class, instead of using instanceof, so that
an object equals only objects of that class. Then a subclass that
adds nothing to the abstract value is unequal to its superclass
too, and the question the subtyping rule asks, whether the
subclass keeps every promise, goes unasked. We do not use it.
Back to the queues
q1 and q2 stand for the same queue, and q1.equals(q2)
printed false. Duration showed the fix: an equals and a
hashCode written from the abstract value. But Duration is one
class, and a help queue has several, so a LinkedQueue and a
FasterQueue have to agree with each other. And a Duration
never changes, while a queue does.
Two classes, one specification. A LinkedQueue holding bob,
eve and a FasterQueue holding bob, eve stand for the same
queue, so they should be equal, whatever their fields look like.
The JDK's lists are the precedent. java.util.List leaves each
implementation free to store its elements as it likes, and its
documentation of equals says:4
In other words, two lists are defined to be equal if they contain the same elements in the same order. This definition ensures that the equals method works properly across different implementations of the
Listinterface.
So an ArrayList can equal a list of any other class. For help
queues the same promise has to be made where every implementation
sees it: in HelpQueue's specification. Every class already has
equals and hashCode from Object, so the interface declares
the two methods only to say what they must do:
/**
* @return whether o is a HelpQueue with the same students
* waiting, in the same order
*/
boolean equals(Object o);
/** @return the hash code of the list waiting returns */
int hashCode();
Then each class writes them. Here are LinkedQueue's, and
FasterQueue's are the same, line for line:
@Override
public boolean equals(Object o) {
if (o instanceof HelpQueue q) {
return waiting().equals(q.waiting());
}
return false;
}
@Override
public int hashCode() {
return waiting().hashCode();
}
Two things to read, again:
o instanceof HelpQueue q, notinstanceof LinkedQueue. The specification says anyHelpQueue, so aFasterQueuehas to count.waiting().equals(q.waiting())compares the two lists of students, front first. Each list is a Java value that stands in for its queue's abstract value, so comparing the lists compares the queues, andList'sequalscompares them element by element, in order.hashCodeis the list's hash code, whichListcomputes from the elements, so equal queues hash alike.
Here is the opening program again, with its main grown by two
questions: whether a set holding q2 finds q1, and what
happens after dan joins q2:
| EqualQueues.java | |
|---|---|
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
Write down the five lines. Here is the run:
$ javac EqualQueues.java
$ java EqualQueues
q1.equals(q2): true
q2.equals(q1): true
set.contains(q1): true
q1.equals(q2): false
set.contains(q2): false
The first three lines are what we wanted. Each queue equals the
other, across two classes, and a HashSet holding q2 finds
q1, since both report the hash code of the list bob, eve.
One class forgets. The interface's specification binds every
implementation, and nothing checks that each one kept it. So
suppose one had not. Here is the run of a copy of the program,
EqualQueuesOneSided.txt in the public repo, in which
LinkedQueue declares no equals and no hashCode, so it keeps
Object's, while HelpQueue still specifies both and
FasterQueue still writes them, and whose main stops after the
first two questions. javac accepts the copy, since the two
methods inherited from Object implement the declarations:
$ javac EqualQueues.java
$ java EqualQueues
q1.equals(q2): true
q2.equals(q1): false
That is NanoDuration's symmetry break again, between two
implementations of one type: FasterQueue's equals counts the
LinkedQueue as equal, and LinkedQueue's, still Object's,
counts nothing but itself. A client of HelpQueue that relied on
the specification, say by keeping queues in a HashSet, can now
be wrong when a LinkedQueue reaches it. So the promise in the
interface rests on every class that implements it: the two here,
and every one written later. Each of them has to write both
methods from the abstract value, and javac does not check that
it did.
A queue that changes. Now the last two lines. q2.join("dan")
changed q2: it stands for the queue bob, eve, dan now, while
q1 still stands for bob, eve. So the fourth line,
q1.equals(q2), prints false, as it should. The fifth line is
the surprise. set.contains(q2) prints false, and q2 is the
only object in the set. Here is what happened, step by step:
- When
set.add(q2)ran, the set askedq2for its hash code.q2stood for bob, eve, sohashCodereturned the hash code of that list, and the set filedq2under that number. - Then dan joined. The set was not told, and it does not ask
again:
q2stays filed under the old number. - Now
set.contains(q2)asksq2for its hash code.q2stands for bob, eve, dan, sohashCodereturns the hash code of that list, a different number. - The set looks for an element filed under the new number. There
is none, so it answers
false.
The set did nothing wrong, and java.util.Set's documentation
says so:5
Note: Great care must be exercised if mutable objects are used as set elements. The behavior of a set is not specified if the value of an object is changed in a manner that affects
equalscomparisons while the object is an element in the set.
So an equals written from the abstract value has a cost on a
class whose abstract value can change. Classes you know take the
two ways around it. Duration never changes, and neither does
String, so a set of either never loses track of one: that is why
FasterQueue's set of NetIDs is safe. A StringBuilder, a
string under construction, does change, and it keeps Object's
equals, so two builders are equal only when they are one
object. List took neither way, and every set of lists carries
the warning above. A help queue is a list of students that
changes, and we gave it List's choice, so Set's warning,
quoted above, applies to it too: a queue that sits in a HashSet
must not change while it is there.
Exercises
Each answer is folded under its question. Write yours first.
1. == on two arrays asks whether they are one array. Suppose
you may not write == between two arrays. Write a method with
this specification that answers the question anyway:
/**
* @requires a and b each have at least one element
* @return whether a and b are the same array
*/
static boolean same(int[] a, int[] b) {
Why does the specification ask for an element in each array? Then
predict the five lines this main prints:
public static void main(String[] args) {
int[] a = {1, 2, 3};
int[] b = a;
int[] c = {1, 2, 3};
System.out.println("same(a, b): " + same(a, b));
System.out.println("same(a, c): " + same(a, c));
System.out.println("a[0]: " + a[0]);
System.out.println("a.equals(c): " + a.equals(c));
System.out.println("Arrays.equals(a, c): " + Arrays.equals(a, c));
}
Answer
-
A change to one array shows in the other exactly when they are one array. So
samechangesaand looks atb:Same.javastatic boolean same(int[] a, int[] b) { int saved = a[0]; a[0] = b[0] + 1; boolean same = b[0] == a[0]; a[0] = saved; return same; }It saves
a's first element, writes intoa[0]a value thatb[0]does not hold, and readsb[0]. Ifaandbare one array, the read sees the new value. If they are two,b[0]is as it was. Thensameputsa's element back, so the caller's arrays are as they were. (The==inb[0] == a[0]compares twoints, not two arrays.) -
Two empty arrays have no element to change. That is why the specification requires one in each.
-
The run.
bis a second name for the arrayanames, andcis a second array with the same elements:$ javac Same.java $ java Same same(a, b): true same(a, c): false a[0]: 1 a.equals(c): false Arrays.equals(a, c): trueThe last two lines are the lecture's question asked of arrays. An array's
equalsisObject's, soa.equals(c)isfalsefor two arrays with the same elements. The comparison by value has another name,Arrays.equals, which compares the elements in order. -
This is why a class whose objects change may keep
Object'sequals. Two changing objects with the same contents can be told apart, by changing one, assamejust did. Two objects that never change can be told apart only by==.
2. Object's specification says that equal objects have equal
hash codes. Take two objects a and b of some class that keeps
that specification, not Duration in particular, and ask two
questions of them: does a.equals(b) return true, and is
a.hashCode() the same int as b.hashCode()? There are four
ways the two answers can come out. Which of the four can a
program observe, and which can it not?6
Answer
-
Three of the four. The one the specification forbids is
equalstrue with the hash codes different:a.equals(b)hash codes equal can a program observe it? trueyes yes: d1andd2inDuration's last runtrueno no: equal objects must have equal hash codes falseyes yes: two values can share a hash code falseno yes: the usual case for two unequal objects -
falsewith equal hash codes is allowed.hashCode's specification does not require unequal objects to have distinct hash codes, and it could not: a hash code is oneint, and most types have more values than there areints. AHashSetallows for it by callingequalson what it finds in a bucket with the same hash code. -
truewith different hash codes is the bug the lecture met. The override without ahashCodeprintedd1.equals(d2)astruewhiled1andd2reported different hash codes, and aHashSetholding one could not find the other.
3. Here is one more subclass of Duration. It adds nothing to
its objects. The class counts how many of them have been made:
/** An immutable span of time. The class counts the objects it has made. */
public class CountedDuration extends Duration {
// How many CountedDurations have been made: a fact about the
// class, not part of any object's abstract value.
private static int created = 0;
/**
* @requires 0 <= min and 0 <= sec
* @effects makes the span of min minutes and sec seconds
*/
public CountedDuration(int min, int sec) {
super(min, sec);
created++;
}
/** @return how many CountedDurations have been made */
public static int created() {
return created;
}
public static void main(String[] args) {
Duration d = new Duration(5, 10);
CountedDuration cd = new CountedDuration(5, 10);
System.out.println("d.equals(cd): " + d.equals(cd));
System.out.println("cd.equals(d): " + cd.equals(d));
System.out.println("CountedDuration.created(): " + created());
}
}
Should CountedDuration override equals? Is it a behavioral
subtype of Duration, where NanoDuration was not? Predict the
three lines main prints.
Answer
-
No:
Duration'sequalsis already right for it. ACountedDurationstands for a span of whole seconds, the same kind of value as aDuration, and the count belongs to the class, not to any object.Duration'sequalsasksinstanceof Duration, which aCountedDurationis, and comparesseconds(), so it gives the right answer in both directions.hashCode, inherited too, is the length, socdlands where an equalDurationwould in aHashSet. Here is the run:$ javac Duration.java CountedDuration.java $ java CountedDuration d.equals(cd): true cd.equals(d): true CountedDuration.created(): 1 -
The
NanoDurationargument does not apply.NanoDurationadded to the abstract value, a span of nanoseconds, so "the same value" had no answer between it and aDuration.CountedDurationadds nothing to the abstract value. WithgetClassinDuration'sequals,cdwould never equald, though the two stand for the same span: that is the cost the section When no answer exists named. -
It is a behavioral subtype. It overrides nothing, so every method keeps
Duration's promises word for word, and its objects never change, as aDuration's do not. -
The count is 1.
mainmade oneCountedDuration.dis aDurationand not aCountedDuration, so the class did not count it.
Further reading
- The contract, and why a subclass cannot add to the value.
Joshua Bloch, Effective Java, third edition (2018), chapter
3, "Methods Common to All Objects", items 10 and 11. Item 10,
"Obey the general contract when overriding equals", works
through the five promises with examples, shows that no subclass
of an instantiable class can add to the value and keep the
contract, the trouble of the section
When no answer exists, weighs the
getClasscheck and rejects it, and arrives at the composition this lecture used. Item 11, "Always override hashCode when you override equals", is the rule of the section The override, and the hash code, with a recipe for a hash code built from several fields. - Equality as the abstraction function. MIT 6.031, Software Construction, Reading 15, "Equality" (2021). It defines equality of an immutable type as AF(a) = AF(b), the definition box's idea in symbols, and names two notions of equality for a type whose objects change, observational and behavioral, that this lecture left unnamed.
- Mutable against immutable. Barbara Liskov and John Guttag,
Program Development in Java (2001), chapter 5, "Data
Abstraction", section 5.4, "Additional Methods". Its rule for
equalsis the strict form of what the last section weighed: mutable objects are equal only when they are the same object, and such a type may inheritObject'sequals; immutable objects are equal when they have the same state, and such a type must write its own. Its similarity, the same state now, is the weaker notion a changing queue'sequalscomputes.
-
The documentation of
Object.equalsin Java 25. Its first sentence puts "equal to" in quotation marks:Objectleaves the meaning to each class. ↩ -
Michael Ernst's lecture on equality for this course (2018) shows
Durationwith its microwave representation and theequalsthat takes aDuration. ↩ -
The documentation of
Object.hashCodein Java 25. ↩ -
The documentation of
List.equalsin Java 25. ↩ -
The documentation of
java.util.Setin Java 25, in its description of the interface. ↩ -
Exercises 2 and 3 follow Michael Ernst's: a question on his first midterm for this course (2018) asked which combinations of
equals,==,hashCode, andtoStringa correct program can produce, and his lecture on equality that year gaveCountedDuration. ↩