Comparing objects is often done in programming when you want
to see if two instances are the same, or you if two variables
are referring to the same object. You might also want to
find out if two objects have the same state, or the same
values in their data members. Furthermore, we often want to
compare objects to see if one is "greater than" or "less than"
another - this is the main task done in various sort algorithms.
Comaparing objects is not as simple as comparing primitives like
integers or floating point numbers. It is a complex task that
requires some extra methods.
Identity vs. Equality
When comparing objects to see if they are equal, there are two
types of comparison you can do:
identity comparison
equality comparison
Let's look at each so we can understand the difference.
Let's say we have a simple class that models a container
(e.g. a box used for shipping products a customer purchases
online):
/**
* This class models a simple container with a specific
* volume. The container must have a name and the volume
* of the container must be greater than 0.
*
* @author Wendi Jollymore
*/
public class Container {
private int id = 0;
private String name = "container";
private double volume = 1.0;
/**
* Construct a default container named "container"
* with a volume of 1.0.
*/
public Container() {}
/**
* Construct a container with a specific name and
* volume. Name can't be empty
* and volume must be greater than 0.
*
* @param name the name of this container
* @param size the programmer-specified volume
*/
public Container( String name, double size) {
// make sure name and volume are valid
setName(name);
setVolume(size);
}
/**
* Construct a container with a specific id, name, and
* volume. ID can't be 0 or less, name can't be empty
* and volume must be greater than 0.
*
* @param id the unique container ID
* @param name the name of this container
* @param size the programmer-specified volume
*/
public Container(int id, String name, double size) {
// make sure id, name, and volume are valid
setId(id);
setName(name);
setVolume(size);
}
/**
* Attempts to place a valid id into this container's
* id member. The id can't be 0 or less, otherwise an
* exception is thrown.
*
* @param name the programmer-specified container ID
* @throws IllegalArgumentException if the IDis invalid
*/
public void setId(int id) {
// make sure id isn't invalid
if (id > 0) {
this.id = id;
} else { // id is not valid
throw new IllegalArgumentException("Error: must be greater than 0.");
}
}
/**
* Retrieves the id of this container.
*
* @return this container's id
*/
public int getId() {
return id;
}
/**
* Attempts to place a valid name into this container's
* name member. The name can't be a null object and can't
* be an empty string, otherwise an exception is thrown.
*
* @param name the programmer-specified container name
* @throws IllegalArgumentException if the name is empty
*/
public void setName(String name) {
// make sure name isn't empty
if (name != null && !name.trim().isEmpty()) {
this.name = name;
} else { // name is not valid
throw new IllegalArgumentException("Error: name can't be empty.");
}
}
/**
* Retrieves the name of this container.
*
* @return this container's name
*/
public String getName() {
return name;
}
/**
* Attempts to place a valid volume into this container's
* volume member. If the volume is not greater than 0, an
* exception is thrown.
*
* @param volume the programmer-specified volume
* @throws IllegalArgumentException if the volume is invalid
*/
public void setVolume(double volume) {
// make sure volume is valid
if (volume > 0) {
this.volume = volume;
} else { // volume is not valid
throw new IllegalArgumentException("Error: size must be greater"
+ " than 0.");
}
}
/**
* Retrieves the volume of this container.
*
* @return this container's volume
*/
public double getVolume() {
return volume;
}
/**
* Gets this container as a String.
*
* @return this container as a formatted string
*/
public String toString() {
// formatted container name and volume
return String.format("%s: %.2f", name, volume);
}
}
Given what we know about objects in memory, what do you think
occurs when the following program runs?
public class ObjectComp {
public static void main(String[] args) {
Container container1 = new Container();
Container container2 = new Container();
System.out.println(container1 == container2);
}
}
What do you think will appear on your screen when the program is run?
What actually does appear?
The == operator compares to see if the two operands on either side of
the operator are of equal value. If we were to compare two integers,
such as 5 == 2, we would know that this statement would result in the
boolean value false. Similarly, we know that the statement 5 == 5
would result in the boolean value true. When comparing two variables,
we know that Java will look at the values inside those variables and
compare them.
When Java looks inside the variables container1,
and container2 it sees
memory addresses. It then compares these two memory addresses -- is
the memory address in container1 equal to the memory address in
container2? In this
case, container1 and container2 are referencing two completely
different objects in
memory, so the memory addresses in the variables are not the same.
This is why you see the boolean value false
display on the console.
This is what we call an identity comparison: we're checking
to see if two object variables point to the same object, or contain the
same reference/addres; we're checking to see if they have the same identity.
What if instead we wanted to compare
two containers to see if they are the same? In other words, what if we
wanted to know if two containers had the same volume? This could be useful
if we ran out of a specific kind of container and wanted to substitute a
different container that could hold the same amount of stuff.
It's very common to need to compare objects is for equality:
to see if two objects are the same or similar in their characteristics.
Programmers will often think of this when designing their classes, and so
they will often include a method that allows another programmer to compare
two objects for equality. The method we generally use is the
equals()
method. You've used the equals() method before to compare strings (remember,
Strings are objects!) so now we'll add our own equals() method to our
classes.
If you haven't learned
Inheritance yet, this might be a bit
difficult to understand:
The equals() method is defined in the
Object
class.
Every class without "extends" in the header is automatically
a child class of object. All other classes indirectly inherit from Object
through the class hierarchy. Therefore, you can override the equals()
method in any one of your classes.
For those of you who haven't learned
Inheritance yet,
it's enough for now to understand that the equals()
method is part of Java, so it should be written a certain way and
it should behave a specific way.
The default implementation of the equals() method in the object class
compares the two objects' memory addresses:
public boolean equals(Object obj) {
return (this == obj);
}
So, the default equals() performs an
identity comparison: it checks to see if two object
variables are pointing to the same object in memory.
This means that an alternative way of writing the original example that
compares Container objects would be:
public class ObjectComp {
public static void main(String[] args) {
Container container1 = new Container();
Container container2 = new Container();
System.out.println(container1.equals(container2));
}
}
The output will be the same: there is no equals() method in the Container
class, so Java will look for it in the parent class. We didn't define
a parent for Container, so Object is Container's parent. This means
Java will then go up to the Object class in search of an equals()
method. Of course, it will find it and then use the Object's
equals() method to compare the address of one Container object to the address
of the other Container object.
If the two objects have the same address, the equals() method will return
the boolean value true. Otherwise, it will return false.
In our example, the two Container objects do not have the same memory
address, so this program will output false.
Try modifying the code this way:
public class ObjectComp {
public static void main(String[] args) {
Container container1 = new Container("box", 5.0);
Container container2 = container1;
System.out.println(container1.equals(container2));
}
}
Now the program outputs true. This is because we took the object address
in container1 and assigned it to the container2 variable.
Now container1 and container2 are both pointing
to or referencing the exact same container object:
container1 and container2 contain the same
address, and therefore container1.equals(container2)
and container1 == container2 both give you a result of
true.
But again, these are all performing identity comparisons.
How do we change the equals() for our class so that it performs
an equality comparison?
For this we override, or create our own custom version of,
the equals() method.
How to Implement an equals() Method
An equals() method must have the following
characteristics:
The method must always be public
The method must always return a boolean primitive
The method must always be called equals
The method must always include an Object parameter (this
contains the object you are comparing this object to)
Note that the parameter mentioned in the last point must always
be of type Object. This parameter contains the object you are
comparing to. For example, when you compare two String objects, you
might say:
if (stringOne.equals(stringTwo)) { ... }
In this example, stringOne is the String object
that you are invoking the method on: inside the code, it's the
this object. The stringTwo object
being passed into equals() is going to be stored
in the Object parameter. So the method compares the Object parameter
String (stringTwo) to the this
(stringOne) object.
As an example,
if we were programming an equals() method for a
class called Employee, it might look like this:
public boolean equals(Object object)
You would call this method by invoking it on one employee object, and
passing it the other employee object:
Similarly, the signature for an equals() method in a
Time class would be:
public boolean equals(Object object)
You would then invoke this with a statement such as:
System.out.println("time1 same as time2? " +
time1.equals(time2));
The code inside the equals() method will vary from class to class. As a
programmer, you would have to determine what makes two objects equal.
For example, what would make two container objects equal? You could probably
test the two volume values.
Inside your equals() method, you'd then have to write the
code that compared the volumes of the this container
and the parameter container. However, your parameter container is
of type Object, not of type Container.
This means that the following line of code is a syntax error:
public boolean equals(Object obj) {
return this.volume == obj.getVolume();
}
The error is that the obj doesn't have a getVolume() method,
and that's true because obj is not a Container (the
Object class has no getVolume() method).
But we know (hopefully) that deep down inside, the obj
is probably a Container. So in order to invoke getVolume(),
we have to cast the obj Object into a Container:
public boolean equals(Object obj) {
Container c = (Container)obj;
return this.volume == c.getVolume();
}
What if it's not a container? That would also crash the program, but
we have a solution to that in a few minutes.
This equals() method will check the current object's volume (the container
on which this method is being invoked, referred to by
this.volume) and
the parameter variable's volume (referred to by c.getVolume()) If the
two are equal, the return statement will return a true value, otherwise
it will return a false value.
Making a Robust equals() Method
There are a few things that your equals() is
required to do: this is called the equals contract.
There are also some things that you should do to make your
equals() more robust (so that it doesn't crash
your program).
For your equals() method to meet the equals contract
and follow Java standards, you must meeet the following
criteria:
the method shouldn't crash when you try to cast the parameter
if the parameter is null the method returns false
an object must equal itself
Checking for the right class type
One issue when using the Object parameter in the equals() method is that
a programmer can pass any object they like into your method. For example,
what would happen with the code below?
Scanner in = new Scanner(System.in);
Container box = new Container("box", 1.5);
if (box.equals(in)) {
System.out.println("These are the same.");
} else {
System.out.println("These are not the same.");
}
If you try to run this program, you'll end up with a run-time error:
Exception in thread "main" java.lang.ClassCastException:
java.util.Scanner cannot be cast to your.package.Container
A ClassCastException
occurs when you try to cast an object into another class type, but the object
is not of that class type. For example, trying to cast a Double object into
a Robot object, or trying to cast an Employee object into a String object.
In the program above, the variable in, which is a Scanner object, is
passed into Container.equals() method. Recall that the
Container.equals() method's first statement is
Container c = (Container)o;
The object parameter contains a reference to the Scanner that was passed
into the equals() method, and we're trying to cast it into a Container.
you can't do that because you can't cast a Scanner into a Container.
We can solve this problem by ensuring that our equals() method checks
to make sure the object parameter is the correct type, using the
instanceof operator:
public boolean equals(Object o) {
if (o instanceof Container) {
Container c = (Container)o;
return this.volume == c.getVolume();
} else {
return false;
}
}
Now the equals method will only perform the comparison as long as the
object parameter is a Container object (or child of Container).
If the object parameter is not
a Container object, the equals() method will return false. Our
sample program that tries to compare a Container and a Scanner will return
false, and display the output "These are not the same." which does
actually make sense.
Checking that there isn't a null object
A second problem with the object parameter is that the programmer could
pass a null object (an object variable that doesn't contain an object
reference; in other words, an object variable that doesn't contain an
address to an object). For example:
Container c1 = new Container("box", 1.5);
Container c2 = null;
if (c1.equals(c2)) {
System.out.println("These are the same.");
} else {
System.out.println("These are not the same.");
}
This program will also give you an error:
Exception in thread "main" java.lang.NullPointerException
at your.package.Container.equals(Container.java:104)
at your.package.MainClass.main(MainClass.java:20)
Java Result: 1
A NullPointerException occurs when
you try to use or invoke a method
on an object variable that contains no reference/memory address. In this case,
c2 contains a null reference (represented by the keyword
null): it's not pointing to any Container object.
To fix this problem, we add an extra condition to our equals() method
that checks to make sure the object parameter is not a null object:
public boolean equals(Object o) {
if (o == null)
return false;
if (o instanceof Container) {
Container c = (Container)o;
return this.volume == c.getVolume();
} else {
return false;
}
}
Now if you run the test program, you'll notice that it won't crash and
the equals() method returns false.
Comparing an object to itself
This one is kind of weird: why would you want to compare an object
to itself? It's actually one of the rules of the equals() method in Java:
it must work so that an object is equal to itself (see
Equals
and Hash Code by Manish Hatwalne for JavaRanch
if you're interested).
Therefore, it's standard for every equals() method to return true
if the object parameter is equal to the object the equals() method
is invoked upon:
public boolean equals(Object o) {
if (o == null)
return false;
if (this == o)
return true;
if (this instanceof Container) {
Container c = (Container)o;
return this.volume == c.getVolume();
} else {
return false;
}
}
The equals() method above is actually going to be your template for
pretty much every equals() method you code. The only thing different
will be the casting, and the statements you use to determine what
makes two objects equal to each other.
Exercise
Create a class called Location with the following members:
Class: Location
- name : String - latitude : double
- longitude : double
The name field should not be empty nor should it be a null object.
Latitude must be between -90.0 and +90.0 degrees, inclusive.
Longitude must be between -180.0 and +180.0, inclusive.
The toString() method that displays the Location object as a formatted String,
which includes the location's name and co-ordinates. For example:
CN Tower: Latitude 43.643047, Longitude -79.387382
Taj Mahal: Latitude 27.175355, Longitude 78.042099
Numeric values should be formatted to 6 decimal places.
The distanceBetween() method calculates the distance between two different
two locations (the current one, and the one passed into the method.
To calculate the distance, you can use the standard formula to calculate
the distance between two points on a line:
So for a location object, x1 and y1 would be the latitude and longitude of
the first Location, and x2 and y2 would be the latitude and longitude of the
second Location.
The equals() method will compare a Location to another Location and consider
them equal if both the latitude and longitude of the Location objects are
the same. Obviously it's extremely difficult to get two pairs of latitude
and longitude values exactly the same, so compare the values rounded to
3 decimal places.
Test your Location class: Write a main class that instantiates
three of Location objects
with different latitude and longitude values of your choice,
for example:
CN Tower: Latitude 43.643047, Longitude -79.387382
360 Restaurant: Latitude 43.643047, Longitude -79.387382
Taj Mahal: Latitude 27.175355, Longitude 78.042099
Parque Nacional Torres del Paine: Latitude -50.942177, Longitude -73.406756
Compare the location objects using the equals() method.