7
$\begingroup$

Given the following:

Dog rover = new Puppy("rover");  // Where Puppy extends Dog 

A student asked why would you ever want or need a Dog variable point to a Puppy object? The only answer I could come up with is the following:

Suppose we have a game program with Dogs and Puppies. A Puppy may have some special attributes and methods that a Dog does not have. Over the course of the game we may want our variable "rover" to be able to point to a Dog instead of a Puppy.

I'm comfortable with this answer but are there other kinds of use cases where having a Parent variable point to a Child class would be useful?

UPDATE: Thank you everyone for your answers. It's not clear that I'm communicating the question properly or that respondents understand the question I'm trying to ask. Let me try again:

Dog Rover = new Puppy("Rover");   //case 1
Puppy Rover = new Puppy("Rover"); //case 2

In both cases Puppy extends Dog. Other than the reason stated in the original post are there any use cases where case 1 allows us to do something that case 2 would not?

It seems in the answers provided (certainly for the ones using an ArrayList) we can add to the ArrayList either case 1 or case 2 and have the same functionality. In other words, there is no inherent advantage of one case over the other.

Whether we use a Dog variable or a Puppy variable we will still access to the overridden methods, and since a Puppy is a Dog we may add either to a ArrayList.

I'm just trying to confirm that I'm not missing something. If I am missing something, a really clear example would be greatly appreciated.

$\endgroup$
5
  • 17
    $\begingroup$ Be careful with your language. There is no "dog object". There is a variable (of type Dog) that points to a Puppy object. I also think dogs and puppies may not be the best example of inheritance. $\endgroup$
    – corvus_192
    Commented May 3, 2023 at 17:38
  • 1
    $\begingroup$ @corvus_192 if a Puppy "is a" Dog, then this is perfectly idiomatic polymorphic inheritance. $\endgroup$ Commented May 4, 2023 at 0:27
  • 2
    $\begingroup$ There is also nowhere a "parent class pointing to a child class" in here. This is a variable typed as the superclass that points to a subclass. $\endgroup$ Commented May 4, 2023 at 14:06
  • 5
    $\begingroup$ The apparent confusion between classes, objects, and variables is concerning. It's going to make it a lot harder for your students to understand such ideas if you use these very specific and fundamentally different terms interchangeably. $\endgroup$
    – JimmyJames
    Commented May 4, 2023 at 16:40
  • 1
    $\begingroup$ The example code is prone to immediate misunderstanding. A puppy is a dog, but its puppy-ness is decided by its age, not an intrinsic quality of the creature itself. At some point a puppy ceases to be a puppy but remains to be a dog. There is no language concept that accounts for this. As the educator, you are responsible for your examples being correct (if simplistic) representations of what you're trying to teach, and I believe you set yourself up for failure with this example. You're also mixing your topics between overriding methods and using polymorphism, which are distinct concepts. $\endgroup$
    – Flater
    Commented May 10, 2023 at 0:35

16 Answers 16

14
$\begingroup$

Just to add on to Bryan R's response, upcasting is typically automatic, as a side-effect of data structures or methods that store or utilize a parent type. There's not much point to the example in the question.

You'll typically find code more like this:

Puppy rover = new Puppy("rover");  // Where Puppy extends Dog 
walk(rover);

Where walk, elsewhere, would be defined as:

public void walk (Dog d) {
   // whatever code you'd like for dog walking
}

All use of d within the walk method has been upcasted just by nature of calling walk.

Alternatively, you might do:

List<Animal> astoundingMenagerie = new ArrayList<Animal>();
astoundingMenagerie.add(rover);

Once again, rover will be upcast by nature of being added to astoundingMenagerie.

When I teach this stuff in my topic, I actually keep examples like this close at hand, because the question of why really is at the core of how students make sense of new material. I will explain the purpose early on, and also explain the purpose of examples like you've placed in your question: they are a reasonable way to help students get a feel for the mechanics of polymorphism even though they are not really reflective of code that students will typically see in the field.

As we go through, I'll periodically re-announce that this particular code is silly, but let's see if we can understand what it's doing.

Eventually, we come back around to healthier use, but with the mechanics safely mastered.

$\endgroup$
2
  • $\begingroup$ I think it's worth showing how you can assign this to various less-specific types including Object and then show that they all produce the same toString() output, and are the same instance using both equals() and ==, $\endgroup$
    – JimmyJames
    Commented May 4, 2023 at 16:53
  • $\begingroup$ "upcasting is typically automatic, as a side-effect of data structures or methods that store or utilize a parent type" In Java (for Object types) casting is nothing more than a check that the types are compatible. Since there's no way for an upcast of this kind to fail (aside from some weird classloader esoterica) I think it's better to think of it as a no-op, than 'automatic'. $\endgroup$
    – JimmyJames
    Commented May 4, 2023 at 17:09
7
$\begingroup$

A common case is if there are multiple subtypes of Dog, you might create an ArrayList which can hold any type of dog.

Or you might have a method that takes a reference to a Dog (doesn't care what subtype of dog) and does something with it. So, you will pass in a Puppy object but will now be referring to it as a Dog object.

$\endgroup$
5
$\begingroup$

This sort of thing happens all the time—is guaranteed to happen‚—if Dog is an interface or an abstract class. You've got this type, Dog that is declared because there are operations that you can perform on any kind of dog (e.g., stroke, call, feed, scold) but it is impossible to instantiate any object that is purely and only a Dog. The only objects you actually can instantiate are some specific kind of dog.

$\endgroup$
0
4
$\begingroup$

I think we often confuse the idea of inheritance when we start thinking about it as one comes before the other. Really it's just a tool to reuse functionality and handle things in a uniform way.

Consider the example of a staffing system at a company.

A base class could be "Employee" which contains all functionality that every employee can do. We can then have various other classes, security, HR...etc that have elevated functions.

There could be a function to raise an issue with HR. This is a function of all employees, but it will have to make use of a HR class employee.

Therefore the base class "Employee" will have to refer to the sub-class "HR".

$\endgroup$
1
  • $\begingroup$ this is a bad design for a real system, by the way, but okay for a first attempt $\endgroup$ Commented May 5, 2023 at 2:30
4
$\begingroup$

As written, you can understand the student's confusion:

Dog rover = new Puppy( "Rover" );

You can "see" that you're creating an instance of Puppy, so why would you put it into a Dog variable?
Answer? Polymorphism. OK, maybe not Week 1, but you can work up to it.

Perhaps some clearer examples might be:

Dog rover = petStore.buy( "Rover" ); // You could buy *any* kind of dog. 

Dog rover = new ThreeLeggedDog( "Hopalong" ); 

Any dog can be asked to walk but each will do so in its own way (i.e. according to its own implementation of the walk method. If a particular kind of "dog" cannot do so , then its implementation should throw an Exception. This follows the Object Oriented design principle, "Tell, don't Ask".

Dog dog = new Puppy( "Rover" ); 
dog.walk();                     // Off he goes! 

dog = new ThreeLeggedDog( "Hopalong" ); 
dog.walk();                     // Not as pretty, but gets there

dog = new DogFish( "Floop" );   // I know, I know; but it's better than NoLeggedDog!
dog.walk();                     // Throws a CannotWalkException.
$\endgroup$
3
$\begingroup$

This is the same as when we very commonly do

Collection<Whatever> toys = new Arraylist<>();

Why do we declare toys as a Collection yet then instantiate an ArrayList?

Reasons can include:

  • The implementation we want might change and we want to decouple the implementation from the use of the object
  • Collection is an interface, so cannot be directly instantiated
  • We want to build a collection of these objects and it is immediately clear we're better off using a super class.

Concretely, this code does not need to change if we change list implementation or even start using a set:

private Collection<Whatever> processStuff(Collection<Whatever> c) {
    c.forEach(...);
}

In general you want to be agnostic as possible to implementation so your code is less brittle.

$\endgroup$
3
$\begingroup$

This applies mostly to Java, though you can do something similar in Python.

But my preference in this case is for Dog to be an interface, not a class. The interface defines the public protocol for messaging "Dog" objects in classes that implement Dog.

Moreover, a variable in Java can have an interface type and it is perfectly natural to do so in this case. You are saying that the rover variable is currently referencing a Puppy object, but could reference any other sort of Dog.

This gives a slightly more abstract view of your program.

If you have a lot of concrete classes that extend one another, especially if they extend the public methods (i.e. the interface) then programming becomes harder as you have to keep track of the specifics at every point in the program. "Is this really just a Dog or is it really some subclass object.

In my (experienced) view, every explicit cast to a variable, allowing access to subclass methods, is a "Code Smell". A flaw in the program, just as is a deeply nested if-elseif-else structure. Cleaner code is possible and it is easier to maintain.

And, as to terminology in what you wrote, a Dog object doesn't point to a Puppy object, The variable rover is a reference to an object, not an object.

In Python you create "interfaces" as classes in which the methods have empty bodies: pass. There is a syntactic difference, but not a semantic one.

$\endgroup$
3
  • $\begingroup$ "In Python you create "interfaces" as classes in which the methods have empty bodies" Who does this and why? For example, I've never seen a 'sequence' type defined anywhere. $\endgroup$
    – JimmyJames
    Commented May 4, 2023 at 16:46
  • 1
    $\begingroup$ Actually, I do this as an organizing principle. It is useful in programs with objects that are similar and need the same protocols for access. There are other options than empty bodies. You can throw an exception which guards against missing an implementation. $\endgroup$
    – Buffy
    Commented May 4, 2023 at 17:45
  • $\begingroup$ My bad. There is a sequence type here. Not exactly what you are describing but you could use these 'empty' types for structural type checking, I suppose. I don't use type hints for static checking so I learned something new and found some good reading material. $\endgroup$
    – JimmyJames
    Commented May 4, 2023 at 17:49
2
$\begingroup$

A variable may contain different values at different times. For instance, we could have:

Dog petersPet = new Poodle("Tiny"); 
// but then, petersPet dies!
// so he gets a new one:
peterPet = new Labrador("Spot"); // surely Labradors are more robust?

Or a variable might contain a different value in different circumstances:

Dog petersPet;
if (peter.isRich()) {
    petersPet = new Labrador("Spot");
} else {
    petersPet = new Poodle("Tiny");
}

Or we might want to intentionally write our code agnostic of the particular breed, so we can reuse it for different breeds. For instance, if we write:

class PoodleParlour {
    public void pamper(Poodle poodle) {
        // ...
    }
}

our parlor won't get much business, because it only admits a single kind of Dog. If Peter gets a Labrador, he needs to find a new parlor. And woe is him if nobody has thought to open a LabradorParlor yet! On the other hand, if there was a

class DogParlour {
    public void pamper(Dog dog) {
        // ...
    }
}

that parlour could serve all dog owners, ensuring that every dog can be pampered. (Of course, there is a trade off here, because a DogParlour able to handle all kinds of breeds might be harder to make that one that can only handle a particular breed ...)

PS: You may have noticed that I changed your example to refer to breeds rather than Puppies. The reason is that in most object oriented programming languages, the type of an object is immutable throughout its lifetime. Your design of creating a different class for Puppies is therefore problematic, because it implies that Puppies remain Puppies throughout their lifetime. In particular, it means that when the dog comes of age, a new AdultDog object would have to be created, and everyone who knew about the Puppy would have to be told to use the AdultDog instead. And if you forget one, that person will continue to use the Puppy, causing different objects with possibly different states to exist for the same dog. That gets messy real quick!

$\endgroup$
2
  • $\begingroup$ isn't this really the same case OP provided in the original post? $\endgroup$
    – rss81
    Commented May 5, 2023 at 1:09
  • 2
    $\begingroup$ @rss81 You are the OP. Why are you referring to yourself in the third person? I don't understand this question; should it not be the same case? I think it should (and is), otherwise it's not answering the original question very well. And I think it's one of the better answers, actually. It's one of only 3 (so far) that actually answer it. $\endgroup$
    – Corrodias
    Commented May 5, 2023 at 1:33
2
$\begingroup$

The three best reasons I can think of as to why this principle is important in practice is the following:

  • Collections
  • Interfaces (Liskov substitution principle)
  • Factory Functions

As for how to teach these concepts to CS 101 students with the Dog and Puppy example, I'd go with the following.

(Forgive me if there are some bugs in the following code, as my Java is a bit rusty.)


Collections

At first glance, it might seem odd to assign a value to a variable when the value and the variable aren't the exact same type. I mean, this is just weird:

Dog dog = new Puppy("Rover");

I mean, if you know dog is a Puppy, why not just make the dog variable a Puppy type?

Ah, but what if you were working at a shelter and had more than one dog of various ages?

ArrayList<Dog> dogs = ?

All puppies are dogs, but not all dogs are puppies. Sometimes you need to give a dog special treatment based on whether it's an adult or just a puppy. On the other hand, you still want to keep track of them all, and it would be inconvenient to have a different list for every type of dog.

This is where inheritance comes in. Because every Puppy is a Dog, you can have puppies and adult dogs in the same list with no issue. And thanks to polymorphism, you can even give them their special treatment.

class Dog {
  ...
  public void feed() {
    System.out.println("Gave " + this.getName() + " 1 can of regular adult dog-food.");
  }
}

class Puppy extends Dog() {
  @override
  public void feed() {
    System.out.println("Gave " + this.getName() + " 1 can of special Puppy-Gro food.");
  }
}
ArrayList<Dog> dogs = new ArrayList<>() {{
  add(new Dog("Fido"));
  add(new Dog("Rover"));
  add(new Puppy("Fido"));
  add(new Dog("Max"));
  add(new Puppy("Chief Masterson"));
}};

There's no reason why you can't extend this concept to other types of dogs, too. In addition to Dog and Puppy, you can have OldDog, ThreeLeggedDog, SmallDog, SuperFullOfEnergyDog, and Labradoodle, with each one extending and overriding Dog in their own way to ensure that each kind of dog gets its special treatment but can still happily co-exist with the other dogs in the ArrayLst.


Interfaces (LSV)

The more nebulous concept of interfaces might be a bit beyond a 101 student, but their usage should still be understandable. Imagine we had another class that did something with a Dog that was given to it:

class DogFeeder {
  public void feed(?) {
    ?
  }
}

In order to feed every dog the food it needs, we would need to create a function that took each kind of dog and treated it accordingly:

public void feedPuppy(Puppy puppy) {
  System.out.println("Gave " + puppy.getName() + " 1 can of Special Puppy-Gro food.");
}

public void feedOldDog(OldDog oldDog) {
  System.out.println("Gave " + oldDog.getName() + " 1 can of Goodest-Boi Max Comfort food.");
}

public void feedLabradoodle(Labradoodle labradoodle) {
  System.out.println("Gave " + labradoodle + " 1 can of Finest Imported French Cuisine For Dogs food.");
}

This quickly gets cumbersome. We would need to define a new function for every type of dog that existed.

However, looking at the strings that get printed, there is also a definite pattern. We can utilize that and once again employ polymorphism to greatly simplify our feed methods down to just a single function:

class Puppy extends Dog {
  ...
  @override
  String getFavoriteFood() {
    return "Special Puppy-Gro";
  }
}

class OldDog extends Dog {
  ...
  @override
  String getFavoriteFood() {
    return "Goodest-Boi Max Comfort";
  }
}

class Labradoodle extends Dog {
  ...
  @override
  String getFavoriteFood() {
    return "Finest Imported French Cuisine For Dogs";
  }
}
public void feedDog(Dog dog) {
  System.out.println("Gave " + dog.getName() + " 1 can of " + dog.getFavoriteFood() + " food.");
}

Factory Functions

Now a student might look at this:

Dog dog = new Puppy("Rover");

And wonder why we couldn't just do this:

Puppy puppy = new Puppy("Rover");

And sure, we could do that. But what if you had a method that returned a Dog and you didn't know what kind of Dog it returned?

? dog = Shelter.findDog("Rover");

What kind of dog is Shelter.findDog going to return? Can you assume it's a Puppy? Not really, since they might have an older dog named "Rover". But if we don't know it's a puppy, how are we supposed to know if we can treat it like a puppy?

Now imagine in this scenario you are the manager of the shelter. It's 6:00 PM, and it's time to feed all the dogs. You have a lot of dogs, though, so you need to delegate. You get one of your subordinates and assign them a dog named "Rover" to feed, reading off the next name on your checklist.

The dog might be a puppy, an old dog, or a labradoodle, and the kind of dog determines how they need to be fed. But as the manager, you don't really care about those specifics. All you care about is ensuring the dog gets fed. Let the employee figure out how to actually feed it.

In that case, it doesn't matter what type of dog it is. You can just say it is a Dog:

Dog rover = Shelter.findDog("Rover");
employee.assignToFeed(rover);

In this scenario, rover could be Puppy, OldDog, or any number of other subclass of Dog. But as the manager, you don't care what kind of dog it is. You just fetch the dog from the Shelter.findDog method and delegate it off to some employee that can worry about the specifics.



Afterword

At the end of the day, the student is right. There isn't much reason to do this directly:

Dog dog = new Puppy("Rover");

But that specific example isn't important to the lesson. What's important is the student understands that you can assign a Puppy value to a Dog variable. Because once they understand that, it opens up a whole slew of other practices and concepts that wouldn't be possible without that one simple fact.

Many times, programmers write code that doesn't care about the specifics of how some class operates (and, in fact, this is by design if the programmer is following SOLID principles). If the object looks like a Dog, eats like a Dog, and barks like a Dog, then often times there isn't a good reason to worry over whether it's a Puppy or a Labradoodle.

This is a fundamental concept that forms the foundation of abstraction, and abstraction is a big part of modern development.

$\endgroup$
2
$\begingroup$

When I was first learning Java, I encountered a variable of some concrete type being assigned to a variable of some interface type. I was a bit confused as to why someone would do that. A cohort explained it this way:

Assigning a variable of a concrete class to a variable of type interface means that the user of the interface variable is only interested in those facets of the concrete class that are exposed by the interface. Consider the Java Comparable interface. When used with a sorting method, it signals that the only thing one cares about in the concrete class is the ability to compare objects (i.e. the compareTo method). Everything else is irrelevant.

The same is true when you assign a member of a child class to a variable of the parent class. It means you are not interested in the additional functionality of the child class, only that which is shared with the parent class. When you invoke a method that is shared between the parent and child (i.e. the child overrides the implementation of the parent), you execute the child's method. This is exactly as expected as the concrete class is actually the child class, even though only the methods of the parent class are visible.

$\endgroup$
1
$\begingroup$

Ok, so I just read your edit about how the example of assigning a Puppy to a Dog collection is missing the point, because you don't need to declare dog as a Dog to achieve that.

And you're right.

So, here are some examples of why you may actually want to declare dog as a Dog instead of a Puppy.

1.- To invoke static methods of the appropriate type.

Yeah, yeah, we all know you shouldn't call static methods from variables. But you can. And, if for whatever reason you do so, the called method will depend on the declared type, not on the instance type.
Example:

class Dog {
    public static void petInfo() {
        System.out.println("Dogs are the best!");
    }
}

class Puppy extends Dog {
    public static void petInfo() {
        System.out.println("Puppies are baby dogs. Treat them well and they will grow up to be awesome dogs!");
    }
}

Of course, if with the above code you do this:

        Dog dog = new Dog();
        Puppy puppy = new Puppy();

        dog.petInfo();
        puppy.petInfo();

You will get this:

Dogs are the best!
Puppies are baby dogs. Treat them well and they will grow up to be awesome dogs!

But what if you do this?

        Dog doggy = new Puppy();
        doggy.petInfo();

If you do that, even though doggy is still a Puppy, you are treating it like a Dog, and so you'll get:

Dogs are the best!
  1. Using the same variable for another "the same" dog.

Usually, puppies grow up and become dogs. This is something they do on their own; you don't have to go to the pet store and give them your puppy so they give you an adult dog in return (not in my country, at least!).

So, something like this:

class Dog {
    String name;

    public Dog(String name) {
        this.name = name;
    }

    public Dog(Puppy puppy) {
        this.name = puppy.name;
        // Establish adult dog attributes based on puppy attributes
    }

    public Dog growUp() {
        System.out.println("I already did!");
        return this;
    }

    public void bark() {
        System.out.println("I am " + name + " and I say: WOOF!");
    }
}

class Puppy extends Dog {
    public Puppy(String name) {
        super(name);
    }

    public void bark() {
        System.out.println("I am " + name + " and I say: bark!");
    }

    public Dog growUp() {
        System.out.println("Wow! I feel stronger!");
        return new Dog(this);
    }
}

Behold: the miracle of life!

        Dog doggy = new Puppy("Doggy");
        doggy.bark();
        doggy = doggy.growUp();
        doggy.bark();

---

I am Doggy and I say: bark!
Wow! I feel stronger!
I am Doggy and I say: WOOF!
$\endgroup$
1
$\begingroup$
Dog rover = new Puppy("rover");

The main thing to consider here is that this is not the main purpose of polymorphism. It is indeed unnecessary to use the base Dog type here since you've hardcoded the Puppy type already.

But what about this:

Dog myNewDog = AdoptRandomDog();

You don't know whether you adopted a "normal" dog or a puppy.

And what about this:

public void Pat(Dog dog)
{
    // ...
}

You don't need to know whether a dog is a puppy in order to be able to pat them.

Which brings us back to:

Dog rover = new Puppy("rover");

Yes, the student is correct that this is not a good example of useful polymorphism.

However, while it's not very useful this syntax is correct, simply because it is a logical consequence of introducing polymorphism to the language, which is useful in the other examples I showed above.

$\endgroup$
1
$\begingroup$

I think, the answer is tangential to polymorphism and use of collections of objects others have mentioned. IMO, that doesn't get to the bottom of what's happening in real-world applications vs sample code.

Let's reiterate the question and try to answer it directly. Why would a variable be typed with a Parent class instead of the exact Child class being instantiated and assigned to it?

Dog rover = new Puppy("Rover");    // why this
Puppy rover = new Puppy("Rover");  // instead of this?

The thing is neither of these statements ever occurs in real-world applications. It's a sample code to demonstrate OOP principles for educational purposes. The only place where such code does occur is fixtures of automated tests.

In real apps it's not feasible to hard-code and statically compile direct object instantiation of domain entities. Instead, objects are being created dynamically in the following ways:

  1. Instantiate from user input (to persist in a database)
  2. Deserialize from data stored in the database

Both use cases cannot possibly enumerate and account for every Child class. The solution is to generalize intended functionality in a Parent class and leverage the polymorphism to invoke specifics of the respective Child classes.

The real code would look like so:

Dog dog = petStore.buy(dogName);
owner.pets.add(dog);

Note that neither the string literal "Rover" nor the variable named rover of type Puppy make sense in the dynamic code that should be able to handle an arbitrary pet. A generalized variable Dog dog represents a user-defined dog of any kind where the variable dogName is initialized dynamically from the user input.

Dog rover is one step closer to the dynamic implementation than Puppy rover. The author is having the dynamic implementation in the back of their head while providing a static example.

$\endgroup$
0
$\begingroup$

A common use-case for me is having a single Dog collection as an ItemsSource for a CollectionView (or ListView) and then using an DataTemplateSelector to decide how to show a Puppy vs a Dog in the View. This also makes it a personal choice whether Dog is a class that Puppy inherits or Dog is an interface that Puppy implements.

$\endgroup$
0
$\begingroup$

The example code is prone to immediate misunderstanding. To cut a long story short, I don't think you should be trying to fix students' understanding of your example by answering their questions, I think you should scrap the example you're using and come up with something that aligns to what you're actually trying to teach.

Why it's a bad example

A puppy is a dog, but its puppy-ness is decided by its age, not an intrinsic quality of the creature itself. At some point in time, a puppy ceases to be a puppy but remains to be a dog.

There is no language concept that accounts for this. As the educator, you are responsible for your examples being correct (if simplistic) representations of what you're trying to teach, and I believe you set yourself up for failure with this example.

You're also mixing your topics between overriding methods and using polymorphism, which are distinct concepts. The goal of doing Base var = new Derived() is not related to the idea of overriding base methods in an unobtrusive way.

A better example

Inheritance is an intrinsic quality that is immutable. A puppy will cease to be a puppy, but a golden retriever will always be a golden retriever. Therefore, dog breeds are a better example.

public class Dog {}

public class GoldenRetriever : Dog {}

public class Beagle : Dog {}

Demonstrating the right behavior

You should also use your example to highlight the behavior that actually makes a difference. For example, what is meaningfully different between this:

Dog rover = new Beagle();

and this:

Beagle rover = new Beagle();

Overridden methods are not the difference, because overridden methods are methods that both Beagle and Dog have access to.

What makes a difference here is methods that Beagle has access to but Dog does not. In other words, methods of Beagle that do not override any prior Dog methods.

I picked Beagle specifically because it is a hunting dog and not every dog can hunt. This presents a good example on such a non-overridden method.

public class Dog 
{
    public void Pet();
}

public class Beagle : Dog
{
    public void Hunt();
}

Beagle rover1 = new Beagle();
rover1.Pet();
rover1.Hunt();

Dog rover2 = new Beagle();
rover2.Pet();
rover2.Hunt();  // Error!

Explaining the right reason

Suppose we have a game program with Dogs and Puppies. A Puppy may have some special attributes and methods that a Dog does not have. Over the course of the game we may want our variable "rover" to be able to point to a Dog instead of a Puppy.

In a vacuum, your explanation is something that relates to how we use inheritance.

However, you didn't really answer the question that was asked, because the question was being asked about your example, and your example does not demonstrate the intention to assign different dog instances to this variable.

A more appropriat answer to address the student's confusion would be:

You're right, in the current scenario it doesn't make sense for us to restrict the type from Puppy to Dog. We lose information and gain nothing from doing so. However, this is a basic example to get you to understand how it works, not why we use it.

Consider the following example:

public class DogHouse
{
    public Dog Dog { get; set; }
}

var rover = new Beagle();

var dogHouse = new DogHouse();
dogHouse.Dog = rover;

var dogFromTheDogHouse = dogHouse.Dog;

dogFromTheDogHouse.Hunt(); // Error!

This example highlights the key difference. Once you've downgraded your dog from a Beagle to a Dog, you lose the ability to address its Beagle-specific features.

$\endgroup$
0
$\begingroup$

In your example you had somthing similair to the following:

Dog
├─ Puppy
├─ AdultDog

Another example might be as follows:

// comments: drinking vessels, goblets, glasses, mugs and more....

Cup
├─ PlasticCup
├─ GlassCup
├─ RoughHewnWoodenCup
├─ CoffeeMug
├─ PorcelinTeaBowl. 

Note that the terms shown below all have the same meaning:

  • "sub-class"
  • "derrived-class"
  • "child-class"

Perhaps all Cups have a method named wash().

Also, perhaps rough hewn wooden cups should not be put in a dish-washing machine. Wood tends to warp, rot and fall apart in steamy hot water.

Thus, each type of cup shall have a different implementation of the method named wash()

// this is written in pseudo-code, not Java
define function wash(WoodenCup woodzy) {
    dump_in_kitchen_sink(woodzy);  
    return woodzy;
    // return to what you were doing before
    // somone told you to wash the wooden cup named `woodzy`   
}

define function wash(GlassCup glassy) {
    dump_in_kitchen_sink(glassy);  

    // imagine that dishwasher is a global variable 

    dishwasher.insert_into_in_top_rack(glassy);

    return glassy;

    // return to what you were doing before
    // somone told you to wash the 
    // glass cup named `glassy`   
}

Extentions of the cup the class might have methods of the same name as the parent super-class, ut the methods to do thing different.

Washing a wood cup might be a different process or procedure than washing a cup made out of glass.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.