A Tale of Two Cats

Let’s revisit our Cat and HouseCat friends. In java-web-dev-projects, open inheritance/testing-inheritance/tale-of-two-cats/src/main/java and examine the two classes inside.

Inheriting Fields and Properties

Notice that Cat has a private string field family, representing the biological family of all cats.

private String family = "Felidae";

This field is not directly accessible by HouseCat since it is private. However, it may be read via the public getter getFamily.

public String getFamily() {
   return family;
}

There is no setter for family, however. It may only be set within Cat. It makes sense that a subclass should not be able to change the biological family of a cat, since this field should rarely, if ever, change.

Methods of the base class Cat may be called on instances of the subclass HouseCat as if they were defined as part of the HouseCat.

Try it out. In your inheritance folder, create a Main class. Within that class, write an instance of HouseCat and call some of the methods it inherits from Cat.

HouseCat garfield = new HouseCat("Garfield", 12);
garfield.eat();
System.out.println(garfield.isTired());   // prints true

The eat method was defined in Cat, but may be called on all HouseCat instances as well. We say: “HouseCat inherits the method eat() from Cat.” We know we have successfully called eat() on garfield because the printed statement indicates the cat is now tired.

super

We mention above that a subclass inherits all non-constructor methods from its base class. Indeed, when extending a class, we will not be able to create new instances of our subclass HouseCat using any constructors provided by Cat.

The base class Cat has a constructor that takes a single parameter of type double.

public Cat(double aWeight) {
   weight = aWeight;
}

But because HouseCat does not have such a constructor, the following code does not compile:

HouseCat mittens = new HouseCat(8.4);

Cat constructors are not inherited by HouseCat. If we want to use a Cat constructor in this subclass, we must explicitly provide it.

To do so, look at the constructor included in HouseCat:

public HouseCat(String aName, double aWeight) {
   super(aWeight);
   name = aName;
}

Here, the super keyword allows the subclass to access the constructor from the base class. This call to the base class constructor must be the first line of the subclass constructor. In the case of HouseCat, the subclass constructor extends the original constructor by setting an additional property, name.

If a base class constructor takes no arguments, then the no-argument constructor is implicitly called for you in the subclass. A no-argument, or no-arg constructor, is just as the name implies, a constructor that takes no arguments.

For example, we can add an additional constructor in Cat:

public Cat() {
   weight = 13;
}

Then in HouseCat, we can simply define another constructor as this:

public HouseCat(String aName) {
   name = aName;
}

Even though we don’t explicitly specify that we want to call a constructor from Cat, the no-argument constructor will be called. Now, we can initialize a new HouseCat with only a name property and the Cat no-argument constructor will still be applied. Back in Main, you can confirm that the base class constructor has been called:

HouseCat spike = new HouseCat("Spike");
System.out.println(spike.getWeight()); // prints 13

As a consequence of this constructor syntax, we can easily expose any constructor from the base class by providing a subclass constructor with the same signature and a body that only calls the base class constructor.

public HouseCat(double aWeight) {
    super(aWeight);
}
Warning

The above constructor is a bad one, and is included merely to introduce syntax and usage. We would not want to have a constructor for HouseCat that didn’t initialize an essential field such as name.

@Override

Sometimes when extending a class, we’ll want to modify behavior provided by the base class. This can be done by replacing the implementation of an inherited method by a completely new method implementation. For a given method, we can do this via method overriding.

In our example, the noise method of HouseCat overrides the method of the same name in Cat. When we override it, we should use the @Override annotation.

Here are the methods in question.

In Cat:

public String noise() {
   return "Meeeeeeooooowww!";
}

In HouseCat:

@Override
public String noise() {
   return "Hello, my name is " + name + "!";
}

If we have a HouseCat object and call its noise() method, we will be using the method defined in HouseCat.

Cat plainCat = new Cat(8.6);
HouseCat cheshireCat = new HouseCat("Cheshire", 12);

System.out.println(plainCat.noise()); // prints "Meeeeeeooooowww!"
System.out.println(cheshireCat.noise()); // prints "Hello, my name is Cheshire!"

Similar to other Java annotations, the @Override annotation is not required. However, it can prevent unintentional errors and make it clear when reading your code what you intend to do. The compiler will see the annotation and check to ensure that the signatures of the base method and the overriding method match up. If they don’t, it will flag an error. This can help prevent you from inadvertently creating a method with a different signature.

When overriding a method from a base class, the method signatures must be exactly the same. The signature of a method includes the method name, access level, return type, and the type and number of input parameters.

In addition, when overriding a method, we may call the method from the base class that we are overriding by using super. This calls the overridden method in the base class via super.noise(), carrying out the original behavior if the given conditional branch is reached.

Object Class

In a previous lesson, we introduced the special methods equals and toString. All classes contain default implementations of these methods that can be overridden. In fact, these default methods are part of a class called Object. If a class does not explicitly extend another class, then it implicitly extends Object. So the default implementations of equals and toString (along with a few other methods) are made available to us via inheritance.

Note that we should use the @Override annotation when we provide new implementations of these methods as well.

References:

Check Your Understanding:

Question 1: For this question, refer to the code block below.

public class Message {
   private boolean friendly = true;
   private String language;
   private String text;

   public Message(String aLanguage, String aText) {
      language = aLanguage;
      text = aText;
   }

   public boolean getFriendly() {
      return friendly;
   }

   public String getLanguage() {
      return language;
   }

   public String getText() {
      return text;
   }
}
Question-1

A class called Greeting extends Message. Greeting and Message are both defined within a package called Speech. Select all of the fields and methods that are inherited by Greeting.

  1. friendly
  2. language
  3. text
  4. Message
  5. getFriendly
  6. getLanguage
  7. getText

Question 2: For this question, refer to the code block below.

public class Message {
   private boolean friendly = true;
   private String language;
   private String text;

   public Message(String aLanguage, String aText) {
      language = aLanguage;
      text = aText;
   }

   public boolean getFriendly() {
      return friendly;
   }

   public String getLanguage() {
      return language;
   }

   public String getText() {
      return text;
   }
}
Question-2

A class called Greeting extends Message. What would a constructor for Greeting need to be to call the Message constructor?

A. public Greeting(String aLanguage, String aText, boolean isFriendly) {
      super(aLanguage, aText);
      friendly = isFriendly;
   }
B. public Greeting(String aLanguage, String aText) {
      super(aLanguage, aText);
   }
C. public Greeting() {
      super(aLanguage, aText);
   }
D. public Greeting(String aLanguage, String aText) {
      language = aLanguage;
      text = aText;
   }