7.5. Animal Example

In this section, we will work through another inheritance example involving animals.

Test Yourself

Take a moment to look over the UML class diagram below and answer the following questions. If you are unsure, do the best you can and write your thoughts in your notes.

  1. How many inheritance relationships are present in the diagram?

  2. In each inheritance relationship, which class is the parent and which is the child?

  3. Look at each inheritance relationship. Intuitively, does an inheritance relationship seem appropriate?

hide circle
set namespaceSeparator none
skinparam classAttributeIconSize 0

package "cs1302.animal" {
   Driver -up-> Animal: uses
   Driver -right-> Dog: uses
   Animal <|-- Dog: extends (is-a)

   class Animal {
     -genus: String
     -species: String
     +<<new>> Animal(genus: String, species: String)
     +getGenus(): String
     +getSpecies(): String
   }

   class Driver {
     {static} +main(args: String[]): void
   }

   class Dog {
     -breed: String

     +<<new>> Dog(breed: String)
     +getBreed(): String
   }

   hide Driver fields
}

Test Yourself Solution (open after answering the question above)
  1. There is one inheritance relationship. Dog extends Animal. The other relationships (uses) indicate a general dependency, not inheritance.

  2. Dog is the child class and Animal is the parent.

  3. When answering this question, ask yourself if the is-a relationship makes sense (the is-a test). Would you say that Dog is an Animal? Yes! So, the inheritance relationship is appropriate.

    Another way to think about it is whether or not it is appropriate for Dog to inherit the genus and species attributes and the getGenus and getSpecies methods. Again, the answer is “yes”!

To work through this example, perform the following steps:

  1. Change into the animal subdirectory of cs1302-inheritance.

  2. Inspect the .java files under src. In particular, familiarize yourself with the code for the Animal and Dog classes and compare the code to what you see in the UML diagram.

  3. Try the following before looking at the solution:

    Test Yourself

    Try Modifying Dog.java so that the Dog constructor invokes the parent constructor via super.

    Hint: You will need to call the Animal constructor from within the Dog constructor using the literal values "Canis" and "Lupus Familiaris" as the genus and species, respectively. This will setup the genus and species variable within any Dog objects that are created.

    Test Yourself Solution (open after attempting the above)

    The parent constructor (in the Animal class) takes two parameters: genus and species. Since we are invoking the Animal constructor on a type of animal where we know the exact genus and species, we can provide the literal values to the parent constructor.

    This line should be the first line in your Dog constructor.
    super("Canis", "Familiaris");
    

    Warning

    Since genus and species are declared with private visibility, you cannot access them directly (without a getter/setter):

    This code will not work in the Dog constructor. We need to call super instead.
    super.genus = "Canis";
    super.species = "Lupus Familiaris";
    

    Even if the visibility allowed you to write the lines above, you should avoid doing so! There is already code that sets up these variables. It is in the parent constructor. We should use super instead of duplicating that initialization code.

  4. Compile the cs1302.animal.Animal and cs1302.animal.Dog classes, specifying bin as the default package for compiled code. Since there is a dependency between those two classes, remember to properly specify the class path, as needed, when you compile.

  5. Modify the main method in the cs1302.animal.Driver class to create some Dog objects, then print out information about them:

    Add this code to the main method in Driver.java
    Dog bulldog = new Dog("Bulldog");
    System.out.println(bulldog.getGenus());
    System.out.println(bulldog.getSpecies());
    System.out.println(bulldog.getBreed());
    System.out.println();
    

    Note

    Because Animal variables are compatible with Dog objects, the datatype of the bulldog variable could be Animal. However, if you made this change, you would no longer be able to call the getBreed method since that method is not available in Animal.

  6. Add code in Driver.java to create Dog objects for the following breeds: poodle, beagle, and pug. Call getGenus, getSpecies, and getBreed on each breed as we did in the previous step.

  7. Compile the cs1302.animal.Driver class, specifying bin as the default package for compiled code and setting the class path as needed.

  8. Run the cs1302.animal.Driver class. You should see the following output:

    expected output from running cs1302.animal.Driver
    Canis
    Lupus Familiaris
    Bulldog
    
    Canis
    Lupus Familiaris
    Poodle
    
    Canis
    Lupus Familiaris
    Beagle
    
    Canis
    Lupus Familiaris
    Pug
    
  9. As you can see from the output, each Dog object does contain genus and species (inherited from Animal) that is set by the Dog constructor using the parameter values that you supplied. Furthermore, you can also see that each Dog object has the getter methods that it inherited and that they return the values of the inherited variables.

Reminder

If a child constructor does not explicitly call a parent class constructor via super, then Java will automatically add a call to super(), i.e., it will attempt to invoke the parent’s default constructor if it exists. We always recommend explicitly calling a parent constructor, even if it’s just super(), so that it’s clear from reading the source code what your intent is.

Here is a video outlining the steps in this section if you got stuck:

7.5.1. Review Question

Test Yourself: Writing a Child Constructor

Given the UML Class Diagram below, write the constructor(s) for the child class(es) in your notes. If the user inputs a negative value for an id, the constructor(s) should throw an IllegalArgumentException containing an appropriate message.

hide circle
set namespaceSeparator none
skinparam classAttributeIconSize 0

package "cs1302.people" {
   Person <|-- Student
   Person <|-- Professor

   class Person {
        -name: String
        -age: int

        +<<new>> Person(name: String, age: int)
        +getName(): String
        +getAge(): int
   }

   class Student {
        -studentId: int

        +<<new>> Student(name: String,
         \t\tage: int, studentId: int)

        +getStudentId(): int
   }

   class Professor {
        -employeeId: int

        +<<new>> Professor(name: String,
         \t\tage: int, employeeId: int)

        +getEmployeeId(): int
   }
}

Review Question 1 Solution (Open after answering the question above)
  1. The constructor for the Student class should look like this:

    /**
     * Initializes the instance variables of a new {@code Student} object.
     *
     * @param name the name of the student.
     * @param age the age of the student.
     * @param studentId the student's id number.
     * @throws IllegalArgumentException if the student id number is a negative value.
     */
    public Student(String name, int age, int studentId) {
    
       super(name, age);
    
       if (studentId < 0) {
          throw new IllegalArgumentException("The student id number must be nonnegative");
       } // if
    
       this.studentId = studentId;
    } // Student
    
  2. The constructor for the Professor class should look almost identical to the Student constructor with studentId replaced with employeeId. Can you think of a way to avoid redundancy by writing a method to check if the id is valid?