Lesson Weekend

Oftentimes, the information we store in our database tables will correspond to objects within our Java applications. For instance, in upcoming lessons we will create a To Do List application complete with a database. This database will contain a table will be called tasks which will hold an entry for each Task item we save. The columns in this table will correspond with the properties of a Task object.

However, before we can begin integrating databases into our applications we need to address several issues that can occur. By ensuring we understand these issues before we encounter them, we'll save ourselves a ton of time tracking down potential bugs in the near future.

Comparing Strings Review

Remember when we learned about Comparing Strings? Checking if two strings are equal with == can produce unexpected errors, because even if two String objects appear the exact same, they're actually stored in two separate places in memory.

So, we use the equals() method instead of ==. It compares String objects based on their content, not their location in memory. If this doesn't sound familiar, take a moment to briefly review Comparing Strings at this time.

Comparing Objects from a Database Using equals() and hashCode()

Now, something similar occurs when retrieving and using objects stored in a database. Let's walk through an example. Say we have a Kitten POJO that looks like this:

Kitten.java
class Kitten {
  String mName;

  public Kitten(String name) {
    mName = name;
  }

  public String getName() {
    return mName;
  }
}

If we ran this assertion:

@Test
public void kittiesAreTheSame() {
  Kitten firstKitten = new Kitten("Squeakers");
  Kitten secondKitten = new Kitten("Squeakers");
  assertTrue(firstKitten.equals(secondKitten));
}

The test would actually fail! In our eyes both objects are representing the same cuddly Kitten named "Squeakers". But Java sees two different instances of Kitten. Why? Because they are located in different memory location in the machine’s RAM (Random Access Memory).

Similar to the issues we encountered when comparing Strings, these are not technically the same object, even though they appear to be. We need to compare the content of the objects to determine whether they are the same.

When we persist our objects' attributes in databases, we will often need to retrieve information, then use that information to create a new object. For instance, we know our To Do List contains both Category and Task objects. If we needed to make a new Task in a particular Category, we would need to fetch that particular Category from our database, and add a new Task to it.

If we repeat this process at different times (which our To Do List inevitably will), we can accidentally create duplicate objects and encounter odd errors.

For instance, imagine:

  • We add a Task with a description of "Do the dishes!" to a Category named "Home Chores".
  • At a later time we add another Task to "Sweep the porch" to the same Category.
  • We want to view all Tasks that belong to that Category we want to see both "Do the dishes!" and "Sweep the porch", right?

But since we retrieved the "Home Chores" category multiple times to add new objects, instead of one Category containing both "Do the dishes!" and "Sweep the porch", we'll have two Category objects in memory called "Home Chores". One containing a Task to "Do the dishes!" and another reminding us we should "Sweep the porch". Obviously this isn't want we want.

Overriding Built-In Methods

Thankfully, we can avoid these issues easily. Returning to our Kitten example, we can include a method that looks like this:

Kitten.java
...
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Kitten kitten = (Kitten) o;

   return name.equals(kitten.name); //more properties would be taken into account here if our model was more complex.
}

Let's walk through this together. It’s a bit complicated, so don’t worry if you don’t get it right away.

  • The method equals() is built into any Object by default. It's included in a set of default behaviors all objects have. However, we can override this default method using the @Override annotation. Overrides allow us to tweak the existing stock method with one that works for our specific context.
  • If the object passed in to compare to is the actual, literal, same object as the current object the method is running on, return true.
  • If the object passed in is null, or does not come from the same class as this object does, return false.
  • Then, cast (see below) the object that was passed in as a Kitten object, and compare the name Strings using the String equals() method. If they are the same, return true.

The @Override annotation indicates to the compiler that the method we define under it should replace the method of the same name built into Java.

hashCode()

Any time we implement an equals() override we should always implement an @Override called hashCode() as well. The hashCode for the above method is as follows:

@Override
public int hashCode() {
   return name.hashCode();
}

hashCode() calculates, on a memory-based level, whether or not two objects are the same. While we never really have to call hashCode() directly, it is considered unsafe and bad practice to implement equals() without a hashCode() method, as this can cause instability and false positives.

Check out this article to read more. And, notice that this article is actually 17 years old - and still relevant! That's how stable Java is!

Type Casting

Let's discuss type casting in more detail, too. As we just discussed, type casting is the act of converting one type, such as a class, to a different type.

Hierarchies

Only classes from the same class hierarchy (sometimes referred to as type hierarchy) can be converted into one another. For example, we can view the hiearchy for the java.util package in the Oracle Documentation here.

A hierarchy is just a group of classes that inherit from each other. Consider this non-technical example:

Animal > Mammal > Canine > Terrier > ScottishTerrier

Here, we have a hierarchy of dogs. It begins with the general Animal class. A specific type of animal is a warm-blooded Mammal. It's still considered an Animal, and therefore inherits all properties of Animal, but it has its own traits specific to Mammal, too. Like being warm-blooded.

Canine is an even more specific type of Mammal. It inherits all Mammal traits (and therefore all Animal traits, too), but also has properties and capabilities specific to canines.

Then, we narrow down the field even further. Terrier inherits from Canine. And therefore inherits basic traits and functionalities from all classes that precede it. Terrier is a type of Canine, which is a type of Mammal, which is a class of Animals.

So, in this fictional example, we could cast an Animal to a ScottishTerrier:

Animal newPuppy = new Puppy("Wilbur"); 
ScottishTerrier newPuppy = (ScottishTerrier) newPuppy;

But we couldn't cast a Book to a Canine since they do not belong in the same hierarchy of interrelated classes that inherit from one another.

Java works like this, too! For instance, we discussed earlier that all Java objects inherit from the Object class. We can cast a Kitten to Object, and vis-versa, since all classes inherit from Object, they're in the same hierarchy. But we couldn't cast a Kitten to Integer.

When to Type Cast

Now, you should know what type casting is, and how to do it. But you actually want to use it as little as possible. If your code is constantly transforming objects between types, that's an indication it may not be very efficient.

Overriding the equals() method is a noteworthy exception to this rule of casting. Because it's a necessary method for comparing objects, and it must take an Object argument; it's considered common practice to cast the argument to the class we are using the method to compare. Like Kitten, in the example above.

Whenever our applications save objects to databases, we'll need to override the equals() method, and cast its general Object argument into a more-specific type for comparison.

Generating equals() and hashCode()

Lucky for us, our IDE offers the ability to generate equals() and hashCode() methods for us, which is super useful when we have more complex data models. Let’s go ahead and do that now for an extended Kitten model:

Kitten.java
public class Kitten {
   String name;
   int age;
   String sex;
   boolean intact;
   ArrayList<String> breeds;

   public Kitten(String name, int age, String sex, boolean intact, ArrayList<String> breeds) {
       this.name = name;
       this.age = age;
       this.sex = sex;
       this.intact = intact;
       this.breeds = breeds;
   }

   public String getName() {
       return name;
   }

   public int getAge() {
       return age;
   }

   public String getSex() {
       return sex;
   }

   public boolean isIntact() {
       return intact;
   }

   public ArrayList<String> getBreeds() {
       return breeds;
   }
}

Click Generate and then choose the “equals() and hashCode()” option.

On the first screen, leave the defaults as-is.

equals-hashcode-1

On the second and third screens, select the fields you think you need to compare in order to gauge equality. We recommend caution if you use system time to calculate a creation date, as this can potentially make equals() fail when comparing a local object to one pulled from a database.

equals-hashcode-2

equals-hashcode-3

On the fourth screen, determine which of the object’s properties cannot be null - that is, properties we cannot do without. In this case, A Kitten needs to have a non-null String for a name, and a boolean for the Kitten’s physical sex, but we can do without a breed listing.

equals-hashcode-4

You should see the following:

Kitten.java
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   Kitten kitten = (Kitten) o;

   if (age != kitten.age) return false;
   if (intact != kitten.intact) return false;
   if (adopted != kitten.adopted) return false;
   if (!name.equals(kitten.name)) return false;
   if (sex != null ? !sex.equals(kitten.sex) : kitten.sex != null) return false;
   return breeds != null ? breeds.equals(kitten.breeds) : kitten.breeds == null;
}

@Override
public int hashCode() {
   int result = name.hashCode();
   result = 31 * result + age;
   result = 31 * result + (sex != null ? sex.hashCode() : 0);
   result = 31 * result + (intact ? 1 : 0);
   result = 31 * result + (breeds != null ? breeds.hashCode() : 0);
   result = 31 * result + (adopted ? 1 : 0);
   return result;
}

Important Note: If you change your data model, that is, add or remove fields, you will need to change your equals() and hashCode() to prevent failing tests and false positives.