Inf1 OP : Lab Sheet Week 10 Q3 - Asteroids
Overview
Warning
Pair Programming:
This exercise is reserved for pair programming in the live lab sessions.
Please skip it when doing the exercises individually.

In this exercise, we would like to show you how you properly override the equals method for a class so you can test different instances for equality.

Like the Comparable and Comparator interfaces, the equals method can compare the state of two objects. Unlike those two, however, it does not rely on external interfaces but uses the equals method which is implemented for each class in the Object superclass. Also, it can only compare for equality but can make no decision about ordering.

For individual classes that want to implement equals, this method needs to be overridden in a similar way as you would do for toString.

@Override
public boolean equals(Object aThat) {
      

Another difference to using the comparable interfaces is that no generic parameter is used which means the incoming parameter could have an entirely different type than the class this method is called on, for example:

Car polo = new Car();
Cow betty = new Cow();
System.out.println(polo.equals(betty));
              

If that were the case there would be a clear difference and the equals method would return false. The same is true, if the incoming object parameter is null.

The first check you should therefore do is if eThat has the correct type. You can do that in various different ways, one is using the keyword instanceof.

If it has the same type, you should cast it to the type of the class this method is called on and continue to a member by member equality comparison. Only if all of its members have exactly the same value, the state of both objects can be considered equal. For different member types, there are different comparison rules. Here are the most commonly used ones:

  • int : simple use the == operator
  • double : using the == operator is error prone here because of floating point inaccuracies. A minimum threshold should be applied. Luckily the number wrapper Double provides a static compare for you that does it. Additionally, Double types implement the Comparable interface which uses a threshold internally.
  • Any Object Type : If you need to compare two object instance with each other, simply call their equals method to delegate the problem.

Combine the results of all member variables and return the common result. If they are all equal, true should be returned.

Note

An initial check should always be added before anything else is done: If the same instance is passed in as this method is called on, the states are definitely the same and you can immediately return true.

Asteroid Defence

In this exercise, you should implement a class Asteroid which overrides the equals method. This class encapsulates location, speed and size of an asteroid coming towards earth. It is entirely immutable and has the following instance members:

double distance
Asteroid's distance to earth
double theta
Asteroid's theta angle in relation to earth's centre
double phi
Asteroid's phi angle in relation to earth's centre
int speed
Asteroid's speed towards or away from earth
SizeCategory size
Asteroid's size category

The asteroid's location is given in spherical coordinates you should be familiar with from Lab 6, Question 5.

All number members can be positive and negative.

The asteroid's size is of the enum type SizeCategory which is defined like this:

public enum SizeCategory {
  SMALL,
  MEDIUM,
  LARGE
}
                      

Implement getters for all instance members and override the equals method as specified above.

An automated test has been created for this exercise: AsteroidEqualsTest.java.

Generating Hash Codes

Warning

As an iron rule: If you override the equals method, you should also always override the hashCode method!

The hashCode method is also implemented by the Object superclass and is used to generate a hashcode for the current instance of an object. The hashcode is based on the current state of the object and represented as a single number. It is used for algorithms and data structures where hashing comes into play. Hashing maps data of arbitrary size to a fixed-sized value. That way, efficient lookups can be performed by using, for example, a HashMap.

The methods hashCode and equals are tightly interlinked as you can see from the general contract of the hashCode method:

  • Whenever it is invoked on the same object more than once during an execution of a Java application, hashCode must consistently return the same value, provided no information used in equals comparisons on the object is modified. This value needs not remain consistent from one execution of an application to another execution of the same application
  • If two objects are equal according to the equals method, then calling the hashCode method on each of the two objects must produce the same value

Note

There is no right or wrong hash code. There is only a more or a less efficient one. As much as is reasonably practical, the hashCode method should return distinct integers for distinct objects. That way, fewer collisions happen when mapping data to fixed-sized values. You will learn more about this in the coming years.

Let's consider the example of a Car. A naive implementation for a hashCode method could look like this:

public class Car {

  private double speed;
  private int numDoors;
  private String brand;

  // ... methods omitted
       
  @Override
  public int hashCode() {
      return 1;
  }

} 
              

This does not violate the contract above but it would have horrible performance since all car instances would map to the same hash value.

There are different ways of generating hashcodes more efficiently. Let's go with this one for our purposes:

  • Start with a fixed integer constant like the number 7.
  • For each number instance member in you class, multiply the hashcode calculated so far with another fixed integer, like 31, and add to it the member's value cast to an integer. For example:
    hash = 31 * hash + (int) speed;
                
  • For each object member instance, multiply the hashcode calculated so far with 31 again and add to it a zero if the object member is null or the object member's hashcode you get from calling its own hashCode method, which delegates the problem. For example:
    hash = 31 * hash + (brand == null ? 0 : brand.hashCode());
                

A full implementation for our Car would look like this:

public class Car {

  private double speed;
  private int numDoors;
  private String brand;

  // ... methods omitted
        
  @Override
  public int hashCode() {
      int hash = 7;
      hash = 31 * hash + (int) speed;
      hash = 31 * hash + numDoors;
      hash = 31 * hash + (brand == null ? 0 : brand.hashCode());
      return hash;
  }

} 
                      
Deduplicating Asteroids

To practice generating hashcodes, let's override the hashCode method in our Asteroid class from the first part of this question. Follow the rules specified above to generate a hashcode.

An automated test has been created for this exercise: AsteroidHashTest.java.

Once you have done that, let's put this to the test: You receive asteroid location data from various satellites around the planet. Some pick up the same asteroids and you need to filter them so that no duplicates exist. To do that, implement a new class AsteroidScanner with a static method for deduplicating a list of Asteroid objects with the following method signature:

public static Set deduplicate(List data)
Deduplicate a list of asteroid objects and return a set of unique entries.

So far, we have not used the Set data structure. This data structure makes use of hashing to create a list which only allows unique entries. Similar to using only the keys of a HashMap. A concrete implementation of a Set in Java is a HashSet. Have a look at the corresponding Java API for more details.

Your implementation of the deduplicate method should create a HashSet and add each asteroid from the given list. The list parameter must not be null and should an Asteroid instance in the list be null, you can skip it.

For each duplicate you find, increase a counter and once you reach the end, print the following message to the console (replacing 'x' with your count value):

x duplicate asteroids found.
        

Finally, return the set you put together.

You can use the following list of Asteroid instances to test your code:

List data = new ArrayList<>();
data.add(new Asteroid(-9684.59,270.82,-132.84,551,SizeCategory.MEDIUM));
data.add(new Asteroid(15303.82,-138.47,166.58,-639,SizeCategory.LARGE));
data.add(new Asteroid(1952.42,106.64,94.28,-173,SizeCategory.LARGE));
data.add(new Asteroid(15303.82,-138.47,166.58,-639,SizeCategory.LARGE));
data.add(new Asteroid(2732.31,273.07,358.68,-284,SizeCategory.LARGE));
data.add(new Asteroid(-13568.4,272.63,-236.04,-669,SizeCategory.SMALL));
data.add(new Asteroid(-9730.6,-14.46,233.29,371,SizeCategory.MEDIUM));
data.add(new Asteroid(-4486.87,-77.56,-317.61,686,SizeCategory.LARGE));
data.add(new Asteroid(-4486.87,-77.56,-317.61,686,SizeCategory.LARGE));
data.add(new Asteroid(-7199.88,-247.91,275.97,-730,SizeCategory.SMALL));
data.add(new Asteroid(2684.32,-164.67,97.95,998,SizeCategory.LARGE));
data.add(new Asteroid( 9666.93,339.71,336.86,-959,SizeCategory.MEDIUM));
data.add(new Asteroid(-9730.6,-14.46,233.29,371,SizeCategory.MEDIUM));
                

Note

You can override the toString method for Astroid as well to quickly print your deduplicated list for validity checking to the console.

An automated test has been created for this exercise: AsteroidScannerTest.java.

Source: https://www.baeldung.com/java-hashcode