Chapter 7. Working with Objects and Classes

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.

The Object Class

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.

Equality and Equivalence

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.

Hashcodes

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.

Cloning Objects

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.

Shallow copy of an object

Figure 7-1. Shallow copy of an object

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.

Deep copy of an object

Figure 7-2. Deep copy of an object

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.