Special Methods
Every class has a few special methods that belong to it, regardless of whether or not we define them. Exactly how every class obtains these methods will be explored in a future lesson. For now, let’s look at two important examples of these methods.
toString
The toString
method returns a string representation of a class. Calling toString
on a class that you’ve written will result in something like this:
|
|
Console Output:
org.launchcode.java.demos.classes2.Student@61bbe9ba
Here, we called toString
on a Student
object. The default toString
implementation is generally not very useful. Most of the time, you’ll want to write your own toString
method to override the default and provide better results.
Here’s how we might do it for Student
to produce a much more friendly message:
|
|
Console Output:
Violet (Credits: 0, GPA: 0.0)
In the example, we define the toString
method to return a string that reports the class fields name
, numberOfCredits
, and gpa
in a clear manner.
Note that toString
is often implicitly called for you. For example, the output above could have been generated by the following code, which calls toString
on person
within System.out.println
.
|
|
equals
Suppose we have two objects of type Student
, say student1
and student2
, and we want to determine if they are equal. If we try to compare them using ==
, we will likely get a result we did not expect. This is because student1
and student2
are reference variables
, which means they hold a reference to, or the address of, the actual Student
objects. student1
and student2
evaluate as equal only when they have the same memory address.
To state that again: student1
and student2
will be equal (==
) only when they refer to, or point at, the exact same object. Consider the example below, which creates two Student
objects:
|
|
Console Output
Maria, 1234: org.launchcode.java.demos.classes2.Student@3e3abc88
Maria, 1234: org.launchcode.java.demos.classes2.Student@6ce253f1
false
Even though the objects have the exact same keys and values, student1
and student2
point to different memory locations. Therefore, the ==
check returns false.
This is not usually how we want to compare objects. For example, we might want to consider two Student
objects equal if they have the same name, email, or student ID.
The equals()
method determines if one object is equal to another in this sense. We introduced the method when discussing strings, but it also applies to all other classes.
The code below shows how to use equals()
to compare two students. Note that they have different names but the same student ID, indicating they are actually the same person.
|
|
If we don’t provide our own equals()
method, the default option only considers two objects equal if they are the exact same object, which means they point to the same memory address. This is identical to the behavior we see when using the ==
operator: bono1 == bono2
.
In the example above we created two different Student
objects, so the expression bono1.equals(bono2)
evaluates to false
. In order to compare two objects based on their properties, rather than their memory references, we need to define our own equals()
method.
The difference between the comparison carried out by the default equals()
method (and by the ==
operator), and how we would like to compare our classes, is the difference between identity and equality.
- Two objects are identical if they both point to the same memory address. In essence, they are the same object. If
object1
andobject2
are identical, then changing one property value inobject1
also changes that value forobject2
. - Two objects are equal if the values they store are the same at the time of comparison.
student1
andstudent2
point to different memory addresses, but their values are all the same. Thus, we can consider them equal, even though they are not identical.
The default equals()
method and the ==
operator test for identity, whereas we want to test for equality instead. We can do so by overriding the equals()
method. We will discuss overriding in more detail later, but for now just recognize that it involves defining different behavior for an existing method.
Two things can be considered equal even if they do NOT have all the same values. In the case of the Student
class, we might specify that two Student
objects are equal if they have the same ID numbers. We would then write a new method definition for equals()
as follows:
|
|
Now if we evaluate bono1.equals(bono2)
we will get a result of true, since the student IDs match.
One catch of working with equals()
is that its input parameter must be of type Object
, even if we’re working in a class like Student
. The reason why will become more clear in the next lesson, when we introduce the Object
class. For now, the practical implication is that we must convert, or cast, the input toBeCompared
to be of type Student
with the syntax (Student) toBeCompared
. Then we compare the converted student’s ID (bono2.id)
to that of the current student (bono1.id)
.
Here’s a visualization of the concepts of equality and identity:
Equality
When you test for equality, you look at two different objects and compare some aspect of them to each other.
Identity
When you test for identity, you look at two variables to see if they reference the exact same object.
Coding a new equals
Method
You’ll often want to implement equals()
yourself. When you do, be sure you understand the best practices around how the method should behave. These are a little more involved compared to coding a new toString
method.
In fact, the equals()
method we defined above isn’t very good by most Java programmers’ standards. Let’s improve it.
Problem #1
The method argument cannot be converted to a Student
instance.
When we attempt to cast the argument toBeCompared
to type Student
, we’ll get an exception if toBeCompared
can’t be properly converted. This happens if something other than a Student
object gets passed into equals()
. To prevent this from happening, we’ll return false
if toBeCompared
was not created from the Student
class. To check this, we use the getClass
method, which is available to every object (similarly to toString
).
|
|
Lines 3 - 5 ensure that the two objects that we want to compare were created from the same class.
Problem #2
toBeCompared
might be null
.
If toBeCompared
is null
then toBeCompared.getClass()
throws an exception. This is an easy issue to fix—just compare the object to null
. If the comparison evaluates to true, then we know the object is null
and equals()
should return false
.
|
|
Line 3 checks toBeCompared
for null
, preventing an error in line 7. Line 7 checks the class of toBeCompared
, preventing an error in line 11.
Problem #3
The two objects to compare are the same object (identical).
This is less of a problem and more of a way to improve our equals()
method. If toBeCompared
is the same literal object that we are comparing it to, then we can make a quick determination and save a few checks.
|
|
Line 3 checks for identity. If true, then the remaining checks become unnecessary.
Components of equals
Almost every equals
method you write will look similar to the last example above. It will contain the following segments in this order:
- Reference check: If the two objects are the same, return
true
right away. - Null check: If the argument is
null
, returnfalse
. - Class check: Compare the classes of the two objects to ensure a safe cast. Return false if the classes are different.
- Cast: Convert the argument to the type of our class, so getters and other methods can be called.
- Custom comparison: Use custom logic to determine whether or not the two objects should be considered equal. This will usually be a comparison of properties or fields.
Characteristics of equals
Now that we know how to write an equals()
method, let’s look at some characteristics that every such method should have. Following the general outline above makes it easier to ensure that your equals()
method has these characteristics.
Reflexivity: For any non-null reference value
x
,x.equals(x)
should returntrue
.Symmetry: For any non-null reference values
x
andy
,x.equals(y)
should return true if and only ify.equals(x)
also returnstrue
.Transitivity: For any non-null reference values
x
,y
, andz
, ifx.equals(y)
returnstrue
andy.equals(z)
returnstrue
, thenx.equals(z)
should returntrue
.Consistency: As long as
x
andy
do not changex.equals(y)
should always return the same result.Non-null: For any non-null reference value
x
,x.equals(null)
should returnfalse
.
If you think back to what your math classes had to say about equality, then these concepts make sense.
Using the general approach outlined above to implement equals()
will make it easier to meet these characteristics. However, always check your method! Missing one or more characteristics can be disastrous for your Java applications.
Seasoned Java developers will tell you that every time you implement your own version of equals()
you should also implement your own version of hashCode()
. hashCode()
is another special method that every class has. Understanding hashCode()
would take us a bit far afield at this point, but we would be remiss to not mention it. If you want to read more, check out this article
and this stack overflow
.
Take Away
You may not need to write your own equals()
method for every class you create. However, as a new Java programmer, remember the following:
Always use
equals()
to compare objects.
This is especially true when working with objects of types provided by Java, such as String
. A class that is part of Java or a third-party library will have implemented equals()
in a way appropriate for the particular class, whereas ==
will only check to see if two variables refer to the same literal object.
Check Your Understanding
Given the code:
|
|
Which of the following statements evaluates to true
?
firstPet == secondPet;
secondPet == thirdPet;
thirdPet.equals(secondPet);
thirdPet.getName().equals(firstPet);
thirdPet.equals(firstPet);
We add the following code inside the Pet
class:
|
|
Which of the following statements evaluated to false
before, but now evaluates to true
?
firstPet == secondPet;
secondPet == thirdPet;
thirdPet.equals(secondPet);
thirdPet.getName().equals(firstPet);
thirdPet.equals(firstPet);