Using methods common to all objects
This article is part of our Academy Course titled Advanced Java.
This course is designed to help you make the most effective use of Java. It discusses advanced topics, including object creation, concurrency, serialization, reflection and many more. It will guide you through your journey to Java mastery! Check it out here!
Table Of Contents
1. Introduction
From part 1 of the tutorial, How to create and destroy objects, we already know that Java is an object-oriented language (however, not a pure object-oriented one). On top of the Java class hierarchy sits the Object
class and every single class in Java implicitly is inherited from it. As such, all classes inherit the set of methods declared in Object
class, most importantly the following ones:
In this part of the tutorial we are going to look at equals
,hashCode
,toString
and clone
methods, their usage and important constraints to keep in mind.
2. Methods equals and hashCode
By default, any two object references (or class instance references) in Java are equal only if they are referring to the same memory location (reference equality). But Java allows classes to define their own equality rules by overriding the equals()
method of the Object
class. It sounds like a powerful concept, however the correct equals()
method implementation should conform to a set of rules and satisfy the following constraints:
- Reflexive. Object x must be equal to itself and equals(x) must return true.
- Symmetric. If equals(y) returns true then y.equals(x) must also return true.
- Transitive. If equals(y) returns true and y.equals(z) returns true, then x.equals(z) must also return true.
- Consistent. Multiple invocation of equals() method must result into the same value, unless any of the properties used for equality comparison are modified.
- Equals To Null. The result of equals(null) must be always false.
Unfortunately, the Java compiler is not able to enforce those constraints during the compilation process. However, not following these rules may cause very weird and hard to troubleshoot issues. The general advice is this: if you ever are going to write your own equals()
method implementation, think twice if you really need it. Now, armed with all these rules, let us write a simple implementation of the equals()
method for the Person
class.
package com.javacodegeeks.advanced.objects; public class Person { private final String firstName; private final String lastName; private final String email; public Person( final String firstName, final String lastName, final String email ) { this.firstName = firstName; this.lastName = lastName; this.email = email; } public String getEmail() { return email; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } // Step 0: Please add the @Override annotation, it will ensure that your // intention is to change the default implementation. @Override public boolean equals( Object obj ) { // Step 1: Check if the 'obj' is null if ( obj == null ) { return false; } // Step 2: Check if the 'obj' is pointing to the this instance if ( this == obj ) { return true; } // Step 3: Check classes equality. Note of caution here: please do not use the // 'instanceof' operator unless class is declared as final. It may cause // an issues within class hierarchies. if ( getClass() != obj.getClass() ) { return false; } // Step 4: Check individual fields equality final Person other = (Person) obj; if ( email == null ) { if ( other.email != null ) { return false; } } else if( !email.equals( other.email ) ) { return false; } if ( firstName == null ) { if ( other.firstName != null ) { return false; } } else if ( !firstName.equals( other.firstName ) ) { return false; } if ( lastName == null ) { if ( other.lastName != null ) { return false; } } else if ( !lastName.equals( other.lastName ) ) { return false; } return true; } }
It is not by accident that this section also includes the hashCode()
method in its title. The last, but not least, rule to remember: whenever you override equals()
method, always override the hashCode()
method as well. If for any two objects the equals()
method returns true, then the hashCode()
method on each of those two objects must return the same integer value (however the opposite statement is not as strict: if for any two objects the equals()
method returns false, the hashCode()
method on each of those two objects may or may not return the same integer value). Let us take a look on hashCode()
method for the Person
class.
// Please add the @Override annotation, it will ensure that your // intention is to change the default implementation. @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ( ( email == null ) ? 0 : email.hashCode() ); result = prime * result + ( ( firstName == null ) ? 0 : firstName.hashCode() ); result = prime * result + ( ( lastName == null ) ? 0 : lastName.hashCode() ); return result; }
To protect yourself from surprises, whenever possible try to use final
fields while implementing equals()
and hashCode()
. It will guarantee that behavior of those methods will not be affected by the field changes (however, in real-world projects it is not always possible).
Finally, always make sure that the same fields are used within implementation of equals()
and hashCode()
methods. It will guarantee consistent behavior of both methods in case of any change affecting the fields in question.
3. Method toString
The toString()
is arguably the most interesting method among the others and is being overridden more frequently. Its purpose is it to provide the string representation of the object (class instance). The properly written toString()
method can greatly simplify debugging and troubleshooting of the issues in real-live systems.
The default toString()
implementation is not very useful in most cases and just returns the full class name and object hash code, separated by @
, f.e.:
com.javacodegeeks.advanced.objects.Person@6104e2ee
Let us try to improve the implementation and override the toString()
method for our Person class example. Here is a one of the ways to make toString()
more useful.
// Please add the @Override annotation, it will ensure that your // intention is to change the default implementation. @Override public String toString() { return String.format( "%s[email=%s, first name=%s, last name=%s]", getClass().getSimpleName(), email, firstName, lastName ); }
Now, the toString()
method provides the string version of the Person
class instance with all its fields included. For example, while executing the code snippet below:
final Person person = new Person( "John", "Smith", "john.smith@domain.com" ); System.out.println( person.toString() );
The following output will be printed out in the console:
Person[email=john.smith@domain.com, first name=John, last name=Smith]
Unfortunately, the standard Java library has a limited support to simplify toString()
method implementations, notably, the most useful methods are Objects.toString()
, Arrays.toString() / Arrays.deepToString()
. Let us take a look on the Office
class and its possible toString()
implementation.
package com.javacodegeeks.advanced.objects; import java.util.Arrays; public class Office { private Person[] persons; public Office( Person ... persons ) { this.persons = Arrays.copyOf( persons, persons.length ); } @Override public String toString() { return String.format( "%s{persons=%s}", getClass().getSimpleName(), Arrays.toString( persons ) ); } public Person[] getPersons() { return persons; } }
The following output will be printed out in the console (as we can see the Person
class instances are properly converted to string as well):
Office{persons=[Person[email=john.smith@domain.com, first name=John, last name=Smith]]}
The Java community has developed a couple of quite comprehensive libraries which help a lot to make toString()
implementations painless and easy. Among those are Google Guava's
Objects.toStringHelper and Apache Commons Lang
ToStringBuilder.
4. Method clone
If there is a method with a bad reputation in Java, it is definitely clone()
. Its purpose is very clear – return the exact copy of the class instance it is being called on, however there are a couple of reasons why it is not as easy as it sounds.
First of all, in case you have decided to implement your own clone()
method, there are a lot of conventions to follow as stated in Java documentation. Secondly, the method is declared protected
in Object
class so in order to make it visible, it should be overridden as public
with return type of the overriding class itself. Thirdly, the overriding class should implement the Cloneable
interface (which is just a marker or mixin interface with no methods defined) otherwise CloneNotSupportedException
exception will be raised. And lastly, the implementation should call super.clone()
first and then perform additional actions if needed. Let us see how it could be implemented for our sample Person
class.
public class Person implements Cloneable { // Please add the @Override annotation, it will ensure that your // intention is to change the default implementation. @Override public Person clone() throws CloneNotSupportedException { return ( Person )super.clone(); } }
The implementation looks quite simple and straightforward, so what could go wrong here? Couple of things, actually. While the cloning of the class instance is being performed, no class constructor is being called. The consequence of such a behavior is that unintentional data sharing may come out. Let us consider the following example of the Office
class, introduced in previous section:
package com.javacodegeeks.advanced.objects; import java.util.Arrays; public class Office implements Cloneable { private Person[] persons; public Office( Person ... persons ) { this.persons = Arrays.copyOf( persons, persons.length ); } @Override public Office clone() throws CloneNotSupportedException { return ( Office )super.clone(); } public Person[] getPersons() { return persons; } }
In this implementation, all the clones of the Office
class instance will share the same persons array, which is unlikely the desired behavior. A bit of work should be done in order to make the clone()
implementation to do the right thing.
@Override public Office clone() throws CloneNotSupportedException { final Office clone = ( Office )super.clone(); clone.persons = persons.clone(); return clone; }
It looks better now but even this implementation is very fragile as making the persons field to be final
will lead to the same data sharing issues (as final
cannot be reassigned).
By and large, if you would like to make exact copies of your classes, probably it is better to avoid clone()
/ Cloneable
and use much simpler alternatives (for example, copying constructor, quite familiar concept to developers with C++ background, or factory method, a useful construction pattern we have discussed in part 1 of the tutorial, How to create and destroy objects).
5. Method equals and == operator
There is an interesting relation between Java ==
operator and equals() method which causes a lot of issues and confusion. In most cases (except comparing primitive types), == operator performs referential equality: it returns true if both references point to the same object, and false otherwise. Let us take a look on a simple example which illustrates the differences:
final String str1 = new String( "bbb" ); System.out.println( "Using == operator: " + ( str1 == "bbb" ) ); System.out.println( "Using equals() method: " + str1.equals( "bbb" ) );
From the human being prospective, there are no differences between str1==”bbb” and str1.equals(“bbb”): in both cases the result should be the same as str1 is just a reference to “bbb” string. But in Java it is not the case:
Using == operator: false Using equals() method: true
Even if both strings look exactly the same, in this particular example they exist as two different string instances. As a rule of thumb, if you deal with object references, always use the equals()
or Objects.equals()
(see please next section Useful helper classes for more details) to compare for equality, unless you really have an intention to compare if object references are pointing to the same instance.
6. Useful helper classes
Since the release of Java 7, there is a couple of very useful helper classes included with the standard Java library. One of them is class Objects
. In particular, the following three methods can greatly simplify your own equals()
and hashCode()
method implementations.
If we rewrite equals()
and hashCode()
method for our Person
’s class example using these helper methods, the amount of the code is going to be significantly smaller, plus the code becomes much more readable.
@Override public boolean equals( Object obj ) { if ( obj == null ) { return false; } if ( this == obj ) { return true; } if ( getClass() != obj.getClass() ) { return false; } final PersonObjects other = (PersonObjects) obj; if( !Objects.equals( email, other.email ) ) { return false; } else if( !Objects.equals( firstName, other.firstName ) ) { return false; } else if( !Objects.equals( lastName, other.lastName ) ) { return false; } return true; } @Override public int hashCode() { return Objects.hash( email, firstName, lastName ); }
7. Download the Source Code
- You may download the source code here: advanced-java-part-2
8. What’s next
In this section we have covered the Object
class which is the foundation of object-oriented programming in Java. We have seen how each class may override methods inherited from Object
class and impose its own equality rules. In the next section we are going to switch our gears from coding and discuss how to properly design your classes and interfaces.
Hi, Regarding point number 6… Would it not be more in line with OO paradigm to write this method in the following manner: @Override public boolean equals( Object obj ) { final PersonObjects other = (PersonObjects) obj; boolean retValue = true; if ( obj == null ) { retValue = false; } else if ( this == obj ) { retValue = true; } else if ( getClass() != obj.getClass() ) { retValue = false; } else if( !Objects.equals( email, other.email ) ) { retValue = false; } else if( !Objects.equals( firstName, other.firstName ) ) { retValue = false; }… Read more »
Hi,
Thank you very much for your question. It is never ending debate in the community about single vs multiple return statements. My personal opinion on that is simple: if for you as for a reader of code it is clear, easy to follow and understandable what the method does and what is going to be returned, than it does not matter much how many return statements are there. However there is no relation to OOP in this case, more to imperative programming styles.
Thank you.
Best Regards,
Andriy Redko
somethings different:
String a = “Ala”;
System.out.println(“a == Ala : ” + (a == “Ala”));
Hi Andriy,
Are practical exercises available somewhere for this course?
Thanks,
Iryna
Thanks for your tuto, I am still finding it difficult to grasp the difference between == and equals() concerning object