Now that we’ve created plenty of objects in Java and we’ve seen several data structures, we need to address a very large elephant in the room. I still haven’t told you how to check—easily, anyway—if two objects are “equal”, or what that means about a non-primitive, anyway.
Let’s start with the easy stuff—equality in primitives:
- Integers are equal if their values are the same. 2 is equal to 2, but not to 327.
- Doubles are equal if their values are the same: 3.14159 is equal to 3.14159, but not 3.15
- And so on with shorts, bytes, longs, and floats
- Characters are equal if they’re the same: ‘A’ is equal to ‘A’, but not ‘a’
- Booleans are equal if they have the same value: false and false are equal, but false and true are not
Because they’re primitives, we learned we make comparisons
with == (or !=, or whatever you want), since single = is used for assignment,
so it can’t be lifted directly from math.
Objects are a little trickier. Equality by = still works, but there’s a catch.
That checks if the memory addresses of two things are the
same.
So let’s suppose I have
String s1 = “hello”;
and
String s2 = “hello”;
Intuitively, there’s nothing different about s1 and s2. Both
have exactly the same contents, an all-lowercase “hello” and nothing else.
But Java doesn’t see those as the same. s1 and s2 are different objects, stored
in different places in memory, so their addresses are different, so
s1==s2 would return false.
Now, in Java, we have a concept called aliasing.
Let’s say we do the following instead:
- We create String s1 = “hello”; as before
- But now, instead, we sat String s2 = s1;
Now, s2 is an alias of s1—s2 and s1 point to the same location in memory.
Suppose s2 then changes to “oi” instead. After this change, s1 points to “hello”
(nothing changed), but s2 points to the brand new, different, in a different
place, “oi”.
Let’s see that in action:
class Main {
public static void
main(String[] args) {
String s1 =
"hello";
// s2 points to the same place
String s2 = s1;
// hello
System.out.println(s1);
// hello
System.out.println(s2);
s2 = "oi";
// hello
System.out.println(s1);
// oi
System.out.println(s2);
}
}
While it is still true that s1 and s2 point to the same place (before a new String
is created, when s2 becomes “oi”), then s1==s2 would return true. But once the
value of s2 changes, s1 and s2 no longer point to the same place, so s1==s2
would return false.
Without aliasing, let’s consider the following scenario:
class Main {
public static void
main(String[] args) {
String s1 =
"hello";
String s2 =
"hello";
}
}
In this case, is it true that s1==s2? Again, it feels like it should be true,
but it’s false. By not saying s2=s1 as before, we are telling the computer that
s1 and s2 are completely different objects, which we don’t intend to point to
the same place in memory.
But we, wise humans who can read and know there’s more to the story, know the
contents of the Strings are identical. Surely, then, there must be a way to
check “even if the Strings don’t have the same address, are they equal in the sense
that every character in one matches every character in the other?”
In fact, most of the time, you won’t care where things are stored in memory—after
all, this
isn’t C, so you aren’t allocating and freeing memory yourself, leaving that
all to Java. What you’ll most likely want to know is whether the contents of a
String are equal to that of another.
For that, we have the equals() method.
equals() isn’t unique to Strings,
however. All objects—whether you created them, or someone else did, like String,
or Random,
or anything else—since they descend from the Object class (this is never stated
explicitly, but it’s always true) inherit some of its methods. If you didn’t
when we covered this fact when we looked at inheritance,
then I suggest you look at the API
documentation now. You’ll see that one of the methods in Object—thus inherited
by every class, whether you use it or not—is the equals() method.
Recall the difference between overloading and overriding. Overloading is, for
example, having an add() method that takes in 2 ints, another that takes in 2
doubles, another for 2 bytes, another for 2 longs, etc. Overriding, meanwhile,
is changing the behavior of whatever you inherited, to better fit the needs of
the child class. The default behavior is exactly what we saw earlier, with the
equality (or not) of the addresses of “hi”.
Since you’ve inherited that method from Object, you can, of course, override it
to better suit your needs. String has done this already, and String’s equals()
checks if the contents are equal. If every character in two
Strings is the same, they are the same.
Note that, for example, “I LIKE PIZZA” is not equal to “I like pizza” or “i
like pizza” or “ I LIKE PIZZA” or “I LIKE PIZZA ”. “I LIKE PIZZA” matches
exactly only with “I LIKE PIZZA” and only in that instance would equals() return
true.
Suppose you have a Student class and you want to check if two Students are the
same. Suppose a Student has an ID, name, date of birth, year, GPA, and major.
You could certainly plausibly override the equals() in Student to return true if
and only if all those fields—ID, name, date of birth, year, GPA, and major—all are
exactly the same. You may have "the same" Student in different places in memory, so what you care about are the stats, not the memory addresses. If they are all the same, then presumably you and your program would both
conclude that the Students are the same. But if any of those properties mismatch,
you’re looking at different Students.
How you define equals() is largely up to you. Just make sure that whatever
criteria you pick make sense and are specific enough to get the definition of
equality you want—and be aware that, if, in the course of comparing Puppy
objects, you need to check if their owner Strings are the same, then leading
and trailing spaces, or capitalization, all matter when detetmining equality or
not.
No comments:
Post a Comment