As we have progressed in this course, we have seen many 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.
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.
Now that we have provided setters, we can 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 a 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.
Our data validity rules for the birth date aren’t very complete because we can still end up with invalid 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 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 need to know the year, month, and day 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.
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 any way, but it does tell the Java compiler to make sure that this method overrides a method from the superclass. 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.
By convention, you should always use @Override
if you are overriding a method.
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 whose instance variables are all the same are considered equal. 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 Person
s and makes sure they are the same before returning true
.
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
.)
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)\) efficiency.)
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;
}
}
One thing we should do now is go back and verify something about three of our methods: equals
, hashcode
, and compareTo
. We need to ensure that they work consistently. If equals
says that two objects are equal, then their hash codes must be the same, and comparing them with compareTo
must return 0.
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. It also means that the order defined by compareTo
should also only make use of the same instance variables checked by equals
.
Our example above does this already.
As you can see, there are 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 functionality (such as Person
being able to be put into a hash table, or sorted using Collections.sort
) or improved functionality (such as verifying the validity of instance variables before setting them).
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.