12.4 Object Methods
All classes inherit from the overarching Object class. The methods that are inherited are as follows:
String toString()
boolean equals(Object obj)
Class <?> getClass()
int hashCode()
protected Objectclone()
protected void finalize()
void notify()
void notifyAll()
void wait()
void wait(long timeout)
void wait(long timeout, int nanos)
We are going to focus on the first two in this chapter. We will take advantage of inheritance to override these two methods in our classes to behave in the ways we want them to.
toString()
The toString()
method provides a string representation of an object. The System.out.println()
function implicitly calls this method on whatever object is passed to it and prints the string returned. When you run System.out.println(dog)
, it's actually doing this:
The default Object
class' toString()
method prints the location of the object in memory. This is a hexadecimal string. Classes like Arraylist and java arrays have their own overridden versions of the toString()
method. This is why, when you were working with and writing tests for Arraylist, errors would always return the list in a nice format like this (1, 2, 3, 4) instead of returning the memory location.
For classes that we've written by ourselves like ArrayDeque
, LinkedListDeque
, etc, we need to provide our own toString()
method if we want to be able to see the objects printed in a readable format.
Let's try to write this method for an ArraySet
class. Read the ArraySet
class below and make sure you understand what the various methods do. Feel free to plug the code into java visualizer to get a better understanding!
You can find the solutions here (ArraySet.java)
Exercise 6.4.1: Write the toString() method so that when we print an ArraySet, it prints the elements separated by commas inside of curly braces. i.e {1, 2, 3, 4}. Remember, the toString() method should return a string.
Solution
This solution, although seemingly simple and elegant, is actually very naive. This is because when you use string concatenation in Java like so: returnString += keys[i];
you are actually not just appending to returnString
, you are creating an entirely new string. This is incredibly inefficient because creating a new string object takes time too! Specifically, linear in the length of the string (we'll discuss why in a later chapter!).
Bonus Question: Let's say concatenating one character to a string takes 1 second. If we have an ArraySet of size 5: {1, 2, 3, 4, 5}
, how long would it take to run the toString()
method?
Answer: We set returnString
to the left bracket which takes one second because this involves adding {
to the empty string ""
. Adding the first element will involve creating an entirely new string, adding } and 1 which would take 2 seconds. Adding the second element takes 3 seconds because we need to add {
, 1
, 2
. This process continues, so for the entire array set the total time is 1 + 2 + 3 + 4 + 5 + 6 + 7.
To remedy this, Java has a special class called StringBuilder
. It creates a string object that is mutable, so you can continue appending to the same string object instead of creating a new one each time.
Exercise 6.4.2: Rewrite the toString() method using StringBuilder.
Solution
Now you've successfully overridden the toString()
method! Try printing the ArraySet to see the fruits of your work.
Next we will override another important object method: equals()
equals()
equals()
and ==
have different behaviors in Java. ==
Checks if two objects are actually the same object in memory. Remember, pass-by-value! ==
checks if two boxes hold the same thing. For primitives, this means checking if the values are equal. For objects, this means checking if the address/pointer is equal.
Say we have this Doge
class:
If we plug this code into the java visualizer, we will see the box in pointer diagram shown below.
Exercise 6.4.3: What would java return if we ran the following?
x == y
x == z
fido == doggo
fido == fidoTwin
fido == fidoRealTwin
Answers
True
False
False
False
True
fido
and fidoTwin
are not considered ==
because they point to different objects. However, this is quite silly since all their attributes are the same. You can see how ==
can cause some problems in Java testing. When we write tests for our ArrayList and want to check if expected is the same as what is returned by our function, we create expected as a new arraylist. If we used ==
in our test, it would always return false. This is what equals(Object o)
is for.
equals(Object o)
equals(Object o)
equals(Object o)
is a method in the Object class that, by default, acts like == in that it checks if the memory address of the this is the same as o. However, we can override it to define equality in whichever way we wish! For example, for two Arraylists to be considered equal, they just need to have the same elements in the same order.
Exercise 6.4.4: Let's write an equals method for the ArraySet class. Remember, a set is an unordered collection of unique elements. So, for two sets to be considered equal, you just need to check if they have the same elements.
Solution
We added a few checks in the beginning of the method to make sure our .equals()
can handle nulls and objects of a different class. We also optimized the function by returning true right away if the == methods returns true. This way, we avoid the extra work of iterating through the set.
Rules for Equals in Java: Overriding a .equals()
method may sometimes be trickier than it seems. A couple of rules to adhere to while implementing your .equals()
method are as follows:
1.) equals
must be an equivalence relation
reflexive:
x.equals(x)
is truesymmetric:
x.equals(y)
if and only ify.equals(x)
transitive:
x.equals(y)
andy.equals(z)
impliesx.equals(z)
2.) It must take an Object argument, in order to override the original .equals()
method
3.) It must be consistent if x.equals(y)
, then as long as x
and y
remain unchanged: x
must continue to equal y
4.) It is never true for null x.equals(null)
must be false
Bonus video
Create an even better toString
method and ArraySet.of
:
Link to the bonus code
Last updated