Inheritance

 

 

The concept of inheritance is one of the 3 defining characteristics of object-oriented programming: (1) inheritance, (2) polymorphism, and (3) abstraction.

 

            We have already seen that Java (as any OO language) allows us to have methods of the same class with the same name.  We distinguish them by the number and/or type of parameters sent to the method.  This is referred to as polymorphism (meaning many shapes).  But when people speak of polymorphism, they usually mean subtype polymorphism, which means the same method name (having the same number and type of parameters) can be referring to different classes.  The actual class to which the method is referring to is decided during run-time, called dynamic binding.

 

            Both polymorphism and abstraction come about from inheritance.  The idea of inheritance is that one does not need to rewrite the code over again, but then it can be reused. 

 

            To understand the concepts of inheritance, let us look at a simple example involving students, both undergraduate and graduate.  We will thus have the following class diagram:

 

 

 

 

The diagram means Academia is the parent or superclass and Student is the child or subclass.  Furthermore, Student is the parent or superclass class of two other classes, Undergraduate and Graduate, which  are children or subclasses of both Student and Academia.

 

            To simplify the example the Academia class will have only as a member the name, which is a String.  The student class will have as members  the scores on the exams (which is an array), and the course grade, which is a String. 

 

            The problem that we have is as follows:  We assign different grades to the students depending on their rank.  I.e., if the Undergraduate student has an average of at least 70, we pass them, while for Graduate students, we insists upon a grade of at least 80.   However, the assigning and retrieving of the scores is the same for both classes of students.  The only difference is the assigning of the grades.

 

            So we want some methods to be common to both Undergraduate and Graduate, while others we want to behave differently.  This is the motivation for inheritance.  We do not want to have to write two exact versions of the methods they have in common.  If we have exact versions of the methods in different places, this leads to a maintenance problem:  one always has to make sure that a modification is done exactly the same to all occurrences of it.   But they have other common methods which we want to behave differently.

 

            To solve this problem of common methods that are to behave differently, we have the idea of an abstract method.  An abstract method (which can only exist in an abstract class), is not a real method, but a method which will be implemented at a lower level (in the class hierarchy). 

 

            To get a more concrete handle on this, let us look at the UML of each of the 4 classes discussed above:

 

Academia

#  String name

#  static int globalID

+ Academia()

+ Academia(String name)

+ getName :String

+ void setName(String)

 

 

 

 

Student

# final int NUMBER_OF_SCORES

# int [] scores

# int [] weights

# String courseGrade

# int ID

+ Student

+ Student(String name,int ID)

+ String getCourseGrade()

+ getID() :int

+ getScore(int scoreNumber) :int

+ setScore(int scoreNumber,int score):void

+ setWeight(int weightNumber,int weight):void

+ getWeight(int weightNumber):int

 

Undergraduate

 

+ void computeGrade()

 

Graduate

 

+ void computeGrade()

 

 

            First of all, we have to discuss the symbol “#” used above.  We have used the “-for private and the “+” for public members and methods.  Recall that private means that the member or method is only known to the class and not to the outside world.  The type public means that it is known to the outside world and can be accessed in other methods not in the class itself. 

 

However, for derived (or subclasses) classes of a base (or superclass) we have a problem.  If we make a member private, then its subclass (by definition) will not have access to it.  By the nature of inheritance, we want the subclass (as a rule) to have access to its parents members.  But, if we make the member public, then the entire outside world will have access to it, which defeats the idea of information hiding.  This creates a conundrum.  This is resolved by the type of protected.  A protected member is not accessible to the outside world, but is accessible to all of its descendants.  The symbol or “#” denotes a protected member or function.

 

Thus, the member name of Academia is protected, hence all three of its derived classes (Student, Undergraduate, and Graduate) have access to it.  Similarly  the three members of Student (NUMBER_OF_SCORES, scores, and courseGrade) are accessible to Undergraduate and Graduate, but not to the outside world.  (See below for the code implementing these four classes.)

 

We have introduced one other term above, the term abstract, both for the class of Student and for the method computeGrade.  We need to create the method computeGrade as an abstract method because we do not implement it inside of Student.  We can compute a grade for a generic Student, but only for an Undergraduate or Graduate student.  However, to fully use polymorphism we need to have a method in Student called computeGrade, even though it will have no code to implement it.  This is why we denote it as abstract.  If one has an abstract method in a class, then the class must be declared as an abstact class by the syntax of Java.  (See below for the discussion of polymorphism.)

 

            The code for each of the 4 classes is as follows:

 

Public class Academia

{

            protected String name;

            protected static int globalID = 1000;

 

            public Academia()

{

                        name = “No Name’;

            }

 

            public Academia(String name)

            {

                        this.name = name;

            }

 

            public String getName()

            {

                        return name;

            }

 

            public void setName(String newName)

            {

                        name = newName;

            }

}//end Academia

 

public class Student extends Academia

{

            protected final int NUMBER_OF_SCORES = 3;

            protected int [] scores;

            protected int[] weights;

            protected String courseGrade;

            protected int ID;

 

            public Student()
            {

                        super();

ID = globalID++;

                        scores = new int[NUMBER_OF_SCORES];

                        courseGrade = “***”;

            }

 

public Student(String name)
            {

                        super(name);

                        ID = globalID++;

                        scores = new int[NUMBER_OF_SCORES];

                        courseGrade = “***”;

            }

 

            public String getCourseGrade()

            {         

                        return courseGrade;

            }

 

            public int getID()

            {

                        return ID;

            }

 

            public int getScore(int scoreNumber)

            {

                        return scores[scoreNumber];

            }

 

            public void setScore(int scoreNumber, int score)

            {

                        scores[scoreNumber] = score;

            }

 

public int getWeight(int weightNumber)

            {

                        return weights[weightNumber];

            }

 

            public void setWeight(int weightNumber, int weight)

            {

                        scores[weightNumber] = weight;

            }

 

}//end Student

 

 

public class Undergraduate

{

            public Undergraduate()

            {

                        super();

            }

 

public Undergraduate(String name,int ID)

            {

                        super(name,ID);

            }

 

 

            public void computeGrade()

            {

                        int total = 0,totalWeight = 0;;

 

                        for(int i = 0; i < NUMBER_OF_SCORES;++i)

                        {

                                    total += scores[i]*weights[i];

                                    totalWeight += weights[i];

                        }

 

                        if (total/totalWeight >= 70)

                                    courseGrade = “Pass”;

                        else

                                    courseGrade = “Not Pass”;

}

 

}//end Undergraduate

 

public class Graduate

{

           

public Graduate()

            {

                        super();

            }

 

public Graduate(String name,int ID)

            {

                        super(name,ID);

            }

 

 

public void computeGrade()

            {

                        int total = 0,totalWeight = 0;;

 

                        for(int i = 0; i < NUMBER_OF_SCORES;++i)

                        {

                                    total += scores[i]*weights[i];

                                    totalWeight += weights[i];

                        }

 

                        if (total/totalWeight >= 80)

                                    courseGrade = “Pass”;

                        else

                                    courseGrade = “Not Pass”;

}

 

}//end Graduate

 

 

Constructors for Base Classes and Derived Classes

 

            Before we can discuss polymorphism and the use of the method computeGrade, we need to discuss what happens with constructors and their relationship with their superclass. 

 

            In the constructor of Students (as well as Graduate and Undergraduate), the call of super in their constructors is a call to their parents.  Thus the super in Students calls the appropriate constructor in Academia, with the constructor with no parameters passed in Students calling the constructor with no parameters passed in Academia and the constructor with a String passed calling the constructor in Academia with a String passed.  This is standard practice in inheritance.  Rather than re-writing all of the code in the constructor, one calls its parents whenever possible.  Thus, one only has to modify the minimum amount of code for maintenance.

 

 

Polymorphism

 

 

            We are now able to explain the advantages of abstract classes and dynamic binding. 

 

            Suppose we have a class of 40 students, which contains both undergraduate and graduate students.  The naďve assumption would be to create two arrays (one for undergraduate and one for graduate) to maintain the class roster:

 

Graduate [] gradRoster = new Graduate[40];

Undergraduate [] undergradRoster = new Undergraduate[40];

 

We need to make each of these of size 40 to handle the possibility that all of the class could only be of one type.

 

            However, using inheritance, we need only declare:

 

Student [] roster = new Student[40];

 

Elements of the array roster can be either instances of the Student class or any of its descendants, Graduate or Undergraduate classes.

 

0

1

2

3

4

.

.

.

.

 

 

 

 

 

 

 

36

37

38

39

 

 

 

*   

G

 

U

 

G

 

G

 
 

 

 

 


 

 

 

 

 

In its simplest form polymorphism allows a single variable to refer to objects from different classes.  Consider for example the following declaration:

 

Student student;

 

student = new Student();

 

But also,

 

student = new Graduate();

student = new Undergraduate();

 

            In other words, the single variable student is not limited to referring to an object from the Student class, but can refer to any object from the descendant classes of Student.  In a similar manner, we can something like:

 

roster[0] = new Graduate(“First Student”);

roster[1] = new Undergraduate(“Second Student”);

roster[2] = new Graduate(“Third Student”);

roster[3] = new Graduate(“Fourth Student”);

.

.

.

 

 

This leads to the situation described above.

 

            However, you can not make a variable refer to an object of a superclass or a sibling class.  (A class is a sibling if they share a common ancestor.)  For example, the following declarations are invalid:

 

Graduate grad1,grad2;

 

grad1 = new Student();

grad2 = new UnderGraduate(“undergrad”);

 

 

            Then, to compute the course grades using the roster array, we would execute the following code:

 

for(int i = 0; i < numberOfStudents; ++i)

            roster[i].computeGrade();

 

If roster[i] refers to a Graduate student, then the computeGrade method of the Graduate class is called; and if roster[i] refers to an Undergraduate student, then the computeGrade of the Undergraduate class is called.  computeGrade is a polymorphic method, because it refers to different methods of different classes depending on the object reference of roster[i].  The actual choice of which method is called is decided at run-time, which is referred to a dynamic binding. When the program runs for the ith element in the array of roster, the program looks at the class type of roster[i] and calls the appropriate method.

 

Polymorphism allows us to maintain the class roster with one array instead of maintaining a separate array for each type of student.  This simplifies the processing considerably.

 

Polymorphism makes possible smooth and easy extensions and modifications of programs.  Suppose for example, we have to add a third type of student, say audit student to the class roster, or non-degree seeking student.  If we have to define a separate array for each type of student, this extension forces to define a new class (no problem) and a new array (a big problem) for both audit and non-degree seeking students.

 

But with polymorphism, we only need to have a derived class of student for Audit and NonDegreeSeeking.  As long as this class includes the code for a computeGrade method, the for loop to compute the course grades for students remains the same.  Without polymorphism, not only do we have add new code, we have to re-write the existing code to accommodate the change.  Modifying existing code is a tedious and error prone activity.  A slight change to existing code could cause a program to stop working correctly.  To be certain that a change in one portion of existing code will not effect other portions of existing code adversely, we must understand the existing code completely. The understanding of code, especially the case that is long and/or written by other people, is a time-consuming and difficult task.

 

An element of the roster array is a reference to an instance of either the Graduate or Undergraduate class.  Most of the time, we do not need to know which is the class one is referring to.  There are times, however, when we need to know the class of the referred object.  For example, we may want to know the number of undergraduates in the course or the number of graduate students who passed the course.  To determine the class of an object, we use the instanceof operator.  For example, the number of UnderGraduate students in the class can be found by the following code:

 

undergradCount = 0;

for (int  i = 0; i < numberOfStudents;++i)

if (roster[i]  instanceof Undergraduate)

            undergradCount++;

 

 

Abstract Classes and Methods

 

We still have to explain the use of abstact classes and their uses. 

 

 Suppose in our code above we had defined:

 

roster[6] = new Student(“Seventh Student”);

 

What happens when we try and execute the following code:

 

roster[i].computeGrade();

 

for i = 6?  The answer is that we get a compiler error because there is no method of

computeGrade for the class of Student.  It is only a method of its subclass.

 

            How can we handle this? There are two possible assumptions:

 

(I)                All students are either undergraduate or graduate or

(II)             Students need not be only of these two classes, e.g., non-degree seeking students.

 

            If only undergraduate or graduate students can exist, then we can define the Student class in such a way that no instances of it can be created.  One way is the use of abstract classes.  An abstract class is a class in which no instances can be created of this class.  We add the keyword abstract before the term class in its definition:

 

public abstract class Student

 

And we add a method of computeGrade, but as an abstract method:

 

abstract public void computeGrade();

 

 

Note the syntax of an abstract method: It has no body, just the term abstract.  It can be passed parameters in its definition, if necessary:

 

abstract public void computeGrade(int passingScore);

 

            An abstract method is a method which has no method body and just a semi-colon after the parenthesis.  It is just an interface description.  A class is abstract if the class contains an abstract method or does not provide an implementation of an inherited abstract method.  We say a method is implemented if it has a method body (i.e., a { and a } after the closing ) for parameter passing).      If a subclass has no abstract methods and no unimplemented abstract methods from an ancestor, then the class is no longer abstract.

 

            So the UML of Student is now:

 

abstract Student

# final int NUMBER_OF_SCORES

# int [] scores

# int [] weights

# String courseGrade

+ Student

+ Student(String name)

+ String getCourseGrade()

+ getScore(int scoreNumber):int

+ setScore(int scoreNumber,int score):void

+ setWeight(int weightNumber,int weight):void

+ getWeight(int weightNumber):int

+ abstract computeGrade()

 

 

With this definition, the line of code:

 

 

roster[6] = new Student(“Seventh Student”);

 

would generate a compiler error of “One can not instantiate an abstract class”.

 

            Suppose we have case II above:  There are students who are neither graduate or undergraduate.  There are two approaches:

 

(1)   Remove abstract from the Student class and make the method of computeGrade a non-abstract method by deciding how non-degree seeking students will pass. 

(2)    Keep Student abstract and make a new class derived from student, say NonDegreeSeeking and implement the method of computeGrade in this class.

 

 

Which is better? There is no easy answer in general.  It depends on the situation. To determine which approach is better for a given situation, we ask ourselves which approach allows for easier modifications and extensions.  From our discussion above, for this case, I would keep Student as an abstract class and make the other classes as derived classes of Student.  But other situations might lead to choice of (1) above.

 

Note that not all methods can be declared as abstract.  Neither static nor private methods can be abstract.

 

 

 

Composition versus Inheritance

 

 

At first glance inheritance appears to be a convenient technique for using instance variables and methods from another class (the super class).

 

A good place to investigate inheritance is with the read-world concept of specialization.  Consider the following:

 

An automobile is a special case of transportation vehicle

A shirt is a special case of clothing

A bald eagle is a special case of bird

A triangle is a special case of polygon

 

These relationships are known as is-a relations, because one is a specialized version of the other.  An is-a relation is an obvious situation for the use of inheritance.

 

            Not all relationships between two classes are is-a relations.  Another common relationship is known as a has-a relation.  (This is alternatively known as a contains-a relation.)  As the name implies, a has-a relation occurs when an instance of one class is contained within another class.  Consider the following:

 

            An automobile has a steering wheel.

            A shirt contains a pocket.

            A bald eagle has a wing.

            A triangle has a vertex.

 

The way one implements a has-a relation in Object Oriented languages is with composition (or aggregation).  We have been doing this already.  An example is:

 

class Card

{

            //define a playing card

}

 

class DeckOfCards

{

            Card [] deck = new Card[52];

}

 

            To define a car, we would have

 

Class car

{

            Wheels [] wheels;

            Doors [] doors;

            .

            .

            .

}

 

class Door

{

            Window window;

            DoorHandle doorHandle;

            .

            .

}

 

            To test your understanding of the difference between is-a and has-a, consider the following 5 criterion:

 

  1. Left class is-a right class.
  2. Right class is-a left class
  3. Left class has-a right class.
  4. Right class has-a left class
  5. None of the above

 

  1. furniture-desk
  2. desk-drawer
  3. hammer-handle
  4. hammer-nail
  5. boat-canoe
  6. bass-fresh water fish
  7. stringed instrument-base
  8. tuba-brass instrument
  9. tube-trombone
  10. cockpit-airplane
  11. footware-shoelace
  12. footware-boot
  13. footware-shoe store
  14. retail establishment-shoe store
  15. plant-leaf
  16. rose bush-plant
  17. rose bush-thorn
  18. tea cup-saucer
  19. tea cup-crockery
  20. book-table of contents
  21. book-library
  22. beverage-coffee
  23. liquid-beverage
  24. liquid-coffee
  25. anima-elephant
  26. tail-elephant

 

 

Many times the difference between is-a and has-a is evident.  However, there are times when it is not so evident.  Consider the class of MilitaryTime, which keeps the time in 24 hours, i.e., there is no AM or PM, but 2 PM is 14 hours.

 

MilitaryTime

- int hour

- int minute

- int second

+ MilitaryTime()  //default time set to midnight

+ MilitaryTime(int hours,int minutes,int seconds)

+ void addHour()   

+ void addMinute()

+ void addSecond()

+ int getHour()

+ int getMinute()

+ int getSecond()

+ void advanceTime() //use system time to add one second to current time via addSecond

 

 

 

 

Suppose I want to create a class TimeWithMilliseconds, where I add the values of milliseconds (1/1000 of a second) to the time kept.  There are two ways to handle this:

 

(1)    Inheritance

 

TimeWithMilliseconds extends MilitaryTime

# int milliseconds

+ TimeWithMilliseconds()

+ TimeWithMilliseconds(int hours,int minutes,int seconds, int milliseconds)

+ void addMillisecond()

+  int getMillisecond()

+ void advanceTime() //use system time to add one millisecond to via addMillisecond

 

 

(2)    Composition

 

 

TimeWithMilliseconds

- MilitaryTime hourMinuteSecond

- int millisecond

+ TimeWithMilliseconds()

+ TimeWithMilliseconds(int hours,int minutes,int seconds, int milliseconds)

+ void addMillisecond()

+  int getMillisecond()

+ void advanceTime() //use system time to add one millisecond to via addMillisecond

 

 

Note the code of addMillisecond in the composition case would be:

 

public void addMillisecond()

{

            millisecond++;

            if (millisecond = = 1000)

            {

                        millisecond = 0;

                        hourMinuteSecond.addSecond();

}

}

 

The inheritance case would not need reference to the inner class, but would just need to call the inherited method addSecond.  It, however, would override the method advanceTime.

 

            Both implementations are valid in this case.  Inheritance (to me) seems to be the better choice because the use of the class hourMinuteSecond each time seems to be superfluous.  I.e., we have not given in the UML of the composition the methods of getHour, getMinute, getSecond, etc, whose code would be like:

 

public int getHour()

{

            return hourMinuteSecond.getHour();

}

 

(Note, we could also just member hour instead of the method getHour.

 

            To see that we understand the ideas of is-a and has-a let us look at another time example.  Consider the class of TimeDuration whose instances are “one hour”, “one and a half hours”, etc.  Another class is TimeInterval, whose instances are “1:00-2:00” or “2:00-3:00”, etc.

 

Question: Is TimeInterval a subclass of TimeDuration, i.e., are all TimeIntervals TimeDurations?

 

            According to the identity criterion for TimeDuration, two durations of the same length are the same duration, i.e., are the same object.  All one hour durations are identical, so that there is only one “one hour” duration.  On the other hand, according to the identity criterion for time intervals, two intervals occurring at different times, even if they are the same length, are different.

 

            So TimeInterval is not a subclass of TimeDuration because two separate instances of TimeIntervals are the same instance of TimeDuration.

 

            This is one of the common confusions of natural languages.  When we say “all time intervals are time durations”, we really mean “all time intervals have a time duration”.  The duration is a component of time interval:

 

class TimeInterval
{

            TimeDuration t;

            .

            .

}

 

            What about species/animals.  Consider mammals and humans? Is there a relationship between them?  Yes, one is a subclass of the other.  What about a particular human, say “Chris”?  Chris is an instance (instantiation of human).

 

 

 

 

Mammal

 

 

Human

 
 

 

 

 

 

 

 

 

 

 

 

 

 


                                    Chris  

 

 

 

Where                     means inheritance, and                      means instance of. 

 

Question: Where does species fit into this picture?

 

 

            Look at the identity criterion.  How are two species the same? Via a biological taxonomy, e.g., genus and differencia, e.g., human is homo sapiens.  How do we differentiate between two humans?  One way is time and location: two humans are different if they are at different places at the same time.

 

If human was a subclass of species, it would inherit its identity criterion.  But genus and differencia do not help to distinguish two humans.  Therefore, species do not generalize (subsume) humans.  Humans are an instance of species.

 

 

 

 

 

 

 

Mammal

 

 

Human

 
 

 

 

 

 

 

 

 

 

 

 

 

 


 

Species

 
                                                Chris