Things about the Object class in Java that programmers ought to know about
After getting started with Java development, getting to know the Object
class well is one of the next steps that programmers should undertake to be proficient with the Java programming language. This post lists some points about the Object
class in Java that programmers ought to know about in order to code well in the Java programming language.
Every class that we define will extend from the Object class; directly or indirectly
Like it or not, the Object
class will be the superclass of all classes. Going by the rule of inheritance, this will mean that every class that we define in Java will inherit the following methods:
getClass()
equals()
hashCode()
toString()
clone()
- 3 different signatures of wait;
wait()
,wait(long timeout)
andwait(long timeout, int nanos)
notify()
notifyall()
finalize()
Understanding the getClass() method of the Object class
The getClass()
method returns a Class object that describes the class that we define. Through the getClass()
method, we will be able to use the facilities of Java reflection to:
- Dynamically create instances of the Class of the object that we had called
getClass()
on. - Dynamically execute the methods of the object that we had called
getClass()
on. - Discover annotations defined in the Class of the object that we had called
getClass()
on. - Discover the methods and variables in the Class of the object that we had called
getClass()
on.
Developers do not override the getClass()
method in practice.
Understanding the equals() method of the Object class
If you look into the method definition of the equals()
method defined in the Object class, you will observe that the equals()
method in the Object
class merely returned the boolean comparison of its input parameter with itself:
public boolean equals(Object obj) { return (this == obj); }
This is known as reference comparison. Hence, if you do not override the equals()
method of the Object
class, the equals()
method of your class will only return true when you pass the same object as the input parameter to the equals()
method of the object that you had invoked.
With the original implementation of the equals()
method in the Object
class, the following code will return true:
public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public static void main(String[] args) { Person john = new Person("John", 21); System.out.println(john.equals(john)); } }
and the following code will return false, even though we expect the two Person
objects to be equal since they have the same name and age:
public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public static void main(String[] args) { Person john1 = new Person("John", 21); Person john2 = new Person("John", 21); System.out.println(john1.equals(john2)); } }
When should we override the equals() method of the Object class?
In practice, if we expect that instances of the class that we define to be compared for equality, we should override the equals()
method to reflect what we intend for two separate instances of our class to be equal.
How would we override the equals() method of the Object class?
Typically the logic to override the equals() method is as follows:
- Check whether the Object in the input parameter is of the same Class type as the instance that the equals() method is invoked on.
- If the input parameter is of the same Class type, check each instance field of both instances to see if they are the same. If all the instance fields of both instances are the same, return true.
- Returns false for all other cases.
An example implementation of the equals()
method for the Person class would be as follows:
@Override public boolean equals(Object another) { if (another instanceof Person) { Person anotherPerson = (Person) another; return this.name.equals(anotherPerson.name) && this.age == anotherPerson.age; } return false; }
Understanding the hashCode() method of the Object class
The hashCode()
method is meant to return an integer representation of the instance which the hashCode()
method is invoked on. By default, the hashCode()
method of the Object
class returns the integer representation of the identifier of the instance which the hashCode()
method is invoked on. We can observe this characteristic with the following code:
public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public static void main(String[] args) { Person john1 = new Person("John"); Person john2 = new Person("John"); Person john3 = john1; System.out.println("John 1's hashCode: " + john1.hashCode()); System.out.println("John 2's hashCode: " + john2.hashCode()); System.out.println("John 3's hashCode: " + john3.hashCode()); } }
Running the above codes gave me the following output in my console terminal:
John 1's hashCode: 167772895 John 2's hashCode: 113017754 John 3's hashCode: 167772895
Since john1
and john3
variables referenced the same Person instance, the hash codes are the same. Also note that since the identifier of object instances are generated randomly, different runs of the code will result in different hash codes being printed.
When should we override the hashCode() method of the Object class
In practice, if we override the equals()
method, we should also override the hashCode()
method. If two objects are deemed to be equal by the overridden equals()
method, the integer value returned from their hashCode()
method should also be the same. However, note that two objects can return the same hash code values via their hashCode()
method even though they are not equal.
How would we override the hashCode() method of the Object class
A good implementation of the hashCode()
method will be one that will result in a proper distribution of hash values for different instances of the class that we define. One good implementation suggested by Joshua Bloch's Effective Java is as follows:
- Create a int result and assign a non-zero value.
- For every field f tested in the equals() method, calculate a hash code
c
with the following guidelines:- If the field f is a boolean: calculate (f ? 0 : 1);
- If the field f is a byte, char, short or int: calculate (int)f;
- If the field f is a long: calculate (int)(f ^ (f >>> 32));
- If the field f is a float: calculate Float.floatToIntBits(f);
- If the field f is a double: calculate Double.doubleToLongBits(f) and handle the return value like every long value;
- If the field f is an object: Use the result of the hashCode() method or 0 if f == null;
- If the field f is an array: see every field as separate element and calculate the hash value in a recursive fashion and combine the values as described next.
- Combine the hash value c with result:
result = 37 * result + c - Return result.
Going by this set of guidelines, an example implementation of the hashCode()
method for the Person class would be as follows:
@Override public int hashCode() { int result = 0; // If the field f is an object: Use the result of the hashCode() method or 0 if f == null; if (this.name != null) { // 37 * 0 is 0, so we can reduce to this assignment result = this.name.hashCode(); } // If the field f is a byte, char, short or int: calculate (int)f; result = 37 * result + this.age; return result; }
Understanding the toString() method of the Object class
While the hashCode()
method is for generating a integer representation of the instance of your class, the toString()
method is meant for generating a String representation of the instance of your class. The toString()
method that is implemented by the Object
class is as follows:
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
Hence, if we do not override the toString()
method in our class, the default String representation of the instance of our class will be the name of our class, followed by an '@' character and then the hexadecimal value of what our hashCode()
returns. If we run the following code:
public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object another) { if (another instanceof Person) { Person anotherPerson = (Person) another; return this.name.equals(anotherPerson.name) && this.age == anotherPerson.age; } return false; } @Override public int hashCode() { int result = 0; // If the field f is an object: Use the result of the hashCode() method or 0 if f == null; if (this.name != null) { // 37 * 0 is 0, so we can reduce to this assignment result = this.name.hashCode(); } // If the field f is a byte, char, short or int: calculate (int)f; result = 37 * result + this.age; return result; } public static void main(String[] args) { Person john1 = new Person("John", 21); System.out.println(john1.toString()); } }
The output would be as follows:
Person@51abb4c
When would we override the toString() method of the Object class
Typically, we would override the toString()
method of the Object class when the class that we are defining is meant for creating value objects, such as an instance of the Person
class. The overridden toString()
method of the subclass is usually invoked for logging and debugging purposes.
How would we override the toString() method of the Object class
We would typically override the toString()
method with a String concatenation of private fields that are defined in our subclass as the return value. Most Integrated Development Environment provide a feature that generate a toString()
method implementation for us. For example in Eclipse Mars, I can trigger the toString()
method generation by clicking on "Source" -> "Generate toString...". Doing so with my Person
class gave me the following wizard:
Clicking the "Ok" button gave me a toString()
method after my hashCode()
method implementation:
@Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; }
Understanding the clone() method of the Object class
The clone()
method is a facility that allows subclasses to clone a copy of the instance where the clone()
method is invoked on. By default, the clone()
method throws a CloneNotSupportedException
when the subclass does not implement the Cloneable
interface. The clone()
method in the Object
class returns a field-by-field copy of the object. If your class definition contains custom class types as fields, the field-by-field copying performed by the clone() method of the Object class will not give a true clone of the instance that the clone()
method is invoked on, as only the references of object instance fields are copied over to the cloned object.
When would we override the clone() method of the Object class?
Typically, we would override the clone() method of the Object class when:
- our class A is used as a field within another class B,
- and that field in class B can be returned by a method call,
- and we do not want any accidental edits made to that instance of class A from outside of class B.
By overriding the clone()
method of the Object class, we are giving the users of our class the ability to clone a copy of an instance via a single method call. If we do not override the clone()
method of the Object class, users of our class will have to use reflection to get a clone of our class instance when there are fields within our classes that are not accessible from outside our class.
How would we override the clone() method of the Object class?
Taking the example from Joshua Bloch's Effective Java, we would override the clone()
method in the Person
class to be as follows:
@Override public Person clone() { try { return (Person) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); // can never happen } }
This implementation would suffice as the age field is a primitive data type and the name field is a String, which is an immutable class. However, if there is a field of a custom class, we will have to clone the field and set it to the instance returned from the clone() method from the Object class. To visualize this, suppose that we have an Address
class with the following class definition:
public class Address implements Cloneable { private String street; public Address(String street) { this.street = street; } @Override public boolean equals(Object another) { if(another instanceof Address) { Address anotherAddress = (Address)another; return anotherAddress.street.equals(this.street); } return false; } @Override public int hashCode() { return street.hashCode(); } @Override public Address clone() { try { return (Address) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); // can never happen } } }
And suppose that the Person
class has a new field of type address, the implementation of the clone()
method will become:
@Override public Person clone() { try { Person person = (Person) super.clone(); person.setAddress(this.address.clone()); return person; } catch (CloneNotSupportedException e) { throw new AssertionError(); // can never happen } }
Note that the clone()
method is possible in the Person
class because the Address
class has overridden the clone()
method as well.
Understanding the the wait(), notify() and notifyAll() methods of the Object class
Every Object instance can be used as a monitor that coordinates thread execution within a code segment. When an object instance is wrapped around a synchronized block, there can only be one Thread running the synchronized code block. The wait()
, notify()
and notifyAll()
methods are thread synchronization mechanism and can only be called:
- when an object is wrapped in a synchronized block like this:
synchronized(anObjectInstance) { // Code region that can only be accessed by the Thread that owns anObjectInstance's monitor }
- and by the Thread that owns the object's monitor. A Thread that owns the object's monitor is the only one at any point in time that managed to enter one of the synchronized blocks that had wrapped around an object instance.
Function of the wait() methods
The wait()
methods allow a thread that owns the object's monitor to give up the ownership so that other threads can have a chance to enter one of the synchronized blocks that had wrapped around that object instance. Typically, we would use one of the wait()
methods if the thread that owns the object's monitor requires some condition to be fulfilled in order to proceed further. Calling the wait()
method will give some other Thread the chance to own the object's monitor; in the hope that the next running Thread will get to fulfill the condition that the current Thread needs for proceeding. After the Thread invokes a wait()
method, it will wait until another Thread invokes either the notify()
or notifyAll()
method. If some positive numeric parameters are passed into the wait()
method, the Thread will be awaken if no calls are made to either notify()
or notifyAll()
within a time duration that are indicated by the numeric parameters.
Function of the notify() and notifyAll() methods
The notify()
method triggers a notification to a Thread that is waiting for a notification while the notifyAll()
method triggers a notification to all the Threads that are waiting for a notification. The recipients of the notification can then proceed on with running codes that come after the wait()
method that caused them to wait.
Understanding the finalize() method of the Object class
The finalize()
method of the Object
class is meant for the garbage collector to call so that the instance which the finalize()
method is invoked on has chances to release any resources that it holds. Since there is no guarantee that the finalize()
method will be called promptly or even called at all, Joshua Blotch had recommended Java developers to avoid implementing the finalize()
method. Instead of relying on the finalize() method to release resources held by instances of our class, we should provide explicit methods for users of our class to release those resources when the instance of our class is no longer needed.
The toString() method is called implicitly when concatenating Strings with instances
When we "add" Object instances to Strings, the toString()
methods of the object instances will be called implicitly. For example, with the following code:
public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } public static void main(String[] args) { Person john = new Person("John", 22); Person susan = new Person("Susan", 23); System.out.println("john contains: " + john + " susan contains: " + susan); } }
We will get the following output:
john contains: Person [name=John, age=22] susan contains: Person [name=Susan, age=23]
The equals() and hashCode() methods are used when an object instance is added to and removed from a HashSet
When we add an object instance to a HashSet
, the equals()
and hashCode()
methods are utilized to determine whether the object instance is a duplicate to the existing items in the HashSet
. We can see how the equals()
and hashCode()
methods are utilized when an object instance is added to and removed from a HashSet
by running the following code:
import java.util.HashSet; public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object another) { System.out.println("equals() is called to test whether (" + this + ") equals (" + another + ")."); if (another instanceof Person) { Person anotherPerson = (Person) another; return this.name.equals(anotherPerson.name) && this.age == anotherPerson.age; } return false; } @Override public int hashCode() { System.out.println("hashCode() is called for (" + this + ")."); int result = 0; // If the field f is an object: Use the result of the hashCode() method or 0 if f == null; if (this.name != null) { // 37 * 0 is 0, so we can reduce to this assignment result = this.name.hashCode(); } // If the field f is a byte, char, short or int: calculate (int)f; result = 37 * result + this.age; return result; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } public static void main(String[] args) { HashSet<Person> personHashSet = new HashSet<Person>(); System.out.println("Adding John to hash set."); personHashSet.add(new Person("John", 22)); System.out.println("Adding Susan to hash set."); personHashSet.add(new Person("Susan", 23)); System.out.println("Adding John to hash set again."); personHashSet.add(new Person("John", 22)); System.out.println(); System.out.println("Contents in personHashSet:"); System.out.println(personHashSet); System.out.println(); personHashSet.remove(new Person("John", 22)); System.out.println(); System.out.println("Contents in personHashSet after removal:"); System.out.println(personHashSet); } }
Running the above code will give us the following output:
Adding John to hash set. hashCode() is called for (Person [name=John, age=22]). Adding Susan to hash set. hashCode() is called for (Person [name=Susan, age=23]). Adding John to hash set again. hashCode() is called for (Person [name=John, age=22]). equals() is called to test whether (Person [name=John, age=22]) equals (Person [name=John, age=22]). Contents in personHashSet: [Person [name=Susan, age=23], Person [name=John, age=22]] hashCode() is called for (Person [name=John, age=22]). equals() is called to test whether (Person [name=John, age=22]) equals (Person [name=John, age=22]). Contents in personHashSet after removal: [Person [name=Susan, age=23]]
From the output, we can observe that the first two invocation of the add
method on personHashSet
had resulted in only the hashCode()
method of the Person instance to be called. However, at the third invocation of the add()
method on personHashSet
, the equals()
method of the Person
instance was called after the call to its hashCode()
method.
Since the equals()
method on the third invocation of the add()
method on personHashSet
would have evaluated to true, we only saw two records when we print personHashSet
after the invocations of the add()
method on personHashSet
.
When we removed the record for John in personHashSet
via an invocation of the remove()
method on personHashSet
, we see that the hashCode()
method and the equals()
method of the Person
instance was called again in the same sequence as the third invocation of the add()
method on personHashSet
.
The equals() and hashCode() methods are used when an object instance is used as a key in a HashMap
When we use an object instance as a key in a HashMap
, the equals()
and hashCode()
methods will be used for certain operations of the HashMap
. To see how the equals()
and hashCode()
methods of an object instance are used in a HashMap, we can run the following code:
import java.util.HashMap; public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object another) { System.out.println("equals() is called to test whether (" + this + ") equals (" + another + ")."); if (another instanceof Person) { Person anotherPerson = (Person) another; return this.name.equals(anotherPerson.name) && this.age == anotherPerson.age; } return false; } @Override public int hashCode() { System.out.println("hashCode() is called for (" + this + ")."); int result = 0; // If the field f is an object: Use the result of the hashCode() method or 0 if f == null; if (this.name != null) { // 37 * 0 is 0, so we can reduce to this assignment result = this.name.hashCode(); } // If the field f is a byte, char, short or int: calculate (int)f; result = 37 * result + this.age; return result; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } public static void main(String[] args) { HashMap<Person, String> personHashMap = new HashMap<Person, String>(); System.out.println("Putting John inside personHashMap"); Person john = new Person("John", 22); personHashMap.put(john, "a man"); System.out.println("Putting Susan inside personHashMap"); Person susan = new Person("Susan", 23); personHashMap.put(susan, "a woman"); System.out.println("Putting John inside personHashMap again"); personHashMap.put(new Person("John", 22), "is the same man"); System.out.println(); System.out.println("Contents in personHashMap"); System.out.println(personHashMap); System.out.println(); System.out.println("Getting the value indexed by John"); System.out.println("John is " + personHashMap.get(new Person("John", 22))); System.out.println(); System.out.println("Removing John from personHashMap"); personHashMap.remove(new Person("John", 22)); System.out.println(); System.out.println("Contents in personHashMap"); System.out.println(personHashMap); System.out.println(); } }
Running the above code will give us the following output:
Putting John inside personHashMap hashCode() is called for (Person [name=John, age=22]). Putting Susan inside personHashMap hashCode() is called for (Person [name=Susan, age=23]). Putting John inside personHashMap again hashCode() is called for (Person [name=John, age=22]). equals() is called to test whether (Person [name=John, age=22]) equals (Person [name=John, age=22]). Contents in personHashMap {Person [name=Susan, age=23]=a woman, Person [name=John, age=22]=is the same man} Getting the value indexed by John hashCode() is called for (Person [name=John, age=22]). equals() is called to test whether (Person [name=John, age=22]) equals (Person [name=John, age=22]). John is is the same man Removing John from personHashMap hashCode() is called for (Person [name=John, age=22]). equals() is called to test whether (Person [name=John, age=22]) equals (Person [name=John, age=22]). Contents in personHashMap {Person [name=Susan, age=23]=a woman}
From the output, we can observe that the first two invocations of the put()
method on personHashMap
had resulted in only the hashCode()
method of the Person instance to be called. However, when it comes to putting in a new Person
instance with a duplicated as a key, the equals()
method of the Person
instance was called after a call to the hashCode()
method. When we print out the contents of personHashMap
, we can see that the duplicated record had taken over the first record that we had put into personHashMap
for John.
When we attempted to get the value indexed by John, we can see that both the hashCode()
and equals()
method were utilized in the call of the get()
method on personHashMap
.
Finally, in the removal of the record for John, we can see that both the hashCode()
and equals()
method were utilized in the call of the remove()
method on personHashMap
.