7.7. Abstract Classes

We will motivate this section using a thought exercise:

Thought Exercise

Given the UML diagram for the animal hierarchy used in the previous section:

hide circle
set namespaceSeparator none
skinparam classAttributeIconSize 0

package "cs1302.animal" {
     Animal <|-- Dog
     Animal <|-- Cat

     class Animal {
        -name: String
        -genus: String
        -species: String

        +<<new>> Animal(name: String, genus: String, species: String)
        +getGenus(): String
        +getSpecies(): String
        +makeSound(): void
        +describe(): void
     }

     class Dog {
        -breed: String

        +<<new>> Dog(name: String, breed: String)
        +<<override>>makeSound() : void
        +<<override>>describe() : void

        +getBreed(): String
     }

     class Cat {
        +Cat(name : String)
        +<<override>>makeSound() : void
        +<<override>>describe() : void
     }

     hide Cat fields
     hide Dog fields
}

Consider the following block of code:

String name = "Freddie";
Animal myAnimal = new Animal(name, ______, ______); // blanks 1 and 2
myAnimal.makeSound(); // print the sound the animal makes
  1. What would you expect to be printed when we call makeSound on an Animal object?

  2. What do you think belongs in blanks 1 and 2 corresponding to the genus and species of the Animal object? Can we specify this information?

Thought Exercise Discussion (open after considering the points above)

The method makeSound would either:

  1. Have a separate if-statement for each allowed genus/species (potentially dozens or hundreds!) to make a specific sound, or

  2. It would have to print a generic animal sound. Something like “the animal makes a noise” instead of “Bark!” (for a dog).

Scenario #1 is not ideal because every time we add a class to the hierarchy, we would have to go in and modify the Animal class’s makeSound method. When we add a new class, that should not force us to modify existing classes.

Scenario #2 is not ideal because we don’t get sounds that correspond to the type of animal we have created.

What about the genus and species? Well, its hard to specify these inputs if we are creating a generic Animal object because there are no correct values. If you wanted to put a specific genus and species, wouldn’t it make more sense to create a more appropriate type of object (a Cat, Dog, Elephant, etc.)?

Given the confusing nature of what it really means to create an Animal object, we would expect that users would always prefer to create objects of specific animal types. Even though the Animal class shouldn’t be instantiated, we still need to use it to gain the benefits of inheritance.

In Java, Abstract Classes are an elegant solution to this problem. Abstract Classes cannot be instantiated. However, they can still contain instance variables and methods that can be passed down to child classes. They can also include abstract methods (just like interfaces can)!

By declaring the Animal class abstract, we are telling the users of our hierarchy that they should not instantiate it (it doesn’t really make sense to do so) and that they should instantiate one of the child classes instead.

Take a moment to consider the updated UML Class Diagram below:

hide circle
set namespaceSeparator none
skinparam classAttributeIconSize 0

package "cs1302.animal" {
   Animal <|-- Dog
   Animal <|-- Cat

   abstract class Animal << abstract >> {
      -name: String
      -genus: String
      -species: String

      +<<new>> Animal(name: String, genus: String, species: String)
      +getName(): String
      +getGenus(): String
      +getSpecies(): String
      +<<abstract>>{abstract} makeSound(): void
      +<<abstract>>{abstract} describe(): void
   }

   class Dog {
      -breed: String

      +<<new>> Dog(name: String, breed: String)
      +getBreed(): String
      +<<override>>makeSound() : void
      +<<override>>describe() : void
   }

   class Cat {
      +Cat(name : String)
      +<<override>>makeSound() : void
      +<<override>>describe() : void
   }

   hide Cat fields
}

Note

By making the Animal class abstract, the users of our hierarchy cannot instantiate the Animal class. However, we still get the benefits of inheritance as any instance variables/methods created in Animal.java are automatically copied to the child classes and do not have to be repeated in each!

Test Yourself

Assume that in Animal.java, the implementation of makeSound looks like this:

in Animal.java
public abstract void makeSound();

Also, assume the following overrides in the child classes:

in Dog.java
@Override
public void makeSound() {
   System.out.println(this.getName() + " growls and then barks!");
} // makeSound
in Cat.java
@Override
public void makeSound() {
   System.out.println(this.getName() + " purrs and then meows!");
} // makeSound

What would be output from the following code block? Note the two different data types of dog and dog2.

What is the output given the code above?
Animal dog = new Dog("Juno", "Jack Russell Terrier");
System.out.println("Dog:\n");
System.out.println(dog.getGenus());
System.out.println(dog.getSpecies());
System.out.println(dog.getName());
System.out.println(dog.makeSound());

Dog dog2 = new Dog("Vince", "Mix");
System.out.println("Dog 2:\n");
System.out.println(dog2.makeSound());
Test Yourself Solution (Open after attempting the question above)
Dog:
Canis
Familiaris
Juno
Juno growls and then barks
Dog 2:
Vince growls and then barks