equlas, hashCode and toString method

Introduction

The Object class defines the equals(), hashCode() and toString() methods. As all the classes are child classes of the Object class, these three methods are available in every Java class. Below is the source code of the Object class. We have removed all other methods for simplicity.

public class Object {

    public native int hashCode();

    public boolean equals(Object obj) {
        return (this == obj);
    }

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
}
equals() method

Let’s first create a CurrencyNote class having two attributes value and currencyCode.

public class CurrencyNote {
    private final int value;
    private final String currencyCode;
    
    public CurrencyNote(int value, String currencyCode) {
        this.value = value;
        this.currencyCode = currencyCode;
    }

    public int getValue() {
        return value;
    }

    public String getCurrencyCode() {
        return currencyCode;
    }
}

Now if you try –

CurrencyNote note1 = new CurrencyNote(100, "INR");
CurrencyNote note2 = new CurrencyNote(100, "INR");
System.out.println(note1.equals(note2));

It will print false. You may expect as both are 100 rupee notes, they should be equal. To understand why that’s not happening, let’s revisit the source code of equals() method in Object class.

public class Object {
    public boolean equals(Object obj) {
        return (this == obj);
    }
}

From the source code we can see, note1.equals(note2) internally returns the value of note1 == note2.

When we use equality operator (==)

  1. For primitive data types, it checks the value.
  2. For objects, it checks if both objects are referring to the same object in the heap.

In our example, as note1 and note2 referring to different objects in the heap, note1 == note2 is returning false.

But, you might want to treat both notes as equals as the value is the same. To achieve that, you have to override the equals() method and compare both value and currency code. So, let’s update our CurrencyNote class as below –

public class CurrencyNote {
    private final int value;
    private final String currencyCode;
    
    public CurrencyNote(int value, String currencyCode) {
        super();
        this.value = value;
        this.currencyCode = currencyCode;
    }
    
    @Override
    public boolean equals(Object obj) {
        CurrencyNote otherNote = (CurrencyNote) obj;
        return this.value == otherNote.value && this.currencyCode.equals(otherNote.currencyCode);
    }
}

Now if you try below, it will return true.

CurrencyNote note1 = new CurrencyNote(100, "INR");
CurrencyNote note2 = new CurrencyNote(100, "INR");
System.out.println(note1.equals(note2));

As you can see, if we want to check the equality of objects based on the property values, we should override the equals() method.

The equals() method Contract

When we override the equals() method, we must fulfill certain criteria. Almost all the time, you don’t have to write anything extra as those contracts are sort of common sense. The contract says, equals() method must be –

  1. Reflexive: an object must equal itself. So in our example, note1.equals(note1) must return true.
  2. Symmetric: note1.equals(note2) must return the same result as note2.equals(note1).
  3. Transitive: if note1.equals(note2) is true and note2.equals(note3) is also true, then note1.equals(note3) must be true.
  4. consistent: multiple invocations of note1.equals(note2) should consistently return true or consistently return false, provided value and currencyCode for both note1 and note2 is not modified.
hashCode() method

We have also seen that hashCode() method present in Object class. So, this method is also available to each and every class.

public class Object {
    public native int hashCode();
}

The hashCode() method defined by class Object does return distinct integers for distinct objects. This is typically implemented by converting the internal address of the object into an integer.

You might wonder, what is the meaning of native keyword? The native keyword is applied to a method to indicate that the method is implemented in a language other than java. So, in our case, we can say, the default implementation of hashCode() method in Object class is not written in java.

Coming back to the hashCode() method, you might ask – what is the purpose of this method? This is not doing anything other than returning an integer value.

The answer is, all hash-based collections like HashMap, HashSet etc. use this hashCode value to quickly locate the object in the collection. When we deal with those hash-based collections, we should first override the hashCode() method. Now the question is, what value should we return?

First of all we have to follow the contract between equals() and hashCode() method –

  1. If two objects are equal, then they must have the same hash code.
  2. If two objects have the same hash code, they may or may not be equal.

Most of the time, you should use all the instance variables that are being used in equals method. In our case, we should use both value and currencyCode attributes. Below is one sample example –

public class CurrencyNote {
    private final int value;
    private final String currencyCode;
       
    @Override
    public int hashCode() {
        return currencyCode.hashCode() + value;
    }
}

There are better ways to write the hashCode() method here, but the point here is, whatever field we have used in equal method, for each field, we have calculated an int value against each of them and combined those value in some way (in our case we have added them) and returned the value. Please remember, when we are dealing with hash-based collections, we must always override equals() and hashCode() methods.

Now, let’s try to understand how the hashCode() method helps to search an element quickly. First create a list and try to search for an element.

List<String> animals = Arrays.asList("Cat", "Bat", "Zebra", "Lion", "Tiger");
boolean isTigerPresent = animals.contains("Tiger");
System.out.println(isTigerPresent);

This will definitely return true. But the problem here is, this is extremely inefficient. List always starts from the first element and looks for the element linearly one by one. So, if the list is huge and the element is towards the end, the contains() method will take a lot of time.

Though we will discuss HashSet separately, just to give you the idea, HashSet stores elements in a sequence of buckets. When we add an element to a set, first the bucket is identified based on hashCode() value of the element and then the element is added to that bucket. So, one bucket may contain more than one element. Though the actual implementation of the hashCode() method of String class is different, just for simplicity, let’s assume, the hashCode() method returns the number of characters the string contains. Again I am repeating, this is not the actual behavior, just to understand the concept, we are assuming this. Also consider, based on the hashCode value, a bucket at that position is chosen.

Now, use HashSet instead of List.

HashSet<String> animals = new HashSet(Arrays.asList("Cat", "Bat", "Zebra", "Lion", "Tiger"));

If you try animals.contains("Tiger"), rather than searching linearly, HashSet will first calculate the hashCode value. As the number of characters is 5, hashCode value will return 5. So, HashSet will directly go to the fifth bucket and only compare the elements present in that bucket. As Tiger is present in that bucket, the result will be true.

So, you see, the hashCode() method is extremely helpful in narrowing down the search and that in turn results in better performance.

One quick tip, if you are using IDE like Eclipse, to generate equals() and hashCode() method, you can just right click on a class file and navigate to Source > Generate hashCode() and equals()... to generate those two methods automatically.

toString()

The purpose of the toString() method is to return a textual representation of an object. The result should be concise but informative.

Let’s consider our CurrencyNote class –

public class CurrencyNote {
    private final int value;
    private final String currencyCode;
    
    public CurrencyNote(int value, String currencyCode) {
        this.value = value;
        this.currencyCode = currencyCode;
    }
}

If you call the toString() method on an object of this class, it will return some useless text.

CurrencyNote note = new CurrencyNote(100, "INR");
System.out.println(note.toString());

Output: CurrencyNote@7852e922

In fact, if you don’t explicitly call the toString() method and just pass the object to println() method, System.out.println() will call the toString() method of the input object to convert the object to a string representation. So, below code will result same output –

CurrencyNote note = new CurrencyNote(100, "INR");
System.out.println(note);

Output: CurrencyNote@7852e922

To understand this weird output, let’s look at the source code in Object class –

public class Object {
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
}

As you can see, this implementation will print: class name, then @ character and then the unsigned hexadecimal representation of the hash code of the object. This is exactly what happened in our example.

But this is not very helpful. To get a better output, we should override the toString() method. For example, we can rewrite our class as below –

public class CurrencyNote {
    private final int value;
    private final String currencyCode;
    
    public CurrencyNote(int value, String currencyCode) {
        this.value = value;
        this.currencyCode = currencyCode;
    }    
    
    @Override
    public String toString() {
        return value + " " + currencyCode;
    }
}

If we pass the object in the println() method, it will now print much useful information.

CurrencyNote note = new CurrencyNote(100, "INR");
System.out.println(note);

Output: 100 INR

So you can see, we can override the toString() method to print useful information about an object.

That’s it for now. Hope you have enjoyed this tutorial. If you have any doubt, please ask in the comment section. I will try to answer that as soon as possible. Till then, bye bye.