CSCI A202 - Introduction to Programming (II)

Lecture 12: Extending Classes: Overriding & Shadowing.

A class gives us a description, a list of features that an object of that class should have. By features we mean what kind of data members and method members it should have.

By extending a class we extend our original description, and specify additional features. An object of the extended class has all the features listed in the original class and the class that extends it. An object of the original class has the features listed in the original class.

This is the reason for which we say that if class B extends class A every object of type B is also an object of type A but an object of type A is not of type B (objects of type B have additional features, listed in the description of class B).

Another way to remember this is in this way: if Child extends Parent we can use an object of type Child everywhere we can use a Parent but not the other way around.

This is one kind of polymorphism.

In our discussion in class we have distinguished between:

Both have a type.

Objects are anonymous, and we refer to them by names (or object references).

Extending classes is a very simple concept.

Creating composite objects from composite descriptions is as easy as putting the two descriptions together, unless we use the same names for different features (class members of the same kind: data, methods) in the two descriptions.

A few examples will help clarify these concepts.

1. Overriding of Methods. Dynamic Binding (Dynamic Method Lookup)

Consider this:

class A {
  void fun() {
    System.out.println("This is fun as defined in class A."); 
  } 
}
 
class B extends A {
  void fun() {
    System.out.println("This is fun as defined in class B."); 
  } 
} 

public class One {
  public static void main(String[] args) {
    System.out.println("Welcome to Program One.");
 
    A m = new B(); 
    m.fun();  
 
    B n = new B(); 
    n.fun();
 
    ((A)n).fun(); 
 
  } 
} 
Essentially we have:
Here's what you get when you run the program:
tucotuco.cs.indiana.edu% javac One.java
tucotuco.cs.indiana.edu% java One
Welcome to Program One.
This is fun as defined in class B.
This is fun as defined in class B.
This is fun as defined in class B.
tucotuco.cs.indiana.edu%
You see then, that it's the type of the object (and not that of its reference) that really matters.

If either A or B do not define fun() then there's no overriding involved and no dynamic method lookup is involved. But it is instructive to cover the three alternatives to the situation described above:

1. A does not define fun()
Only objects of type B referenced through object references of type B will be able to invoke fun(). Casting an object of type B to type A and asking for fun() will get you into trouble early (as early as compile time).

2. B does not define fun()
Objects of type b inherit fun() from A.

3. Neither A or B define fun()
In that case there's nothing to talk about.

To summarize, looking up for a method to invoke involves taking into account the class of the object and the class of its reference (casting included here). The object reference's class determine the method unless the object's class also defines a method with the same name, in which case that's the one that will be invoked.

Here's another example:

class A {
  void fun() {
    System.out.println("This is fun defined in A.");
  }
}
 
class B extends A {
 
}
 
class C extends B {
  void fun() {
    System.out.println("This is fun defined in C.");
  }
}
 
public class Four {
  public static void main(String[] args) {
    System.out.println("Welcome to Program Four"); 
    B m = new B(); 
    m.fun(); // fun inherited (A)        
    C n = new C(); 
    n.fun(); // C has its own fun        
    B p = new C(); 
    p.fun(); // Type C overrides the fun
             // that B inherited from A  
    A q = new C(); 
    q.fun(); // the fun defined in C is used 
             // over the one defined in A, since
             // the type of the object q points
             // to (that class) has own fun  
  } 
}
This produces:
tucotuco.cs.indiana.edu% javac Four.java
tucotuco.cs.indiana.edu% java Four
Welcome to Program Four
This is fun defined in A.
This is fun defined in C.
This is fun defined in C.
This is fun defined in C.
Once you override a function the only way you can get to it is through a super reference.

You can't get to it from outside the class (through casting).

2. Shadowing of Variables

Overriding is a stronger notion than shadowing.

If you shadow a variable you can still get to it from outside by casting.

Here's the example:

public class Five {
  public static void main(String[] args) {
    B m = new B(); 
    m.fun(); 
    m.x = 3; 
    ((A)m).x = 6;
    m.fun();  
  } 
} 
 
class A {
  int x; 
} 
 
class B extends A {
  int x; 
  void fun() {
    System.out.println(x + "  " + super.x); 
  } 
}
Can you annotate this program and tell what's doing?

Two final points: