next up previous contents
Next: Subtyping vs inheritance Up: Subclasses and inheritance Previous: Multiple inheritance   Contents

The Java class hierarchy

Java rules out multiple inheritance. In Java, the class hierarchy is tree like.

In fact, not only is the hierarchy tree-like, Java provides a universal superclass called Object that is defined to be the root of the entire class hierarchy. Every class that is defined in a Java program implicitly extends the class Object.

The class Object defines some useful methods that are automatically inherited by all classes. These include the following:

  boolean equals(Object o)  // defaults to pointer equality

  String toString()         // converts the values of the
                            // instance variable to String

The built-in method equals checks whether two variables actually point to the same physical object in memory. This is rather strong and most classes will redefine this to say that two objects are the same if their instance variables are the same.

The toString() method is useful because Java implicitly invokes it if an object is used as part of a string concatenation expression. To print the contents of an object o, it is sufficient to write:

  System.out.println(o+"");

Here, o+"" is implicitly converted to o.toString()+"".

Recall that a variable of a higher type can be assigned an object of a lower type (e.g., Employee e = new Manager(...)) This compatibility also holds for arguments to methods. A method that expects an Employee will also accept a Manager.

We can write some ``generic'' programs, as follows:

  public int find (Object[] objarr, Object o){
    int i;
    for (i = 0; i < objarr.length(); i++){
        if (objarr[i] == o) {return i};
    }
    return (-1);
  }

As mentioned above, we will probably redefine methods like equals to be less restrictive than just referring to pointer equality. However, we should be careful of the types we use. Suppose we enhance our class Date to say:

  boolean equals(Date d){
    return ((this.day == d.day) &&
            (this.month == d.month) &&
            (this.year == d.year));
  }

Before proceeding, we note a couple of facts about this method. Recall that day, month and year are private fields of class Date. Nevertheless, we are able to refer to these fields explictly for the incoming Date object d. This is a general feature--any Date object can directly read the private variables of any other Date object. Without this facility, it would be tedious, or even impossible, to write such an equals(..) method.

Second, note the use of this to unambiguously refer to the object within which equals is being invoked. The word this can be omitted. If we write, instead,

  boolean equals(Date d){
    return ((day == d.day) &&
            (month == d.month) &&
            (year == d.year));
  }

then, automatically, the unqualified fields day, month and year refer to the fields in the current object. However, we often use this to remove ambiguity.

Now, what happens when we call our generic find with find(datearr,d), where datearr and d are declared as Date[] datearr and Date d?

When we call datearr[i].equals(d) in the generic find, we search for a method with signature

  boolean equals(Object o)

because, within find, the parameter to find is an Object. This does not match the revised method equals in Date, so we end up testing pointer equality after all!

To fix this, we have to rewrite the equals method in Date as:

  boolean equals(Object d){
    if (d instanceof Date){
      return ((this.day == d.day) &&   
              (this.month == d.month) &&
              (this.year == d.year));
    }
    return(false);
  }

Notice that this is another use of the predicate instanceof to examine the actual class of d at runtime.

In general, the method used is the closest match. If Employee defines

  boolean equals(Employee e)

and Manager extends Employee but does not redefine equals, then in the following code

  Manager m1 = new Manager(...);
  Manager m2 = new Manager(...);
  ...
  if (m1.equals(m2)){ ... }

the call m1.equals(m2) refers to the definition in Employee because the call to a non existent method

  boolean equals(Manager m)

is compatible with both

  boolean equals(Employee e) in Employee

and

  boolean equals(Object o) in Object.

However, the definition in Employee is a ``closer'' match than the one in Object.


next up previous contents
Next: Subtyping vs inheritance Up: Subclasses and inheritance Previous: Multiple inheritance   Contents
Madhavan Mukund 2004-04-29