Espresso: A Concentrated Introduction to Java
Summary: We consider the standard methods that all (or most) classes provide.
Prerequisites: Basics of Java.
Contents:
In Java, all template classes provide a variety of standard methods.
By standard method, I mean that it is a standard that you
include the method. You have already encountered one such method,
toString.
Why does Java have standard methods? Because programmers benefit
from knowing that certain methods are there. For example, you can
easily convert any object to a string with the toString
method, and you don't even have to bother to look it up. Similarly,
you can compare any two objects in the same class for equality with
the equals method.
Java has two groups of standard methods. Some must be there
for every class. These methods include toString
and equals. Others you may choose whether or
not to include, and you specify in an interesting way that you
have included them. These methods include clone
and compareTo.
What happens if you don't write the standard methods? Java supplies its own, and they don't always work the way you would expect them to work.
One of the simplest methods that most classes provide is the
toString method, which has the signature
public String toString()
As the name of the method suggests, this method converts the object to a string. Why would we want to convert an object to a string? Most typically, so that we can print the object (to the screen, to a file).
The toString method is so fundamental that Java
uses it implicitly in a number of cases. For example, whenever
you try to print an object, Java calls the toString
method, even if you haven't explicitly listed it in your call.
For example, suppose we have declared f1 to be a
Fraction. Then Java will automatically (and invisibly) convert
the call pen.print(f1) to
pen.print(f1.toString()).
Similarly, if you concatenate an object with a
string using the + operator, Java will first convert the
object to a string using the toString method and then
concatenate the two strings.
What happens when you don't write the toString method?
Java supplies a default version that prints the class of the object
followed by the address of the object in memory. (Yes, it's a strange
default, but it's about as good as anything.)
Another simple standard method is equals, which you
use to determine whether one object is naturally
the
same as another object. You first have a responsibility to
determine what naturally
means. For example, are
two decimal numbers the same if they are not precisely equal?
Are two fractions the same if one is simplified and one is
not? Are two vectors in two-space the same if one has an
angle of 360 degrees more than the other? Are two vectors
in two-space the same if they have the same radius, and their
angles differ by a very small amount (less than 1/1000 of
a degree)?
The standard signature for the equals method
is
public boolean equals(Object other)
Note that Object, used in the example above, is a fundamental
class in the Java language. The role it plays is central to the topic of
inheritance, and we will discuss it more fully when we address that
topic.
For now, let me simply say that Object is a
general class that encompasses all other classes, and I ask you once
again to use this particular method header as "boiler-plate."
The task of this method then is to compare a general object (passed in as a
parameter) with a particular object from a
particular class (i.e., the specific object that the equals
method was called on).
Note that it is somewhat difficult to compare a specific object
to a general object. So, what do you do? My standard solution
is to provide a second form of equals that takes the
appropriate specific kind of object as a parameter (i.e., the parameter
is in the class for which we are writing the equals
method). Then, in the
first equals method, we call the second equals
method, while casting the (general) parameter to the specific
type. (Recall that you cast a value to another
type by prefacing the name of the value with the name of the
type in parentheses.)
For example, in the Fraction class, we might write the
following two methods.
public boolean equals(Object other)
{
return this.equals((Fraction) other);
} // equals(Object)
public boolean equals(Fraction other)
{
return this.numerator.equals(other.numerator)
&& this.denominator.equals(other.denominator);
} // equals(Fraction)
Unfortunately, the cast may fail. An object must be compatible with a given type before it can be cast to that type. For example, suppose we call the method as follows.
Fraction f1 = new Fraction("4/3");
Counter c1 = new Counter();
pen.print(f1.equals(c1));
Here, we are trying to compare a Counter to a
Fraction,
but a Counter and a Fraction are really quite
different. When we try to cast the counter to a fraction type, the cast
will fail.
What happens when the cast fails? Java throws a
ClassCastException. How do you then write
the equals(Object) method?
Before making the cast, we check whether it is safe with the
instanceof operator. (We know that if the parameter can
not be cast to the appropriate type, then the two objects are of
incompatible types, and they certainly are not equal to one another.)
public boolean equals(Object other)
{
if (other instanceof Fraction) {
return this.equals((Fraction) other);
}
else {
return false;
}
} // equals(Object)
Of course, you should express that more concisely as
public booleans equals(Object other)
{
return (other instanceof Fraction)
&& this.equals((Fraction) other);
} // equals(Object)
If you don't bother to write equals, Java provides a
default version that returns true only if the two values
share the same memory locations. In that case, in the example below
f1 and f2 will be found not equal to one
another.
Fraction f1 = new Fraction("4/3");
Fraction f2 = new Fraction("4/3");
The equals method provides the simplest form of comparison,
but there are times when we need more information than it provides. For
example, if we want to put a sequence of values in order from smallest to
largest, it is not sufficient to know whether a particular pair of values
are, or are not, equal to one another. We also need to know which precedes
the other in the "natural" ordering. Classes may (but need not) provide the
compareTo method for this purpose. Perhaps you have used this
method in some of the standard classes, such as
java.math.BigInteger.
This method has the signature
public int compareTo(Class other)
where Class is typically the class you're defining. For example,
in the Fraction class, we would write
public boolean compareTo(Fraction other)
The method call f1.compareTo(f2) returns
f1 naturally precedes f2;
f1.equals(f2);
f2 naturally precedes
f1
How should you decide if one object naturally precedes
another? That's up to you. What if you can't choose such a
relationship? Then you shouldn't bother implementing
compareTo. What if there are many possible
relationships, as in the case of students, who you might compare
by name, by age, by student ID, by height, by GPA, or by something
completely different? Then you can either pick one as the default
or choose not to implement compareTo. In a subsequent reading,
we'll consider how to handle multiple comparisons.
The ordering given by compareTo should be transitive
and reflexive. When we say that it is transitive, we mean that if a.compareTo(b) returns a
negative number and b.compareTo(c) returns a negative
number, then a.compareTo(c) should also return a negative
number. (We can say something similar when it returns positive
numbers.) When we say that it is reflexive, we mean that if
a.compareTo(b) returns a negative number, then
b.compareTo(a) should return a positive number (and vice
versa).
Because this standard method is not always implementable (that is, there
is sometimes no natural ordering), you need not include it. If you do,
you should add the following line to the header of your class
implements Comparable<Class>
You must also import java.util.Comparable. (Again, the meaning
of this will be taken up more fully later, when we consider Java
interfaces.)
For example,
import java.util.Comparable;
public class Fraction
implements Comparable<Fraction> {
}
Note that for the compareTo method, you need not
follow the "two method" strategy that you had to use in
equals. Only one compareTo,
which compares two objects in the same class, is all that
is necessary.
At times, you have one copy of an object and you need another copy. For
example, you may have created a StringBuffer and want
to keep the original and make a copy that you will modify. To support
such situations, Java encourages you to provide a method called clone.
The signature of this method is
public Object clone()
You may find it strange that clone returns an
Object rather than explicitly returning a member of
the specified class. This form of return was all that was supported
in an early version of Java (that is, there was no way to have multiple
methods with the same name and parameter types, but different return
types), and it seems to have been retained.
Because clone returns an object, you need to cast
the value that is returned. For example, the Java compiler will complain
about
Fraction frac2 = frac1.clone();
and insist that you instead write
Fraction frac2 = (Fraction) frac1.clone();
Although clone is standard, it is also optional. If you
supply the method, you should indicate that your class
implements Cloneable. For example,
public class Fraction
implements Cloneable, Comparable<Fraction> {
}
There are some subtleties involved in writing the clone method
that we may or may not have time to address in this course. For now, I
would like you to understand the purpose of clone. You may
also find it useful to call clone on objects for which it has
been defined.
The last of the standard methods is somewhat strange. The
hashCode method returns some integer that represents
this object
. What integer should you return? It's up to you.
The two general rules are that
equals method)
should have the same value returned by hashCode.
Unfortunately, it is impossible to guarantee that unequal objects return different numbers since there are a finite number of representable integers but no fixed limit on the number of values that an object can take on. Hence, we should simply try to give unequal objects different numbers.
The purpose of a hash code arises when we want to store an object
in a hash table, a data structure that we will consider later in
the semester. At that time, we will consider more carefully what value we
might return from the hashCode method.
Why does Java include the hashCode method as a standard
method? The designers of Java made some
strange decisions as to what to make default. Some folks find this
one of the stranger ones.
Do you have to write the hashCode method? It's not
a bad idea. The Java standard suggests that if you write equals,
then you must write hashCode.
What is the default behavior of hashCode? By default,
hashCode returns some value computed from the memory
location of the object. Hence, two equal values are unlikely to have
the same hash code.