--- tags: fall 2018 cs61b --- # Intro Inheritance Notes [TOC] ## Inheritance In Java, we can have a class inherit methods and fields from another class by **extending** it. We call the class that we inherit methods from: **superclass**. The class that inherits the methods is called: **subclass**. ```Java //the superclass class Animal { public void makeSound() { //random code } } //the subclass that extends the superclass class Dog extends Animal { public void bark() { //random code } } ``` ### Polymorphism Polymorphism is the ability of an object to take different forms. The most common one in Java is when a superclass refers to its subclass like so: ```Java Animal a = new Dog(); ``` `Animal` is considered the *static type* and `Dog` is considered the *dynamic type* of `a`. When the static type and dynamic type of an object do not match, we have a polymorphic object. ### Why use Polymorphism? **A concrete example:** We want to create a list of different objects. Suppose we have an `Animal` class and two classes, `Dog` and `Cat`, that extend the `Animal` class. We can create a list of `Dog` and `Cat` objects by instantiating a list of objects that are related to `Animal`. So, thanks to polymorphism, we can running the following lines without error: ```Java List<Animal> animalArr = new ArrayList<>(); Dog d = new Dog(); Cat c = new Cat(); animalArr.add(d); animalArr.add(c); ``` **More abstract example:** Polymorphism allows the same object to have many different forms. In Java, this means we can have the same object act differently based on the reference type passed to it. Consider the code below: ```Java class Animal { //all animals sleep public void sleep(); public void makeNoise(); } class Dog extends Animal { public void makeNoise(); public void bark(); } ``` ```Java Animal a = new Animal(); //uses methods sleep and makeNoise Dog d = new Dog(); //uses methods makeNoise, bark, sleep Animal ad = new Dog(); //uses methods makeNoise and sleep ``` From the code above, we notice that `ad` is limited to less methods during compile-time, especially if we don't cast. Intutively, it does seem less useful to consider polymorphism. However, this just isn't true. When we write efficient code, the purpose isn't to make things as multi-purpose as possible. If it was, the developers of Java would not have come up with inheritance, which allows us to cut down on the number of methods we're required to write for each class and allows us to be more flexible with changes. Suppose 3 classes share the exact same method and we want to delete that method. If inheritance didn't exist, we'd have to delete that same method three times. However, if we used inheritance, we'd have the method in one superclass that the 3 classes extend and deleting the method from the superclass would accomplish the task. Similarily, polymorphism gives the programmer more freedom to adjust the variable to suit changing needs. Referring to the code above, suppose Programmer A designs the program so that she wants to use the `makeNoise` method from `Dog`, but avoid using its `bark` method. Then, Programmer B comes in, not understanding much about the spec. In this case, `ad` comes in handly because if Programmer B calls `ad.bark()`, he'll get a compile-time error and assuming he completely trusts A and her implementation has been green-lit, understands that it works this way for a reason. Another way `ad` could be handy is if Programmer A suddenly decides she does want to use the `bark` method for one of the functions she's writing. Then, all she has to do is cast `((Dog) ad).bark()` rather than going back and changing whatever she originally instantiated. **Note:** There are possible scenarios in which directly using `a` or `d` is more beneficial, but I wanted to give an example that made it easier to work with `ad`. ### Static Type vs Dynamic Type When instantiating an object, **static type** is the type on the left hand side of the equal sign. **Dynamic typ**e is the type on the right hand side of the equal sign. ### How does this affect the compiler? The compiler looks at the **static type** of the object and compares it to the **dynamic type** when checking for errors. Why does this matter? Consider the code below: ```Java a.bark(); ``` The code above will **error out** because the compiler will only see `a` as an `Animal` object. It cannot see the object's dynamic type! When it checks for `bark()`, it will look in the `Animal` class, not find it, and throw a compile-time error. ### How to fix this? We fix this by casting `a` as a `Dog` object. We will go later into how casting works. ```Java ((Dog) a).bark(); ``` ### Polymorphic Method Selection: Method Signature When it comes to running methods with polymorphic objects, we choose which method to run by referring to the **method signature**. The **method signature** is chosen by the **compiler** (which *mainly* looks at the static type of an object). During runtime, the method we run must have the same method signature that the compiler chose! The method signature is chosen based on the **method name, parameter types, order of the parameters** and **number of parameters**. A common misconception is that the return type will change the method signature. However, the scenario below will result in a compile time error because the method signatures of the two methods are exactly the same! ```Java class Dog { void bark(Dog dog) { System.out.println("bark"); } String bark(Dog dog) { return "bark"; } } ``` **Common Example of Tricky Method Selection:** Suppose we have the following classes set up: ```Java class Trick { public void trickyMethod(Trick t) { System.out.println("Tricky...."); } } class Trap extends Trick { public void trickyMethod(Trap a) { System.out.println("I'm a trick!"); } public void trickyMethod(Trick b) { System.out.println("Avoided the trick!"); } } ``` Then, we run the following code: ```Java Trick t = new Trap(); //static type = Trick; dynamic type = Trap t.trickyMethod(t); //prints Avoided the trick! ``` **What's happening?** The static type of `t` is `Trick` and the dynamic type is `Trap`. During compile time, the compiler will search for a method signature in the class of the static type. Therefore, the compiler will look at the methods in the `Trick` class and choose the method signature to be a method with the name `trickyMethod` and one parameter of type `Trick`. During runtime, Java will look in the class of the dynamic type, search for a method that has the *same exact method signature*, and run that method. In this situation, Java looks in the `Trap` class, searches for a method that has the signature, and chooses to run `trickyMethod(Trick b)`. ### Overriding A class and its subclass contain the same method signature. If the dynamic type of an object is `Poodle` and the `bark(Dog dog)` method is called, then the method in `Poodle` will override the one in `Dog`. ```Java class Dog { void bark(Dog dog) { System.out.println("bark"); } } class Poodle extends Dog { @Override void bark(Dog dog) { System.out.println("woof"); } } ``` **Note:** The **@Override** symbol is nice to have, but not necessary to pass the compiler. What happens if we have an object with static type `Dog` and dynamic type `Poodle` calling `bark`, but `Poodle` does not have the method `bark`? The `bark` method in the `Dog` class will be called. ### Overloading Two methods with the same name in the same class have different parameter types. Which method is chosen to run is based on the method signature! ```Java class Dog { void bark(Dog dog) { System.out.println("bark"); } void bark() { System.out.println("woof"); } } ``` ### Casting Clear-Ups The purpose of casting is to trick the compiler into thinking an object's static type is different. Sometimes it succeeds! Consider the code below in which `Dog` is the superclass of `Beagle`. ```Java //upcasting: casting to a superclass //redundant: this is polymorphism! Dog d = (Dog) new Beagle(); //downcasting: casting to a subclass Dog d = new Beagle(); Beagle b = (Beagle) d; ``` Sometimes, it does not. Casting can be responsible for compile-time and runtime errors! **Compile-Time Error:** occurs when an object is casted to a class that is not in the same class hierarchy (the classes are not at all related) ```Java Dog d = (Dog) new Cat(); ``` In this context, `Cat` is not in the same class hierarchy as `Dog`. The compiler notices this and complains immediately! **Runtime Error:** occurs when an object passes the compiler, but is incorrectly casted to its subclass/superclass; specifically called a *ClassCastException* ```Java Dog d = new Dog(); Beagle b = (Beagle) d; ``` In this context, `Beagle` is a subclass of `Dog`. `d` is *downcasted* to `Beagle` and the compiler is okay with it! During runtime, Java discovers the compiler has been lied to since `d` is actually a `Dog`, not a `Beagle`, and throws a *ClassCastException*. How can we fix this to have no error? Change the dynamic type of `d` to `Beagle`! ```Java Dog d = new Beagle(); Beagle b = (Beagle) d; ``` The compiler will pass as it did for the last example. However, during runtime for these lines of code, Java will understand that the dynamic type of `d` is `Beagle` and that we weren't lying when we casted `d` as a `Beagle`. **A Common Misconception:** ```Java Dog d = new Beagle(); //casts the result of calling the method bark (Dog) d.bark(); //casts the class calling the method ((Dog) d).bark(); ``` ## Polymorphism with Fields For fields, Java does not take into account the dynamic type during runtime. It only looks at the static type when choosing to return the value of the field referenced. **Example:** ```Java class Animal { int happiness = 100; } class Dog extends Animal{ int happiness = 10000; } ``` If we run the following code: ```Java Animal a = new Dog(); System.out.println(a.happiness); //prints out 100, not 10000 ```