1 Abstracting over Common Methods in Different Classes
2 Inheritence
2.1 Simplifying the Boa/ Dillo Classes
2.1.1 Fields and Constructor
2.1.2 The is Normal Size Helper
3 Sharing the implements Specification
4 Abstract Classes
5 Abstract Classes versus Interfaces
6 Class Extension without Abstraction
7 Summary

Abstracting over Classes

1 Abstracting over Common Methods in Different Classes

Recall our isNormalSize methods on animals:

  // in the Boa class

  public boolean isNormalSize () {

    return 5 <= this.length && this.length <= 10;

  }

  

  // in the Dillo class

  public boolean isNormalSize () {

    return 2 <= this.length && this.length <= 3;

  }

The method bodies on Boa and Dillo differ only in the numbers for the low and high bounds. We know that we should create helper functions to share common code in cases such as this. How do we do this in Java?

To make sure our goal is clear, consider what the helper function and revised code would have looked like in Racket:

  (define (len-within? len low high)

    (and (<= low len) (<= len high)))

  

  ;; normal-size? : animal -> boolean

  ;; determine whether animal is within an expected size range

  (define (normal-size? an-ani)

    (cond [(boa? an-ani) (len-within? (boa-length an-ani) 5 10)]

          [(dillo? an-ani) (len-within? (dillo-length an-ani) 2 3)]))

Our goal here is to do something similar. The main question is where to put isLenWithin. Remember that in Java, every method must be in a class. In Java, classes can’t directly see the contents of other classes. They can create objects and access them through methods, but that is different from using code that is defined in another class.

2 Inheritence

Fortunately, Java (and all other object-oriented languages) follows a model of class hierarchies in which one class can build upon (or extend) the definitions in another. We can define a class for the shared information between Boa and Dillo and make isLenWithin a method in that class. Since our new class exists to abstract over animals, we name the class AbsAnimal.

We initially populate AbsAnimal with the code that is common to Boa and Dillo. We have already noted that the isNormalSize method is (largely) common. Looking at the two classes, however, we see that the length field is also common. This suggests that AbsAnimal needs the following contents:

  class AbsAnimal {

    int length;

  

    // the constructor

    AbsAnimal (int length) {

      this.length = length;

    }

  

    boolean isNormalSize(int low, int high) {

      return low <= this.length && this.length <= high;

    }

  }

Next, we need a way to say that Boa and Dillo should extend on what is already defined in AbsAnimal. In standard OO (object-oriented) terminology, Boa and Dillo should inherit from AbsAnimal. We indicate inheritence using a new keyword called extends in the class definition:

  class Dillo extends AbsAnimal implements IAnimal {

    ...

  }

  

  class Boa extends AbsAnimal implements IAnimal {

    ...

  }

  

In OO terminology, AbsAnimal is the superclass of each of Boa and Dillo. Each of Boa and Dillo is a subclass of AbsAnimal.

2.1 Simplifying the Boa/Dillo Classes

As a result of using extends, every field and method in AbsAnimal is now part of Boa and Dillo. This means that we can remove some code from each of these classes. We’ll work just with Boa in these notes (the changes to Dillo are similar).

2.1.1 Fields and Constructor

The first thing to note is that we can remove the length field from Boa. Furthermore, the AbsAnimal constructor, not the Boa constructor, should set the value of the length field. The Boa constructor passes the value for length to the AbsAnimal constructor by calling super(length). In general, super refers to the superclass; here, it is the name of the constructor in the superclass. This gives the following code:

  class Boa extends AbsAnimal implements IAnimal{

    String name;

    String eats;

  

    Boa (String name, int length, String eats) {

      super(length);

      this.name = name;

      this.eats = eats;

    }

    ...

  }

In Java, each class may extend at most one other class, so the meaning of super is unambiguous. Note that the call to super must be the first line in the Boa constructor (this is again one of the rules of Java).

2.1.2 The isNormalSize Helper

Finally, we need to clean up Boa to use the isNormalSize method that is in AbsAnimal. If we did this Racket-style, we might write the following:

  class Boa extends AbsAnimal implements IAnimal {

    String name;

    String eats;

  

    Boa (String name, int length, String eats) {

      super(length);

      this.name = name;

      this.eats = eats;

    }

  

    boolean isNormalSize() {

      return super.isNormalSize(5,10);

    }

  }

In this code, think of super as similar to this, in that it refers to the parent object. We have merely called the helper method, supplying the data that differs as parameters.

Yet this approach isn’t fully OO. The whole idea of class hierarchies is that subclasses get methods from their superclasses for free. We should be able to leave isNormalSize out of the Boa class entirely, and just let Boa inherit that code from AbsAnimal. The question, then, is where do we put the parameters 5 and 10 that customize isNormalSize to Boa.

The answer is that these data should become fields in AbsAnimal that we initialize with the constructor. First, let’s add the low and high fields to AbsAnimal and use those fields instead of parameters in isNormalSize:

  class AbsAnimal {

    int length;

    int low;

    int high;

  

    // the constructor

    AbsAnimal (int length, int low, int high) {

      this.length = length;

      this.low = low;

      this.high = high;

    }

  

    boolean isNormalSize() {

      return this.low <= this.length && this.length <= this.high;

    }

  }

Now, we edit Boa to (1) pass the low and high length bounds to the AbsAnimal constructor and (2) eliminate the isNormalSize method entirely:

  class Boa extends AbsAnimal implements IAnimal {

    String name;

    String eats;

  

    Boa (String name, int length, String eats) {

      super(length, 5, 10);

      this.name = name;

      this.eats = eats;

    }

  }

Now, Boa is exploiting inheritence fully, and AbsAnimal is more accurately capturing all the data that distinguish one animal from another. In Racket, functions had to call their helpers directly because the language has no mechanisms for inheriting code from one type to another. This ability is one of the main advantages of working in object-oriented languages.

3 Sharing the implements Specification

Finally, we note that the methods required in the IAnimal interface are part of AbsAnimal. We therefore move the implements IAnimal specification to AbsAnimal instead of Boa:

  class AbsAnimal implements IAnimal {

    ...

  }

  

  class Boa extends AbsAnimal {

    ...

  }

The revised class diagram appears as follows: Class
diagram with AbsAnimal

The diagram helps illustrate the concepts of subclass (Boa and Dillo) and superclass (AbsAnimal).

4 Abstract Classes

One last detail: our current code allows someone to write new AbsAnimal(6, 1, 7). This would mean "some animal of length 6 who should have length between 1 and 7". If our goal were only to create instances of specific animals (which seems reasonable), this object wouldn’t make much sense. We therefore want to prevent someone from creating AbsAnimal objects (allowing only Boa and Dillo objects).

An abstract class is a class that can be extended but not instantiated as a standalone object. We specify this through the keyword abstract before a class name:

  abstract class AbsAnimal {

    int length;

  

    ...

  }

Now, new AbsAnimal(6, 1, 7) yields an error.

5 Abstract Classes versus Interfaces

We now have two mechanisms, abstract classes and interfaces, through which classes can share information. What is each one best used for?

Those with prior Java experience may have learned to use abstract classes for both situations, but this is not good OO programming. Interfaces are fairly permissive: a class must provide methods that implement those outlined in the interface, but how that implementation works (including what data structures get used) is entirely up to the author of the class. Abstract classes provide actual code, which means that any class which uses an abstract class must do so in a way that is consistent with the existing code or data structures. The restriction that a class may only ever extend one other class reflects this consistency problem. In contrast, a class can implement any number of interfaces, because interfaces do not constrain behavior.

Interfaces also recognize that the world is not neatly hierarchical. Different kinds of real world objects have all sorts of different properties that affect how we use them. For example, within WPI’s information system, I am each of a faculty member, Insight advisor, and member of various committees. Different tasks within the university view me as having these different roles. Interfaces let a program say "I need an object that has the methods associated with this particular role". Class hierarchies can’t do this (because of the single-extension restriction).

Put differently, there is an important distinction between stating which operations are required and stating how those operations are implemented (we’ll see a lot of this in the coming days). The former is called specification; the latter implementation. Interfaces are for specification; abstract classes for implementation.

In the context of this particular example, however, IAnimal still feels redundant since only the abstract class implements it. The point is that we might add other animals later that don’t have interesting lengths (fruit flies, for example) – they might still satisfy IAnimal (with isNormalSize always returning true), but not be meaningful extensions of the AbsAnimal class.

6 Class Extension without Abstraction

Abstract classes support field and method abstraction, but class extensions are also used to express hierarchy among data. For example, let’s add two kinds of animals to our class hierarchy: Fish, which have a length and an optimal saline level for water in their tanks; and Sharks, which are fish for which we record the number of times they attacked people. The new classes appear as follows:

  class Fish extends AbsAnimal {

    double salinity;

  

    Fish (int length, double salinity) {

      super(length);

      this.salinity = salinity;

    }

  

    public boolean isNormalSize () {

      return isLenWithin(this.length, 3, 15);

    }

  }

  

  class Shark extends Fish {

    int attacks;

  

    Shark (int length, int attacks) {

      super(length, 3.75);

      this.attacks = attacks;

    }

  }

A few things to note here:

7 Summary

This lecture introduced the following concepts:

While we did not do an example here, you can also inherit between non-abstract classes. For example, you could have a class of graphical icons (with a field for the icon’s picture), then a sub-class of icons placed on a drawing canvas, that would extend the first class with x- and y-coordinates. We will see more examples of this as the course progresses.