CMUQ 15-121 Things to Override



1. Introduction

As we have progressed in this course, we have seen a number of helper methods and methods that we have overridden in classes that we have written. In this set of notes, we’ll summarize many of those methods and discuss when, why, and how we should override them when writing our own class.

Think of these notes as methods you should provide for most classes that you write.

As the basis for our discussion, let’s consider a very simple class designed to store information about a person:

public class Person {
	private String firstName;
	private String lastName;
	private int birthMonth;
	private int birthDay;
	private int birthYear;
	
	public Person(String firstName, String lastName, int birthMonth, int birthDay, int birthYear) {
		this.firstName = firstName;
		this.lastName = lastName;
		this.birthMonth = birthMonth;
		this.birthDay = birthDay;
		this.birthYear = birthYear;
	}
}

As you can see, this class has some instance variables as well as a constructor. It declares all of the instance variables private, following the general convention to keep instance variables private whenever possible.

We’ve written the Person class so many times this semester that at this point it should be trivial for you. However, we never built the Person class completely, and properly. That’s what we’ll do in these notes. Get ready for Person to get big.

2. Getters and Setters

Given that our instance variables are entirely private, it is good practice to provide getters and setters so that other code that makes use of our Person class can still read and modify those values as needed. Let’s do that now:

public class Person {
	private String firstName;
	private String lastName;
	private int birthMonth;
	private int birthDay;
	private int birthYear;
	
	public Person(String firstName, String lastName, int birthMonth, int birthDay, int birthYear) {
		setFirstName(firstName);
		setLastName(lastName);
		setBirthMonth(birthMonth);
		setBirthDay(birthDay);
		setBirthYear(birthYear);
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public int getBirthMonth() {
		return birthMonth;
	}

	public void setBirthMonth(int birthMonth) {
		this.birthMonth = birthMonth;
	}

	public int getBirthDay() {
		return birthDay;
	}

	public void setBirthDay(int birthDay) {
		this.birthDay = birthDay;
	}

	public int getBirthYear() {
		return birthYear;
	}

	public void setBirthYear(int birthYear) {
		this.birthYear = birthYear;
	}
}

That’s a lot of getters and setters! This is perfectly normal, however. We are simply ensuring that all of our instance variables are accessible if needed. (If we didn’t want some of them to be accessible, then we could simply remove the associated getters and setters.) You will also notice that we updated the constructor to make use of the getters and setters.

2.1. Better Setters: Part 1

Now that we have provided setters, we can actually do a little bit of “gate-keeping” to make sure that the values being set are valid. For example, 40 isn’t a valid day and -5 isn’t a valid month. So, let’s update our setters to check the validity of the values being passed:

public class Person {
	private String firstName;
	private String lastName;
	private int birthMonth;
	private int birthDay;
	private int birthYear;

	public Person(String firstName, String lastName, int birthMonth, int birthDay, int birthYear) {
		setFirstName(firstName);
		setLastName(lastName);
		setBirthMonth(birthMonth);
		setBirthDay(birthDay);
		setBirthYear(birthYear);
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public int getBirthMonth() {
		return birthMonth;
	}

	public void setBirthMonth(int birthMonth) {
		// Only allow valid months
		if (birthMonth < 1 || birthMonth > 12) {
			throw new IllegalArgumentException("Invalid month.");
		} else {
			this.birthMonth = birthMonth;
		}
	}

	public int getBirthDay() {
		return birthDay;
	}

	public void setBirthDay(int birthDay) {
		// Only allow valid days
		if (birthDay < 1 || birthDay > 32) {
			throw new IllegalArgumentException("Invalid day.");
		} else {
			this.birthDay = birthDay;
		}
	}

	public int getBirthYear() {
		return birthYear;
	}

	public void setBirthYear(int birthYear) {
		// Only allow years after 0. (This could be improved...)
		if (birthYear < 1) {
			throw new IllegalArgumentException("Invalid year.");
		} else {
			this.birthYear = birthYear;
		}
	}
}

Take a look at the new code in the setters. If the setters are called with invalid values, an exception is thrown. As an added bonus, this same checking occurs with the constructor, because the constructor calls our setters. The advantage of enforcing rules like this in the setters and constructor is that now all of the rest of the code we write for the Person class can safely assume that the values are valid; there is no way for them to be changed to invalid values. (We know that birthDay will never be 40 because now there is no way to set it to be 40.) This is the advantage of having setters instead of making your instance variables public: You can enforce rules on data validity and thus make assumptions about it.

2.2. Better Setters: Part 2

Our data validity rules for the birth date actually aren’t very complete because we can still end up with valid dates. For example, February 31, 1999 is a valid date according to the rules we are enforcing, but it isn’t an actual date that can occur. (There are not 31 days in February.) This means that, in order to validate the birthDay, we need to also consider the birthMonth. Leap years are also involved (because some years have an extra day in February), so we actually need to know the year, month, and day in order to validate the day. Given all of this, it seems like we should merge our three setters into one and do all of our checks there.

public class Person {
	private String firstName;
	private String lastName;
	private int birthMonth;
	private int birthDay;
	private int birthYear;

	public Person(String firstName, String lastName, int birthMonth, int birthDay, int birthYear) {
		setFirstName(firstName);
		setLastName(lastName);
		setBirthdate(birthDay, birthMonth, birthYear);
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public int getBirthMonth() {
		return birthMonth;
	}

	public int getBirthDay() {
		return birthDay;
	}

	public int getBirthYear() {
		return birthYear;
	}

	public void setBirthdate(int birthDay, int birthMonth, int birthYear) {
		/*
		 * Credits to https://www.tutorialathome.in/java/check-date-valid-java for a
		 * simple idea of how to do this.
		 */

		// The number of days in each month (non-leap year)
		int daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

		// Validate the year
		if (birthYear < 1) {
			throw new IllegalArgumentException("Invalid year.");
		}

		// Validate the month
		if (birthMonth < 1 || birthMonth > 12) {
			throw new IllegalArgumentException("Invalid month.");
		}

		// If this is a leap year, adjust daysInMonth to add a day to February
		// This painful if statement detects leap years
		if (((birthYear % 4 == 0) && (birthYear % 100 != 0)) || (birthYear % 400 == 0)) {
			daysInMonth[1]++;
		}

		// Now validate the days by checking the table
		if (birthDay < 1 || birthDay > daysInMonth[birthMonth - 1]) {
			throw new IllegalArgumentException("Invalid day.");
		}

		// Great, it is valid! Let's set everything.
		this.birthDay = birthDay;
		this.birthMonth = birthMonth;
		this.birthYear = birthYear;
	}
}

As you can see, we’ve removed all three birth date setters and replaced them with one, combined setter that does our checks.

3. toString

Anytime you write a class, it is good to provide a useful toString. If you don’t, then your object will inherit the default toString from Object, and that toString simply prints the type of the object and its memory address. This is almost certainly not what you want. So, let’s add a useful toString to Person:

public class Person {
	private String firstName;
	private String lastName;
	private int birthMonth;
	private int birthDay;
	private int birthYear;

	public Person(String firstName, String lastName, int birthMonth, int birthDay, int birthYear) {
		setFirstName(firstName);
		setLastName(lastName);
		setBirthdate(birthDay, birthMonth, birthYear);
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public int getBirthMonth() {
		return birthMonth;
	}

	public int getBirthDay() {
		return birthDay;
	}

	public int getBirthYear() {
		return birthYear;
	}

	public void setBirthdate(int birthDay, int birthMonth, int birthYear) {
		/*
		 * Credits to https://www.tutorialathome.in/java/check-date-valid-java for a
		 * simple idea of how to do this.
		 */

		// The number of days in each month (non-leap year)
		int daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

		// Validate the year
		if (birthYear < 1) {
			throw new IllegalArgumentException("Invalid year.");
		}

		// Validate the month
		if (birthMonth < 1 || birthMonth > 12) {
			throw new IllegalArgumentException("Invalid month.");
		}

		// If this is a leap year, adjust daysInMonth to add a day to February
		// This painful if statement detects leap years
		if (((birthYear % 4 == 0) && (birthYear % 100 != 0)) || (birthYear % 400 == 0)) {
			daysInMonth[1]++;
		}

		// Now validate the days by checking the table
		if (birthDay < 1 || birthDay > daysInMonth[birthMonth - 1]) {
			throw new IllegalArgumentException("Invalid day.");
		}

		// Great, it is valid! Let's set everything.
		this.birthDay = birthDay;
		this.birthMonth = birthMonth;
		this.birthYear = birthYear;
	}

	@Override
	public String toString() {
		return "Person [firstName=" + firstName + ", lastName=" + lastName + ", birthMonth=" + birthMonth
				+ ", birthDay=" + birthDay + ", birthYear=" + birthYear + "]";
	}
}

This is a very simple toString that shows the type of the object and all of its instance variables. You’ll also notice that we use the @Override annotation. This doesn’t change the functionality of the code in anyway, but it does tell the Java compiler to make sure that this method actually overrides a method from the super class. This is primarily to detect typos you make while specifying the method prototype. It is good practice to use @Override whenever you are overriding an existing method.

4. Equals

The next thing we should take a look at is the equals method. This method is used to check and see if two different classes are equivalent. Any class you write inherits a default equals method from the Object class, and that method simply checks to see if the two items are references to the same object. This is not usually what you want. Instead, you want to ensure that two different objects with all of the sample instance variables are considered equal. In order to ensure that, you need to override the equals method and provide your own implementation:

public class Person {
	private String firstName;
	private String lastName;
	private int birthMonth;
	private int birthDay;
	private int birthYear;

	public Person(String firstName, String lastName, int birthMonth, int birthDay, int birthYear) {
		setFirstName(firstName);
		setLastName(lastName);
		setBirthdate(birthDay, birthMonth, birthYear);
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public int getBirthMonth() {
		return birthMonth;
	}

	public int getBirthDay() {
		return birthDay;
	}

	public int getBirthYear() {
		return birthYear;
	}

	public void setBirthdate(int birthDay, int birthMonth, int birthYear) {
		/*
		 * Credits to https://www.tutorialathome.in/java/check-date-valid-java for a
		 * simple idea of how to do this.
		 */

		// The number of days in each month (non-leap year)
		int daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

		// Validate the year
		if (birthYear < 1) {
			throw new IllegalArgumentException("Invalid year.");
		}

		// Validate the month
		if (birthMonth < 1 || birthMonth > 12) {
			throw new IllegalArgumentException("Invalid month.");
		}

		// If this is a leap year, adjust daysInMonth to add a day to February
		// This painful if statement detects leap years
		if (((birthYear % 4 == 0) && (birthYear % 100 != 0)) || (birthYear % 400 == 0)) {
			daysInMonth[1]++;
		}

		// Now validate the days by checking the table
		if (birthDay < 1 || birthDay > daysInMonth[birthMonth - 1]) {
			throw new IllegalArgumentException("Invalid day.");
		}

		// Great, it is valid! Let's set everything.
		this.birthDay = birthDay;
		this.birthMonth = birthMonth;
		this.birthYear = birthYear;
	}

	@Override
	public String toString() {
		return "Person [firstName=" + firstName + ", lastName=" + lastName + ", birthMonth=" + birthMonth
				+ ", birthDay=" + birthDay + ", birthYear=" + birthYear + "]";
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}

		if (obj == null || !(obj instanceof Person)) {
			return false;
		}

		Person other = (Person) obj;

		if (birthDay != other.birthDay)
			return false;
		if (birthMonth != other.birthMonth)
			return false;
		if (birthYear != other.birthYear)
			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;
	} 
}

Here, our equals method compares every instance variable between the two Persons and makes sure they are the same before returning true.

5. Hashcode

The hashCode method, you may recall, is used to figure out which bucket an object goes into in a hash table. There is a default hashCode method that comes from Object, but it determines the hash code based on the memory address of the object. Instead, we want the hash code to be based on the contents of the object so that two objects with the same instance variables also have the same hash code. (This is similar to our problem with equals.)

In addition to this, because we overrode equals, we now must override hashCode to ensure that they work consistently: If equals says that two objects are equal, then their hash code must be the same as well. In practice, that means that our hash code needs to include information from all of the same instance variables that equals uses for its comparisons.

Let’s take a look at a hashCode method for Person:

public class Person {
	private String firstName;
	private String lastName;
	private int birthMonth;
	private int birthDay;
	private int birthYear;

	public Person(String firstName, String lastName, int birthMonth, int birthDay, int birthYear) {
		setFirstName(firstName);
		setLastName(lastName);
		setBirthdate(birthDay, birthMonth, birthYear);
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public int getBirthMonth() {
		return birthMonth;
	}

	public int getBirthDay() {
		return birthDay;
	}

	public int getBirthYear() {
		return birthYear;
	}

	public void setBirthdate(int birthDay, int birthMonth, int birthYear) {
		/*
		 * Credits to https://www.tutorialathome.in/java/check-date-valid-java for a
		 * simple idea of how to do this.
		 */

		// The number of days in each month (non-leap year)
		int daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

		// Validate the year
		if (birthYear < 1) {
			throw new IllegalArgumentException("Invalid year.");
		}

		// Validate the month
		if (birthMonth < 1 || birthMonth > 12) {
			throw new IllegalArgumentException("Invalid month.");
		}

		// If this is a leap year, adjust daysInMonth to add a day to February
		// This painful if statement detects leap years
		if (((birthYear % 4 == 0) && (birthYear % 100 != 0)) || (birthYear % 400 == 0)) {
			daysInMonth[1]++;
		}

		// Now validate the days by checking the table
		if (birthDay < 1 || birthDay > daysInMonth[birthMonth - 1]) {
			throw new IllegalArgumentException("Invalid day.");
		}

		// Great, it is valid! Let's set everything.
		this.birthDay = birthDay;
		this.birthMonth = birthMonth;
		this.birthYear = birthYear;
	}

	@Override
	public String toString() {
		return "Person [firstName=" + firstName + ", lastName=" + lastName + ", birthMonth=" + birthMonth
				+ ", birthDay=" + birthDay + ", birthYear=" + birthYear + "]";
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}

		if (obj == null || !(obj instanceof Person)) {
			return false;
		}

		Person other = (Person) obj;

		if (birthDay != other.birthDay)
			return false;
		if (birthMonth != other.birthMonth)
			return false;
		if (birthYear != other.birthYear)
			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;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + birthDay;
		result = prime * result + birthMonth;
		result = prime * result + birthYear;
		if (firstName != null) {
			result = prime * result + firstName.hashCode();
		}
		if (lastName != null) {
			result = prime * result + lastName.hashCode();
		}
		return result;
	}
}

You’ll notice that this hashCode method also mixes a prime number in with the hash codes from the instance variables. This just helps ensure a more even distribution of hash code values. (Which is important for making your hash tables have close to \(O(1)\).)

6. Comparable

If there is any chance that your class will end up in an array or Collection and need to be sorted, then you should override the Comparable interface and provide a proper compareTo method. You’ll need to define the natural ordering for your class. (If you don’t remember this at all, then go back to the notes on comparators and refresh.) Here is one for Person that considers last name, first name, birth year, birth month, and birth day (in that order):

public class Person implements Comparable<Person> {
	private String firstName;
	private String lastName;
	private int birthMonth;
	private int birthDay;
	private int birthYear;

	public Person(String firstName, String lastName, int birthMonth, int birthDay, int birthYear) {
		setFirstName(firstName);
		setLastName(lastName);
		setBirthdate(birthDay, birthMonth, birthYear);
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public int getBirthMonth() {
		return birthMonth;
	}

	public int getBirthDay() {
		return birthDay;
	}

	public int getBirthYear() {
		return birthYear;
	}

	public void setBirthdate(int birthDay, int birthMonth, int birthYear) {
		/*
		 * Credits to https://www.tutorialathome.in/java/check-date-valid-java for a
		 * simple idea of how to do this.
		 */

		// The number of days in each month (non-leap year)
		int daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

		// Validate the year
		if (birthYear < 1) {
			throw new IllegalArgumentException("Invalid year.");
		}

		// Validate the month
		if (birthMonth < 1 || birthMonth > 12) {
			throw new IllegalArgumentException("Invalid month.");
		}

		// If this is a leap year, adjust daysInMonth to add a day to February
		// This painful if statement detects leap years
		if (((birthYear % 4 == 0) && (birthYear % 100 != 0)) || (birthYear % 400 == 0)) {
			daysInMonth[1]++;
		}

		// Now validate the days by checking the table
		if (birthDay < 1 || birthDay > daysInMonth[birthMonth - 1]) {
			throw new IllegalArgumentException("Invalid day.");
		}

		// Great, it is valid! Let's set everything.
		this.birthDay = birthDay;
		this.birthMonth = birthMonth;
		this.birthYear = birthYear;
	}

	@Override
	public String toString() {
		return "Person [firstName=" + firstName + ", lastName=" + lastName + ", birthMonth=" + birthMonth
				+ ", birthDay=" + birthDay + ", birthYear=" + birthYear + "]";
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}

		if (obj == null || !(obj instanceof Person)) {
			return false;
		}

		Person other = (Person) obj;

		if (birthDay != other.birthDay)
			return false;
		if (birthMonth != other.birthMonth)
			return false;
		if (birthYear != other.birthYear)
			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;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + birthDay;
		result = prime * result + birthMonth;
		result = prime * result + birthYear;
		if (firstName != null) {
			result = prime * result + firstName.hashCode();
		}
		if (lastName != null) {
			result = prime * result + lastName.hashCode();
		}
		return result;
	}

	@Override
	public int compareTo(Person p) {
		// Check last name
		int ret = this.lastName.compareTo(p.lastName);
		if (ret != 0) {
			return ret;
		}

		// Last names were equal, now check first names
		ret = this.firstName.compareTo(p.firstName);
		if (ret != 0) {
			return ret;
		}

		// Both last and first names were equal, now do birth year
		ret = this.birthYear - p.birthYear;
		if (ret != 0) {
			return ret;
		}

		// Now do birth month
		ret = this.birthMonth - p.birthMonth;
		if (ret != 0) {
			return ret;
		}

		// Now do day of birth
		return this.birthDay - p.birthDay;
	}
}

7. Conclusion

As you can see, there are actually a lot of different methods to write and override in even a simple class. Person, which started with only 14 lines of code ended up with over 120! However, each new method we wrote enables new (such as Person being able to be put into a hash table, or sorted using Collections.sort) or improved (such as verifying the validity of instance variables before setting them) functionality.

Another important thing to note is that many of these methods can be generated automatically in Eclipse. Using the “Source” menu in Eclipse you can generate basic versions of a constructor, getters, setters, toString, equals, and hashCode. (Although you may need to customize some of the generated methods.) That can greatly reduce the burden on you when writing your class.