Return to index

Object-Oriented
Programming Basics 2

 

Topics covered include :

 

Abstract Classes

Consider greetings cards. These come in many types. You can buy valentine cards and birthday cards, for example, but not a non-specific card. 'Card' is an abstract idea of which there are actual instances only of subclasses.

If we kept all the cards received in the past in an old shoebox, we'd write 'cards' on the lid because all the items inside are types of card - not instances of card itself, but of postcards, anniversary cards etc.

An abstract class in Java is a class that cannot be instantiated but that can be the parent of other classes. Its purpose is to be a parent to several related classes. Grouping classes together is important for maintaining a good level of organisation, and abstract classes can help with the arrangement of classes into a well-organised class hierarchy.

Even though it can not be instantiated, an abstract class can define methods and variables that children classes inherit.

In the following example, each card class has its own version of the greeting() method - each one is implemented differently. It is useful to put an abstract greeting() method in the parent class. This says that each child inherits the "idea" of a greeting() method (and will use the same method signature), although each implementation will be different. Here is the class definition of the abstract class Card:

public abstract class Card {
      String recipient;     // name of who gets the card
      public abstract void greeting();     // abstract greeting() method
} 

This is the complete definition of this abstract class. Notice the abstract method. Abstract classes can (but don't have to) contain abstract methods. An abstract class can also contain non-abstract methods, which will be inherited by the children.

An abstract method has no body - that is, it has no statements. It declares an access modifier, return type, and method signature followed by a semicolon. A non-abstract child class inherits the abstract method and must define a non-abstract method that matches the abstract method signature.

An abstract child of an abstract parent does not have to define non-abstract methods for the abstract signatures it inherits. This means that there may be several steps from an abstract base class to a child class that is completely non-abstract.

Because no constructor is defined in class Card, the default no-argument constructor is automatically supplied by the compiler. However, this constructor cannot be used directly because no Card object can be constructed.

Not everything defined in an abstract class needs to be abstract. The non-abstract variable recipient is defined in Card and inherited in the usual way. However, if a class contains even one abstract method, then the class itself has to be declared to be abstract. So we could add a non-abstract method that returns the recipient's name. This could be overrided by a child class but need not be.

public abstract class Card {
     String recipient;     // name of who receives the card 
     public abstract void greeting();     // abstract greeting() method 
     public String getRecipient() {
          return recipient; 
     }
} 

Sometimes non-abstract methods can be 'empty' - they don't actually do anything - and so a child class is likely to override such a method and provide a full implementation.

Here is a class definition for class HolidayCard. It is a non-abstract child of the abstract parent class Card.

class HolidayCard extends Card {
     public HolidayCard(String r) {
          recipient = r;
     }

 

     public void greeting() {
          System.out.println("Dear " + recipient + ",\n");
          System.out.println("Season's Greetings!\n\n");
     }
} 

The class HolidayCard is not an abstract class. Objects can be instantiated from it. Its constructor implicitly calls the no-argument constructor in its parent, class Card, which in turn calls the no-argument constructor in Object. So even though it has an abstract parent, HolidayCard objects are as much objects as any other.

Here is a program to test the Card classes.

public class CardTester {
     public static void main(String[] args) {
          HolidayCard hc = new HolidayCard("Santa");
          hc.greeting();
     }
} 

Note that you can't do the following - it will not compile :

  public static void main(String[] args) {
       Card card = new Card();     // You cannot instantiate an abstract class
       card.greeting();
  } 

Because Card is an abstract class, the compiler flags this as a syntax error. Card does have a constructor that is (implicitly) invoked by its children, but it cannot be invoked directly. However, the following IS ok.

  public static void main(String[] args) {
        Card card = new ValentineCard("Joe");  // a ValentineCard IS A (type of) Card
        card.greeting();
  } 

You can instantiate class ValentineCard because it is not an abstract class. It is ok to save a reference to a ValentineCard object in a reference variable of type Card because ValentineCard is-a (type of) Card. You can think of the reference variable card as being a card rack designed to hold any instance of type Card.

 

Polymorphism

When a method is invoked, it is the class of the object (not of the reference variable) that determines which method is run.

This is what you would expect. It makes sense that the object's method is invoked, since, after all, the method is part of the object and the method uses the object's data. For example:

public class CardTester {
     public static void main (String[] args) {
          Card card = new ValentineCard("Jo");
          card.greeting();
          Card card2 = new HolidayCard("Bob"); 
          card2.greeting();
     }
} 

This will run the greeting() method for a ValentineCard, then it will run the greeting() method for a HolidayCard, then it will run the greeting() method for a BirthdayCard. The type of the object in each case determines which version of the method is run.

Polymorphism means "having many forms". In Java, it means that a single variable might be used with several objects of related classes at different times in a program. When the variable is used with "dot notation" - variable.method() - to invoke a method, exactly which method is run depends on the object that the variable currently refers to.

A variable can hold a reference to an object whose class is a descendent of the class of the variable. That is, the class of the object must be 'a descendant' of the class of the variable that holds a reference to that object. A descendant of a class is a child of that class, or a child of a child of that class, and so on.

Note, however, that this is only the case when the method exists in the class type of the reference variable. To run a method that only exists in the child class, we would need to class cast the reference first. We may also wish to check that the object is an instance of the child class we expect in the case that the reference variable could contain an object of several child class types.

For example (code snippet) :

Card[] cards=new Card[2];
cards[0]=new HolidayCard("Fred");
cards[1]=new ValentineCard("Kate");
for (int i=0; i<cards.length; i++) {
     Card card=cards[i];
     card.greeting();
     if (card instanceof ValentineCard) {
          card.addKisses();
     }
} 

Notice that we have used an array in the above program to hold a number of different card types. So naturally the array is of type Card.

Let's now change the loop part of the above code slightly to give :

Card card;    // we will reuse this object reference
for (int i=0; i<cards.length; i++) {
     card=cards[i];
     card.greeting();
     if (card instanceof ValentineCard) {
          card.addKisses();
     }
}


The object reference 'card' is reused on each loop and so at different times referers to different objects. This is a very important thing to remember about Java - object references are not 'holders' but 'pointers'. The statement card2=card will make card2 point at whatever card is
currently pointing to. If card is later changed to point to something else, card2 will still point to the object it was originally assigned.

 

Download and run ...

An improved version of the example project is available as a TJI project in the 'Resources' page of our web site.

Simply download the zip file and then choose 'Project Import' from the 'Project' menu. Select the zip file and the project will be imported and setup automatically. Now simply click on the 'Run' button to compile and run.

 

Note : This document was partly based on material written by Bradley Kjell of the Central Connecticut State University.

Return to index