Java 101: Foundations

Java 101: Inheritance in Java, Part 2

Object and its methods

1 2 3 Page 3
Page 3 of 3

Because finalize() can execute arbitrary code, it's capable of throwing an exception. Because all exception classes ultimately derive from Throwable (in the java.lang package), Object declares finalize() with a throws Throwable clause appended to its method header.

Finalization for superclasses

It is also possible for a superclass to have a finalize() method that must be called. In these cases we can use a try-finally construct within finalize() to execute the finalization code. The finally block ensures that the superclass's finalize() method will be called, regardless of whether finalize() throws an exception.

When finalize() throws an exception, the exception is ignored. Finalization of the object terminates, which can leave the object in a corrupt state. If another thread (i.e., path of execution) tries to use this object, the resulting behavior will be nondeterministic. (See "Java 101: Understanding Java threads" for a complete introduction to threaded programming in Java.)

The finalize() method is never called more than once by the JVM for any given object. If you choose to resurrect an object by making it reachable to application code (such as assigning its reference to a static field), finalize() will not be called a second time when the object becomes unreachable (i.e., eligible for garbage collection).

Supporting hash-based collections: hashCode()

The hashCode() method returns a hash code (the value returned from a hash -- scrambling -- function) for the object on which this method is called. This method is used by hash-based collection classes, such as the java.util package's HashMap, HashSet, and Hashtable classes to ensure that objects are properly stored and retrieved.

You typically override hashCode() when also overriding equals() in your classes, in order to ensure that objects instantiated from these classes work properly with all hash-based collections. This is a good habit to get into, even when your objects won't be stored in hash-based collections.

The JDK documentation for Object's hashCode() method presents a general contract that must be followed by an overriding hashCode() method:

  • Whenever hashCode() is invoked on the same object more than once during an execution of a Java application, hashCode() must consistently return the same integer, provided no information used in equals() comparisons on the object is modified. However, this integer doesn't need to remain consistent from one execution of an application to another.
  • When two objects are equal according to the overriding equals() method, calling hashCode() on each of the two objects must produce the same integer result.
  • When two objects are unequal according to the overriding equals() method, the integers returned from calling hashCode() on these objects can be identical. However, having hashCode() return distinct values for unequal objects may improve hashtable performance.

When I discuss hash-based collections in a future article, I'll demonstrate what can go wrong when you don't override hashCode(). I'll also present a recipe for writing a good hashCode() method and demonstrate this method with various hash-based collections.

String representation and debugging: toString()

The toString() method returns a string representation of the object on which this method is called. The returned string is useful for debugging purposes. Consider Listing 10.

Listing 10. Returning a default string representation

class Employee
{
   private String name;
   private int age;

   public Employee(String name, int age)
   {
      this.name = name;
      this.age = age;
   }
}

Listing 10 presents an Employee class that doesn't override toString(). When this method isn't overridden, the string representation is returned in the format classname@hashcode, where hashcode is shown in hexadecimal notation.

Suppose you were to execute the following code fragment, which instantiates Employee, calls the object's toString() method, and outputs the returned string:

Employee e = new Employee("Jane Doe", 21);
System.out.println(e.toString());

You might observe the following output:

Employee@1c7b0f4d

Listing 11 augments this class with an overriding toString() method.

Listing 11. Returning a non-default string representation

class Employee
{
   private String name;
   private int age;
   public Employee(String name, int age)
   {
      this.name = name;
      this.age = age;
   }
   @Override
   public String toString()
   {
      return name + ": " + age;
   }
}

Listing 11's overriding toString() method returns a string consisting of a colon-separated name and age. Executing the previous code fragment would result in the following output:

Jane Doe: 21

Waiting and notification: wait() and notify()

Object's three wait() methods and its notify() and notifyAll() methods are used by threads to coordinate their actions. For example, one thread might produce an item that another thread consumes. The producing thread should not produce an item before the previously produced item is consumed; instead, it should wait until it's notified that the item was consumed. Similarly, the consuming thread should not attempt to consume a non-existent item; instead, it should wait until it's notified that the item is produced. These methods support this coordination. I'll introduce threaded programming in Java, including thread coordination, in a future Java 101 article.

In conclusion

The Object class is an important part of classes and other reference types. Every class inherits Object's methods and has the opportunity to override some of them (e.g., toString()), for various purposes. Being familiar with Object and its methods is foundational for understanding the Java class hierarchy. It should also help you make more sense of Java source code -- or at least the code won't look quite so strange.

My next article in the Java 101 series addresses polymorphism, a technique that wouldn't exist without inheritance.

1 2 3 Page 3
Page 3 of 3