In the previous two chapters, we came to know Java objects and then
their interrelationships. We have now climbed the scaffolding of the
Java class hierarchy and reached the top. In this chapter,
we’ll talk about the Object
class itself,
which is the “grandmother” of all classes in Java.
We’ll also describe the even more fundamental
Class
class (the class named “Class”)
that represents Java classes in the Java virtual machine. We’ll
discuss what you can do with these objects in their own right.
Finally, this will lead us to a more general topic: the reflection
interface, which lets a Java program inspect and interact with
(possibly unknown) objects on the fly.
java.lang.Object
is
the ancestor of all objects; it’s the primordial class from
which all other classes are ultimately derived.
Methods defined in
Object
are therefore very important because they
appear in every instance of any class, throughout all of Java. At
last count, there were nine public
methods in
Object
. Five of these are versions of
wait( )
and notify( )
that are
used to synchronize threads on object instances, as we’ll
discuss in Chapter 8. The remaining four methods
are used for basic comparison, conversion, and administration.
Every
object has a toString( )
method that is called
implicitly when it’s to be represented as a text value.
PrintStream
objects use toString( )
to print data, as discussed in Chapter 10. toString( )
is also used
when an object is referenced in a string
concatenation.
Here are some examples:
MyObj myObject = new MyObj( ); Answer theAnswer = new Answer( ); System.out.println( myObject ); String s = "The answer is: " + theAnswer ;
To be friendly, a new kind of object
should override toString( )
and implement its own
version that provides appropriate printing functionality. Two other
methods, equals( )
and hashCode( )
, may
also require specialization when you create a new class.
equals( )
determines
whether two objects are equivalent. Precisely what that means for a
particular class is something that you’ll have to decide for
yourself. Two String
objects, for example, are
considered equivalent if they hold precisely the same characters in
the same sequence:
String userName = "Joe"; ... if ( userName.equals( suspectName ) ) arrest( userName );
Using equals( )
is not the
same as:
if ( userName == suspectName ) // Wrong!
This code tests whether the two reference variables,
userName
and suspectName
, refer
to the same object; which is sufficient but not necessary for them to
be equivalent objects.
A class should override the equals( )
method if it
needs to implement its own notion of equality. If you have no need to
compare objects of a particular class, you don’t need to
override equals( )
.
Watch
out for accidentally overloading equals( )
when
you mean to override it. With overloading, the method signatures
differ; with overriding, they must be the same. The
equals()
method signature specifies an
Object
argument and a boolean return value.
You’ll probably want to check only objects of the same type for
equivalence. But in order to override (not overload)
equals()
, the method must specify its argument to
be an Object
.
Here’s an example of correctly overriding an
equals()
method in class Shoes
with an equals()
method in subclass
Sneakers
. Using its own method, a
Sneakers
object can compare itself with any other
object.
class Sneakers extends Shoes { public boolean equals( Object arg ) { if ( (arg != null) && (arg instanceof Sneakers) ) { // compare arg with this object to check equivalence // If comparison is okay... return true; } return false; } ... }
If we specified public
boolean
equals(Sneakers
arg)
...
in the Sneakers
class,
we’d overload the equals()
method instead of
overriding it. If the other object happens to be assigned to a
non-Sneakers
variable, the method signature
won’t match. The result: superclass
Shoes
’s implementation of
equals()
will be called, possibly causing an
error.
The
hashCode( )
method returns an integer that is a
hashcode for the object. A hashcode is like a
signature or checksum for an object; it’s a random-looking
identifying number that is usually generated from the contents of the
object. The hashcode should always be different for instances of the
class that contain different data, but should normally be the same
for instances that compare “equal” with the
equals( )
method. Hashcodes are used in the
process of storing objects in a Hashtable
, or a
similar kind of collection. The hashcode helps the
Hashtable
optimize its storage of objects by
serving as an identifier for distributing them into storage evenly,
and locating them quickly later.
The default implementation of hashCode( )
in
Object
assigns each object instance a unique
number. If you don’t override this method when you create a
subclass, each instance of your class will have a unique hashcode.
This is sufficient for some objects. However, if your classes have a
notion of equivalent objects (if you have overriden equals( )
) and you want equal objects to serve as equivalent keys
in a Hashtable
, then you should override
hashCode( )
so that your equivalent objects
generate the same hashcode value.
Objects can
use the clone( )
method of the
Object
class to make copies of themselves. A
copied object will be a new object instance, separate from the
original. It may or may not contain exactly the same state (the same
instance variable values) as the original—that’s
controlled by the object being copied. Just as important, the
decision as to whether the object allows itself to be cloned at all
is up to the object.
The Java Object
class provides the mechanism to
make a simple copy of an object including all of its state—a
bitwise copy. But by default this capability is turned off.
(We’ll hit upon why in a moment.) To make itself cloneable, an
object must implement the
java.lang.Cloneable
interface. This is a flag
interface indicating to Java that the object wants to cooperate in
being cloned (the interface does not actually contain any methods).
If the object isn’t cloneable, the clone( )
method throws a
CloneNotSupportedException
.
clone( )
is a protected method, so by default it
can be called only by an object on itself, an object in the same
package, or another object of the same type or a subtype. If we want
to make an object cloneable by everyone, we have to override its
clone( )
method and make it public.
Here is a simple, cloneable class—Sheep
:
import java.util.Hashtable; public class Sheep implements Cloneable { Hashtable flock = new Hashtable( ); public Object clone( ) { try { return super.clone( ); } catch (CloneNotSupportedException e ) { throw new Error("This should never happen!"); } } }
Sheep
has one instance variable, a
Hashtable
called flock
(which
the sheep uses to keep track of its fellow sheep). Our class
implements the Cloneable
interface, indicating
that it is okay to copy Sheep
and it has
overridden the clone( )
method to make it public.
Our clone( )
simply returns the object created by
the superclass’s clone( ) method—a copy of our
Sheep
. Unfortunately, the compiler is not smart
enough to figure out that the object we’re cloning will never
throw the CloneNotSupportedException
, so we have
to guard against it anyway. Our sheep is now cloneable. We can make
copies like so:
Sheep one = new Sheep( ); Sheep anotherOne = (Sheep)one.clone( );
The cast is necessary here because the return type of clone( )
is Object
.[24]
We now have two sheep instead of one.
The equals( )
method would tell us that the sheep
are equivalent, but ==
tells us that they
aren’t equal—that is, they are two distinct objects. Java
has made a “shallow” copy of our
Sheep
. What’s so shallow about it? Java has
simply copied the bits of our variables. That means that the
flock
instance variable in each of our
Sheep
still holds the same information—that
is, both sheep have a reference to the same
Hashtable
. The situation looks like that shown in
Figure 7.1.
This may or may not be what you intended. If we instead want our
Sheep
to have separate copies of all of its
variables (or something in between), we can take control ourselves.
In the following example, DeepSheep
,
we implement a
“deep” copy, duplicating our own flock
variable:
public class DeepSheep implements Cloneable { Hashtable flock = new Hashtable( ); public Object clone( ) { try { DeepSheep copy = (DeepSheep)super.clone( ); copy.flock = (Hashtable)flock.clone( ); return copy; } catch (CloneNotSupportedException e ) { throw new Error("This should never happen!"); } } }
Our clone( )
method now clones the
Hashtable
as well. Now, when a
DeepSheep
is cloned, the situation looks more like
that shown in Figure 7.2.
Each DeepSheep
now has its own hashtable. You can
see now why objects are not cloneable by default. It would make no
sense to assume that all objects can be sensibly duplicated with a
shallow copy. Likewise, it makes no sense to assume that a deep copy
is necessary, or even correct. In this case, we probably don’t
need a deep copy; the flock contains the same members no matter which
sheep you’re looking at, so there’s no need to copy the
Hashtable
. But the decision depends on the object
itself and its
requirements.
[24] You might
think that we could override the clone()
method in
our objects to refine the return type of the clone( )
method. However this is currently not possible in Java.
You can’t override methods and change their return types.
Technically this would be called covariant return
typing. It’s something that may find its way into
the language eventually.
Get Learning Java now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.