Reading assignment: Ch. 7

Part I: Inheritance

Often, we have need to write classes that have similarities to and differences from other existing classes. It is better to combine the similarities and avoid re-writing code in many cases. To do this, we use a feature of the Java language called "inheritance".

For example, suppose we want to design two classes, B and C, that have some of the same data fields. Further suppose that class B contains some data fields (X1, X2, ..., Xn) that are not logically a part of a class C object but all the data fields in C (Y1, Y2, ..., Ym) are logically a part of a class B object. Inheritance involves defining class C so that it declares data fields Y1, Y2,...,Ym and then define class B such that it "extends" C and defines the data fields X1, X2,..., Xn. To do so, we actually use the keyword "extends" in the header of class B.

        class C {                        class B extends C {
           int Y1,                          int X1,
               Y2,...,                          X2,...,
               Ym;                              Xn;
         ...                                ...
        }                                }

If classes B and C are defined as shown above, we say that class C is the superclass, base class, or parent class of class B and class B is the subclass, derived class, or child class of class C. In the example above, the following statements are true:

        class B inherits from class C
        class B is derived from class C
        class B is a subclass of class C
        class B is a child (descendant) of class C
and
        class C is a base class of class B
        class C is a superclass of class B
        class C is the parent (ancestor) class of class B

As the descendant and ancestor terms suggest, the class hierarchies, or "family trees" in Java can be extensive. For an example of a more extended hierarchy, see the Student.java file associated with this lecture. This file contains a base class, Student, with classes HSStudent, UGStudent, and GStudent derived directly from Student; JunHighStudent and SenHighStudent derived from HSStudent; and Masters and PhD derived from GStudent.

Notice that each parent class has its data fields defined as "protected" and only the non-parent classes have "private" data fields. The "protected" visibility modifier is used only with inheritance. A protected (or public) data field of an ancestor class is part of and directly accessible to any object of a descendant class. However, to "unrelated" classes, protected data fields are like private data fields - i.e., they are inaccessible. Private data fields in an ancestor class are not directly accessible to descendant classes (although a copy is created for each descendant object, that copy cannot be directly accessed by any of the descendant object's methods).

In general, the following rules apply:

        Data Field                  ...means
      Visibility Modifier       Access Allowed to
      -------------------       -----------------
      public                    all other classes,
                                even unrelated ones

      protected                 descendants only

      private                   no related or unrelated
                                classes

Inheritance only works in one direction, i.e., the ancestor class is allowed access to NONE of the descendant classes data fields or methods, no matter how they are declared. That's why in diagrams of class hierarchies, the arrows point from child to parent class...think of visibility going in only one direction such that all the non-private members of the parent class are visible (and therefore accessible) to the child classes, but not vice versa.

Part 2: Polymorphism

If a class A extends class B, we say that an object of type A "IS-A" type B object, but B "IS-NOT-A" type A object. Because derived classes have this "IS-A" relationship with base classes, we can use a variable declared as the base type to reference an object that was instantiated as a derived type. This is the "Polymorphism rule" of inheritance: a variable of a super(base,ancestor) type can reference objects of a sub(derived,descendant)type. In this simplest form, polymorphism allows a single variable to refer to objects from different classes.

For example, suppose we wanted to create an array called "whosWho", containing the 50 most remarkable students from high school through graduate school in a given region. Then, given the Student.java class hierarchy, we could define this array as follows:

         Student[] whosWho = new Student[50];
The declaration does not show the benefits of polymorphism, but what we can store in the whosWho array does. Given the above definition, we can create objects in the whosWho array of the Student class or any class that is derived from the Student class, as follows:
         whosWho[0] = new JunHighStudent();
         whosWho[1] = new Masters();
         whosWho[2] = new Resident();
         whosWho[3] = new UGradStudent();
         whosWho[4] = new HSStudent();
         whosWho[5] = new Student();
         whosWho[6] = new PhD();
         ...
Notice that we can create new objects at any level of the hierarchy, because all the classes in the hierarchy are instantiable. But the further down we go in the hierarchy, the more data fields are created in the object, i.e., the object becomes more precisely defined.

What this means for us, as program designers, is that we can write arrays that are "generic", i.e., able to hold objects of different, but related, types. The above example shows how we would define such a generic array and how we could go about adding elements to this array.

The instanceof operator allows us to check whether the element we are accessing is a more specific type (where the most specific is the "instantiated type") than the declared type of the array. For example, after creating the elements in the whosWho array, we could count the number of Masters objects in the array as follows:

  1.    int numMasters = 0;
  2.    for (int i = 0; i < whosWho.length; i++)
  3.    {
  4.       if (whosWho[i] instanceof Masters)
  5.          numMasters++;
  6.    }
We could have substituted the GStudent for Masters in line 4 to count all the Masters and PhD objects in the array. The "actual type" of an object is the type following the "new" operator when the object was instantiated.

Part 3: The Java hierarchy and the Object class

All user defined classes in Java, including arrays, are automatic descendants of the "most super" or "basest" Java class...the Object class. Given the discussion of polymorphism in Part 2, the most general type of array possible is an array of type Object. A complication arises when we try to access a member of an element contained in an array of Object type: we must cast the element as the most specific type that contains the method or data field we want to access before we can refer to that member. For example, suppose we created an array of Objects and inserted objects of String type into the array as follows:
     Object[] a = new Object[3];  // declare generic container
     a[0] = "One String";
     a[1] = "Two String";
     a[2] = "Last String";
Now suppose that we wanted to print the length of each String in a. The following code would cause an error to occur because the Object class has no length method defined:
     for (int j = 0; j < a.length; j++)
     {
          System.out.println(a[j].length());
     } 
In fact, the error would be very specific and tell us that there is no `length' method in java.lang.Object. To fix this problem, we need to cast each call to a[j].length() as the type that actually contains a length method...in this case, the String class:
     for (int j = 0; j < a.length; j++)
     {
          System.out.println(((String)a[j]).length());
     } 
Notice the extra parentheses around "((String)a[j])". These are necessary because the object at index j must be a String object before its length method is called (without the extra parentheses, we would be casting AFTER calling the length method of the Object class...a method that does not exist). Therefore, we need to use the notation "((String)a[j]).length()". Without these extra parentheses, we would get the same error as we did without the cast.