Overview of This Lesson

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:

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):

(code on GitHub)

/**
 * 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.

In this case, we might write code such as

boolean equal = container1.getVolume() == container2.getVolume();

This would be an equality comparison.

Creating the equals() Method

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:

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:

boolean sameGuys = someEmployee.equals(someOtherEmployee)

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:

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
+ Location()
+ Location(latitude:double, longitude:double
+ getName() : String
+ setName(name : String) : void
+ getLatitude() : double
+ setLatitude(latitude:double) : void
+ getLongitude() : double
+ setLongitude(longitude:double) : void
+ distanceBetween(Location location) : double
+ toString() : String
+ equals(object:Object) : boolean

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:

Distance Formula: sum the square of the difference between the x-values and the square of the difference between the y-values, then take the square root of that sum.

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:

  1. CN Tower: Latitude 43.643047, Longitude -79.387382
  2. 360 Restaurant: Latitude 43.643047, Longitude -79.387382
  3. Taj Mahal: Latitude 27.175355, Longitude 78.042099
  4. Parque Nacional Torres del Paine: Latitude -50.942177, Longitude -73.406756

Compare the location objects using the equals() method.