Advertisements

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:

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:

Udemy offer
  1. Check whether the Object in the input parameter is of the same Class type as the instance that the equals() method is invoked on.
  2. 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.
  3. 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:

  1. Create a int result and assign a non-zero value.
  2. 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.
  3. Combine the hash value c with result:
    result = 37 * result + c
  4. 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:

toString() generation wizard in Eclipse for Person class

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:

  1. our class A is used as a field within another class B,
  2. and that field in class B can be returned by a method call,
  3. 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.

Advertisements

About Clivant

Clivant a.k.a Chai Heng enjoys composing software and building systems to serve people. He owns techcoil.com and hopes that whatever he had written and built so far had benefited people.

Udemy deals
Advertisements