Monday, June 2, 2025

Inheritance

 Everyone agrees that, for example, a Dog is an Animal, biologically speaking. In Java, just as in biology, this is called “inheritance.”


Without giving too many specifics, let’s say we want for the Dog class to receive some behaviors from Animal, or we want to slightly tweak Animal’s behavior as it comes across in a Dog.

Before we go any further, it’s important to realize the obvious fact that all Dogs are Animals, but not necessarily all Animals are Dogs. This matters when we are talking about inheritance.

Let’s say an Animal has:

  • a name—this is a String
  • an age—this is an integer
  • a type—this is a String
  • a color—this is a String
  • a munch(int) method—this represents that the Animal can eat, and takes in a number; “much” is then printed that many times, each time on a new line
  • and a makeSound(int) method—this represents that the animal can make a sound, and takes in the number of times that sound should happen; the default sound is “moo”

We like all that in Animal, but obviously, Dogs don’t “moo”, they “woof.” Through inheritance—linking Animal and Dog in a way that gives Dog Animal’s properties for free—we can then say “in the special case that an Animal is a Dog, change the makeSound() behavior to ‘woof’ from ‘moo.’”

We imply the “is a” relationship (as in, “a Dog is an Animal”) with the extends keyword in Java.

  • Implement Animal
  • Then start working on Dog, declared as public class Dog extends Animal
  • Then, when you create a Dog object—let’s say, by Dog lucky = new Dog();, you can call lucky.makeSound(3); and you will see
    Moo
    Moo
    Moo


But Lucky (my actual dog) is a Golden Retriever, not a cow, so he does not moo.

Inside Dog, we can then change this behavior by declaring the following method: one that has the same signature as makeSound() did in Animal, but appears exclusively in Dog.

Inside that, we can print “Woof” instead, and when lucky.makeSound(n) is called, we will see many “Woof”s instead of many “Moo”s. 

There are two things we can do with those method signatures: overload and override.

  • Overloading is when you use the same name with different parameters, like two versions of an add() method, one that takes in ints and another that takes in doubles
  • Overriding, on the other hand, is when Dog extends Animal and Dog’s makeSound() says “Woof” when the generic Animal makeSound()  says “Moo”—you change the behavior in the more specific class to fit your needs, from what it was in the more generic.
It is a fact that everything you ever create—a MathLesson, a HelloWorld, a Puppy, a Student—is a child of Object. A MathLesson is an Object; a HelloWorld is an Object; and so on. This is so universally true that it is never explicit.

Since you get access to all the public fields and methods of an object upon inheritance from that—this is why Dog objects can, for free, get the default behavior of makeSound() and the other Animal fields and methods—every object ever has several common methods before anything is ever explicitly defined.

Two of the most common methods from Object are:

  • hashCode(): which feeds the object in question (that is, one particular FluffyUnicorn) through a function that is only easily computable in one direction (and nearly impossible in the other, with the caveat that checking the original direction’s result is just as easy), to get a unique identifier for that object
  • toString(): which finds some way—by default, with where the object is in memory—of converting the object to a String for visual representation

If I have a CollegeStudent who extends Student (explicitly, and, of course, Object, implicitly), where CollegeStudent samJones is in memory is of next to no value to anyone. I would much rather know what samJones.major, samJones.year, samJones.college, samJones.GPA, etc., are. So, many times, people override the toString method to something much more useful, like a “{“ plus something like “GPA:“ + samJones.GPA, and then comma-separating the name of the property and its value, before closing out with a matching “}”

like, for instance

    @Override

    public String toString() {

        return "CollegeStudent{" +

                "name='" + name + '\'' +

                ", idNumber=" + idNumber +

                ", age=" + age +

                ", creditHours=" + creditHours +

                ", points=" + points +

                ", GPA=" + String.format("%.2f", getGPA()) +

                ", courses=" + courses +

                '}';

    }

which might produce an output something like CollegeStudent{name='Alice Smith', idNumber=12345, age=20, creditHours=30, points=90, GPA=3.00, courses=[Math 101, English 201]}

The @Override line is called an “annotation.” They’re good practice but omitting them is not an error that will trip the compiler. They simply tell the compiler that some default behavior is being replaced by more desirable behavior.  

Overloading and overriding are dealt with by the compiler at different times: overloading at compile-time and overriding only at runtime.

Extending a parent class into a child (in “Dog extends Animal,” Animal is the parent class of Dog, since all Dogs are Animals but not vice versa) gives you access to anything public in the parent class, for free. It also means that, for instance

Animal myAnimal = new Dog();

Is valid, since every Dog is an Animal, but that

Dog myDog = new Animal() is not allowed, since not every Animal is guaranteed to be a Dog.

This (correct usage, like Animal myAnimal = new Dog()) is an example of polymorphism in Java, which is a really important concept. I cannot stress enough how critical it is to understand this relationship, and why the first of these two last code snippets works, and why the second one does not.

Finally, let’s say I have a class Percheron. There are some properties in Horse I would like to get, and some other particular properties in DraftHorse I would like to get. In some languages, you could certainly say

public class Percheron extends DraftHorse, Horse

and be totally fine.

However, this is called “multiple inheritance” and, except for the fact that everything implicitly inherits from Object, this (inheriting from both Horse and DraftHorse) is not allowed at all in Java. You must pick whether Percheron should inherit from Horse or DraftHorse, and then if it extends any of them, it must extend at most one of them. 


No comments:

Post a Comment

Switch

 Other than if/if-else/if-else if-else and the ternary operator, there is yet another common and important conditional expression in Java th...